【滤波】扩展卡尔曼滤波器

本文主要翻译自rlabbe/Kalman-and-Bayesian-Filters-in-Python的第11章节11-Extended-Kalman-Filters(扩展卡尔曼滤波)。

%matplotlib inline
#format the book
import book_format
book_format.set_style()

我们发展了线性卡尔曼滤波器的理论。然后在上两章中,我们提出了使用卡尔曼滤波器解决非线性问题的主题。在本章中,我们将学习扩展卡尔曼滤波器(EKF)。EKF通过在当前估计点对系统进行线性化来处理非线性,然后使用线性卡尔曼滤波器对该线性化系统进行滤波。这是最早用于非线性问题的技术之一,也是最常用的技术。

EKF为滤波器的设计者设置了较大的数学挑战,因此我尽我所能避免EKF,而采用其他技术来解决非线性滤波问题。然而,这是不可避免的,有很多经典论文和大多数当前论文都使用EKF。即使你没有使用过EKF,你也需要熟悉它才能阅读文献。


卡尔曼滤波器的线性化

卡尔曼滤波器需要使用线性方程组,因此它不适用于非线性问题。非线性可以体现在两个方面。首先,过程模型可能是非线性的。例如:物体在大气中下落时会遇到阻力,从而降低其加速度。阻力系数随对象的速度而变化。由此产生的行为是非线性的——它不能用线性方程来建模。其次,观测模型可能是非线性的。例如:雷达提供目标的距离和方向。我们需要使用非线性的三角函数来计算目标的坐标。

对于线性滤波器,我们有以下过程和观测模型方程:

x ˙ = A x + w x \dot{\mathbf{x}}=\mathbf{Ax}+w_{x} x˙=Ax+wx

z = H x + w z \mathbf{z}=\mathbf{Hx}+w_{z} z=Hx+wz

其中, A \mathbf{A} A是系统动态矩阵。使用卡尔曼滤波数学一章中介绍的状态空间方法,可以将这些方程转换为:

x ˙ = F x \dot{\mathbf{x}}=\mathbf{Fx} x˙=Fx

z = H x \mathbf{z}=\mathbf{Hx} z=Hx

其中, F \mathbf{F} F是状态转移矩阵。噪音 w x w_{x} wx w z w_{z} wz项被合并到矩阵 R \mathbf{R} R Q \mathbf{Q} Q中。这种形式的方程允许我们在给定步骤 k k k的观测值和步骤 k − 1 k-1 k1的状态估计值,计算步骤 k k k的状态。在前面的章节中,我们通过使用牛顿方程描述的问题来使数学模型最小化。我们知道如何根据高中物理设计 F \mathbf{F} F

对于非线性模型,线性表达式 F x + B u \mathbf{Fx}+\mathbf{Bu} Fx+Bu替换为非线性函数 f ( x , u ) f(\mathbf{x}, \mathbf{u}) f(x,u),线性表达式 H x \mathbf{Hx} Hx替换为非线性函数 h ( x ) h(\mathbf{x}) h(x)

x ˙ = f ( x , u ) + w x \dot{\mathbf{x}}=f(\mathbf{x}, \mathbf{u})+w_{x} x˙=f(x,u)+wx

z = h ( x ) + w z \mathbf{z}=h(\mathbf{x})+w_{z} z=h(x)+wz

如果你还记得非线性滤波章节中的图表,通过一个非线性函数传递一个高斯分布,会得到一个不再是高斯分布的概率分布。所以这是行不通的。

EKF不会改变卡尔曼滤波器的线性方程组。相反,它将当前估计点处的非线性方程线性化,并在线性卡尔曼滤波器中使用此线性化方程组

线性化正如字面的意思,将非线性问题变成线性问题。我们在定义的点上找到一条与曲线最接近的直线,下图将抛物线 f ( x ) = x 2 − 2 x f(x)=x^{2}-2x f(x)=x22x x = 1.5 x=1.5 x=1.5处线性化。

import kf_book.ekf_internal as ekf_internal

ekf_internal.show_linearization()

【滤波】扩展卡尔曼滤波器_第1张图片

如果上面的曲线是过程模型,则虚线表示该曲线在 x = 1.5 x=1.5 x=1.5的线性化。

我们通过求导数使系统线性化,该导数求出曲线的斜率:

f ( x ) = x 2 − 2 x f(x)=x^{2}-2x f(x)=x22x

d f d x = 2 x − 2 \frac{df}{dx}=2x-2 dxdf=2x2

然后在 x x x处对其进行估计:

m = f ′ ( x = 1.5 ) = 1 m={f}'(x=1.5)=1 m=f(x=1.5)=1

一组微分方程的线性化也是类似的。我们通过对 x t \mathbf{x}_{t} xt点处求偏导来估计 F \mathbf{F} F H \mathbf{H} H,从而将 f ( x , u ) f(\mathbf{x}, \mathbf{u}) f(x,u) h ( x ) h(\mathbf{x}) h(x)线性化。我们称矩阵的偏导数为雅可比矩阵。这为我们提供了状态转移矩阵和观测模型矩阵:

F = ∂ f ( x t , u t ) ∂ x ∣ x t , u t \mathbf{F}=\frac{\partial f(\mathbf{x}_{t}, \mathbf{u}_{t})}{\partial \mathbf{x}}\mid _{\mathbf{x}_{t},\mathbf{u}_{t}} F=xf(xt,ut)xt,ut

H = ∂ h ( x ˉ t ) ∂ x ˉ ∣ x ˉ t \mathbf{H}=\frac{\partial h(\bar{\mathbf{x}}_{t})}{\partial \bar{\mathbf{x}}}\mid _{\bar{\mathbf{x}}_{t}} H=xˉh(xˉt)xˉt

这导致EKF流程是以下的方程,和线性卡尔曼滤波器之间存在一些不同:

KF EKF F = ∂ f ( x t , u t ) ∂ x ∣ x t , u t x ˉ = F x x ˉ = f ( x , u ) P ˉ = F P F T + Q P ˉ = F P F T + Q H = ∂ h ( x ˉ t ) ∂ x ˉ ∣ x ˉ t y = z − H x y = z − h ( x ˉ ) S = H P ˉ H T + R S = H P ˉ H T + R K = P ˉ H T S − 1 K = P ˉ H T S − 1 x = x ˉ + K y x = x ˉ + K y P = ( I − K H ) P ˉ P = ( I − K H ) P ˉ \begin{array}{l|l} \text{KF} & \text{EKF} \\ \hline & \mathbf{F}=\frac{\partial f(\mathbf{x}_{t}, \mathbf{u}_{t})}{\partial \mathbf{x}}\mid _{\mathbf{x}_{t},\mathbf{u}_{t}} \\ \bar{\mathbf{x}} = \mathbf{F}\mathbf{x} & \bar{\mathbf{x}}=f(\mathbf{x}, \mathbf{u}) \\ \bar{\mathbf{P}}=\mathbf{F}\mathbf{P}\mathbf{F}^{T}+\mathbf{Q} & \bar{\mathbf{P}}=\mathbf{F}\mathbf{P}\mathbf{F}^{T}+\mathbf{Q} \\ \hline & \mathbf{H}=\frac{\partial h(\bar{\mathbf{x}}_{t})}{\partial \bar{\mathbf{x}}}\mid _{\bar{\mathbf{x}}_{t}} \\ \mathbf{y}=\mathbf{z}-\mathbf{H}\mathbf{x} & \mathbf{y}=\mathbf{z}-h(\bar{\mathbf{x}}) \\ \mathbf{S}=\mathbf{H}\bar{\mathbf{P}}\mathbf{H}^{T} + \mathbf{R} & \mathbf{S}=\mathbf{H}\bar{\mathbf{P}}\mathbf{H}^{T} + \mathbf{R} \\ \mathbf{K}=\bar{\mathbf{P}}\mathbf{H}^{T}\mathbf{S}^{-1} & \mathbf{K}=\bar{\mathbf{P}}\mathbf{H}^{T}\mathbf{S}^{-1} \\ \mathbf{x}=\bar{\mathbf{x}}+\mathbf{K}\mathbf{y} & \mathbf{x}=\bar{\mathbf{x}}+\mathbf{K}\mathbf{y} \\ \mathbf{P}=(\mathbf{I}-\mathbf{K}\mathbf{H})\bar{\mathbf{P}} & \mathbf{P}=(\mathbf{I}-\mathbf{K}\mathbf{H})\bar{\mathbf{P}} \end{array} KFxˉ=FxPˉ=FPFT+Qy=zHxS=HPˉHT+RK=PˉHTS1x=xˉ+KyP=(IKH)PˉEKFF=xf(xt,ut)xt,utxˉ=f(x,u)Pˉ=FPFT+QH=xˉh(xˉt)xˉty=zh(xˉ)S=HPˉHT+RK=PˉHTS1x=xˉ+KyP=(IKH)Pˉ

对于EKF,我们通常不使用 F x \mathbf{Fx} Fx进行状态转移,因为线性化会导致不准确。使用合适的数值积分技术来计算 x ˉ \bar{\mathbf{x}} xˉ是很典型的做法,如Euler或Runge Kutta。因此,我将状态转移表示为 x ˉ = f ( x , u ) \bar{\mathbf{x}}=f(\mathbf{x}, \mathbf{u}) xˉ=f(x,u)。出于同样的原因,我们不使用 H x \mathbf{Hx} Hx计算残差,选择更精确的 h ( x ˉ ) h(\bar{\mathbf{x}}) h(xˉ)

我认为理解EKF最简单的方法是从一个例子开始。


示例:跟踪飞机

此示例使用地基雷达跟踪飞机。在上一章中,我们针对这个问题使用UKF来实现。现在,我们将针对同一问题使用EKF来实现,以便比较滤波器性能和滤波器实现所需的工作量。

雷达的工作原理是发射一束无线电波并扫描回波。光束路径中的任何东西都会将部分信号反射回雷达。通过计算反射信号返回雷达所需的时间,系统可以计算出从雷达装置到目标的直线距离。

雷达的直线距离 γ \gamma γ和仰角 ε \varepsilon ε,与飞机水平位置 x x x和高度 y y y的关系如下图所示:

ekf_internal.show_radar_chart()

【滤波】扩展卡尔曼滤波器_第2张图片

这给了我们等式:

ε = t a n − 1 y x \varepsilon = tan^{-1}\frac{y}{x} ε=tan1xy

γ 2 = x 2 + y 2 \gamma ^{2}=x^{2}+y^{2} γ2=x2+y2

设计状态量

假设我们在跟踪一架速度恒定、高度恒定的飞机的位置,雷达能够观测到距飞机的直线距离。这意味着我们需要3个状态量——水平距离、水平速度和高度:

x = [ d i s t a c n e v e l o c i t y a l t i t u d e ] = [ x x ˙ y ] \mathbf{x}=\begin{bmatrix} distacne \\ velocity \\ altitude \end{bmatrix}=\begin{bmatrix} x \\ \dot{x} \\ y \end{bmatrix} x= distacnevelocityaltitude = xx˙y

设计过程模型

我们假设飞机的运动符合牛顿运动学。我们在前面的章节中已经使用了这个模型了:

F = [ 1 △ t 0 0 1 0 0 0 1 ] \mathbf{F}=\begin{bmatrix} 1 & \bigtriangleup t & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} F= 100t10001

我将矩阵分成了两块,左上的 2 × 2 2×2 2×2矩阵表示 x x x的等速模型,右下的 1 × 1 1×1 1×1矩阵表示 y y y的等速模型。

但是让我们练习寻找这些矩阵,我们用一组微分方程对系统建模。我们需要的一个形式为:

x ˉ = A x + w \bar{\mathbf{x}}=\mathbf{Ax}+\mathbf{w} xˉ=Ax+w

其中, w \mathbf{w} w为系统噪声。

变量 x x x y y y是独立的,所以我们可以分别计算它们。一维运动的微分方程为:

v = x ˙ v=\dot{x} v=x˙

a = x ¨ = 0 a=\ddot{x}=0 a=x¨=0

现在我们把微分方程转化为状态空间形式。如果这是一个二阶或更高阶的微分系统,我们必须首先将它们简化为一个等价的一阶方程组。这些方程是一阶的,所以我们把它们放在状态空间矩阵形式中:

[ x ˙ x ¨ ] = [ 0 1 0 0 ] [ x x ˙ ] \begin{bmatrix} \dot{x} \\ \ddot{x} \end{bmatrix}=\begin{bmatrix} 0 & 1 \\ 0 & 0 \end{bmatrix} \begin{bmatrix} x \\ \dot{x} \end{bmatrix} [x˙x¨]=[0010][xx˙]

x ˉ = A x \bar{x}=\mathbf{Ax} xˉ=Ax

其中, A = [ 0 1 0 0 ] \mathbf{A}=\begin{bmatrix} 0 & 1 \\ 0 & 0 \end{bmatrix} A=[0010]

回想一下, A \mathbf{A} A是系统动态矩阵。它描述了一组线性微分方程,我们必须从中计算状态转移矩阵 F \mathbf{F} F F \mathbf{F} F描述了一组离散线性方程组,该方程组计算离散时间步长 △ t \bigtriangleup t t x \mathbf{x} x

计算 F \mathbf{F} F的常用方法是使用矩阵指数的幂级数展开式:

F ( △ t ) = e A △ t = I + A △ t + ( A △ t ) 2 2 ! + ( A △ t ) 3 3 ! + . . . \mathbf{F}(\bigtriangleup t)=e^{\mathbf{A}\bigtriangleup t}=\mathbf{I}+\mathbf{A}\bigtriangleup t+\frac{(\mathbf{A}\bigtriangleup t)^{2}}{2!}+\frac{(\mathbf{A}\bigtriangleup t)^{3}}{3!}+... F(t)=eAt=I+At+2!(At)2+3!(At)3+...

其中, A 2 = [ 0 0 0 0 ] \mathbf{A}^{2}=\begin{bmatrix}0 & 0 \\ 0 & 0 \end{bmatrix} A2=[0000],因此 A \mathbf{A} A的更高阶都是 0 \mathbf{0} 0。因此,幂级数展开为:

F = I + A t + 0 = [ 1 △ t 0 1 ] \mathbf{F}=\mathbf{I}+\mathbf{A}t+\mathbf{0}=\begin{bmatrix}1 & \bigtriangleup t \\ 0 & 1\end{bmatrix} F=I+At+0=[10t1]

这与运动学方程使用的结果相同!

设计观测模型

观测函数采用状态量的先验 x ˉ \bar{\mathbf{x}} xˉ,并将其转换为表示直线距离的观测值。我们使用毕达哥拉斯定理推导:

h ( x ˉ ) = x 2 + y 2 h(\bar{\mathbf{x}})=\sqrt{x^{2}+y^{2}} h(xˉ)=x2+y2

由于平方根的存在,直线距离和地面位置之间的关系是非线性的。我们通过计算它在 x t \mathbf{x}_{t} xt处的偏导数来线性化它:

H = ∂ h ( x ˉ ) ∂ x ˉ ∣ x ˉ t \mathbf{H}=\frac{\partial h(\bar{\mathbf{x}})}{\partial \bar{\mathbf{x}}}\mid _{\bar{\mathbf{x}}_{t}} H=xˉh(xˉ)xˉt

矩阵的偏导数称为雅可比矩阵,其形式为

∂ H ∂ x ˉ = [ ∂ h 1 ∂ x 1 ∂ h 1 ∂ x 2 . . . ∂ h 2 ∂ x 1 ∂ h 2 ∂ x 2 . . . ⋮ ⋮ ] \frac{\partial \mathbf{H}}{\partial \bar{\mathbf{x}}}=\begin{bmatrix} \frac{\partial h_{1}}{\partial x_{1}} & \frac{\partial h_{1}}{\partial x_{2}} & ... \\ \frac{\partial h_{2}}{\partial x_{1}} & \frac{\partial h_{2}}{\partial x_{2}} & ... \\ \vdots & \vdots &\end{bmatrix} xˉH= x1h1x1h2x2h1x2h2......

换句话说,矩阵中的每个元素都是函数 h h h x x x变量的偏导数。对于我们的问题,我们有:

H = [ ∂ h / ∂ x ∂ h / ∂ x ˙ ∂ h / ∂ y ] \mathbf{H}=\begin{bmatrix} \partial h/\partial x & \partial h/\partial \dot{x} & \partial h/\partial y \end{bmatrix} H=[h/xh/x˙h/y]

依次解决每个问题:

∂ h ∂ x = ∂ ∂ x x 2 + y 2 = x x 2 + y 2 \frac{\partial h}{\partial x}=\frac{\partial}{\partial x}\sqrt{x^{2} + y^{2}}=\frac{x}{\sqrt{x^{2} + y^{2}}} xh=xx2+y2 =x2+y2 x

∂ h ∂ x ˙ = ∂ ∂ x ˙ x 2 + y 2 = 0 \frac{\partial h}{\partial \dot{x}}=\frac{\partial}{\partial \dot{x}}\sqrt{x^{2} + y^{2}}=0 x˙h=x˙x2+y2 =0

∂ h ∂ x = ∂ ∂ y x 2 + y 2 = y x 2 + y 2 \frac{\partial h}{\partial x}=\frac{\partial}{\partial y}\sqrt{x^{2} + y^{2}}=\frac{y}{\sqrt{x^{2} + y^{2}}} xh=yx2+y2 =x2+y2 y

因此:

H = [ x x 2 + y 2 0 y x 2 + y 2 ] \mathbf{H}=\begin{bmatrix} \frac{x}{\sqrt{x^{2} + y^{2}}} & 0 & \frac{y}{\sqrt{x^{2} + y^{2}}} \end{bmatrix} H=[x2+y2 x0x2+y2 y]

这可能看起来令人望而生畏,所以你要认识到所有这些数学都在做一些非常简单的事情。我们有一个关于飞机直线距离的方程,它是非线性的。卡尔曼滤波器只适用于线性方程组,因此我们需要找到一个近似于 H \mathbf{H} H的线性方程。正如我们前面所讨论的,在给定点找到非线性方程的斜率是一个很好的近似值。对于卡尔曼滤波器,给定点就是状态量 x \mathbf{x} x,因此我们需要取直线距离相对于 x \mathbf{x} x的导数。对于线性卡尔曼滤波器, H \mathbf{H} H是我们在运行滤波器之前计算的恒量。对于EKF, H \mathbf{H} H在每一步都要进行更新,因为状态量 x \mathbf{x} x点在每个循环后都会发生变化

为了使这更具体,现在让我们编写一个Python函数来计算这个问题中 h h h的雅可比矩阵。

from math import sqrt

def HJacobian_at(x):
    """ compute Jacobian of H matrix at x """
    horiz_dist = x[0]
    altitude   = x[2]
    denom = sqrt(horiz_dist**2 + altitude**2)
    return array ([[horiz_dist/denom, 0., altitude/denom]])

最后,让我们提供 h ( x ˉ ) h(\bar{\mathbf{x}}) h(xˉ)代码:

def hx(x):
    """ compute measurement for slant range that
    would correspond to state x.
    """
    return (x[0]**2 + x[2]**2) ** 0.5

现在让我们为我们的雷达写一个模拟。

from numpy.random import randn
import math

class RadarSim:
    """ Simulates the radar signal returns from an object
    flying at a constant altityude and velocity in 1D. 
    """
    def __init__(self, dt, pos, vel, alt):
        self.pos = pos
        self.vel = vel
        self.alt = alt
        self.dt = dt

    def get_range(self):
        """ Returns slant range to the object. Call once 
        for each new measurement at dt time from last call.
        """
        # add some process noise to the system
        self.vel = self.vel  + .1*randn()
        self.alt = self.alt + .1*randn()
        self.pos = self.pos + self.vel*self.dt
    
        # add measurement noise
        err = self.pos * 0.05*randn()
        slant_dist = math.sqrt(self.pos**2 + self.alt**2)
        
        return slant_dist + err

设计过程噪声与观测噪声

雷达可以观测到与目标之间的直线距离,假如定义 σ r a n g e = 5 m \sigma_{range}=5m σrange=5m表示观测噪声:

R = [ σ r a n g e 2 ] = [ 25 ] \mathbf{R}=\begin{bmatrix}\sigma_{range}^{2}\end{bmatrix}=\begin{bmatrix}25\end{bmatrix} R=[σrange2]=[25]

Q \mathbf{Q} Q的设计需要一些讨论。状态量为 x = [ x x ˙ y ] T \mathbf{x}=\begin{bmatrix}x & \dot{x} & y\end{bmatrix}^{T} x=[xx˙y]T。前两个元素是地面投影距离和它的变化速度,因此我们可以使用Q_discrete_white_noise()噪声,来计算 Q \mathbf{Q} Q左上角的值。第三个元素是高度,我们假设它与地面投影距离无关。这就导致了 Q \mathbf{Q} Q的块设计:

Q = [ Q x 0 0 Q y ] \mathbf{Q}=\begin{bmatrix}\mathbf{Q}_{\mathbf{x}} & 0 \\ 0 & \mathbf{Q}_{\mathbf{y}}\end{bmatrix} Q=[Qx00Qy]

实现

FilterPy提供ExtendedKalmanFilter类。它的工作原理与我们使用的KalmanFilter类类似,只是它允许你提供一个计算 H \mathbf{H} H h ( x ) h(\mathbf{x}) h(x)函数的雅可比矩阵。

我们首先导入滤波器并创建它。 x \mathbf{x} x的尺寸为3, z \mathbf{z} z的尺寸为1。

from filterpy.kalman import ExtendedKalmanFilter

rk = ExtendedKalmanFilter(dim_x=3, dim_z=1)

我们创建了雷达模拟器:

radar = RadarSim(dt, pos=0., vel=100., alt=1000.)

我们将在飞机实际位置附近初始化滤波器:

rk.x = array([radar.pos, radar.vel-10, radar.alt+100])

我们使用上面计算的泰勒级数展开式的第一项分配系统矩阵:

dt = 0.05
rk.F = eye(3) + array([[0, 1, 0],
                       [0, 0, 0],
                       [0, 0, 0]])*dt

在为 R \mathbf{R} R Q \mathbf{Q} Q P \mathbf{P} P分配了合理的值之后,我们可以用简单地运行滤波器。我们将计算 H \mathbf{H} H h ( x ) h(\mathbf{x}) h(x)的雅可比矩阵的函数传递到update方法中。

for i in range(int(20/dt)):
    z = radar.get_range()
    rk.update(array([z]), HJacobian_at, hx)
    rk.predict()

添加一些代码以保存和绘制我们得到的结果:

from filterpy.common import Q_discrete_white_noise
from filterpy.kalman import ExtendedKalmanFilter
from numpy import eye, array, asarray
import numpy as np

dt = 0.05
rk = ExtendedKalmanFilter(dim_x=3, dim_z=1)
radar = RadarSim(dt, pos=0., vel=100., alt=1000.)

# make an imperfect starting guess
rk.x = array([radar.pos-100, radar.vel+100, radar.alt+1000])

rk.F = eye(3) + array([[0, 1, 0],
                       [0, 0, 0],
                       [0, 0, 0]]) * dt

range_std = 5. # meters
rk.R = np.diag([range_std**2])
rk.Q[0:2, 0:2] = Q_discrete_white_noise(2, dt=dt, var=0.1)
rk.Q[2,2] = 0.1
rk.P *= 50

xs, track = [], []
for i in range(int(20/dt)):
    z = radar.get_range()
    track.append((radar.pos, radar.vel, radar.alt))
    
    rk.update(array([z]), HJacobian_at, hx)
    xs.append(rk.x)
    rk.predict()

xs = asarray(xs)
track = asarray(track)
time = np.arange(0, len(xs)*dt, dt)
ekf_internal.plot_radar(xs, track, time)

【滤波】扩展卡尔曼滤波器_第3张图片
【滤波】扩展卡尔曼滤波器_第4张图片
【滤波】扩展卡尔曼滤波器_第5张图片

用SymPy计算雅可比矩阵

根据你对导数的经验,你可能会发现雅可比矩阵的计算很困难。可能你会很容易地发现它,但也可能稍微难一点的问题就会导致非常困难的计算。

你可以使用SymPy软件包为我们计算雅可比矩阵:

import sympy
from IPython.display import display
sympy.init_printing(use_latex='mathjax')

x, x_vel, y = sympy.symbols('x, x_vel y')
H = sympy.Matrix([sympy.sqrt(x**2 + y**2)])

state = sympy.Matrix([x, x_vel, y])
J = H.jacobian(state)

display(state)
display(J)

[ x x v e l y ] \begin{bmatrix} x \\ x_{vel} \\ y \end{bmatrix} xxvely

[ x x 2 + y 2 0 y x 2 + y 2 ] \begin{bmatrix} \frac{x}{\sqrt{x^{2} + y^{2}}} & 0 & \frac{y}{\sqrt{x^{2} + y^{2}}} \end{bmatrix} [x2+y2 x0x2+y2 y]

这个结果与我们上面计算的结果相同,而且我们不必花太大的力气!


机器人定位

是时候尝试解决一个真正的问题了,我提醒一下你,这一部分很难。然而,大多数书籍都选择了简单的教科书式的问题和解决办法,而你想要解决的却是现实世界中的问题。

我们将考虑机器人定位的问题。我们已经在无迹卡尔曼滤波一章中实现了这一点,如果你还没有阅读到,我建议你现在就去阅读它。在这个场景中,我们有一个机器人在一个环境中移动,它使用传感器来检测地标。这可能是一辆自动驾驶汽车,使用计算机视觉技术来识别树木、建筑物和其他地标;可能是一个小型机器人,可以为你的房子吸尘;也可能是仓库里的机器人。

该机器人有4个轮子,形状与汽车相近。它通过转动前轮来操纵,这会使机器人在向前移动并转向时,绕后轴转动。这是我们必须建模的非线性行为。

该机器人有一个传感器,它可以提供与地面已知目标的大致距离和方向。这是非线性的,因为从距离和方向计算位置需要平方根和三角函数。

过程模型和观测模型都是非线性的。EKF兼顾了两者,因此我们暂时得出结论,EKF是解决这一问题的可行选择。

机器人运动模型

首先假设,汽车在向前行驶过程中通过转动前轮胎来转向。汽车的前部沿着车轮指向的方向移动,同时绕后轮胎旋转。由于摩擦导致的滑移,轮胎在不同速度下的不同行为,以及外侧轮胎与内侧轮胎的行驶半径不同等问题,这一简单描述将会变得复杂。对转向进行精确建模需要一组复杂的微分方程。

对于低速机器人应用,一个简单的自行车模型就可以表现良好。这是对模型的描述:

ekf_internal.plot_bicycle()

【滤波】扩展卡尔曼滤波器_第6张图片

在无迹卡尔曼滤波一章中,我们推导了以下方程式:

β = d w t a n ( α ) \beta=\frac{d}{w}tan(\alpha) β=wdtan(α)

x = x − R s i n ( θ ) + R s i n ( θ + β ) x=x-Rsin(\theta)+Rsin(\theta+\beta) x=xRsin(θ)+Rsin(θ+β)

y = y + R c o s ( θ ) − R c o s ( θ + β ) y=y+Rcos(\theta)-Rcos(\theta+\beta) y=y+Rcos(θ)Rcos(θ+β)

θ = θ + β \theta=\theta+\beta θ=θ+β

其中, θ \theta θ是机器人的航向角。

如果你对转向模型没有兴趣,你就没有必要了解这个数学模型的细节。你需要认识到的一点是,我们的运动模型是非线性的,需要用卡尔曼滤波器来处理这一点。

设计状态量

对于我们的滤波器,我们将追踪机器人的位置 x x x y y y和方向 θ \theta θ

x = [ x y θ ] \mathbf{x}=\begin{bmatrix}x \\ y \\ \theta \end{bmatrix} x= xyθ

我们的控制输入 u \mathbf{u} u是速度 v v v和转向角 α \alpha α

u = [ v α ] \mathbf{u}=\begin{bmatrix}v \\ \alpha \end{bmatrix} u=[vα]

设计系统模型

我们将我们的系统建模为一个非线性运动模型加上噪声:

x ˉ = f ( x , u ) + N ( 0 , Q ) \bar{x}=f(x, u) + \mathcal{N}(0, Q) xˉ=f(x,u)+N(0,Q)

使用上面研究的机器人运动模型,我们可以将其扩展到:

[ x ˉ y θ ] = [ x y θ ] + [ − R s i n ( θ ) + R s i n ( θ + β ) R c o s ( θ ) − R c o s ( θ + β ) β ] \begin{bmatrix} \bar{x} \\ y \\ \theta \end{bmatrix}=\begin{bmatrix} x \\ y \\ \theta \end{bmatrix}+\begin{bmatrix}-Rsin(\theta)+Rsin(\theta+\beta)\\ Rcos(\theta)-Rcos(\theta+\beta) \\ \beta \end{bmatrix} xˉyθ = xyθ + Rsin(θ)+Rsin(θ+β)Rcos(θ)Rcos(θ+β)β

我们通过取 f ( x , u ) f(x,u) f(x,u)的雅可比矩阵来求 F \mathbf{F} F

F = ∂ f ( x , u ) ∂ x = [ ∂ f 1 ∂ x ∂ f 1 ∂ y ∂ f 1 ∂ θ ∂ f 2 ∂ x ∂ f 2 ∂ y ∂ f 2 ∂ θ ∂ f 3 ∂ x ∂ f 3 ∂ y ∂ f 3 ∂ θ ] \mathbf{F}=\frac{\partial f(x,u)}{\partial x}=\begin{bmatrix} \frac{\partial f_{1}}{\partial x} & \frac{\partial f_{1}}{\partial y} & \frac{\partial f_{1}}{\partial \theta} \\ \frac{\partial f_{2}}{\partial x} & \frac{\partial f_{2}}{\partial y} & \frac{\partial f_{2}}{\partial \theta} \\ \frac{\partial f_{3}}{\partial x} & \frac{\partial f_{3}}{\partial y} & \frac{\partial f_{3}}{\partial \theta} \end{bmatrix} F=xf(x,u)= xf1xf2xf3yf1yf2yf3θf1θf2θf3

通过计算,我们得到:

F = [ 1 0 − R c o s ( θ ) + R c o s ( θ + β ) 0 1 − R s i n ( θ ) + R s i n ( θ + β ) 0 0 1 ] \mathbf{F}=\begin{bmatrix} 1 & 0 & -Rcos(\theta)+Rcos(\theta+\beta)\\ 0 & 1 & -Rsin(\theta)+Rsin(\theta+\beta) \\ 0 & 0& 1 \end{bmatrix} F= 100010Rcos(θ)+Rcos(θ+β)Rsin(θ)+Rsin(θ+β)1

我们可以根据SymPy再核对一下:

import sympy
from sympy.abc import alpha, x, y, v, w, R, theta
from sympy import symbols, Matrix

sympy.init_printing(use_latex="mathjax", fontsize='16pt')
time = symbols('t')
d = v*time
beta = (d/w)*sympy.tan(alpha)
r = w/sympy.tan(alpha)

fxu = Matrix([[x-r*sympy.sin(theta) + r*sympy.sin(theta+beta)],
              [y+r*sympy.cos(theta)- r*sympy.cos(theta+beta)],
              [theta+beta]])
F = fxu.jacobian(Matrix([x, y, theta]))
F

[ 1 0 − w c o s ( θ ) t a n ( α ) + w c o s ( t v t a n ( α ) w + θ ) t a n ( α ) 0 1 − w s i n ( θ ) t a n ( α ) + w s i n ( t v t a n ( α ) w + θ ) t a n ( α ) 0 0 1 ] \begin{bmatrix} 1 & 0 & -\frac{wcos(\theta)}{tan(\alpha)}+\frac{wcos(\frac{tvtan(\alpha)}{w}+\theta)}{tan(\alpha)} \\ 0 & 1 & -\frac{wsin(\theta)}{tan(\alpha)}+\frac{wsin(\frac{tvtan(\alpha)}{w}+\theta)}{tan(\alpha)} \\ 0 & 0& 1 \end{bmatrix} 100010tan(α)wcos(θ)+tan(α)wcos(wtvtan(α)+θ)tan(α)wsin(θ)+tan(α)wsin(wtvtan(α)+θ)1

这看起来有点复杂。我们可以用SymPy来得到更简洁的形式:

# reduce common expressions
B, R = symbols('beta, R')
F = F.subs((d/w)*sympy.tan(alpha), B)
F.subs(w/sympy.tan(alpha), R)

[ 1 0 − R c o s ( θ ) + R c o s ( θ + β ) 0 1 − R s i n ( θ ) + R s i n ( θ + β ) 0 0 1 ] \begin{bmatrix} 1 & 0 & -Rcos(\theta)+Rcos(\theta+\beta)\\ 0 & 1 & -Rsin(\theta)+Rsin(\theta+\beta) \\ 0 & 0& 1 \end{bmatrix} 100010Rcos(θ)+Rcos(θ+β)Rsin(θ)+Rsin(θ+β)1

该形式验证了之前雅可比矩阵的计算是正确的。

现在我们可以把注意力转移到噪音上。这里,噪声在我们的控制输入中,所以它在控制空间中。换句话说,我们需要一个特定的速度和转向角,但我们需要把它转换成 x x x y y y θ \theta θ的误差。在实际系统中,这可能因速度而异,因此每次预测都需要重新计算。我会选择这个作为噪声模型。对于一个真正的机器人,你需要选择一个能准确描述系统错误的模型。

M = [ σ v e l 2 0 0 σ α 2 ] \mathbf{M}=\begin{bmatrix}\sigma _{vel}^{2} & 0 \\ 0 & \sigma _{\alpha}^{2}\end{bmatrix} M=[σvel200σα2]

如果这是一个线性问题,我们可以使用现在熟悉的 F M F T \mathbf{FMF}^{T} FMFT形式,从控制空间转换到状态空间。但由于我们的运动模型是非线性的,需要使用雅可比矩阵将其线性化,我们将其命名为 V \mathbf{V} V

V = ∂ f ( x , u ) ∂ u = [ ∂ f 1 ∂ v ∂ f 1 ∂ α ∂ f 2 ∂ v ∂ f 2 ∂ α ∂ f 3 ∂ v ∂ f 3 ∂ α ] \mathbf{V}=\frac{\partial f(x,u)}{\partial u}=\begin{bmatrix}\frac{\partial f_{1}}{\partial v} & \frac{\partial f_{1}}{\partial \alpha} \\ \frac{\partial f_{2}}{\partial v} & \frac{\partial f_{2}}{\partial \alpha} \\ \frac{\partial f_{3}}{\partial v} & \frac{\partial f_{3}}{\partial \alpha}\end{bmatrix} V=uf(x,u)= vf1vf2vf3αf1αf2αf3

这些偏导数有时会变得很难处理,让我们用SymPy计算它们。

V = fxu.jacobian(Matrix([v, alpha]))
V = V.subs(sympy.tan(alpha)/w, 1/R) 
V = V.subs(time*v/R, B)
V = V.subs(time*v, 'd')
V

[ t c o s ( β + θ ) d ( t a n 2 ( α ) + 1 ) c o s ( β + θ ) ( t a n ( α ) − w ( − t a n 2 ( α ) − 1 ) s i n ( θ ) ( t a n 2 ( α ) + w ( − t a n 2 ( α ) − 1 ) s i n ( β + θ ) ( t a n 2 ( α ) t s i n ( β + θ ) d ( t a n 2 ( α ) + 1 ) c o s ( β + θ ) ( t a n ( α ) + w ( − t a n 2 ( α ) − 1 ) c o s ( θ ) ( t a n 2 ( α ) − w ( − t a n 2 ( α ) − 1 ) c o s ( β + θ ) ( t a n 2 ( α ) t R d ( t a n 2 ( α ) + 1 ) w ] \begin{bmatrix} tcos(\beta+\theta) & \frac{d(tan^2(\alpha)+1)cos(\beta+\theta)}{(tan(\alpha)} - \frac{w(-tan^2(\alpha)-1)sin(\theta)}{(tan^2(\alpha)} + \frac{w(-tan^2(\alpha)-1)sin(\beta+\theta)}{(tan^2(\alpha)} \\ tsin(\beta+\theta) & \frac{d(tan^2(\alpha)+1)cos(\beta+\theta)}{(tan(\alpha)} + \frac{w(-tan^2(\alpha)-1)cos(\theta)}{(tan^2(\alpha)} - \frac{w(-tan^2(\alpha)-1)cos(\beta+\theta)}{(tan^2(\alpha)} \\ \frac{t}{R} & \frac{d(tan^2(\alpha)+1)}{w}\end{bmatrix} tcos(β+θ)tsin(β+θ)Rt(tan(α)d(tan2(α)+1)cos(β+θ)(tan2(α)w(tan2(α)1)sin(θ)+(tan2(α)w(tan2(α)1)sin(β+θ)(tan(α)d(tan2(α)+1)cos(β+θ)+(tan2(α)w(tan2(α)1)cos(θ)(tan2(α)w(tan2(α)1)cos(β+θ)wd(tan2(α)+1)

这为我们提供了预测方程的最终形式:

x ˉ = x + [ − R s i n ( θ ) + R s i n ( θ + β ) R c o s ( θ ) − R c o s ( θ + β ) β ] \bar{\mathbf{x}}=\mathbf{x}+\begin{bmatrix}-Rsin(\theta)+Rsin(\theta+\beta)\\ Rcos(\theta)-Rcos(\theta+\beta) \\ \beta \end{bmatrix} xˉ=x+ Rsin(θ)+Rsin(θ+β)Rcos(θ)Rcos(θ+β)β

P ˉ = F P F T + V M V T \bar{\mathbf{P}}=\mathbf{FPF}^{T}+\mathbf{VMV}^{T} Pˉ=FPFT+VMVT

这种形式的线性化并不是预测 x \mathbf{x} x的唯一方法。例如,我们可以使用数值积分技术,如Runge Kutta来计算机器人的运动。如果时间步长相对较大,则需要这样做。对于卡尔曼滤波器来说,使用EKF并没有KF那样简单。对于一个实际问题,你必须仔细地用微分方程对你的系统建模,然后确定解决这个系统的最合适的方法。正确的方法取决于所需的精度、方程的非线性程度、处理器预算和数值稳定性。

设计观测模型

传感器提供了相对于已知地标的、有噪声的方向和距离观测结果。观测模型必须转换状态量 [ x y θ ] T \begin{bmatrix} x & y & \theta \end{bmatrix}^{T} [xyθ]T,到传感器观测量。如果 p p p是地标的位置,则距离r为:

r = ( p x − x ) 2 + ( p y − y ) 2 r=\sqrt{(p_{x}-x)^{2}+(p_{y}-y)^{2}} r=(pxx)2+(pyy)2

我们假设传感器提供相对于机器人方向的方向,因此我们必须从方向中减去机器人的方向,以获得传感器读数,如下所示:

ϕ = t a n − 1 ( p y − y p x − x ) − θ \phi =tan^{-1}(\frac{p_{y}-y}{p_{x}-x})-\theta ϕ=tan1(pxxpyy)θ

因此,我们的观测方程是:

z = h ( x , P ) + N ( 0 , R ) \mathbf{z}=h(\mathbf{x},\mathbf{P})+\mathcal{N}(0,R) z=h(x,P)+N(0,R)

z = [ ( p x − x ) 2 + ( p y − y ) 2 t a n − 1 ( p y − y p x − x ) − θ ] + N ( 0 , R ) \mathbf{z}=\begin{bmatrix} \sqrt{(p_{x}-x)^{2}+(p_{y}-y)^{2}} \\ tan^{-1}(\frac{p_{y}-y}{p_{x}-x})-\theta \end{bmatrix}+\mathcal{N}(0,R) z=[(pxx)2+(pyy)2 tan1(pxxpyy)θ]+N(0,R)

这显然是非线性的,所以我们需要通过取它在 x \mathbf{x} x处的雅可比矩阵,来线性化 h h h。我们用Symphy来计算。

px, py = symbols('p_x, p_y')
z = Matrix([[sympy.sqrt((px-x)**2 + (py-y)**2)],
            [sympy.atan2(py-y, px-x) - theta]])
z.jacobian(Matrix([x, y, theta]))

[ − p x + x ( p x − x ) 2 + ( p y − y ) 2 − p y + y ( p x − x ) 2 + ( p y − y ) 2 0 − − p y + y ( p x − x ) 2 + ( p y − y ) 2 − p x − x ( p x − x ) 2 + ( p y − y ) 2 − 1 ] \begin{bmatrix} \frac{-p_x+x}{\sqrt{(p_x-x)^2+(p_y-y)^2}} & \frac{-p_y+y}{\sqrt{(p_x-x)^2+(p_y-y)^2}} & 0 \\ - \frac{-p_y+y}{(p_x-x)^2+(p_y-y)^2} & - \frac{p_x-x}{(p_x-x)^2+(p_y-y)^2} & -1\end{bmatrix} [(pxx)2+(pyy)2 px+x(pxx)2+(pyy)2py+y(pxx)2+(pyy)2 py+y(pxx)2+(pyy)2pxx01]

现在我们需要将其编写为Python函数。例如,我们可以写:

from math import sqrt

def H_of(x, landmark_pos):
    """ compute Jacobian of H matrix where h(x) computes 
    the range and bearing to a landmark for state x """
    px = landmark_pos[0]
    py = landmark_pos[1]
    hyp = (px - x[0, 0])**2 + (py - x[1, 0])**2
    dist = sqrt(hyp)

    H = array(
        [[-(px - x[0, 0]) / dist, -(py - x[1, 0]) / dist, 0],
         [ (py - x[1, 0]) / hyp,  -(px - x[0, 0]) / hyp, -1]])
    return H

我们还需要定义一个将状态量转换为观测量的函数。

from math import atan2

def Hx(x, landmark_pos):
    """ takes a state variable and returns the measurement
    that would correspond to that state.
    """
    px = landmark_pos[0]
    py = landmark_pos[1]
    dist = sqrt((px - x[0, 0])**2 + (py - x[1, 0])**2)

    Hx = array([[dist],
                [atan2(py - x[1, 0], px - x[0, 0]) - x[2, 0]]])
    return Hx

设计观测噪声

可以合理地假设距离观测和方向观测的噪声是独立的,因此:

R = [ σ r a n g e 2 0 0 σ b e a r i n g 2 ] \mathbf{R}=\begin{bmatrix}\sigma_{range}^{2} & 0 \\ 0 & \sigma_{bearing}^{2}\end{bmatrix} R=[σrange200σbearing2]

实现

我们将使用FilterPy的ExtendedKalmanFilter类来实现该滤波器。它的predict()方法使用标准线性方程组的过程模型。而我们的是非线性的,所以我们必须用自己的实现覆盖predict()。我还想使用这个类来模拟机器人,所以我将添加一个move()方法来计算机器人的位置。

预测步骤的矩阵相当大。evalf给变量设置特定值来计算Symphy矩阵。我向你演示这种用法,并在卡尔曼滤波代码中使用它。你需要了解几点。

首先,evalf使用dict类型来指定值。例如,如果矩阵包含 x x x y y y,则可以编写:

M.evalf(subs={x:3, y:17})

来计算 x = 3 x=3 x=3 y = 17 y=17 y=17的矩阵。

其次,evalf返回一个sympy.Matrix对象。使用numpy.array(M).astype(float)将其转换为NumPy数组。numpy.array(M)创建一个object类型的Numpy数组,这并不是你想要的。

以下是EKF的代码:

from filterpy.kalman import ExtendedKalmanFilter as EKF
from numpy import array, sqrt

class RobotEKF(EKF):
    def __init__(self, dt, wheelbase, std_vel, std_steer):
        EKF.__init__(self, 3, 2, 2)
        self.dt = dt
        self.wheelbase = wheelbase
        self.std_vel = std_vel
        self.std_steer = std_steer

        a, x, y, v, w, theta, time = symbols(
            'a, x, y, v, w, theta, t')
        d = v*time
        beta = (d/w)*sympy.tan(a)
        r = w/sympy.tan(a)
    
        self.fxu = Matrix(
            [[x-r*sympy.sin(theta)+r*sympy.sin(theta+beta)],
             [y+r*sympy.cos(theta)-r*sympy.cos(theta+beta)],
             [theta+beta]])

        self.F_j = self.fxu.jacobian(Matrix([x, y, theta]))
        self.V_j = self.fxu.jacobian(Matrix([v, a]))

        # save dictionary and it's variables for later use
        self.subs = {x: 0, y: 0, v:0, a:0, 
                     time:dt, w:wheelbase, theta:0}
        self.x_x, self.x_y, = x, y 
        self.v, self.a, self.theta = v, a, theta

    def predict(self, u):
        self.x = self.move(self.x, u, self.dt)

        self.subs[self.theta] = self.x[2, 0]
        self.subs[self.v] = u[0]
        self.subs[self.a] = u[1]

        F = array(self.F_j.evalf(subs=self.subs)).astype(float)
        V = array(self.V_j.evalf(subs=self.subs)).astype(float)

        # covariance of motion noise in control space
        M = array([[self.std_vel*u[0]**2, 0], 
                   [0, self.std_steer**2]])

        self.P = F @ self.P @ F.T + V @ M @ V.T

    def move(self, x, u, dt):
        hdg = x[2, 0]
        vel = u[0]
        steering_angle = u[1]
        dist = vel * dt

        if abs(steering_angle) > 0.001: # is robot turning?
            beta = (dist / self.wheelbase) * tan(steering_angle)
            r = self.wheelbase / tan(steering_angle) # radius

            dx = np.array([[-r*sin(hdg) + r*sin(hdg + beta)], 
                           [r*cos(hdg) - r*cos(hdg + beta)], 
                           [beta]])
        else: # moving in straight line
            dx = np.array([[dist*cos(hdg)], 
                           [dist*sin(hdg)], 
                           [0]])
        return x + dx

现在我们还有另一个问题要处理。残差的计算理论上为 y = z − h ( x ) y=z−h(x) y=zh(x),但这不起作用,因为我们的观测包含一个角度。假设 z z z的方向为 1 ∘ 1^{\circ} 1 h ( x ) h(x) h(x) 35 9 ∘ 359^{\circ} 359。直接利用残差公式计算得到的残差为 − 35 8 ∘ -358^{\circ} 358,而正确的角度差是 2 ∘ 2^{\circ} 2。因此,我们必须编写代码来正确计算方向残差。

def residual(a, b):
    """ compute residual (a-b) between measurements containing 
    [range, bearing]. Bearing is normalized to [-pi, pi)"""
    y = a - b
    y[1] = y[1] % (2 * np.pi)    # force in range [0, 2 pi)
    if y[1] > np.pi:             # move to [-pi, pi)
        y[1] -= 2 * np.pi
    return y

其余的代码运行模拟并绘制结果,现在不需要太多注释。我创建了一个变量landmarks 来包含地标的坐标。我每秒更新机器人的位置10次,但每秒只运行一次EKF。这有两个原因。首先,我们没有使用Runge Kutta积分运动微分方程,因此较窄的时间步长使我们的模拟更精确。其次,在嵌入式系统中,处理速度有限是很正常的。这迫使你仅在绝对需要时运行卡尔曼滤波器。

from filterpy.stats import plot_covariance_ellipse
from math import sqrt, tan, cos, sin, atan2
import matplotlib.pyplot as plt

dt = 1.0

def z_landmark(lmark, sim_pos, std_rng, std_brg):
    x, y = sim_pos[0, 0], sim_pos[1, 0]
    d = np.sqrt((lmark[0] - x)**2 + (lmark[1] - y)**2)  
    a = atan2(lmark[1] - y, lmark[0] - x) - sim_pos[2, 0]
    z = np.array([[d + randn()*std_rng],
                  [a + randn()*std_brg]])
    return z

def ekf_update(ekf, z, landmark):
    ekf.update(z, HJacobian=H_of, Hx=Hx, 
               residual=residual,
               args=(landmark), hx_args=(landmark))
    
                
def run_localization(landmarks, std_vel, std_steer, 
                     std_range, std_bearing,
                     step=10, ellipse_step=20, ylim=None):
    ekf = RobotEKF(dt, wheelbase=0.5, std_vel=std_vel, 
                   std_steer=std_steer)
    ekf.x = array([[2, 6, .3]]).T # x, y, steer angle
    ekf.P = np.diag([.1, .1, .1])
    ekf.R = np.diag([std_range**2, std_bearing**2])

    sim_pos = ekf.x.copy() # simulated position
    # steering command (vel, steering angle radians)
    u = array([1.1, .01]) 

    plt.figure()
    plt.scatter(landmarks[:, 0], landmarks[:, 1],
                marker='s', s=60)
    
    track = []
    for i in range(200):
        sim_pos = ekf.move(sim_pos, u, dt/10.) # simulate robot
        track.append(sim_pos)

        if i % step == 0:
            ekf.predict(u=u)

            if i % ellipse_step == 0:
                plot_covariance_ellipse(
                    (ekf.x[0,0], ekf.x[1,0]), ekf.P[0:2, 0:2], 
                     std=6, facecolor='k', alpha=0.3)

            x, y = sim_pos[0, 0], sim_pos[1, 0]
            for lmark in landmarks:
                z = z_landmark(lmark, sim_pos,
                               std_range, std_bearing)
                ekf_update(ekf, z, lmark)

            if i % ellipse_step == 0:
                plot_covariance_ellipse(
                    (ekf.x[0,0], ekf.x[1,0]), ekf.P[0:2, 0:2],
                    std=6, facecolor='g', alpha=0.8)
    track = np.array(track)
    plt.plot(track[:, 0], track[:,1], color='k', lw=2)
    plt.axis('equal')
    plt.title("EKF Robot localization")
    if ylim is not None: plt.ylim(*ylim)
    plt.show()
    return ekf
landmarks = array([[5, 10], [10, 5], [15, 15]])

ekf = run_localization(
    landmarks, std_vel=0.1, std_steer=np.radians(1),
    std_range=0.3, std_bearing=0.1)
print('Final P:', ekf.P.diagonal())
Final P: [0.025 0.042 0.002]

【滤波】扩展卡尔曼滤波器_第7张图片

我把这些地标画成了实心正方形,机器人的路径是用一条黑线表示。预测步骤的协方差椭圆为浅灰色,更新的协方差椭圆以绿色显示。为了使它们在这个比例下可见,我将椭圆边界设置为 6 σ 6\sigma 6σ

我们可以看到,我们的运动模型增加了很多不确定性,而且大部分误差都发生在运动方向上(通过绿色椭圆的形状可以确定)。经过几步之后,我们可以看到滤波器结合了地标测量值,误差得到了改善。

我在UKF章节中使用了相同的初始条件和地标位置。UKF在误差椭圆方面获得了更好的精度。就 x \mathbf{x} x的估计而言,两者的表现大致相同。

现在,让我们添加另一个地标。

landmarks = array([[5, 10], [10, 5], [15, 15], [20, 5]])

ekf = run_localization(
    landmarks, std_vel=0.1, std_steer=np.radians(1),
    std_range=0.3, std_bearing=0.1)
plt.show()
print('Final P:', ekf.P.diagonal())
Final P: [0.02  0.02  0.002]

【滤波】扩展卡尔曼滤波器_第8张图片

轨道末端附近估计值的不确定性较小,我们可以看到使用多个地标对我们的不确定性的影响还是很大的。下面仅使用前两个地标:

ekf = run_localization(
    landmarks[0:2], std_vel=1.e-10, std_steer=1.e-10,
    std_range=1.4, std_bearing=.05)
print('Final P:', ekf.P.diagonal())
Final P: [0.022 0.045 0.   ]

【滤波】扩展卡尔曼滤波器_第9张图片

经过地标后,估计值很快偏离机器人的路径,协方差也快速增长。让我们看看只有一个地标会发生什么:

ekf = run_localization(
    landmarks[0:1], std_vel=1.e-10, std_steer=1.e-10,
    std_range=1.4, std_bearing=.05)
print('Final P:', ekf.P.diagonal())
Final P: [0.263 0.798 0.004]

【滤波】扩展卡尔曼滤波器_第10张图片

正如你所猜测的那样,一个地标会产生非常糟糕的结果。相反,大量的地标使我们能够做出非常准确的估计。

landmarks = array([[5, 10], [10,  5], [15, 15], [20,  5], [15, 10], 
                   [10,14], [23, 14], [25, 20], [10, 20]])

ekf = run_localization(
    landmarks, std_vel=0.1, std_steer=np.radians(1),
    std_range=0.3, std_bearing=0.1, ylim=(0, 21))
print('Final P:', ekf.P.diagonal())
Final P: [0.008 0.009 0.001]

【滤波】扩展卡尔曼滤波器_第11张图片

讨论

我说这是一个真正的现实问题,在某些方面确实如此。我看到过使用机器人运动模型的其他演示,那些模型使用了更简单的雅可比矩阵。其实,我的运动模型从某个角度上看也是简化了的——它使用自行车模型。一辆真正的汽车有两套轮胎,每一套都以不同的半径行驶。车轮也不能很好地抓住路面。我还假设机器人会即时响应控制输入。Sebastian Thrun在《概率机器人》(Probabilistic Robots)中写道,这种简化模型是合理的,因为滤波器在用于跟踪真实车辆时表现良好。这里的结论是,虽然你必须有一个较精确的非线性模型,但它并不需要非常完美才能很好地运行。作为一名设计师,你需要平衡模型的保真度、数学难度以及执行线性代数所需的CPU时间。

这个问题被简单化的另一个点是,我们知道地标和观测值之间的对应关系。但是假设我们使用的是雷达——我们怎么知道特定的信号返回对应于本地场景中的哪个特定地标?这个问题暗示了SLAM算法——同步定位和建图。SLAM不是本书的重点,所以我不会详细阐述这个话题。


UKF与EKF

在上一章中,我使用UKF来解决这个问题。两者实现上的差异应该非常明显。尽管有一个基本的运动模型,但计算状态模型和观测模型的雅可比矩阵并不是一件小事。不同的问题,可能导致雅可比矩阵难以或不可能通过解析的方法得到。相比之下,UKF只需要提供一个状态模型的函数和另一个观测模型的函数。

在许多情况下,雅可比矩阵无法解析地找到。具体的细节超出了本书的范围,但你必须通过数值的方法来计算雅可比矩阵。这项工作并非简单,你可能需要花费相当大一部分精力学习技术来处理此类情况。即使这样,你也可能只能解决与你的领域相关的问题——航空工程师对Navier Stokes方程了解很多,但对化学反应速率模型可能就了解不多了。

因此,UKF很容易。那么,它准确吗?在实践中,它通常比EKF表现更好。你可以找到大量的研究论文,证明UKF在各种问题领域的表现优于EKF。不难理解为什么这是真的。EKF通过在单点处线性化系统模型和观测模型来工作,UKF使用 2 n + 1 2n+1 2n+1个sigma点。

让我们看一个具体的例子。拿 f ( x ) = x 3 f(x)=x^{3} f(x)=x3并通过高斯分布,我将使用蒙特卡罗模拟计算准确答案。我根据高斯分布随机生成50000个点,每个点通过 f ( x ) f(x) f(x),然后计算结果的均值和方差。

EKF通过在求值点 x x x处求导数来线性化函数,该斜率成为我们用于变换高斯函数的线性函数。

import kf_book.nonlinear_plots as nonlinear_plots

nonlinear_plots.plot_ekf_vs_mc()
actual mean=1.30, std=1.14
EKF    mean=1.00, std=0.95

【滤波】扩展卡尔曼滤波器_第12张图片

EKF的计算相当不准确。相比之下,以下是UKF的表现:

nonlinear_plots.plot_ukf_vs_mc(alpha=0.001, beta=3., kappa=1.)
actual mean=1.30, std=1.12
UKF    mean=1.30, std=1.08

【滤波】扩展卡尔曼滤波器_第13张图片

这里我们可以看到UKF平均值的计算精确到小数点后2位。标准偏差稍有偏差,但你也可以通过使用生成sigma点的 α \alpha α β \beta β κ \kappa κ参数来微调UKF计算分布的方式。这里我使用了 α = 0.001 \alpha=0.001 α=0.001 β = 3 \beta=3 β=3 κ = 1 \kappa=1 κ=1。可以随意修改它们以查看结果,你可能能得到比我更好的结果。但是,避免针对特定测试过度调整UKF。对于你的测试用例,它的性能可能更好,但总体上却可能更差。


相关阅读

  • Kalman-and-Bayesian-Filters-in-Python/blob/master/11-Extended-Kalman-Filters

你可能感兴趣的:(《人工智能》常用滤波器,卡尔曼滤波器,扩展卡尔曼滤波器,KF,EKF,线性化)