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.
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.
>>> 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
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.
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.
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)
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 **
Il y a deux façons de s'entraîner:
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.
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).
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 **
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.
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
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
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).
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.
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.
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.
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
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é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.
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é.
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