函数生成的错误长度的顺序

时间:2017-02-10 00:25:03

标签: f# lazy-evaluation lazy-sequences

当repl变量设置为false时,为什么以下函数返回不正确长度的序列?

open MathNet.Numerics.Distributions
open MathNet.Numerics.LinearAlgebra
let sample (data : seq<float>) (size : int) (repl : bool) =

    let n = data |> Seq.length

    // without replacement
    let rec generateIndex idx =
        let m = size - Seq.length(idx)
        match m > 0 with
        | true ->
            let newIdx = DiscreteUniform.Samples(0, n-1) |> Seq.take m 
            let idx = (Seq.append idx newIdx) |> Seq.distinct
            generateIndex idx
        | false -> 
            idx

    let sample =
        match repl with
        | true ->
            DiscreteUniform.Samples(0, n-1) 
            |> Seq.take size 
            |> Seq.map (fun index -> Seq.item index data)
        | false ->
            generateIndex (seq []) 
            |> Seq.map (fun index -> Seq.item index data)

    sample

运行功能......

let requested = 1000
let dat = Normal.Samples(0., 1.) |> Seq.take 10000
let resultlen = sample dat requested false |> Seq.length 
printfn "requested -> %A\nreturned -> %A" requested resultlen

结果长度错误。

> 
requested -> 1000
returned -> 998

> 
requested -> 1000
returned -> 1001

> 
requested -> 1000
returned -> 997

知道我在做什么错误吗?

1 个答案:

答案 0 :(得分:9)

首先,我想对编码风格做出评论。然后,我将解释为什么你的序列会以不同的长度返回。

在评论中,我提到使用简单的match (bool) with true -> ... | false -> ...表达式替换if ... then ... else,但是我认为可以改进的是另一种编码风格。你写道:

let sample (various_parameters) =  // This is a function
    // Other code ...
    let sample = some_calculation  // This is a variable
    sample  // Return the variable

虽然F#允许您重复使用这样的名称,但函数内部的名称将会&#34; shadow&#34;在函数外部的名称,重用名称与原始名称具有完全不同的类型通常是个坏主意。换句话说,这可能是一个好主意:

let f (a : float option) =
    let a = match a with
            | None -> 0.0
            | Some value -> value
    // Now proceed, knowing that `a` has a real value even if had been None before

或者,因为以上内容正是F#为您提供的defaultArg

let f (a : float option) =
    let a = defaultArg a 0.0
    // This does exactly the same thing as the previous snippet

在这里,我们在函数中使名称a引用与名为a的参数不同的类型:参数是float optiona我们的函数内部是float。但是他们会对#34;同样的&#34;类型 - 也就是说,#34之间的心理差异非常小;调用者可能已经指定了一个浮点值,或者他们可能没有&#34;和#34;现在我肯定有一个浮点值&#34;。但是&#34之间存在非常的心理差距。名称sample是一个带有三个参数的函数&#34;和&#34;名称sample是一系列花车&#34;。我强烈建议您使用result之类的名称作为您要从函数返回的值,而不是重复使用函数名称。

此外,这似乎不必要地冗长:

let result =
    match repl with
    | true ->
        DiscreteUniform.Samples(0, n-1) 
        |> Seq.take size 
        |> Seq.map (fun index -> Seq.item index data)
    | false ->
        generateIndex (seq []) 
        |> Seq.map (fun index -> Seq.item index data)

result

任何时候我发现自己写作&#34;让结果=(某事);导致&#34;在我的函数结束时,我通常只想用(something)替换整个代码块。即,上面的代码段可能只是:

match repl with
| true ->
    DiscreteUniform.Samples(0, n-1) 
    |> Seq.take size 
    |> Seq.map (fun index -> Seq.item index data)
| false ->
    generateIndex (seq []) 
    |> Seq.map (fun index -> Seq.item index data)

反过来可以用if...then...else表达式替换:

if repl then
    DiscreteUniform.Samples(0, n-1) 
    |> Seq.take size 
    |> Seq.map (fun index -> Seq.item index data)
else
    generateIndex (seq []) 
    |> Seq.map (fun index -> Seq.item index data)

这是代码中的最后一个表达式。换句话说,我可能会按如下方式重写您的函数(仅更改样式,不更改逻辑):

open MathNet.Numerics.Distributions
open MathNet.Numerics.LinearAlgebra
let sample (data : seq<float>) (size : int) (repl : bool) =

    let n = data |> Seq.length

    // without replacement
    let rec generateIndex idx =
        let m = size - Seq.length(idx)
        if m > 0 then
            let newIdx = DiscreteUniform.Samples(0, n-1) |> Seq.take m 
            let idx = (Seq.append idx newIdx) |> Seq.distinct
            generateIndex idx
        else
            idx

    if repl then
        DiscreteUniform.Samples(0, n-1) 
        |> Seq.take size 
        |> Seq.map (fun index -> Seq.item index data)
    else
        generateIndex (seq []) 
        |> Seq.map (fun index -> Seq.item index data)

如果我能弄清楚你的序列长度错误的原因,我也会用这些信息更新这个答案。

更新:好的,我想我会看到generateIndex功能中发生了什么,会给您带来意想不到的结果。绊倒你有两件事:一个是序列懒惰,另一个是随机性。

我将您的generateIndex函数复制到VS代码中,并添加了一些printfn语句来查看正在进行的操作。首先,我运行的代码,然后是结果:

let rec generateIndex n size idx =
    let m = size - Seq.length(idx)
    printfn "m = %d" m
    match m > 0 with
    | true ->
        let newIdx = DiscreteUniform.Samples(0, n-1) |> Seq.take m
        printfn "Generating newIdx as %A" (List.ofSeq newIdx)
        let idx = (Seq.append idx newIdx) |> Seq.distinct
        printfn "Now idx is %A" (List.ofSeq idx)
        generateIndex n size idx
    | false -> 
        printfn "Done, returning %A" (List.ofSeq idx)
        idx

所有这些List.ofSeq idx调用都是这样的,当我打印出来时,F#Interactive将打印超过四个seq项(默认情况下,如果您尝试使用%A打印seq,它将会只打印出四个值,然后如果seq中有更多可用值,则打印省略号。此外,我将nsize转换为参数(我不会在通话之间进行更改),以便我可以轻松地对其进行测试。然后我将其称为generateIndex 100 5 (seq [])并获得以下结果:

m = 5
Generating newIdx as [74; 76; 97; 78; 31]
Now idx is [68; 28; 65; 58; 82]
m = 0
Done, returning [37; 58; 24; 48; 49]
val it : seq<int> = seq [12; 69; 97; 38; ...]

了解数字如何变化?这是我的第一个线索,有些事情发生了。请注意,seq 懒惰 。在他们不得不这样做之前,他们不会评估他们的内容。您不应该将seq视为数字列表。相反,将其视为生成器,当被要求提供数字时,会根据某些规则生成它们。在您的情况下,规则为&#34;选择0和n - 1之间的随机整数,然后取m这些数字&#34;。关于seq s的另一个问题是它们不会缓存它们的内容(尽管有Seq.cache函数可用于缓存它们的内容)。因此,如果你有一个基于随机数生成器的seq,它的结果每次都会有所不同,正如你在输出中看到的那样。当我打印出newIdx时,打印出[74; 76; 97; 78; 31],但是当我将它附加到空seq时,结果打印为[68; 28; 65; 58; 82]。

为什么会出现这种差异?因为Seq.append 强制评估。它只是创建一个新的seq,其规则是&#34;从第一个seq获取所有项目,然后当那个耗尽时,从第二个seq中获取所有项目。当那个人耗尽时,结束。&#34;并且Seq.distinct也不会强制进行评估;它只是创建一个新的seq,其规则是&#34;将seq中的项目交给您,并在被问到时开始将它们移出。但是当你去的时候记住它们,如果你以前把它们中的一个交出去,那就不要再把它拿出去了。&#34;所以你在调用generateIdx之间传递的是一个对象,在评估时会选择一组介于0和n-1之间的随机数(在我的简单情况下,介于两者之间) 0和100)然后将该设置减少到一组不同的数字。

现在,这就是事情。每次评估seq时,它都会从头开始:首先调用DiscreteUniform.Samples(0, n-1)生成无限的随机数流,然后从该流中选择m个数字,然后抛弃任何数字重复。 (我现在忽略了Seq.append,因为它会造成不必要的心理复杂性,而且它无论如何都不是真正的一部分)。现在,在函数的每个循环开始时,检查序列的长度,这会导致它被评估。这意味着它选择(在我的示例代码的情况下)0到99之间的5个随机数,然后确保它们全部不同。如果它们都是不同的,那么m = 0并且函数将退出,返回...不是数字列表,而是 seq对象。当评估seq对象时,它将从头开始,选择不同的5个随机数集,然后丢弃任何重复项。因此,至少有一组5个数字中的至少一个最终会成为副本,因为测试了其长度的序列(我们知道它不包含重复,否则m会有已大于0)返回的序列。返回的序列具有1.0 * 0.99 * 0.98 * 0.97 * 0.96的机会,不包含任何重复项,大约为0.9035。因此,即使您检查Seq.length并且它是5,也只有不到10%的可能性,返回的seq的长度最终为4 - 因为它选择了不同的随机数集,而不是您检查的随机数。

为了证明这一点,我再次运行该功能,这次只挑选4个数字,以便在F#Interactive提示下完全显示结果。我的generateIndex 100 4 (seq [])运行产生了以下输出:

m = 4
Generating newIdx as [36; 63; 97; 31]
Now idx is [39; 93; 53; 94]
m = 0
Done, returning [47; 94; 34]
val it : seq<int> = seq [48; 24; 14; 68]

请注意我何时打印&#34;完成,返回(idx的值)&#34;,它只有3个值?即使它最终返回4个值(因为它为实际结果选择了不同的随机数选择,并且该选择没有重复),这证明了问题。

顺便说一下,你的功能存在一个其他问题,这个问题远比它需要的慢得多。在某些情况下,函数Seq.item必须从头开始执行序列才能选择序列的n项。在函数开头(let arrData = data |> Array.ofSeq)将数据存储在数组中会更好,然后替换

        |> Seq.map (fun index -> Seq.item index data)

        |> Seq.map (fun index -> arrData.[index])

数组查找是在恒定时间内完成的,因此可以将样本函数从O(N ^ 2)降低到O(N)。

TL; DR :在 之前使用Seq.distinct 从中获取m值并且错误将会消失远。您只需使用简单的generateIdx替换整个DiscreteUniform.Samples(0, n-1) |> Seq.distinct |> Seq.take size函数即可。 (并使用数组进行数据查找,以便您的函数运行得更快)。换句话说,这里是 final 几乎 - 我将如何重写代码的最终版本:

let sample (data : seq<float>) (size : int) (repl : bool) =
    let arrData = data |> Array.ofSeq
    let n = arrData |> Array.length

    if repl then
        DiscreteUniform.Samples(0, n-1) 
        |> Seq.take size 
        |> Seq.map (fun index -> arrData.[index])
    else
        DiscreteUniform.Samples(0, n-1) 
        |> Seq.distinct
        |> Seq.take size 
        |> Seq.map (fun index -> arrData.[index])

那就是它!简单,易于理解,并且(据我所知)无错误。

编辑 ...但不是完全干,因为在那&#34;最终&#34;中仍然有一些重复的代码版。 (感谢CaringDev在下面的评论中指出它)。在Seq.take size |> Seq.map表达式的两个分支中重复if,因此可以简化该表达式。我们可以这样做:

let randomIndices =
    if repl then
        DiscreteUniform.Samples(0, n-1) 
    else
        DiscreteUniform.Samples(0, n-1) |> Seq.distinct

randomIndices
|> Seq.take size 
|> Seq.map (fun index -> arrData.[index])

所以这是我的建议的真正最终版本:

let sample (data : seq<float>) (size : int) (repl : bool) =
    let arrData = data |> Array.ofSeq
    let n = arrData |> Array.length
    let randomIndices =
        if repl then
            DiscreteUniform.Samples(0, n-1) 
        else
            DiscreteUniform.Samples(0, n-1) |> Seq.distinct
    randomIndices
    |> Seq.take size 
    |> Seq.map (fun index -> arrData.[index])