4

我是功能世界的新手,并感谢对此的帮助。

我想从这个简单的函数中取代丑陋的命令式代码,但不知道该怎么做。

我想要的是从 IEnumerable(F# 中的 seq)中随机选择一些关于概率值的元素 - 元组中的第二个项目(因此“概率”为 0.7 的项目将比 0.1 更频繁地被选择)。

/// seq<string * float>
let probabilitySeq = seq [ ("a", 0.7); ("b", 0.6); ("c", 0.5); ("d", 0.1) ]

/// seq<'a * float> -> 'a
let randomPick probSeq =
    let sum = Seq.fold (fun s dir -> s + snd dir) 0.0 probSeq 
    let random = (new Random()).NextDouble() * sum
    // vvvvvv UGLY vvvvvv
    let mutable count = random
    let mutable ret = fst (Seq.hd probSeq )
    let mutable found = false
    for item in probSeq  do
        count <- count - snd item
        if (not found && (count < 0.0)) then
            ret <- fst item  //return ret;  //in C# 
            found <- true
    // ^^^^^^ UGLY ^^^^^^
    ret

////////// at FSI: //////////

> randomPick probabilitySeq;;
    val it : string = "a"
> randomPick probabilitySeq;;
    val it : string = "c"
> randomPick probabilitySeq;;
    val it : string = "a"
> randomPick probabilitySeq;;
    val it : string = "b"

我认为这randomPick很容易以命令方式实现,但在功能上呢?


这是功能性的,但需要list not seq (wanted)。

//('a * float) list -> 'a
let randomPick probList =
    let sum = Seq.fold (fun s dir -> s + snd dir) 0.0 probList
    let random = (new Random()).NextDouble() * sum
    let rec pick_aux p list = 
        match p, list with
        | gt, h::t when gt >= snd h -> pick_aux (p - snd h) t
        | lt, h::t when lt < snd h -> fst h 
        | _, _ -> failwith "Some error"
    pick_aux random probList
4

7 回答 7

4

使用Matajon建议的原则的 F# 解决方案:

let randomPick probList =
    let ps = Seq.skip 1 (Seq.scan (+) 0.0 (Seq.map snd probList))
    let random = (new Random()).NextDouble() * (Seq.fold (fun acc e -> e) 0.0 ps)
    Seq.find (fun (p, e) -> p >= random)
             (Seq.zip ps (Seq.map fst probList))
    |> snd

但在这种情况下,我可能也会使用基于列表的方法,因为无论如何都需要预先计算概率值的总和......

于 2009-10-19T08:10:26.197 回答
2

我理解它的方式,你的逻辑是这样的:

对所有权重求和,然后在 0 和所有权重之和之间选择一个随机双精度数。找到与您的概率相对应的项目。

换句话说,您希望按如下方式映射您的列表:

Item    Val    Offset    Max (Val + Offset)
----    ---    ------    ------------------
a       0.7    0.0       0.7
b       0.6    0.7       1.3
c       0.5    1.3       1.8
d       0.1    1.8       1.9

转换(item, probability)to的列表(item, max)很简单:

let probabilityMapped prob =
    [
        let offset = ref 0.0
        for (item, probability) in prob do
            yield (item, probability + !offset)
            offset := !offset + probability
    ]

尽管这依赖于可变变量,但它是纯粹的、确定性的并且本着可读代码的精神。如果你坚持避免可变状态,你可以使用这个(不是尾递归):

let probabilityMapped prob =
    let rec loop offset = function
        | [] -> []
        | (item, prob)::xs -> (item, prob + offset)::loop (prob + offset) xs
    loop 0.0 prob

虽然我们通过列表线程化状态,但我们执行的是映射,而不是折叠操作,所以我们不应该使用 Seq.fold 或 Seq.scan 方法。我开始使用 Seq.scan 编写代码,它看起来很笨拙和奇怪。

无论您选择哪种方法,一旦您映射了列表,就很容易在线性时间内选择一个随机加权的项目:

let rnd = new System.Random()
let randomPick probSeq =
    let probMap =
        [
            let offset = ref 0.0
            for (item, probability) in probSeq do
                yield (item, probability + !offset)
                offset := !offset + probability
        ]

    let max = Seq.maxBy snd probMap |> snd
    let rndNumber = rnd.NextDouble() * max    
    Seq.pick (fun (item, prob) -> if rndNumber <= prob then Some(item) else None) probMap
于 2009-10-19T14:38:04.243 回答
2

我将只提供 Haskell 版本,因为我的笔记本上没有 F#,它应该是相似的。原理是将您的序列转换为类似的序列

[(0.7,"a"),(1.3,"b"),(1.8,"c"),(1.9,"d")]

其中元组中的每个第一个元素表示的不是概率,而是范围之类的东西。然后很容易,从0到最后一个数字(1.9)中选择一个随机数并检查它属于哪个范围。例如,如果选择 0.5,它将是“a”,因为 0.5 低于 0.7。

哈斯克尔代码 -

probabilitySeq = [("a", 0.7), ("b", 0.6), ("c", 0.5), ("d", 0.1)]

modifySeq :: [(String, Double)] -> [(Double, String)]
modifySeq seq = modifyFunction 0 seq where 
    modifyFunction (_) [] = []
    modifyFunction (acc) ((a, b):xs) = (acc + b, a) : modifyFunction (acc + b) xs

pickOne :: [(Double, String)] -> IO String
pickOne seq = let max = (fst . last) seq in
    do 
        random <- randomRIO (0, max)
        return $ snd $ head $ dropWhile (\(a, b) -> a < random) seq

result :: [(String, Double)] -> IO String
result = pickOne . modifySeq

例子 -

*Main> result probabilitySeq
"b"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"d"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"b"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"c"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"c"
于 2009-10-19T00:07:23.027 回答
1

我会使用Seq.to_list将输入序列转换为列表,然后使用基于列表的方法。引用的列表足够短,不应该是不合理的开销。

于 2009-10-18T23:27:38.090 回答
1

最简单的解决方案是使用 ref 在对 Seq 模块中任何合适函数的迭代器调用之间存储状态:

let probabilitySeq = seq [ ("a", 0.7); ("b", 0.6); ("c", 0.5); ("d", 0.1) ]

let randomPick probSeq =
    let sum = Seq.fold (fun s (_,v) -> s + v) 0.0 probSeq 
    let random = ref (System.Random().NextDouble() * sum)
    let aux = function
        | _,v when !random >= v ->
            random := !random - v
            None
        | s,_ -> Some s
    match Seq.first aux probSeq with
        | Some r -> r
        | _ -> fst (Seq.hd probSeq)
于 2009-10-19T05:27:51.220 回答
0

I would use your functional, list-based version, but adapt it to use LazyList from the F# PowerPack. Using LazyList.of_seq will give you the moral equivalent of a list, but without evaluating the whole thing at once. You can even pattern match on LazyLists with the LazyList.(|Cons|Nil|) pattern.

于 2009-10-19T20:15:18.013 回答
0

我认为cfern的建议实际上是最简单(?=最佳)的解决方案。

需要评估整个输入,因此 seq 的按需产量优势无论如何都会丢失。最简单的似乎是将序列作为输入并同时将其转换为列表和总和。然后将列表用于算法的基于列表的部分(列表将以相反的顺序排列,但这对计算无关紧要)。

let randomPick moveList =
    let sum, L = moveList
        |> Seq.fold (fun (sum, L) dir -> sum + snd dir, dir::L) (0.0, [])
    let rec pick_aux p list = 
        match p, list with
        | gt, h::t when gt >= snd h -> pick_aux (p - snd h) t
        | lt, h::t when lt < snd h -> fst h 
        | _, _ -> failwith "Some error"
    pick_aux (rand.NextDouble() * sum) L

感谢您的解决方案,尤其是 Juliet 和 Johan(我已经阅读了几次才能真正了解它)。
:-)

于 2009-10-19T22:34:15.263 回答