习惯用于过滤的方法

时间:2016-11-21 02:31:18

标签: f# functional-programming

我正在寻找一种在F#中编程过滤器的惯用方法。为清楚起见,我将过滤器称为一种功能,它随着时间的推移使用一系列测量值并产生不断变化的估计值。这意味着该功能能够维持状态。例如,在Python中,可以使用协同程序以非常干净的方式维护状态。

我正在寻找的是用F#编程过滤器的惯用方法。鉴于我的思想受到OOP和程序原则的彻底污染,我自然而然地想出了表达它们的课程。在F#中有一种更惯用的过滤方法,这可能会开启功能范式的其他好处吗?

open System
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.Random
open MathNet.Numerics.Distributions
open MathNet.Numerics.Statistics
open FSharp.Charting

type ScalarKalman (A : float, H : float, Q : float, R : float) = class

    let mutable A = A
    let mutable H = H
    let mutable Q = Q
    let mutable R = R

    let mutable p = 0.
    let mutable x = 0.
    let mutable k = 0.

    let mutable result = 0.

    member this.X
        with get() = x
        and set(value) = x <- value

    member this.P
        with get() = p
        and set(value) = p <- value

    member this.K
        with get() = k
        and set(value) = k <- value

    member this.update(newVal : float) =
        let xp = A * this.X
        let Pp = A * this.P * A + Q
        this.K <- Pp * H / (H * Pp * H + R)
        this.X <- xp + this.K * (newVal - H * xp)
        this.P <- Pp - this.K * H * Pp

end

let n = 100
let obsv = [|for i in 0 .. n do yield 0.|]
let smv = [|for i in 0 .. n do yield 0.|]
let kal = new ScalarKalman(1., 1., 0., 5.)
kal.P <- 4.
kal.X <- 6.
for i in 0 .. n do
    obsv.[i] <- Normal.Sample(10., 5.)
    kal.update(obsv.[i])
    smv.[i] <- kal.X

Chart.Combine([obsv |> Chart.FastLine
               smv |> Chart.FastLine]) |> Chart.Show

1 个答案:

答案 0 :(得分:8)

在您的情况下,术语&#34; functional&#34;和&#34; F#惯用&#34;将由两部分组成:不可变数据和代码中的数据分离。

不可变数据:您将拥有一个表示过滤器参数的数据结构(即AHQR) ,以及表示过滤器当前状态的另一个结构(即XKP)。两者都是不变的。而不是改变状态,你会产生一个新的状态。

从代码中分离数据:过滤器本身将由一个函数组成,该函数接受参数,当前状态,下一个观察值,并产生下一个状态。然后,下一个状态将与下一个观察值一起反馈到函数中,从而产生下一个+ 1状态,依此类推。参数始终保持不变,因此可以使用部分应用程序将它们传递一次(见下文)。

一旦你有了这样的功能,你可以&#34;申请&#34;它作为&#34;滚动投影&#34;的观察列表, - 如上所述, - 将每个观察结果与最后一个状态一起送入函数,产生下一个状态。这个&#34;滚动投影&#34;在函数式编程中,操作是很常见的,通常称为scan。 F#确实为所有标准集合提供scan的实现 - listseq等。

作为scan的结果,您将获得过滤器的连续状态列表。现在剩下要做的就是从每个州中取出X值。

以下是完整的解决方案:

module ScalarKalman =

   type Parameters = { A : float; H : float; Q : float; R : float }
   type State = { K: float; X: float; P: float }

   let initState (s: State) = s

   let getX s = s.X

   let update parms state newVal =
      let xp = parms.A * state.X
      let Pp = parms.A * state.P * parms.A + parms.Q
      let newK = Pp * parms.H / (parms.H * Pp * parms.H + parms.R)
      { K = newK
        X = xp + newK * (newVal - parms.H * xp)
        P = Pp - newK * parms.H * Pp }


let n = 100
let obsv = [for i in 0 .. n -> Normal.Sample(10., 5.)]
let kal = ScalarKalman.update { A = 1.; H = 1.; Q = 0.; R = 5. }
let initialState = ScalarKalman.initState { X = 6.; P = 4.; K = 0. }

let smv = 
  obsv 
  |> List.scan kal initialState 
  |> List.map ScalarKalman.getX

关于设计的说明
请注意模块中声明的initState函数。这个函数表面看起来很愚蠢,但它有重要意义:它允许我按名称指定状态字段,而不用open模块,从而避免命名空间污染。此外,消费代码现在看起来更具可读性:它说它做了什么,不需要评论。

另一个常见的方法是声明一个&#34; base&#34;模块中的状态,消耗代码然后可以通过with语法修改:

module ScalarKalman = 
    ...
    let zeroState = { K = 0.; X = 0.; P = 0. }

...
let initialState = { ScalarKalman.zeroState with X = 6.; P = 4. }

关于馆藏的说明
F#列表适用于少量数据和小型处理流水线,但随着这两个维度的增长而变得昂贵。如果您正在处理大量流数据,并且/或者如果您连续应用多个过滤器,那么最好使用延迟序列 - seq。为此,只需将List.scanList.map分别替换为Seq.scanSeq.map。如果你这样做,你会得到一个懒惰的序列作为最终结果,然后你需要以某种方式消费 - 要么将其转换为列表,将其打印出来,发送到下一个组件,或者更大的上下文所暗示的。 / p>