Explique comment utiliser TensorFlow 2.X avec l'implémentation de VGG16 / ResNet50

introduction

TensorFlow, une bibliothèque d'apprentissage en profondeur développée par Google, vous permet de créer des modèles et de mettre en œuvre des boucles d'entraînement de différentes manières. Bien que cela soit utile pour les experts, cela peut être un obstacle à la compréhension pour les débutants.

Cette fois, nous présenterons de manière exhaustive le style d'écriture recommandé par TensorFlow 2.X et expliquerons comment l'utiliser lors de la mise en œuvre de VGG16 et ResNet50, qui sont des modèles bien connus dans le domaine de la reconnaissance d'images.

Public cible

couler

Tout d'abord, examinons les quatre API de création de modèles de TensorFlow. Après cela, j'expliquerai deux méthodes de formation. Enfin, nous implémenterons VGG16 et ResNet50 en utilisant ces styles d'écriture.

Environnement de vérification

>>> import sys
>>> sys.version
'3.7.7 (default, Mar 10 2020, 15:43:33) \n[Clang 11.0.0 (clang-1100.0.33.17)]'
pip list | grep tensorflow
tensorflow               2.2.0
tensorflow-estimator     2.2.0

4 API de création de modèles dans TensorFlow

TensorFlow fournit deux API majeures pour la création de modèles et quatre API subdivisées.

Tout d'abord, je présenterai brièvement les principales classifications.

API symbolique (déclarative)

Il s'agit d'une méthode d'écriture qui déclare (≒ compile) la forme du modèle avant d'exécuter l'entraînement.

Les modèles écrits avec cette API ne peuvent pas changer de forme pendant la formation. Par conséquent, certains modèles à changement dynamique (tels que Tree-RNN) ne peuvent pas être implémentés. Au lieu de cela, vous pouvez vérifier la forme du modèle avant de donner les données au modèle.

API pédagogique (sous-classification de modèle)

Contrairement à l'API symbolique, c'est une manière impérative (≒ intuitive) d'écrire sans déclarer.

Il s'agissait du premier style d'écriture adopté par Chainer, une bibliothèque d'apprentissage en profondeur originaire du Japon (Preferred Networks), et PyTorch a également adopté ce style d'écriture. Vous pouvez implémenter le modèle comme si vous écriviez une classe en Python, ce qui facilite la personnalisation, comme les modifications de couche et les extensions. Au lieu de cela, le programme ne peut pas reconnaître à quoi ressemblera le modèle tant que les données ne seront pas fournies une fois.

Ensuite, je présenterai une méthode d'écriture concrète avec un exemple simple.

Sequential API Comme son nom l'indique, il s'agit d'une API qui implémente un modèle en ajoutant des couches à Sequential. Ceci est souvent utilisé dans les didacticiels Keras et TensorFlow, vous l'avez donc peut-être vu une fois.

Comme indiqué ci-dessous, après avoir instancié une classe vide tensorflow.keras.Sequential, ajoutez des couches avec la méthode ʻadd, et donnez des couches sous forme de liste aux arguments de la classe tensorflow.keras.Sequential`. Il est courant d'instancier.

import tensorflow as tf
from tensorflow.keras import layers

def sequential_vgg16_a(input_shape, output_size):
    model = tf.keras.Sequential()
    model.add(layers.Conv2D(64, 3, 1, padding="same", batch_input_shape=input_shape))
    model.add(layers.BatchNormalization())
    # ...(Omission)...
    model.add(layers.Dense(output_size, activation="softmax"))    
    return model

def sequential_vgg16_b(input_shape, output_size):
    model = tf.keras.Sequential([
        layers.Conv2D(64, 3, 1, padding="same", batch_input_shape=input_shape),
        layers.BatchNormalization(),
        # ...(Omission)...
        layers.Dense(output_size, activation="softmax")
    ]
    return model

Il ne prend en charge que les méthodes pour ajouter des couches, vous ne pouvez donc pas écrire de réseaux complexes avec plusieurs entrées, des fonctionnalités intermédiaires, plusieurs sorties ou des branches conditionnelles. Vous pouvez utiliser cette API pour implémenter un réseau simple (comme VGG) qui traverse simplement les couches en séquence.

Functional API Une API qui implémente des modèles complexes qui ne peuvent pas être décrits par l'API séquentielle.

Commencez par instancier tensorflow.keras.layers.Input et passez-le à la première couche. Après cela, le flux de données du modèle est défini en passant la sortie d'une couche à la couche suivante. Enfin, vous pouvez construire le modèle en donnant la sortie résultante et la première entrée comme arguments à tensorflow.keras.Model.

from tensorflow.keras import layers, Model

def functional_vgg16(input_shape, output_size, batch_norm=False):
    inputs = layers.Input(batch_input_shape=input_shape)

    x = layers.Conv2D(64, 3, 1, padding="same")(inputs)
    if batch_norm:
        x = layers.BatchNormalization()(x)
    x = layers.ReLU()(x)
    # ...(Omission)...
    outputs = layers.Dense(output_size, activation="softmax")(x)

    return Model(inputs=inputs, outputs=outputs)    

Dans l'exemple ci-dessus, la valeur de la variable «batch_norm» change la présence ou l'absence de la couche de normalisation de lots. Si vous avez besoin d'une définition flexible qui change la forme du modèle en fonction des conditions, vous avez besoin de l'API fonctionnelle au lieu de l'API séquentielle.

Notez qu'il existe une façon apparemment étrange d'écrire des parenthèses suivies de parenthèses, mais ce n'est pas spécifique à TensorFlow et est couramment utilisé en Python, et les deux suivants représentent la même chose.

#Comment écrire 1
x = layers.BatchNormalization()(x)

#Comment écrire 2
layer = layers.BatchNormalization()
x = layer(x)

Primitive API Cette API était principalement utilisée dans la série TensorFlow 1.X. ** La série 2.X est actuellement obsolète. ** **

L'API séquentielle et l'API fonctionnelle mentionnées ci-dessus pourraient définir le modèle en décrivant le flux de données passant par le modèle, mais l'API primitive décrit de manière déclarative l'ensemble du flux de traitement, y compris les autres traitements de calcul. Faire.

Il n'y a pas beaucoup de mérite d'apprendre cette méthode d'écriture à partir de maintenant, donc je vais omettre l'explication, mais si vous vous entraînez en utilisant tensorflow.Session, cette méthode d'écriture est applicable.

import tensorflow as tf
sess = tf.Session()

Subclassing API Une API qui est devenue disponible avec la mise à jour de TensorFlow 2.X. Il est écrit à peu près de la même manière que Chainer et PyTorch, et il est intuitif et facile à personnaliser car vous pouvez implémenter le modèle comme si vous écriviez une classe en Python.

Tout d'abord, créez une classe en héritant de tensorflow.keras.Model. Puis construisez le modèle en implémentant les méthodes __init__ et call.

La méthode __init__ de la classe appelle la méthode __init__ de la classe parente et enregistre la couche que vous voulez apprendre. ** Les poids de couche non répertoriés ici ne sont pas entraînés par défaut. ** **

La méthode call de la classe décrit la propagation vers l'avant des couches. (Semblable à __call__ de Chainer, PyTorch à l'avant.)

from tensorflow.keras import layers, Model


class VGG16(Model):
    def __init__(self, output_size=1000):
        super().__init__()
        self.layers_ = [
            layers.Conv2D(64, 3, 1, padding="same"),
            layers.BatchNormalization(),
            # ...(Omission)...
            layers.Dense(output_size, activation="softmax"),
        ]
    def call(self, inputs):
        for layer in self.layers_:
            inputs = layer(inputs)
        return inputs

Cela semble un peu redondant par rapport aux autres styles d'écriture, mais vous pouvez voir que vous pouvez implémenter le modèle comme vous le feriez normalement pour une classe.

La méthode super qui initialise la classe parente a également un modèle qui donne un argument, mais ceci est écrit en tenant compte de la 2e série Python, et dans la 3e série Python, le même traitement est effectué sans argument.

from tensorflow.keras import Model


#Comment écrire la série Python 3
class VGG16_PY3(Model):
    def __init__(self, output_size=1000):
        super().__init__()

#Comment écrire la série Python 2
class VGG16_PY2(Model):
    def __init__(self, output_size=1000):
        super().__init__(VGG16_PY2, self)

Examen de l'API de création de modèles

Ceci conclut l'explication de la façon de construire un modèle. En résumé, je pense que vous pouvez l'utiliser correctement comme suit.

--Si vous souhaitez écrire facilement un modèle qui ne traverse les couches qu'unilatéralement ** API séquentielle ** --Si vous souhaitez écrire un modèle compliqué afin de pouvoir vérifier correctement la forme avant d'exécuter la formation ** API fonctionnelle ** --Si vous voulez écrire dans le style Chainer ou PyTorch, ou si vous voulez écrire un modèle dynamique ** API de sous-classification **

Deux méthodes de formation dans TensorFlow

Il y a deux façons de s'entraîner:

formation intégrée

Il s'agit d'une méthode d'entraînement utilisant la fonction intégrée de tensorflow.keras.Model.

Beaucoup d'entre vous le savent peut-être car il est également utilisé dans les didacticiels Keras et TensorFlow. De plus, bien qu'il s'agisse d'une bibliothèque différente, scikit-learn utilise également cette méthode.

Tout d'abord, instanciez le modèle implémenté par l'API ci-dessus (tensorflow.keras.Model, ou un objet qui en hérite).

Cette instance a des méthodes compile et fit comme fonctions intégrées.

Exécutez cette méthode compile pour enregistrer la fonction de perte, la fonction d'optimisation et la métrique. Puis entraînez-vous en exécutant la méthode fit.

import tensorflow as tf

(train_images, train_labels), _ = tf.keras.datasets.cifar10.load_data()

#J'utilise un modèle formé pour l'illustration
model = tf.keras.applications.VGG16()

model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

model.fit(train_images, train_labels)

Vous pouvez maintenant effectuer la formation.

La spécification de la taille du lot, le nombre d'époques, l'enregistrement de la fonction de rappel, l'évaluation des données de validation, etc. peuvent être enregistrés comme arguments de mot-clé de la méthode fit, de sorte qu'une certaine personnalisation est possible.

Dans de nombreux cas, cela peut être suffisant, mais les cas qui ne rentrent pas dans ce cadre (par exemple, les cas où plusieurs modèles tels que GAN sont entraînés en même temps) doivent être décrits dans la formation personnalisée décrite plus loin.

Formation personnalisée

Il n'a pas d'API spéciale, c'est juste un moyen normal de s'entraîner avec une boucle Python for.

Tout d'abord, instanciez le modèle implémenté par l'API ci-dessus (tensorflow.keras.Model, ou un objet qui en hérite).

Ensuite, en plus de définir la fonction de perte et la fonction d'optimisation, regroupez l'ensemble de données. Après cela, l'époque et le lot sont tournés dans une boucle for.

Dans la boucle for, décrivez d'abord le processus de propagation avant dans la portée tf.GradientTape. Nous appelons ensuite la méthode gradient pour calculer le gradient et la méthode ʻapply_gradients` pour mettre à jour les poids en fonction de la fonction d'optimisation.

import tensorflow as tf

batch_size = 32
epochs = 10

(train_images, train_labels), _ = tf.keras.datasets.cifar10.load_data()
#J'utilise un modèle formé pour l'illustration

model = tf.keras.applications.VGG16()

buffer_size = len(train_images)
train_ds = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
train_ds = train_ds.shuffle(buffer_size=buffer_size).batch(batch_size)

criterion = tf.keras.losses.SparseCategoricalCrossentropy()
optimizer = tf.keras.optimizers.Adam()

for epoch in range(epochs):
    for x, y_true in train_ds:
        with tf.GradientTape() as tape:
            y_pred = model(x, training=True)
            loss = criterion(y_true=y_true, y_pred=y_pred)
        gradients = tape.gradient(loss, model.trainable_variables)
        optimizer.apply_gradients(zip(gradients, model.trainable_variables))

Vous pouvez maintenant effectuer la formation.

Dans l'exemple ci-dessus, les données de validation ne sont pas du tout évaluées et la sortie vers TensorBoard n'est pas du tout effectuée, mais comme la boucle for est normalement tournée, vous pouvez ajouter un traitement à votre guise.

D'un autre côté, puisque la quantité de description est forcément importante, il devient un peu difficile de garantir la qualité du code source. Chainer et PyTorch peuvent être écrits presque de la même manière (bien qu'il y ait des différences mineures).

Rétrospective de la méthode de formation

Ceci conclut l'explication de la méthode de formation. En résumé, je pense que vous pouvez l'utiliser correctement comme suit.

--Si vous n'avez pas besoin d'effectuer de traitement spécial pendant la formation, ** formation intégrée ** pour les techniques de formation générales --Si vous n'êtes pas accro au cadre intégré, si vous souhaitez ajouter divers processus pendant la formation et essayez et faites une erreur, si vous souhaitez écrire à la main ** Formation personnalisée **

Vue d'ensemble de VGG16 / ResNet 50

Je pense qu'il y a certaines parties qui ne peuvent être comprises à partir de l'explication seule, je vais donc approfondir ma compréhension par la mise en œuvre.

Commençons par une brève introduction aux deux modèles.

Qu'est-ce que VGG16

C'est un modèle haute performance avec une structure très simple qui comporte 13 couches de convolution 3x3 et 3 couches de couches entièrement connectées. Il est utilisé pour extraire des caractéristiques d'image dans diverses tâches de reconnaissance d'image. L'article original compte plus de 37 000 citations et est très connu.

Il peut être implémenté avec l'API séquentielle, l'API fonctionnelle et l'API de sous-classification.

Le papier original est ici. https://arxiv.org/abs/1409.1556

Qu'est-ce que ResNet50

Il s'agit d'un modèle multicouche avec un mécanisme résiduel (49 couches pour la convolution et 1 couche pour les couches entièrement connectées). À partir de 2020, cette variante ResNet est toujours l'une des meilleures en termes de précision de classification d'images, et c'est également un modèle haute performance. Semblable à VGG16, il est utilisé pour extraire des caractéristiques d'image dans diverses tâches de reconnaissance d'image. L'article original compte plus de 45 000 citations (environ 10 fois celui de BERT), qui est également très célèbre.

Il ne peut pas être implémenté par l'API séquentielle seule. Il peut être implémenté avec l'API fonctionnelle et l'API de sous-classification.

Le papier original est ici. https://arxiv.org/abs/1512.03385

Mise en œuvre de VGG16

Implémentons-le dans chaque style d'écriture.

VGG16 Sequential API

Je n'ai pas à y penser, alors je l'écris normalement.

from tensorflow.keras import layers, Sequential


def sequential_vgg16(input_shape, output_size):
    params = {
        "padding": "same",
        "use_bias": True,
        "kernel_initializer": "he_normal",
    }
    model = Sequential()
    model.add(layers.Conv2D(64, 3, 1, **params, batch_input_shape=input_shape))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(64, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.MaxPool2D(2, padding="same"))
    model.add(layers.Conv2D(128, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(128, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.MaxPool2D(2, padding="same"))
    model.add(layers.Conv2D(256, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(256, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(256, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.MaxPool2D(2, padding="same"))
    model.add(layers.Conv2D(512, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(512, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(512, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.MaxPool2D(2, padding="same"))
    model.add(layers.Conv2D(512, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(512, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.Conv2D(512, 3, 1, **params))
    model.add(layers.BatchNormalization())
    model.add(layers.ReLU())
    model.add(layers.MaxPool2D(2, padding="same"))
    model.add(layers.Flatten())
    model.add(layers.Dense(4096))
    model.add(layers.Dense(4096))
    model.add(layers.Dense(output_size, activation="softmax"))
    return model

C'est assez simple à écrire, mais il s'avère difficile à voir à cause des nombreuses couches. Par exemple, il semble que vous ne remarquerez pas si ReLU manque quelque part. Aussi, par exemple, si vous voulez éliminer la normalisation par lots, vous devez commenter ligne par ligne, ce qui est mal réutilisable et personnalisable.

VGG16 Functional API Il est plus flexible à écrire que l'API séquentielle. Cette fois, utilisons un groupe de couches à réutiliser (Convolution --Batch Normalization --ReLU) en tant que fonction.

from tensorflow.keras import layers, Model


def functional_cbr(x, filters, kernel_size, strides):
    params = {
        "filters": filters,
        "kernel_size": kernel_size,
        "strides": strides,
        "padding": "same",
        "use_bias": True,
        "kernel_initializer": "he_normal",
    }

    x = layers.Conv2D(**params)(x)
    x = layers.BatchNormalization()(x)
    x = layers.ReLU()(x)
    return x


def functional_vgg16(input_shape, output_size):
    inputs = layers.Input(batch_input_shape=input_shape)
    x = functional_cbr(inputs, 64, 3, 1)
    x = functional_cbr(x, 64, 3, 1)
    x = layers.MaxPool2D(2, padding="same")(x)
    x = functional_cbr(x, 128, 3, 1)
    x = functional_cbr(x, 128, 3, 1)
    x = layers.MaxPool2D(2, padding="same").__call__(x)  #Tu peux écrire comme ça
    x = functional_cbr(x, 256, 3, 1)
    x = functional_cbr(x, 256, 3, 1)
    x = functional_cbr(x, 256, 3, 1)
    x = layers.MaxPool2D(2, padding="same").call(x)  #Tu peux écrire comme ça
    x = functional_cbr(x, 512, 3, 1)
    x = functional_cbr(x, 512, 3, 1)
    x = functional_cbr(x, 512, 3, 1)
    x = layers.MaxPool2D(2, padding="same")(x)
    x = functional_cbr(x, 512, 3, 1)
    x = functional_cbr(x, 512, 3, 1)
    x = functional_cbr(x, 512, 3, 1)
    x = layers.MaxPool2D(2, padding="same")(x)
    x = layers.Flatten()(x)
    x = layers.Dense(4096)(x)
    x = layers.Dense(4096)(x)
    outputs = layers.Dense(output_size, activation="softmax")(x)
    return Model(inputs=inputs, outputs=outputs)

J'ai pu écrire assez clairement. Si vous voulez vous débarrasser de BatchNormalization ou changer ReLU en LeaklyReLU, il vous suffit de corriger quelques lignes.

VGG16 Subclassing API Écrivons un groupe de couches (Convolution --BatchNormalization --ReLU) qui est réutilisé en tant que classe comme l'API fonctionnelle.

from tensorflow.keras import layers, Model


class CBR(Model):
    def __init__(self, filters, kernel_size, strides):
        super().__init__()

        params = {
            "filters": filters,
            "kernel_size": kernel_size,
            "strides": strides,
            "padding": "same",
            "use_bias": True,
            "kernel_initializer": "he_normal",
        }

        self.layers_ = [
            layers.Conv2D(**params),
            layers.BatchNormalization(),
            layers.ReLU()
        ]

    def call(self, inputs):
        for layer in self.layers_:
            inputs = layer(inputs)
        return inputs


class VGG16(Model):
    def __init__(self, output_size=1000):
        super().__init__()
        self.layers_ = [
            CBR(64, 3, 1),
            CBR(64, 3, 1),
            layers.MaxPool2D(2, padding="same"),
            CBR(128, 3, 1),
            CBR(128, 3, 1),
            layers.MaxPool2D(2, padding="same"),
            CBR(256, 3, 1),
            CBR(256, 3, 1),
            CBR(256, 3, 1),
            layers.MaxPool2D(2, padding="same"),
            CBR(512, 3, 1),
            CBR(512, 3, 1),
            CBR(512, 3, 1),
            layers.MaxPool2D(2, padding="same"),
            CBR(512, 3, 1),
            CBR(512, 3, 1),
            CBR(512, 3, 1),
            layers.MaxPool2D(2, padding="same"),
            layers.Flatten(),
            layers.Dense(4096),
            layers.Dense(4096),
            layers.Dense(output_size, activation="softmax"),
        ]

    def call(self, inputs):
        for layer in self.layers_:
            inputs = layer(inputs)
        return inputs

C'est plus facile à comprendre intuitivement que l'API fonctionnelle car «init» est responsable de la définition du modèle et «call» est responsable de l'appel du modèle, mais le code est plus long. Un autre point est que l'API pédagogique de sous-classification ne nécessite pas de forme d'entrée lors de la génération d'un modèle (aucune «forme_entrée» n'est requise dans l'argument).

Examen de la mise en œuvre de la VGG16

J'avais l'intention de rendre la comparaison aussi simple que possible, mais comment était-ce?

Cette implémentation utilise la normalisation par lots entre les couches de convolution et l'initialisation He pour l'initialisation du poids, mais ces techniques n'ont pas encore été publiées lorsque l'article original a été soumis. Il n'y avait donc pas de couche de normalisation par lots et l'initialisation de Grolot a été utilisée pour initialiser les poids. Par conséquent, dans l'article original, une méthode d'apprentissage de type transfert par apprentissage est adoptée dans laquelle un modèle à 7 couches est formé, puis des couches sont progressivement ajoutées afin d'éviter la disparition du gradient.

Il serait intéressant d'essayer ce qui se passe si vous supprimez la couche de normalisation par lots, ce qui se passe si vous changez la méthode d'initialisation du poids, etc. pour mieux comprendre l'implémentation ci-dessus.

Implémentation ResNet50

Ensuite, implémentez ResNet50. Comme il ne peut pas être écrit uniquement par l'API séquentielle, il est écrit par l'API fonctionnelle et l'API de sous-classification.

ResNet50 Functional API Fonctionnalisez et implémentez le mécanisme résiduel réutilisé.

from tensorflow.keras import layers, Model


def functional_bottleneck_residual(x, in_ch, out_ch, strides=1):
    params = {
        "padding": "same",
        "kernel_initializer": "he_normal",
        "use_bias": True,
    }
    inter_ch = out_ch // 4
    h1 = layers.Conv2D(inter_ch, kernel_size=1, strides=strides, **params)(x)
    h1 = layers.BatchNormalization()(h1)
    h1 = layers.ReLU()(h1)
    h1 = layers.Conv2D(inter_ch, kernel_size=3, strides=1, **params)(h1)
    h1 = layers.BatchNormalization()(h1)
    h1 = layers.ReLU()(h1)
    h1 = layers.Conv2D(out_ch, kernel_size=1, strides=1, **params)(h1)
    h1 = layers.BatchNormalization()(h1)

    if in_ch != out_ch:
        h2 = layers.Conv2D(out_ch, kernel_size=1, strides=strides, **params)(x)
        h2 = layers.BatchNormalization()(h2)
    else:
        h2 = x

    h = layers.Add()([h1, h2])
    h = layers.ReLU()(h)
    return h


def functional_resnet50(input_shape, output_size):
    inputs = layers.Input(batch_input_shape=input_shape)
    x = layers.Conv2D(64, 7, 2, padding="same", kernel_initializer="he_normal")(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding="same")(x)

    x = functional_bottleneck_residual(x, 64, 256)
    x = functional_bottleneck_residual(x, 256, 256)
    x = functional_bottleneck_residual(x, 256, 256)

    x = functional_bottleneck_residual(x, 256, 512, 2)
    x = functional_bottleneck_residual(x, 512, 512)
    x = functional_bottleneck_residual(x, 512, 512)
    x = functional_bottleneck_residual(x, 512, 512)

    x = functional_bottleneck_residual(x, 512, 1024, 2)
    x = functional_bottleneck_residual(x, 1024, 1024)
    x = functional_bottleneck_residual(x, 1024, 1024)
    x = functional_bottleneck_residual(x, 1024, 1024)
    x = functional_bottleneck_residual(x, 1024, 1024)
    x = functional_bottleneck_residual(x, 1024, 1024)

    x = functional_bottleneck_residual(x, 1024, 2048, 2)
    x = functional_bottleneck_residual(x, 2048, 2048)
    x = functional_bottleneck_residual(x, 2048, 2048)

    x = layers.GlobalAveragePooling2D()(x)
    outputs = layers.Dense(
        output_size, activation="softmax", kernel_initializer="he_normal"
    )(x)
    return Model(inputs=inputs, outputs=outputs)

Dans la méthode Functional_bottleneck_residual, h1, h2 et h apparaissent. De cette manière, un modèle dans lequel le flux de données se branche au milieu ne peut pas être décrit par l'API séquentielle.

De plus, «h2» ne fait rien si le nombre de canaux d'entrée / sortie est le même, et exécute le processus d'ajustement du nombre de canaux (Projection) s'ils sont différents. Un tel branchement conditionnel ne peut pas être décrit par l'API séquentielle.

Une fois que vous avez créé cette méthode, il ne vous reste plus qu'à l'écrire en séquence.

ResNet50 Subclassing API

Classifiez et implémentez un mécanisme résiduel qui est réutilisé comme l'API fonctionnelle.

from tensorflow import layers, Model


class BottleneckResidual(Model):
    """Module résiduel de goulot d'étranglement de ResNet.
En réduisant la dimension ch avec 1x1 conv sur la première couche
Réduction de la quantité de calcul de 3x3 conv dans la deuxième couche
Restaurez les dimensions de la sortie ch avec un 1x1 conv sur la troisième couche.
On l'appelle goulot d'étranglement car il réduit la dimension de la deuxième couche 3x3 conv, ce qui nécessite beaucoup de calcul..
    """

    def __init__(self, in_ch, out_ch, strides=1):
        super().__init__()

        self.projection = in_ch != out_ch
        inter_ch = out_ch // 4
        params = {
            "padding": "same",
            "kernel_initializer": "he_normal",
            "use_bias": True,
        }

        self.common_layers = [
            layers.Conv2D(inter_ch, kernel_size=1, strides=strides, **params),
            layers.BatchNormalization(),
            layers.ReLU(),
            layers.Conv2D(inter_ch, kernel_size=3, strides=1, **params),
            layers.BatchNormalization(),
            layers.ReLU(),
            layers.Conv2D(out_ch, kernel_size=1, strides=1, **params),
            layers.BatchNormalization(),
        ]

        if self.projection:
            self.projection_layers = [
                layers.Conv2D(out_ch, kernel_size=1, strides=strides, **params),
                layers.BatchNormalization(),
            ]

        self.concat_layers = [layers.Add(), layers.ReLU()]

    def call(self, inputs):
        h1 = inputs
        h2 = inputs

        for layer in self.common_layers:
            h1 = layer(h1)

        if self.projection:
            for layer in self.projection_layers:
                h2 = layer(h2)

        outputs = [h1, h2]
        for layer in self.concat_layers:
            outputs = layer(outputs)
        return outputs


class ResNet50(Model):
    """ResNet50.
L'élément est
    conv * 1
    resblock(conv * 3) * 3
    resblock(conv * 3) * 4
    resblock(conv * 3) * 6
    resblock(conv * 3) * 3
    dense * 1
Consiste en, conv * 49 + dense *50 couches de 1.
    """

    def __init__(self, output_size=1000):
        super().__init__()

        self.layers_ = [
            layers.Conv2D(64, 7, 2, padding="same", kernel_initializer="he_normal"),
            layers.BatchNormalization(),
            layers.MaxPool2D(pool_size=3, strides=2, padding="same"),
            BottleneckResidual(64, 256),
            BottleneckResidual(256, 256),
            BottleneckResidual(256, 256),
            BottleneckResidual(256, 512, 2),
            BottleneckResidual(512, 512),
            BottleneckResidual(512, 512),
            BottleneckResidual(512, 512),
            BottleneckResidual(512, 1024, 2),
            BottleneckResidual(1024, 1024),
            BottleneckResidual(1024, 1024),
            BottleneckResidual(1024, 1024),
            BottleneckResidual(1024, 1024),
            BottleneckResidual(1024, 1024),
            BottleneckResidual(1024, 2048, 2),
            BottleneckResidual(2048, 2048),
            BottleneckResidual(2048, 2048),
            layers.GlobalAveragePooling2D(),
            layers.Dense(
                output_size, activation="softmax", kernel_initializer="he_normal"
            ),
        ]

    def call(self, inputs):
        for layer in self.layers_:
            inputs = layer(inputs)
        return inputs

Ce n'est pas si différent de l'API fonctionnelle. La couche __init__ est écrite pour rassembler les couches dans une liste, mais vous pouvez écrire cette zone librement tant qu'elle est enregistrée dans la variable de classe.

Examen de la mise en œuvre de ResNet50

Nous avons introduit ResNet50 en tant que modèle qui ne peut pas être implémenté par l'API séquentielle seule. Pour être honnête, il n'y a pas de grande différence, donc je pense que vous pouvez utiliser l'API fonctionnelle et l'API de sous-classification selon vos préférences.

Mise en place de formations

Enfin, comparons l'implémentation de la boucle d'apprentissage.

Comme il serait assez long de mettre tout le code source, la méthode est partiellement découpée en src.utils. Ce n'est pas si compliqué, il serait donc utile que vous puissiez le lire tout en le complétant.

Pour le moment, toutes les sources sont dans le référentiel suivant, alors jetez un œil si vous êtes intéressé. https://github.com/Anieca/deep-learning-models

mise en œuvre de la formation intégrée

Spécifions quelques options telles que le calcul de la précision des données de test et la sortie du journal pour TensorBoard.


import os
import tensorflow as tf

from src.utils import load_dataset, load_model, get_args, get_current_time


def builtin_train(args):
    # 1. load dataset and model
    (train_images, train_labels), (test_images, test_labels) = load_dataset(args.data)
    input_shape = train_images[: args.batch_size, :, :, :].shape
    output_size = max(train_labels) + 1
    model = load_model(args.arch, input_shape=input_shape, output_size=output_size)
    model.summary()

    # 2. set tensorboard cofigs
    logdir = os.path.join(args.logdir, get_current_time())
    tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=logdir)

    # 3. loss, optimizer, metrics setting
    model.compile(
        optimizer=tf.keras.optimizers.Adam(),
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )

    # 4. dataset config (and validation, callback config)
    fit_params = {}
    fit_params["batch_size"] = args.batch_size
    fit_params["epochs"] = args.max_epoch
    if args.steps_per_epoch:
        fit_params["steps_per_epoch"] = args.steps_per_epoch
    fit_params["verbose"] = 1
    fit_params["callbacks"] = [tensorboard_callback]
    fit_params["validation_data"] = (test_images, test_labels)

    # 5. start train and test
    model.fit(train_images, train_labels, **fit_params)

C'est assez simple à écrire.

Il existe de nombreuses autres fonctions de rappel, donc si vous êtes intéressé, veuillez lire la documentation. https://www.tensorflow.org/api_docs/python/tf/keras/callbacks

Implémentation de formations personnalisées

Implémentons le même processus que la formation intégrée ci-dessus.

import os
import tensorflow as tf

from src.utils import load_dataset, load_model, get_args, get_current_time


def custom_train(args):
    # 1. load dataset and model
    (train_images, train_labels), (test_images, test_labels) = load_dataset(args.data)
    input_shape = train_images[: args.batch_size, :, :, :].shape
    output_size = max(train_labels) + 1
    model = load_model(args.arch, input_shape=input_shape, output_size=output_size)
    model.summary()

    # 2. set tensorboard configs
    logdir = os.path.join(args.logdir, get_current_time())
    train_writer = tf.summary.create_file_writer(os.path.join(logdir, "train"))
    test_writer = tf.summary.create_file_writer(os.path.join(logdir, "test"))

    # 3. loss, optimizer, metrics setting
    criterion = tf.keras.losses.SparseCategoricalCrossentropy()
    optimizer = tf.keras.optimizers.Adam()
    train_loss_avg = tf.keras.metrics.Mean()
    train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy()
    test_loss_avg = tf.keras.metrics.Mean()
    test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy()

    # 4. dataset config
    buffer_size = len(train_images)
    train_ds = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
    train_ds = train_ds.shuffle(buffer_size=buffer_size).batch(args.batch_size)
    test_ds = tf.data.Dataset.from_tensor_slices((test_images, test_labels))
    test_ds = test_ds.batch(args.batch_size)

    # 5. start train and test
    for epoch in range(args.max_epoch):
        # 5.1. initialize metrics
        train_loss_avg.reset_states()
        train_accuracy.reset_states()
        test_loss_avg.reset_states()
        test_loss_avg.reset_states()

        # 5.2. initialize progress bar
        train_pbar = tf.keras.utils.Progbar(args.steps_per_epoch)
        test_pbar = tf.keras.utils.Progbar(args.steps_per_epoch)

        # 5.3. start train
        for i, (x, y_true) in enumerate(train_ds):
            if args.steps_per_epoch and i >= args.steps_per_epoch:
                break
            # 5.3.1. forward
            with tf.GradientTape() as tape:
                y_pred = model(x, training=True)
                loss = criterion(y_true=y_true, y_pred=y_pred)
            # 5.3.2. calculate gradients from `tape` and backward
            gradients = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(gradients, model.trainable_variables))

            # 5.3.3. update metrics and progress bar
            train_loss_avg(loss)
            train_accuracy(y_true, y_pred)
            train_pbar.update(
                i + 1,
                [
                    ("avg_loss", train_loss_avg.result()),
                    ("accuracy", train_accuracy.result()),
                ],
            )

        # 5.4. start test
        for i, (x, y_true) in enumerate(test_ds):
            if args.steps_per_epoch and i >= args.steps_per_epoch:
                break
            # 5.4.1. forward
            y_pred = model(x)
            loss = criterion(y_true, y_pred)

            # 5.4.2. update metrics and progress bar
            test_loss_avg(loss)
            test_accuracy(y_true, y_pred)
            test_pbar.update(
                i + 1,
                [
                    ("avg_test_loss", test_loss_avg.result()),
                    ("test_accuracy", test_accuracy.result()),
                ],
            )

        # 5.5. write metrics to tensorboard
        with train_writer.as_default():
            tf.summary.scalar("Loss", train_loss_avg.result(), step=epoch)
            tf.summary.scalar("Acc", train_accuracy.result(), step=epoch)
        with test_writer.as_default():
            tf.summary.scalar("Loss", test_loss_avg.result(), step=epoch)
            tf.summary.scalar("Acc", test_accuracy.result(), step=epoch)

Cela ne change pas tant que ça jusqu'au début de la formation, mais la quantité de description dans la boucle de formation (commentaire 5) est considérablement importante.

Rétrospective de mise en œuvre de la formation

La gestion des utilitaires tels que la gestion de la sortie TensorBoard et la création de barres de progression vous-même peut être coûteuse, mais la fonction intégrée est assez facile à utiliser.

Si vous souhaitez écrire un processus qui n'est pas fourni dans intégré, vous devez l'écrire dans une formation personnalisée, mais sinon, il semble préférable d'utiliser intégré.

À la fin

c'est tout. Je vous remercie pour votre travail acharné.

J'ai introduit diverses méthodes d'écriture du système TensorFlow 2 avec implémentation.

J'ai l'intention d'écrire catégoriquement sans donner trop de supériorité ou d'infériorité à chaque style d'écriture.

Lorsque vous écrivez par vous-même, je pense que vous devriez écrire en fonction de la situation et du goût, mais lorsque vous recherchez le code source, vous rencontrerez différents styles d'écriture, donc je pense que c'est bien si vous comprenez tous les styles d'écriture d'une manière ou d'une autre.

Nous espérons que vous trouvez cela utile.

Recommended Posts

Explique comment utiliser TensorFlow 2.X avec l'implémentation de VGG16 / ResNet50
Python: comment utiliser async avec
Pour utiliser virtualenv avec PowerShell
Résumé de l'utilisation de pyenv-virtualenv
Résumé de l'utilisation de csvkit
NumPy> [1, 2, 3, 4, 5, 6, 7, 8, 9] avec 3 ndarrays ([1,4,7], [2,5,8], [3,6,9]) Implémentation à diviser en> {Comment utiliser slice / reshape () + Méthode de translocation matricielle}
Comment utiliser ManyToManyField avec l'administrateur de Django
Comment utiliser OpenVPN avec Ubuntu 18.04.3 LTS
Comment utiliser Cmder avec PyCharm (Windows)
Comment calculer Utiliser% de la commande df
Comment utiliser le japonais avec le tracé NLTK
[Python2.7] Résumé de l'utilisation d'unittest
Comment utiliser le notebook Jupyter avec ABCI
Comment utiliser Tensorflow dans un environnement Docker
Jupyter Notebook Principes d'utilisation
Comment utiliser la commande CUT (avec exemple)
Bases de PyTorch (1) -Comment utiliser Tensor-
Résumé de l'utilisation de la liste Python
[Python2.7] Résumé de l'utilisation du sous-processus
Comment utiliser SQLAlchemy / Connect avec aiomysql
[Question] Comment utiliser plot_surface de python
Comment utiliser le pilote JDBC avec Redash
Comment utiliser la trace GCP avec la télémétrie ouverte
Comment utiliser Folium (visualisation des informations de localisation)
[Python] Comment utiliser deux types de type ()
Pas beaucoup de mention de la façon d'utiliser Pickle
Résumé de l'utilisation de MNIST avec Python
Comment spécifier des attributs avec Mock of Python
Comment implémenter "named_scope" de RubyOnRails avec Django
Histoire d'essayer d'utiliser Tensorboard avec Pytorch
[Algorithm x Python] Comment utiliser la liste
Comment utiliser tkinter avec python dans pyenv
Comment utiliser xml.etree.ElementTree
Comment utiliser Python-shell
Remarques sur l'utilisation de tf.data
Comment utiliser virtualenv
Comment utiliser xgboost: classification multi-classes avec des données d'iris
Comment utiliser la correspondance d'image
Comment utiliser le shogun
Remarques sur l'utilisation d'AIST Spacon ABCI
Comment utiliser Pandas 2
J'ai essayé de résumer comment utiliser matplotlib de python
Préparation à l'utilisation de Tensorflow (Anaconda) avec Visual Studio Code
Comment utiliser Virtualenv
Expliquez en détail comment créer un son avec python
Comment utiliser numpy.vectorize
Remarques sur la façon d'utiliser lors de la combinaison de pandas.
Comment utiliser pytest_report_header
Comment installer Caffe sur OS X avec macports
Comment utiliser partiel
Comment utiliser Bio.Phylo
Pour utiliser TensorFlow sur des GPU inférieurs à Titan
Comment utiliser SymPy
Comment utiliser le mode interactif python avec git bash
Comment utiliser x-means
Comment utiliser WikiExtractor.py
Comment utiliser IPython
Comment utiliser Python Kivy ① ~ Bases du langage Kv ~