Manually calculating integrated gradient

vainaijr: <p>in the paper formula is,<br>
<img src="https://discuss.pytorch.org/uploads/default/original/3X/e/1/e102a68ff9da9ebeaf0d8be48aba37bb39c4c9f1.png" alt="Screenshot (485)" data-base62-sha1="w6x6O6zmzwmHclvncLhuX6JdAFb" width="461" height="75"></p>
<p>how do we manually calculate this value, for example,</p>
<pre><code class="lang-auto">from captum.attr import IntegratedGradients
import torch, torch.nn as nn, torch.nn.functional as F
</code></pre>
<pre><code class="lang-auto">class ToyModel(nn.Module):
    r"""
    Example toy model from the original paper (page 10)

    https://arxiv.org/pdf/1703.01365.pdf


    f(x1, x2) = RELU(ReLU(x1) - 1 - ReLU(x2))
    """

    def __init__(self):
        super().__init__()

    def forward(self, input1, input2):
        relu_out1 = F.relu(input1)
        relu_out2 = F.relu(input2)
        return F.relu(relu_out1 - 1 - relu_out2)
</code></pre>
<pre><code class="lang-auto">net = ToyModel()
net.eval()
# defining model input tensors
input1 = torch.tensor([3.0], requires_grad=True)
input2 = torch.tensor([1.0], requires_grad=True)

# defining baselines for each input tensor
baseline1 = torch.tensor([0.0])
baseline2 = torch.tensor([0.0])

# defining and applying integrated gradients on ToyModel and the
ig = IntegratedGradients(net)
attributions, approximation_error = ig.attribute((input1, input2),
                                                 baselines=(baseline1, baseline2),
                                                 method='gausslegendre',
                                                 return_convergence_delta=True)
attributions
</code></pre>
<p>gives<br>
(tensor([1.5000], grad_fn=),<br>
tensor([-0.5000], grad_fn=))</p>
<p>so, here baseline is 0, 0 input is 3, 1 if our function is,</p>
<pre><code class="lang-auto">f(x1, x2) = x1 - 1 - x2
</code></pre>
<p>do we replace x1 with x1*alpha, then differentiate wrt x1, so we get alpha, then integrate, so we get alpha**2 / 2 with alpha from 0 to 1, that is 1/2</p>
<p>and same thing for x2, replace x2 with x2*alpha, then differentiate wrt x2, so we get -alpha, then integrate, so we get -alpha**2 / 2 with alpha from 0 to 1, that is -1/2</p>
<p>then multiply these with input, which gives 1.5, -0.5.</p>
<p>what is the intuition behind using this technique, and how does one understand this formula in a better way?</p>

tom: <aside class="quote no-group" data-username="vainaijr" data-post="1" data-topic="70584">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/vainaijr/48/49167_2.png" class="avatar"> vainaijr:</div>
<blockquote>
<p>what is the intuition behind using this technique, and how does one understand this formula in a better way?</p>
</blockquote>
</aside>
<p>So the goal of the attribution exercise we are doing is to attribute the difference between F(x) and F(x’) to the inidividual components of the input difference (x-x’).<br>
What happens is that we are following the line form x’ to x. If we integrate the derivative in the direction of this line that, we get the difference between F(x) and F(x’), this is the fundamental theorem of calculus.<br>
But now, the “derivative in the direction of this line” can be written as a scalar product of the gradient of F with the direction vector ((x-x’) / |x-x’|). The integrated gradient now collects the gradient of F parts separately (before the scalar product).<br>
This and a change of variables leads to the formula you cite above.<br>
Inherent in the construction is sum_i IntegratedGrads_i(x) = F(x) - F(x’), so we are indeed decomposing the difference.</p>
<p>Best regards</p>
<p>Thomas</p>

vainaijr: <p>I think you explain correct but I am still confused, could you give an example with a function like f(x) = x**2.</p>

tom: <p>A 1d example won’t do. <img src="https://discuss.pytorch.org/images/emoji/apple/slight_smile.png?v=12" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"><br>
But here is a simple 2d one.<br>
Take f(x, y) = x * exp(y). We can plot this in 3d or as a countour plot:</p>
<pre data-code-wrap="python"><code class="lang-python">f = lambda x, y: x * y.exp()
xx = torch.linspace(-1,1)[None].expand(100,100)
yy = torch.linspace(-1,1)[:, None].expand(100,100)
zz = f(xx, yy)
x0 = torch.tensor([-0.5, 0.5])
y0 = torch.tensor([-0.25, 0.5])
z0 = f(x0, y0)
fig = pyplot.figure()
ax = fig.gca(projection='3d')
ax.scatter3D(x0[0], y0[0], z0[0], color=['k'], s=20)
ax.scatter3D(x0[1], y0[1], z0[1], color=['k'], s=20)
ax.set_transform
ax.plot_surface(xx.numpy(),yy.numpy(),zz.numpy(), cmap=pyplot.cm.coolwarm)
ax.view_init(elev=40)
pyplot.figure()
pyplot.contourf(xx.numpy(),yy.numpy(),zz.numpy(), cmap=pyplot.cm.coolwarm, levels=20)
pyplot.plot(x0, y0)
</code></pre>
<p>3d:</p>
<p><img src="https://discuss.pytorch.org/uploads/default/original/3X/9/9/99e41fe4371553b142303dc63ba6b5c32f83d6a5.png" alt="image" data-base62-sha1="lXnO4wxO7rJuqFYPXSKfXDzEJUh" width="349" height="231"></p>
<p>Contour plot:<br>
<img src="https://discuss.pytorch.org/uploads/default/original/3X/f/1/f126c0df844d31a5ea945a017a67cf55f55b9128.png" alt="image" data-base62-sha1="ypk69eM2uACFMmlt8VGHxlacLt6" width="398" height="252"></p>
<p>Now I added two points (in the 3d) and a line in the countour plot. We take the top right end of the line as x and the lower bottom as x’.</p>
<p>We can parametrize this line:</p>
<pre data-code-wrap="python"><code class="lang-python">x_line = (x0[0] + torch.linspace(0, 1, 1000) * (x0[1] - x0[0])).requires_grad_()
y_line = (y0[0] + torch.linspace(0, 1, 1000) * (y0[1] - y0[0])).requires_grad_()
</code></pre>
<p>Note that this looks a like the x’ + α(x-x’) you have as an argument to ∂F/∂x_i in the integral in your equation (1).</p>
<p>And indeed we can compute the gradients for each point on the line:</p>
<pre data-code-wrap="python"><code class="lang-python">z_line = f(x_line, y_line)
z_line.sum().backward()
</code></pre>
<p>We can approximate the integral over 0…1 by taking the mean over the <code>.grad</code>s. Thus we can calculate the integrated gradients:</p>
<pre data-code-wrap="python"><code class="lang-python">ig_x = (x0[1] - x0[0]) * x_line.grad.mean()
ig_y = (y0[1] - y0[0]) * y_line.grad.mean()
</code></pre>
<p>This gives <code>ig_x</code> as 1.1599 and <code>ig_y</code> as 0.0540.</p>
<p>As a sanity check, we can compare <code>ig_x + ig_y</code> with <code>z0[1]-z0[0]</code> and indeed they seem to differ by 0.0001, which looks good.</p>
<p>Best regards</p>
<p>Thomas</p>

vainaijr: <p>thanks for your reply, I carry experiment and find</p>
<pre><code class="lang-auto">ig_x + ig_y
</code></pre>
<p>and</p>
<pre><code class="lang-auto">z0[1] - z0[0]
</code></pre>
<p>to be equal to,</p>
<pre><code class="lang-auto">1.21376102688577
</code></pre>
<p>when integrating, with alpha from 0 to 1.</p>
<p>in the paper, they also have this figure,<br>
<img src="https://discuss.pytorch.org/uploads/default/original/3X/c/7/c7e577fb1b096f649c0e338faeb15546caacf4f2.png" alt="Screenshot (489)" data-base62-sha1="swmEmoRLY7G4O44tMNjnx4GpRt0" width="620" height="438"><br>
does this mean, that instead of parameterization of a line,  we could parameterize a curve also, like a sigmoid curve, or a sinusoidal curve, or a circle and that would be a different attribution method.</p>

tom: <aside class="quote no-group" data-username="vainaijr" data-post="5" data-topic="70584">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/vainaijr/48/49167_2.png" class="avatar"> vainaijr:</div>
<blockquote>
<p>does this mean, that instead of parameterization of a line, we could parameterize a curve also, like a sigmoid curve, or a sinusoidal curve, or a circle and that would be a different attribution method.</p>
</blockquote>
</aside>
<p>So there are several things to be considered here:</p>
<ul>
<li>The line parametrization (x’ + α(x-x’)) is of constant speed along the path. This enables multiplying with (x_i-x’_i) at the last step rather than having to do this separately for each “time step” in the integral. But done properly such a change of in the parametrization of the path would not change the result.</li>
<li>As the figure illustrates, if you took a path different than the straight line, then you would get a different attribution. While the “sum of the two attribution parts” is again the difference in function values by the fundamental theorem of calculus, you can get a wildly different split by coordinate. e.g. if you take F = min(x_1, x_2) as your function and consider paths between (0, 1) and (1, 0), where the function is both 0. You could go along the coordinate axes to get (depending on which you do first) an attribution of (0, 0) or (1, -1). You can also get anything in between by interleaving directions.</li>
<li>One of the bad parts of all this (with having to arbitrarily choose the line) is that it will no <em>compose</em> well, i.e. if you declare the last conv layer of your network the features and now you want to attribute the change in features to the change in input and the change in output to the change in features, you’ll get something completely different between the two, because it’s unlikely that the straight line in input space corresponds to a straight line in feature-space.</li>
<li>In the end, it is some sort of non-definedness issue around the choice of path. The next question could be how to get around this. The straight line is straightforward to pick in Euclidean space, as would be geodesics (but even then it might not be unique if you have several) for manifolds. You could also try to get avoid the choice of a single path by introducing some probability measure on the paths and then integrating over that. But by then you’re in a much more complex setting (and I’m not sure anyone has done this).</li>
</ul>
<p>Hmhm. Now the last post might have been peak happiness. <img src="https://discuss.pytorch.org/images/emoji/apple/slight_smile.png?v=12" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"></p>
<p>Best regards</p>
<p>Thomas</p>