[PYTHON] Classification d'images avec un jeu de données d'images de fond d'oeil grand angle

1.Tout d'abord

Pour les débutants, cet article vise à utiliser TensorFlow 2.0 pour classer les images avec Deep Learning pour le moment. Puisque l'ensemble de données d'image n'est pas intéressant avec MNIST, j'utiliserai l'ensemble de données d'image de fond d'oeil grand angle [^ 1] publié par l'hôpital de Tsukazaki. En outre, le réseau est un simple CNN à 10 niveaux.

Tout le code

2. Environnement

3. Ensemble de données d'images de fond d'oeil

Un ensemble de données de fond de l'oeil grand angle de 13047 feuilles (5389 personnes, 8588 yeux) publié par l'hôpital Tsukazaki. Vous pouvez télécharger le fichier csv avec l'image et l'étiquette de la maladie qui lui sont associées à partir du lien ci-dessous. Tsukazaki Optos Public Project https://tsukazaki-ai.github.io/optos_dataset/

La répartition de l'étiquette de la maladie est la suivante.

étiquette maladie Nombre de feuilles
AMD Dégénérescence des taches jaunes liée à l'âge 413
RVO Occlusion de la veine rétinienne 778
Gla Glaucome 2619
MH Trou de tache jaune 222
DR La rétinopathie diabétique 3323
RD Décollement de la rétine 974
RP Dégénérescence pigmentaire rétinienne 258
AO Occlusion artérielle 21
DM Diabète sucré 3895

Le nombre total de feuilles du tableau est-il différent du nombre d'images? Je suis sûr que certains d'entre vous ont peut-être pensé, alors jetons un coup d'œil au fichier csv réel.

filename age sex LR AMD RVO Gla MH DR RD RP AO DM
000000_00.jpg 78 M L 0 0 0 0 0 0 0 0 0
000000_01.jpg 78 M R 0 0 0 0 0 0 0 0 0
000001_02.jpg 69 M L 0 0 1 0 0 0 0 0 0
000011_01.jpg 70 F L 0 0 0 0 1 0 0 0 1

De cette façon, il s'agit d'un problème multi-étiquettes avec plusieurs étiquettes (complications) pour une image. Il y a un total de 4364 images non malades qui ne sont pas étiquetées. De plus, un exemple d'image est présenté ci-dessous.

<détails>

Contient des images grotesques </ summary>

000000_00.jpg 000000_01.jpg 000001_02.jpg 000011_01.jpg

Il y a un déséquilibre dans le nombre de données, et c'est assez ennuyeux avec le multi-étiquette ~ ~ C'est un ensemble de données pratique, mais dans cet article, il est facile d'utiliser uniquement des images non multi-étiquettes et uniquement celles avec un grand nombre de classes Classer.

4. Répartition des données

Tout d'abord, extrayez uniquement les images non multi-étiquetées du fichier csv. Cependant, comme il y a également DM dans l'image DR, l'image dans laquelle DR et DM se produisent en même temps est également extraite. Cependant, nous avons décidé de ne pas utiliser DR et AO, qui n'ont respectivement que 3 et 11 images. De plus, comme il y avait 3113 DR + DM et 530 DM avec des étiquettes partiellement superposées, nous avons décidé de ne pas utiliser le DM avec le plus petit nombre cette fois. De plus, j'ai changé le format du fichier csv afin qu'il puisse être traité plus tard.

Code pour extraire des images non multi-étiquettes et les combiner dans un fichier csv
from collections import defaultdict
import pandas as pd


#Lire le fichier csv de l'ensemble de données du fond d'œil grand angle
df = pd.read_csv('data.csv')

dataset = defaultdict(list)

for i in range(len(df)):
    #Convertir l'étiquette jointe en caractères
    labels = ''
    if df.iloc[i]['AMD'] == 1:
        labels += '_AMD'
    if df.iloc[i]['RVO'] == 1:
        labels += '_RVO'
    if df.iloc[i]['Gla'] == 1:
        labels += '_Gla'
    if df.iloc[i]['MH'] == 1:
        labels += '_MH'
    if df.iloc[i]['DR'] == 1:
        labels += '_DR'
    if df.iloc[i]['RD'] == 1:
        labels += '_RD'
    if df.iloc[i]['RP'] == 1:
        labels += '_RP'
    if df.iloc[i]['AO'] == 1:
        labels += '_AO'
    if df.iloc[i]['DM'] == 1:
        labels += '_DM'
    if labels == '':
        labels = 'Normal'
    else:
        labels = labels[1:]

    #Pas multi-étiquettes(DR+Hors DM)Image et
    #Quelques DR, DM et
    #Dupliquer les étiquettes mais DR+Extraire moins d'images non-DM que DM
    if '_' not in labels or labels == 'DR_DM':
        if labels not in ('DR', 'AO', 'DM'):
            dataset['filename'].append(df.iloc[i]['filename'])
            dataset['id'].append(df.iloc[i]['filename'].split('_')[0].split('.')[0])
            dataset['label'].append(labels)

#Enregistrer en tant que fichier csv
dataset = pd.DataFrame(dataset)
dataset.to_csv('dataset.csv', index=False)

J'ai créé le fichier csv suivant avec le code ci-dessus. Puisque les images sont nommées selon la règle de {numéro de série ID} _ {numéro de série} .jpg, l'ID du numéro de série est utilisé comme identifiant.

filename id label
000000_00.jpg 0 Normal
000000_01.jpg 0 Normal
000001_02.jpg 1 Gla
000011_01.jpg 11 DR_DM

À la suite de l'extraction, la répartition de la classe de classification et du nombre d'images est la suivante. Normal est une image non-maladie.

étiquette Nombre de feuilles
Normal 4364
Gla 2293
AMD 375
RP 247
DR_DM 3113
RD 883
RVO 537
MH 161

Ensuite, divisez les données d'image. Étant donné que l'ensemble de données comprend 1 047 feuilles (5 389 personnes, 8 588 yeux), les images de la même personne et du même œil sont incluses. Les images de la même personne ou des mêmes yeux contiennent des caractéristiques et des étiquettes similaires, ce qui peut provoquer des fuites de données. Par conséquent, la division est effectuée de sorte que la même personne n'existe pas parmi les données d'apprentissage et les données de test. De plus, assurez-vous que le ratio de chaque classe de répartition des données d'entraînement et des données de test est approximativement le même. Cette fois, les données d'entraînement étaient de 60%, les données de vérification étaient de 20% et les données de test étaient de 20%.

Code de division K de stratification de groupe

5. Construction et apprentissage de modèles

Tout d'abord, importez la bibliothèque que vous souhaitez utiliser.

import matplotlib.pyplot as plt
import pandas as pd
from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.models import Model
from tensorflow.keras.layers import GlobalAveragePooling2D, Input, MaxPool2D
from tensorflow.keras.layers import Conv2D, Dense, BatchNormalization, Activation
from tensorflow.keras.optimizers import Adam

Ensuite, décrivez les paramètres et ainsi de suite. label_list est arrangé dans l'ordre abc pour la commodité de la bibliothèque.

directory = 'img' #Dossier dans lequel les images sont stockées
df_train = pd.read_csv('train.csv') #DataFrame avec informations sur les données d'entraînement
df_validation = pd.read_csv('val.csv') #DataFrame avec informations de données de validation
label_list = ['AMD', 'DR_DM', 'Gla', 'MH', 'Normal', 'RD', 'RP', 'RVO'] #Nom de l'étiquette
image_size = (224, 224) #Taille de l'image d'entrée
classes = len(label_list) #Nombre de classes de classification
batch_size = 32 #Taille du lot
epochs = 300 #Nombre d'époques
loss = 'categorical_crossentropy' #Fonction de perte
optimizer = Adam(lr=0.001, amsgrad=True) #Fonction d'optimisation
metrics = 'accuracy' #Méthode d'évaluation
#ImageDataGenerator Paramètres d'amplification d'image
aug_params = {'rotation_range': 5,
              'width_shift_range': 0.05,
              'height_shift_range': 0.05,
              'shear_range': 0.1,
              'zoom_range': 0.05,
              'horizontal_flip': True,
              'vertical_flip': True}

Ce qui suit est appliqué comme processus de rappel pendant l'apprentissage.

# val_Enregistrer le modèle uniquement lorsque la perte est minimisée
mc_cb = ModelCheckpoint('model_weights.h5',
                        monitor='val_loss', verbose=1,
                        save_best_only=True, mode='min')
#Lorsque l'apprentissage stagne, le taux d'apprentissage est de 0.Double
rl_cb = ReduceLROnPlateau(monitor='loss', factor=0.2, patience=3,
                          verbose=1, mode='auto',
                          min_delta=0.0001, cooldown=0, min_lr=0)
#Si l'apprentissage ne progresse pas, l'apprentissage sera interrompu de force
es_cb = EarlyStopping(monitor='loss', min_delta=0,
                      patience=5, verbose=1, mode='auto')

Comme le nombre de données dans chaque classe est déséquilibré, si vous faites une erreur dans une classe avec un petit nombre de données, assurez-vous que la perte est importante.

#Ajustez les poids des pertes pour correspondre au nombre de données
weight_balanced = {}
for i, label in enumerate(label_list):
    weight_balanced[i] = (df_train['label'] == label).sum()
max_count = max(weight_balanced.values())
for label in weight_balanced:
    weight_balanced[label] = max_count / weight_balanced[label]
print(weight_balanced)

Génère un générateur de données de formation et de validation. Utilisez ImageDataGenerator pour l'expansion des données et chargez des images à partir de DataFrame avec flow_from_dataframe. La raison pour laquelle label_list est dans l'ordre abc est que lorsqu'une image est lue par flow_from_dataframe, les classes sont attribuées dans l'ordre abc de la chaîne de caractères, de sorte que la correspondance entre le numéro de classe et le nom d'étiquette peut être comprise. Vous pouvez vérifier la correspondance plus tard, mais c'est ennuyeux, alors ...

#Générer un générateur
##Générateur de données d'entraînement
datagen = ImageDataGenerator(rescale=1./255, **aug_params)
train_generator = datagen.flow_from_dataframe(
    dataframe=df_train, directory=directory,
    x_col='filename', y_col='label',
    target_size=image_size, class_mode='categorical',
    classes=label_list,
    batch_size=batch_size)
step_size_train = train_generator.n // train_generator.batch_size
##Générateur de données de validation
datagen = ImageDataGenerator(rescale=1./255)
validation_generator = datagen.flow_from_dataframe(
    dataframe=df_validation, directory=directory,
    x_col='filename', y_col='label',
    target_size=image_size, class_mode='categorical',
    classes=label_list,
    batch_size=batch_size)
step_size_validation = validation_generator.n // validation_generator.batch_size

Créez un CNN simple à 10 couches.

#Construire un CNN à 10 couches
def cnn(input_shape, classes):
    #Couche d'entrée
    inputs = Input(shape=(input_shape[0], input_shape[1], 3))

    #1ère couche
    x = Conv2D(32, (3, 3), padding='same', kernel_initializer='he_normal')(inputs)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = MaxPool2D(pool_size=(2, 2))(x)

    #2ème couche
    x = Conv2D(64, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = MaxPool2D(pool_size=(2, 2))(x)

    #3e couche
    x = Conv2D(128, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = MaxPool2D(pool_size=(2, 2))(x)

    #4ème couche
    x = Conv2D(256, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = MaxPool2D(pool_size=(2, 2))(x)

    #5ème et 6ème couches
    x = Conv2D(512, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = Conv2D(512, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = MaxPool2D(pool_size=(2, 2))(x)

    #7ème et 8ème couches
    x = Conv2D(1024, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = Conv2D(1024, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = GlobalAveragePooling2D()(x)

    #9e et 10e couches
    x = Dense(256, kernel_initializer='he_normal')(x)
    x = Dense(classes, kernel_initializer='he_normal')(x)
    outputs = Activation('softmax')(x)


    return Model(inputs=inputs, outputs=outputs)

#Construction de réseau
model = cnn(image_size, classes)
model.summary()
model.compile(loss=loss, optimizer=optimizer, metrics=[metrics])

Apprenez le réseau.

#Apprentissage
history = model.fit_generator(
    train_generator, steps_per_epoch=step_size_train,
    epochs=epochs, verbose=1, callbacks=[mc_cb, rl_cb, es_cb],
    validation_data=validation_generator,
    validation_steps=step_size_validation,
    class_weight=weight_balanced,
    workers=3)

Enfin, enregistrez le graphique de la courbe d'entraînement sous forme d'image.

#Dessinez et enregistrez un graphique de la courbe d'apprentissage
def plot_history(history):
    fig, (axL, axR) = plt.subplots(ncols=2, figsize=(10, 4))

    # [la gauche]Graphique sur les métriques
    L_title = 'Accuracy_vs_Epoch'
    axL.plot(history.history['accuracy'])
    axL.plot(history.history['val_accuracy'])
    axL.grid(True)
    axL.set_title(L_title)
    axL.set_ylabel('accuracy')
    axL.set_xlabel('epoch')
    axL.legend(['train', 'test'], loc='upper left')

    # [Côté droit]Graphique sur la perte
    R_title = "Loss_vs_Epoch"
    axR.plot(history.history['loss'])
    axR.plot(history.history['val_loss'])
    axR.grid(True)
    axR.set_title(R_title)
    axR.set_ylabel('loss')
    axR.set_xlabel('epoch')
    axR.legend(['train', 'test'], loc='upper left')

    #Enregistrer le graphique en tant qu'image
    fig.savefig('history.jpg')
    plt.close()

#Sauvegarder la courbe d'apprentissage
plot_history(history)

Les résultats d'apprentissage sont les suivants.

history.jpg

6. Évaluation

Puisque l'évaluation est des données déséquilibrées, elle est évaluée par le score F1. Commencez par déduire les données de test à l'aide du modèle que vous avez appris précédemment.

Importation supplémentaire.

import numpy as np
from PIL import Image
from sklearn.metrics import classification_report
from tqdm import tqdm

Décrivez les paramètres. Cette fois, lisez le fichier csv du test.

directory = 'img' #Dossier dans lequel les images sont stockées
df_test = pd.read_csv('test.csv') #DataFrame avec informations sur les données de test
label_list = ['AMD', 'DR_DM', 'Gla', 'MH', 'Normal', 'RD', 'RP', 'RVO'] #Nom de l'étiquette
image_size = (224, 224) #Taille de l'image d'entrée
classes = len(label_list) #Nombre de classes de classification

Construisez le réseau appris et chargez les poids que vous avez appris précédemment.

#Construction de réseau&Lire les poids appris
model = cnn(image_size, classes)
model.load_weights('model_weights.h5')

L'inférence est effectuée en lisant et en convertissant l'image de sorte que les conditions soient les mêmes que pendant l'apprentissage.

#inférence
X = df_test['filename'].values
y_true = list(map(lambda x: label_list.index(x), df_test['label'].values))
y_pred = []
for file in tqdm(X, desc='pred'):
    #Redimensionner l'image pour qu'elle ait les mêmes conditions que lors de l'apprentissage&conversion
    img = Image.open(f'{directory}/{file}')
    img = img.resize(image_size, Image.LANCZOS)
    img = np.array(img, dtype=np.float32)
    img *= 1./255
    img = np.expand_dims(img, axis=0)

    y_pred.append(np.argmax(model.predict(img)[0]))

Calculez le score F1 à l'aide de scikit-learn.

#Évaluation
print(classification_report(y_true, y_pred, target_names=label_list))

Voici les résultats de l'évaluation. Effectivement, AMD et MH, qui ont une petite quantité de données, ont des scores faibles.

              precision    recall  f1-score   support

         AMD       0.17      0.67      0.27        75
       DR_DM       0.72      0.75      0.73       620
         Gla       0.76      0.69      0.72       459
          MH       0.09      0.34      0.14        32
      Normal       0.81      0.50      0.62       871
          RD       0.87      0.79      0.83       176
          RP       0.81      0.86      0.83        50
         RVO       0.45      0.65      0.53       107

    accuracy                           0.64      2390
   macro avg       0.58      0.66      0.59      2390
weighted avg       0.73      0.64      0.67      2390

7. Résumé

Dans cet article, nous avons utilisé un simple CNN à 10 couches pour classer les images de l'ensemble de données du fond d'œil grand angle publié par l'hôpital Tsukazaki. À l'avenir, sur la base de ce résultat, nous améliorerons les performances tout en intégrant les dernières méthodes telles que la structure du réseau et la méthode d'expansion des données.

Recommended Posts

Classification d'images avec un jeu de données d'images de fond d'oeil grand angle
Segmentation d'image avec CaDIS: un ensemble de données sur la cataracte
Détection d'objets de cuisson par classification d'images Yolo +
Classification d'image MNIST (numéro manuscrit) avec Perceptron multicouche
Traitement d'image avec MyHDL
Reconnaissance d'image avec keras
Traitement d'image avec Python
Challenge classification des images par TensorFlow2 + Keras 3 ~ Visualiser les données MNIST ~
Traitement d'image avec PIL
"Classer les déchets par image!" Journal de création d'application day2 ~ Mise au point avec VGG16 ~
[Apprentissage en profondeur] Classification d'images avec un réseau neuronal convolutif [DW jour 4]
Téléchargement d'image avec l'API Flickr
[PyTorch] Classification des images du CIFAR-10
J'ai essayé la classification d'image d'AutoGluon
Lire les coordonnées de l'image avec Python-matplotlib
Traitement d'image avec PIL (Pillow)
Édition d'image avec python OpenCV
Classification des documents avec une phrase
Téléchargement d'images et personnalisation avec django-ckeditor
Tri des fichiers image avec Python (3)
CNN (1) pour la classification des images (pour les débutants)
Créer une visionneuse d'images avec Tkinter
Tri des fichiers image avec Python
Traitement d'image avec Python (3)
Génération de légende d'image avec Chainer
Obtenez des fonctionnalités d'image avec OpenCV
Reconnaissance d'image avec Keras + OpenCV
[Python] Traitement d'image avec scicit-image
Défi la classification des images par TensorFlow2 + Keras 4 ~ Prédisons avec un modèle entraîné ~
Défiez la classification des images avec TensorFlow2 + Keras 9-Apprentissage, sauvegarde et chargement de modèles-