# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Explanation: Copyright 2017 Google LLC.
End of explanation
"""


from __future__ import print_function

import tensorflow as tf

"""
Explanation: # Créer et manipuler des Tensors
Objectifs de formation :
  * Initialiser et affecter des objets Variable TensorFlow
  * Créer et manipuler des Tensors
  * Rafraîchir ses connaissances sur les opérations de somme et de produit en algèbre linéaire (lecture conseillée de l'introduction à l'addition et au produit matriciels, si ces notions vous sont inconnues)
  * Se familiariser avec les opérations mathématiques et de tableau basiques dans TensorFlow
End of explanation
"""


with tf.Graph().as_default():
  # Create a six-element vector (1-D tensor).
  primes = tf.constant([2, 3, 5, 7, 11, 13], dtype=tf.int32)

  # Create another six-element vector. Each element in the vector will be
  # initialized to 1. The first argument is the shape of the tensor (more
  # on shapes below).
  ones = tf.ones([6], dtype=tf.int32)

  # Add the two vectors. The resulting tensor is a six-element vector.
  just_beyond_primes = tf.add(primes, ones)

  # Create a session to run the default graph.
  with tf.Session() as sess:
    print(just_beyond_primes.eval())

"""
Explanation: ## Somme vectorielle
Vous pouvez réaliser de nombreuses opérations mathématiques standards sur les Tensors (reportez-vous à l'API TensorFlow). Le code suivant permet de créer et de manipuler deux vecteurs (Tensors à une dimension), constitués chacun de six éléments :
End of explanation
"""


with tf.Graph().as_default():
  # A scalar (0-D tensor).
  scalar = tf.zeros([])

  # A vector with 3 elements.
  vector = tf.zeros([3])

  # A matrix with 2 rows and 3 columns.
  matrix = tf.zeros([2, 3])

  with tf.Session() as sess:
    print('scalar has shape', scalar.get_shape(), 'and value:\n', scalar.eval())
    print('vector has shape', vector.get_shape(), 'and value:\n', vector.eval())
    print('matrix has shape', matrix.get_shape(), 'and value:\n', matrix.eval())


"""
Explanation: ### Formats de Tensor
Le format caractérise la taille et le nombre de dimensions d'un Tensor. Il est indiqué sous la forme d'une liste, où le ie élément désigne la taille par rapport à la dimension i. La longueur de la liste indique le rang du Tensor (c'est-à-dire le nombre de dimensions).
Pour en savoir plus, consultez la documentation TensorFlow.
Quelques exemples basiques :
End of explanation
"""


with tf.Graph().as_default():
  # Create a six-element vector (1-D tensor).
  primes = tf.constant([2, 3, 5, 7, 11, 13], dtype=tf.int32)

  # Create a constant scalar with value 1.
  ones = tf.constant(1, dtype=tf.int32)

  # Add the two tensors. The resulting tensor is a six-element vector.
  just_beyond_primes = tf.add(primes, ones)

  with tf.Session() as sess:
    print(just_beyond_primes.eval())

"""
Explanation: ### Broadcasting
En mathématiques, les Tensors de format identique peuvent subir uniquement des opérations au niveau de l'élément (opérations ajouter et égal, par exemple). Dans TensorFlow, en revanche, il est possible de réaliser des opérations traditionnellement incompatibles. ce modèle autorise ainsi le broadcasting (un concept emprunté à Numpy), qui permet d'agrandir un petit tableau pour qu'il prenne le même format que le grand tableau. Exemples de possibilités offertes par le broadcasting :

Si une opération exige un Tensor de taille [6], un Tensor de taille [1] ou [] peut être utilisé comme opérande.
Si une opération exige un Tensor de taille [4, 6], vous pouvez utiliser comme opérande l'une des tailles de Tensor suivantes :
[1, 6]
[6]
[]

Si une opération exige un Tensor de taille [3, 5, 6], vous pouvez utiliser comme opérande l'une des tailles de Tensor suivantes :


[1, 5, 6]

[3, 1, 6]
[3, 5, 1]
[1, 1, 1]
[5, 6]
[1, 6]
[6]
[1]
[]

REMARQUE : Lorsqu'un Tensor est broadcasté, ses entrées sont copiées de manière conceptuelle. (Elles ne sont pas réellement copiées, pour des raisons liées aux performances. Le broadcasting a été conçu comme un outil d'optimisation des performances.)
La documentation sur le broadcasting Numpy, qui se veut facile d'accès, fournit une description détaillée de l'ensemble de règles de broadcasting.
Le code suivant reprend l'opération de somme de Tensors précédente, cette fois avec le broadcasting :
End of explanation
"""


with tf.Graph().as_default():
  # Create a matrix (2-d tensor) with 3 rows and 4 columns.
  x = tf.constant([[5, 2, 4, 3], [5, 1, 6, -2], [-1, 3, -1, -2]],
                  dtype=tf.int32)

  # Create a matrix with 4 rows and 2 columns.
  y = tf.constant([[2, 2], [3, 5], [4, 5], [1, 6]], dtype=tf.int32)

  # Multiply `x` by `y`. 
  # The resulting matrix will have 3 rows and 2 columns.
  matrix_multiply_result = tf.matmul(x, y)

  with tf.Session() as sess:
    print(matrix_multiply_result.eval())

"""
Explanation: ## Produit matriciel
En algèbre linéaire, lorsque vous calculez le produit de deux matrices, le nombre de colonnes dans la première doit être égal au nombre de lignes dans la seconde.

Une matrice 3x4 peut être multipliée par une matrice 4x2. Vous obtiendrez une matrice 3x2.
Une matrice 4x2 ne peut pas être multipliée par une matrice 3x4.
End of explanation
"""


with tf.Graph().as_default():
  # Create an 8x2 matrix (2-D tensor).
  matrix = tf.constant([[1,2], [3,4], [5,6], [7,8],
                        [9,10], [11,12], [13, 14], [15,16]], dtype=tf.int32)

  # Reshape the 8x2 matrix into a 2x8 matrix.
  reshaped_2x8_matrix = tf.reshape(matrix, [2,8])
  
  # Reshape the 8x2 matrix into a 4x4 matrix
  reshaped_4x4_matrix = tf.reshape(matrix, [4,4])

  with tf.Session() as sess:
    print("Original matrix (8x2):")
    print(matrix.eval())
    print("Reshaped matrix (2x8):")
    print(reshaped_2x8_matrix.eval())
    print("Reshaped matrix (4x4):")
    print(reshaped_4x4_matrix.eval())

"""
Explanation: ## Modification du format des Tensors
La somme de Tensors et le produit matriciel sont deux opérations qui imposent des contraintes spécifiques aux opérandes, obligeant ainsi les programmeurs TensorFlow à modifier régulièrement le format des Tensors. 
La méthode tf.reshape permet de modifier le format d'un Tensor. 
Ainsi, un Tensor 8x2 peut être converti en Tensor 2x8 ou 4x4 :
End of explanation
"""


with tf.Graph().as_default():
  # Create an 8x2 matrix (2-D tensor).
  matrix = tf.constant([[1,2], [3,4], [5,6], [7,8],
                        [9,10], [11,12], [13, 14], [15,16]], dtype=tf.int32)

  # Reshape the 8x2 matrix into a 3-D 2x2x4 tensor.
  reshaped_2x2x4_tensor = tf.reshape(matrix, [2,2,4])
  
  # Reshape the 8x2 matrix into a 1-D 16-element tensor.
  one_dimensional_vector = tf.reshape(matrix, [16])

  with tf.Session() as sess:
    print("Original matrix (8x2):")
    print(matrix.eval())
    print("Reshaped 3-D tensor (2x2x4):")
    print(reshaped_2x2x4_tensor.eval())
    print("1-D vector:")
    print(one_dimensional_vector.eval())

"""
Explanation: Vous pouvez également utiliser tf.reshape pour modifier le nombre de dimensions (le \'rang\') d'un Tensor.
Par exemple, le même Tensor 8x2 peut être converti en Tensor 2x2x4 à trois dimensions ou en Tensor une dimension de 16 éléments.
End of explanation
"""


  # Write your code for Task 1 here.

"""
Explanation: ### Exercice n° 1 : Modifier le format de deux Tensors pour les multiplier
L'opération de produit matriciel est impossible sur les deux vecteurs suivants :

a = tf.constant([5, 3, 2, 7, 1, 4])
b = tf.constant([4, 6, 3])

Modifiez leur format pour les convertir en opérandes compatibles avec l'opération de produit matriciel.
Réalisez ensuite cette opération sur les Tensors ainsi modifiés.
End of explanation
"""


with tf.Graph().as_default(), tf.Session() as sess:
  # Task: Reshape two tensors in order to multiply them
  
  # Here are the original operands, which are incompatible
  # for matrix multiplication:
  a = tf.constant([5, 3, 2, 7, 1, 4])
  b = tf.constant([4, 6, 3])
  # We need to reshape at least one of these operands so that
  # the number of columns in the first operand equals the number
  # of rows in the second operand.

  # Reshape vector "a" into a 2-D 2x3 matrix:
  reshaped_a = tf.reshape(a, [2,3])

  # Reshape vector "b" into a 2-D 3x1 matrix:
  reshaped_b = tf.reshape(b, [3,1])

  # The number of columns in the first matrix now equals
  # the number of rows in the second matrix. Therefore, you
  # can matrix mutiply the two operands.
  c = tf.matmul(reshaped_a, reshaped_b)
  print(c.eval())

  # An alternate approach: [6,1] x [1, 3] -> [6,3]

"""
Explanation: ### Solution
Cliquez ci-dessous pour afficher la solution.
End of explanation
"""


g = tf.Graph()
with g.as_default():
  # Create a variable with the initial value 3.
  v = tf.Variable([3])

  # Create a variable of shape [1], with a random initial value,
  # sampled from a normal distribution with mean 1 and standard deviation 0.35.
  w = tf.Variable(tf.random_normal([1], mean=1.0, stddev=0.35))

"""
Explanation: ## Variables, initialisation et affectation
Les opérations réalisées jusqu'à maintenant portaient uniquement sur des valeurs statiques (tf.constant). L'appel de la méthode eval() renvoyait systématiquement le même résultat. Avec TensorFlow, vous pouvez définir des objets Variable, dont la valeur peut changer. 
Lors de la création d'une variable, vous avez le choix entre définir explicitement sa valeur initiale ou utiliser un initialiseur (comme pour une distribution) :
End of explanation
"""


with g.as_default():
  with tf.Session() as sess:
    try:
      v.eval()
    except tf.errors.FailedPreconditionError as e:
      print("Caught expected error: ", e)

"""
Explanation: L'une des particularités de TensorFlow est que l'initialisation des variables n'est pas automatique. Ainsi, le bloc suivant renverra une erreur :
End of explanation
"""


with g.as_default():
  with tf.Session() as sess:
    initialization = tf.global_variables_initializer()
    sess.run(initialization)
    # Now, variables can be accessed normally, and have values assigned to them.
    print(v.eval())
    print(w.eval())


"""
Explanation: Le plus simple pour initialiser une variable consiste à appeler global_variables_initializer. La méthode Session.run() employée ici équivaut à eval(), à peu de chose près.
End of explanation
"""


with g.as_default():
  with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    # These three prints will print the same value.
    print(w.eval())
    print(w.eval())
    print(w.eval())

"""
Explanation: Une fois initialisées, les variables conservent leur valeur pour toute la session (il convient de les réinitialiser au démarrage d'une nouvelle session) :
End of explanation
"""


with g.as_default():
  with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    # This should print the variable's initial value.
    print(v.eval())

    assignment = tf.assign(v, [7])
    # The variable has not been changed yet!
    print(v.eval())

    # Execute the assignment op.
    sess.run(assignment)
    # Now the variable is updated.
    print(v.eval())

"""
Explanation: Pour modifier la valeur d'une variable, utilisez l'opération assign. Créer simplement cette opération n'a aucun effet. Comme pour l'initialisation, vous devez exécuter l'opération d'affectation (via run) pour pouvoir mettre à jour la valeur de la variable :
End of explanation
"""


# Write your code for Task 2 here.

"""
Explanation: Chargement, stockage… les thématiques autour des variables ne manquent pas. Pour en savoir plus sur un sujet non abordé dans cette formation, consultez la documentation TensorFlow.
### Exercice n° 2 : Simuler 10 lancers de deux dés
Simulez un lancer de dés, qui génère un Tensor 10x3 à deux dimensions :

Les colonnes 1 et 2 enregistrent un lancer de chaque dé.
La colonne 3 contient la somme des colonnes 1 et 2, sur la même ligne.

Exemple de valeurs sur la première ligne :

Colonne 1 : 4
Colonne 2 : 3
Colonne 3 : 7

Pour effectuer cet exercice, nous vous invitons à consulter la documentation TensorFlow.
End of explanation
"""


with tf.Graph().as_default(), tf.Session() as sess:
  # Task 2: Simulate 10 throws of two dice. Store the results
  # in a 10x3 matrix.

  # We're going to place dice throws inside two separate
  # 10x1 matrices. We could have placed dice throws inside
  # a single 10x2 matrix, but adding different columns of
  # the same matrix is tricky. We also could have placed
  # dice throws inside two 1-D tensors (vectors); doing so
  # would require transposing the result.
  dice1 = tf.Variable(tf.random_uniform([10, 1],
                                        minval=1, maxval=7,
                                        dtype=tf.int32))
  dice2 = tf.Variable(tf.random_uniform([10, 1],
                                        minval=1, maxval=7,
                                        dtype=tf.int32))

  # We may add dice1 and dice2 since they share the same shape
  # and size.
  dice_sum = tf.add(dice1, dice2)

  # We've got three separate 10x1 matrices. To produce a single
  # 10x3 matrix, we'll concatenate them along dimension 1.
  resulting_matrix = tf.concat(
      values=[dice1, dice2, dice_sum], axis=1)

  # The variables haven't been initialized within the graph yet,
  # so let's remedy that.
  sess.run(tf.global_variables_initializer())

  print(resulting_matrix.eval())

"""
Explanation: ### Solution
Cliquez ci-dessous pour afficher la solution.
End of explanation
"""


