Trying to get a better grasp of the fundamentals -- can an optimizer be called on a tensor that's not part of an nn.Module?

mkaic: <p>Hi all –</p>
<p>I’m brand new to PyTorch, so please forgive my naivete. I’m trying to build my own version of the SnapChat gender-swapping filter. The approach I’ve settled on, in <strong>pseudo</strong>-code, is as follows:</p>
<pre><code class="lang-auto">#I already have two working models ready to be loaded as follows, both set to .eval() after loading.
gender_classifier = load(pretrained_gender_classifier_model)
face_embedder = load(pretrained_VGGFace2_model)

#The user submits a photo to be processed, and the "ground truth" of that photo is determined.
ground_truth_gender = gender_classifier(user_submitted_image)
ground_truth_embedding = face_embedder(user_submitted_image)

#Initialize a random image to refine into the final product
candidate = tensor(white_noise_image, requires_grad = True)

optimizer = torch.optim.Adam([candidate], lr = lr)

#Training Loop
for i in num_iterations:
    optimizer.zero_grad()

    #Determine the embedding and gender of the candidate
    candidate_gender = gender_classifier(candidate)
    candidate_embedding = face_embedder(candidate)

    #Determine how bad these values are compared to what I *want*, which is
    #a similar embedding, but opposite gender.
    gender_loss = binary_cross_entropy(candidate_gender, !ground_truth_gender)
    embedding_loss = mse_loss(candidate_embedding, ground_truth_embedding)
    
    loss = gender_loss * hyperparameter_weight + embedding_loss

    optimizer.step()
</code></pre>
<p>I realize this is not a great implementation of what is effectively a form of neural style transfer, and I’ve tried reading through the official PyTorch tutorial on NST, but found that the approach used there is vastly different from what I’m attempting here.</p>
<p>My implementation throws no errors, but simply outputs the original white-noise candidate image unaltered, no matter how many iterations I run the optimizer. I’ve accepted that, whatever I’m doing, I must be doing it wrong, but I’m at a loss as to what I’m messing up.</p>
<p>If anyone could lend some insight into what makes the above schema not work, or what tenet of PyTorch architecture I’m violating, I would hugely appreciate it. I want to know what my mistake is so I can really understand why any alternate solution I find on the interwebz works better.</p>
<p>As it stands, I’m guessing it has something to do with not being able to use optimizers on tensors that aren’t parameters in a model? If that’s the case, should I try defining a third model that takes in the user image and returns the embedding and classification from its forward() method, with the candidate image initially defined in the <strong>init</strong>()?</p>
<p>I’ve spent a good number of hours on this without much progress, so thank you in advance to anyone who takes the time to respond.</p>

KFrank: <p>Hi Kai!</p>
<aside class="quote no-group" data-username="mkaic" data-post="1" data-topic="113296" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/m/47e85d/48.png" class="avatar"> mkaic:</div>
<blockquote>
<pre><code class="lang-auto">#Initialize a random image to refine into the final product
candidate = tensor(white_noise_image, requires_grad = True)

optimizer = torch.optim.Adam([candidate], lr = lr)

#Training Loop
for i in num_iterations:
    optimizer.zero_grad()

    #Determine the embedding and gender of the candidate
    candidate_gender = gender_classifier(candidate)
    candidate_embedding = face_embedder(candidate)

    #Determine how bad these values are compared to what I *want*, which is
    #a similar embedding, but opposite gender.
    gender_loss = binary_cross_entropy(candidate_gender, !ground_truth_gender)
    embedding_loss = mse_loss(candidate_embedding, ground_truth_embedding)
    
    loss = gender_loss * hyperparameter_weight + embedding_loss

    optimizer.step()
</code></pre>
</blockquote>
</aside>
<p>In short, you need to call <code>loss.backward()</code> before calling<br>
<code>optimizer.step()</code>.</p>
<p>There is no need for a tensor to be part of a model in order to<br>
optimize it.</p>
<p>You need:</p>
<ol>
<li>
<p>The tensor to have <code>requires_grad = True</code>, which you have.</p>
</li>
<li>
<p>The tensor to be included as an optimizer parameter, which<br>
you have.</p>
</li>
<li>
<p>To calculate the gradient of the loss function with respect to<br>
the tensor.  This is done by the autograd system when you call<br>
<code>loss.backward()</code>, which you’re not doing.</p>
</li>
<li>
<p>To use the gradient to perform the optimization by calling<br>
<code>optimizer.step()</code> (which you are doing, but the gradient<br>
isn’t there).</p>
</li>
</ol>
<p>Best.</p>
<p>K. Frank</p>

mkaic: <p>Thank you for your reply! I appreciate it. It turns out I just forgot to include loss.backward() in the above post, but it is present in my actual code (which still does not update the parameter <img src="https://discuss.pytorch.org/images/emoji/apple/confused.png?v=9" title=":confused:" class="emoji" alt=":confused:">)</p>
<p>While at first this is frustrating, I think it may actually be helpful in the long run, because I’m able to narrow down the true source of the bug – it’s probably somewhere in the homebrew classifier that I’m importing. I’ll have to take a more indepth look at that code and see if I can’t track down what I’m doing wrong there.</p>
<p>Your response clarified a lot about the overall hierarchy/structure of PyTorch for me, so again, thank you. I’m excited to keep learning and experimenting with the framework, it seems so powerful and has so many possibilities.</p>
<p>One additional question – if I import a model I’ve pretrained using load_state_dict, and then use that model as a part of the calculation of the cost, the parameters of the model won’t be trained, because they aren’t being passed to the optimizer, right? If I understand correctly, an optimizer can only modify the parameters directly passed to it?</p>
<p>Cheers,<br>
Kai</p>

mkaic: <p>I was able to track down my bug! It turns out that I tried to set the input tensor’s <code>requires_grad = True</code>, but managed to do it in a way that didn’t <em>actually</em> set <code>requires_grad =True</code>. Essentially, I created a tensor with <code>requires_grad = True</code>, then passed that through a <code>nn.transform</code> module, and only <em>then</em> assigned it my candidate variable. It looked something like this:</p>
<pre><code class="lang-auto">candidate = transform.ToPILImage()(torch.rand(C, W, H, requires_grad = True))
</code></pre>
<p>Which to me as a beginner <em>seemed</em> fine, but resulted in <code>candidate</code> not actually having <code>requires_grad</code> be <code>True</code>.</p>
<p>Just thought I’d post this in case some other poor soul falls into the same trap I did and manages to serendipitously happen across this exact forum post.</p>