【问题标题】:Vectorizing calculation of values using numpy which requires previously calculated value使用 numpy 对值进行矢量化计算,这需要先前计算的值
【发布时间】:2019-08-29 07:06:14
【问题描述】:

我正在尝试从 Investopedia 计算一个特定的 EMA 公式,看起来像

EmaToday = (ValueToday ∗ (Smoothing / 1+Days)) 
           + (EmaYesterday * (1 - (Smoothing / 1+Days)))

我们可以将其简化为:

Smoothing and Days are constants.
Let's call (Smoothing / 1 + Days) as 'M'

The simplified equation becomes:
EmaToday = ((ValueToday - EmaYesterday) * M) + EmaYesterday

我们可以在传统的 python 中使用如下循环来做到这一点:

# Initialize an empty numpy array to hold calculated ema values
emaTodayArray = np.zeros((1, valueTodayArray.size - Days), dtype=np.float32)

ema = emaYesterday
# Calculate ema
for i, valueToday in enumerate(np.nditer(valueList)):
    ema = ((valueToday - ema) * M) + ema
    emaTodayArray[i] = ema

emaTodayArray 保存所有计算的 EMA 值。

我很难弄清楚如何将其完全矢量化,因为每次新计算都需要 emaYesterday 值。

如果首先可以使用 numpy 进行完全矢量化,如果有人能告诉我方法,我将不胜感激。

【问题讨论】:

  • 首先,我认为您不需要nditer 来进行迭代;它对速度没有帮助(如果这就是您使用它的原因)。如果你确实使用它,有一种方法可以获取索引和值,所以你不应该使用enumerate
  • 你的问题本质上是连续的;一个值取决于前一个。大多数快速编译的numpy 操作本质上是并行的 - 至少从 Python 的角度来看 - 所有值都是一次计算的。 (底层的 C 代码实际上是迭代的)。我们最接近串行操作的是ufuncsaccumulate 方法,其中cumsum 是最常见的。
  • @hpaulj 所以基本上你的意思是,一旦我修复了循环的 nditer 部分,只要这个计算正确,就没有什么可以做的了吗?

标签: python numpy vectorization


【解决方案1】:

注意:我必须填写一些假人才能使您的代码运行,请检查它们是否正常。

可以通过转换 ema[i] ~> ema'[i] = ema[i] x (1-M)^-i 来对循环进行矢量化,然后它就变成了 cumsum

这在下面被实现为ema_pp_naive

这种方法的问题在于,对于中等大小的i (~10^3)​​,(1-M)^-i 项可能会溢出,导致结果无用。

我们可以通过转到日志空间来规避这个问题(使用np.logaddexp 进行求和)。这个ema_pp_safe 比天真的方法要贵很多,但仍然比原始循环快 10 倍以上。在我快速而肮脏的测试中,这给出了一百万个甚至更多的正确结果。

代码:

import numpy as np

K = 1000
Days = 0

emaYesterday = np.random.random()
valueTodayArray = np.random.random(K)
M = np.random.random()

valueList = valueTodayArray


import time

T = []

T.append(time.perf_counter())

# Initialize an empty numpy array to hold calculated ema values
emaTodayArray = np.zeros((valueTodayArray.size - Days), dtype=np.float32)

ema = emaYesterday
# Calculate ema
for i, valueToday in enumerate(np.nditer(valueList)):
    ema = ((valueToday - ema) * M) + ema
    emaTodayArray[i] = ema

T.append(time.perf_counter())

scaling = np.broadcast_to(1/(1-M), valueTodayArray.size+1).cumprod()
ema_pp_naive = ((np.concatenate([[emaYesterday], valueTodayArray * M]) * scaling).cumsum() / scaling)[1:]

T.append(time.perf_counter())

logscaling = np.log(1-M)*np.arange(valueTodayArray.size+1)
log_ema_pp = np.logaddexp.accumulate(np.log(np.concatenate([[emaYesterday], valueTodayArray * M])) - logscaling) + logscaling
ema_pp_safe = np.exp(log_ema_pp[1:])

T.append(time.perf_counter())

print(f'K = {K}')
print('naive method correct:', np.allclose(ema_pp_naive, emaTodayArray))
print('safe method correct:', np.allclose(ema_pp_safe, emaTodayArray))
print('OP {:.3f} ms   naive {:.3f} ms   safe {:.3f} ms'.format(*np.diff(T)*1000))

示例运行:

K = 100
naive method correct: True
safe method correct: True
OP 0.236 ms   naive 0.061 ms   safe 0.053 ms

K = 1000
naive method correct: False
safe method correct: True
OP 2.397 ms   naive 0.224 ms   safe 0.183 ms

K = 1000000
naive method correct: False
safe method correct: True
OP 2145.956 ms   naive 18.342 ms   safe 108.528 ms

【讨论】:

  • 这太棒了。标记为正确。当我将它分解并解决时,我花了一些时间来弄清楚你是如何想出幼稚版本的。非常感谢。如果您能更多地扩展您的处理方式并在每一步分解您的逻辑,即使是日志版本,我将不胜感激。再次感谢您花时间帮助我! :)
  • @AbhishekManiyal 首先再看看 hpaulj 的评论。内置ufuncs 之一的累积确实或多或少是矢量化顺序操作的唯一方法(cumsum 本质上是add.accumulate)。发现ema 可以缩放累积的总和是经验问题。至于log 版本,从概念上来说,其实无非是取了naive 版本的log。我们得到的是容易溢出的 (1-M)^-i 现在只是非常易于管理的 -i x log (1-M)。棘手的一点是,实际上“一个人不能接受”总和的对数......
  • ...(或 cumsum)在某种意义上没有封闭的公式,但幸运的是有 np.logaddexp 可以从术语的日志中计算总和的对数。它本质上确实需要 exp、添加并再次获取日志,但以一种安全的方式防止溢出。
  • 非常感谢所有帮助和见解!
猜你喜欢
  • 2022-01-14
  • 2012-12-15
  • 1970-01-01
  • 2016-04-01
  • 1970-01-01
  • 2015-09-03
  • 2020-12-25
  • 2021-11-26
  • 1970-01-01
相关资源
最近更新 更多