<jupyter_start><jupyter_text>$\newcommand{\xbf}{{\bf x}}\newcommand{\ybf}{{\bf y}}\newcommand{\wbf}{{\bf w}}\newcommand{\Ibf}{\mathbf{I}}\newcommand{\Xbf}{\mathbf{X}}\newcommand{\Rbb}{\mathbb{R}}\newcommand{\vec}[1]{\left[\begin{array}{c}1\end{array}\right]}$ Introduction aux réseaux de neurones : TD 1  (partie 1)Matériel de cours rédigé par <NAME>, 2018************<jupyter_code>%matplotlib inline
from matplotlib import pyplot as plt
import numpy as np
from math import sqrt
import aidecours<jupyter_output><empty_output><jupyter_text>La libraire pyTorchhttps://pytorch.org/<jupyter_code>import torch<jupyter_output><empty_output><jupyter_text>Les tenseurs<jupyter_code>torch.tensor?<jupyter_output><empty_output><jupyter_text>Un tenseur peut contenir un scalaire<jupyter_code>a = torch.tensor(1.5)
a
a + 2
a.item()<jupyter_output><empty_output><jupyter_text>Les tenseurs contenant des vecteurs ou des matrices se comportent similairement aux *array numpy*.<jupyter_code>v = torch.tensor([1,2,3])
v
torch.sum(v)
u = torch.tensor([1.,2.,3.])
u
torch.log(u)
u[0]
u[1:]
np.array(u)
M = torch.tensor([[1.,2.,3.], [4, 5, 6]])
M
M.shape
2 * M + 1
M @ u
torch.ones((2, 3))
torch.zeros((3, 2))
torch.rand((3, 4))
torch.randn((3, 4))<jupyter_output><empty_output><jupyter_text>**ATTENTION:** Les *tenseurs pyTorch* sont plus capricieux sur le type des variables que les *array numpy*.<jupyter_code>v = np.array([.3, .6, .9])
v.dtype
w = np.array([-1, 3, 8])
w.dtype
v_tensor = torch.from_numpy(v)
v_tensor.dtype
w_tensor = torch.from_numpy(w)
w_tensor.dtype
print('v:', v.dtype)
print('w:', w.dtype)

result = v @ w
print('v @ w:', result.dtype)
result
print('v_tensor:', w_tensor.dtype)
print('w_tensor:', v_tensor.dtype)
result = v_tensor @ w_tensor
print('v_tensor @ w_tensor:', result.dtype)
w_tensor = torch.tensor(w, dtype=torch.float64)
w_tensor
print('v_tensor:', v_tensor.dtype)
print('w_tensor:', w_tensor.dtype)
result = v_tensor @ w_tensor
print('v_tensor @ x_tensor:', result.dtype)<jupyter_output><empty_output><jupyter_text>Dérivation automatique Lors de l'initialisation d'un tenseur, l'argument `requires_grad=True` indique que nous désirons calculer le gradient des variables contenues dans le tenseur.<jupyter_code>x = torch.tensor(3., requires_grad=True)<jupyter_output><empty_output><jupyter_text>Le graphe de calcul est alors bâti au fur et à mesure des opérations impliquant les tenseurs.<jupyter_code>F = x ** 2<jupyter_output><empty_output><jupyter_text>La fonction `F.backward()` parcours le graphe de calcul en sens inverse et calcule le gradient de la fonction $F$ selon les variables voulues.<jupyter_code>F.backward()<jupyter_output><empty_output><jupyter_text>Après avoir exécuté la fonction `backward()`, l'attribut `grad` des tenseurs impliqués dans le calcul contient la valeur du gradient calculé au point courant. Ici, on aura la valeur :$$\left[\frac{\partial F(x)}{\partial x}\right]_{x=3} = \big[\,2\,x\,\big]_{x=3} = 6$$<jupyter_code>x.grad<jupyter_output><empty_output><jupyter_text>Illustrons le fonctionnement de la dérivation par quelques autres exemples<jupyter_code>x = torch.linspace(-1, 1, 11, requires_grad=True)
x
quad = x @ x
quad
quad.backward()
x.grad
a = torch.tensor(-3., requires_grad=True)
b = torch.tensor(2., requires_grad=True)
m = a*b
m.backward()
print('a.grad =', a.grad)
print('b.grad =', b.grad)
a = torch.tensor(-3., requires_grad=True)
b = torch.tensor(2., requires_grad=True)
m = 2*a + b
m.backward()
print('a.grad =', a.grad)
print('b.grad =', b.grad)
a = torch.tensor(3., requires_grad=True)
b = torch.tensor(2., requires_grad=False)
m = a ** b
m.backward()
print('a.grad =', a.grad)
print('b.grad =', b.grad)
a = torch.tensor(-3., requires_grad=True)
b = torch.tensor(2., requires_grad=True)
c = torch.tensor(4., requires_grad=True)
m1 = (a + b)
m2 = m1 * c
m2.backward()
print('a.grad =', a.grad)
print('b.grad =', b.grad)
print('c.grad =', c.grad)
vecteur_a = torch.tensor([-1., 2, 3], requires_grad=True)
vecteur_b = torch.ones(3, requires_grad=True)
produit = vecteur_a @ vecteur_b
produit.backward()
print('vecteur_a =', vecteur_a, '; vecteur_a.grad =', vecteur_a.grad)
print('vecteur_b =', vecteur_b, '; vecteur_b.grad =', vecteur_b.grad)
print('produit =', produit.item())
vecteur_a = torch.tensor([1., 4, 9], requires_grad=True)
result = torch.sum(torch.sqrt(vecteur_a))
result.backward()
print('vecteur_a =', vecteur_a, '; vecteur_a.grad =', vecteur_a.grad)
print('result =', result.item())<jupyter_output><empty_output><jupyter_text>Descente de gradient Commencons par un exemple en une dimension.$$f(x) = x^2 - x + 3$$<jupyter_code>def fonction_maison(x):
    return x**2 - x + 3

x = np.linspace(-2, 2)
plt.plot(x, fonction_maison(x) )
plt.plot((.5,),(fonction_maison(.5)), 'r*');
eta = .4 # Pas de gradient
T =  20 # Nombre d'itérations

# Initialisation aléatoire 
x = torch.randn(1, requires_grad=True)

for t in range(T):
 
    # Calcul de la fonction objectif
    val = fonction_maison(x)
    
    # Calcul des gradients
    val.backward()
    
    print('Interation', t+1, ': x =', x.item(), '; f(x) =', val.item(), '; f\'(x) =', x.grad.item())
    
    # Mise à jour de la variable x
    with torch.no_grad():
        x -= eta * x.grad
    
    # Remise à zéro du gradient
    x.grad.zero_()<jupyter_output><empty_output><jupyter_text>Reprenons l'exemple des moindres carrés présentés dans les transparents du cours.$$\min_\wbf \left[\frac1n \sum_{i=1}^n (\wbf\cdot\xbf_i- y_i)^2\right].$$<jupyter_code>def moindre_carres_objectif(x, y, w): 
    return np.mean((x @ w - y) ** 2)
x = np.array([(1,1), (0,-1), (2,.5)])
y = np.array([-1, 3, 2])
fonction_objectif = lambda w: moindre_carres_objectif(x, y, w)
aidecours.show_2d_function(fonction_objectif, -5, 5, .5)
w_opt = np.linalg.inv(x.T @ x) @ x.T @ y

aidecours.show_2d_function(fonction_objectif, -5, 5, .5, optimal=w_opt)<jupyter_output><empty_output><jupyter_text>Nous créons une classe `moindre_carres` avec un fonctionnement semblable aux algorithmes de *scikit-learn* qui résout le problème des moindres carrés par descente de gradient, en utilisant les fonctionnalités de *pyTorch*<jupyter_code>class moindre_carres:
    def __init__(self, eta=0.4, nb_iter=50, seed=None):
        self.eta=eta
        self.nb_iter=nb_iter
        self.seed = seed
        
    def fit(self, x, y):
        if self.seed is not None:
            torch.manual_seed(seed)
        
        x = torch.tensor(x, dtype=torch.float32)
        y = torch.tensor(y, dtype=torch.float32) 

        n, d = x.shape
        self.w = torch.randn(d, requires_grad=True)
        
        self.w_list = list() # Servira à garder une trace de la descente de gradient
        self.obj_list = list()
    
        for t in range(self.nb_iter+1):
            
            loss = torch.mean((x @ self.w - y) ** 2)
            
            self.w_list.append(np.array(self.w.detach()))
            self.obj_list.append(loss.item()) 
            if t == self.nb_iter: break 
        
            with torch.no_grad():
                loss.backward()
                self.w -= self.eta * self.w.grad
                
            self.w.grad.zero_()
                
    def predict(self, x):
        x = torch.tensor(x, dtype=torch.float32)
        pred = x @ self.w.detach()
        return pred.numpy()<jupyter_output><empty_output><jupyter_text>Exécution de l'algorithme.<jupyter_code>eta = 0.4     # taille du pas
nb_iter = 20  # nombre d'itérations

algo = moindre_carres(eta, nb_iter)
algo.fit(x, y)
fig, axes = plt.subplots(1, 2, figsize=(14.5, 4))
aidecours.sgd_trajectoire(algo.w_list, fonction_objectif, w_opt=w_opt, ax=axes[0])
aidecours.sgd_courbe_objectif(algo.obj_list, ax=axes[1], obj_opt=fonction_objectif(w_opt))<jupyter_output><empty_output><jupyter_text>ExerciceDans cet exercice, nous vous demandons de vous inspirer de la classe `moindre_carrees` ci-haut et de l'adapter au problème de la régression logistique présenté dans les transparents du cours.<jupyter_code>from sklearn.datasets import make_blobs
xx, yy = make_blobs(n_samples=100, centers=2, n_features=2, cluster_std=1, random_state=0)

aidecours.show_2d_dataset(xx, yy)<jupyter_output><empty_output><jupyter_text>Illustrons la fonction à optimiser (avec $\lambda=0.01$):    $$\frac1n \sum_{i=1}^n - y_i \wbf\cdot\xbf_i + \log(1+e^{\wbf\cdot\xbf_i})+ \frac\rho2\|\wbf\|^2\,.$$<jupyter_code>def sigmoid(x):
    return 1 / (1+np.exp(-x))

def calc_perte_logistique(w, x, y, rho):
    pred = sigmoid(x @ w)
    pred[y==0] = 1-pred[y==0]
    return np.mean(-np.log(pred)) + rho*w @ w/2

fct_objectif = lambda w: calc_perte_logistique(w, xx, yy, 0.01)
aidecours.show_2d_function(fct_objectif, -4, 4, .05)<jupyter_output><empty_output><jupyter_text>Compléter le code de la classe suivante.<jupyter_code>class regression_logistique:
    def __init__(self, rho=.01, eta=0.4, nb_iter=50, seed=None):
        self.rho = rho
        self.eta = eta
        self.nb_iter = nb_iter
        self.seed = seed
        
    def fit(self, x, y):
        if self.seed is not None:
            torch.manual_seed(seed)
        
        x = torch.tensor(x, dtype=torch.float32)
        y = torch.tensor(y, dtype=torch.float32) 

        n, d = x.shape
        self.w = torch.randn(d, requires_grad=True)
        
        self.w_list = list() # Servira à garder une trace de la descente de gradient
        self.obj_list = list()
    
        for t in range(self.nb_iter+1):
            pass # Compléter
                
    def predict(self, x):
        x = torch.tensor(x, dtype=torch.float32)
        pred = x @ self.w.detach()
        return np.array(pred.numpy() > .5, dtype=np.int)<jupyter_output><empty_output><jupyter_text>Exécuter le code suivant pour vérifier le bon fonctionnement de votre algorithme. Essayer ensuite de varier les paramètres `rho`, `eta` et `nb_iter` afin d'évaluer leur impact sur le résultat obtenu.<jupyter_code>rho = 0.01
eta = 0.4     # taille du pas
nb_iter = 20  # nombre d'itérations

algo = regression_logistique(rho, eta, nb_iter)
algo.fit(xx, yy)

fig, axes = plt.subplots(1, 3, figsize=(16, 4))
aidecours.sgd_trajectoire(algo.w_list, fct_objectif, -4, 4, .05, ax=axes[0])
aidecours.sgd_courbe_objectif(algo.obj_list, ax=axes[1])
aidecours.show_2d_predictions(xx, yy, algo.predict, ax=axes[2]);<jupyter_output><empty_output><jupyter_text>Reprenons l'exercice précédent en ajoutant l'apprentissange d'un *biais* à la régression logistique:$$\frac1n \sum_{i=1}^n - y_i (\wbf\cdot\xbf_i+b) + \log(1+e^{\wbf\cdot\xbf_i+b})+ \frac\rho2\|\wbf\|^2\,.$$<jupyter_code>class regression_logistique_avec_biais:
    def __init__(self, rho=.01, eta=0.4, nb_iter=50, seed=None):
        self.rho = rho
        self.eta = eta
        self.nb_iter = nb_iter
        self.seed = seed
        
    def fit(self, x, y):
        if self.seed is not None:
            torch.manual_seed(seed)
        
        x = torch.tensor(x, dtype=torch.float32)
        y = torch.tensor(y, dtype=torch.float32) 

        n, d = x.shape
        self.w = torch.randn(d, requires_grad=True)
        self.b = torch.zeros(1, requires_grad=True)
        
        self.w_list = list() # Servira à garder une trace de la descente de gradient
        self.obj_list = list()
    
        for t in range(self.nb_iter+1):
            pass # Compléter
                
    def predict(self, x):
        x = torch.tensor(x, dtype=torch.float32)
        pred = x @ self.w.detach() + self.b.item()
        return np.array(pred.numpy() > .5, dtype=np.int)
rho = 0.01
eta = 0.4     # taille du pas
nb_iter = 20  # nombre d'itérations

algo = regression_logistique_avec_biais(rho, eta, nb_iter)
algo.fit(xx, yy)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
aidecours.sgd_courbe_objectif(algo.obj_list, ax=axes[0])
aidecours.show_2d_predictions(xx, yy, algo.predict, ax=axes[1]);<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.14872616838198077, 'fraction_numerical': 0.019487092964400203, 'mean_word_length': 3.826954397394137, 'pattern_counts': {'":': 0, '<': 82, '<?xml version=': 0, '>': 84, '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': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '668365', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 4757, 'n_tokens_neox': 4661, 'n_words': 976, 'path': 'travaux_diriges/TD1 - Intro a pyTorch - Partie 1.ipynb', 'repo_name': 'pgermain/cours2018-Intro_aux_r-seaux_de_neurones', 'size': '11862'}