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.
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> 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. 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. 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. À 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. 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 Tout d'abord, importez la bibliothèque que vous souhaitez utiliser. Ensuite, décrivez les paramètres et ainsi de suite. Ce qui suit est appliqué comme processus de rappel pendant l'apprentissage. 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. 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 Créez un CNN simple à 10 couches. Apprenez le réseau. Enfin, enregistrez le graphique de la courbe d'entraînement sous forme d'image. Les résultats d'apprentissage sont les suivants. 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. Décrivez les paramètres. Cette fois, lisez le fichier csv du test. Construisez le réseau appris et chargez les poids que vous avez appris précédemment. 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. Calculez le score F1 à l'aide de scikit-learn. Voici les résultats de l'évaluation. Effectivement, AMD et MH, qui ont une petite quantité de données, ont des scores faibles. 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.4. Répartition des données
Code pour extraire des images non multi-étiquettes et les combiner dans un fichier csv summary>
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)
filename
id
label
000000_00.jpg
0
Normal
000000_01.jpg
0
Normal
─
─
─
000001_02.jpg
1
Gla
─
─
─
000011_01.jpg
11
DR_DM
étiquette
Nombre de feuilles
Normal
4364
Gla
2293
AMD
375
RP
247
DR_DM
3113
RD
883
RVO
537
MH
161
5. Construction et apprentissage de modèles
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
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}
# 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')
#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)
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
#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])
#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)
#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)
6. Évaluation
import numpy as np
from PIL import Image
from sklearn.metrics import classification_report
from tqdm import tqdm
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
#Construction de réseau&Lire les poids appris
model = cnn(image_size, classes)
model.load_weights('model_weights.h5')
#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]))
#Évaluation
print(classification_report(y_true, y_pred, target_names=label_list))
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é