Vectorizing adstock/geometric-decay operations for speed-up

richardhansson: <p>I am trying to make this operation below a bit faster, does anyone have a smart way of doing so utilizing vectorized operations?</p>
<p>Also it would be nice to know how to initialize a tensor that has size 0, that case should also work in the below setup(see ones and hundres variables, imagine another variable with the below setup):<br>
<div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/f/4/f4b4b9c11444052056dca8833e8919e5a663bb2c.png" data-download-href="https://discuss.pytorch.org/uploads/default/f4b4b9c11444052056dca8833e8919e5a663bb2c" title="image"><img src="https://discuss.pytorch.org/uploads/default/original/3X/f/4/f4b4b9c11444052056dca8833e8919e5a663bb2c.png" alt="image" data-base62-sha1="yULHKlTwh7ddhkXUzPP3L4qa0BC" width="690" height="340" data-dominant-color="3F4347"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">image</span><span class="informations">1136×561 30.3 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div></p>
<pre><code class="lang-auto">
def adstock_geometric(x: torch.Tensor, theta: float):
    x_decayed = torch.zeros_like(x)
    x_decayed[0] = x[0]

    for xi in range(1, len(x_decayed)):
        x_decayed[xi] = x[xi] + theta * x_decayed[xi - 1]

    return x_decayed

def adstock_multiple_samples(x: torch.Tensor, theta: torch.Tensor):

    listtheta = theta.tolist()
    if isinstance(listtheta, float):
        return adstock_geometric(x=x,
                                 theta=theta)
    x_decayed = torch.zeros((100, 112, 1))
    for idx, theta_ in enumerate(listtheta):
        x_decayed_one_entry = adstock_geometric(x=x,
                                                theta=theta_)
        x_decayed[idx] = x_decayed_one_entry
    return x_decayed

if __name__ == '__main__':
      ones = torch.tensor([1])
      hundreds = torch.tensor([idx for idx in range(100)])
      x = torch.tensor([[idx] for idx in range(112)])
      ones = adstock_multiple_samples(x=x,
                                      theta=ones)
      hundreds = adstock_multiple_samples(x=x,
                                          theta=hundreds)
      print(ones)
      print(hundreds)

</code></pre>

tom: <p>Hello Richard,</p>
<p>So to give a more detailed answer, we’d need to know something about <code>theta</code> and the tensor shapes involved, so that remains the big <strong>If</strong> to the discussion below:</p>
<p>It’s a bit touchy w.r.t. numerical stability, but I think if you precomputed <code>x_scaled = x * (theta ** (-xi))</code>, you could use <code>x_decayed = x_scaled.cumsum(0) * (theta ** (xi))</code> (with <code>xi = torch.arange(...)</code>. It really depends on the size if <code>xi</code> and the magnitude of <code>theta</code> if this has a stability problem. In that case, you could try to split the computation in several parts and combine the vectorized approach with a for loop similar to what you have now to balance stability and speed.</p>
<p>Best regards</p>
<p>Thomas</p>

richardhansson: <p>Hi Tom,<br>
Thanks for replying.<br>
theta.size() == ()  # True<br>
theta.size() == (100,)  # True<br>
x.size() == (sizex,) where sizex [1…1000].  # True<br>
are the ones involved.</p>
<p>kind regards,</p>
<p>Richard</p>

tom: <p>But what is a typical value of theta?</p>

richardhansson: <p>sampled from a beta distribution so in the range [0, 1]</p>

tom: <p>Yeah, so for close-to-one values of beta, the above would likely work, not sure about small ones.</p>