<jupyter_start><jupyter_text>AMAL - TP 2 - Graphes de calcul, autograd et modules L'objectif de ce TP est d'automatiser les fonctions codées à la main lors du TP 1. On le fait en 3 étapes :+ 1. Calcul automatique du gradient par autograd+ 2. Descente de gradient automatique avec la classe `torch.optim`+ 3. Modules préprogrammés et conteneurs Dans ce notebook, on implémente le point 2. La partie intéressante se situe dans la fonction `run_gradient_descent`, en particulier les lignes suivantes```pythonoptim = torch.optim.SGD(params=[w,b],lr=eps)...optim.step()optim.zero_grad()```<jupyter_code>import numpy as np
import torch
from torch.autograd import Function
from torch.autograd import gradcheck
from datamaestro import prepare_dataset 
from torch.utils.tensorboard import SummaryWriter
import time<jupyter_output>WARNING:root:This caffe2 python run does not have GPU support. Will run in CPU only mode.<jupyter_text>Implémentation & test des classes Pytorch<jupyter_code>class Context:
    """Very simplified context object"""
    def __init__(self):
        self._saved_tensors = ()
    def save_for_backward(self, *args):
        self._saved_tensors = args
    @property
    def saved_tensors(self):
        return self._saved_tensors<jupyter_output><empty_output><jupyter_text>On commence par implémenter le cas unidimensionnel avant d'implémenter le cas multi-dimensionnel. Pour chaque classe, on la teste juste après sa définition.<jupyter_code>class Linear1(Function):
    """Implementation of f(x, w, b) = <x, w> + b"""
    @staticmethod
    def forward(ctx,x, w, b):
        ## Calcul la sortie du module
        ctx.save_for_backward(x,w)
        return torch.dot(x, w) + b

    @staticmethod
    def backward(ctx, grad_output):
        ## Calcul du gradient du module par rapport a chaque groupe d'entrées
        x,w = ctx.saved_tensors
        return grad_output * w , grad_output * x, grad_output
def test_linear1():
    ## test de la fonction
    estimator = Linear1()
    ctx_estimator = Context()
    x = torch.randn(5,requires_grad=True,dtype=torch.float64)
    w = torch.randn(5,requires_grad=True,dtype=torch.float64)
    b = torch.randn(1,requires_grad=True,dtype=torch.float64)
    output = estimator.forward(ctx_estimator,x,w, b)

    estimator_grad = estimator.backward(ctx_estimator,1)

    ## Pour tester le gradient 
    estimator_check = Linear1.apply
    return torch.autograd.gradcheck(estimator_check,(x,w, b))

test_linear1()
class LinearBatch(Function):
    """Implementation of f(X, w, b) = X.dot(w) + b"""
    @staticmethod
    def forward(ctx, X, w, b):
        ## Calcul la sortie du module
        ctx.save_for_backward(X,w)
        return torch.mm(X, torch.transpose(w, 0, 1)) + b

    @staticmethod
    def backward(ctx, grad_output):
        ## Calcul du gradient du module par rapport a chaque groupe d'entrées
        X, w = ctx.saved_tensors
        return torch.mm(grad_output, w) , torch.mm(torch.transpose(grad_output, 0, 1), X), torch.sum(grad_output, 0)
b_size = 10
def test_linearBatch():
    ## Test de la fonction 
    estimator = LinearBatch()
    ctx_estimator = Context()
    x = torch.randn(b_size, 5,requires_grad=True,dtype=torch.float64)
    w = torch.randn(1, 5,requires_grad=True,dtype=torch.float64)
    b = torch.randn(1,requires_grad=True,dtype=torch.float64)
    output = estimator.forward(ctx_estimator,x,w, b)

    grad_output = torch.randn(b_size, 1,requires_grad=True,dtype=torch.float64)
    estimator_grad = estimator.backward(ctx_estimator, grad_output)

    ## Pour tester le gradient 
    estimator_check = LinearBatch.apply
    return torch.autograd.gradcheck(estimator_check,(x,w, b))

test_linearBatch()
class MSE(Function):
    """Implementation of f(yhat, y) = ||yhat-y||^2"""
    @staticmethod
    def forward(ctx, yhat, y):
        ## Calcul la sortie du module
        ctx.save_for_backward(yhat,y)
        return torch.norm(yhat-y, p=2)**2 / len(y)

    @staticmethod
    def backward(ctx, grad_output):
        ## Calcul du gradient du module par rapport a chaque groupe d'entrées
        yhat,y = ctx.saved_tensors
        return 2 * grad_output * (yhat-y)/ len(y) ,  -2 * grad_output * (yhat-y)  / len(y)
def test_MSE():
    mse = MSE()
    ctx_mse = Context()
    y = torch.randn(2,requires_grad=True,dtype=torch.float64)
    yhat = torch.randn(2,requires_grad=True,dtype=torch.float64)
    output = mse.forward(ctx_mse,yhat,y)
    mse_grad = mse.backward(ctx_mse,1)
    ## Pour tester le gradient 
    mse_check = mse.apply
    return torch.autograd.gradcheck(mse_check,(yhat, y))

test_MSE()<jupyter_output><empty_output><jupyter_text>Implémentation de l'algorithme de descente de gradient La fonction `run_gradient_descent` permet d'implémenter les trois variantes de descente de gradient :+ Gradient batch: `n_batch` = 1+ Gradient stochastique: `n_batch` = N où N est le nombre de lignes de données+ Gradient mini-batch: 1 < `n_batch` < N<jupyter_code>def run_gradient_descent(data, nb_iteration, n_batch, writer=False):
    
    N = data.shape[0]   # nombre de données
    n_attribute = data.shape[1]-1
    
    if writer:
        writer = SummaryWriter(log_dir='runs/nb_batch=' + str(n_batch) + 'nb_pass=' + str(nb_iteration/n_batch) + '_timestamp=' + str(int(time.time())))
        
    # Initialisation aléatoire des paramètres w et b
    w = torch.nn.Parameter(torch.randn(1, n_attribute,dtype=torch.float64))
    b = torch.nn.Parameter(torch.randn(1,dtype=torch.float64))
    
    eps = 0.05 # Pas de la descente de gradient
    optim = torch.optim.SGD(params=[w,b],lr=eps)
    
    estimator = LinearBatch()
    ctx_estimator = Context()
    mse = MSE()
    ctx_mse = Context()

    L_train = 0     # perte pour la partie train
    for k in range(nb_iteration):
        
        # Choix des lignes des données qui servent pour cette itération, parmi la première moitié des données
        if n_batch == N: # stochastic 
            idx = [np.random.randint(0, N/2)]
        else:   # batch or mini-batch
            idx_min = int(N/n_batch * k) % N
            idx_max = int(N/n_batch * (k+1)) % N
            if idx_min < idx_max:
                idx = range(idx_min, idx_max)
            else:
                idx = list(range(idx_min, N)) + list(range(0, idx_max))
            
        X = torch.from_numpy(data[idx, :-1])
        y = torch.from_numpy(data[idx][: , [-1]])
        loss = mse.forward(ctx_mse, estimator.forward(ctx_estimator, X, w, b), y)
        L_train = (k*L_train + loss) / (k+1)    # update L_train
        loss.backward()
        optim.step()
        optim.zero_grad()
    
        if k % 10 == 0 and writer:
            # Pour comparer les différente méthodes, on compte le nombre de lignes de données utilisées à chaque itération
            writer.add_scalar('Loss/train', L_train/len(idx), k*len(idx))

    with torch.no_grad():
        L_test = 0  # perte pour la partie test
        for k in range(int(N/2)):     # on teste sur la seconde moitié du data set
            x = torch.from_numpy(data[[int(N/2) + k], :-1])
            y = torch.from_numpy(data[[int(N/2) + k]][: , [-1]])
            L_test = (k*L_test + mse.forward(ctx_mse, estimator.forward(ctx_estimator, x, w, b), y)) / (k+1)
            if writer:
                writer.add_scalar('Loss/test', L_test, k)
    
    print('Test loss: ' + str(L_test.item()))
    if writer:
        writer.close()<jupyter_output><empty_output><jupyter_text>Test avec les données de Boston Housing<jupyter_code>## Pour telecharger le dataset Boston
ds=prepare_dataset("edu.uci.boston")
fields, data =ds.files.data() 
N = data.shape[0]
# Pour randomizer les données et diminuer le conditionnement
np.random.shuffle(data)
data = data / data.max(axis=0)
# On compare les trois variantes (batch / stochastic / mini-batch) en utilisant à chaque fois au total `n_pass` fois les données
# Le nombre d'itérations n'est donc pas le même selon les variantes
n_pass = 100
run_gradient_descent(data, n_pass, 1, writer=True)    # batch
run_gradient_descent(data, n_pass*N, N, writer=True)  # stochastic
n_batch = 10
run_gradient_descent(data, n_pass*n_batch, n_batch, writer=True)   # mini-batch<jupyter_output>Test loss: 0.07727138162464059
Test loss: 0.009941845629447284
Test loss: 0.014954277493030738
{'fraction_non_alphanumeric': 0.11214501510574018, 'fraction_numerical': 0.01824773413897281, 'mean_word_length': 3.4209401709401708, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 20, 'https://': 0, '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': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '428914', 'license': 'MIT', 'n_tokens_mistral': 2975, 'n_tokens_neox': 2892, 'n_words': 730, 'path': 'AMAL/week2/td2_2_optimiseur.ipynb', 'repo_name': 'PierreMarion23/ML_exercises', 'size': '8275'}