Intelligence Artificielle

Shadow IT : définition, risques, et comment reprendre la main avec l’IA

Il y a une question que tout responsable informatique finit par se poser un jour : combien d’outils tournent dans mon entreprise sans que je le sache ? La réponse est rarement rassurante. Le shadow IT — littéralement « informatique de l’ombre » — désigne l’ensemble des outils, services et applications utilisés par les collaborateurs sans l’accord, ni même parfois la connaissance, du service informatique. Ce n’est pas un phénomène marginal ni une indiscipline : c’est devenu une façon normale de travailler, et l’arrivée des outils d’intelligence artificielle l’a accéléré.

Définition : ce que recouvre exactement le shadow IT

Un tableur partagé sur un compte personnel, un service de signature électronique souscrit avec une carte bancaire de service, une extension de navigateur qui résume des réunions, un outil de gestion de projet essayé par une équipe puis adopté par trois autres : tous ces cas relèvent du shadow IT. La définition est large, parce que le phénomène l’est aussi. Dès qu’une ressource informatique est utilisée hors du périmètre validé, avec des données de l’entreprise, on est dedans.

Depuis deux ans, une catégorie particulière a pris de l’ampleur : le shadow AI. Un collaborateur ouvre un compte personnel sur un assistant conversationnel, y colle un contrat, un extrait de code ou une liste de clients, et obtient en trente secondes ce qui aurait demandé une réunion. Aucune validation, aucune trace, aucun contrôle sur ce que devient le texte envoyé. C’est le même mécanisme que le shadow IT classique, avec une différence de taille : la donnée sort sous forme de texte brut, souvent sans que personne n’y pense.

Pourquoi les collaborateurs contournent l’informatique

Il serait confortable de n’y voir qu’un problème de discipline. C’est faux. Le shadow IT apparaît presque toujours là où la demande légitime rencontre un délai. Un service a besoin d’un outil pour livrer dans trois semaines ; le processus interne demande deux mois et un comité. La personne tranche, paie avec sa carte, et livre. Le mécanisme se répète à chaque fois que l’informatique est perçue comme un ralentisseur plutôt qu’un accélérateur.

S’y ajoutent trois accélérateurs : le paiement à l’usage, qui rend la souscription invisible dans les budgets ; le télétravail, qui brouille la frontière entre l’ordinateur personnel et professionnel ; et l’essor des outils sans installation, accessibles depuis un simple navigateur.

Les implications réelles, au-delà du principe

Sécurité et données. Un outil non validé n’est pas couvert par les engagements de l’entreprise : ni chiffrement négocié, ni clause de localisation, ni engagement de non-entraînement sur vos données. Ce dernier point est décisif avec les IA grand public : selon les conditions d’utilisation, vos textes peuvent servir à améliorer le modèle. Vous avez confié votre propriété intellectuelle sans le savoir.

Conformité. Le RGPD ne s’arrête pas à la porte du service informatique. Un outil qui traite des données personnelles crée un sous-traitant de fait, sans contrat de sous-traitance, sans registre, sans analyse d’impact. Le règlement européen sur l’IA ajoute une couche : un système d’IA utilisé pour qualifier des candidats ou des clients n’a pas le même statut qu’un correcteur orthographique.

Coût. Les licences se multiplient sans consolidation. Il n’est pas rare de découvrir trois outils payants qui font la même chose, dans trois services différents, chacun persuadé d’avoir trouvé la solution.

Dépendance. Une automatisation construite par un collaborateur sur un outil no-code s’arrête le jour où il quitte l’entreprise. Personne ne sait ce qu’elle faisait, ni comment la reconstruire. C’est le coût le plus souvent sous-estimé : la connaissance part avec la personne.

Angle mort. Ce qui n’est pas connu ne peut être ni sauvegardé, ni supervisé, ni corrigé. Le vrai risque du shadow IT n’est pas l’outil lui-même : c’est l’absence de visibilité.

Comment adresser le problème : quatre étapes

1. Découvrir avant d’interdire. On ne gouverne pas ce qu’on ignore. Les sources d’information sont multiples : journaux des passerelles web, journaux d’authentification unique, notes de frais, extensions installées sur les navigateurs, inventaire des boîtes mail. L’objectif n’est pas de dresser une liste de coupables, mais une cartographie.

2. Qualifier le risque. Tous les outils ne se valent pas. Une grille simple suffit : quelles données circulent, quelle serait la conséquence d’une fuite, et que se passe-t-il si l’outil disparaît demain. Un éditeur de polices n’a rien à voir avec un outil qui stocke des dossiers clients.

3. Canaliser plutôt qu’interdire. Une interdiction déplace le problème vers des canaux encore moins visibles. La méthode qui fonctionne est inverse : proposer un chemin validé plus rapide que le chemin sauvage. Un catalogue d’outils approuvés, une procédure de validation en quelques jours, un budget d’expérimentation encadré. Si l’alternative officielle est plus lente, le shadow IT reviendra — c’est mécanique.

4. Encadrer l’usage de l’IA explicitement. Une charte d’usage qui dit ce qu’on peut mettre dans un assistant, ce qu’on ne peut pas, et vers quel outil se tourner. Sans cette clarté, chacun improvise, et l’improvisation est toujours plus risquée que la règle.

Ce que l’IA change dans la façon de traiter le sujet

L’intelligence artificielle est d’abord le problème. Elle devient aussi une partie de la réponse, à condition de l’utiliser sur des tâches précises.

Cartographier et classer. Un modèle lit des centaines de domaines ou de noms de services et les classe par fonction : gestion de projet, traduction, signature, support. Ce qui prenait des semaines de recoupement manuel devient une après-midi, et les doublons apparaissent immédiatement.

Lire les conditions générales. Les contrats des éditeurs font quarante pages. Un modèle les parcourt et remonte les points qui comptent : où sont hébergées les données, sont-elles utilisées pour entraîner le modèle, combien de temps sont-elles conservées, quelles sont les modalités de résiliation. C’est exactement le travail qu’aucune équipe informatique n’a le temps de faire pour chaque outil.

Rendre la règle interrogeable. Une charte rangée dans un intranet ne sert à rien. La même charte, indexée et interrogeable en langage naturel, répond à la question réelle du collaborateur : « puis-je envoyer ce document à cet outil ? ». La réponse arrive en dix secondes, au moment où la décision se prend.

Détecter les sorties sensibles. Les outils de prévention de fuite de données classent désormais les contenus sortants avec des modèles : données personnelles, code source, contrats, coordonnées bancaires. On passe d’une liste de mots-clés à une compréhension du contexte.

Le paradoxe à garder en tête. On ne traite pas le shadow IT avec du shadow AI. Si l’entreprise déploie un assistant interne sans validation, sans clause de confidentialité et sans contrôle, elle reproduit exactement le problème qu’elle prétend résoudre.

Ce qu’il faut retenir

Le shadow IT n’est pas un accident de parcours, c’est le symptôme d’un écart entre ce que les équipes attendent de l’informatique et ce qu’elle parvient à fournir dans les délais. L’objectif n’est pas d’atteindre zéro outil non validé — cet objectif est inatteignable et pousserait le phénomène sous terre. L’objectif est de savoir ce qui existe, de qualifier ce qui compte, et de rendre le chemin officiel plus rapide que le chemin sauvage.

L’IA ne remplace pas cette gouvernance. Elle la rend praticable à une échelle où l’analyse manuelle ne suffit plus : classer, lire, répondre, détecter. Encore faut-il décider de regarder.

Piloter un téléphone Android depuis Hermes avec ADB et phone-harness

Un agent qui rédige, qui cherche, qui range — mais qui reste incapable de toucher un téléphone. C’est la limite que le plugin phone-harness fait sauter : il ajoute à Hermes un outil, phone_exec, qui exécute un court script Python sur un vrai téléphone et renvoie ce que l’écran affiche. Ouvrir une application, saisir du texte, faire défiler, lire une liste, prendre une capture : tout passe par le même canal.

Ce tutoriel couvre l’installation complète : ADB sur Windows, macOS et Linux, la préparation du téléphone, la connexion filaire pour Android 10, la connexion WiFi pour Android 11 et plus, l’accès hors du réseau local, puis l’installation du plugin dans Hermes.

Ce que fait réellement phone-harness

Le plugin n’embarque pas de pilote maison : il s’appuie sur ADB, l’outil officiel d’Android, et sur le paquet phone_harness qu’il installe dans l’environnement de Hermes. L’agent écrit un script, le plugin le transmet, le téléphone l’exécute, la sortie revient.

phone_exec(code='''
ensure_device()
open_app("Horloge")
wait_stable()
print([o["text"] for o in screen()][:10])
''')

Ce qui remonte n’est pas une capture d’écran abstraite mais le texte réel des éléments affichés. L’agent travaille donc sur des données, pas sur des images — et quand une capture est nécessaire, elle est enregistrée et rattachée au résultat.

Qu’est ce que ADB?

ADB signifie Android Debug Bridge, et son nom décrit exactement ce qu’il fait : un pont entre un ordinateur et un appareil Android. Ce pont repose sur trois composants qui fonctionnent ensemble. Le premier est un client — c’est la commande adb que vous tapez, qui n’a qu’une durée de vie, celle de son exécution. Le deuxième est un serveur, un démon lancé sur l’ordinateur dès le premier appel, qui écoute en local sur le port 5037 et reste actif tant qu’on ne l’arrête pas. Le troisième est adbd, un démon qui tourne en permanence sur le téléphone lui-même.

Le client ne parle jamais directement au téléphone. Il transmet sa commande au serveur local, qui se charge de trouver l’appareil et de router la demande. C’est ce qui explique qu’une même commande puisse viser plusieurs téléphones branchés en même temps : le serveur tient le registre des appareils disponibles. La liaison entre le serveur et adbd se fait soit par USB, soit par TCP/IP — c’est cette seconde option qui permet le débogage sans fil. Avant d’établir la connexion, ADB vérifie l’identité de l’ordinateur par une paire de clés RSA : au premier branchement, le téléphone affiche une empreinte et demande votre accord, puis conserve la clé publique. C’est pour cette raison qu’un appareil fraîchement branché apparaît en unauthorized tant que vous n’avez pas validé la fenêtre, et en offline lorsque le démon ne répond plus.

Ce que ce pont ouvre est considérable : adb shell donne un interpréteur de commandes sur le téléphone, sous l’utilisateur shell et non root, ce qui suffit déjà à lister les applications, lire des fichiers, en installer, capturer l’écran ou simuler une saisie. C’est exactement ce que le plugin phone-harness exploite : il n’invente pas de protocole, il s’appuie sur ce pont officiel, fourni par Google et utilisé par Android Studio. Et c’est aussi ce qui justifie la mise en garde de ce tutoriel — un serveur ADB joignable depuis Internet n’exige aucun mot de passe pour exécuter des commandes sur l’appareil.

Installer ADB sur l’ordinateur

ADB fait partie des SDK Platform-Tools publiés par Google. Les liens de téléchargement officiels sont stables : ils pointent toujours vers la version la plus récente.

Windows

Le plus rapide, si vous disposez de winget :

winget install --id Google.PlatformTools -e

Sinon, téléchargez platform-tools-latest-windows.zip depuis la page officielle des Platform-Tools, puis extrayez l’archive dans C:\platform-tools — à la racine du disque, sans espace dans le chemin. Vérifiez ensuite :

cd C:\platform-tools
.\adb.exe version

Pour utiliser adb depuis n’importe quel dossier, ajoutez le répertoire au PATH utilisateur :

[Environment]::SetEnvironmentVariable("Path",
  [Environment]::GetEnvironmentVariable("Path","User") + ";C:\platform-tools", "User")

macOS

brew install android-platform-tools
adb version

Sans Homebrew, la même archive existe en version darwin. Extrayez-la dans ~/platform-tools et ajoutez cette ligne à ~/.zshrc :

export PATH="$HOME/platform-tools:$PATH"

À noter : sur Mac, un iPhone ne passe pas par ADB mais par iPhone Mirroring, qui exige les autorisations Accessibilité et Enregistrement d’écran.

Linux

sudo apt install android-tools-adb
adb version

Fedora : sudo dnf install android-tools. Arch : sudo pacman -S android-tools. Sur Debian et Ubuntu, ajoutez votre utilisateur au groupe plugdev, sans quoi adb devices répondra no permissions :

sudo usermod -aG plugdev $USER

Préparer le téléphone

La manipulation est identique sur tous les Android. Ouvrez Paramètres → À propos du téléphone → Numéro de build et touchez sept fois : le système confirme que le mode développeur est activé. Les Options pour les développeurs apparaissent alors dans Paramètres → Système.

C’est là que tout se joue, et le réglage diffère selon la version d’Android.

Android 10 : par USB, puis sans fil

Activez Débogage USB, branchez le câble, choisissez le mode Transfert de fichiers si le téléphone le demande, puis acceptez la fenêtre « Autoriser le débogage USB ? » en cochant « Toujours autoriser pour cet ordinateur ».

adb devices
# R58M12345AB    device

Android 10 ne connaît pas le débogage sans fil avec code d’association : il faut passer par le réseau en une seule commande, câble encore branché.

adb tcpip 5555
# restarting in TCP mode port: 5555

Débranchez ensuite le câble et connectez-vous à l’adresse réseau du téléphone :

adb connect 192.168.1.42:5555
adb devices
# 192.168.1.42:5555    device

Le port 5555 reste fixe, mais il ne survit pas à un redémarrage du téléphone : il faut rebrancher le câble et relancer adb tcpip 5555.

Android 11 et plus : en WiFi

Depuis Android 11, le débogage sans fil est natif — et il n’utilise jamais le port 5555. Activez Débogage sans fil, puis touchez « Associer l’appareil avec un code » : le téléphone affiche une adresse, un port d’association et un code à six chiffres.

adb pair 192.168.1.42:37421
# saisir le code à six chiffres

Revenez ensuite à l’écran principal du débogage sans fil : il affiche un autre port, celui de connexion, différent du port d’association.

adb connect 192.168.1.42:41037
adb devices

Deux erreurs classiques ici. Utiliser le port 5555 renvoie connection refused, puisque rien n’écoute sur ce port. Et confondre le port d’association avec le port de connexion donne le même résultat : les deux chiffres sont affichés à deux endroits différents de l’écran.

Sortir du réseau local sans ouvrir de port

ADB accepte n’importe quelle adresse joignable, y compris hors du domicile — à condition de passer par un tunnel. Tailscale fait très bien l’affaire : installez-le sur le téléphone et sur l’ordinateur, puis utilisez l’adresse du tailnet à la place de l’IP locale.

adb connect 100.87.208.118:5555

Ne publiez jamais le port 5555 sur Internet. ADB n’applique aucune authentification : quiconque atteint le port peut exécuter des commandes sur le téléphone. Derrière un tunnel WireGuard, le problème disparaît ; en redirection de port sur la box, il devient une porte ouverte.

Installer le plugin dans Hermes

Dans un terminal, sur la machine où tourne Hermes :

hermes plugins install ShawnPana/phone-harness/integrations/hermes

Répondez oui à l’activation, puis fermez et relancez l’application : les outils ne se chargent qu’au démarrage, et une nouvelle conversation est nécessaire pour qu’ils apparaissent.

Déclarez enfin la plateforme, pour qu’Hermes sache quel type d’appareil piloter. Sous Windows :

[Environment]::SetEnvironmentVariable("PHONE_HARNESS_PLATFORM","android","User")

Sous macOS ou Linux :

export PHONE_HARNESS_PLATFORM=android

Premier essai

Dans une nouvelle conversation, demandez la lecture de l’écran :

phone_exec(code='''
ensure_device()
print(screen()[:5])
''')

Hermes affiche le script et demande votre accord avant de l’exécuter. Ce n’est pas une formalité : le plugin fait tourner du Python écrit par le modèle sur votre ordinateur, avec vos droits. L’approbation peut être accordée une fois, pour la session, ou de façon permanente.

Les limites à connaître

  • Aucun mot de passe, code PIN ou 2FA. Le plugin ne les saisit pas : il rend le téléphone à l’utilisateur pour ces étapes, et c’est volontaire.
  • iPhone : Mac uniquement. Le pilotage passe par iPhone Mirroring, absent de Windows et de Linux.
  • Le mode automatique est une heuristique. Il évite l’approbation pour les scripts qui ne touchent qu’au téléphone, mais il est contournable : à laisser désactivé par défaut.
  • Un téléphone par session. Les appels concurrents attendent leur tour, et au-delà de deux minutes l’outil signale l’appareil comme occupé.

Ce que ça change vraiment

La valeur n’est pas de télécommander un téléphone pour le plaisir. Elle est de donner à un agent une surface de plus : lire un écran d’application qui n’expose pas d’API, déclencher une action dans un service mobile, vérifier qu’un parcours utilisateur fonctionne réellement. C’est la différence entre un agent qui parle du monde et un agent qui y agit.

Un réceptionniste IA pour une TPE : qu’est-ce que ça change vraiment ?

Quand on tient un garage, un cabinet ou un atelier, le téléphone sonne toujours au mauvais moment. Sous la voiture, en rendez-vous, en pleine pose. On laisse sonner, et l’appel part chez le concurrent. Ce n’est pas un problème de savoir-faire, c’est un problème de disponibilité.

C’est là qu’un réceptionniste IA entre en jeu. Mais comme le mot « IA » sert aujourd’hui à tout vendre, mieux vaut regarder ce qu’il fait précisément.

Il ne décroche pas à votre place, il accueille à votre place

Un réceptionniste IA ne prend pas de décision à votre place et ne vend rien. Il fait trois choses simples : il décroche quand vous ne pouvez pas, il comprend ce que la personne appelle pour demander, et il vous transmet l’essentiel — qui a appelé, pour quoi, et à quelle heure elle souhaite être rappelée.

La nuance compte. Vous ne récupérez pas une liste d’appels manqués : vous récupérez des demandes qualifiées. Un client qui explique son besoin à un agent, c’est un client qui a déjà commencé la conversation avec vous.

Ce qu’il ne faut pas en attendre

Un agent mal configuré est pire qu’une messagerie : il donne l’impression d’être pris en charge, et il ne l’est pas.

Donc, par ordre d’importance : il ne remplace pas votre relation client, il ne décide pas du prix d’une intervention, il n’arbitre pas un cas litigieux. Sur tout ce qui engage votre responsabilité ou votre chiffre d’affaires, il doit passer la main — et le dire clairement à l’appelant.

La question à poser avant de se lancer

Avant de parler de technologie, une seule question utile : que se passe-t-il aujourd’hui quand un appel arrive et que vous êtes occupé ?

Si la réponse est « rien, et je rappelle le soir », l’agent a du sens. Si la réponse est « ma messagerie fait déjà le travail et je rappelle tout le monde », l’IA n’est pas votre problème — votre organisation l’est peut-être. C’est ce travail d’analyse, appel par appel, qui décide de la suite : qui appelle, à quelle fréquence, pour demander quoi, et ce que devient sa demande quand personne ne décroche.

Ce que je regarde, en tant que formateur

J’apprends à des développeurs et à des porteurs de projet à construire ce genre d’outils. Entre ce qu’on voit dans les démonstrations et ce qui tient en production, il y a un écart que peu de vidéos montrent : la qualification d’un motif d’appel, la gestion des limites, le suivi après la mise en service.

Des projets comme celui d’Intelligentiae, conçu et suivi depuis Toulouse, partent du bon bout : comprendre le métier et les appels avant de choisir un outil, définir un périmètre explicite, et rester présent après la mise en route. C’est la différence entre une démonstration et un service qui rend service.

Un dernier point, qui vaut pour toute automatisation : ce qui se mesure, s’améliore. Si personne ne regarde ce que l’agent a réellement pris en charge au bout d’un mois, vous n’avez rien gagné — vous avez juste déplacé le problème. Demandez toujours les chiffres avant, pendant, et après.

Formation au vibe coding développez avec l’IA

Pourquoi apprendre le vibecoding?

Le vibecoding, qui consiste à parler à une IA pour générer du code fonctionnel, a transformé l’équilibre des forces entre les sachants et les non sachant coder. En effet le développement d’application était réservé à une minorité de personnes qui ont mis des années à parfaire leurs connaissances en code. L’arrivée des LLM (modèle larges de données) a permis à tout un chacun de développer des application web sans vraiment avoir passsé des années à apprendre et pratiquer le code.

Est ce à dire qu’il n’est plus besoin de savoir lire du code basique ou d’écrire du code basique pour développer une application? Rien n’et moins sûr, en effet, ce que vous voyez dans les réseaux sociaux montre des petites applications simples développée à partir de zéro.

Le jeu se complique lorsque l’application grandit. Or vous ne pouvez pas ignorer votre oeuvre, il faut au moins en maitriser les grandes lignes. au fur et à msur qu’il grandit, que vous ajoutez des fonctionnalité, il faut pouvoir suivre son évolution, comme un enfant qui grandit il faut savoir l’appréhender sinon il va mal finir !

Public cible :

Porteur de projet à la recherche de prototype fonctionnel, SEO, Marketeur, curieux passionné mais ne sachant pas encore coder, créateur d’entreprise, indépendants. Chef de projet, product Owner,développeur, chef d’entreprise TPE/PME

Objectif de la formation : à l’issue de la formation, vous saurez comment fonctionne un LLM, la génération de code, comment prompter un LLM pour générer une application sécurisée. Vous saurez déployer cette application en ligne. La maintenir et la faire évoluer.

Programme de formation

Comprendre les LLM et le prompting

Maitriser les outils de vibecoding : Lovable pour les interfaces, Antigravity, Cursor, Windsurf,Claude Code

Générer tester et corriger du code

construire un mini projet fonctionnel

intégrer le workflow avec Git et tests

Déployer l’application en ligne

Mettre en place un serveur MCP en Python 3.13

Qu’est ce qu’un serveur MCP (Model Context Protocole?)

Un serveur MCP est un webservice qui fournit de la data, cette data va servir de contexte pour l’IA générative. A la différence du RAG (Retrieval Augmented Generation), il n’est pas besoin de base vectorielle pour stocker des informations.

Le workflow est le suivant, le client IA (Claude Desktop) va requêter le serveur MCP pour retirer les informations dont il a besoin pour envoyer au LLM.(chatGPT par exemple).

Cette technologie inventée par Anthropic (à l’origine de Claude) est devenue très populaire. Je pense que c’est dû au fait qu’il n’est besoin que de connaissances existantes (Webservice) par les développeurs pour pouvoir taquiner les IA afin d’éviter les hallucination.

Mais cela va au delà des hallucination, un serveur MCP est un webservice à destination non du programmeur mais de l’IA générative. Je pense que c’est une bonne comparaison, l’IA (ou plutôt l’agent IA) va pouvoir décider en autonomie de requêter un serveur MCP pour chercher les information dont il a besoin?

Donc un serveur MCP est un webservice pour agent IA, au même titre qu’un webservice est un …. webservice pour un programmeur. Dans notre cas on va utiliser Claude Desktop comme « agent IA » ou plutôt comme Client IA.

Mise en place du serveur MCP dans Pycharm

Création de mon environnement virtuel

On crée le répertoire MCP puis on fait la commande :

vous avez un répertoire appelé MCP rentrez dedans

$ python -m venv .venv

# activez l'environnement virtuel (sous powershell)
$ .\.venv\Scripts\Activate.ps1

#c'est activé (normalement) vous avez accès à la commande pip
$ pip --version

Installation de uv

uv est un gestionnaire de paquet nouvelle génération écrite en RUST.

pip install uv

puis avec uv on installe mcp 

uv add "mcp[cli]"

A partir de maintenant on va utiliser uv pour installer notre projet

uv init 

# la commande tree -L 2 nous donne la structure suivante
.
|-- README.md
|-- main.py
|-- .venv
|   |-- Include
|   |-- Lib
|   |-- Scripts
|   `-- pyvenv.cfg
|-- pyproject.toml
`-- uv.lock

Pour install tree dans Windows Gitbash, il faut télécharger l’EXE et le mettre dans C:\Program Files\Git\usr\bin, un tuto est accessible sur ce lien.

Le soucis c’est que mcp s’attends à ce que le dossier environnement virtuel soit nommé .venv, si vous faites la commande:

$ uv run mcp
warning: `VIRTUAL_ENV=mcpvenv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead


Désativez l'environnement avec 
$ deactivate
si un .venv est présent, exécutez le venv  de ce répertoire.

Exemple rapide de serveur MCP:

#server.py

from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo")


# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b


# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"


# Add a prompt
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
    """Generate a greeting prompt"""
    styles = {
        "friendly": "Please write a warm, friendly greeting",
        "formal": "Please write a formal, professional greeting",
        "casual": "Please write a casual, relaxed greeting",
    }

    return f"{styles.get(style, styles['friendly'])} for someone named {name}."

Pour le faire tourner

mcp dev server.py

#il vous sera demandé d'installer le paquet @modelcontextprotocol/inspector@0.17.0

Source: https://composio.dev/blog/mcp-server-step-by-step-guide-to-building-from-scrtch

Installer votre propre IA avec Ollama sur votre ordinateur (Windows)

Installation de Ollama

Pour commencer il faut aller sur le site Ollama.com (à ne pas confondre avec Llama.com). Le lien Github, pour consulter les sources et la documentation sur les modèles

Téléchargez et installez. Une fois installé, vous aurez une opup comme ci-dessous :

Vous allez vous perdre dans la jungle des modèles. Vous connaissez sans doute aussi de nom Hugging Face, c’est un repository de modèle LLM open source. Une fois que vous avez installé Ollama, ouvrez un terminal et tapez la commande $ ollama, une liste de commande sera affichée. Allez sur le Github, vous verrz un tableau de modèles LLM, différentes capacité et taille, ça va de 1 Go à 404 Go ! En fonction de votre machine téléchargez celui que vous pouvez faire fonctionner !

La colonne parameters indique combien de paramètres est considéré par le modèle, comptez environs 1 giga de RAM pour 1 Giga (Billion milliards) de paramètres. Mon ordinateur fait 32 Giga de RAM, je peux aller avec le modèle à 30B paramètres.

Les commandes pour installer les LLM

ollama run llama2


Pour connaitres les différentes commande au sein du chat
/?
>>> /?
Available Commands:
  /set            Set session variables
  /show           Show model information
  /load <model>   Load a session or model
  /save <model>   Save your current session
  /clear          Clear session context
  /bye            Exit
  /?, /help       Help for a command
  /? shortcuts    Help for keyboard shortcuts

Use """ to begin a multi-line message.


Pour quitter :
/bye

Pour lister les modèles :
ollama list

.

Ecran du téléchargement du modèle, à la vue de la sortie terminal, ça ressemble à une image Docker

Test de llama2

La première chose que je constate, est que le modèle est vraiment limité, surtout quand vous avez l’habitude de vous amuser avec chatGPT ou Claude sur le web, donc sur les modèles les plus sophistiqués.

Par exemple je lui demande de me donner la recette du roesti, il me répond qu’il ne sait pas

Quand je lui demande la recette de la tarte tatin, il me liste en anglais (pas vraiment multilingue, ok pour la taille du modèle cependant !). Par contre comme il est hébergé en local il est très rapide. Je lui demande pourquoi il me répond qu’il n’a pas réussi à détecter (alors que je lui ai demandé en français), mais il me propose de donner la version française ce qu’il fit très bien.

Nous allons télécharger un autre modèle

Cette fois ci nous allons télécharger le modèle Mistral, équivalent en taille. Sur le repository, vous trouverez la commande dans le tableau

ollama run mistral

Après le téléchargement, il démarre le prompt automatiquement. Quand vous ferez bye, vous pourrez démarrer entre les deux modèles (Mistral ou llma2 en ligne de commande)
Pour effacer un modèle, vous avez la comamnde remove, mais pensez à faire ollama pour afficher les différentes commandes.

Et maintenant explorons l’API !

Eh oui c’est là que cela devient très intéressant car nous allons pouvoir exploiter un LLM depuis un programme pour automatiser nos tâche (notre GRAAL)

Ollama expose une API sur le localhost. Dans la barre des tâches, vous pouvez voir une image de Llama, cela veut dire que l’application Desktop est lancée, et donc que l’API est active. Mais si le desktop n’est pas llancé, vous pouvez toujours lancer le serveur en ligne de commande

ollama serve

En parlant de l’icone, vous pouvez accéder aux paramètres

Vous remarquez (j’espère) le slider pour définir la « mémoire » du LLM (la taille du contexte), ceci est utile pour se souvenir d’une conversation !

Vous pouvez exposer le LLM au réseau local (très intéressant !)

Installer le package Python ollama

Très simplement on va faire un script pour requêter Ollama par API avec pip install ollama. Si vous installez le paquet après avoir démarré l’environnement virtuel, il sera en local, si vous le faites hors environnement virtuels, il sera dans le système. Pour connaitre où est installé un package

#affiche les emplacements pouvant héberger les paquets
python -m site

# cherche un paquet en particuliers
python -m pip show <paquet>

#lister les paquets de l'environnement virtuel en cours
pip list
import ollama
client = ollama.Client()

model = "llama2"
prompt = "How to set up a venv?"

response = client.generate(model, prompt)
print(f"Response from {model}:")
print(response.response)

Si vous préférez que la réponse arrive morceau par morceau voici le script (il suffit de passer stream= True)

import ollama

client = ollama.Client()

model = "llama2"
prompt = "How to set up a venv?"

# Use the stream=True option
stream = client.generate(model=model, prompt=prompt, stream=True)

print(f"Response from {model}:\n")

for chunk in stream:
    # Each chunk is a small piece of the model's output
    print(chunk['response'], end='', flush=True)

print()  # final newline

Ok là c’est mieux mais on peut wrapper les phrases (retour à la ligne quand ça déborde)

Avec retour à la ligne sans streaming

import ollama
import textwrap
import shutil

client = ollama.Client()
model = "llama2"
prompt = "How to set up a venv?"

response = client.generate(model=model, prompt=prompt)
terminal_width = shutil.get_terminal_size().columns

print(f"Response from {model}:\n")
print(textwrap.fill(response['response'], width=terminal_width))

Avec retour à la ligne avec streaming

import ollama
import shutil
import sys

client = ollama.Client()
model = "llama2"
prompt = "How to set up a venv?"

stream = client.generate(model=model, prompt=prompt, stream=True)

print(f"Response from {model}:\n")

terminal_width = shutil.get_terminal_size().columns
current_line = ""

for chunk in stream:
    text = chunk["response"]
    for char in text:
        current_line += char
        if len(current_line) >= terminal_width - 1:
            print(current_line)
            current_line = ""
# print remaining text
if current_line:
    print(current_line)

Personnalisation du modèle

Avec un fichier de configuration, on peut agir sur la façon dont les réponse vont être apportées, il y a de nombreux paramètres qui font qu’un LLM va se comporer d’une certianes façon. Vous avez peut être remarqué que ChatGPT était plus jovial que Claude qui est plus sérieux. Perso j’aim bien cette touche.

Mettez Le fichier de personnalisation Modefile dans un répertoire et invoquez une commande sur ce fichier dans ce répertoire.

$ ollama create formabot -f ./Modelfile

# ici j'ai invoqué un modèle non présent ça met du temps à télécharger le modèle

On va lancer avec la commande run

$ ollama run formabot

>>> qui es tu?



# pour effacer le modèle
$ ollama rm formabot

Conclusion :

Dans sa forme, l’outil ressemble beaucoup à Docker (les commandes, les modèles). Voilà pour une première introduction de Ollama. Pourquoi est ce important d’avoir un LLM en local? Si vous voulez rester privé, avoir des temps de réponse rapide, et ne pas vous ruiner en crédit token, alors n’hésitez pas à utiliser un LLM open source, il y en a d’autres allez voir du côté de Deepseek et de HuggingFace.

Tuto Réalisez un serveur MCP Model Context Protocol

Dans quelle situation un serveur MCP est pertinent?

Un modèle LLM comme ChatGPT est généraliste et a été entrainé sur un dataset limité bien que grand. Il ne dispose pas d’une connaissance universelle. Mais avez vous remarqué qu’une IA a toujours réponse à tous néanmoins? quitte à vous sortir des insanités? qu’on appelle des hallucinations.

Pour éviter ce phénomène d’hallucination, qui résulte d’un trou dans sa connaissance, il faut pouvoir compléter avec des données supplémentaires. Seulement voilà, le modèle de chatGPT a été entrainé déjà (et c’est ultra coûteux d’entrainer une IA, tout le monde ne dispose pas les moyens de le faire.

La solution du RAG (Retrieval Augmented Generation)

Le principe repose sur le stockage dans une base vectorielle des données, avec une base de données vectorielle comme FAISS ou ChromaDB, puis de prompter la base au lieu de prompter chatGPT, le résultat de ce prompt est envoyé à chatGPT. (prompt augmenté).

C’est une bonne solution, et vraiment adapté à certaines situation comme chatPDF. Cependant c’est un peu lourd. Nous allons voir qu’Anthropic propose une solution qui marche dans le sens inverse mais qui semble être plus légère et performante (quoique différent).

La solution MCP (Model Context Protocol)

Cette solution à base de données structurée (et c’est là la différence avec la RAG) requiert à Claude (et non chatGPT puisque qu’au moment d’écrire cet article chatGPT ne le prend pas encore en charge), de faire un sorte de requête AJAX avec un serveur MCP pour contextualiser la réponse à un prompt.

MCP ouvre tout un univers de champs d’application très intéressant, tout en préservant l’anonymat des données.

Exemple de code en Python d’un serveur MCP

Assistant RH intelligent

rh-assistant-mcp/
├── employee_db.json
├── mcp_server.py
├── mcp_manifest.json

Notre serveur MCP est simplement un serveur REST, le différence c’est ce n’est pas vous qui requêtez en AJAX mais c’est Claude qui va le requêter (on comprend mieux pourquoi les autres vendors d’IA ne l’ont pas encore implémenté)

employee_db.json est la base de données, mcp_manifest/json va décrire la façon dont les données seront formatées en input (venant de Claude) et en output (allant vers Claude).

{
  "employees": [
    {
      "id": "E001",
      "name": "Alice Dupont",
      "role": "Développeuse Backend",
      "hire_date": "2021-06-12",
      "skills": ["Python", "Django", "PostgreSQL"]
    },
    {
      "id": "E002",
      "name": "Bruno Martin",
      "role": "Data Analyst",
      "hire_date": "2022-01-20",
      "skills": ["SQL", "Power BI", "Python"]
    }
  ]
}

Le fichier manifest.json qui décrit les format d’entrée et sortie

{
  "name": "employee-context",
  "description": "Provides structured context about employees from the HR database.",
  "version": "1.0",
  "input_schema": {
    "type": "object",
    "properties": {
      "employee_name": {
        "type": "string",
        "description": "Name of the employee to look up"
      }
    },
    "required": ["employee_name"]
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "employee_info": {
        "type": "object",
        "properties": {
          "role": { "type": "string" },
          "hire_date": { "type": "string" },
          "skills": { "type": "array", "items": { "type": "string" } }
        }
      }
    }
  }
}

Le fichier serveur avec FastAPI

from fastapi import FastAPI, Request
import json

app = FastAPI()

with open("employee_db.json", "r") as f:
    db = json.load(f)

@app.post("/mcp/context")
async def provide_context(request: Request):
    body = await request.json()
    name = body.get("employee_name", "").lower()

    for emp in db["employees"]:
        if emp["name"].lower() == name:
            return {
                "employee_info": {
                    "role": emp["role"],
                    "hire_date": emp["hire_date"],
                    "skills": emp["skills"]
                }
            }
    return {"employee_info": None}

Pour tester rapidement le serveur:

curl -X POST http://localhost:8000/mcp/context   -H "Content-Type: application/json"   -d '{"employee_name": "alice dupont"}'

Comment déployer ce système

Pour que Claude puisse lire le manifest.json, on peut le stocker sur une github page, ou un serveur accessible publiquement. On va notifier cette url à Claude via son interface graphique ou par API.

Le serveur REST doit être accessible publiquement, si vous le déployez en local il faut faire un tunnel vers votre serveur.

Comment utiliser ce système

Il faut que ce soit clair que dans ce cas de figure vous ne pouvez requêter Claude que par API. Quand est ce que Claude va savoir qu’il va avoir besoin de requêter le serveur MCP? C’est lui qui détermine quand il faut requêter le serveur MCP, quand il va s’apercevoir que le contexte lui manque et qu’il existe un serveur MCP décrit par le manifest.json existe.

Exemple de requête faite à Claude

vous pouvez faire en Javascript cette requête POST

POST /v1/messages
{
  "model": "claude-3-opus-20240229",
  "messages": [
    { "role": "user", "content": "Peux-tu me dire ce que fait Bruno Martin ?" }
  ],
  "context_providers": [
    { "url": "https://ton-domaine.com/mcp_manifest.json" }
  ]
}

Pour exposer le site sur un serveur il suffit de simplement déployer sur un serveur. Une alternative serait de créer un tunnel SSH pour exposer votre site local à Claude.

Quelques visuels pour évaluer le modèle

Je vous présente quelques graphique qui servent à avoir un insight dans le modèle ainsi que l’évaluation de la qualité du modèle, sa précision etc.

Confusion Matrix

Ceci graphique permet de voir el taux de faux positifs et de faux négatifs.

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix

import joblib
loaded_rf = joblib.load('random_forest_model.pkl')
#load the datas
import pickle
with open('X_test.pkl', 'rb') as f:
    X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
    y_test = pickle.load(f)

prediction = loaded_rf.predict(X_test)
# Create confusion matrix
cm = confusion_matrix(y_test, prediction)

# Plot confusion matrix
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False)
plt.title('Confusion Matrix')
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.show()

Receiver Operating Characteristics

from sklearn.metrics import roc_curve, auc

# Get probability scores for the positive class
if hasattr(loaded_rf, "predict_proba"):
    proba = loaded_rf.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, proba)
    roc_auc = auc(fpr, tpr)
    
    # Plot ROC curve
    plt.figure(figsize=(8, 6))
    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.05])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver Operating Characteristic (ROC) Curve')
    plt.legend(loc='lower right')
    plt.show()

Precision recall curve

from sklearn.metrics import precision_recall_curve, average_precision_score

# Get probabilities and calculate precision-recall
if hasattr(loaded_rf, "predict_proba"):
    proba = loaded_rf.predict_proba(X_test)[:, 1]
    precision, recall, _ = precision_recall_curve(y_test, proba)
    avg_precision = average_precision_score(y_test, proba)
    
    # Plot precision-recall curve
    plt.figure(figsize=(8, 6))
    plt.plot(recall, precision, color='blue', lw=2, label=f'Precision-Recall curve (AP = {avg_precision:.2f})')
    plt.xlabel('Recall')
    plt.ylabel('Precision')
    plt.title('Precision-Recall Curve')
    plt.legend(loc='lower left')
    plt.show()

Classification report

from sklearn.metrics import classification_report
import pandas as pd

# Get classification report as a dictionary
report = classification_report(y_test, prediction, output_dict=True)
report_df = pd.DataFrame(report).transpose()

# Plot classification report
plt.figure(figsize=(10, 6))
sns.heatmap(report_df.iloc[:-1, :].drop(['support'], axis=1), annot=True, cmap='Blues')
plt.title('Classification Report')
plt.tight_layout()
plt.show()

Feature importance

if hasattr(loaded_rf, "feature_importances_"):
    # Create a dataframe of feature importances
    feature_importance = pd.DataFrame({
        'feature': X_test.columns,
        'importance': loaded_rf.feature_importances_
    }).sort_values('importance', ascending=False)
    
    # Plot feature importances
    plt.figure(figsize=(10, 8))
    sns.barplot(x='importance', y='feature', data=feature_importance)
    plt.title('Feature Importance')
    plt.tight_layout()
    plt.show()

Overall Performance Metrics

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

accuracy = accuracy_score(y_test, prediction)
precision = precision_score(y_test, prediction, average='weighted')
recall = recall_score(y_test, prediction, average='weighted')
f1 = f1_score(y_test, prediction, average='weighted')

metrics = pd.DataFrame({
    'Metric': ['Accuracy', 'Precision', 'Recall', 'F1 Score'],
    'Value': [accuracy, precision, recall, f1]
})

# Plot metrics
plt.figure(figsize=(10, 6))
sns.barplot(x='Value', y='Metric', data=metrics, hue='Metric',palette='viridis')
plt.title('Model Performance Metrics')
plt.xlim(0, 1)
for i, v in enumerate(metrics['Value']):
    plt.text(v + 0.01, i, f'{v:.4f}', va='center')
plt.tight_layout()
plt.show()

Réutiliser un modèle Machine Learning généré

Afin de ne pas à avoir à calculer toujours le modèle pour déterminer si un client va partir ou non, on sauve le modèle entrainé et on on va le réutiliser

Charger le modèle

import joblib

#load the model
loaded_rf = joblib.load('random_forest_model.pkl')
print(loaded_rf)

Charger les données

Normalement nous chargeons de nouvelles données qu’on va donner à moudre au modèle, mais on n’en a pas, donc on va repasser les même données qu’on a sauvegardé.

#load the datas
import pickle

with open('X_test.pkl', 'rb') as f:
    X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
    y_test = pickle.load(f)

X_test.head()

Injection des données de test (les données de test n’ont pas servi à entrainer le modèle), on va obtenir une Series avec des 0 et 1

# Now you can use the loaded model to make predictions

from sklearn.metrics import accuracy_score
prediction = loaded_rf.predict(X_test)
#la série qui contient les outcome (churn si 1 , 0 si no churn)
print(prediction)

[0 1 1 1 0 0 0.... ]

Test sur une ligne (un client)

# test on row
# subset = X_test[X_test['TotalCharges']<100]
# extract a line from X_test
oneline = X_test.loc[[6125]]
#convert to dataframe
#oneline = oneline.to_frame()

import pandas as pd
isinstance(oneline,pd.DataFrame) # test if it's DataFrame
 
# affichage des informations de la ligne
print(oneline)
oneline.head()

#prediction sur la ligne
prediction = loaded_rf.predict(oneline)
print(prediction)

chatPDF clone sans base vectorielle

Cet exemple est plus facile à comprendre

Installer les module Python suivants

pip install PyPDF2
pip install openai
pip install tiktoken
#!/usr/bin/env python3
"""
ChatPDF Simple - Version corrigée pour OpenAI v1.0+
Permet d'extraire le texte d'un PDF et de poser des questions dessus
Sans base vectorielle
"""

import PyPDF2
from openai import OpenAI
import os
import argparse
import sys
from typing import List, Optional
import tiktoken
import re


class ChatPDF:
    def __init__(self, api_key: Optional[str] = None):
        """
        Initialise ChatPDF avec la clé API OpenAI
        """
        self.api_key = 'sk-proj-BBXFnLZzRMt8EZp4LStJXyfzc_6knvyK'
        if not self.api_key:
            raise ValueError("Clé API OpenAI requise. Définissez OPENAI_API_KEY ou passez api_key")

        self.client = OpenAI(api_key=self.api_key)
        self.pdf_content = ""
        self.pdf_path = ""
        self.encoding = tiktoken.get_encoding("cl100k_base")

    def extract_text_from_pdf(self, pdf_path: str) -> str:
        """
        Extrait tout le texte d'un fichier PDF
        """
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                text = ""

                print(f"Extraction du texte de {len(pdf_reader.pages)} pages...")

                for page_num, page in enumerate(pdf_reader.pages):
                    try:
                        page_text = page.extract_text()
                        text += f"\n--- Page {page_num + 1} ---\n{page_text}\n"
                    except Exception as e:
                        print(f"Erreur lors de l'extraction de la page {page_num + 1}: {e}")
                        continue

                self.pdf_content = text
                self.pdf_path = pdf_path
                print(f"Extraction terminée. {len(text)} caractères extraits.")
                return text

        except FileNotFoundError:
            raise FileNotFoundError(f"Fichier PDF non trouvé: {pdf_path}")
        except Exception as e:
            raise Exception(f"Erreur lors de la lecture du PDF: {e}")

    def chunk_text(self, text: str, max_tokens: int = 3000) -> List[str]:
        """
        Divise le texte en chunks pour respecter les limites de tokens
        """
        sentences = re.split(r'[.!?]+', text)
        chunks = []
        current_chunk = ""

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue

            test_chunk = current_chunk + " " + sentence if current_chunk else sentence

            if len(self.encoding.encode(test_chunk)) <= max_tokens:
                current_chunk = test_chunk
            else:
                if current_chunk:
                    chunks.append(current_chunk)
                current_chunk = sentence

        if current_chunk:
            chunks.append(current_chunk)

        return chunks

    def ask_question(self, question: str, model: str = "gpt-3.5-turbo") -> str:
        """
        Pose une question sur le contenu du PDF
        """
        if not self.pdf_content:
            return "Aucun PDF chargé. Veuillez d'abord extraire le contenu d'un PDF."

        # Préparer le contexte
        chunks = self.chunk_text(self.pdf_content, max_tokens=3000)

        # Si le document est court, utiliser tout le contenu
        if len(chunks) == 1:
            context = chunks[0]
        else:
            # Pour des documents longs, prendre les premiers chunks
            context = " ".join(chunks[:2])  # Utiliser les 2 premiers chunks
        print(('======'))
        print(context)
        print(('======'))

        prompt = f"""
Vous êtes un assistant qui répond aux questions basées sur le contenu d'un document PDF.

Contenu du document:
{context}

Question: {question}

Instructions:
- Répondez uniquement basé sur le contenu fourni
- Si l'information n'est pas dans le document, dites-le clairement
- Citez des passages spécifiques quand c'est pertinent
- Soyez précis et concis

Réponse:"""

        try:
            response = self.client.chat.completions.create(
                model=model,
                messages=[
                    {"role": "system", "content": "Vous êtes un assistant spécialisé dans l'analyse de documents PDF."},
                    {"role": "user", "content": prompt}
                ],
                max_tokens=500,
                temperature=0.1
            )

            return response.choices[0].message.content.strip()

        except Exception as e:
            return f"Erreur lors de la génération de la réponse: {e}"

    def summarize_pdf(self) -> str:
        """
        Génère un résumé du PDF
        """
        if not self.pdf_content:
            return "Aucun PDF chargé."

        chunks = self.chunk_text(self.pdf_content, max_tokens=3000)
        context = " ".join(chunks[:3])  # Utiliser les 3 premiers chunks

        prompt = f"""
Veuillez créer un résumé concis du document suivant:

{context}

Le résumé doit:
- Capturer les points clés et idées principales
- Être structuré et facile à lire
- Faire environ 200-300 mots maximum

Résumé:"""

        try:
            response = self.client.chat.completions.create(
                model="gpt-3.5-turbo",
                messages=[
                    {"role": "system", "content": "Vous créez des résumés clairs et concis de documents."},
                    {"role": "user", "content": prompt}
                ],
                max_tokens=400,
                temperature=0.2
            )

            return response.choices[0].message.content.strip()

        except Exception as e:
            return f"Erreur lors de la génération du résumé: {e}"


def main():
    parser = argparse.ArgumentParser(description="ChatPDF Simple - Analyseur de PDF avec IA")
    parser.add_argument("pdf_path", help="Chemin vers le fichier PDF")
    parser.add_argument("--api-key", help="Clé API OpenAI (ou utilisez la variable d'environnement OPENAI_API_KEY)")
    parser.add_argument("--model", default="gpt-3.5-turbo", help="Modèle OpenAI à utiliser")

    args = parser.parse_args()

    try:
        # Initialiser ChatPDF
        chat_pdf = ChatPDF(api_key=args.api_key)

        # Extraire le texte du PDF
        print(f"Chargement du PDF: {args.pdf_path}")
        chat_pdf.extract_text_from_pdf(args.pdf_path)

        # Générer un résumé automatique
        print("\n=== RÉSUMÉ AUTOMATIQUE ===")
        summary = chat_pdf.summarize_pdf()
        print(summary)

        # Mode interactif pour les questions
        print("\n=== MODE QUESTIONS-RÉPONSES ===")
        print("Posez vos questions sur le document (tapez 'quit' pour quitter):")

        while True:
            try:
                question = input("\n❓ Votre question: ").strip()

                if question.lower() in ['quit', 'exit', 'q']:
                    break

                if not question:
                    continue

                print("🤔 Recherche de la réponse...")
                answer = chat_pdf.ask_question(question, model=args.model)
                print(f"\n💡 Réponse:\n{answer}")

            except KeyboardInterrupt:
                break
            except Exception as e:
                print(f"Erreur: {e}")

        print("\nAu revoir!")

    except Exception as e:
        print(f"Erreur: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()

# Exemple d'utilisation en tant que module
"""
from chatpdf_simple import ChatPDF

# Initialiser
chat = ChatPDF(api_key="votre_clé_api")

# Charger un PDF
chat.extract_text_from_pdf("document.pdf")

# Poser des questions
response = chat.ask_question("Quels sont les points principaux de ce document?")
print(response)

# Générer un résumé
summary = chat.summarize_pdf()
print(summary)
"""

Usage

Macos
python3 main.py  document.pdf
Windows
python main.py document.pdf

Créez un clone de chatPDF en Python et la base de données vectorielle FAISS

Si vous ne connaissez pas chatPDF, je vous recommande de le tester sur le site en uploadant un fichier pdf, et interrogez le document comme si vous interrogiez chatGPT. L’intérêt est que vous pouvez connaitre c’e qui vous intéresse du document sans le lire entièrement.

Par exemple vous souscrivez à une assurance, et il y a un long contrat en petits caractères à lire, plutôt que de perdre des heures à décortiquer le document, vous interrogez chatGPT sur le document.

Techniquement ce n’est pas comme ça. En fait vous injecter le texte du document dans la base vectorielle FAISS, vous interrogez FAISS, qui vous retournera un résultat (qui est un contexte) et vous le soumettez à chatGPT.

Vous contextualisez très précisément votre prompt.

Installation des composants

La base de données vectorielle FAISS

FAISS est un logiciel fait par Facebook, acronyme de Facebook AI Similarity Search. Elle tokenise le pdf et le stocke sous forme vectorielle.

Le parseur de PDF

PyPDF2 est un très bon parseur de pdf.

Un tokeniser

tiktoken va transformer le texte en token. Un token est une unité qui a une signification mathématique pour le LLM. tiktoken travaille de concert avec le modèle de OpenAI.

Openai

Permet d’appeler le webservice d’OpenAI facilement

Une clé API par exemple chatGPT

Il vous faudra acheter des crédits pour le webservice chatGPT, 10 euros est pas mal et vous permettra de faire beaucoup d’appels.

Le code Python

#!/usr/bin/env python3
"""
ChatPDF Clone avec Base de Données Vectorielle
Utilise des embeddings et une recherche vectorielle pour une meilleure précision
"""

import PyPDF2
from openai import OpenAI
import os
import argparse
import sys
import numpy as np
from typing import List, Optional, Dict, Tuple
import tiktoken
import re
import faiss
import pickle
from sentence_transformers import SentenceTransformer
from dataclasses import dataclass
import json


@dataclass
class TextChunk:
    text: str
    page_num: int
    chunk_id: int
    embedding: Optional[np.ndarray] = None


class VectorChatPDF:
    def __init__(self, api_key: Optional[str] = None, embedding_model: str = "all-MiniLM-L6-v2"):
        """
        Initialise ChatPDF avec base vectorielle
        """
        self.api_key = "sk-proj-BBFnLZzRMt8EZp4LStJXyfzc_6knvyKX2jOhYXIRh27MmGHIkxzcGFfcisYMS3uiRYBP0r2qT3BlbkFJObsGY8qW-VjRQNLsB9Fr-iPhSmCs0Aaj2Y8hSF34dBQQ4dEgiq7mQO30BCWDItJC775SUA"
        if not self.api_key:
            raise ValueError("Clé API OpenAI requise. Définissez OPENAI_API_KEY ou passez api_key")

        self.client = OpenAI(api_key=self.api_key)

        # Modèle d'embeddings local (gratuit)
        print("Chargement du modèle d'embeddings...")
        self.embedding_model = SentenceTransformer(embedding_model)

        # Base de données vectorielle FAISS
        self.vector_db = None
        self.chunks: List[TextChunk] = []
        self.pdf_path = ""
        self.encoding = tiktoken.get_encoding("cl100k_base")

    def extract_text_from_pdf(self, pdf_path: str) -> List[TextChunk]:
        """
        Extrait le texte du PDF et le divise en chunks
        """
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                chunks = []
                chunk_id = 0

                print(f"Extraction du texte de {len(pdf_reader.pages)} pages...")

                for page_num, page in enumerate(pdf_reader.pages):
                    try:
                        page_text = page.extract_text()
                        if page_text.strip():
                            # Diviser la page en chunks
                            page_chunks = self._split_text_into_chunks(page_text, max_tokens=500)
                            for chunk_text in page_chunks:
                                chunk = TextChunk(
                                    text=chunk_text,
                                    page_num=page_num + 1,
                                    chunk_id=chunk_id
                                )
                                chunks.append(chunk)
                                chunk_id += 1

                    except Exception as e:
                        print(f"Erreur lors de l'extraction de la page {page_num + 1}: {e}")
                        continue

                self.chunks = chunks
                self.pdf_path = pdf_path
                print(f"Extraction terminée. {len(chunks)} chunks créés.")
                return chunks

        except FileNotFoundError:
            raise FileNotFoundError(f"Fichier PDF non trouvé: {pdf_path}")
        except Exception as e:
            raise Exception(f"Erreur lors de la lecture du PDF: {e}")

    def _split_text_into_chunks(self, text: str, max_tokens: int = 500) -> List[str]:
        """
        Divise le texte en chunks optimaux pour les embeddings
        """
        # Nettoyer le texte
        text = re.sub(r'\s+', ' ', text).strip()

        # Diviser par phrases
        sentences = re.split(r'[.!?]+', text)
        chunks = []
        current_chunk = ""

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue

            test_chunk = current_chunk + " " + sentence if current_chunk else sentence

            if len(self.encoding.encode(test_chunk)) <= max_tokens:
                current_chunk = test_chunk
            else:
                if current_chunk:
                    chunks.append(current_chunk)
                current_chunk = sentence

        if current_chunk:
            chunks.append(current_chunk)

        return [chunk for chunk in chunks if len(chunk.strip()) > 20]  # Filtrer les chunks trop courts

    def create_embeddings(self):
        """
        Crée les embeddings pour tous les chunks et construit l'index FAISS
        """
        if not self.chunks:
            raise ValueError("Aucun chunk disponible. Extrayez d'abord le texte du PDF.")

        print("Création des embeddings...")
        texts = [chunk.text for chunk in self.chunks]

        # Créer les embeddings par batch pour l'efficacité
        embeddings = self.embedding_model.encode(texts, show_progress_bar=True)

        # Associer les embeddings aux chunks
        for i, chunk in enumerate(self.chunks):
            chunk.embedding = embeddings[i]

        # Créer l'index FAISS
        dimension = embeddings.shape[1]
        self.vector_db = faiss.IndexFlatIP(dimension)  # Index par produit scalaire (cosine similarity)

        # Normaliser les vecteurs pour la similarité cosine
        normalized_embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
        self.vector_db.add(normalized_embeddings.astype('float32'))

        print(f"Index vectoriel créé avec {len(self.chunks)} embeddings.")

    def search_similar_chunks(self, query: str, top_k: int = 5) -> List[Tuple[TextChunk, float]]:
        """
        Recherche les chunks les plus similaires à la requête
        """
        if not self.vector_db:
            raise ValueError("Index vectoriel non créé. Appelez create_embeddings() d'abord.")

        # Créer l'embedding de la requête
        query_embedding = self.embedding_model.encode([query])
        query_embedding = query_embedding / np.linalg.norm(query_embedding)

        # Rechercher les chunks similaires
        scores, indices = self.vector_db.search(query_embedding.astype('float32'), top_k)

        results = []
        for score, idx in zip(scores[0], indices[0]):
            if idx < len(self.chunks):  # Vérification de sécurité
                results.append((self.chunks[idx], float(score)))

        return results

    def ask_question(self, question: str, model: str = "gpt-3.5-turbo", top_k: int = 5) -> Dict:
        """
        Pose une question en utilisant la recherche vectorielle
        """
        if not self.vector_db:
            return {"error": "Index vectoriel non créé. Appelez create_embeddings() d'abord."}

        # Rechercher les chunks pertinents
        similar_chunks = self.search_similar_chunks(question, top_k=top_k)

        if not similar_chunks:
            return {"error": "Aucun contenu pertinent trouvé."}

        # Construire le contexte à partir des chunks les plus pertinents
        context_parts = []
        source_info = []

        for chunk, score in similar_chunks:
            context_parts.append(f"[Page {chunk.page_num}] {chunk.text}")
            source_info.append({
                "page": chunk.page_num,
                "chunk_id": chunk.chunk_id,
                "similarity_score": score,
                "text_preview": chunk.text[:100] + "..." if len(chunk.text) > 100 else chunk.text
            })

        context = "\n\n".join(context_parts)

        prompt = f"""
Vous êtes un assistant expert qui répond aux questions basées sur le contenu d'un document PDF.

Contexte pertinent extrait du document:
{context}

Question: {question}

Instructions:
- Répondez uniquement basé sur le contexte fourni
- Si l'information n'est pas suffisante, dites-le clairement
- Citez les numéros de page quand c'est pertinent
- Soyez précis et structuré dans votre réponse

Réponse:"""

        try:
            response = self.client.chat.completions.create(
                model=model,
                messages=[
                    {"role": "system",
                     "content": "Vous êtes un assistant spécialisé dans l'analyse de documents PDF avec une expertise en recherche d'information."},
                    {"role": "user", "content": prompt}
                ],
                max_tokens=800,
                temperature=0.1
            )

            answer = response.choices[0].message.content.strip()

            return {
                "answer": answer,
                "sources": source_info,
                "context_used": len(similar_chunks)
            }

        except Exception as e:
            return {"error": f"Erreur lors de la génération de la réponse: {e}"}

    def save_index(self, filepath: str):
        """
        Sauvegarde l'index vectoriel et les métadonnées
        """
        if not self.vector_db:
            raise ValueError("Aucun index à sauvegarder.")

        # Sauvegarder l'index FAISS
        faiss.write_index(self.vector_db, f"{filepath}.faiss")

        # Sauvegarder les métadonnées
        metadata = {
            "chunks": [
                {
                    "text": chunk.text,
                    "page_num": chunk.page_num,
                    "chunk_id": chunk.chunk_id
                } for chunk in self.chunks
            ],
            "pdf_path": self.pdf_path
        }

        with open(f"{filepath}.json", 'w', encoding='utf-8') as f:
            json.dump(metadata, f, ensure_ascii=False, indent=2)

        print(f"Index sauvegardé: {filepath}.faiss et {filepath}.json")

    def load_index(self, filepath: str):
        """
        Charge un index vectoriel sauvegardé
        """
        try:
            # Charger l'index FAISS
            self.vector_db = faiss.read_index(f"{filepath}.faiss")

            # Charger les métadonnées
            with open(f"{filepath}.json", 'r', encoding='utf-8') as f:
                metadata = json.load(f)

            # Reconstruire les chunks
            self.chunks = []
            for chunk_data in metadata["chunks"]:
                chunk = TextChunk(
                    text=chunk_data["text"],
                    page_num=chunk_data["page_num"],
                    chunk_id=chunk_data["chunk_id"]
                )
                self.chunks.append(chunk)

            self.pdf_path = metadata["pdf_path"]
            print(f"Index chargé: {len(self.chunks)} chunks disponibles.")

        except FileNotFoundError:
            raise FileNotFoundError(f"Fichiers d'index non trouvés: {filepath}.faiss ou {filepath}.json")


def main():
    parser = argparse.ArgumentParser(description="ChatPDF avec Base Vectorielle")
    parser.add_argument("pdf_path", help="Chemin vers le fichier PDF")
    parser.add_argument("--api-key", help="Clé API OpenAI")
    parser.add_argument("--model", default="gpt-3.5-turbo", help="Modèle OpenAI à utiliser")
    parser.add_argument("--save-index", help="Sauvegarder l'index vectoriel")
    parser.add_argument("--load-index", help="Charger un index vectoriel existant")
    parser.add_argument("--top-k", type=int, default=5, help="Nombre de chunks à récupérer")

    args = parser.parse_args()

    try:
        # Initialiser ChatPDF vectoriel
        chat_pdf = VectorChatPDF(api_key=args.api_key)

        if args.load_index:
            # Charger un index existant
            print(f"Chargement de l'index: {args.load_index}")
            chat_pdf.load_index(args.load_index)
        else:
            # Traiter un nouveau PDF
            print(f"Chargement du PDF: {args.pdf_path}")
            chat_pdf.extract_text_from_pdf(args.pdf_path)
            chat_pdf.create_embeddings()

            if args.save_index:
                chat_pdf.save_index(args.save_index)

        # Mode interactif
        print("\n=== MODE QUESTIONS-RÉPONSES VECTORIEL ===")
        print("Posez vos questions sur le document (tapez 'quit' pour quitter):")
        print("Tapez 'stats' pour voir les statistiques de l'index")

        while True:
            try:
                question = input("\n❓ Votre question: ").strip()

                if question.lower() in ['quit', 'exit', 'q']:
                    break

                if question.lower() == 'stats':
                    print(f"📊 Statistiques:")
                    print(f"   - Nombre de chunks: {len(chat_pdf.chunks)}")
                    print(f"   - Pages: {max(c.page_num for c in chat_pdf.chunks) if chat_pdf.chunks else 0}")
                    print(f"   - Document: {os.path.basename(chat_pdf.pdf_path)}")
                    continue

                if not question:
                    continue

                print("🔍 Recherche vectorielle en cours...")
                result = chat_pdf.ask_question(question, model=args.model, top_k=args.top_k)

                if "error" in result:
                    print(f"❌ {result['error']}")
                else:
                    print(f"\n💡 Réponse:\n{result['answer']}")
                    print(f"\n📚 Sources utilisées ({result['context_used']} chunks):")
                    for i, source in enumerate(result['sources'][:3], 1):
                        print(f"   {i}. Page {source['page']} (score: {source['similarity_score']:.3f})")
                        print(f"      {source['text_preview']}")

            except KeyboardInterrupt:
                break
            except Exception as e:
                print(f"Erreur: {e}")

        print("\nAu revoir!")

    except Exception as e:
        print(f"Erreur: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()

Lancer le programme en ligne de commande

lancement du script en mode normal, cela va créer un fichier binaire mon_index.faiss qui est la base vectorielle

python3 avecBaseVectorielle.py document.pdf --save-index mon_index

La même chose en plus léger

python3 avecBaseVectorielle.py document.pdf --fast --save-index mon_index

La même chose avec un LLM personnalisé

python3 avecBaseVectorielle.py document.pdf --embedding-model "paraphrase-MiniLM-L3-v2"

La même chose en chargeant une base vectorielle déjà calculée

python3 avecBaseVectorielle.py document.pdf --load-index mon_index

Lors du lancement du script en mode normal, une base vectorielle va être créée. Vous allez poser une question qui va requêter sur cette base de données. Par exemple, « Combien de jours de formation y a t il dans cette formation? », la requête ne va pas partir vers chatGPT directement mais va extraire un contexte de la base vectorielle, qui va être mis dans le prompt vers chatGPT.

Sans le vouloir vraiment, vous venez de faire du RAG, Retrieval Augmented Generation, autrement dit intelligence générative augmentée par une extraction de données.

Modèle d’embedding c’est quoi?

Un modèle d’embedding est un système qui transforme du texte (mots, phrases, paragraphes) en vecteurs numériques de dimension fixe. Ces vecteurs capturent le sens sémantique du texte de manière à ce que des textes similaires aient des représentations vectorielles proches dans l’espace mathématique.

Comment ça fonctionne

Les modèles d’embedding utilisent généralement des réseaux de neurones entraînés sur de grandes quantités de texte pour apprendre les relations sémantiques. Par exemple, les mots « chien » et « animal » auront des vecteurs plus proches que « chien » et « voiture ».

Utilité pratique

Ces représentations vectorielles permettent de :

  • Mesurer la similarité sémantique entre textes
  • Effectuer des recherches par similarité
  • Regrouper des documents par thème
  • Alimenter des systèmes de question-réponse

Le modèle « paraphrase-MiniLM-L3-v2 » est un modèle d’embedding compact et efficace développé par Sentence Transformers. Il est particulièrement adapté pour :

  • Identifier des paraphrases (textes exprimant la même idée différemment)
  • Créer des bases de données vectorielles pour la recherche sémantique

Le mode –fast

C’est u mode d’optimisation qui accélère le traitement en faisant certains compromis.

Le mode fast améliore généralement la vitesse au détriment de :

  • La précision de la recherche sémantique
  • La qualité des réponses
  • La finesse de l’analyse du document

Introduction au machine learning avec un modèle de prédiction d’abandon de souscription

vous avez entendu parler de machine learning partout, mais avez vous vu comment cela fonctionnait? Nous allons voir avec un exemple comment ça marcce à quoi ça sert et vous comprendrez à la fin pourquoi il est très intéressant d’en faire dans les entreprises.

Le contexte : avec des données d’abandon de souscription à un forfait mobile (churn), nous allons établir un modèle mathématique pour prédire les futur churn. L’intérêt de faire confiance au machine learning est qu’il y a un grand nombre de paramètres qui influence le fait qu’un client part à la concurrence, il est quasi impossible de faire le diagnostic à la main pour savoir si le client va partir ou non, plus exactement il y a 18 paramètres.

Le principe du machine learning est de prendre ces données, les travailler et les faire passer dans un algorithme qui va générer un modèle mathématique qui servira aux futures données, pour prédire si le client va churner ou non. En gros se servir du passé pour prédire le futur.

Le dataset à télécharger

Allez sur Kaggle et téléchargez le dataset (il vous faudra créer un compte). On va explorer ce dataset, mais ce qu’il faut savoir c’est que dans ce dataset, 30% des lignes concernent les clients ayant churné, et 70% sont des clients qui n’ont pas churné. On n’a pas une distribution 50/50. En machine learning, entrainer sur ce dataset va produire un modèle mathématique qui va prédire de façon biaisé, quelquesoit l’entrée donnée, les probabilités que la prédiction soit en faveur d’un non churn sera anormalement haute.

Pour éviter ce déséquilibre dans les données (et ce déséquilibre importe car le type d’issue d’une prédiction est unique : le churn), on va créer artificiellement (oui artificiellement) des données pour compenser ce déséquilibre, donc on va générer des données ayant pour issue le churn, afin d’avoir un rapport 50/50 (churn/non-churn)

Installation des paquets

Pour faire du traitement de machine learning, il vous faudra installer les paquets suivants : numpy, pandas, matplotlib, seaborn,sklearn

Note: lors de l’installation de sklearn, vous aurez une erreur de paquet déprécié, en fait dans l’import de bibliothèque, c’est bien sklearn qu’il faut importer, mais lors que vous installez le paquet avec pip, c’est scikit-learn qu’il faut installer ! (Lien pour en savoir plus)

Utiliser l’éditeur de code Jupyter Notebook

Jupyter noteboopk est un éditeur de code progressif, très adapté à des non codeur pur et dur. Pour les datascientistes, c’est très pertinent car ils peuvent voir comment évolue leur traitement de données. Et pour tout le monde, c’est très visuel lors de l’apprentissage du machine learning, même pour un codeur pur et dur comme moi.

Vous pouvez décomposer le code en étape et refaire exécuter une étape à tout moment, pratique si vous vous trompez dans votre code et qu’il y a une erreur.

Pour installer Jupyter Notebook, faites la commande

# installation de jupyterlab
pip install jupyterlab
pour les macs
pip3 install jupyterlab


pour démarrer
jupyter lab



pip install notebook
ou pour les mac
pip3 install notebook

puis démarrer en faisant :
jupyter notebook

Note sur Jupyterlab : jupyterlab est la version moderne de jupyter notebook, et possède un système d eplugin plus simple à utiliser et un terminal et un éditeur de texte.

Commençons à explorer les caractéristiques de notre jeu de données

Notre objectif est d’entrainer un modèle sur le jeu de données, pour ensuite prédire la probabilité d’un client à passer à la concurrencer (churner). customerChurn.csv est le fichier de chez Kaggle.

import pandas as pd

df = pd.read_csv('customerChurn.csv')
df.head()

La commande df.head() affiche les 5 premières ligne du dataframe. Le dataframe est la version de Pandas d’un fichier Excel en gros.

Nous avons un fichier avec des colonnes, qu’on appelle de feature, puisque chaque colonne est une caractéristique d’un client. Pour voir le nom et le type de la donnée stockée dans chaque colonne ainsi que le nombre de colonnes, on fait

df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

Et pour savoir combien il y a d’enregistrements on fait:

df["Churn"].value_counts()

Churn
No     5174
Yes    1869
Name: count, dtype: int64

donc on a 19 colonnes utile et une colonne inutile d’un point de vue machine learning, c’est la colonne customerID, elle ne comporte aucune information métier. La dernière colonne appelée Churn, prend deux valeuer, elle indique si le client est parti chez le concurrent ou non. Elle prend les valeur No ou Yes. Pour afficher le compte on fait:

df["Churn"].values_count()

Churn
No     5174
Yes    1869
Name: count, dtype: int64

il y a 1869 personnes partie, on note ici que le nombre de Yes et de No est différent dans un rapport 1 à 3 environ. Cela on le verra plus tard aura son importance dans l'entrainement du modèle. Il y a inégalité dans les 2 camps, cette disparité va avoir une incidence sur la qualité du modèle entrainé. En effet, le modèle résultant va être biaisé avec le No, car ce dernier est majoritaire. On devra équilibrer artificiellement ce dataset pour avoir un bon modèle de prédiction.

Analyse exploratoire des données

Maintenant explorons les relations entre les différentes catégorie et la colonne Churn

import matplotlib.pyplot as plt
import seaborn as sns 
import numpy as np

cols = ['gender','SeniorCitizen',"Partner","Dependents"]
numerical = cols

plt.figure(figsize=(20,4))

for i, col in enumerate(numerical):
    ax = plt.subplot(1, len(numerical), i+1)
    unique_vals = df[col].nunique()
    sns.countplot(x=col, hue=col, data=df, legend=False, 
                  palette=sns.color_palette("husl", df[col].nunique()))
    #sns.countplot(x=str(col), data=df, palette=sns.color_palette("husl", unique_vals))
    ax.set_title(f"{col}")

L’idée est ici de graphiquement regarder pour chaque colonne la distribution des valeurs. Ici on voit la colonne Genre, SeniorCitizen, Partner, Dependents, des varaible catégorielle relative à la démographie.

On voit qu’il y a autant d’homme que de femmes, mais que les seniors sont minoritaires, qu’il y a autant de célibataires que de mariés, et il y a 1 tier de la population qui est dépendantes.

Maintenant regardons la relation entre le MonthlyCharges et le Churn, on peut se dire que plus le forfait est élevé plus les gens ont tendance à partir

sns.boxplot(x='Churn', y='MonthlyCharges', hue='Churn', data=df, palette='Set2', dodge=False, legend=False)

Effectivement c’est ce que nous constatons, les Yes sont entre 50 et 100 dollars / mois.

Pour comprendre le boxplot je vous joins ce petit schéma pris sur datatab.net

Maintenant analysons la relation entre quelques autres variable catégorielle avec le Churn

cols = ['InternetService',"TechSupport","OnlineBackup","Contract"]

plt.figure(figsize=(14,4))

for i, col in enumerate(cols):
    ax = plt.subplot(1, len(cols), i+1)
    sns.countplot(x ="Churn", hue = str(col), data = df)
    ax.set_title(f"{col}")

Cette fois-ci les catégories ont plusieurs valeurs (3). On peut voir que pour InternetService, donc le forfait box de l’abonnement, les churners n’ont pas de forfait box ou très peu, idem pour le service ADSL, par contre ils sont nombreux à être sur fibre optique. Concernant les non churners, ils sont plus équitablement distribué.

InternetService : Il ressort clairement du graphique ci-dessus que les clients utilisant une connexion Internet par fibre optique résilient leur abonnement plus souvent que les autres. Cela pourrait s’expliquer par le fait que la fibre est un service plus coûteux, ou bien que ce fournisseur n’offre pas une bonne couverture.

TechSupport : De nombreux utilisateurs ayant résilié leur abonnement ne s’étaient pas inscrits au service d’assistance technique. Cela pourrait signifier qu’ils n’ont reçu aucune aide pour résoudre leurs problèmes techniques et ont décidé d’arrêter d’utiliser le service.

OnlineBackup : Beaucoup de clients ayant résilié leur abonnement ne s’étaient pas inscrits au service de sauvegarde en ligne pour le stockage de données.

Contract : Les utilisateurs qui ont résilié leur abonnement étaient presque toujours sous contrat mensuel. Cela semble logique, car ces clients paient au mois et peuvent facilement annuler leur abonnement avant le prochain cycle de paiement.

Même sans construire un modèle d’apprentissage automatique sophistiqué, une simple analyse basée sur les données comme celle-ci peut aider les organisations à comprendre pourquoi elles perdent des clients et ce qu’elles peuvent faire pour y remédier.

Par exemple, si l’entreprise se rend compte que la majorité des clients qui résilient leur abonnement ne se sont pas inscrits au service d’assistance technique, elle pourrait inclure ce service gratuitement dans certaines de ses futures offres afin d’éviter que d’autres clients ne partent.

Préparation des données pour la modélisation

Rappel
df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

Beaucoup de données sont de type Object, des valeurs non numériques, « TotalCharges » qui est un montant est un objet par exemple, on va le convertir en valeur numérique

df['TotalCharges'] = df['TotalCharges'].apply(lambda x: pd.to_numeric(x, errors='coerce')).dropna()

Encodage des variables catégorielles

Les valeurs catégorielles (non numérique) doivent être convertie en valeur numérique pour être modélisée, des colonnes avec pour valeur Yes/No, on peut rendre le Yes en 1 et le No en 0. Avec Sckitlearn on va pouvoir transformer ces valeurs en nombres. Si on regarde le dataframe sans les valeur numériques

cat_features = df.drop(['customerID','TotalCharges','MonthlyCharges','SeniorCitizen','tenure'],axis=1)

cat_features.head()

La commande ci-dessus nous permet de voir les valeurs catégorielles des features. Transformaons les en valuer numérique avec ScikitLearn

from sklearn import preprocessing

le = preprocessing.LabelEncoder()
df_cat = cat_features.apply(le.fit_transform)
df_cat.head(1)

Scikitlearn convertit automatiquement les valeurs catégorielle en valeur numérique. Il n’y a pas de règle particulière, la valeur 0 correspond à la première valeur catégorielle ordonnées dans l’ordre alphabétique. No est avant Yes donc No vaut zéro et Yes vaut 1.

Regardons maintenant les types

df_cat.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 16 columns):
 #   Column            Non-Null Count  Dtype
---  ------            --------------  -----
 0   gender            7043 non-null   int64
 1   Partner           7043 non-null   int64
 2   Dependents        7043 non-null   int64
 3   PhoneService      7043 non-null   int64
 4   MultipleLines     7043 non-null   int64
 5   InternetService   7043 non-null   int64
 6   OnlineSecurity    7043 non-null   int64
 7   OnlineBackup      7043 non-null   int64
 8   DeviceProtection  7043 non-null   int64
 9   TechSupport       7043 non-null   int64
 10  StreamingTV       7043 non-null   int64
 11  StreamingMovies   7043 non-null   int64
 12  Contract          7043 non-null   int64
 13  PaperlessBilling  7043 non-null   int64
 14  PaymentMethod     7043 non-null   int64
 15  Churn             7043 non-null   int64
dtypes: int64(16)
memory usage: 880.5 KB

On va maintenant fusionner avec le dataframe qui était déjà numérique

num_features = df[['customerID','TotalCharges','MonthlyCharges','SeniorCitizen','tenure']]
finaldf = pd.merge(num_features, df_cat, left_index=True, right_index=True)

Oversampling

On avait dit que le Churn Yes était minoritaire dans les données, alors qu’il devait être à 50/50 avec le churn No.

Comme mentionné précédemment, le jeu de données est déséquilibré, ce qui signifie que la majorité des valeurs de la variable cible appartiennent à une seule classe. La plupart des clients du jeu de données n’ont pas résilié leur abonnement — seulement 27 % l’ont fait.

Ce problème de déséquilibre des classes peut entraîner de mauvaises performances d’un modèle d’apprentissage automatique. Certains algorithmes, lorsqu’ils sont entraînés sur un jeu de données déséquilibré, finissent toujours par prédire la classe majoritaire. Dans notre cas, par exemple, le modèle pourrait prédire qu’aucun client n’a résilié. Bien qu’un tel modèle semble très précis (il serait correct dans 73 % des cas), il ne nous est d’aucune utilité puisqu’il prédit toujours le même résultat.

Il existe différentes techniques pour résoudre le problème de déséquilibre des classes en apprentissage automatique. Dans ce tutoriel, nous allons utiliser une technique appelée suréchantillonnage (oversampling). Ce processus consiste à sélectionner aléatoirement des échantillons de la classe minoritaire et à les ajouter au jeu de données d’entraînement. Nous allons suréchantillonner la classe minoritaire jusqu’à ce que le nombre d’exemples soit égal à celui de la classe majoritaire.

Avant de procéder au suréchantillonnage, effectuons une séparation entre le jeu d’entraînement et le jeu de test. Nous appliquerons le suréchantillonnage uniquement sur le jeu d’entraînement, car le jeu de test doit rester représentatif de la population réelle.

On va constituer un dataset pour le training du modèle, et un dataset de test qu’on va soumettre qu modèle entrainé.

from sklearn.model_selection import train_test_split

finaldf = finaldf.dropna()
finaldf = finaldf.drop(['customerID'],axis=1)

X = finaldf.drop(['Churn'],axis=1)
y = finaldf['Churn']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

Chaque dataset est en deux parties X (majuscule) et y (minuscule), y représente la dernière colonne qui est le churn. Ainsi Dans le dataset de training, on a X_train et y_train. Et pour le dataset de test on a X_test et y_test.

#y_train
X_test.info()

<class 'pandas.core.frame.DataFrame'>
Index: 2321 entries, 2481 to 5585
Data columns (total 19 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   TotalCharges      2321 non-null   float64
 1   MonthlyCharges    2321 non-null   float64
 2   SeniorCitizen     2321 non-null   int64  
 3   tenure            2321 non-null   int64  
 4   gender            2321 non-null   int64  
 5   Partner           2321 non-null   int64  
 6   Dependents        2321 non-null   int64  
 7   PhoneService      2321 non-null   int64  
 8   MultipleLines     2321 non-null   int64  
 9   InternetService   2321 non-null   int64  
 10  OnlineSecurity    2321 non-null   int64  
 11  OnlineBackup      2321 non-null   int64  
 12  DeviceProtection  2321 non-null   int64  
 13  TechSupport       2321 non-null   int64  
 14  StreamingTV       2321 non-null   int64  
 15  StreamingMovies   2321 non-null   int64  
 16  Contract          2321 non-null   int64  
 17  PaperlessBilling  2321 non-null   int64  
 18  PaymentMethod     2321 non-null   int64  
dtypes: float64(2), int64(17)
memory usage: 362.7 KB

C’est avec la librairie imblearn qu’on va construire le suréchantillon sur les données de training

from imblearn.over_sampling import SMOTE

oversample = SMOTE(k_neighbors=5)
X_smote, y_smote = oversample.fit_resample(X_train, y_train)
X_train, y_train = X_smote, y_smote

Ensuite consmptez le nombre de rangées : 

y_train.value_counts()
Churn
0    3452
1    3452
Name: count, dtype: int64

On est à 50/50

Construction du modèle de prédiction du churn

Maintenant la partie vraiment importante ! On a passé beaucoup de temps pour nettoyer, changé la valeur catégorielle en valeur numérique, rééquilibré les population, maintenant on va entrainer le modèle !

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(random_state=46)
rf.fit(X_train,y_train)

Evaluation du modèle

On va évaluer l aprécision du modèle avec les données de test (dont on connait l’issue car ce sont les données d’origine)

#customer churn model evaluation
from sklearn.metrics import accuracy_score

preds = rf.predict(X_test)
print(accuracy_score(preds,y_test))


0.7733735458853942

On a une précision de 77%

Sauvegarder le modèle

On en va pas à chaque fois réentrainer le modèle quand on va faire une prédiction, donc on va le sauvegarder pour pouvoir le réutiliser.

#save the model
import joblib
# After training your model  (format Pickle)
joblib.dump(rf, 'random_forest_model.pkl')


Sauvergarder les données de test et de training

#save training and testing data to file
import pickle

# Save X data
with open('X_train.pkl', 'wb') as f:
    pickle.dump(X_train, f)
with open('X_test.pkl', 'wb') as f:
    pickle.dump(X_test, f)

# Save y data
with open('y_train.pkl', 'wb') as f:
    pickle.dump(y_train, f)
with open('y_test.pkl', 'wb') as f:
    pickle.dump(y_test, f)

Chargement du modèle et des données sauvegardées

On va ouvrir un autre fichier Notebook vierge et on va juste charge le modèle et les données pour passer au crible. En effet le modèle que vous avez calculé va être réutilisé, et il n’est pas question de le calculer à chaque fois que vous avez besoin de scorer la probabilité d’un client de patir.

Chargement du modèle

import joblib
#load the model
loaded_rf = joblib.load('random_forest_model.pkl')
print(loaded_rf)

# Now you can use the loaded model to make predictions
#prediction = loaded_rf.predict(new_customer)
#print(prediction)

Chargement des données

#load the datas
import pickle

with open('X_test.pkl', 'rb') as f:
    X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
    y_test = pickle.load(f)

X_test.head()

On rejoue le test, X_test contient les clients, et on passe en argument de la fonction predict qui va nous retourner la variable prediction, qui est une Serie, contenant des 0 et 1, 0 correspondant à « No » et 1 à « Yes ».

# Now you can use the loaded model to make predictions
from sklearn.metrics import accuracy_score
prediction = loaded_rf.predict(X_test)
#la série qui contient les outcome (churn si 1 , 0 si no churn)
print(prediction)

[0 0 1 ... 0 1 1]

Ce qu'on voit c'est la colonne churn

Test avec un sous ensemble

Ici je simule le fait que j’ai un nouveau client à scorer, c’est donc une ligne (un seul client), je passe cette ligne en argument de la fonction predict, qui va nous retourner un Serie avec un seul élément (on n’a scoré qu’un client), ici on obtient un « Yes ».

# test on row
# subset = X_test[X_test['TotalCharges']<100]
# extract a line from X_test
oneline = X_test.loc[[6125]]
#convert to dataframe
#oneline = oneline.to_frame()


import pandas as pd
isinstance(oneline,pd.DataFrame) # test if it's DataFrame
print(oneline)
oneline.head()


prediction = loaded_rf.predict(oneline)
print(prediction)

[1] //  Churn

Retour en haut