<jupyter_start><jupyter_text>Automatic differentation using PyTorchWe show how to do Automatic differentation using PyTorch.<jupyter_code>import sklearn
import scipy
import scipy.optimize
import matplotlib.pyplot as plt
import itertools
import time
from functools import partial
import os

import numpy as np
from scipy.special import logsumexp

np.set_printoptions(precision=3)
import torch
import torch.nn as nn
import torchvision

print("torch version {}".format(torch.__version__))
if torch.cuda.is_available():
    print(torch.cuda.get_device_name(0))
    print("current device {}".format(torch.cuda.current_device()))
else:
    print("Torch cannot find GPU")

use_cuda = torch.cuda.is_available()
device = torch.device("cuda:0" if use_cuda else "cpu")<jupyter_output>torch version 1.8.0+cu101
Tesla P100-PCIE-16GB
current device 0<jupyter_text>Example: binary logistic regressionObjective = NLL for binary logistic regression<jupyter_code># Fit the model usign sklearn

import sklearn.datasets
from sklearn.model_selection import train_test_split

iris = sklearn.datasets.load_iris()
X = iris["data"]
y = (iris["target"] == 2).astype(np.int)  # 1 if Iris-Virginica, else 0'
N, D = X.shape  # 150, 4

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

from sklearn.linear_model import LogisticRegression

# We set C to a large number to turn off regularization.
# We don't fit the bias term to simplify the comparison below.
log_reg = LogisticRegression(solver="lbfgs", C=1e5, fit_intercept=False)
log_reg.fit(X_train, y_train)
w_mle_sklearn = np.ravel(log_reg.coef_)
print(w_mle_sklearn)<jupyter_output>[-4.414 -9.111  6.539 12.686]<jupyter_text>Computing gradients by hand<jupyter_code># Binary cross entropy
def BCE_with_logits(logits, targets):
    N = logits.shape[0]
    logits = logits.reshape(N, 1)
    logits_plus = np.hstack([np.zeros((N, 1)), logits])  # e^0=1
    logits_minus = np.hstack([np.zeros((N, 1)), -logits])
    logp1 = -logsumexp(logits_minus, axis=1)
    logp0 = -logsumexp(logits_plus, axis=1)
    logprobs = logp1 * targets + logp0 * (1 - targets)
    return -np.sum(logprobs) / N


# Compute using numpy
def sigmoid(x):
    return 0.5 * (np.tanh(x / 2.0) + 1)


def predict_logit(weights, inputs):
    return np.dot(inputs, weights)  # Already vectorized


def predict_np(weights, inputs):
    return sigmoid(predict_logit(weights, inputs))


def NLL(weights, batch):
    X, y = batch
    logits = predict_logit(weights, X)
    return BCE_with_logits(logits, y)


def NLL_grad(weights, batch):
    X, y = batch
    N = X.shape[0]
    mu = predict_np(weights, X)
    g = np.sum(np.dot(np.diag(mu - y), X), axis=0) / N
    return g
w_np = w_mle_sklearn
y_pred = predict_np(w_np, X_test)
loss_np = NLL(w_np, (X_test, y_test))
grad_np = NLL_grad(w_np, (X_test, y_test))
print("params {}".format(w_np))
# print("pred {}".format(y_pred))
print("loss {}".format(loss_np))
print("grad {}".format(grad_np))<jupyter_output>params [-4.414 -9.111 6.539 12.686]
loss 0.1182400709961879
grad [-0.235 -0.122 -0.198 -0.064]<jupyter_text>PyTorch code To compute the gradient using torch, we proceed as follows.- declare all the variables that you want to take derivatives with respect to using the requires_grad=True argumnet- define the (scalar output) objective function you want to differentiate in terms of these variables, and evaluate it at a point. This will generate a computation graph and store all the tensors.- call objective.backward() to trigger backpropagation (chain rule) on this graph.- extract the gradients from each variable using variable.grad field. (These will be torch tensors.)See the example below.<jupyter_code># data. By default, numpy uses double but torch uses float
X_train_t = torch.tensor(X_train, dtype=torch.float)
y_train_t = torch.tensor(y_train, dtype=torch.float)

X_test_t = torch.tensor(X_test, dtype=torch.float)
y_test_t = torch.tensor(y_test, dtype=torch.float)
# parameters
W = np.reshape(w_mle_sklearn, [D, 1])  # convert 1d vector to 2d matrix
w_torch = torch.tensor(W, requires_grad=True, dtype=torch.float)
# w_torch.requires_grad_()


# binary logistic regression in one line of Pytorch
def predict(X, w):
    y_pred = torch.sigmoid(torch.matmul(X, w))[:, 0]
    return y_pred


# This returns Nx1 probabilities
y_pred = predict(X_test_t, w_torch)

# loss function is average NLL
criterion = torch.nn.BCELoss(reduction="mean")
loss_torch = criterion(y_pred, y_test_t)
print(loss_torch)

# Backprop
loss_torch.backward()
print(w_torch.grad)

# convert to numpy. We have to "detach" the gradient tracing feature
loss_torch = loss_torch.detach().numpy()
grad_torch = w_torch.grad[:, 0].detach().numpy()
# Test
assert np.allclose(loss_np, loss_torch)
assert np.allclose(grad_np, grad_torch)

print("loss {}".format(loss_torch))
print("grad {}".format(grad_torch))<jupyter_output>loss 0.11824005842208862
grad [-0.235 -0.122 -0.198 -0.064]<jupyter_text>Autograd on a DNNBelow we show how to define more complex deep neural networks, and how to accesstheir parameters. We can then call backward() on the scalar loss function, and extract their gradients. We base our presentation on http://d2l.ai/chapter_deep-learning-computation/parameters.html.  Sequential models First we create a shallow MLP.<jupyter_code>torch.manual_seed(0)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))  # batch x Din, batch=2, Din=4
out = net(X)  # batch x Dout, Dout=1
print(out)<jupyter_output>tensor([[-0.2531],
        [-0.3098]], grad_fn=<AddmmBackward>)<jupyter_text>Let's visualize the model and all the parameters in each layer.<jupyter_code>print(net)
for i in range(3):
    print(f"layer {i}")
    print(net[i].state_dict())
print(*[(name, param.shape) for name, param in net.named_parameters()])<jupyter_output>('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))<jupyter_text>Access a specific parameter.<jupyter_code>print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

print(net.state_dict()["2.bias"].data)<jupyter_output><class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([-0.1568], requires_grad=True)
tensor([-0.1568])
tensor([-0.1568])<jupyter_text>The gradient is not defined until we call backward.<jupyter_code>net[2].weight.grad == None<jupyter_output><empty_output><jupyter_text>Nested models<jupyter_code>def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 4), nn.ReLU())


def block2():
    net = nn.Sequential()
    for i in range(4):
        # Nested here
        net.add_module(f"block {i}", block1())
    return net


rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
print(rgnet(X))
print(rgnet)<jupyter_output>tensor([[0.2138],
        [0.2138]], grad_fn=<AddmmBackward>)
Sequential(
  (0): Sequential(
    (block 0): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 1): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 2): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 3): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
  )
  (1): Linear(in_features=4, out_features=1, bias=True)
)<jupyter_text>Let us access the 0 element of the top level sequence,which is block 0-3. Then we access element 1 of this,which is block 1. Then we access element 0 of this, which is the first linear layer.<jupyter_code>rgnet[0][1][0].bias.data<jupyter_output><empty_output><jupyter_text>Backprop<jupyter_code># set loss function to output squared
out = rgnet(X)
loss = torch.mean(out**2, dim=0)

# Backprop
loss.backward()
print(rgnet[0][1][0].bias.grad)<jupyter_output>tensor([-6.0363e-05,  0.0000e+00,  0.0000e+00,  7.7047e-05,  0.0000e+00,
         5.7246e-05,  0.0000e+00,  0.0000e+00])<jupyter_text>Tied parametersSometimes parameters are reused in multiple layers, as we show below.In this case, the gradients are added.<jupyter_code># We need to give the shared layer a name so that we can refer to its
# parameters
torch.manual_seed(0)
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1))
net(X)
# Check whether the parameters are the same
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# Make sure that they are actually the same object rather than just having the
# same value
print(net[2].weight.data[0] == net[4].weight.data[0])<jupyter_output>tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])<jupyter_text>Other material- [Stackoverflow post on gradient accumulation](https://stackoverflow.com/questions/62067400/understanding-accumulated-gradients-in-pytorch) To compute gradient of a function that does not return a scalar(eg the gradient of each output wrt each input), you can do the following.<jupyter_code>x = torch.tensor([-2, -1, 0, 1, 2], dtype=float, requires_grad=True)
print(x)
y = torch.pow(x, 2)
print(y)
y.backward(torch.ones_like(x))
print(x.grad)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1376720901126408, 'fraction_numerical': 0.03900709219858156, 'mean_word_length': 4.985642946317103, 'pattern_counts': {'":': 0, '<': 46, '<?xml version=': 0, '>': 46, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '113820', 'license': 'MIT', 'n_tokens_mistral': 3715, 'n_tokens_neox': 3503, 'n_words': 901, 'path': 'notebooks/misc/autodiff_pytorch.ipynb', 'repo_name': 'karm-patel/pyprobml', 'size': '9588'}