Concernant le tutoriel MNIST sur le site Web de TensorFlow, j'ai mené une expérience ( Gist ) pour voir quel serait l'effet des différentes initialisations de poids sur l'apprentissage. J'ai remarqué que, contrairement à ce que j'ai lu dans le populaire [Xavier, Glorot 2010] , l'apprentissage est très bien indépendamment de l'initialisation du poids.
Les différentes courbes représentent différentes valeurs de w
pour initialiser les poids des couches convolutionnelles et entièrement connectées. Notez que toutes les valeurs de w
fonctionnent correctement, même si 0.3
et 1.0
finissent à des performances inférieures et certaines valeurs s'entraînent plus rapidement - en particulier, 0.03
et 0.1
sont les plus rapides. Néanmoins, l'intrigue montre une gamme assez large de w
qui fonctionne, suggérant une "robustesse" w.r.t. initialisation du poids.
def weight_variable(shape, w=0.1):
initial = tf.truncated_normal(shape, stddev=w)
return tf.Variable(initial)
def bias_variable(shape, w=0.1):
initial = tf.constant(w, shape=shape)
return tf.Variable(initial)
Question : Pourquoi ce réseau ne souffre-t-il pas du problème de gradient qui disparaît ou explose?
Je vous suggère de lire le Gist pour les détails d'implémentation, mais voici le code de référence. Cela a pris environ une heure sur mon Nvidia 960m, bien que j'imagine qu'il pourrait également fonctionner sur un processeur dans un délai raisonnable.
import time
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
from tensorflow.python.client import device_lib
import numpy
import matplotlib.pyplot as pyplot
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
# Weight initialization
def weight_variable(shape, w=0.1):
initial = tf.truncated_normal(shape, stddev=w)
return tf.Variable(initial)
def bias_variable(shape, w=0.1):
initial = tf.constant(w, shape=shape)
return tf.Variable(initial)
# Network architecture
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def build_network_for_weight_initialization(w):
""" Builds a CNN for the MNIST-problem:
- 32 5x5 kernels convolutional layer with bias and ReLU activations
- 2x2 maxpooling
- 64 5x5 kernels convolutional layer with bias and ReLU activations
- 2x2 maxpooling
- Fully connected layer with 1024 nodes + bias and ReLU activations
- dropout
- Fully connected softmax layer for classification (of 10 classes)
Returns the x, and y placeholders for the train data, the output
of the network and the dropbout placeholder as a Tuple of 4 elements.
"""
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
x_image = tf.reshape(x, [-1,28,28,1])
W_conv1 = weight_variable([5, 5, 1, 32], w)
b_conv1 = bias_variable([32], w)
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
W_conv2 = weight_variable([5, 5, 32, 64], w)
b_conv2 = bias_variable([64], w)
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
W_fc1 = weight_variable([7 * 7 * 64, 1024], w)
b_fc1 = bias_variable([1024], w)
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
W_fc2 = weight_variable([1024, 10], w)
b_fc2 = bias_variable([10], w)
y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
return (x, y_, y_conv, keep_prob)
# Experiment
def evaluate_for_weight_init(w):
""" Returns an accuracy learning curve for a network trained on
10000 batches of 50 samples. The learning curve has one item
every 100 batches."""
with tf.Session() as sess:
x, y_, y_conv, keep_prob = build_network_for_weight_initialization(w)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
sess.run(tf.global_variables_initializer())
lr = []
for _ in range(100):
for i in range(100):
batch = mnist.train.next_batch(50)
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
assert mnist.test.images.shape[0] == 10000
# This way the accuracy-evaluation fits in my 2GB laptop GPU.
a = sum(
accuracy.eval(feed_dict={
x: mnist.test.images[2000*i:2000*(i+1)],
y_: mnist.test.labels[2000*i:2000*(i+1)],
keep_prob: 1.0})
for i in range(5)) / 5
lr.append(a)
return lr
ws = [0.0001, 0.0003, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0]
accuracies = [
[evaluate_for_weight_init(w) for w in ws]
for _ in range(3)
]
# Plotting results
pyplot.plot(numpy.array(accuracies).mean(0).T)
pyplot.ylim(0.9, 1)
pyplot.xlim(0,140)
pyplot.xlabel('batch (x 100)')
pyplot.ylabel('test accuracy')
pyplot.legend(ws)
Les stratégies d'initialisation du poids peuvent être une étape importante et souvent négligée dans l'amélioration de votre modèle, et comme c'est maintenant le meilleur résultat sur Google, je pensais que cela pourrait justifier une réponse plus détaillée.
En général, le produit total du gradient de la fonction d'activation de chaque couche, le nombre de connexions entrantes/sortantes (fan_in/fan_out) et la variance des poids doivent être égaux à un. De cette façon, pendant que vous rétropropagerez à travers le réseau, la variance entre les gradients d'entrée et de sortie restera cohérente, et vous ne souffrirez pas d'explosion ou de disparition des gradients. Même si ReLU est plus résistant aux explosions/évanouissements, vous pouvez toujours rencontrer des problèmes.
tf.truncated_normal utilisé par OP effectue une initialisation aléatoire qui encourage les poids à être mis à jour "différemment", mais ne pas tient compte de la stratégie d'optimisation ci-dessus. Sur des réseaux plus petits, ce n'est peut-être pas un problème, mais si vous voulez des réseaux plus profonds ou des temps de formation plus rapides, il vaut mieux essayer une stratégie d'initialisation de poids basée sur des recherches récentes.
Pour les poids précédant une fonction ReLU, vous pouvez utiliser les paramètres par défaut de:
tf.contrib.layers.variance_scaling_initializer
pour les couches activées par tanh/sigmoïde, "xavier" pourrait être plus approprié:
tf.contrib.layers.xavier_initializer
Plus de détails sur ces fonctions et les articles associés peuvent être trouvés sur: https://www.tensorflow.org/versions/r0.12/api_docs/python/contrib.layers/initializers
Au-delà des stratégies d'initialisation du poids, une optimisation supplémentaire pourrait explorer la normalisation des lots: https://www.tensorflow.org/api_docs/python/tf/nn/batch_normalization
Les fonctions logistiques sont plus sujettes à la disparition du gradient, car leurs gradients sont tous <1, donc plus vous les multipliez lors de la rétropropagation, plus votre gradient devient petit (et assez rapidement), tandis que RelU a un gradient de 1 sur le positif partie, donc il n'a pas ce problème.
De plus, votre réseau n'est pas du tout assez profond pour en souffrir.