You might want to clamp the tensor inplace to avoid having to pass it to the optimizer repeatedly as seen here:
G = nn.Linear(1, 1)
# freeze
for param in G.parameters():
    param.requires_grad = False

z_initial = torch.randn(1, 1, requires_grad=True)
optimizer = torch.optim.Adam([z_initial], lr=1e-3)

for epoch in range(10):
    optimizer.zero_grad()
    gan_out = G(z_initial)

    loss = nn.MSELoss()(gan_out, torch.rand_like(gan_out))
    loss.backward()
    print('epoch {}, loss {}, z_initial {}, .grad {}'.format(
        epoch, loss.item(), z_initial, z_initial.grad))
    optimizer.step()
    with torch.no_grad():
        torch.clamp_(z_initial, -1., 1.)