SGD: unexpected parameters evolution during model training

username: cntl_cntl
cooked: <p>Hi,</p>
<p>I have a very basic code to train a linear regression model. The code seems to work and it reacts as expected to the modifications of the learning rate. However I have noticed that I do not understand the evolution of the model’s parameters during the training.</p>
<p>Here is the code I use:</p>
<pre data-code-wrap="python"><code class="lang-python">import pandas as pd
import torch

torch.manual_seed(42)

# generate some sample data
X = torch.rand(100, 1)
y = 2 * X + 1

# initialize model, loss and optimizer
model = torch.nn.Linear(1, 1)
criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

# train the model
num_epochs = 100

stats_params = {"weight": [model.weight.item()], "bias": [model.bias.item()]}

for epoch in range(num_epochs):
    # Forward pass
    outputs = model(X)
    loss = criterion(outputs, y)

    # Backward pass and optimization
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # store model's parameters
    stats_params["weight"].append(model.weight.item())
    stats_params["bias"].append(model.bias.item())

pd.DataFrame(stats_params).plot()
</code></pre>
<p>And here is the plot of the model’s parametrs evolution during the training:</p>
<p><img src="https://discuss.pytorch.org/uploads/default/original/3X/0/8/081bedefb20b69f68271774459149bbb7deed856.png" alt="output" data-base62-sha1="19JEOxIFJOrTl6Cjs4TxigGmXmC" width="559" height="413"></p>
<p>Given that the initial value of <code>bias</code> is below its true value 1, I would expect that in the process of model training it should monotonically increase and asymptotically tend towards the true value. As you can see, this is not what happens.</p>
<p>So is there an error in my code or is this evolution of the model’s parameters expected?</p>

username: J_Johnson
cooked: <p>Until your weights get approximately in range they need to be, the bias will tend to be more chaotic. This is because weights are multiplied while the bias is added. So the weights are more dominant in the model outputs than the bias.</p>
<p>One way to think about this is considering a simple linear function:</p>
<p>f(x) = ax + b</p>
<p>Plug that into a graphing calculator. Now, let’s suppose you can only change a and b in discrete steps. Recall that a is the slope and b is the y-intercept. You might move the y-intercept closer, but find that changing the slope moves it away again.</p>
<p>So the slope is more important to get correct first because it scales as a multiple, while the bias only scales additively.</p>

username: cntl_cntl
cooked: <p>Thank you for your reply!</p>
<p>I understand what you mean, but I don’t agree with this explanation. In the case we are considering the model’s loss as a function of the model’s parameters is a simple quadratic surface, so the SGD optimizer should smoothly move the model’s parameters towards the optimum, provided that the learning rate is not too high.</p>
<p>Here is an updated example with a linear model without bias and with two weights, both equal to 2:</p>
<pre data-code-wrap="python"><code class="lang-python">import matplotlib.pyplot as plt
import pandas as pd
import torch

torch.manual_seed(42)

# generate some sample data
n_features = 2
X = torch.rand(100, n_features)
true_weights = torch.full((n_features, ), 2.)
y = torch.matmul(X, true_weights).reshape(-1, 1)

# initialize model, loss and optimizer
model = torch.nn.Linear(n_features, 1, bias=False)
criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

# train the model
num_epochs = 300

def get_model_params(model):
    """Extract model params"""
    return torch.cat([param.data.squeeze().reshape(1, -1) for param in model.parameters()], dim=1)

stats_params = [get_model_params(model)]

for epoch in range(num_epochs):
    # Forward pass
    outputs = model(X)
    loss = criterion(outputs, y)

    # Backward pass and optimization
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # store model's parameters
    stats_params.append(get_model_params(model))

stats_params = pd.DataFrame(torch.cat(stats_params).detach().numpy(), columns=[f"weight {i} = {w}" for i, w in enumerate(true_weights, 1)])

stats_params.plot()
plt.grid(axis="y")
</code></pre>
<p>The evolution of the model weights:<br>
<img src="https://discuss.pytorch.org/uploads/default/original/3X/f/0/f091bf65fa494de23d1f81bc42698450d42cab1b.png" alt="output" data-base62-sha1="ykaR3IQOOhHiKP45bt8niHaZK4j" width="559" height="413"></p>
<p>Once again, one of them overshoots the optimum and this is at complete odds with my understanding of the gradient descent.</p>
<p>The trajectory of the model’s parameters during optimization, also very strange for me:<br>
<img src="https://discuss.pytorch.org/uploads/default/original/3X/4/d/4d1ccb0c84c9051c6ef34a61e9fe0bd8a2585241.png" alt="output" data-base62-sha1="b0asagNZr6vpT96PQsgayHUp3Tb" width="570" height="438"></p>
<p>This is what happens when we have more than two parameters (the model is still without bias and all weights are equal to 2) - half of the weights overshoot the optimum:<br>
<img src="https://discuss.pytorch.org/uploads/default/original/3X/8/b/8b9e4936b220c090a6252f331e18d0140fb79041.png" alt="output" data-base62-sha1="jV7uF6olDV2LAjgeT7grn7Hv5S1" width="559" height="413"></p>
<p>Note that in all cases the optimization finally converges to the true parameters and we get a correct model. However, I am puzzled by the behaviour of the optimizer and suspect an error in my code.</p>

username: J_Johnson
cooked: <p>When I mentioned f(x) = ax + b, I was referring to your original code, whereas a is the weight, b is the bias and x is the input.<br>
<div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/1/a/1a086c971cb2e42f0fc13721f32bb6f354d814fa.png" data-download-href="https://discuss.pytorch.org/uploads/default/1a086c971cb2e42f0fc13721f32bb6f354d814fa" title="image"><img src="https://discuss.pytorch.org/uploads/default/original/3X/1/a/1a086c971cb2e42f0fc13721f32bb6f354d814fa.png" alt="image" data-base62-sha1="3IisHBk3EkHaDk94bDb2BLZgUCS" width="690" height="151" data-dominant-color="F7F5F5"><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">806×177 9.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>
<p>That is what a linear layer simplifies to in the case of 1 input and 1 output with bias=True. How the weight and bias converge is different, and that is what your original post demonstrated.</p>
<p>Additionally, some overshooting is to be expected when you have more than 1 weight as they are interdependent. If we could from the loss, alone, know both the relative angle and the magnitude of where the minimum is in relation to the current weights, we would not need to iterate. Just one run of the data and we could determine the optimum weights exactly. However, gradient descent only determines the magnitude and the slope. Thus it’s typically not traversed in a straight line.</p>

username: cntl_cntl
cooked: <p>Oh, that’s true. I haven’t revisited multivariate calculus for a long time and was stuck with a wrong picture that a general quadratic surface has gradient pointing towards its minimum. Now everything has come into the order.</p>
<p>Thank’s for your replies!</p>