<jupyter_start><jupyter_text>Escreva aqui seu nome:  PyTorch: Gradientes e Grafo Computacional  Objetivos Este notebook introduz - o conceito de autograd do PyTorch,- uma interpretação numérica intuitiva do gradiente, e o- grafo computacional, utilizado para o cálculo automático do gradiente de uma função. Um dos principais fundamentos para que o PyTorch seja adequado para deep learning é a sua habilidade decalcular o gradiente automaticamente a partir da expressões definidas. Essa facilidade é implementadapelo tensor através do cálculo automático do gradiente pela construção dinâmica do grafo computacional.  Grafo computacional ```    y_pred = x * w    e = y_pred - y    e2 = e**2    J = e2.sum()```  Para conhecer com maior profundidade a diferenciação automática usando grafo computacional, veja esta nota de aula:https://cs231n.github.io/optimization-2/<jupyter_code>import torch
torch.__version__<jupyter_output><empty_output><jupyter_text>Se um tensor possui .requires_grad=True<jupyter_code>y = 2 * torch.arange(0,4).float()
y
x = torch.arange(0,4).float(); x
w = torch.ones(1,requires_grad=True); w<jupyter_output><empty_output><jupyter_text>Cálculo automático do gradiente da função perda J Seja a expressão: $$ J = \sum ((x  w) - y)^2 $$Queremos calcular a derivada de $J$ em relação a $w$.  Montagem do grafo computacional<jupyter_code># predict (forward)
y_pred = x * w

# cálculo da perda J: loss
e = y_pred - y
e2 = e.pow(2)
J = e2.sum()
J<jupyter_output><empty_output><jupyter_text>Auto grad - processa o grafo computacional backwards O `backward()` varre o grafo computacional a partir da variável a ele associada e calcula o gradiente para todos os tensores que possuem o atributo `requires_grad` como verdadeiro.O `backward()` destroi o grafo após sua execução. Isso é intrínsico ao PyTorch pelo fato dele ser uma rede dinâmica.<jupyter_code>J.backward()
print(w.grad)
w.grad.data.zero_();<jupyter_output><empty_output><jupyter_text>Interpretação do Gradiente O gradiente de uma variável final (J) com respeito à outra variável de entrada (w) pode ser interpretado como o quanto a variável final J vai aumentar se houver um pequeno aumento na variável de entrada (w).Por exemplo suponha que o gradiente seja 28. Isto significa se aumentarmos a variável w de 0.001, então J vai aumentar de 0.028.<jupyter_code>eps = 0.001
y_pred = x * (w + eps)
J_new = (y_pred - y).pow(2).sum()
J_new
print(J_new - J)<jupyter_output>tensor(-0.0280, grad_fn=<SubBackward0>)<jupyter_text>Backpropagation Uma forma equivalente explícita de calcular o gradiente é fazendo o processamento do backpropagation no grafo computacional, de forma explícita.Apenas como ilustração.<jupyter_code>import numpy as np

dJ = 1.
de2 = dJ * np.ones((4,))
de = de2 * 2 * e.data.numpy()
dy_pred = de
dw = (dy_pred * x.data.numpy()).sum()
print(dJ)
print(de2)
print(de)
print(dw)<jupyter_output>1.0
[1. 1. 1. 1.]
[ 0. -2. -4. -6.]
-28.0<jupyter_text>Visualizando o grafo computacional<jupyter_code>!pip install torchviz
import torchviz
J = ((w * x) - y).pow(2).sum()
p = {'w':w} # dicionário de parâmetros
out = torchviz.make_dot(J,params=p)
out<jupyter_output><empty_output><jupyter_text>Iremos agora visualizar a Resnet, que é uma rede neural bastante popular em visão computacional.<jupyter_code>import torch
model = torch.hub.load('pytorch/vision:v0.9.0', 'resnet18', pretrained=True)
x_temp = torch.randn(1,3,224,224)  # First, create a random image.
y_temp = model(x_temp)  # We need one forward pass so the graph can be build.
out = torchviz.make_dot(y_temp, params=dict(model.named_parameters()))  # Create a figure from the computaional graph..
torchviz.dot.resize_graph(out, size_per_element=0.05)  # Resize to fit on the screen.
out<jupyter_output>Downloading: "https://github.com/pytorch/vision/archive/v0.9.0.zip" to /root/.cache/torch/hub/v0.9.0.zip
Downloading: "https://download.pytorch.org/models/resnet18-5c106cde.pth" to /root/.cache/torch/hub/checkpoints/resnet18-5c106cde.pth<jupyter_text>Exercício 1O que acontece com o grafo computacional após execução do `backward()`? Resposta:  Exercício 2Execute um passo de atualização do valor de w, pelo gradiente descendente. Utilize um fator de aprendizado (*learning rate*) de 0.01 para atualizar o `w`. Após, recalcule a função de perda:    - w = w - lr * w.grad.data    - Verifique o quanto que a perda J diminuiu<jupyter_code>raise Exception("Escreva seu código aqui")
eps = 0.001
y_pred = x * (w + eps)
J_new = (y_pred - y).pow(2).sum()
J_new<jupyter_output><empty_output><jupyter_text>Treinando uma rede no PytorchPara ajudar na entendimento dos exercícios abaixo, apresentamos o código em Pytorch para treinar uma rede de uma camada não-linear, com pesos `w` e `b`:$y' = \sigma(wx + b)$<jupyter_code># É importante fixar as seeds para passar nos asserts abaixo.

import random
import numpy as np
import torch

random.seed(123)
np.random.seed(123)
torch.manual_seed(123)
from typing import List

class NonLinearPytorch(torch.nn.Module):
    def __init__(self):
        super(NonLinearPytorch, self).__init__()

        self.layer1 = torch.nn.Linear(1, 1)
        # Inicializa os pesos w e b em zero.
        self.layer1.load_state_dict(dict(weight=torch.zeros(1,1), bias=torch.zeros(1)))
        
    def forward(self, x):
        y_pred = torch.sigmoid(self.layer1(x))
        return y_pred

learning_rate = 0.1
model = NonLinearPytorch()
loss_fn = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
x = torch.tensor([-5], dtype=torch.float)
y_target = torch.tensor([0.76], dtype=torch.float)
num_iterations = 50
for i in range(num_iterations):
    # Zere os gradientes dos passo anterior.
    optimizer.zero_grad()
    # Rode a um passo forward do modelo.
    y_pred = model.forward(x)
    # Calcule a loss
    loss = loss_fn(y_pred, y_target)
    # Calcule os gradientes
    loss.backward()
    # Atualize os pesos
    optimizer.step()

    print(f'iter:{i}: y_prime: {y_pred}')<jupyter_output><empty_output><jupyter_text>Exercício 3Vamos agora escrever nosso próprio código para calcular os gradientes da rede apresentada acima.Para tanto, temos que primeiro implementar a classe Tensor, que é parecida com a classe Tensor do pytorch. Quando instanciada, o objeto resultante armazena o valor do tensor e uma referencia para o nó do grafo computacional que gerou os valores do tensor, quando houver. Com isso podemos construir percorrer o grafo computacional no sentido reverso, realizando o passo `backward` do algoritmo de backpropagation.Para simplificar a implementação, o gradiente também pode ser armazenado nesta variável. Com isso, podemos tratar os pesos da rede como Tensor's. Isso dispensa a necessidade de criarmos a classe Parameters, como é feito no pytorch.<jupyter_code>class Tensor():
    def __init__(self, data: float, previous_node=None):
        raise Exception("Escreva seu código aqui")

    def backward(self, upstream_grad: float = None):
        # Esta vai ser uma chamada recursiva cujo critéria de parada é quando previous_node==None.
        raise Exception("Escreva seu código aqui")
        

    # Implementar as funções abaixo é opcional.
    def __add__(self, other: 'Tensor'):
        raise Exception("Escreva seu código aqui")

    def __sub__(self, other: 'Tensor'):
        raise Exception("Escreva seu código aqui")

    def __mul__(self, other: 'Tensor'):
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>A seguir, implementaremos as funções `forward` e `backward` de cada um dos nós do grafo acima, além do nó de subtração, que é usado pela função de custo.Vamos começar pelo nó da função sigmoid ($\sigma$), cuja derivada é:$\frac{\delta\sigma}{\delta x} = \sigma(x)(1-\sigma(x))$<jupyter_code>class SigmoidNode():
    def forward(self, x: Tensor):
        raise Exception("Escreva seu código aqui")

    def backward(self, upstream_grad: float):
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Implementamos agora o `forward` e `backward` do nó da soma $z = x + y$, cujas derivadas parciais em relação a cada entrada $x$ e $y$ são:$\frac{\delta z}{\delta x} = 1$$\frac{\delta z}{\delta y} = 1$<jupyter_code>class AddNode():
    def forward(self, x: Tensor, y: Tensor):
        raise Exception("Escreva seu código aqui")
      
    def backward(self, upstream_grad: float):
        # A operação de soma tem o objetivo de ser um "distribuidor", logo a derivada de z /derivada de x = 1 
        # basta então, multiplicar 1 (gradiente local) pelo gradiente da frente.
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Implementamos agora o `forward` e `backward` do nó da subtração $z = x - y$, cujas derivadas parciais em relação a cada entrada $x$ e $y$ são:$\frac{\delta z}{\delta x} = 1$$\frac{\delta z}{\delta y} = -1$<jupyter_code>class SubNode():
    def forward(self, x: Tensor, y: Tensor):
        raise Exception("Escreva seu código aqui")
      
    def backward(self, upstream_grad: float):
        # A operação de soma tem o objetivo de ser um "distribuidor", logo a derivada de z /derivada de x = 1 
        # basta então, multiplicar 1 (gradiente local) pelo gradiente da frente.
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Implementamos agora o `forward` e `backward` do nó da multiplicação $z = xy$, cujas derivadas parciais em relação a cada entrada $x$ e $y$ são:$\frac{\delta z}{\delta x} = y$$\frac{\delta z}{\delta y} = x$<jupyter_code>class MulNode():
    def forward(self, x: Tensor, y: Tensor):
        raise Exception("Escreva seu código aqui")
    
    def backward(self, upstream_grad: float):
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Agora que temos todos os nós implementados, podemos implementar as funções `forward` e `backward` de uma camada não-linear.<jupyter_code>class NonLinear():
    def __init__(self):
        raise Exception("Escreva seu código aqui")

    def forward(self, x: Tensor):
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Para treinar esta rede, usaremos a função de custo Mean Squared Error (MSE):$L = (y_\text{pred} - y_\text{target})^2$Como por simplicidade optamos por não criar o nó de exponenciação, iremos substituir a operação de elevar ao quadrado pela multiplicacão das diferenças:$L = (y_\text{pred} - y_\text{target}) * (y_\text{pred} - y_\text{target})$<jupyter_code>def compute_loss(y_target: Tensor, y_pred: Tensor):
    raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Também precisamos criar a classe do optimizador SGD, para atualizar os pesos da rede.<jupyter_code>class SGD():
    def __init__(self, parameters: List[Tensor], learning_rate: float):
        raise Exception("Escreva seu código aqui")

    def step(self):
        raise Exception("Escreva seu código aqui")

    def zero_grad(self):
        raise Exception("Escreva seu código aqui")<jupyter_output><empty_output><jupyter_text>Por fim, vamos aprender os pesos `w` e `b` para mapear um valor de entrada $x$ para um valor de saída $y_\text{target}$. Para isso, inicializamos o grafo da rede e rodamos o laço de optimização, que vai aplicar a descida do gradiente a cada iteração:<jupyter_code>learning_rate = 0.1

model = NonLinear()
optimizer = SGD(parameters=[model.w, model.b], learning_rate=learning_rate)
x = Tensor(-5)
y_target = Tensor(0.76)
num_iterations = 50
for i in range(num_iterations):
    # Zera os gradientes dos passo anterior.
    optimizer.zero_grad()
    # Roda a um passo forward do modelo.
    y_pred = model.forward(x)
    # Calcula o gradiente do erro, data a predição do modelo.
    loss_grad = compute_loss(y_target=y_target, y_pred=y_pred)
    # Calcula agora os gradientes de w e b usando a função backward do modelo.
    loss_grad.backward()
    # Atualiza os pesos w e b usando os seus respectivos gradientes.
    optimizer.step()
    print(f'iter:{i}: y_prime: {y_pred.data}')<jupyter_output><empty_output><jupyter_text>Exercício 4Repita o exercício 3 mas usando uma rede com duas camadas não-lineares:$a = \sigma(w_1x + b_1)$$y' = \sigma(w_2a + b_2)$<jupyter_code>class Net():
    def __init__(self):
        raise Exception("Escreva seu código aqui")

    def forward(self, x: Tensor):
        raise Exception("Escreva seu código aqui")
learning_rate = 1.0

model = Net()
optimizer = SGD(parameters=[model.layer1.w, model.layer1.b, model.layer2.w, model.layer2.b], learning_rate=learning_rate)
x = Tensor(-5)
y_target = Tensor(0.76)
num_iterations = 50
for i in range(num_iterations):
    # Zera os gradientes dos passo anterior.
    optimizer.zero_grad()
    # Roda a um passo forward do modelo.
    y_pred = model.forward(x)
    # Calcula o gradiente do erro, data a predição do modelo.
    loss_grad = compute_loss(y_target=y_target, y_pred=y_pred)
    # Calcula agora os gradientes de w e b usando a função backward do modelo.
    loss_grad.backward()
    # Atualiza os pesos w e b usando os seus respectivos gradientes.
    optimizer.step()
    print(f'iter:{i}: y_prime: {y_pred.data}')<jupyter_output><empty_output><jupyter_text>Exercício 5Repita o exercício 4 mas compartilhando os pesos das duas camadas não-lineares. Mostre que ambas tem os mesmos pesos após o treino.<jupyter_code>class NetShared():
    def __init__(self):
        raise Exception("Escreva seu código aqui")
        
    def forward(self, x: Tensor):
        raise Exception("Escreva seu código aqui")
learning_rate = 1.0

model = NetShared()
optimizer = SGD(parameters=[model.layer1.w, model.layer1.b, model.layer2.w, model.layer2.b], learning_rate=learning_rate)
x = Tensor(-5)
y_target = Tensor(0.76)
num_iterations = 50
for i in range(num_iterations):
    # Zera os gradientes dos passo anterior.
    optimizer.zero_grad()
    # Roda a um passo forward do modelo.
    y_pred = model.forward(x)
    # Calcula o gradiente do erro, data a predição do modelo.
    loss_grad = compute_loss(y_target=y_target, y_pred=y_pred)
    # Calcula agora os gradientes de w e b usando a função backward do modelo.
    loss_grad.backward()
    # Atualiza os pesos w e b usando os seus respectivos gradientes.
    optimizer.step()
    print(f'iter:{i}: y_prime: {y_pred.data}')
assert model.layer1.w.data == model.layer2.w.data
assert model.layer1.b.data == model.layer2.b.data<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10166218359886889, 'fraction_numerical': 0.01255258983378164, 'mean_word_length': 4.420560747663552, 'pattern_counts': {'":': 0, '<': 83, '<?xml version=': 0, '>': 83, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '21', 'dir': 'jupyter-structured-clean-dedup', 'id': '208800', 'license': 'MIT', 'n_tokens_mistral': 5243, 'n_tokens_neox': 5080, 'n_words': 1568, 'path': 'ex03/rodrigo_cabrera_castaldoni.ipynb', 'repo_name': 'rocabrera/IA025A_2022S1', 'size': '14499'}