Backpropagating through noise

username: michaelklachko
cooked: <p>I want to add random gaussian noise to my network weights, for every forward pass. When backpropagating, I want to calculate gradients in respect to distorted weights, then update the original weights using those gradients. Am I doing it right in the example below?</p>
<pre><code class="lang-auto">class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		self.linear = nn.Linear(784, 10)
		self.gaussian = Normal(loc=0, scale=torch.ones_like(self.linear.weight))

	def forward(self, x):
		orig_weight = self.linear.weight.clone()
		noise = self.gaussian.sample()
		self.linear.weight.data = self.linear.weight.data + noise
		x = self.linear(x)
		self.linear.weight.data = orig_weight.data
		return x

model = Net()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
model.train()

for epoch in range(10):
	for i in range(100):
		output = model(input)
		loss = nn.CrossEntropyLoss()(output, label)
		optimizer.zero_grad()
		loss.backward()
		optimizer.step()
</code></pre>

username: ptrblck
cooked: <p>I’ve debugged your code and it seems to do exactly what you wish to achieve.<br>
I have to say I don’t really like the usage of <code>.data</code> in general, but this might be a valid use case. <img src="https://discuss.pytorch.org/images/emoji/apple/wink.png?v=6" title=":wink:" class="emoji" alt=":wink:"><br>
At least I’m not sure how to make it better without manipulating the linear implementation.<br>
Maybe someone else will have a good idea.</p>

username: michaelklachko
cooked: <p>I appreciate your help! Initially I was surprised by your answer, because shortly after I posted my question, I realized that the (more) correct way to do it is like this:</p>
<pre><code class="lang-auto">class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		self.linear = nn.Linear(784, 10)

	def forward(self, x):
		return self.linear(x)

model = Net()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
model.train()

for epoch in range(10):
	for i in range(100):

             orig_params = []
             for p in model.parameters():
	         orig_params.append(p.clone())
	         gaussian = Normal(loc=0, scale=torch.ones_like(p))
	         p.data = p.data + gaussian.sample()

	     output = model(input)
	     loss = nn.CrossEntropyLoss()(output, label)

             optimizer.zero_grad()
	     loss.backward()

             for p, orig_p in zip(model.parameters(), orig_params):
		 p.data = orig_p.data

	     optimizer.step()
</code></pre>
<p>However, now I see that the reason you didn’t see any issues with my initial example is that I simplified it too much. Because there’s no hidden layer to backpropagate through, it does not make any difference. However, if we consider, for example, MLP with a hidden layer:</p>
<pre><code class="lang-auto">class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		self.linear1 = nn.Linear(784, 100)
                self.linear2 = nn.Linear(100, 10)
                self.relu = nn.ReLU()

	def forward(self, x):
		x = self.linear1(x)
                x = self.relu(x)
                x = self.linear2(x)
                return x
</code></pre>
<p>Well, now we would have a problem with the method in the first example: if we apply noise to both weight matrices (in linear1 and linear2), then the error would backpropagate through the original linear2 weights, while what we really want is to backpropagate through the distorted linear2 weights, to get the true gradient in respect to distorted weights of the linear1 layer. Do you agree?</p>
<p>By the way, I’m curious, how did you debug/verify the code?</p>

username: ptrblck
cooked: <p>You are right, I assumed you are adding the noise and resetting the weights of all layers before and after the update step respectively.</p>
<p>Well, I just initialized the weights to a constant value, calculated the expected gradients and had a look what happens after adding/resetting the weights and the update step.<br>
It was just a numerical check to see, if the “right” gradients are used as I’m always worried about manipulating <code>.data</code>. <img src="https://discuss.pytorch.org/images/emoji/apple/wink.png?v=6" title=":wink:" class="emoji" alt=":wink:"></p>

username: AlbertZeyer
cooked: <p><a class="mention" href="/u/michaelklachko">@michaelklachko</a> Was there some paper which motivated you to do this? Can you share it?</p>
<p>I know this under the term “variational weight noise” or “variational parameter noise” but I’m not sure where that term comes from.</p>
<p>Regarding the implementation, I think this can be implemented in a similar way than <code>WeightNorm</code>, right?</p>

username: michaelklachko
cooked: <p>At the time I was working on this paper: <a href="https://arxiv.org/abs/1904.01705" class="inline-onebox" rel="noopener nofollow ugc">[1904.01705] Improving Noise Tolerance of Mixed-Signal Neural Networks</a></p>
<p>I actually ended up creating custom layers to do this: <a href="https://github.com/michaelklachko/NoisyNet/blob/master/hardware_model.py#L357-L362" class="inline-onebox" rel="noopener nofollow ugc">NoisyNet/hardware_model.py at master · michaelklachko/NoisyNet · GitHub</a></p>
<p>I’m not sure what is “variational weight noise”, this paper might give you some ideas: <a href="https://arxiv.org/abs/1506.02557" class="inline-onebox" rel="noopener nofollow ugc">[1506.02557] Variational Dropout and the Local Reparameterization Trick</a></p>