حساب المتوسط المتحرك في F#
-
08-07-2019 - |
سؤال
ما زلت أعمل على تحسين الشيء F# - أحاول معرفة كيفية "التفكير" في F# بدلاً من مجرد الترجمة من اللغات الأخرى التي أعرفها.
لقد فكرت مؤخرًا في الحالات التي لا يكون لديك فيها خريطة 1: 1 بين قبل وبعد.الحالات التي يسقط فيها List.map.
أحد الأمثلة على ذلك هو المتوسطات المتحركة، حيث عادةً ما يكون لديك نتائج len-n+1 لقائمة الطول len عند حساب المتوسط على n من العناصر.
بالنسبة للمعلمين، هل هذه طريقة جيدة للقيام بذلك (باستخدام قائمة الانتظار المضغوطة من جومو فيشر)?
//Immutable queue, with added Length member
type Fifo<'a> =
new()={xs=[];rxs=[]}
new(xs,rxs)={xs=xs;rxs=rxs}
val xs: 'a list;
val rxs: 'a list;
static member Empty() = new Fifo<'a>()
member q.IsEmpty = (q.xs = []) && (q.rxs = [])
member q.Enqueue(x) = Fifo(q.xs,x::q.rxs)
member q.Length() = (List.length q.xs) + (List.length q.rxs)
member q.Take() =
if q.IsEmpty then failwith "fifo.Take: empty queue"
else match q.xs with
| [] -> (Fifo(List.rev q.rxs,[])).Take()
| y::ys -> (Fifo(ys, q.rxs)),y
//List module, add function to split one list into two parts (not safe if n > lst length)
module List =
let splitat n lst =
let rec loop acc n lst =
if List.length acc = n then
(List.rev acc, lst)
else
loop (List.hd lst :: acc) n (List.tl lst)
loop [] n lst
//Return list with moving average accross len elements of lst
let MovingAverage (len:int) (lst:float list) =
//ugly mean - including this in Fifo kills genericity
let qMean (q:Fifo<float>) = ((List.sum q.xs) + (List.sum q.rxs))/(float (q.Length()))
//get first part of list to initialise queue
let (init, rest) = List.splitat len lst
//initialise queue with first n items
let q = new Fifo<float>([], init)
//loop through input list, use fifo to push/pull values as they come
let rec loop (acc:float list) ls (q:Fifo<float>) =
match ls with
| [] -> List.rev acc
| h::t ->
let nq = q.Enqueue(h) //enqueue new value
let (nq, _) = nq.Take() //drop old value
loop ((qMean nq)::acc) t nq //tail recursion
loop [qMean q] rest q
//Example usage
MovingAverage 3 [1.;1.;1.;1.;1.;2.;2.;2.;2.;2.]
(ربما تكون الطريقة الأفضل هي تنفيذ MovingAverageQueue عن طريق الوراثة من Fifo؟)
المحلول
إذا كنت لا تهتم كثيراً بالأداء، فإليك الحل البسيط للغاية:
#light
let MovingAverage n s =
Seq.windowed n s
|> Seq.map Array.average
let avgs = MovingAverage 5000 (Seq.map float [|1..999999|])
for avg in avgs do
printfn "%f" avg
System.Console.ReadKey() |> ignore
يؤدي هذا إلى إعادة حساب متوسط كل "نافذة" من الصفر، لذلك يكون الأمر سيئًا إذا كانت النوافذ كبيرة.
على أية حال، تحقق من Seq.windowed:
لأنه من السهل أن يكون لديك في جيبك الخلفي لمثل هذه الأشياء.
نصائح أخرى
اذا أنت يفعل إذا كنت تهتم بالأداء، فيمكنك حساب المتوسط المتحرك بكفاءة باستخدام شيء من هذا القبيل (بافتراض أننا نحسب المتوسط المتحرك خلال نافذة مدتها 3 أيام)
Numbers[n] Running Total[n] --------- --------------- n[0] = 7 7 = Numbers[0] n[1] = 1 8 = RunningTotal[1-1] + Numbers[1] n[2] = 2 10 = RunningTotal[2-1] + Numbers[2] n[3] = 8 11 = RunningTotal[3-1] + Numbers[3] - Numbers[3-3] n[4] = 4 14 = RunningTotal[4-1] + Numbers[4] - Numbers[4-3] n[5] = 1 13 = RunningTotal[5-1] + Numbers[5] - Numbers[5-3] n[6] = 9 14 = RunningTotal[6-1] + Numbers[6] - Numbers[6-3] ... N RunningTotal[N] = RunningTotal[N-1] + Numbers[N] - Numbers[N-3]
الجزء الصعب في هذا الأمر هو الاحتفاظ بإجمالي ورقم التشغيل السابقنافذة N.لقد توصلت إلى الكود التالي:
let movingAverage days l =
seq {
let queue = new Queue<_>(days : int)
let divisor = decimal days
let total = ref 0m
for cur in l do
queue.Enqueue(cur)
total := !total + cur
if queue.Count < days then
yield (cur, 0m)
else
yield (cur, !total / divisor)
total := !total - (queue.Dequeue())
}
لا يبدو هذا الإصدار جميلًا مثل كود Haskell، لكنه يجب أن يتجنب مشكلات الأداء المرتبطة بإعادة حساب "النافذة" الخاصة بك في كل تشغيل.فهو يحتفظ بإجمالي قيد التشغيل ويحتفظ بالأرقام المستخدمة مسبقًا في قائمة الانتظار، لذلك يجب أن يكون سريعًا جدًا.
من باب التسلية فقط كتبت معيارًا بسيطًا:
#light
open System
open System.Collections.Generic
open System.Diagnostics;
let windowAverage days (l : #seq<decimal>) = Seq.windowed days l |> Seq.map (Seq.average)
let princessAverage days l =
seq {
let queue = new Queue<_>(days : int)
let divisor = decimal days
let total = ref 0m
for cur in l do
queue.Enqueue(cur)
total := !total + cur
if queue.Count < days then
yield (cur, 0m)
else
yield (cur, !total / divisor)
total := !total - (queue.Dequeue())
}
let testData =
let rnd = new System.Random()
seq { for a in 1 .. 1000000 -> decimal (rnd.Next(1000)) }
let benchmark msg f iterations =
let rec loop = function
| 0 -> ()
| n -> f 3 testData |> ignore; loop (n - 1)
let stopWatch = Stopwatch.StartNew()
loop iterations
stopWatch.Stop()
printfn "%s: %f" msg stopWatch.Elapsed.TotalMilliseconds
let _ =
let iterations = 10000000
benchmark "princessAverage" princessAverage iterations
benchmark "windowAverage" windowAverage iterations
printfn "Done"
نتائج:
princessAverage: 1670.791800 windowAverage: 2986.146900
الإصدار الخاص بي أسرع بـ 1.79 مرة تقريبًا.
وهنا هو ل(تصحيح، وآمل) F # نسخة من الحل المقترح هاسكل هنا .
وتحرير: الآن الذيل عودي، وليس أي أسرع، ولكن لا تنفجر مع ن = 50000. (انظر تحرير التاريخ لنسخة غير الذيل عودي)
let LimitedAverage n ls =
let rec loop acc i n ls =
match i with
| 0 -> acc //i counts down from n to 0, when we hit 0 we stop
| _ -> match ls with
| [] -> acc //if we hit empty list before end of n, we stop too
| x::xs -> (loop (acc + (x / float n)) (i - 1) n xs) //divide this value by n, perform average on 'rest' of list
loop 0. n n ls
LimitedAverage 50000 (List.map float [1..9999999])
//>val it : float = 25000.5
let rec MovingAverage3 n ls =
let rec acc loop i n ls =
match i with
| 0 -> List.rev acc //i counts down from n to 0, when we hit 0 we stop
| _ -> match ls with
| [] -> List.rev acc //if we hit empty list before end of n, we stop too
| x::xs -> loop (LimitedAverage2 n ls :: acc) (i - 1) n xs // limited average on whole list, then moving average on tail
loop [] (n + 1) n ls
MovingAverage3 50000 (List.map float [1..9999999])
//>val it : float list = [25000.5; 25001.5; 25002.5; ...]
إذا كنت تهتم الأداء ومثل الرمز أنيقة ثم حاول
module MovingAverage =
let selfZip n l =
Seq.skip n l |> Seq.zip l
let runTotal i z =
Seq.scan ( fun sum (s, e) -> sum - s + e ) i z
let average n l:seq<'a> =
Seq.skip n l
|> selfZip n
|> runTotal (l |> Seq.take n |> Seq.sum)
|> Seq.map ( fun sum -> decimal sum / decimal n )
let ma = MovingAverage.average 2 myseq
وعن طريق FSUnit يمكننا اختبار
let myseq = seq { for i in 0..10 do yield i }
Seq.nth 0 ma |> should equal 0.5
Seq.nth 1 ma |> should equal 1.5
Seq.nth 2 ma |> should equal 2.5
Seq.nth 3 ma |> should equal 3.5
والحيلة من الخوارزمية هي مجموع أول أعداد ن الأولى و ثم الحفاظ على تشغيل إجمالي بإضافة الرأس من النافذة وطرح الذيل من النافذة. نافذة انزلاق هو تحقيق ذلك من خلال القيام البريدي النفس على تسلسل ولكن مع ثاني حجة لالبريدي التي تقدمت بها حجم النافذة.
في نهاية خط الأنابيب نحن فقط تقسيم المجموع على التوالي من النافذة بحجم.
ملحوظة المسح هو تماما مثل أضعاف، ولكنه سيحقق كل إصدار من الدولة إلى تسلسل.
وحلا حتى أكثر أناقة على الرغم possibley مع ضرب الأداء لجعل الملاحظة أنه إذا كنا الصفر لوحة تسلسل نحن لسنا بحاجة لحساب المبلغ الأولي.
namespace Utils
module MovingAverage =
let selfZip n l =
Seq.skip n l |> Seq.zip l
let rec zeros =
seq { yield 0.0; yield! zeros}
// Create a running total given
let runTotal z =
Seq.scan (fun sum (s,e) -> sum - s + e ) 0.0 z
let average n l =
Seq.concat [(Seq.take n zeros); l]
|> selfZip n
|> runTotal
|> Seq.map ( fun sum -> sum / float n )
|> Seq.skip n
ويمكن أن يكون هناك أداء إصابة بسبب المراوغة الثاني تتعلق التفاف تسلسل اثنين ولكن ربما ليس كبيرا اعتمادا على حجم النافذة
وهذا هو الإصدار بلدي.
let sma list n =
let len = List.length list
let rec loop acc sum cnt =
if cnt >= len then List.rev acc
else if cnt < n-1 then loop (0.0::acc) (sum + List.nth list cnt) (cnt+1)
else loop (((sum + List.nth list cnt)/(float n))::acc) (sum + (List.nth list cnt) - (List.nth list (cnt-n+1))) (cnt+1)
loop [] 0.0 0
مثال:
sma (List.map float [5..50]) 5
[0, 0, 0, 0, 7, 8, 9, ...]
بقدر ما أستطيع أن أرى، التعليمات البرمجية الخاصة بك مليئة let
صياغات.لست على دراية بـ F# ولكني قمت ببعض أعمال Haskell.النموذج الوظيفي يعني عدم التفكير في "كيف" بل في "ماذا":أنت تعتقد أن Fifo، ولكن يجب عليك في الواقع تحديد دلالات المتوسط المتحرك.
-- the limited average of a list
limitedaverage 0 _ = 0
limited verage n (x:xs) = (x/n) + ( limited average (n-1) xs )
-- a list, transformed into a sequence of moving averages of
movingaverages n [] = []
movingaverages n (x:xs) = ( movingaverage n (x:xs) : movingaverages n xs )