2016-11-21 5 views
5

Я ищу идиоматический подход к программированию фильтров в F #. Для ясности я имею в виду фильтр как функцию, которая использует серию измерений с течением времени и производит изменяющиеся оценки. Это означает, что функция может поддерживать состояние. Например, в Python можно использовать сопрограммы для поддержания состояния очень чистым способом.Идиоматический подход к фильтрации

Что я ищу - это идиоматический подход к программированию фильтров в F #. Учитывая, что мой ум полностью загрязнен ООП и процедурные принципы, я, естественно, придумал классы, чтобы выразить их. Есть ли более идиоматический подход к фильтрации в 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 

ответ

8

В вашем случае, термины «функциональный» и «F # идиоматические» будет состоять из двух вещей: неизменные данные и разделения данных из кода.

Неизменяемых данные: вы бы одну структуры данных, представляющих параметры фильтра (т.е. A, H, Q и R), а другая структура, представляющая текущее состояние фильтра (т.е. X, K и P). Оба неизменяемы. Вместо того, чтобы мутировать состояние, вы должны создать новый.

Разделение данных из кода: сам фильтр будет состоять из одной функции, которая принимает параметры, текущее состояние, следующее значение наблюдения и производит следующее состояние. Затем это следующее состояние будет возвращено в функцию вместе со следующим значением наблюдения, таким образом создавая следующее состояние + 1 и т. Д. Параметры всегда остаются неизменными, поэтому их можно передавать только один раз, используя частичное приложение (см. Ниже).

После того, как у вас есть такая функция, вы можете «применить» ее к списку наблюдений в качестве «прокатки», как описано выше, - взяв каждое наблюдение и подав его в функцию вместе с последним состоянием, производя следующее состояние. Эта операция «прокатки проекции» является очень распространенным явлением в функциональном программировании и обычно называется scan. F # не предусматривает реализацию scan для всех стандартных коллекций - list, seq и т.д.

В результате 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 функция объявлена ​​в модуле. Эта функция может показаться глупой на поверхности, но имеет важное значение: она позволяет мне указывать поля состояний по имени без модуля, тем самым избегая загрязнения пространства имен. Кроме того, потребительский код теперь выглядит более читаемым: он говорит, что он делает, никаких комментариев не требуется.

Другой общий подход к этому является объявить «базовое» состояние в модуле, который потребляя код может затем внести поправки через with синтаксиса:

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

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

записку о коллекциях
F # списков мелкие мелкие объемы данных и небольшие технологические трубопроводы, но становятся дорогими по мере роста этих двух измерений. Если вы работаете с большим количеством потоковых данных и/или если вы применяете несколько фильтров подряд, вам может быть лучше использовать ленивые последовательности - seq. Для этого просто замените List.scan и List.map на Seq.scan и Seq.map соответственно. Если вы это сделаете, вы получите ленивую последовательность в качестве конечного результата, которую вам тогда нужно будет как-то поглотить - либо преобразовать в список, распечатать, отправить его следующему компоненту, либо независимо от вашего более широкого контекста.

Смежные вопросы