lundi 28 août 2023

Régression linéaire, RIDGE et LASSO

 Le modèle de régression linéaire est un des piliers de l'apprentissage automatique. Simple et facile à mettre en œuvre, il n'est pourtant pas le seul de ce type.

Pour commencer savez vous à quoi sert la queue d'un cerf-volant ?

Son rôle premier n'est pas décoratif, la queue sert à lester l'arrière du cerf volant et ainsi le stabiliser. Sans elle le cerf-volant serait trop réactif et incontrôlable. 
Il en est de même pour les modèles d'apprentissage: ils ont besoin d'une régulation pour être moins sensibles aux variations des données. C'est ce que nous allons voir avec les modèles RIDGE et LASSO.

Un modèle de régression linéaire consiste à trouver l'équation d'une droite qui traverse au plus prés un nuage de point.

Construction d'un jeu de données. 

Je vais utiliser le générateur de données de SKLEARN pour construire une jeu de données se prêtant à une régression linéaire. Les principaux paramètres à choisir sont:
  • La taille du jeu de données
  • Le nombre de facteurs déterminants 
  • Le niveau de distorsion
 

from  sklearn.datasets import make_regression
 
X,y = make_regression(n_samples=100, 
                      n_features=3,
                      bias = 1,
                      n_informative=1, 
                      noise=30, 
                      random_state=1) 
Ici nous aurons 3 facteurs dont 2 sans trop d'information pour le modèle.

On peut le visualiser par l'affichage des corrélations

Seul de le dernier facteur (numeroté 2 ) affiche une corrélation avec le résultat (y) : en bas à droite.

J'isole des données de test avec la méthode split de SKLEARN:



Modélisation.

Je vais appliquer successivement une regression linéaire, Ridge et Lasso et comparer les pentes de la droite des modèles.

# Les imports
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Ridge
from sklearn import linear_model
 
# regression linéaire
reg = LinearRegression()
reg.fit(X_train,y_train)
 
# regression ridge
ridg = Ridge(alpha=10.0)
ridg.fit(X_train, y_train)
 
# regression Lasso
lasso = linear_model.Lasso(alpha=5)
lasso.fit(X_train, y_train)
On va tout d'abord vérifier que seul un facteur sur les trois est significatif:

X2 est bien le facteur dominant.

Dans le cadre de la régression linéaire , l'apprentissage va consister à déterminer la combinaison des coefficient des trois facteurs qui minimise les distances entre les points de mesure et le point calculé.

Si, on a trop de données ou à l'inverse très peu, le modèle va se comporter comme un cerf-volant sans queue. Il sera sensible aux variations des données à prédire. 

Aussi les modèles Ridge et Lasso visent à ajouter des contraintes sur la valeurs des coefficients. Mathématiquement le modèle Ridge va ajouter une régulation dite L2 et le modèle Lasso , une régulation L1.
L1 et L2 sont comme les handicaps pour les courses de chevaux:  une contrainte afin de réduire les inégalités.

La méthode générale pour calculer les coefficients restent les mêmes, on ajoutera en plus des contraintes ne concernant que les coefficients.

Le modèle RIDGE.

Ridge va tenter d'obtenir des coefficient plus petit afin d'adoucir la pente de la droite.
Cela se traduira pour une variation des X, une moindre variation du 'y'.
Illustration: 



Le modèle Lasso. 

Il va plus loin , il peut donner un coefficient égal à 0 pour un facteur et ainsi réduire le nombre de facteur d'un modèle.
Illustration pour le premier facteur:

 

Au niveau du score des modèles: ils sont proches, le Ridge est meilleur mais le Lasso sera peut être plus efficace dans le temps.

Cet article (lien )pour aller plus loin.

Conclusion:


Il ne faut pas hésiter à comparer plusieurs modèles, SKLEARN est votre ami, il simplifie grandement les choses.

Le notebook portant le code est sur github.






jeudi 24 août 2023

Python et golang : le passage de paramètre


 

En programmation le mode de passage de paramètre pour l'appel d'une fonction (ou d'une méthode) est un point central car il est souvent source de bug. Un paramètre passé à une fonction est avant tout une donnée. Généralement on peut distinguer deux modes de passage:

  • Passer la valeur de la donnée: le paramètre local de la fonction va être construit à partir de la recopie des valeurs de la donnée du bloc appelant.
  • Passer la référence de la donnée : La variable locale ne contiendra pas les valeurs de la donnée de départ mais la référence (adresse) de la donnée. C'est ce qu'on appelle un pointeur.

Une donnée d'un programme recoupe deux choses: une nom de donnée et un espace de stockage contenant la ou les valeurs associées à ce nom de donnée. 

Lorsque qu'on passe un paramètre par valeur, une nouvelle donnée est créée  et son espace associé est alimenté par une copie de la donnée émettrice. Dans le cas d'un passage par référence ou par adresse, le nom de donnée est créée et son espace associé est le même que celui de la donnée émettrice. Les deux données pointent vers le même espace mémoire.

Dans les langages sophistiqués, ce mécanisme est masqué au développeur. Il n'aura pas à se poser la question. Ainsi en Python, c'est l'adresse de la variable qui sera passée comme paramètre à l'appel de la fonction pour les données de type conteneur (list, tuple, set , obj) . C'est le contenu de la variable 'self' d'une méthode Python.

 Exemple pour la fonction suivante en Python:



Avec comme exécution: 

Et un effet de bord pas toujours voulu, illustré par Pythontutor : les deux variables pointent sur la même zone mémoire.

La même fonction appelée avec un tuple donnera un effet différent:




Explication: l'instruction '+='  n'a pas le même comportement en présence d'objet muable (list) qu' immuable (tuple). C'est pourquoi, il faut éviter de passer en paramètre des objets muables à une fonction Python.

Et en Golang ? .

Le Go permet de choisir son mode de passage de paramètre : par valeur (recopie) ou par pointeur (adresse).  Mais il va plus loin que ça. Pour une méthode, il va générer plusieurs signatures: une par valeur et une par pointeur. En fonction de ce que recevra la méthode, la bonne méthode sera appelée.

Exemple si on a codé ceci :

// GetFace: cette fonction/methode retourne le nombre de face du dé.
func (d De) GetFace() int {
	return d.Nbface
}
 
Bien que la signature préfigure une passage par valeur, il sera possible de fournir un pointeur.
Le compilateur aura généré les 2 formes de transmission: par valeur ou par pointeur
d1 := De{6}
    fmt.Printf("%v , %T\n", d1, d1)
    fmt.Printf("nb de face (adresse):%v\n", d1.GetFace())
    fmt.Printf("nb de face (pointeur):%v\n", (&d1).GetFace())
    d1p := &d1
    fmt.Printf("nb de face (pointeur V2):%v\n", d1p.GetFace())

Avec comme résultat:
{6} , libde.De
nb de face (adresse):6
nb de face (pointeur):6
nb de face (pointeur V2):6 
Cette simplification fonctionne dans aussi dans le cas inverse où une fonction doit utiliser un pointeur.

Cette facilité à une contrepartie : des difficultés à comprendre ou à reprendre un code.
Aussi, il est recommandé d'utiliser dans le codage et dans l'exécution le passage par adresse (pointeur)
Car seule l'utilisation des pointeurs permettra de modifier les attributs de la structures. 
Comme ci-dessous:
// methode setter
    d4 := DePipe{
        De{6},
        5,
    }
    fmt.Println("avant appel", d4)
    d4.SetFaceValeur(2)
    fmt.Println("apres appel", d4)
    tmp := &d4
    tmp.SetFacePointeur(3)
    fmt.Println("apres appel", d4)
    (&d4).SetFacePointeur(6)
    fmt.Println("apres appel", d4)

Avec comme exécution:
avant appel {{6} 5}
par valeur {{6} 2}
apres appel {{6} 5}
par pointeur {{6} 3}
apres appel {{6} 3}
par pointeur {{6} 6}
apres appel {{6} 6}

Conclusion: utilisez les pointeurs.

source https://github.com/germanlinux/lemongo/tree/main/lemonlabs/src/pointeurs

samedi 12 août 2023

De Python à Golang: premiers pas

 Passer d'un langage à un autre n'est pas toujours une démarche facile mais constitue un excellent exercice.

Avant de commencer à faire du Go (golang) , il faut retenir 3 choses:

  • La preparation du répertoire projet est importante: on ne peut pas faire un bout de code sur le coin de la table.
  • Il faut un bon IDE : Vscode ou VScodium sont tout à fait adaptés au Go: ils mettent en forme le code automatiquement.
  • La lecture de code Go est le meilleur moyen d'apprendre les bonnes pratiques du langage. Il ne faut pas chercher dans un premier temps à comprendre le code mais plutôt à observer comment le projet s'organise.


Commençons par un exemple simple: Je vais utiliser l'implémentation d'une librairie qui émule le lancer d'un dé en Python pour la transposer en Golang.




Cette classe propose 2 méthodes en plus du constructeur.

En Golang, il n'y pas de mécanisme d'encapsulation comme dans les autres langages, la description de l'objet est portée dans une structure, les fonctions viennent compléter l'ensemble.

type De struct{ Nbface int }
 
 
func (d De) GetFace() int {
	return d.Nbface
}
func (d De) Lance() int {
	n := rand.Intn(d.Nbface) + 1
	return n
}
 
On peut voir que les structures remplacent les classes. Que le 'self' envoyé en coulisse à une méthode Python, prend la forme d'un préfixe de la définition de la fonction en Go.
Comme Go est un langage compilé avec des types de variables statiques, une déclaration du type de la donnée est nécessaire. Mais il existe un sucre syntaxique bien utile l'opérateur ':='. Le type de la donnée d'affectation sera déduit à partir de la partie droite : signature de la fonction, type de la donnée reçue etc.

2 remarques: un élément déclaratif: type , nom de variable, conteneur, fonction etc.. sera utilisable en dehors de son fichier source (exportable) qu'a la condition que son nom commence par une lettre MAJUSCULE.

Utilisation de notre librairie


En python et en Go : par un import.

En python : from libde import De

Puis
En Go :



package main
 
import (
	"fmt"
	"lemonlabs/src/de/libde"
	"sort"
)
 
type De = libde.De
 
func main() {
 
	d1 := De{6}
	fmt.Printf("%v , %T\n", d1, d1)
	fmt.Printf("nb de face :%v\n", d1.GetFace())
	n1 := d1.Lance()
	fmt.Printf("face :%v", n1)
Exécution: go run .\main.go

{6} , libde.De
nb de face :6
face :3

Heritage vs composition.

Soit une classe Depipe qui permet de doubler les chances de sortie d'une face particulière.

En Python, on pourra utiliser les propriétés de l'héritage:

En Go, on utilisera la composition:


type De struct{ Nbface int }
 
type DePipe struct {
	De
	Faceplus int
}
 
A l'exécution de ce code:
d2 := DePipe{
        De{6},
        6,
    }
    fmt.Printf("%v , %T\n", d2, d2)
Avec le résultat suivant:

{{6} 6} , libde.DePipe

Par contre il faudra redevelopper la fonction getface pour depipe au moins d'utiliser une interface. 
(voir les prochains articles)

La suite des fonctions en Go



func (d DePipe) GetFace() int {
	return d.De.Nbface
}
func (d DePipe) Lance() int {
	n := rand.Intn(d.De.Nbface+1) + 1
	if n == d.De.Nbface+1 {
		n = d.Faceplus
	}
	return n
}

Remarque:  la structure qui sera passée aux fonctions Go (GetFace ou Lance) , sera une copie de la structure appelante  (par valeur) . 

Voici pour une première introduction, le source est disponible sur github .
Dans ces exemples, les champs ne sont pas mis à jour. Pour modifier un champ  d'une structure Go, il faudra utiliser un pointeur (dans les prochains articles) .

jeudi 3 août 2023

Collecte sur le web (3)

 A nouveau une liste de liens relatifs à Python, Golang , webSSO et divers sujets.

Python

Divers

Cloud - IAM

Outils de présentation

Langage GO - golang