[PYTHON] Comprendre l'espace de noms TensorFlow et les variables partagées principales

Intro

Ceci est l'article du 9ème jour du Calendrier de l'Avent TensorFlow 2016.

TensorFlow est sorti en novembre 2015, mais la fonction «namespace» était prise en charge depuis le début. Ceci est utilisé dans la visualisation graphique avec TensorBoard, mais bien sûr, ce n'est pas seulement pour cela. Les espaces de noms sont très utiles pour gérer les identifiants. La prise en charge solide des "espaces de noms" me rappelle le C ++, mais je vais le citer dans le manuel d'instructions C ++ (auto-apprentissage C ++).

Le but des espaces de noms est de localiser les noms d'identificateurs et d'éviter les conflits de noms. Dans l'environnement de programmation C ++, les noms de variables, de fonctions et de classes ont continué à proliférer. Avant l'avènement des espaces de noms, tous ces noms se disputaient l'espace dans l'espace de noms global, créant de nombreux conflits.

D'autre part, la portée des variables de Python n'a que le minimum de local, global (global), + α ( nonlocal), donc un ingénieur de Google qui a écrit la partie principale de TensorFlow en C ++ Il semble naturel de penser à implémenter la prise en charge des espaces de noms au niveau C ++ dans TensorFlow.

Dans MLP (Multi-Layer Perceptron), qui n'a pas beaucoup de couches dans Neural Network, la gestion des noms n'est pas un problème, mais pour les CNN profonds et les grands modèles trouvés dans les RNN, le partage de poids est également possible, donc des variables appropriées. Une portée est requise. De plus, compte tenu de la mise à l'échelle (même si j'ai peu d'expérience avec moi-même), je dois appliquer le code à des environnements distribués tels que Multi-Device (GPU) et clusters. Encore une fois, nous avons besoin d'une portée variable.

Dans cet article, je voudrais confirmer l'API associée dans le but de bien comprendre «l'espace de nom» de TensorFlow. (L'environnement de programmation est TensorFlow 0.11.0, Python 3.5.2, Ubuntu 16.04LTS.)

Point de capture de portée variable TensorFlow

Si vous lisez correctement le document, la portée des variables n'est pas difficile, mais si vous la comprenez «de manière ambiguë», les points suivants seront capturés.

--Il existe tf.name_scope () et tf.variable_scope () pour définir la portée, mais quelle est la différence?

J'écrirai d'abord la réponse, mais la réponse à la question 1 est que tf.name_scope () est une définition de portée plus générale, et tf.variable_scope () est une définition de portée dédiée pour la gestion des variables (identificateurs). Il devient. De plus, la réponse à la question 2 est que tf.Variable () est une définition de variable plus primitive (niveau inférieur), tandis que tf.get_variable () est une variable (de niveau supérieur) qui considère la portée de la variable. Cela devient une définition. (Le document connexe, TesorFlow - "COMMENT" - "Partage de variable" explique en détail la relation de variable partagée.)

Ci-dessous, je voudrais déplacer le code et étudier les détails.

# tf.name_scope
with tf.name_scope("my_scope"):
    v1 = tf.get_variable("var1", [1], dtype=tf.float32)
    v2 = tf.Variable(1, name="var2", dtype=tf.float32)
    a = tf.add(v1, v2)

print(v1.name)  # var1:0
print(v2.name)  # my_scope/var2:0
print(a.name)   # my_scope/Add:0

Tout d'abord, j'ai utilisé tf.name_scope () pour définir les variables qu'il contient. L'identifiant géré par TensorFlow est sorti dans la seconde moitié, mais la sortie est affichée sous forme de commentaire à droite de l'instruction d'impression. La portée de "my_scope" est correctement définie pour la variable v2 définie par tf.Variable () et l'opération d'ajout a. D'un autre côté, la v1 définie par tf.get_varible () a brillamment ignoré la portée.

 # tf.variable_scope
with tf.variable_scope("my_scope"):
    v1 = tf.get_variable("var1", [1], dtype=tf.float32)
    v2 = tf.Variable(1, name="var2", dtype=tf.float32)
    a = tf.add(v1, v2)

print(v1.name)  # my_scope/var1:0
print(v2.name)  # my_scope_1/var2:0  ...Le nom de l'étendue a été mis à jour.
print(a.name)   # my_scope_1/Add:0   ...Le nom de l'étendue après la mise à jour est conservé.

Ensuite, j'ai utilisé tf.variable_sope (). (Notez que l'extrait de code précédent et l'extrait actuel sont en mouvement continu.) L'identifiant de la variable v1 défini par tf.get_variable () a été créé avec "my_scope" attaché au nom de la variable comme prévu. De plus, la variable v2 en dessous et l'opération a ont ajouté "my_scope_1" (malgré le fait qu'il s'agit de tf.variable_scope ("my_scope")). La raison en est qu'il aurait dû être donné "my_scope" (dans l'état initial du programme), mais c'est automatique car le même identifiant ("my_scope / var2: 0") a déjà été utilisé dans l'extrait de code précédent. C'est parce que j'ai mis à jour "my_scope_1". (L'instruction ʻa = tf.add (v1, v2) ʻaprès la mise à jour du nom de la portée ("my_scope" -> "my_scope_1") semble conserver cette portée ("my_scope_1").)

Ça devient compliqué, alors je vais régler un peu les choses.

--tf.name_scope () est une définition de portée de nom à usage général. (Comme vous le savez, la sortie vers TensorBoard utilise ce paramètre d'identificateur.) --tf.variable_scope () est une définition de portée utilisée pour la gestion des variables. (Le nom de la fonction variable_scope est tel quel ...) --tf.get_variable () définit les variables tout en gérant les identificateurs de nom de variable (nouveau ou dupliqué?). Assurez-vous de l'utiliser comme un ensemble avec tf.variable_scope ().

Dans les deux extraits ci-dessus, la situation était compliquée à cause de l'expérience, mais ce n'est pas particulièrement difficile si vous suivez le principe de base que "tf.get_variable () est utilisé comme un ensemble avec tf.variable_scope ()".

Voyons maintenant comment utiliser les variables partagées en utilisant tf.get_variable ().

with tf.variable_scope("my_scope2"):
    v4_init = tf.constant_initializer([4.])
    v4 = tf.get_variable("var4", shape=[1], initializer=v4_init)

print(v4.name)  # my_scope2/var4:0

Tout d'abord, dans la portée "my_scope2", nous avons défini la variable v4. tf.get_variable () spécifie un initialiseur de variable pour définir une variable. Ici, nous avons utilisé un initialiseur constant pour faire une instruction qui contient 4. dans la v4. Pour l'identificateur de TensorFlow, "var4" a été spécifié dans le premier argument.

Ensuite, essayez d'allouer une variable avec le même identifiant.

with tf.variable_scope("my_scope2"):
    v5 = tf.get_variable("var4", shape=[1], initializer=v4_init)
ValueError: Variable my_scope2/var4 already exists, disallowed. Did you mean to set reuse=True in VarScope? Originally defined at:

  File "name_scope_ex.py", line 47, in <module>
    v4 = tf.get_variable("var4", shape=[1], initializer=v4_init)

Comme prévu. Une ValueError s'est produite. L'erreur est "N'est-il pas étrange de prendre des variables avec le même identifiant?" La réallocation de variable ** utilisant le même identifiant utilise l'option réutilisation comme suit.

with tf.variable_scope("my_scope2", reuse=True):
    v5 = tf.get_variable("var4", shape=[1])
print(v5.name)  # my_scope2/var4:0

Vous pouvez également effectuer les opérations suivantes.

with tf.variable_scope("my_scope2"):
    tf.get_variable_scope().reuse_variables()
    v5 = tf.get_variable("var4", shape=[1])
print(v5.name)  # my_scope2/var4:0

Jusqu'à présent, nous avons confirmé les fonctions de base de tf.variable_scope () et tf.get_variable ().

Exemple de variable partagée - Autoencoder

Regardons maintenant un exemple d'utilisation de variables partagées, mais dans le document TensorFlow --Sharing Variable , Veuillez vous référer à l'exemple d'utilisation suivant.

Étant donné que les deux sont une quantité considérable de code, cette fois, je voudrais reprendre le partage de poids d'un auto-encodeur (ci-après, Autoencoder) différent de ceux-ci. Le côté codage / côté décodage d'Autoencoder peut être exprimé comme suit.

y = f(\textbf{W}x + \textbf{b})  \\
\hat{x} = \tilde{f}(\tilde{\textbf{W}}y + \tilde{\textbf{b}})

Le partage de poids suivant peut être utilisé dans un tel Autoencoder symétrique.

\tilde{\textbf{W} } = \textbf{W} ^{\mathrm{T}}

Implémentons le réseau de configuration ci-dessus en utilisant des variables partagées de TensorFlow. Tout d'abord, définissez la classe Encoder.

# Encoder Layer   
class Encoder(object):
    def __init__(self, input, n_in, n_out, vs_enc='encoder'):
        self.input = input
        with tf.variable_scope(vs_enc):
            weight_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.05)
            W = tf.get_variable('W', [n_in, n_out], initializer=weight_init)
            bias_init = tf.constant_initializer(value=0.0)
            b = tf.get_variable('b', [n_out], initializer=bias_init)
        self.w = W
        self.b = b
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.sigmoid(linarg)
        
        return self.output

La portée de la variable est spécifiée par l'option «vs_enc» et définie, et «W» est définie par «tf.get_variable ()». Vient ensuite la classe Decoder, qui est la suivante.

# Decoder Layer
class Decoder(object):
    def __init__(self, input, n_in, n_out, vs_dec='decoder'):
        self.input = input
        if vs_dec == 'decoder': # independent weight
            with tf.variable_scope(vs_dec):
                weight_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.05)
                W = tf.get_variable('W', [n_in, n_out], initializer=weight_init)
        else:                   # weight sharing (tying)
            with tf.variable_scope(vs_dec, reuse=True):     # set reuse option
                W = tf.get_variable('W', [n_out, n_in])
                W = tf.transpose(W)

        with tf.variable_scope('decoder'):  # in all case, need new bias
            bias_init = tf.constant_initializer(value=0.0)
            b = tf.get_variable('b', [n_out], initializer=bias_init)
        self.w = W
        self.b = b
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.sigmoid(linarg)
        
        return self.output

La plupart sont identiques à la classe Encoder, mais l'instruction de définition de la variable W est traitée par branchement. La partie définition du réseau est la suivante.

# make neural network model
def make_model(x):
    enc_layer = Encoder(x, 784, 625, vs_enc='encoder')
    enc_out = enc_layer.output()
    dec_layer = Decoder(enc_out, 625, 784, vs_dec='encoder')
    dec_out = dec_layer.output()

    return enc_out, dec_out

Si vous spécifiez vs_dec = 'decoder' lors de la création d'un objet Decoder, ou si vous omettez cette option, une nouvelle variable de poids W sera allouée, et Encoder l'allouera comme vs_dec =' encoder' ci-dessus. Lorsque la même portée de variable a été utilisée, la variable de pondération a été implémentée de sorte que «W» soit réutilisé comme variable partagée. (Si vous souhaitez le réutiliser, transposez «W» pour qu'il corresponde au réseau.)

Un exemple d'exécution d'un calcul avec des données MNIST est présenté. Premièrement, si le partage de poids n'est pas effectué, le résultat est le suivant.

Training...
  step, loss =      0:  0.732
  step, loss =   1000:  0.271
  step, loss =   2000:  0.261
  step, loss =   3000:  0.240
  step, loss =   4000:  0.234
  step, loss =   5000:  0.229
  step, loss =   6000:  0.219
  step, loss =   7000:  0.197
  step, loss =   8000:  0.195
  step, loss =   9000:  0.193
  step, loss =  10000:  0.189
loss (test) =  0.183986

Lorsque le partage du poids est effectué, c'est comme suit.

Training...
  step, loss =      0:  0.707
  step, loss =   1000:  0.233
  step, loss =   2000:  0.215
  step, loss =   3000:  0.194
  step, loss =   4000:  0.186
  step, loss =   5000:  0.174
  step, loss =   6000:  0.167
  step, loss =   7000:  0.154
  step, loss =   8000:  0.159
  step, loss =   9000:  0.152
  step, loss =  10000:  0.152
loss (test) =  0.147831

En raison du paramètre de partage de poids, la perte (entropie croisée) diminue plus rapidement avec le même nombre d'apprentissage. Puisque le degré de liberté du réseau est d'environ la moitié, on peut dire que le résultat est comme attendu.

Classificateur d'exemple de modèle assez complexe avec deux MLP

Considérons un autre modèle légèrement compliqué. (Bien que ce ne soit pas si compliqué ...) MNIST est utilisé pour les données à traiter comme ci-dessus. Cette fois, nous effectuerons une classification multi-classes. En tant que classificateur, nous avons utilisé MLP (Multi-layer Perceptron) avec 2 couches cachées et 1 couche de sortie. La figure ci-dessous est un graphique de TensorBoard.

Fig. Graph of 2 MLP networks mnist_2nets_60.png

(Je ne connais pas TensorBoard. Veuillez le prendre comme une image approximative.)

Tout d'abord, définissez les classes de la couche masquée (couche entièrement connectée) et de la couche de sortie.

# Full-connected Layer   
class FullConnected(object):
    def __init__(self, input, n_in, n_out, vn=('W', 'b')):
        self.input = input

        weight_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.05)
        bias_init = tf.constant_initializer(value=0.0)
        W = tf.get_variable(vn[0], [n_in, n_out], initializer=weight_init)
        b = tf.get_variable(vn[1], [n_out], initializer=bias_init)
        self.w = W
        self.b = b
        self.params = [self.w, self.b]
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.relu(linarg)
        
        return self.output
#

# Read-out Layer
class ReadOutLayer(object):
    def __init__(self, input, n_in, n_out, vn=('W', 'b')):
        self.input = input

        weight_init = tf.random_normal_initializer(mean=0.0, stddev=0.05)
        bias_init = tf.constant_initializer(value=0.0)
        W = tf.get_variable(vn[0], [n_in, n_out], initializer=weight_init)
        b = tf.get_variable(vn[1], [n_out], initializer=bias_init)
        self.w = W
        self.b = b
        self.params = [self.w, self.b]
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.softmax(linarg)  

        return self.output

Le nom de la variable est défini comme une option du constructeur de classe, mais l'opération de partage de variable n'est pas effectuée ici. Ensuite, la partie qui définit le réseau est la suivante.

# Create the model
def mk_NN_model(scope='mlp', reuse=False):
    '''
      args.:
        scope   : variable scope ID of networks
        reuse   : reuse flag of weights/biases
    '''
    with tf.variable_scope(scope, reuse=reuse):
        hidden1 = FullConnected(x, 784, 625, vn=('W_hid_1','b_hid_1'))
        h1out = hidden1.output()
        hidden2 = FullConnected(h1out, 625, 625, vn=('W_hid_2','b_hid_2'))
        h2out = hidden2.output()    
        readout = ReadOutLayer(h2out, 625, 10, vn=('W_RO', 'b_RO'))
        y_pred = readout.output()
     
    cross_entropy = -tf.reduce_sum(y_*tf.log(y_pred))
    
    # Regularization terms (weight decay)
    L2_sqr = tf.nn.l2_loss(hidden1.w) + tf.nn.l2_loss(hidden2.w)
    lambda_2 = 0.01
    # the loss and accuracy
    with tf.name_scope('loss'):
        loss = cross_entropy + lambda_2 * L2_sqr
    with tf.name_scope('accuracy'):
        correct_prediction = tf.equal(tf.argmax(y_pred,1), tf.argmax(y_,1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    
    return y_pred, loss, accuracy

Cette fonction est spécifiée pour prendre la portée de la variable «scope» et l'indicateur de partage de variable «réutiliser» comme options. Dans deux réseaux MLP, le partage de poids alignera les noms de portée et définira l'indicateur réutilisation comme suit:

    y_pred1, loss1, accuracy1 = mk_NN_model(scope='mlp1')
    y_pred2, loss2, accuracy2 = mk_NN_model(scope='mlp1', reuse=True)

Si vous ne souhaitez pas partager de poids, définissez comme suit. (Bien que ce soit une syntaxe naturelle ...)

    y_pred1, loss1, accuracy1 = mk_NN_model(scope='mlp1')
    y_pred2, loss2, accuracy2 = mk_NN_model(scope='mlp2')

Les deux cas suivants ont été réalisés comme expériences de calcul.

  1. Les données d'apprentissage sont divisées en deux et fournies aux deux classificateurs «mlp1» et «mlp2». Les deux classificateurs définissent le partage du poids. Réalisé en série avec les formations "mlp1" et "mlp2". Classez les données de test en utilisant les paramètres finaux.
  2. Les données d'entraînement sont divisées en deux et fournies aux deux classificateurs «mlp1» et «mlp2». «mlp1» et «mlp2» sont des réseaux indépendants (non partagés), et chacun apprend. Les données de test sont appliquées à chaque classificateur et le résultat est une moyenne pour obtenir le résultat de classification final.

Puisque je voulais expérimenter le partage de poids, le nombre de couches et le nombre d'unités des deux classificateurs doivent être les mêmes. Cependant, comme le même classificateur est ennuyeux, l'optimiseur est différent et le taux d'apprentissage est finement ajusté.

Premièrement, le résultat de l'exécution du cas n ° 1 est le suivant.

Training...
  Network No.1 :
  step, loss, accurary =      0: 178.722,   0.470
  step, loss, accurary =   1000:  22.757,   0.950
  step, loss, accurary =   2000:  15.717,   0.990
  step, loss, accurary =   3000:  10.343,   1.000
  step, loss, accurary =   4000:   9.234,   1.000
  step, loss, accurary =   5000:   8.950,   1.000
  Network No.2 :
  step, loss, accurary =      0:  14.552,   0.980
  step, loss, accurary =   1000:   7.353,   1.000
  step, loss, accurary =   2000:   5.806,   1.000
  step, loss, accurary =   3000:   5.171,   1.000
  step, loss, accurary =   4000:   5.043,   1.000
  step, loss, accurary =   5000:   4.499,   1.000
accuracy1 =   0.9757
accuracy2 =   0.9744

Notez que la perte augmente légèrement au début de l'apprentissage du réseau n ° 2, mais elle est considérablement inférieure à la valeur au début de l'apprentissage n ° 1. Cela indique qu'à la suite du partage de poids, les paramètres (héritant du résultat d'apprentissage du n ° 1) ont commencé à partir du début du n ° 2. Cependant, la précision de la classification finale, ʻaccuracy2 = 0.9744, ne s'est pas améliorée par rapport à ʻaccuracy1, et il a été constaté que cet "apprentissage d'ensemble" était un échec.

Naturellement, quand vous y réfléchissez, vous êtes dans une situation où vous utilisez le même classificateur dans deux sessions d'apprentissage. Puisque les données d'apprentissage ont été simplement divisées en deux parties et fournies, on ne peut pas s'attendre à ce que cela améliore la précision de l'ensemble.

Le résultat de l'exécution de l'ensemble correct avec la configuration de classificateur indépendant du cas n ° 2 est le suivant.

Training...
  Network No.1 :
  step, loss, accurary =      0: 178.722,   0.470
  step, loss, accurary =   1000:  15.329,   0.990
  step, loss, accurary =   2000:  12.242,   0.990
  step, loss, accurary =   3000:  10.827,   1.000
  step, loss, accurary =   4000:  10.167,   0.990
  step, loss, accurary =   5000:   8.178,   1.000
  Network No.2 :
  step, loss, accurary =      0: 192.382,   0.570
  step, loss, accurary =   1000:  10.037,   0.990
  step, loss, accurary =   2000:   7.590,   1.000
  step, loss, accurary =   3000:   5.855,   1.000
  step, loss, accurary =   4000:   4.678,   1.000
  step, loss, accurary =   5000:   4.693,   1.000
accuracy1 =   0.9751
accuracy2 =   0.9756
accuracy (model averaged) =   0.9810

Comme prévu, la précision (taux de réponse correcte), qui était d'environ 0,975 pour chaque classificateur, est légèrement meilleure à 0,980 selon la moyenne du modèle.

(Le code créé cette fois a été téléchargé sur Gist.)

Cela semble un peu hors des sentiers battus, mais j'espère que vous avez une idée de la façon d'utiliser des portées variables et des variables partagées. Je ne pense pas qu'il soit vraiment nécessaire d'utiliser des portées variables pour gérer les variables dans les modèles plus petits, mais dans les modèles plus grands, vous pouvez utiliser des portées variables et des variables partagées. C'est une fonctionnalité de TensorFlow que l'on ne trouve pas souvent dans d'autres frameworks d'apprentissage profond, alors veuillez l'utiliser!

Recommended Posts

Comprendre l'espace de noms TensorFlow et les variables partagées principales
Sélectionnez les variables requises dans TensorFlow et enregistrez / restaurez
Comprendre la différence entre l'affectation cumulative aux variables et l'affectation cumulative aux objets
Comprendre l'arbre de décision et classer les documents
Vérification des méthodes et des variables à l'aide de la bibliothèque voir
Examiner la relation entre TensorFlow et Keras pendant la période de transition
Comprenez attentivement la distribution exponentielle et dessinez en Python
Visualisez les données et saisissez la corrélation en même temps
Tracer et comprendre la distribution normale multivariée en Python
Comprendre attentivement la distribution de Poisson et dessiner en Python