samedi 29 octobre 2022

Refactorisations assistées par des DOCTEST

 Python permet facilement de passer d'un programme structuré en fonction à un programme utilisant des classes et des objets.

C'est à mon avis une des forces de Python. Pas besoin de tout réécrire le gros du travail est conservé. 

Aussi , Python favorise la  refactorisation de son code au fur et à mesure de la progression du développement.

(le notebook est disponible sur : https://github.com/germanlinux/notebooks

(https://github.com/germanlinux/notebooks/blob/main/init/refactorisation.ipynb)

La refactorisation en  objet et classe consiste à :

  • Ajouter le paramètre 'self' dans les déclarations des fonctions 
  • Préfixer les variables locales par 'self'
  • Créer un cadre pour la classe
  • Améliorer le nommage des méthodes


Pour sécuriser ces opérations de refactorisation, j'utilise le dispositif  du DOCTEST pour m'assurer de la non régression du programme.

Le DOCTEST permet d'inclure des tests dans la documentation des méthodes ou des fonctions.

Il utilise pour cela le motif '>>>'  pour insérer des instructions Python dans la documentation. Le resultat de ces appels est comparé avec la ligne suivante sans préfixe.

Exemple:





Ces insertions n'auront aucun impact sur le fonctionnement des méthodes ou des fonctions.

Pour activer les tests il suffit d'ajouter l'appel au module de DOCTEST.


Dans cet exemple, la fonction à tester est indiquée. Il est possible de lancer tous les test présents dans un programme en remplaçant la methode run_docstring_examples(...) par testmod(..)

Lancement des tests 
sous condition 





Ci dessous un exemple complet (projet pycobol) 

class ZoneGroupe:
    ''' Cette classe prend en charge la creation d'une zone groupe. 
    Par nature cette zone est de type ALN.
    Sa longueur est la somme des longueurs des composants qui la composent
    >>> obj = ZoneGroupe('zoneessai', 1, 0)
    >>> obj.nom
    'zoneessai'
    >>> print(obj)
    >>> objfils = ZoneGroupe('zonefils', 2)
    >>> obj.ajout_fils_groupe(objfils)
    >>> len(obj.fils)
    1
    >>> objfils.pere # doctest: +ELLIPSIS
    ZoneGroupe(nom='zoneessai'...
    >>> objfils2 = ZoneGroupe('zonefils2', 1)
    >>> obj.ajout_fils_groupe(objfils2)  # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    >>> objfilsimp = ZoneFilsSimple('essaifils', 5, picture = '999')
    >>> obj.ajout_fils_simple(objfilsimp)
    >>> obj.longueur_utile
    3
    '''

La directive #doctest: +ELLIPSIS  permet de signaler au module DOCTEST d'arrêter la comparaison du résultat après les trois points (...) . C'est très utile pour tester une exception: la sortie complète de l'exception (trace de la pile d'exécution) sera  ignorée. Il est aussi utilisé ici pour des contenus un peu longs comme les objets.


class ZoneGroupe(builtins.object)
 |  ZoneGroupe(nom: str, rang: int, pere: int = 0, fils: list[int] = <factory>, son_type: str = 'GRP', usage: str = 'DISPLAY', longueur_utile: int = 0, valeur_interne: str = '', valeur_externe: str = '', section: str = 'NON RENSEIGNE') -> None
 |
 |  Cette classe prend en charge la creation d'une zone groupe
 |  par nature cette zone est de type ALN.
 |  Sa longueur est la somme des longueurs des composants qui la composent
 |  >>> obj = ZoneGroupe('zoneessai', 1, 0)
 |  >>> obj.nom
 |  'zoneessai'
 |  >>> print(obj)
 |  >>> objfils = ZoneGroupe('zonefils', 2)
 |  >>> obj.ajout_fils_groupe(objfils)
 |  >>> len(obj.fils)
 |  1
 |  >>> objfils.pere # doctest: +ELLIPSIS
 |  ZoneGroupe(nom='zoneessai'...
 |  >>> objfils2 = ZoneGroupe('zonefils2', 1)
 |  >>> obj.ajout_fils_groupe(objfils2)  # doctest: +ELLIPSIS
 |  Traceback (most recent call last):
 |  ...
 |  >>> objfilsimp = ZoneFilsSimple('essaifils', 5, picture = '999')
 |  >>> obj.ajout_fils_simple(objfilsimp)
 |  >>> obj.longueur_utile
 |  3

Résultat de l'instruction help 

Ci-dessous un résultat partiel d'exécution des tests

Trying:
    objfils = ZoneGroupe('zonefils', 2)
Expecting nothing
ok
Trying:
    obj.ajout_fils_groupe(objfils)
Expecting nothing
ok
Trying:
    len(obj.fils)
Expecting:
    1
ok
Trying:
    objfils.pere # doctest: +ELLIPSIS
Expecting:
    ZoneGroupe(nom='zoneessai'...
ok
Trying:
    objfils2 = ZoneGroupe('zonefils2', 1)
Expecting nothing
ok

Résultats des tests.

En conclusion.

Python nous donne la possibilité de se tromper et d'améliorer notre ouvrage par petites touches. 

Ces tests intégrés sont à compléter par des tests unitaires avec une couverture du code mesuré avec la commande coverage.



mardi 12 octobre 2021

Les listes chainées

 La connaissance des structures de bases en algorithmie est nécessaire pour bien maitriser la programmation.

Les listes chainées sont très utilisées dans les blockchains. Je vais présenter des  exemples de manipulation de liste chainées en relation avec un des mécanismes qu'on trouve dans une blockain.


Principes d'une liste chainée.

Une liste chainée, est un ensemble d'élément (maillon) qui en plus de de porter une valeur, possède un attribut qui pointe vers l'élément suivant  ou précédent. Une liste est doublement chainée si elle permet de se déplacer vers l'élément suivant (chainage avant) et précédent (chainage arrière).

Dans la cas d'une blockchain le chainage est vers l'arrière.  Le bloc 'n' fourni les informations pour retrouver le bloc  précédent (n-1). La raison est simple: dans le cas contraire : en cas d'ajout d'un bloc, il faudrait 'défaire' le dernier bloc  pour mettre à jour son pointeur vers le bloc suivant.

Notre liste chainée.


Nous allons commencer par une liste à chainage arrière comme ceci:
 


(illustration réalisée avec https://excalidraw.com/) 

En Python cela donnera:

class Maillon():
    def __init__(self, idblock,valeur):
        self.idblock = idblock
        self.valeur= valeur
        self.prec = None
Pour chaque élément de base.

Pour la chaine en elle même :

import random
import string
class Blockchain:
    lettre = set(string.ascii_uppercase)
    lettre = lettre - {'I','O', 'Z'}
    chiffre = set([x for x in range(1,10)])
    possible = lettre | chiffre
    
    def __init__(self):
        self.chaine = []
    

Les variables de classes (lettre, chiffre, possible) vont servir à générer des données aléatoires pour l'identifiant d'un nouveau maillon et pour sa valeur.
Ces données sont mis en œuvre dans une méthode de classe et une méthode statique (décorateurs). La difference n'est pas épaisse: la méthode de classe reçoit le nom de la classe en premier paramètre, et elle pourra ainsi accéder à des attributs de la classe. L'attribut 'possible' est la réunion d'une suite de chiffre et de lettre.


    @classmethod    
    def genere_id(cls):
        indexblock = [ random.choices(list(cls.possible))[0] for x in range(1,10)]
        return  ''.join(str(x)  for x in indexblock)
    @staticmethod   
    def genere_montant():
        return round(random.uniform(20.00, 2000.00), 2)

La creation d'une chaine et d'un premier maillon se fera de la manière suivante:

ma_chaine = Blockchain()
un_maillon=  Maillon(Blockchain.genere_id(), Blockchain.genere_montant())
ma_chaine.ajoute(un_maillon)

Un fois le maillon crée , il sera ajouté à la chaine par la méthode  'ajoute':
Cette méthode fait deux choses: 
  • Recherche du dernier maillon
  • Ajout du maillon avec la référence sur le maillon précédent complétée.

def ajoute(self, maillon):
    maillon.prec = self.recherche_tete()
    self.chaine.append(maillon)
        

Le point le plus délicat est la recherche du dernier bloc. On ne peut pas systématiquement dépiler le dernier maillon, seul le chainage contient l'ordre des maillons. Le dernier maillon est le seul dont l'identifiant n'est pas référencé par un autre bloc.

Le motif de recherche du dernier bloc va consister à avoir une boucle principale pour parcourir tous les blocs et pour chaque bloc rechercher son référencement par un bloc: deuxième parcours de la chaine.
 

def recherche_tete(self):
        cle = None
        for  item in self.chaine:
            for item2 in self.chaine:
                if item.idblock == item2.idblock:
                    continue
                if item2.prec == item.idblock:
                    break
            else: 
                return item.idblock

Cette méthode 'brute' a un gros inconvénient: le temps de traitement augmente exponentiellement en fonction du nombre de maillon : l'ajout d'un 2001eme maillon prendra plus de 3 minutes.

import matplotlib.pyplot as plt


# Data for plotting
t = [0, 500, 1000, 1500, 2000 ]
s = [0.0 , 2.91 , 22.8 , 79.00, 191.00]

fig, ax = plt.subplots()
ax.plot(t, s)

ax.set(xlabel='nombre de maillon', ylabel='duree',
       title='visualisation')
ax.grid()


plt.show()lock

La solution à ce problème de performance consistera  à utiliser une 'sentinelle'.
Une sentinelle est un maillon spécial, crée à l'initialisation de la chaine et qui contiendra l'adresse du dernier bloc.


 def ajoute_sentinelle(self):
        return  Maillon('0'* 10, None)

 def ajoute(self, maillon,sentinelle= None):
        if sentinelle:
            _tmp_maillon =self.recherche_la_sentinelle()
            maillon.prec = _tmp_maillon.prec
            _tmp_maillon.prec = maillon.idblock
        else:    
            maillon.prec = self.recherche_tete()
        self.chaine.append(maillon)
        
    
L'identifiant de la sentinelle sera spécial pour ne pas être confondu avec un maillon normal. 
La recherche de la tête de la liste se résumera à rechercher la sentinelle. cette sentinelle ne fait pas partie d'une blockchain car on doit pouvoir la modifier à chaque ajout d'un nouveau bloc.

samedi 18 septembre 2021

Algorithmie: comment trouver la 2eme valeur la plus élevée d'une liste

 Lors des entretiens d'embauche d'un développeur, il est possible de rencontrer cette question: 

 Comment  trouver le la 2eme valeur la plus élevée d'une liste ? .

Bien qu'anodine, cette question ouvre vers de nombreuses déclinaisons et soulève des nouvelles questions.

Est ce que la liste est finie ?, de taille compatible avec la mémoire du serveur ? etc..

Première réponse qui vient spontanément: trier la liste et prendre le 2eme élément.

Démonstration en Python

Commençons par générer une liste aléatoire de 1000000 éléments de 1 à 50.

 
import random
randomlist = []
for i in range(0,1000000):
    n = random.randint(1,50)
    randomlist.append(n)

Puis récupérons le deuxième élément après le tri.

 
%%time
listetriee = sorted(randomlist,reverse =1)
print(listetriee[1])

50
Wall time: 210 ms

Erreur, on devrait retourner vraisemblablement la valeur 49 et non pas 50.

En examinant le début de la liste triée:

print(listetriee[:20])

[50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50]

On n'a pas traité le cas des valeurs identiques... Pour rendre unique les éléments d'une liste, le plus simple est de passer par un 'set'.

 
%%time
unique = set(randomlist)
print('taille de la liste:', len(unique))
listetriee =sorted(unique, reverse = 1)
print(listetriee[1])

taille de la liste: 50
49
Wall time: 48 ms

La taille finale de la liste fait 50 (ce qui est normal) Le temps d exécution est plus petit en raison de cette diminution de la taille. Le résultat est à présent correct.

Cette méthode ne fonctionnerait pas avec une liste issue d'un flux où d'un fichier de plusieurs Go. Elle ne pourrait pas tenir en mémoire centrale.
Il faut dissocier la recherche des deux éléments les plus élevés et l'alimentation au fils de l'eau.

Pour la recherche du 2eme élément, le tri de toute la collection est inutile: on ne désire que le premier et le deuxième élément. La connaissance du premier élément ne sert qu'à ordonner le deuxième (celui qui vient juste après le premier).

%%time
unmax = 0
deuxmax = 0
for i in randomlist:
    if i > unmax:
        deuxmax = unmax
        unmax = i
    else:
        if i > deuxmax and i < unmax:
            deuxmax =i
            
print(deuxmax)        

49
Wall time: 73.5 ms

Ici, on va parcourir la liste sans la trier, juste sélectionner les deux plus fortes valeurs.
Le temps d'exécution donnée par %%time dans jupyter est plus important que le tri sur la liste sans doublon mais plus rapide que le tri complet de la liste.

A présent on peut s'attaquer au problème de l'alimentation à la volée de ce filtre, à partir par exemple de la lecture d'un fichier.

Nous allons tout d abord fabriquer un fichier de nombre à partir d'une compréhension de liste.

maliste = [ random.randint(1,50) for i in range(10000000)]
with open('grosfichier.txt', 'a') as the_file:
    for i in maliste:
        the_file.write(f"{i}\n")
        

Puis on va lire le fichier et le filtrer à la volée.

%%time
unmax = 0
deuxmax = 0
with open('grosfichier.txt', 'r')  as file:
    for ligne in file:
        i = int(ligne[:-1])
        if i > unmax:
            deuxmax = unmax
            unmax = i
    else:
        if i > deuxmax and i < unmax:
            deuxmax =i
print(deuxmax)


49
Wall time: 3.65 s
        

Et voila, traitement sera plus long du fait de la lecture du fichier mais le filtre sera capable de traiter plusieurs giga de données sans risque de saturer la mémoire.


Good hacking ! with http://hilite.me/ pour la mise en forme du code

dimanche 8 août 2021

Guide de survie pour pandas et Numpy

 

Les librairies Pandas et Numpy sont deux piliers de l’outillage Python pour l’apprentissage automatique. Aussi faut-il connaitre quelques subtilités pour éviter de perdre du temps à la recherche d’erreur.

 Par expérience, je réserve l’utilisation de Pandas au chargement des données et à leur transformation pour ensuite les exporter vers les frameworks comme Tensorflow ou Pytorch.

La phase d’export d’une colonne vers un array Numpy doit s’accompagner d’une vérification des dimensions de l’export. Car Pandas l’exporte vers un tableau qui est à plutôt les caractéristiques d’une liste.

Exemple : Création d'un dataFrame et export vers Numpy d'une colonne.

 


Une dimension unique comme (3,) est le propre d’une série au sens Dataframe ou d’un type liste en Python : ce n’est pas une vrai matrice (array) Numpy.

Dans numpy on peut repérer 3 structures particulières :

Les listes

La matrice sur 1 seule ligne mais bien de dimension 2

La matrice d’une colonne mais bien de dimension 2 (un vecteur)

Il est possible de passer d’une structure à une autre par une opération de ‘reshape’

 Illustration ci-dessous:


Une structure a 1 dimension peut provoquer des erreurs dans des fonctions d'apprentissage automatique et surtout être la cause d'erreur de broadcasting.

Le broadcasting dans numpy.

C'est un mécanisme puissant mais qui peut provoquer des erreurs en cascade.

Les opérations  entre matrice imposent de respecter des contraintes sur les dimensions des matrices:
Le broadcasting permet des opérations hybrides à mi chemin entre les opérations  entre des matrices ou des opérations avec des scalaires.

Exemple : création de 3 matrices: une de 3X3 , une de 1X3 et enfin une de 3X1:


Si on réalise la multiplication scalaire de la matrice a par le 'scalaire' 2 on obtiendra: 

Que ce passe-t-il si je souhaite multiplier la matrice a (3X3) par b (1X3) ? Comme on pourrait le faire avec un scalaire ?  Cette opération hybride n'est pas autorisée et c'est là qu'intervient le broadcasting. Numpy va étendre sous certaines conditions les matrices. 
Les conditions sont les suivantes:
  • Dimensions identiques
  • Une des deux dimensions est égale à 1 
 
Numpy va prendre la ligne de la matrice b et la dupliquer vers le bas afin d'avoir une matrice 3X3.

 


Erreurs de broadcasting.

Prenons maintenant un cas qui va générer des erreurs en cascade.

Soit deux matrice de dimension 5X1 

Leur multiplication 'scalaire' va donner une matrice 5X1


 Si on prend la matrice 'c' qui n'a qu'une seule dimension: le résultat sera différent : une matrice de 5X5




Ainsi la règle d'or est de toujours vérifier la forme de la matrice (shape) avant une opération.




 

vendredi 18 décembre 2020

Régression linéaire avec Pytorch (deep learning)

 Après  avoir exposé différentes manières de mettre en œuvre des ajustements par régressions linéaires en Python, je terminerai par le framework de l'apprentissage automatique ou profond Python. Pytorch est soutenu par facebook. Son approche est différente que celle de Keras/Tensorflow , elle se veut plus programmatique et moins 'boite noire'.

Comme dans les autres billets, je vais commencer par préparer Jupyter pour Pytorch.

Les opérations sont les mêmes que pour tensorflow.

  • Création d'un environnement virtuel Python (voir billet )
  •  Ajout d'un noyau à Jupyter

Installation de Pytoch 

Il suffit de sélectionner  les bonnes options sur la grille et en retour , il vous générera la ligne de commande pour PIP



Apres l'installation il suffira d'ajouter un noyau Pytorch à Jupyter (voir billet).

Sur mon PC cela donne: 



Premier essai de résolution de regression linéaire avec Pytorch

En reprenant le même jeu de donnée que dans les billets précédents:

import torch
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from torch.autograd import Variable
class linearRegression(torch.nn.Module):
    def __init__(self, inputSize, outputSize):
        super(linearRegression, self).__init__()
        self.linear = torch.nn.Linear(inputSize, outputSize)

    def forward(self, x):
        out = self.linear(x)
        return out
On importe les modules de base.
Et on déclare une classe qui doit fournir l'initialisation d'une classe de regression linéaire et comment propager le modèle.

Après initialisation des variables (nombre de tenseur) 
inputDim = 1        # takes variable 'x' 
outputDim = 1       # takes variable 'y'
learningRate = 0.01 
epochs = 150

model = linearRegression(inputDim, outputDim)
criterion = torch.nn.MSELoss() 
optimizer = torch.optim.SGD(model.parameters(), lr=learningRate)
Il faut définir comment le modèle va évaluer l'ecart (loss function)  et comment arriver à un optimum.

On revient ensuite à une recherche de solution conventionnelle dans une boucle:


history = []

for epoch in range(epochs):
    # Converting inputs and labels to Variable
    inputs = Variable(torch.from_numpy(X_train))
    labels = Variable(torch.from_numpy(y_train))
    # Clear gradient buffers because we don't want any gradient from previous epoch to carry forward, dont want to cummulate gradients
    optimizer.zero_grad()

    # get output from the model, given the inputs
    outputs = model(inputs)

    # get loss for the predicted output
    loss = criterion(outputs, labels)
   # print(loss.item())
    history.append(loss.item())
    # get gradients w.r.t to parameters
    loss.backward()

    # update parameters
    optimizer.step()
   
   # print('epoch {}, loss {}'.format(epoch, loss.item()))
[w, b] = model.parameters()
print(w)
print(b)
print('intercept :' ,0 - model.linear.bias.data.numpy()[0])
print('slope :' , model.linear.weight.data.numpy()[0][0])

Il est possible de visualiser l'évolution de la fonction de perte : 

Avec un résultat final semblable à nos autres méthodes : 


Il est possible d'obtenir des évaluations: 

ww = -model.linear.bias.data.numpy()[0]
with torch.no_grad(): # we don't need gradients in the testing phase
    v =  torch.tensor([3.5])
    w = torch.tensor([7.0])
    orig = torch.tensor([ww ])
    test1 = model(v) 
    test2 = model(w)
    test3 = model(orig)
print(test1*10000,test2*10000, test3 * 10000)    

A noter qu'il n'a pas été nécessaire de normaliser ou de remettre à l'échelle des données. 

Pytorch propose un mix entre tensorflow (apprentissage profond) et l'approche en apprentissage automatique.


Le notebook est disponible ici sur github 

Les sites consultés et sources sont:
et



mercredi 9 décembre 2020

Régression linéaire en deep learning avec Keras et tensorflow

Dans des billets précédents, j'avais montré différentes manières de trouver une droite qui s'ajuste au mieux à une série de point. essayons maintenant avec de l'apprentissage profond. Les données seront les mêmes.

L'environnement d 'exécution est un notebook sous Jupyter (voir ici sa préparation) .

Un réseau simple.

Pour monter notre dispositif nous avons besoin de:

  • Un réseau avec des couches et des fonctions d'activation
  • Une fonction d'évaluation de coût (ou de perte) 
  • Un optimiseur :dispositif qui met à jour les paramètres afin d'atteindre une solution optimum

Et en option

  • Un indicateur de performance à surveiller

Le réseau sera très simple pour un premier essai: une couche en entrée et une en sortie: (utilisation de http://hilite.me/)

model = Sequential()
model.add(Dense(1, input_dim= 1, kernel_initializer =initializers.RandomNormal(seed= 1), activation='linear'))
model.add(Dense(1, input_dim= 1, kernel_initializer =initializers.random_normal(seed= 1)))
model.compile(loss="mean_squared_error", optimizer ='sgd')

Ici , l'optimisateur est  de  type SGD  "Gradient descent (with momentum) optimizer".

Une préparation des données.

Ici, pas question d'utiliser des données brutes. Il est nécessaire de les mettre à l'echelle ou de les normaliser. 

Ci dessous la fonction de normalisation :

def normalise(dataf):
    mu    = 0
    sigma = 0
    mu = dataf.mean()
    sigma = dataf.std( ddof=0)
    snorm = dataf
    snorm.columns = ['normalised']
    snorm =(snorm - mu )/ sigma
    dtnorm = pd.concat([dataf,snorm],  axis =1)
    dtnorm.columns = ['x', 'nx']
    print(mu, sigma)
    print(snorm.head(5))
    return dtnorm , mu , sigma

Lancement.

Pour l'exécution, on peut jouer sur deux paramètres: le nombre cycle d'apprentissage (epochs) et la taille de chaque lot de donnée (batch size) 
Si on fixe à 10 la taille des lots pour un jeu de 100 données, il faut 10 passages pour faire un cycle complet(epochs) 

history =model.fit(X,y, epochs =  epoch,  batch_size = bz,verbose  = 0)

Résultats.


J'ai essayé avec 10 cycles et des tailles de lot différentes (taille totale = 97 lignes)

les résultats sont les suivants:


Les 3 premières tailles sont les plus rapides à converger: 



Passons maintenant aux estimations  pour les 2 valeurs : 3500 et 70000
pour une taille de 1
écart de
[1952.696]  
[3562.5508]
pour une taille de 10
écart de
[1271.1694]
[1076.8672]

pour une taille de 15
écart de
[589.0293]
[125.20703]

Une taille de 15 semble la bonne valeur, elle correspond à une proportion de 10 à 20% de la taille totale des données d'apprentissage.

Explication des écarts: on a utilisé une fonction qui normalise les données et qui va avoir une action d'écrasement des données. Une mise à l'échelle aurait été peut être plus fidèle.

Recommandation: pour faire plusieurs essais, il est nécessaire de redémarrer le noyau Jupyter. Tensorflow stocke des résultats en cache et cela peut avoir un impact. Pour ma part, je stocke les résultats intermédiaires dans un fichier python (pickle).

Les deux notebook  (réseau et affichage des résultats) sont sur github .





lundi 23 novembre 2020

Jupyter pour le deep learning

 A l'occasion de la sortie en Français du livre de François Chollet: 



J'ai préparé mon environnement pour utiliser Keras et Tensorflow dans un notebook Jupyter.

Le prérequis est d'avoir les notebook Jupyter installés.

Création d'un environnement Python spécifique à Keras et Tensorflow.

Utiliser le module Python natif venv  pour créer un espace propre à vos programmes et  librairies Python.

python -m venv --system-site-packages <env_a_creer>

Activer l'environnement virtuel par la commande suivante exécutée depuis le répertoire crée: 

source bin/activate

Installer dans cet environnement Keras et Tensorflow avec la commande 'pip'

(suivre les instruction du site Tensorflow)

Vérifier la bonne installation par une tentative d'import de keras depuis l'invite python (exemple)

from keras.models import Sequential


Vérifier le fichier de configuration keras.json situé sous le répertoire '.keras' du répertoire de l'utilisateur.

Ce fichier renseigne sur le backend à utiliser : ici tensorflow.

Installer un nouveau noyau  (kernel) à Jupyter.

Toujours depuis votre répertoire <venv>, lancer la commande:

python -m ipykernel install --user --name=<venv> --display-name 'keras'

 Pour vérifier les kernels installés :

jupyter kernelspec list

Le kernel ajouté doit figurer dans la liste.

Pour le supprimer: 

jupyter kernelspec remove <venv>

Pour tester:

Lancer Jupyter et vérifier la presence d'un kernel supplémentaire (ici keras)

Puis tester l'import des librairies Keras




 Accéder à distance à l'instance Jupyter.

option utile  comme dans mon cas où Jupyter est installé sur un ultramini-portable  

Lancer Jupyter sur le mini-portable: 

 jupyter notebook --ip=0.0.0.0 --NotebookApp.token=''

L'option 'ip' permet de demander à Jupyter d'écouter sur toutes les interfaces réseaux.

La dernière option évite d'avoir à rentrer un token à rallonge dans le navigateur distant.

Il suffit alors de faire pointer son navigateur distant sur http://192.168.1.xx:8888/





Mon ultramini-portable Chuwi: grand comme ma main mais très puissant. 

En bonus: changer l'aspect de Jupyter

Dans le répertoire  .jupyter de l'utilisateur 

Créer l'arborescence suivante: 

/home/<user>/.jupyter/custom

Puis éditer un nouveau fichier custom.css

Le mien contient les lignes suivantes:

egerman@egerman-MiniBook:~/.jupyter/custom$ cat custom.css
#ipython_notebook::before{
 content:"Jupyter sur Chuwi"
}
#ipython_notebook img{
 display:none;
}
body > #header {
    background: #78abd2;
}
div.cell {
    transition: all 0.25s;
    border: none;
    position: relative;
    top: 0;
}
div.cell.selected, div.cell.selected.jupyter-soft-selected {
    border: none;
    background: transparent;
    box-shadow: 0 6px 18px #aaa;
    z-index: 10;
    top: -10px;
}

Avec comme résultat: 
  • Un titre personnalisé
  • Un mise en avant de la cellule active