Dans la partie Training Script, vous allez travailler sur un problème de super-résolution d’image en utilisant une architecture de deep learning inédite. L’objectif est d’apprendre une mise en correspondance non linéaire d’une IRM cérébrale à bas champ 3 Tesla vers une IRM à haut champ 7 Tesla. Dans cette partie Testing Script, vous utiliserez les poids entraînés en Partie 1 pour faire des prédictions sur des données inédites. Vous verrez aussi comment enregistrer des images 2D sous forme d’un volume combiné.
Le tutoriel est divisé en deux parties : la première couvre l’entraînement, la seconde le test.
Ensuite, vous définirez les initialiseurs et chargerez les jeux 3T et 7T ; au chargement, vous redimensionnerez aussi les images à la volée,
Puis vous préprosesserez les données chargées : conversion des listes train et test en matrices numpy, conversion des types en float32, remise à l’échelle min–max, reshape des tenseurs et découpe en 80 % entraînement et 20 % validation,
Vous créerez ensuite l’architecture 1-Encoder-3-Decoder avec connexions de fusion et décodeurs multiples,
Avant de suivre ce tutoriel, assurez-vous d’utiliser exactement les versions ci-dessous :
Keras==2.0.4
tensorflow==1.8.0
scipy==0.19.0
numpy==1.14.5
Pillow==4.1.1
nibabel==2.1.0
scikit_learn==0.18.1
Remarque : Le modèle est entraîné sur une machine équipée d’un GPU Nvidia 1080 Ti, processeur Xeon e5 GeForce et 32 Go de RAM. Si vous utilisez Jupyter Notebook, ajoutez trois lignes pour définir l’ordre des périphériques CUDA et les GPU visibles via le module os.
Dans le code ci-dessous, vous définissez des variables d’environnement via os.environ dans le notebook. Il est recommandé de le faire avant d’initialiser Keras pour limiter TensorFlow (backend Keras) au premier GPU. Si la machine possède un GPU à l’index 0, utilisez 0 au lieu de 1. Vous pouvez le vérifier en lançant la commande nvidia-smi dans votre terminal.
import os
os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"]="0"#model will be trained on GPUPropulsé par
Importation des modules
Commencez par importer les modules requis comme tensorflow, numpy et surtout keras, ainsi que les fonctions/couches nécessaires (Input, Conv2D, MaxPooling2D, etc.), puisque vous les utiliserez pour l’entraînement du modèle. Pour lire des images au format NIfTI, importez également le module nibabel.
import os
import numpy as np
import math
import tensorflow as tf
import nibabel as nib
import numpy as np
from keras.layers import Input,Dense,merge,Reshape,Conv2D,MaxPooling2D,UpSampling2D
from keras.layers.normalization import BatchNormalization
from keras.models import Model,Sequential
from keras.callbacks import ModelCheckpoint
from keras.optimizers import RMSprop
from keras import backend as K
import scipy.misc
from sklearn.utils import shuffle
from sklearn.cross_validation import train_test_split
import matplotlib.pyplot as plt
from keras.models import model_from_json
Propulsé par
Using TensorFlow backend./usr/local/lib/python3.5/dist-packages/sklearn/cross_validation.py:41: DeprecationWarning: This module was deprecated in version 0.18in favor of the model_selection module into which all the refactored classes and functions are moved. Also, note that the interfaceof the newCV iterators is different from that ofthis module. This module will be removed in0.20."This module will be removed in 0.20.", DeprecationWarning)Propulsé par
Comprendre le jeu de données IRM cérébrales 3T et 7T
Le jeu de données 3T et 7T se compose de volumes 3D ; chaque volume contient 207 coupes/images d’IRM cérébrales à différentes profondeurs. Chaque coupe mesure 173 x 173. Les images sont en niveaux de gris (monocanal). On compte 39 sujets au total, chaque sujet correspondant à une acquisition IRM d’un patient. Le format d’image n’est pas jpeg ou png, mais NIfTI. Vous verrez plus loin comment lire ce format.
Le jeu se compose d’images IRM en modalité T1, traditionnellement adaptées à l’évaluation des structures anatomiques. Dans ce tutoriel, vous travaillerez sur des IRM cérébrales 3T et 7T.
Le jeu de données est public et téléchargeable à cette adresse.
28 sujets sont utilisés pour l’entraînement et les 11 restants pour le test.
Définir les initialiseurs
Commençons par définir les dimensions des données. Vous redimensionnerez les images de 173x173 à 176x176 lors de la lecture. Ici, vous définissez aussi le répertoire des données, la taille de lot pour l’entraînement, le nombre de canaux, une couche Input(), des listes pour les matrices d’entraînement et de test, et enfin, pour la remise à l’échelle, vous chargez un fichier texte contenant les valeurs minimale et maximale du jeu IRM.
Remarque : Pour la remise à l’échelle, vous pouvez également prendre le min et le max de votre propre jeu de données si vous n’avez pas le fichier maxANDmin.txt.
Chargez ensuite les données IRM via la bibliothèque nibabel et redimensionnez les images de 173 x 173 à 176 x 176 en complétant par des zéros selon x et y.
Notez qu’au chargement d’un volume NIfTI, Nibabel ne lit pas immédiatement le tableau d’images ; il attend qu’on le lui demande, via la méthode get_data().
Comme vous souhaitez des coupes 2D (et non 3D), vous utiliserez les listes train et test initialisées plus haut ; à chaque volume lu, itérez sur les 207 coupes du volume 3D et ajoutez-les une par une à la liste.
for f in folder:
temp = np.zeros([resizeTo,full_z,resizeTo])
a = nib.load(inp + f)
a = a.get_data()
temp[3:,:,3:]= a
a = temp
for j inrange(full_z):
train_matrix.append(a[:,j,:])Propulsé par
Puis chargez les données 7T. Vous pouvez réutiliser la variable folder puisque le nombre de volumes 3T et 7T est identique.
for f in folder:
temp = np.zeros([resizeTo,full_z,resizeTo])
b = nib.load(out + f)
b = b.get_data()
temp[3:,:,3:]= b
b = temp
for j inrange(full_z):
test_matrix.append(b[:,j,:])Propulsé par
Prétraitement des données
Les matrices train et test sont des listes ; convertissez-les en tableaux numpy.
Ensuite, convertissez leur type en float32 et remettez à l’échelle à l’aide de la stratégie min–max, pour l’input et le ground truth.
Créez ensuite deux nouvelles variables : augmented_images (3T/input) et Haugmented_images (7T/ground truth) ayant la forme des matrices train et test. Ce seront des matrices 4D : première dimension = nombre total d’images, deuxième et troisième = dimensions de chaque image, dernière = nombre de canaux (ici 1).
augmented_images=np.zeros(shape=[(train_matrix.shape[0]),(train_matrix.shape[1]),(train_matrix.shape[2]),(1)])
Haugmented_images=np.zeros(shape=[(train_matrix.shape[0]),(train_matrix.shape[1]),(train_matrix.shape[2]),(1)])Propulsé par
Passez ensuite sur toutes les images : à chaque itération, faites un reshape en 176 x 176 et affectez aux tenseurs augmented_images (3T/input) et Haugmented_images (7T/ground truth).
for i inrange(train_matrix.shape[0]):
augmented_images[i,:,:,0]= train_matrix[i,:,:].reshape(resizeTo,resizeTo)
Haugmented_images[i,:,:,0]= test_matrix[i,:,:].reshape(resizeTo,resizeTo)Propulsé par
Il est ensuite crucial de partitionner les données. Pour une bonne généralisation, découpez en train/validation : 80 % pour l’entraînement et 20 % pour la validation.
Cela réduit aussi le risque de surapprentissage en validant sur des données non vues pendant l’entraînement.
Utilisez train_test_split de scikit-learn défini au début pour un découpage correct :
Définissez ensuite l’architecture proposée avec des blocs de couches de filtres successives suivies d’un max pooling dans la partie encodeur, comme ci-dessous. Pour reconstruire la taille d’origine en sortie, une couche d’upsampling est ajoutée dans chaque bloc de décodeur. Lors de l’upsampling, des artefacts peuvent apparaître par manque de détails dans l’entrée sous-échantillonnée du décodeur. Vous concaténez donc l’entrée du décodeur avec sa version mise à l’échelle issue de l’encodeur, pour fournir des détails pertinents et améliorer la reconstruction. L’ajout de connexions de fusion améliore sensiblement le PSNR (de l’ordre de 5 dB). Ce choix architectural s’inspire de cet article.
Multi-décodeurs
L’approche utilise un encodeur unique et plusieurs décodeurs avec une entrée monocanal. Trois couches convolutionnelles sont utilisées dans chaque bloc de l’encodeur et des trois décodeurs, suivies d’une couche de normalisation de lot pour la stabilité numérique.
Dans l’encodeur, le premier bloc convolutionnel comporte 32 filtres, puis le nombre de filtres est doublé à chaque bloc. Dans les décodeurs, le premier bloc a 256 filtres, puis ce nombre est divisé par deux à chaque bloc. La taille des filtres est 3 x 3 partout.
La fonction d’activation ReLU est utilisée dans toutes les couches sauf la dernière. Les données étant normalisées entre 0 et 1, une activation Sigmoid est utilisée en sortie finale.
On sait que les détails locaux à différentes échelles jouent un rôle clé en reconstruction d’image. L’architecture considère des images à plusieurs échelles via des couches hiérarchiques de sous-échantillonnage (max pooling) et suréchantillonnage (facteur 2) dans l’encodeur et les décodeurs. La représentation encodée après trois sous-échantillonnages projette l’entrée de haute dimension vers un espace latent.
Dans les 4 cellules ci-dessus, vous avez défini quatre fonctions : une pour l’encodeur et trois pour les décodeurs. Vous pourriez définir une fonction decoder() unique et l’appeler trois fois, mais pour la clarté et afin d’éviter des effets de hasard entre décodeurs, elles sont séparées.
Fonction de perte
Vous utiliserez une erreur quadratique moyenne en excluant les valeurs (pixels) égales à zéro dans le ground truthy_t et la prédiction y_p.
defroot_mean_sq_GxGy(y_t, y_p):
a1=1
where = tf.not_equal(y_t,0)
a_t=tf.boolean_mask(y_t,where,name='boolean_mask')
a_p=tf.boolean_mask(y_p,where,name='boolean_mask')return a1*(K.sqrt(K.mean((K.square(a_t-a_p)))))Propulsé par
Définition et compilation du modèle
Commencez par appeler la fonction encoder en lui passant l’entrée. Comme l’architecture utilise des connexions de fusion, la fonction encoder renvoie les sorties de cinq couches convolution qui seront ensuite fusionnées séparément avec chaque décodeur.
conv5,conv4,conv3,conv2,conv1 = encoder(input_img)Propulsé par
/usr/local/lib/python3.5/dist-packages/ipykernel_launcher.py:2: UserWarning: The `merge`function is deprecated and will be removed after 08/2017. Use instead layers from`keras.layers.merge`, e.g.`add`,`concatenate`, etc./usr/local/lib/python3.5/dist-packages/keras/legacy/layers.py:460: UserWarning: The `Merge` layer is deprecated and will be removed after 08/2017. Use instead layers from`keras.layers.merge`, e.g.`add`,`concatenate`, etc.
name=name)/usr/local/lib/python3.5/dist-packages/ipykernel_launcher.py:10: UserWarning: The `merge`function is deprecated and will be removed after 08/2017. Use instead layers from`keras.layers.merge`, e.g.`add`,`concatenate`, etc.
# Remove the CWD from sys.path while we load stuff./usr/local/lib/python3.5/dist-packages/ipykernel_launcher.py:18: UserWarning: The `merge`function is deprecated and will be removed after 08/2017. Use instead layers from`keras.layers.merge`, e.g.`add`,`concatenate`, etc./usr/local/lib/python3.5/dist-packages/ipykernel_launcher.py:26: UserWarning: The `merge`function is deprecated and will be removed after 08/2017. Use instead layers from`keras.layers.merge`, e.g.`add`,`concatenate`, etc.WARNING:tensorflow:From /usr/local/lib/python3.5/dist-packages/keras/backend/tensorflow_backend.py:1257: calling reduce_mean(from tensorflow.python.ops.math_ops)with keep_dims is deprecated and will be removed in a future version.
Instructions forupdating:
keep_dims is deprecated, use keepdims instead
Propulsé par
Entraîner le modèle
Vous enregistrerez les poids uniquement lorsque le peak signal-to-noise ratio sur la validation s’améliore. Pour cela, définissez une liste psnr_gray_channel avec une valeur initiale 1 (valeur factice largement dépassée dès les premières itérations).
Initialisez le taux d’apprentissage à 1e-3 et appliquez une décroissance : réduction de 10 % tous les 20 epochs.
psnr_gray_channel =[]
psnr_gray_channel.append(1)
learning_rate =0.001
j=0Propulsé par
Remarque : Le code qui suit doit idéalement être exécuté en un seul bloc, mais il est découpé ici pour faciliter la compréhension.
Le modèle est entraîné pendant 500 epochs. Le learning rate initial est 1e-3 comme défini. Vous enregistrerez aussi PSNR et MSE après chaque epoch entre 7T et la prédiction. Vous utiliserez K.set_value pour diminuer le taux d’apprentissage de 10 % toutes les 20 époques pour les trois modèles.
for jj inrange(500):
myfile_valid_psnr_7T =open('../1_encoder_3_decoders_complete_slices_single_channel/validation_psnr7T_1encoder_3decoders.txt','a')
myfile_valid_mse_7T =open('../1_encoder_3_decoders_complete_slices_single_channel/validation_mse7T_1encoder_3decoders.txt','a')
K.set_value(autoencoder_1.optimizer.lr, learning_rate)
K.set_value(autoencoder_2.optimizer.lr, learning_rate)
K.set_value(autoencoder_3.optimizer.lr, learning_rate)Propulsé par
Brassez ensuite les paires 3T/7T pour éviter le surapprentissage lié à l’ordre des échantillons. Calculez le nombre de lots selon la batch_size définie, puis itérez sur num_batches.
train_X,train_Y = shuffle(X_train,y_train)print("Epoch is: %d\n"% j)print("Number of batches: %d\n"%int(len(train_X)/batch_size))
num_batches =int(len(train_X)/batch_size)for batch inrange(num_batches):Propulsé par
En plus des valeurs de PSNR, stockez aussi les pertes des trois autoencodeurs et de leurs décodeurs respectifs.
À chaque batch, vous souhaitez faire voir les 32 échantillons suivants au modèle : le code suivant s’en charge.
batch_train_X = train_X[batch*batch_size:min((batch+1)*batch_size,len(train_X)),:]
batch_train_Y = train_Y[batch*batch_size:min((batch+1)*batch_size,len(train_Y)),:]Propulsé par
Pour la stratégie d’autoencodeur minimum, après chaque epoch vous testez sur les données d’entraînement via test_on_batch de Keras, qui renvoie trois pertes. Affichez-les.
Six cas de figure sont possibles dans votre réseau : loss_1 : perte de l’autoencodeur 1 loss_2 : perte de l’autoencodeur 2 loss_3 : perte de l’autoencodeur 3
loss_1 peut être plus petite que loss_2 et loss_3 : dans ce cas, vous n’entraînez que l’autoencodeur 1. Les parties encodeur des autoencodeurs 2 et 3 sont gelées (False) et seuls leurs décodeurs sont entraînés ; consignez toutes les pertes dans les fichiers.
loss_2 plus petite que loss_1 et loss_3 : vous n’entraînez que l’autoencodeur 2, et vous gelez les parties encodeur de 1 et 3, etc.
loss_3 plus petite que loss_1 et loss_2 : vous n’entraînez que l’autoencodeur 3, etc.
loss_1 égale à loss_2 : entraînez soit l’autoencodeur 1 soit le 2, gelez l’encodeur du 3 et celui non choisi entre 1 et 2, puis n’entraînez que leurs décodeurs ; enregistrez les pertes.
loss_2 égale à loss_3 : entraînez soit le 2 soit le 3, gelez l’encodeur du 1 et celui non choisi entre 2 et 3 ; enregistrez les pertes.
loss_3 égale à loss_1 : entraînez soit le 3 soit le 1, gelez l’encodeur du 2 et celui non choisi entre 3 et 1 ; enregistrez les pertes.
Étape essentielle : comme vous avez mis certaines couches à False ci-dessus, il faut les remettre à True afin que tous les autoencodeurs soient utilisés par test_on_batch et qu’ils ne restent pas gelés.
for layer in autoencoder_1.layers[:34]:
layer.trainable =Truefor layer in autoencoder_2.layers[:34]:
layer.trainable =Truefor layer in autoencoder_3.layers[:34]:
layer.trainable =TruePropulsé par
Vous enregistrerez les poids de trois manières, dont deux ci-dessous : d’une part après chaque 100 epochs, d’autre part après chaque epoch (écrasement).
if jj %100==0:
autoencoder_1.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE1_"+str(jj)+".h5")
autoencoder_2.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE2_"+str(jj)+".h5")
autoencoder_3.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE3_"+str(jj)+".h5")
autoencoder_1.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE1.h5")
autoencoder_2.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE2.h5")
autoencoder_3.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE3.h5")Propulsé par
Test sur les données de validation
Brassez d’abord les données de validation, puis appliquez les mêmes 6 conditions que précédemment. Quelle que soit la condition vraie, testez votre modèle avec l’autoencodeur correspondant.
Calculez ensuite MSE et PSNR entre decoded_imgs (prédictions) et ground truth, puis enregistrez-les dans des fichiers texte.
Ici, vous n’enregistrez les poids que si le PSNR entre la prédiction 7T et le ground truth (7T) dépasse la meilleure valeur précédente stockée dans psnr_gray_channel.
ifmax(psnr_gray_channel)< psnr_7T:
autoencoder_1.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE1_"+str(jj)+".h5")
autoencoder_2.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE2_"+str(jj)+".h5")
autoencoder_3.save_weights("../Model/CROSSVAL1/CROSSVAL1_AE3_"+str(jj)+".h5")
psnr_gray_channel.append(psnr_7T)Propulsé par
Enregistrer input, ground truth et prédictions : résultats quantitatifs
Définissez une matrice numpy temp de taille 176 x 528 pour sauvegarder 3 images côte à côte (176 x 176 chacune). Enregistrez l’une des images validation 3T, 7T et prédite, et multipliez par 255 car les images ont été normalisées entre 0 et 1.
La définition des initialiseurs et le chargement du jeu de test 3T et 7T, avec redimensionnement à la volée,
Le prétraitement des données : conversion des listes en matrices numpy, passage en float32, remise à l’échelle min–max, reshape et (si besoin) découpe train/validation,
La création de l’architecture 1-Encoder-3-Decoder : connexions de fusion et multi-décodeurs,
La définition de la perte, la création de trois modèles et le chargement des poids entraînés,
Enfin, la prédiction sur des données inédites et l’enregistrement des résultats quantitatifs et qualitatifs. Vous apprendrez aussi à sauvegarder des images 2D en un volume combiné avec nibabel.
Dépendances des modules Python
Avant de commencer, assurez-vous d’utiliser les mêmes versions que ci-dessous :
Keras==2.0.4
tensorflow==1.8.0
scipy==0.19.0
numpy==1.14.5
Pillow==4.1.1
nibabel==2.1.0
scikit_learn==0.18.1
Remarque : Le modèle est entraîné sur une machine avec GPU Nvidia 1080 Ti, processeur Xeon e5 GeForce et 32 Go de RAM. Sous Jupyter Notebook, ajoutez trois lignes de configuration CUDA via le module os.
Dans le code ci-dessous, vous définissez des variables d’environnement via os.environ. Faites-le avant d’initialiser Keras pour contraindre TensorFlow à utiliser le premier GPU. Si votre machine a un GPU en position 0, utilisez 0 au lieu de 1. Vérifiez via nvidia-smi.
import os
os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"]="0"#model will be trained on GPUPropulsé par
Importation des modules
Importez d’abord tensorflow, numpy et surtout keras ainsi que les couches nécessaires (Input, Conv2D, MaxPooling2D, etc.), car vous les utiliserez pour l’entraînement du modèle. Pour lire des images au format NIfTI, importez aussi nibabel.
import os
from keras.layers import Input,Dense,Flatten,Dropout,merge,Reshape,Conv2D,MaxPooling2D,UpSampling2D
from keras.layers.normalization import BatchNormalization
from keras.models import Model,Sequential
from keras.callbacks import ModelCheckpoint
from keras.optimizers import Adadelta, RMSprop,SGD,Adam
from keras import regularizers
from keras import backend as K
import numpy as np
import scipy.misc
import numpy.random as rng
from sklearn.utils import shuffle
import nibabel as nib
from sklearn.cross_validation import train_test_split
import math
Propulsé par
Comprendre le jeu de données IRM cérébrales 3T et 7T
Le jeu 3T/7T comprend des volumes 3D ; chaque volume contient 207 coupes d’IRM cérébrales. Chaque coupe mesure 173 x 173. Les images sont monocanal en niveaux de gris. On compte 39 sujets au total. Le format est NIfTI (et non jpeg/png). Vous verrez plus loin comment lire ce format.
Il s’agit d’images T1, traditionnellement adaptées à l’étude de l’anatomie. Aujourd’hui, vous travaillerez sur des IRM cérébrales 3T et 7T.
Le jeu est public et disponible au téléchargement à cette source.
Définir les initialiseurs
Définissons d’abord les dimensions. Vous redimensionnerez de 173x173 à 176x176 lors de la lecture. Nous définissons aussi le répertoire des données, la taille des lots, le nombre de canaux, une couche Input(), des listes train/test et, pour la remise à l’échelle, le fichier texte contenant min et max du jeu IRM.
train_matrix =[]
test_matrix =[]
ff = os.listdir("../test_crossval1")
save ="../Result_nii_crossval1"
folder_ground = os.listdir("../test_g_crossval1")
ToPredict_images=[]
predict_matrix=[]Propulsé par
ground_images=[]
ground_matrix=[]
min_max=np.loadtxt('../maxANDmin.txt')Propulsé par
Charger les volumes de test
Chargez les données IRM avec nibabel et redimensionnez de 173 x 173 à 176 x 176 par padding en x et y.
Rappel : au chargement d’un volume NIfTI, Nibabel ne lit pas directement le tableau d’images ; utilisez get_data().
Comme vous voulez des coupes 2D, utilisez les listes train et test initialisées ; pour chaque volume lu, itérez sur les 207 coupes et ajoutez-les une à une à la liste.
for f in ff:
temp = np.zeros([resizeTo,full_z,resizeTo])
a = nib.load("../test_crossval1"+ f)
affine = a.affine
a = a.get_data()
temp[3:,:,3:]= a
a = temp
for j inrange(full_z):
predict_matrix.append(a[:,j,:])Propulsé par
for f in ff:
temp = np.zeros([resizeTo,full_z,resizeTo])
a = nib.load("../test_g_crossval1"+ f)
affine = a.affine
a = a.get_data()
temp[3:,:,3:]= a
a = temp
for j inrange(full_z):
ground_matrix.append(a[:,j,:])Propulsé par
Prétraitement des données
Les matrices de test 3T et 7T étant des listes, convertissez-les en tableaux numpy.
Convertissez ensuite le type en float32 et remettez à l’échelle input et ground truth.
Créez ensuite deux variables ToPredict_images (test 3T/input) et ground_images (test 7T/ground truth) ayant la forme des matrices train/test : matrices 4D (nombre d’images, hauteur, largeur, canaux = 1).
ToPredict_images=np.zeros(shape=[(ToPredict_images.shape[0]),(ToPredict_images.shape[1]),(ToPredict_images.shape[2]),(1)])
ground_images=np.zeros(shape=[(ground_images.shape[0]),(ground_images.shape[1]),(ground_images.shape[2]),(1)])Propulsé par
Puis itérez sur toutes les images : à chaque fois, faites un reshape en 176 x 176 et affectez à ToPredict_images (test 3T/input) et ground_images (test 7T/ground truth).
for i inrange(ToPredict_images.shape[0]):
ToPredict_images[i,:,:,0]= ToPredict_images[i,:,:].reshape(resizeTo,resizeTo)Propulsé par
for i inrange(ground_images.shape[0]):
ground_images[i,:,:,0]= ground_images[i,:,:].reshape(resizeTo,resizeTo)Propulsé par
Vous utiliserez une erreur quadratique moyenne en excluant les pixels nuls dans y_t (ground truth) et y_p (prédiction).
defroot_mean_sq_GxGy(y_t, y_p):
a1=1
zero = tf.constant(0, dtype=tf.float32)
where = tf.not_equal(y_t, zero)
a_t=tf.boolean_mask(y_t,where,name='boolean_mask')
a_p=tf.boolean_mask(y_p,where,name='boolean_mask')return a1*(K.sqrt(K.mean((K.square(a_t-a_p)))))Propulsé par
Définition du modèle et chargement des poids dans les trois autoencodeurs
Appelez d’abord la fonction encoder en lui passant l’entrée. Comme l’architecture utilise des connexions de fusion, l’encodeur retourne la sortie de cinq couches convolution qui seront fusionnées avec les sorties des trois décodeurs.
conv5,conv4,conv3,conv2,conv1 = encoder(input_img)Propulsé par
Créez maintenant trois modèles et chargez les poids entraînés correspondants.
autoencoder_1 = Model(input_img, decoder(conv5,conv4,conv3,conv2,conv1))
autoencoder_1.load_weights("../Model/CROSSVAL1/CROSSVAL1_AE1.h5")Propulsé par
autoencoder_2 = Model(input_img, decoder(conv5,conv4,conv3,conv2,conv1))
autoencoder_2.load_weights("../Model/CROSSVAL1/CROSSVAL1_AE2.h5")Propulsé par
autoencoder_3 = Model(input_img, decoder(conv5,conv4,conv3,conv2,conv1))
autoencoder_3.load_weights("../Model/CROSSVAL1/CROSSVAL1_AE3.h5")Propulsé par
Prédiction sur volumes de test : résultats quantitatifs et qualitatifs
Initialisons rapidement deux tableaux numpy de taille 11 x 3 x 1 : la première dimension correspond au nombre de volumes testés, la seconde aux métriques MSE et PSNR pour : (1) sortie prédite 7T vs. ground truth 7T, (2) input 3T vs. ground truth 7T, (3) input 3T vs. sortie prédite ; la troisième dimension correspond au nombre de canaux en entrée du modèle.
mse= np.zeros([11,3,1])
psnr= np.zeros([11,3,1])
i=0#for iterating over the slices of all the 11 volumesPropulsé par
Dans la suite, vous itérerez sur les 11 volumes. À chaque itération, vous prédirez chaque volume avec les trois autoencodeurs puis ferez la moyenne des prédictions.
Pour chaque volume, vous parcourrez les canaux et calculerez PSNR et MSE pour les trois cas vus plus haut.
Avec nibabel, vous enregistrerez la sortie prédite, l’entrée (3T) et le ground truth (7T) au format .nii : chacun des 11 volumes comportant 207 coupes.
Enfin, vous enregistrerez la matrice PSNR dans un fichier texte via numpy.
Comme indiqué dans l’article, la moyenne des sorties prédites réduit le bruit tout en préservant les caractéristiques locales dans les images reconstruites, ce qui améliore le PSNR par rapport aux sorties individuelles des décodeurs.
Dans la partie Training Script, vous allez travailler sur un problème de super-résolution d’image en utilisant une architecture de deep learning inédite. L’objectif est d’apprendre une mise en correspondance non linéaire d’une IRM cérébrale à bas champ 3 Tesla vers une IRM à haut champ 7 Tesla. Dans cette partie Testing Script, vous utiliserez les poids entraînés en Partie 1 pour faire des prédictions sur des données inédites. Vous verrez aussi comment enregistrer des images 2D sous forme d’un volume combiné.
Le tutoriel est divisé en deux parties : la première couvre l’entraînement, la seconde le test.
Remarque : Si vous souhaitez lire l’article, vous le trouverez ici.
Training Script
En bref, vous aborderez les points suivants dans la Partie 1 du tutoriel :
Dépendances des modules Python
Avant de suivre ce tutoriel, assurez-vous d’utiliser exactement les versions ci-dessous :
Remarque : Le modèle est entraîné sur une machine équipée d’un GPU Nvidia 1080 Ti, processeur Xeon e5 GeForce et 32 Go de RAM. Si vous utilisez Jupyter Notebook, ajoutez trois lignes pour définir l’ordre des périphériques CUDA et les GPU visibles via le module os.
Dans le code ci-dessous, vous définissez des variables d’environnement via os.environ dans le notebook. Il est recommandé de le faire avant d’initialiser Keras pour limiter TensorFlow (backend Keras) au premier GPU. Si la machine possède un GPU à l’index 0, utilisez 0 au lieu de 1. Vous pouvez le vérifier en lançant la commande nvidia-smi dans votre terminal.
Importation des modules
Commencez par importer les modules requis comme tensorflow, numpy et surtout keras, ainsi que les fonctions/couches nécessaires (
Input,Conv2D,MaxPooling2D, etc.), puisque vous les utiliserez pour l’entraînement du modèle.Pour lire des images au format NIfTI, importez également le module
nibabel.Comprendre le jeu de données IRM cérébrales 3T et 7T
Le jeu de données 3T et 7T se compose de volumes 3D ; chaque volume contient 207 coupes/images d’IRM cérébrales à différentes profondeurs. Chaque coupe mesure 173 x 173. Les images sont en niveaux de gris (monocanal). On compte 39 sujets au total, chaque sujet correspondant à une acquisition IRM d’un patient. Le format d’image n’est pas jpeg ou png, mais NIfTI. Vous verrez plus loin comment lire ce format.
Le jeu se compose d’images IRM en modalité T1, traditionnellement adaptées à l’évaluation des structures anatomiques. Dans ce tutoriel, vous travaillerez sur des IRM cérébrales 3T et 7T.
Le jeu de données est public et téléchargeable à cette adresse.
28 sujets sont utilisés pour l’entraînement et les 11 restants pour le test.
Définir les initialiseurs
Commençons par définir les dimensions des données. Vous redimensionnerez les images de 173x173 à 176x176 lors de la lecture. Ici, vous définissez aussi le répertoire des données, la taille de lot pour l’entraînement, le nombre de canaux, une couche
Input(), des listes pour les matrices d’entraînement et de test, et enfin, pour la remise à l’échelle, vous chargez un fichier texte contenant les valeurs minimale et maximale du jeu IRM.Remarque : Pour la remise à l’échelle, vous pouvez également prendre le min et le max de votre propre jeu de données si vous n’avez pas le fichier
maxANDmin.txt.Chargement des données
Chargez ensuite les données IRM via la bibliothèque
nibabelet redimensionnez les images de 173 x 173 à 176 x 176 en complétant par des zéros selon x et y.Notez qu’au chargement d’un volume NIfTI, Nibabel ne lit pas immédiatement le tableau d’images ; il attend qu’on le lui demande, via la méthode get_data().
Comme vous souhaitez des coupes 2D (et non 3D), vous utiliserez les listes
trainettestinitialisées plus haut ; à chaque volume lu, itérez sur les 207 coupes du volume 3D et ajoutez-les une par une à la liste.Chargeons d’abord les données 3T.
Puis chargez les données 7T. Vous pouvez réutiliser la variable
folderpuisque le nombre de volumes 3T et 7T est identique.Prétraitement des données
Les matrices train et test sont des listes ; convertissez-les en tableaux numpy.
Ensuite, convertissez leur
typeenfloat32et remettez à l’échelle à l’aide de la stratégie min–max, pour l’inputet leground truth.Vérifions rapidement la forme de train_matrix (3T) et test_matrix (7T). Elles doivent contenir 28 x 207 = 5 796 images au total, chacune en 176 x 176.
Créez ensuite deux nouvelles variables :
augmented_images(3T/input) etHaugmented_images(7T/ground truth) ayant la forme des matrices train et test. Ce seront des matrices 4D : première dimension = nombre total d’images, deuxième et troisième = dimensions de chaque image, dernière = nombre de canaux (ici 1).Passez ensuite sur toutes les images : à chaque itération, faites un reshape en 176 x 176 et affectez aux tenseurs
augmented_images(3T/input) etHaugmented_images(7T/ground truth).Il est ensuite crucial de partitionner les données. Pour une bonne généralisation, découpez en train/validation : 80 % pour l’entraînement et 20 % pour la validation.
Cela réduit aussi le risque de surapprentissage en validant sur des données non vues pendant l’entraînement.
Utilisez train_test_split de scikit-learn défini au début pour un découpage correct :
Le modèle : 1 encodeur – 3 décodeurs
Connexions de fusion
Définissez ensuite l’architecture proposée avec des blocs de couches de filtres successives suivies d’un max pooling dans la partie encodeur, comme ci-dessous. Pour reconstruire la taille d’origine en sortie, une couche d’upsampling est ajoutée dans chaque bloc de décodeur. Lors de l’upsampling, des artefacts peuvent apparaître par manque de détails dans l’entrée sous-échantillonnée du décodeur. Vous concaténez donc l’entrée du décodeur avec sa version mise à l’échelle issue de l’encodeur, pour fournir des détails pertinents et améliorer la reconstruction. L’ajout de connexions de fusion améliore sensiblement le PSNR (de l’ordre de 5 dB). Ce choix architectural s’inspire de cet article.
Multi-décodeurs
L’approche utilise un encodeur unique et plusieurs décodeurs avec une entrée monocanal. Trois couches convolutionnelles sont utilisées dans chaque bloc de l’encodeur et des trois décodeurs, suivies d’une couche de normalisation de lot pour la stabilité numérique.
Dans l’encodeur, le premier bloc convolutionnel comporte 32 filtres, puis le nombre de filtres est doublé à chaque bloc. Dans les décodeurs, le premier bloc a 256 filtres, puis ce nombre est divisé par deux à chaque bloc. La taille des filtres est 3 x 3 partout.
La fonction d’activation ReLU est utilisée dans toutes les couches sauf la dernière. Les données étant normalisées entre 0 et 1, une activation Sigmoid est utilisée en sortie finale.
On sait que les détails locaux à différentes échelles jouent un rôle clé en reconstruction d’image. L’architecture considère des images à plusieurs échelles via des couches hiérarchiques de sous-échantillonnage (max pooling) et suréchantillonnage (facteur 2) dans l’encodeur et les décodeurs. La représentation encodée après trois sous-échantillonnages projette l’entrée de haute dimension vers un espace latent.
Encodeur
Décodeur 1
Décodeur 2
Décodeur 3
Dans les 4 cellules ci-dessus, vous avez défini quatre fonctions : une pour l’encodeur et trois pour les décodeurs. Vous pourriez définir une fonction
decoder()unique et l’appeler trois fois, mais pour la clarté et afin d’éviter des effets de hasard entre décodeurs, elles sont séparées.Fonction de perte
Vous utiliserez une erreur quadratique moyenne en excluant les valeurs (pixels) égales à zéro dans le
ground truthy_tet la prédictiony_p.Définition et compilation du modèle
Commencez par appeler la fonction
encoderen lui passant l’entrée. Comme l’architecture utilise des connexions de fusion, la fonctionencoderrenvoie les sorties de cinq couchesconvolutionqui seront ensuitefusionnéesséparément avec chaque décodeur.Entraîner le modèle
Vous enregistrerez les poids uniquement lorsque le
peak signal-to-noise ratiosur la validation s’améliore. Pour cela, définissez une listepsnr_gray_channelavec une valeur initiale 1 (valeur factice largement dépassée dès les premières itérations).Initialisez le taux d’apprentissage à 1e-3 et appliquez une décroissance : réduction de 10 % tous les 20 epochs.
Remarque : Le code qui suit doit idéalement être exécuté en un seul bloc, mais il est découpé ici pour faciliter la compréhension.
Le modèle est entraîné pendant 500 epochs. Le learning rate initial est 1e-3 comme défini. Vous enregistrerez aussi
PSNRetMSEaprès chaque epoch entre 7T et la prédiction. Vous utiliserezK.set_valuepour diminuer le taux d’apprentissage de 10 % toutes les 20 époques pour les trois modèles.Brassez ensuite les paires 3T/7T pour éviter le surapprentissage lié à l’ordre des échantillons. Calculez le nombre de lots selon la
batch_sizedéfinie, puis itérez surnum_batches.En plus des valeurs de PSNR, stockez aussi les pertes des trois autoencodeurs et de leurs décodeurs respectifs.
À chaque batch, vous souhaitez faire voir les 32 échantillons suivants au modèle : le code suivant s’en charge.
Pour la stratégie d’autoencodeur minimum, après chaque epoch vous testez sur les données d’entraînement via
test_on_batchde Keras, qui renvoie trois pertes. Affichez-les.Six cas de figure sont possibles dans votre réseau :
loss_1 : perte de l’autoencodeur 1
loss_2 : perte de l’autoencodeur 2
loss_3 : perte de l’autoencodeur 3
Étape essentielle : comme vous avez mis certaines couches à False ci-dessus, il faut les remettre à True afin que tous les autoencodeurs soient utilisés par
test_on_batchet qu’ils ne restent pas gelés.Vous enregistrerez les poids de trois manières, dont deux ci-dessous : d’une part après chaque 100 epochs, d’autre part après chaque epoch (écrasement).
Test sur les données de validation
Brassez d’abord les données de validation, puis appliquez les mêmes 6 conditions que précédemment. Quelle que soit la condition vraie,
testezvotre modèle avec l’autoencodeur correspondant.Calculez ensuite
MSEetPSNRentre decoded_imgs (prédictions) et ground truth, puis enregistrez-les dans des fichiers texte.Ici, vous n’enregistrez les poids que si le
PSNRentre la prédiction 7T et le ground truth (7T) dépasse la meilleure valeur précédente stockée danspsnr_gray_channel.Enregistrer input, ground truth et prédictions : résultats quantitatifs
Définissez une matrice numpy
tempde taille 176 x 528 pour sauvegarder 3 images côte à côte (176 x 176 chacune). Enregistrez l’une des images validation 3T, 7T et prédite, et multipliez par 255 car les images ont été normalisées entre 0 et 1.Enfin, utilisez
scipypour enregistrer l’image.Fermez les fichiers
PSNRetMSEà la fin.Enfin, réduisez le taux d’apprentissage de 10 % de sa valeur courante toutes les 20 époques.
Testing Script
Dans la partie 2 de ce tutoriel, vous verrez :
nibabel.Dépendances des modules Python
Avant de commencer, assurez-vous d’utiliser les mêmes versions que ci-dessous :
Remarque : Le modèle est entraîné sur une machine avec GPU Nvidia 1080 Ti, processeur Xeon e5 GeForce et 32 Go de RAM. Sous Jupyter Notebook, ajoutez trois lignes de configuration CUDA via le module os.
Dans le code ci-dessous, vous définissez des variables d’environnement via os.environ. Faites-le avant d’initialiser Keras pour contraindre TensorFlow à utiliser le premier GPU. Si votre machine a un GPU en position 0, utilisez 0 au lieu de 1. Vérifiez via nvidia-smi.
Importation des modules
Importez d’abord tensorflow, numpy et surtout keras ainsi que les couches nécessaires (
Input,Conv2D,MaxPooling2D, etc.), car vous les utiliserez pour l’entraînement du modèle.Pour lire des images au format NIfTI, importez aussi
nibabel.Comprendre le jeu de données IRM cérébrales 3T et 7T
Le jeu 3T/7T comprend des volumes 3D ; chaque volume contient 207 coupes d’IRM cérébrales. Chaque coupe mesure 173 x 173. Les images sont monocanal en niveaux de gris. On compte 39 sujets au total. Le format est NIfTI (et non jpeg/png). Vous verrez plus loin comment lire ce format.
Il s’agit d’images T1, traditionnellement adaptées à l’étude de l’anatomie. Aujourd’hui, vous travaillerez sur des IRM cérébrales 3T et 7T.
Le jeu est public et disponible au téléchargement à cette source.
Définir les initialiseurs
Définissons d’abord les dimensions. Vous redimensionnerez de 173x173 à 176x176 lors de la lecture. Nous définissons aussi le répertoire des données, la taille des lots, le nombre de canaux, une couche
Input(), des listes train/test et, pour la remise à l’échelle, le fichier texte contenant min et max du jeu IRM.Charger les volumes de test
Chargez les données IRM avec
nibabelet redimensionnez de 173 x 173 à 176 x 176 par padding en x et y.Rappel : au chargement d’un volume NIfTI, Nibabel ne lit pas directement le tableau d’images ; utilisez get_data().
Comme vous voulez des coupes 2D, utilisez les listes
trainettestinitialisées ; pour chaque volume lu, itérez sur les 207 coupes et ajoutez-les une à une à la liste.Prétraitement des données
Les matrices de test 3T et 7T étant des listes, convertissez-les en tableaux numpy.
Convertissez ensuite le
typeenfloat32et remettez à l’échelleinputetground truth.Créez ensuite deux variables
ToPredict_images(test 3T/input) etground_images(test 7T/ground truth) ayant la forme des matrices train/test : matrices 4D (nombre d’images, hauteur, largeur, canaux = 1).Puis itérez sur toutes les images : à chaque fois, faites un reshape en 176 x 176 et affectez à
ToPredict_images(test 3T/input) etground_images(test 7T/ground truth).Le modèle
Encodeur
Décodeur
Fonction de perte
Vous utiliserez une erreur quadratique moyenne en excluant les pixels nuls dans
y_t(ground truth) ety_p(prédiction).Définition du modèle et chargement des poids dans les trois autoencodeurs
Appelez d’abord la fonction
encoderen lui passant l’entrée. Comme l’architecture utilise des connexions de fusion, l’encodeur retourne la sortie de cinq couchesconvolutionqui serontfusionnéesavec les sorties des trois décodeurs.Créez maintenant trois modèles et chargez les poids entraînés correspondants.
Prédiction sur volumes de test : résultats quantitatifs et qualitatifs
Initialisons rapidement deux tableaux numpy de taille 11 x 3 x 1 : la première dimension correspond au nombre de volumes testés, la seconde aux métriques
MSEetPSNRpour : (1) sortie prédite 7T vs. ground truth 7T, (2) input 3T vs. ground truth 7T, (3) input 3T vs. sortie prédite ; la troisième dimension correspond au nombre de canaux en entrée du modèle.Dans la suite, vous itérerez sur les 11 volumes. À chaque itération, vous prédirez chaque volume avec les trois autoencodeurs puis ferez la moyenne des prédictions.
Pour chaque volume, vous parcourrez les canaux et calculerez
PSNRetMSEpour les trois cas vus plus haut.Avec
nibabel, vous enregistrerez la sortie prédite, l’entrée (3T) et le ground truth (7T) au format.nii: chacun des 11 volumes comportant 207 coupes.Enfin, vous enregistrerez la matrice
PSNRdans un fichier texte via numpy.Comme indiqué dans l’article, la moyenne des sorties prédites réduit le bruit tout en préservant les caractéristiques locales dans les images reconstruites, ce qui améliore le PSNR par rapport aux sorties individuelles des décodeurs.
Pour aller plus loin en Python, suivez le cours de DataCamp : Introduction à la visualisation de données avec Matplotlib.
Découvrez aussi le Tutoriel Keras : Deep Learning en Python.