Thématiques principales

Affichage des articles dont le libellé est python. Afficher tous les articles
Affichage des articles dont le libellé est python. Afficher tous les articles

lundi 19 août 2019

Docker Compose

Nous rebondissons une nouvelle fois! Dans l’article précédent nous avions traité d’un exemple de déploiement de python dans docker [1] . Mais nous avions constaté que nous devions spécifier dans nos images des informations concernant le contexte d'exécution, entre autre le hostname de la machine hôte… et franchement c’est un peu dégueu...

Alors jusqu’à maintenant nous avons utilisé docker dans différents exemples ou soit nous voulions expliquer certains aspects de son fonctionnement soit parce que tout simplement c'était plus simple pour  traiter le sujet en question.

À chaque fois nous n’avions eu besoin que d’un container ce qui ne posait pas de problème sauf dans ce dernier exemple!

Du coup on va voir comment s’en défaire et ça va justement être l’occasion de parler un peu de docker-compose [2].

Docker compose est justement la pour faciliter l’orchestration et le lancement de container docker dans un contexte commun cohérent [24].

vendredi 16 août 2019

Python dans docker

Nous avons dans un article précédent [1] proposer un exemple de mise en oeuvre  de deux applications python exploitant ou exposant une interface ReST.

Dans un second article [2],  nous avons également vu qu’il était possible de de packager et livrer des applications python avec divers outils comme setuptools[3] ou pyinstaller [4].

Nous avions cependant laisser une question en suspend concernant une dernière façon de déployer nos appli python. Ainsi, comme vous l’avez deviné, (le titre à tout spoiler…) nous allons dans ce présent article nous intéresser au packaging python dans des container docker.

Alors du coup après avoir lu l’article précédent, vous allez surement demander, mais pourquoi? quel est l'intérêt? et bien pour deux raisons:

  • la première est la devise de docker [5]: build once, run everywhere ce qui permet d’aller bien au delà des plateformes et des machines virtuelles
  • la seconde est que comme à la place de livrer un package ou des sources, on va livrer une image docker, alors peu importe ce que nous mettrons dans le conteneur, cela restera dans le conteneur sous notre responsabilité en minimisant complètement les interventions d’installations d’un utilisateur

La limitation est évidemment que nous ne nous intéresserons qu’aux applications basées sur le réseaux…. (WEB, CLI, etc)

Du coup pour illustrer cet article, je vous propose de reprendre l’exemple déjà traité précédemment [1] avec lequel nous proposons de déployer dans un ou plusieurs conteneur docker les différentes parties applicatives.

Pour cela nous allons donc utiliser une image de base de type alpine dans laquelle nous allons ensuite déployer ce que nous aurons décidé d’etre notre livrable. Ici, le plus simple, et de directement pousser nos sources dans l’image en s’assurant préalablement que python est bien installé ainsi que les dépendances de notre application.

On va donc définir deux Dockerfile en s’inspirant de [6]: Dockerfile_people et Dockerfile_stat respectivement pour chaque applications:


#Dockerfile_people:

FROM alpine:latest
RUN apk add --no-cache python3
RUN pip3 install setuptools Wheel
RUN pip3 install flask
COPY . /opt/
CMD cd /opt;python3 -m people



#Dockerfile_stat:

FROM alpine:latest
RUN apk add --no-cache python3
RUN pip3 install setuptools Wheel
RUN pip3 install flask python_http_client
COPY . /opt/
CMD cd /opt;python3 -m people_stat


Puis on construit nos images:


$ docker build -t people -f ./Dockerfile_people .
$ docker build -t people_stat -f ./Dockerfile_stat .


Et on les lances (en oubliant pas d’exposer leur ports d'écoutes respectifs 5001 et 5002 avec le paramètre -p)


$ docker run -it -p 5001:5001 people
$ docker run -it -p 5002:5002 people_stat


Et la ca marche! euh oui mais pas complètement… autant si on essaye d'accéder à l’interface http://localhost:5001/humans, cela fonctionne mais…. si ont tente http://localhost:5002/recall, la rien ne va plus!

Qu’est ce qui se passe?

C’est simple dans la configuration applicative réalisant les requêtes sur “recall”, nous avons dit à notre application d’aller chercher les informations sur localhost:5001/humans…. mais ce localhost…. c’est lui même! Ce que l’on voulait c'était qu'il aille sur l’autre container! Alors l’erreur semble évidente mais souvent on la fait car on développe sur un poste en local sans se préoccuper de l'aspect distribué du système que l’on est en train de faire… et donc je vous assure tout le monde tombera dans cette erreur.

Donc du coup on fait quoi?

C’est simple on ne met pas localhost mais le hostname de la machine qui expose réellement ce port! C’est à dire dans notre situation, la machine physique. (je vous laisse essayer ).

Alors bien sur généralement une information comme celle ci ne devra pas être mis en dur dans le code, on préférera s’appuyer sur des fichiers de configuration, accessibles depuis un volume ou définis par des variables d’environnement alimenté au moment du lancement du container. L’idée ici n’est pas d’aller jusque la mais déjà de voir ce que permet docker basiquement et certaines des limitations que cela induit, ici entre autre une adhérence résiduelle à la machine physique.

Nous verrons dans un prochain article comme avec docker toujours s’en affranchir.

Références:

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2019/08/rest-avec-python.html
[2] https://un-est-tout-et-tout-est-un.blogspot.com/2019/08/deployer-du-python.html
[3] https://setuptools.readthedocs.io/en/latest/setuptools.html
[4] https://www.pyinstaller.org/
[5] https://uwm.edu/business/event/docker/
[6] https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xix-deployment-on-docker-containers

dimanche 11 août 2019

Déployer du Python

L’idée de cet article est de nous intéresser aux modes de livraison possible d’un programme python. L'enjeux est important car, en effet, écrire du code c’est bien mais sans livraison, c’est comme si ce code n’existait pas! Il faut donc avoir des moyens fiables et rapides permettant de fournir à un client son application python “clef en main”.

Via les sources

Dans cette approche ce qui sera fourni ce sera directement les sources qui seront mis à disposition de l’utilisateur final. Ce dernier aura alors la charge de fournir un environnement disposant non seulement de python dans la version adéquate mais aussi d’avoir pre-installer les modules dont l’application dépende.

Par exemple, sur une machine Debian, pour lancer l’application, il va être nécessaire préalablement de faire :

1
2
3
4
5
$ apt-get update && apt-get install -y --no-install-recommends python3 \
   python3-pip python3-dev \
$ apt-get clean && rm -rf /var/lib/apt/lists/
$ pip3 install --upgrade pip setuptools
$ pip3 install dependencies

Pour finalement pouvoir lancer l’application sur un appel du module :

1
$ python3 -m monAppli

Le point négatif de cet approche est la gestion des droits car lors du lancement de l’application, et à moins de préciser que l’on veille que python reste en mode interpréteur [pyt-pyc], différents fichiers vont être générés sur le poste cible. Il importe donc que ces fichiers puisse être généré sans être gêné par des permissions du systèmes mal prévu (et qui seront de toute façon délicat à mettre en oeuvre).

Via les fichiers pré-compilés

C’est une possibilité il faut l’avouer mais alors il ne sera pas possible de faire de patch ou de modification du code source sans compter que ce code sera globalement dispatcher dans un ensemble de fichiers et de répertoire (à l’image des sources).

Cela n'empêchera pas de devoir aussi préparer l’environnement d'exécution comme avec les sources. Donc c’est moins risqué coté permissions systèmes mais on perd un peu l'intérêt du python (le côté interpréteur)

À ce compte là, il serait alors mieux de simplifier la livraison en réalisant un package de l’application.

Exe, deb, rpm, etc...

Alors il est clair que faire un package de livraison, ca sera forcement en prenant en compte l’environnement de déploiement et il ne sera pas possible de déployer de la même manière sous windows où sous linux.

L'intérêt de l’approche est de fournir au client une seule référence et sera donc plus simple à gérer d’autant plus que selon la plateforme, cela permettre de bénéficier mécaniquement des avantages du mode de distribution (utilisation de aptitude et gestionnaire de paquet sous linux, pre-installation et configuration de paquets en dépendances, etc.

Pourtant les problèmes cités ci dessus seront toujours présents, code sources ou fichiers pre-compilés, il faudra gérer la dispersion de fichiers dans l’environnement.

On voit bien donc que le problème ici n’est pas le package de livraison, mais son contenu. Heureusement la communauté python à pensé à tout et à fournir différents outils pour livrer de façon cohérente notre application.

Pre-packaging

Ainsi pour réaliser ce pre-packaging, il existe quelques outils comme [setuptools], [distutils], [zipapp], [pyInstaller], py2exe (Windows) ou encore [cx_Freeze] (Windows et Linux) pour ne citer que les plus connu.

PyInstaller


Le plus simple à utiliser est PyInstaller, on l’installe et on l’applique sur le main:

1
2
$ pip install pyinstaller
$ pyinstaller monAppliv.py

et on obtient un répertoire dist dans nos sources contenant notre applications et toutes les librairies nécessaire à son exécution.

En voyant ca vous allez me dire, ok on a troqué les fichiers sources oi pre-compilé avec des librairies…. oui mais la il n’est pas nécessaire d’installer python sur le poste! Par contre attention, la construction qui à été réalisé à être spécifique à une plateforme… donc il faudra prévoir un build pyinstaller par plateforme cible.

setuptools


Setuptools est à l’inverse un package qui va permettre de construire un module versionable comme ceux télécharger lors de l’utilisation de pip install.

Setuptools repose sur distutils et je ne présenterai donc pas ce dernier qui est de plus bas niveau dans la construction des modules package python.

Setuptools est probablement l’outil le plus employé par la communauté car il permet l'intégration du module développé au sein d’un Server exploitable via pip. Ainsi même si python doit être préalablement installer sur le poste cible, la gestion des sources et fichier pre-compilés devient complètement transparent pour le développeur et le client.

De plus, contrairement au contraintes d’une gestion externe du packaging, setuptools, en gérant le versionning va faciliter via pip la mise à jour et l'évolution de l’application.

Pour cela il faut d’abord créer un descripteur de l’application et ce en python. Par exemple dans le cas de nos applications ReST [rest-py], on va créer un fichier setup.py contenant le code suivant:

1
2
3
4
5
6
7
8
from setuptools import setup, find_packages
setup(
    name="people",
    version="0.1.0",
    packages=find_packages(),
    scripts=['people.py','json_tk.py','human.py'],
    install_requires=['docutils>=0.3','Flask>=1.1.1']
)

Ensuite ce code sera exécuté bêtement comme un script :

1
$python setup.py sdist

Ce code va alors produire un targz de notre application contenant nos sources et déclarant les dépendances adéquates pour son installation.

Ainsi une fois packager il suffit alors de fournir ce package au client que celui ci exécute la commande suivante:

1
$ pip install people-0.1.0.tar.gz

Notre application est alors installée dans python (à cette occasion on préférera utiliser un virtualenv…) Et utilisable comme ceci:


1
$ python -m people

Dans cet exemple, avec setuptools, on voit que l’on est beaucoup moi adhérant à la plateforme mais que cela implique que python soit préalable installer dans celle-ci.

L’avantage sera évidemment sa mise à jour mais on notera que si ici on a créé un module setup.py, le nom de ce fichier est impératif sinon l’installation ne sera pas possible avec pip. du coup on notera que notre façon de faire nos applications dans l’exemple [rest-py] se prête finalement assez mal avec cette approche (à moins de séparer le code métier du code applicatif mais cela peut être laborieux)

Conclusions

Nous venons de passer en revue l’ensemble des manière de livrer et déployer une application python. Toutes ces façons de faire comportent leur lot d’avantages et d'inconvenants. On notera malgré tout que la communauté python n’est pas en reste de solution pour fournir des approches, il restera ensuite à la charge des développeurs de faire un choix selon le contexte et les contraintes.

Cependant, nous n’avons pas évoqué une dernière solution possible pour la livraison et le déploiement de nos applications python…. à votre avis???

Références:

[rest-py] https://un-est-tout-et-tout-est-un.blogspot.com/2019/08/rest-avec-python.html
[pyt-pyc] https://stackoverflow.com/questions/154443/how-to-avoid-pyc-files
[distutils] https://docs.python.org/fr/3/library/distutils.html
[pyinstaller] https://www.pyinstaller.org/
[zipapp] https://docs.python.org/fr/3/library/zipapp.html
[setuptools] https://setuptools.readthedocs.io/en/latest/setuptools.html
[cx-freeze] https://cx-freeze.readthedocs.io/en/latest/index.html

mardi 6 août 2019

Rest avec Python


Après de nouveau un bon mois sans rien avoir poster dans le blog, voici la suite de l'article précédent sur ReST [rest-th] mais en rentrant un peu plus dans le concret.

Ainsi je vous propose de voir ce que python nous propose pour aborder cette problématique de réalisation ou d'interrogation d’une interface ReST.

Pour faire simple, nous allons considérer une population d’individu sur laquelle nous allons collecter des informations et que nous exposerons via une première interface ReST. Grâce à ce référentiel, nous pousserons un peu plus loin le travail en proposant une seconde interface ReST dont la logique ici sera d’exploiter la première interface afin de fournir divers indicateurs.



Le schema precedent présente les deux applications et leur environnement. L’idée est simple l’une expose des données génériques, l’autre les exploite pour exposer des données de plus haut niveau.

Rest-people

Cette partie de l’application est celle présentant les données à plat. Elle s’alimente sur une base de données (que l’on simplifiera par un liste par soucis de simplicité) et les exposera via deux interfaces ReST, un GET permettant de récupérer la liste complément des individu de la base et un GET permettant d’en sélectionner un seul en fonction de son Id. On complètera cette application par une méthode POST pour ajouter un élément à la population.

En python pour faire cela, on va commencer par créer des humains, des hommes et des femmes en faisant des classes python très classiques.


class Human:

    def __init__(self, id, name, age):
        self.id = id
        self.name = name
        self.age = age

    def __getitem__(self, name):
        return self.__getattribute__(name)

    def __setitem__(self, name, valeur):
        return self.__setattr__(name, valeur)

    def __delitem__(self):
        return ","


class Men(Human):

    def __init__(self, id, name, age):
        Human.__init__(self, id, name, age)


class Women(Human):

    def __init__(self, id, name, age):
        Human.__init__(self, id, name, age)


Ensuite avant de réaliser l’interface ReST, on va s'intéresser au format des données que l’on va manipuler. Ici pas de xml, mais du Json [json].

Dans python, pour manipuler du json, on utilise…. la librairie Json… c’est aussi simple et dans les faits la seule chose dont on va avoir besoin ce sont les méthodes “jsonify“ et “loads“.

  • La première nous servira à transformer un objet en json à la particularité que l’objet en paramètre doit être un dictionnaire ou une liste de dictionnaires. 
  • La seconde à l’inverse nous chargera notre json… pourtant comme le travail est fait à moitié, on va devoir compenser un peu et au passage se faciliter la vie.

En l'occurrence nous allons utiliser du code permettant de serialiser les informations d’un objet (avec les données des attributs, le nom de classe ou son module) et à l’inverse de deserialiser afin de recréer les instances. Ce code peut se trouver sur internet [json-conv].


def convert_to_dict(obj):
    """
    A function takes in a custom object and returns a dictionary representation of the object.
    This dict representation includes meta data such as the object's module and class names.
    """

    #  Populate the dictionary with object meta data
    obj_dict = {
        "__class__": obj.__class__.__name__,
        "__module__": obj.__module__
    }

    #  Populate the dictionary with object properties
    obj_dict.update(obj.__dict__)

    return obj_dict


def dict_to_obj(our_dict):
    """
    Function that takes in a dict and returns a custom object associated with the dict.
    This function makes use of the "__module__" and "__class__" metadata in the dictionary
    to know which object type to create.
    """
    if "__class__" in our_dict:
        # Pop ensures we remove metadata from the dict to leave only the instance arguments
        class_name = our_dict.pop("__class__")

        # Get the module name from the dict and import it
        module_name = our_dict.pop("__module__")

        # We use the built in __import__ function since the module name is not yet known at runtime
        module = __import__(module_name)

        # Get the class from the module
        class_ = getattr(module, class_name)

        # Use dictionary unpacking to initialize the object
        obj = class_(**our_dict)
    else:
        obj = our_dict
    return obj


Enfin maintenant que les éléments sont en place, il reste à mettre en oeuvre l’interface ReST. Pour cela on va utiliser le framework Flask [flask] qui permet comme avec Spring-WS (nous y reviendrons) de spécifier des routes associées aux verbes http et décorant des méthodes qui seront alors exécuté en fonction des chemins appelés.

Mais avant cela initialisons le contexte Flask: il nous faut des imports, un jeu de données (oui parce que dans cet exemple, on va pas non plus faire une BDD  pour 3 humains….) et définir le main qui construira le contexte Flask:


#!flask/bin/python
from flask import Flask, jsonify, request
import json

from json_tk import convert_to_dict, dict_to_obj
from human import Men, Women

people = [
    Men(1, "Bob", 12),
    Women(2, "Alice", 10)
]

app = Flask(__name__)

if __name__ == '__main__':
    app.run(debug=True, port=5001, host='0.0.0.0')


Maintenant on va définir les méthodes associées aux routes et verbes http.


@app.route('/humans', methods=['GET'])
def get_humans():
    return jsonify([convert_to_dict(human) for human in people])


@app.route('/humans/<int:human_id>', methods=['GET'])
def get_human(human_id):
    return jsonify([convert_to_dict(human) for human in people if human.id == human_id][0])


@app.route('/humans', methods=['POST'])
def post_human():
    print("REQUEST:" + str(request.data))
    json_response = json.loads(request.data)
    human = dict_to_obj(json_response)
    human.id = len(people)+1
    people.append(human)
    return jsonify(human.id)


On lance ca avec IntelliJ et on consulte les chemins avec un navigateur (http://0.0.0.0:5001/humans)


[
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 12, 
    "id": 1, 
    "name": "Bob"
  }, 
  {
    "__class__": "Women", 
    "__module__": "human", 
    "age": 10, 
    "id": 2, 
    "name": "Alice"
  }
]


Pour le test du POST, on va utiliser Postman [postman], un outil indispensable pour tester une API ReST.

On interroge alors la liste et on voit que notre nouvel humain est bien la!


[
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 12, 
    "id": 1, 
    "name": "Bob"
  }, 
  {
    "__class__": "Women", 
    "__module__": "human", 
    "age": 10, 
    "id": 2, 
    "name": "Alice"
  }, 
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 24, 
    "id": 3, 
    "name": "Micka"
  }
]

ReST-people-stat

On a une première application qui fournit des données de base. La seconde doit s’interfacer sur celle-ci et disons

  • fournir la liste des humains tel quel
  • fournir la moyenne des âges des humains
Il faut un outil pour interroger la première interface. Pour cela on va utiliser le framework  python_http_client qui fournit un client http.


import json
import functools
from flask import Flask, jsonify
from python_http_client import Client

from response import str_http_response
from json_tk import dict_to_obj, convert_to_dict
from human import Moyenne

client = Client(host='http://localhost:5001/humans')
app = Flask(__name__)

if __name__ == '__main__':
    app.run(debug=True, port=5002, host='0.0.0.0')

On va ajouter une classe calculant la moyenne en utilisant la librairie functools:


import functools

class Moyenne:

    def __init__(self, humans):
        self.moyenne = (functools.reduce(lambda x, y: x.age + y.age, humans)) / len(humans)


Ensuite on va d’un côté faire un recall pour voir comment fonctionne le client.

@app.route('/recall', methods=['GET'])
def get_recall():
    response = client.get()

    str_http_response(response)

    json_response = json.loads(response.body)
    humans = [dict_to_obj(human) for human in json_response]
    print("HUMANS" + str(humans))
    return jsonify([convert_to_dict(human) for human in humans])


Et on va pouvoir implementer la methode get_moy. pour  cela on aussi utiliser un module permettant une approche map/reduce. Je vous laisse analyser le code:


@app.route('/moy', methods=['GET'])
def get_moy():
    response = client.get()
    str_http_response(response)

    json_response = json.loads(response.body)
    humans = [dict_to_obj(human) for human in json_response]
    print("HUMANS" + str(humans))

    moyenne = Moyenne(humans)
    print("MOYENNE:" + str(moyenne))
    return jsonify(moyenne.__dict__)


Il ne reste plus qu’à tester! On lance l’application people. La second ensuite, on vérifie alors la methode recall:


[
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 12, 
    "id": 1, 
    "name": "Bob"
  }, 
  {
    "__class__": "Women", 
    "__module__": "human", 
    "age": 10, 
    "id": 2, 
    "name": "Alice"
  }
]


Puis on regarde si la moyenne fonctionne:


{
  "moyenne": 11.0
}


Pour le jeux on ajoute dynamiquement un nouvel humain dans l’application people… avec Postman et on vérifie que la moyenne à changé:


{
  "moyenne": 13.0
}


Voilà nous avons fait le tour de notre application en deux temps. Pourquoi en deux parties? ba parce que ça va être un bon prétexte de mettre ça dans des conteneurs docker! Mais ça sera pour le prochain article!

Références

[rest-th] https://un-est-tout-et-tout-est-un.blogspot.com/2019/06/rest-introduction.html
[json-conv] https://school.geekwall.in/p/ByaW0IVqN/json-the-python-way
[json] https://medium.com/python-pandemonium/json-the-python-way-91aac95d4041
[flask] https://flask.palletsprojects.com/en/1.1.x/
[rest-tuto] https://www.codementor.io/sagaragarwal94/building-a-basic-restful-api-in-python-58k02xsiq
[flask-mega] https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
[postman] https://www.getpostman.com/

samedi 6 avril 2019

IA : Kernel Trick

On a vu que les SVM sont très bon sur des données qui suivent des modèles linéaires [1] mais que lorsque ces données ont un profils non linéaire, on va avoir des soucis.

Alors bien sur différentes approches sont envisageables comme de faire une linéarisation par partie (c’est à dire construire un découpage dans lequel les données sur ces segments peuvent etre reduites à une droite) mais la solution la plus efficace est encore l’emploi du kernel trick.

À la base, la question est simple: si le SVM est efficace sur un domaine linéaire alors est il possible de transformer d’une manière ou d’une autre des données non linéaire en données linéaires?

Bien sûr vous allez dire oui, sinon on ne serait pas la… et oui c’est ce que va nous permettre d’une certaine manière le kernel trick, pas juste en cherchant à retrouver de la linéarité dans les données mais en ajoutant des dimensions supplémentaires à ces données [2].

Comment ca des dimensions supplémentaire? oui oui car de l’idée de l’astuce du noyau est formellement de chercher à définir une fonction sur nos données dont l’espace cible sera plus grand que celui initial.

Bizarre? non, mais illustrons l’idée avec la suite.

Le cas simple (très utilisé)

Le premier exemple est basique dans la littérature. Il s’agit de considérer des données de deux types différents répartis sur une même droite. Cela ressemble à cela:

fig=plt.figure(1,figsize=(8,8))
nbrEl=20
front=5
f=0
plt.plot(np.linspace(-10,-front-1,nbrEl),[f for x in np.linspace(-10,-front-1,nbrEl)],"b.")
plt.plot(np.linspace(-front,front,nbrEl),[f for x in np.linspace(-front,front,nbrEl)],"r.")
plt.plot(np.linspace(front+1,10,nbrEl),[f for x in np.linspace(front+1,10,nbrEl)],"b.")



Dans ce cas, on remarquera que l’ensemble qui nous intéresse est autour de zéro alors que les autres sont aux extrêmes… Si l’on veut user d’un changement de dimension, il nous faut ajouter une nouvelle composante. mais nous avons que x, comment obtenir un y?

Très simple, passons x au carré et disons que ca sera la valeur de y! Ainsi, on obtient des données en fonction de leur distance à zéro sur un axe y tout en les gardant avec la même valeur sur l’axe x. LATEX Cela aboutit à la transformation suivante:

fig=plt.figure(1,figsize=(8,8))
nbrEl=20
f=lambda x: math.pow(x,2)

plt.plot(np.linspace(-10,-front-1,nbrEl),[f(x) for x in np.linspace(-10,-front-1,nbrEl)],"b.")
plt.plot(np.linspace(-front,front,nbrEl),[f(x) for x in np.linspace(-front,front,nbrEl)],"r.")
plt.plot(np.linspace(front+1,10,nbrEl),[f(x) for x in np.linspace(front+1,10,nbrEl)],"b.")
Nous obtenons alors donc des données en dimension 2 tels que :



Et donc dans le cadre du SVC (SVM pour la classification), il est evident que l’on pourra tirer une droite séparatrice par exemple en y=30 et résoudre le problème de classification.

Ce cas est le cas simple [3] où l’on passe d’un espace de dimension 1 à 2. Que serait ce même cas en 2 dimensions?

Extension du cas simple

Lorsque l’on est en dimension 2 on peut souvent se retrouver avec un problème de classification dans lequel l’une des classes de données est inscrite dans un cercle. Pour illustrer cela, on considérera comme exemple directement la frontière que l’on souhaiterait linéariser.


zline = np.linspace(0, 15, 100)
X = np.sin(zline)
Y = np.cos(zline)
single= np.linspace(-1, 1, 40)
singlerev= np.linspace(1, -1, 40)
Xsingle=single
Ysingle=single
Xrev=single
Yrev=singlerev

fig=plt.figure(1,figsize=(8,8))
plt.plot(X,Y,"b.")
plt.plot(Xsingle,Ysingle,"r.")
plt.plot(Xrev,Yrev,"g.")




Comme illustré par l’image précédente, si on doit séparer ce qui est dans le cercle de ce qui est en dehors, on à un gros problème …. pas de linéarité possible ici! (À noter les deux droites ajoutées dans l’images vont nous servir de mire pour comprendre la déformation appliquée à l’espace mais elles ne correspondent pas à des données particulières liées à l’exemple)

Bien nous avons vu dans l’exemple précédent, qu’il suffisait d’augmenter le nombre de dimension pour faire apparaître des linéarités (enfin une au moins ca sera bien).

Ne sachant pas à priori quels sont les transformations qui seront adéquat, en plus de x et y, on propose du coup de construire les composantes supplémentaire suivantes: x², y² ou x*y.


def Kernel4Circle(xdat,ydat):
    X=[]
    Y=[]
    Z=[]
    for (x,y) in zip(xdat,ydat):
        X=X+[x*x]
        Y=Y+[y*y]
        Z=Z+[math.sqrt(2)*x*y]
    return (X,Y,Z)


(A,B,C)=Kernel4Circle(X,Y)
(Asingle,Bsingle,Csingle)=Kernel4Circle(Xsingle,Ysingle)
(Arev,Brev,Crev)=Kernel4Circle(Xrev,Yrev)


On a donc maintenant un ensemble de données sur 5 dimensions: (x,y,x², y² ,x*y) alors attention, rien ne dit que la solution soit un hyperplan de dimensions 5, cela peut aussi être un plan défini en 3 dimensions:


plt.figure(2,figsize=(10,10))
ax = plt.axes(projection='3d')
plt.plot(A,B,C,"b.")
plt.plot(Asingle,Bsingle,Csingle,"r.")
plt.plot(Arev,Brev,Crev,"g.")
ax.view_init(0,45)

Dans ce cas par exemple, ce n’est pas forcément très explicite cependant si on prend la représentation suivante, on voit que la partie interne du cercle est partie d’un côté du plan dans lequel se trouve le cercle et que la partie externe est de l’autre (un peu comme dans le cas précédent)


fig = plt.figure(1,figsize=(10,10))
ax = fig.add_subplot(111, projection='3d')
plt.plot(A,B,C,"b.")
plt.plot(Asingle,Bsingle,Csingle,"r.")
plt.plot(Arev,Brev,Crev,"g.")
ax.view_init(0,135)

En fait, dans ce cas, nous ne sommes pas obligé de changer le nombre de dimension mais plutot de “juste” choisir le bon espace 2D, comme suit:


Il n’est clairement pas simple de choisir le nombre de dimension utile ni lesquelles seront à joindre pour être pertinentes. Ce qu’il faut retenir c’est que malgré tout, par l’augmentation des dimensions, les algorithmes de régression ou de classification auront plus de moyen pour trouver un hyperplan.

Encore un exemple?

Le cas non linéaire classique

Prenons le cas une fonction non linéaire comme nous en avions parlé dans l’article [4]. LATEX Comme pour le cercle, nous allons ajouter des données mais ici de chaque côté de la frontière (on se placera dans le cadre d’un problème de classification) comme suit:


Xin=np.linspace(-10,10,100)
Yin=[2*math.pow(x,2)-5*x+1 for x in Xin]


Xsingle=np.linspace(00, 0, 40)
Ysingle=[x*25 for x in np.linspace(-10, 0, 40)]
Xrev=np.linspace(00, 0, 40)
Yrev=[x*25 for x in np.linspace(10, 0, 40)]

plt.figure(1,figsize=(8,8))
plt.plot(Xin,Yin,"b.")
plt.plot(Xsingle,Ysingle,"r.")
plt.plot(Xrev,Yrev,"g.")



Du coup comme pour les cas precedent, on va appliquer un noyau pour changer le nombre de dimensions:


def Kernel4XSquare(xdat,ydat):
    X=[]
    Y=[]
    Z=[]
    U=[]
    V=[]
    for (x,y) in zip(xdat,ydat):
        X=X+[x]
        Y=Y+[y]
        Z=Z+[x*x]
        U=U+[math.sqrt(2)*y*x]
        V=V+[y*y]
    return (X,Y,Z,U,V)
           
(X,Y,Z,U,V)=Kernel4XSquare(Xin,Yin)
(Xs,Ys,Zs,Us,Vs)=Kernel4XSquare(Xsingle,Ysingle)
(Xr,Yr,Zr,Ur,Vr)=Kernel4XSquare(Xrev,Yrev)

Du coup en choisissant bien les axes à exploiter, on voit que les deux ensembles deviennent séparable et qu’un algo de classification linéaire parviendra à retrouver ses petits:


angleBase=-100
altitude=30

fig = plt.figure(1,figsize=(15,15))
ax = plt.subplot(211, projection='3d')# equivalent a fig.addsubplot
plt.plot(X,Y,Z,"b.")
plt.plot(Xs,Ys,Zs,"r.")
plt.plot(Xr,Yr,Zr,"g.")
ax.view_init(altitude,angleBase)
ax.set_title('x,y,x^2');

ax = plt.subplot(212, projection='3d')# equivalent a fig.addsubplot
plt.plot(X,Y,V,"b.")
plt.plot(Xs,Ys,Vs,"r.")
plt.plot(Xr,Yr,Vr,"g.")
ax.view_init(altitude,angleBase)
ax.set_title('x,y,y^2');


Kernel Trick

C’est bien beau tout ca mais au final, c’est quoi le rapport avec le kernel trick?

Ce que nous venons de voir est qu’il est possible de transformer l’espace initial des données vers un nouvel espace de définition où il est possible de trouver des relations de linéarité et donc d’appliquer des algo ne fonctionnant que sur des jeux de données ayant ces propriétés.

Dans le concret, nous avons donc une fonction de la forme: LATEX Et cette fonction est utilisable en lieu et place des données d’entrées dans par exemple la formule duale. Celle ci devient donc: LATEX Du coup on voit que l’on peut si on connaît bien phi, appliquer notre algo de machine learning. Pourtant il y à un hic… c’est que nous avons vu que pour trouver les linéarités, généralement, on augmente le nombre de dimension et la c’est problematique pour le produit scalaire qui va vite devenir incalculable.

Pour résoudre ce problème, on va alors appliquer l’astuce du noyau.

Celui ci consiste à considérer que le produit scalaire des fonctions de transformations est équivalent d’une fonction K prenant en paramètre nos données initiales. En gros: LATEX L'intérêt de K est que celui-ci s’il respecte les mêmes propriétés que le produit scalaire alors il peut nous permettre (faute d’en trouver une définition adéquate) de linéariser nos données au même titre que la fonction phi comme nous le faisions dans les exemples précédents.

À ce titre, il est même possible de calculer après coup la fonction K.

Par exemple si l’on reprend le cas du cercle, nous avions choisi comme fonction de redescription la fonction comme suit: LATEX Ainsi on peut en déduire K: LATEX Correspondant alors à utiliser un noyau polynomial.

Ce qu’il faut bien comprendre à ce stade est que ici nous avons réalisé une déduction du noyau à partir d’une fonction phi que nous avions pre-déterminé comme fonctionnant pour linéariser les données. Mais en fait, l’astuce du noyau est justement de ne pas faire cela et d’utiliser directement un noyau sans en connaître à priori la fonction phi.

Alors bien sur sachant qu une fonction phi efficace dans la linéarisation est complètement conditionné par la nature des données initiales, il en va de même pour le noyau.

Ainsi selon les données d’entrée, il faudra choisir un modèle de noyau adapté capable de rendre compte des même propriétés que le produit scalaire des données d'entrées sur lesquelles on applique une fonction de redescription.

Heureusement pour nous faciliter la vie, un certain nombre de noyau prédéfini on été élaboré et généralisé. Ainsi si tout à l’heure nous avons construit un noyau polynomial, il en existe d’autres comme le linéaire, polynomial, gaussien, exponentiel, laplacien, etc [5].

Nous ne les détaillerons pas ici car cela n'a pas d'intérêt.

Références

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2018/12/ia-classification-svc-avec-scikit-learn.html
[2] https://zestedesavoir.com/tutoriels/1760/un-peu-de-machine-learning-avec-les-svm/#5-systemes-non-lineaires--astuce-du-noyau
[3] https://towardsdatascience.com/the-kernel-trick-c98cdbcaeb3f
[4] https://un-est-tout-et-tout-est-un.blogspot.com/2019/04/math-linearite-or-not-linearite.html
[5] http://crsouza.com/2010/03/17/kernel-functions-for-machine-learning-applications/