Haskell中的移动平均

4
给定一个重量列表:
let weights = [0.1, 0.2, 0.4, 0.2, 0.1]

根据一系列的测量值,我想要实现加权平均。以下是Python代码示例:
y=[]
w = length(weights)
for n in range(w,len(x)-w):
    y[n-w/2-1]=sum([a*b for a,b in zip(weights,x[n-w/2:n+w/2+1])])
    #y[n-3]=W[1]*x[n-2]+W[2]*x[n-1]+W[3]*x[n]+W[4]*x[n+1]+W[5]*x[n+2]

我知道Haskell没有数组,我想要实现的是一个手动定义权重的低通滤波器

如果有魔法的话,我想要一只小马。但说真的,这个说明太过简略了。x 是什么?w 又是什么(或许你指的是 weight)?length w 是否总是等于 5?你是否在其他语言中有参考实现?你尝试过什么呢? - Zeta
X是输入数组,w是权重,y是结果数组。 - Uri Goren
考虑到我们使用的是没有数组的语言,我假设你是一个编程初学者。无论如何,额外的信息应该被编辑到问题本身中,这样其他用户就不必从评论中搜寻信息了。我没有时间给出完整的答案,但这应该可以通过使用 tailszipWith 和一点边界调整来实现。 - Zeta
我已经在Python中添加了解决方案来解决我的问题,如果您能取消关闭投票,我将不胜感激。 - Uri Goren
3个回答

4

移动平均可以通过Mealy机器进行计算,其中内部状态是先前的值。

我将展示一个关于三个参数的移动平均值的例子,您可以自己调整以使其可参数化大小。

Mealy机器本质上是一个初始状态和“状态+输入”到“新状态+输出”函数:

Mealy i o ~ (s, s -> i -> (o, s))

假设初始状态全为零,编写一个计算3个数的移动平均值的函数。
type S = (Double, Double)
type I = Double
type O = Double

initialState :: S
initialState = (0, 0)

weight0, weight1, weight2 :: Double
weight0 = 0.25
weight1 = 0.5
weight2 = 0.25

ma :: S -> I -> (O, S)
ma (x0, x1) x2 = (o, s)
    where
    s = (x1, x2)
    o = x0 * weight0 + x1 * weight1 + x2 * weight2

现在我们已经准备好所有的组件,让我们对输入运行这台机器。
runMealy :: (S -> I -> (O, S)) -> S -> [I] -> [O]
runMealy _ _ [] = []
runMealy f s (x : xs) =
    let (o, s') = f s x
    in o : runMealy f s' xs

并尝试它:

λ *Main > runMealy ma initialState [1,2,3,4,5,6,7,8,9]
[0.25,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0]

在机器内部状态“热身”时,您可以丢弃第一个生成的值。


对于任意大小的移动平均机器,您可以使用 Data.Sequence,因为它是一种更好的数据结构,当您从一端推送时,同时从另一端弹出,而不是像单链表 [] 那样。


为什么要谈论Mealy机?因为在某些情况下,您很可能需要在Haskell中使用一些流库: pipesconduitmachines。然后,Mealy机方法将是唯一合理的解决方案。

此外,您还可以制作自回归模型!


“那么Mealy机器方法将是唯一合理的解决方案”——这是一个相当强烈的说法。你有参考资料吗? - effectfully
如果你限制自己一次只处理单个输入元素,那么最终会得到类似Mealy机的东西。可能会有一些组合器为您创建移动缓冲区,但它本身就是Mealy机。 - phadej

3
压缩文件会自动处理对齐问题:
wma :: Num a => [a] -> [a] -> [a]
wma weights = map (sum . zipWith (*) weights )   -- weighted-moving-average
                . foldr (zipWith (:)) (repeat []) 
                . take (length weights) 
                . tails 

(另请参见)。


2

tails 命令可以列出输入列表的所有尾部。例如:tails [1,2,3] = [[1,2,3],[2,3],[3],[]]。由于我们不需要最后一个空列表,因此使用 (init.tails) 命令可以得到除最后一个元素外的所有尾部列表。

import Data.List (tails)
averages :: Num a => [a] -> [a] -> [a]
averages weights xs = sum . zipWith (*) weights <$> (init.tails) xs

请注意,在列表的开头和结尾,它的行为很可能不是您所期望的。特别是在开头和结尾的行为不同。第一个元素将是前 length weight 个元素的平均值,但最后一个元素仅为 head weight * last xs
如果您希望在开头时获得结束的行为,可以使用类似以下内容的代码:
import Data.List (tails)
averages :: Num a => [a] -> [a] -> [a]
averages weights xs = sum . zipWith (*) weights <$>
  (init.tails) (replicate (length weights - 1) 0 ++ xs)

如果你想在开头时就实现结尾的行为,可以使用以下方法:
import Data.List (tails)
averages :: Num a => [a] -> [a] -> [a]
averages weights xs = sum . zipWith (*) weights <$>
  takeWhile (not . null . drop (l-1)) (tails xs)
  where l = length weights

如果您想使用权重列表的中心元素来乘以第一个/最后一个元素,我们需要结合上述两个答案:

import Data.List (tails)
averages :: Num a => [a] -> [a] -> [a]
averages weights xs = sum . zipWith (*) weights <$>
  takeWhile (not . null . drop half) (replicate half 0 ++ xs)
  where half = length weights `quot` 2

2
takeWhile (>= length weights) 不太友好于资源。相反,使用 l = length weights 一次,然后使用 not . null . drop (l - 1) 来检查列表是否至少有 l 个元素。 - Zeta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接