【问题标题】:Sequence of incorrect length generated by function函数生成的长度不正确的序列
【发布时间】:2017-02-10 00:25:02
【问题描述】:

为什么当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

知道我犯了什么错误吗?

【问题讨论】:

  • 你是一个孤独的人我接受它。
  • Seq.distinct 是您的问题吗?
  • 我把Seq改成List,每次都得到1000。
  • 风格挑剔:match (boolean) with true -&gt; (do-if-true) | false -&gt; (do-if-false) 通常读起来比if (boolean) then (do-if-true) else (do-if-false) 好得多。更容易立即掌握if...then...else 语句在做什么。作为一般规则,更少的脑力劳动理解语法 = 更多的脑力可用于理解逻辑 = 更少的错误。至于为什么你会得到你得到的结果,我也怀疑Seq.distinct,但我还不能证明。
  • 我有第二种风格的评论,它太长了,无法发表评论,所以我会把它作为答案,即使它没有回答你问题的主要部分。

标签: f# lazy-evaluation lazy-sequences


【解决方案1】:

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

在 cmets 中,我提到将 match (bool) with true -&gt; ... | false -&gt; ... 替换为简单的 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# 允许您重用这样的名称,并且函数内的名称会“遮蔽”函数外的名称,但重用名称具有完全不同的类型通常不是一个好主意 比原来的名字。换句话说,这可能是个好主意:

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 option,而函数内部的 afloat .但它们是“相同”的类型——也就是说,“调用者可能指定了一个浮点值,或者他们可能没有”和“现在我肯定有一个浮点值”之间的心理差异很小.但是在“名称sample 是一个接受三个参数的函数”和“名称sample 是一个浮点数序列”之间存在非常很大的心理差距。我强烈建议您使用 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

每当我发现自己在函数末尾写“let result = (something) ; result”时,我通常只想用 (something) 替换整个代码块。也就是说,上面的 sn-p 可以变成:

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 Code 中,并添加了一些 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,它只会打印出四个如果序列中有更多可用值,则打印省略号)。此外,我将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; ...]

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

为什么会有这种差异?因为Seq.append 强制评估。它只是创建一个新的seq,其规则是“从第一个 seq 中取出所有项目,然后当那个用完时,从第二个 seq 中取出所有项目。当那个用完时,结束。”并且Seq.distinct 也不强制评估;它只是创建了一个新的seq,其规则是“从seq 中取出交给你的物品,并在被要求时开始分发它们。但是在你去的时候记住它们,如果你之前已经分发过其中一个,不要再发了。”因此,您在调用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,但返回的 seq 的长度最终还是 4 的可能性略低于 10%——因为它选择了一个 不同的 em> 一组随机数,而不是你检查的那个。

为了证明这一点,我再次运行了该函数,这次只选择了 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]

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

顺便说一句,您的函数还有一个其他问题,那就是它比它需要的要慢得多。函数Seq.item,在某些情况下,必须从头遍历序列,才能选择序列中的第n项。最好在函数开始时将数据存储在数组中 (let arrData = data |&gt; 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 值,错误就会消失。你可以用一个简单的DiscreteUniform.Samples(0, n-1) |&gt; Seq.distinct |&gt; Seq.take size 替换整个generateIdx 函数。 (并使用数组进行数据查找,以便您的函数运行得更快)。换句话说,这是我将如何重写您的代码的 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])

就是这样!简单、易于理解并且(据我所知)没有错误。

编辑: ...但不是完全干燥,因为在那个“最终”版本中仍然有一些重复的代码。 (感谢CaringDev 在下面的 cmets 中指出)。 Seq.take size |&gt; Seq.mapif 表达式的两个分支中重复出现,因此有一种方法可以简化该表达式。我们可以这样做:

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])

【讨论】:

  • 如果 Seq.length(idx) size 也可能?
  • 否,因为if 语句(以前是match)中需要m 的值。至少,他的原始代码版本需要m 的值——但是一旦他修复了这个错误,他就不再需要它了。
  • 小改进的想法:分离值生成和映射以增加 DRYness,即(if repl then ... else ...) |&gt; Seq.take size |&gt; Seq.map(...)
  • 这确实是懒惰可以咬你的一个很好的例子。以意想不到的方式......
猜你喜欢
  • 1970-01-01
  • 2012-11-23
  • 1970-01-01
  • 2020-12-16
  • 1970-01-01
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多