Section courante

A propos

Section administrative du site

Construire avec bricks

Blocks est un cadre d'application censé faciliter la construction de modèles de réseaux neuronaux complexes basés sur Theano. Pour ce faire, ils ont introduit le concept de «bricks», que vous avez peut-être déjà rencontré dans le tutoriel d'introduction <model_building>.

Cycle de vie des bricks

Blocks utilisent des «bricks» pour construire des modèles. Les bricks sont des opérations Theano paramétrées. Une brick est généralement définie par un ensemble d'attributs et un ensemble de paramètres. Les premiers spécifient les attributs qui définissent le bloc (par exemple, le nombre d'unités d'entrée et de sortie), les seconds représentent les paramètres de l'objet brique variant pendant l'apprentissage (par exemple, les pondérations et les biais).

Le cycle de vie d'une brick est le suivant :

Remarque

Si les variables Theano de l'objet brick n'ont pas été allouées lors de l'appel de :meth:`~.Application.apply`, Blocks appellera discrètement :meth:`~.bricks.Brick.allocate`.

Exemple

Les bricks prennent des variables Theano en entrée et fournissent des variables Theano en sortie :

>>> import theano
>>> from theano import tensor
>>> from blocks.bricks import Tanh
>>> x = tensor.vector('x')
>>> y = Tanh().apply(x)
>>> print(y)
tanh_apply_output
>>> isinstance(y, theano.Variable)
True

Il s'agit clairement d'un exemple artificiel, car cela semble une écriture compliquée de y = tenseur.tanh(x). Pour comprendre l'utilité de Blocks, considérons une tâche très courante lors de la création de réseaux de neurones : appliquer une transformation linéaire (avec biais optionnel) à un vecteur, puis initialiser la matrice de pondération et le vecteur de biais avec des valeurs issues d'une distribution particulière.

>>> from blocks.bricks import Linear
>>> from blocks.initialization import IsotropicGaussian, Constant
>>> linear = Linear(input_dim=10, output_dim=5,
...                 weights_init=IsotropicGaussian(),
...                 biases_init=Constant(0.01))
>>> y = linear.apply(x)

Alors, que s'est-il passé ? Nous avons construit une brique appelée :class:`.Linear` avec une configuration particulière : la dimension d'entrée (10) et la dimension de sortie (5). Lorsque nous avons appelé :attr:`.Linear.apply`, la brick a automatiquement construit les variables Theano partagées nécessaires au stockage de ses paramètres. Dans le cycle de vie d'une brique, on parle d'allocation.

>>> linear.parameters
[W, b]
>>> linear.parameters[1].get_value() # doctest: +SKIP
array([ nan, nan, nan, nan, nan])

Par défaut, tous nos paramètres sont définis sur NaN. Pour les initialiser, appelez simplement la méthode :meth:`~.bricks.Brick.initialize`. Il s'agit de la dernière étape du cycle de vie d'une brick : l'initialisation.

>>> linear.initialize()
>>> linear.parameters[1].get_value() # doctest: +SKIP
array([ 0.01, 0.01, 0.01, 0.01, 0.01])

Gardez à l'esprit qu'en fin de compte, les bricks servent uniquement à construire un graphe de calcul Theano ; il est donc possible d'y intégrer des instructions Theano classiques lors de la construction de modèles. (Cependant, vous risquez de manquer certaines fonctionnalités plus intéressantes des blocs, comme l'annotation de variables.)

>>> z = tensor.max(y + 4)

Initialisation différée

Dans l'exemple ci-dessus, nous avons configuré la brick :class:`.Linear` lors de l'initialisation. Nous avons spécifié les dimensions d'entrée et de sortie, ainsi que la manière dont les matrices de pondération doivent être initialisées. Prenons le cas suivant, assez courant : nous souhaitons utiliser la sortie d'un modèle comme entrée pour un autre modèle, mais les dimensions de sortie et d'entrée ne correspondent pas. Nous devons donc ajouter une transformation linéaire au milieu.

Pour prendre en charge ce cas d'utilisation, les briques permettent l'initialisation différée, activée par défaut. Cela signifie que vous pouvez créer une brique sans la configurer complètement (voire pas du tout) :

>>> linear2 = Linear(output_dim=10)
>>> print(linear2.input_dim)
NoneAllocation

Bien sûr, tant que la brique n'est pas configurée, nous ne pouvons pas réellement l'appliquer !

>>> linear2.apply(x)
Traceback (most recent call last):
 ...
ValueError: allocation config not set: input_dim

Nous pouvons désormais configurer facilement notre brique en fonction d'autres bricks.

>>> linear2.input_dim = linear.output_dim
>>> linear2.apply(x)
linear_apply_output

Dans les exemples précédents, l'allocation des paramètres s'est toujours faite implicitement lors de l'appel des méthodes apply, mais elle peut également être appelée explicitement. Prenons l'exemple suivant :

>>> linear3 = Linear(input_dim=10, output_dim=5)
>>> linear3.parameters
Traceback (most recent call last):
 ...
AttributeError: 'Linear' object has no attribute 'parameters'
>>> linear3.allocate()
>>> linear3.parameters
[W, b]

Bricks imbriquées

De nombreux modèles de réseaux neuronaux, notamment les plus complexes, peuvent être considérés comme des structures hiérarchiques. Même un simple perceptron multicouche est constitué de couches, elles-mêmes constituées d'une transformation linéaire suivie d'une transformation non linéaire.

De ce fait, les bricks peuvent avoir des enfants. Les bricks mères peuvent configurer leurs enfants, par exemple pour garantir la compatibilité de leurs configurations ou pour définir des valeurs par défaut adaptées à un cas d'utilisation particulier.

>>> from blocks.bricks import MLP, Logistic
>>> mlp = MLP(activations=[Logistic(name='sigmoid_0'),
...           Logistic(name='sigmoid_1')], dims=[16, 8, 4],
...           weights_init=IsotropicGaussian(), biases_init=Constant(0.01))
>>> [child.name for child in mlp.children]
['linear_0', 'sigmoid_0', 'linear_1', 'sigmoid_1']
>>> y = mlp.apply(x)
>>> mlp.children[0].input_dim
16

Nous pouvons constater que la brick :class:`~.bricks.MLP` a automatiquement construit deux bricks enfants pour effectuer les transformations linéaires. Lorsque nous avons appliqué la MLP à x, elle a automatiquement configuré les dimensions d'entrée et de sortie de ses briques enfants. De même, lorsque nous avons appelé :meth:`~.bricks.Brick.initialize`, elle a automatiquement transmis la matrice de pondération et la configuration d'initialisation des biais à ses bricks enfants.

>>> mlp.initialize()
>>> mlp.children[0].parameters[0].get_value() # doctest: +SKIP
array([[-0.38312393, -1.7718271 , 0.78074479, -0.74750996],
       ...
       [ 1.32390416, -0.56375355, -0.24268186, -2.06008577]])

Il existe des cas où l'on souhaite modifier la configuration de la brick parente pour ses enfants. Par exemple, on souhaite initialiser les poids de la première couche d'un MLP de manière légèrement différente des autres. Pour ce faire, il est nécessaire d'examiner de plus près le cycle de vie d'une brick. Dans les deux premières sections, nous avons déjà abordé les trois étapes du cycle de vie d'une brick :

Lorsqu'il s'agit d'enfants, le cycle de vie devient un peu plus complexe. (L'intégralité du cycle de vie est documentée dans la classe :class:`~.bricks.Brick`.) Avant d'allouer ou d'initialiser des paramètres, la brique parente appelle ses méthodes :meth:`~.bricks.Brick.push_allocation_config` et :meth:`~.bricks.Brick.push_initialization_config`, configurant les enfants. Si vous souhaitez surcharger la configuration des enfants, vous devrez appeler ces méthodes manuellement, après quoi vous pourrez surcharger la configuration des bricks enfants.

>>> mlp = MLP(activations=[Logistic(name='sigmoid_0'),
...           Logistic(name='sigmoid_1')], dims=[16, 8, 4],
...           weights_init=IsotropicGaussian(), biases_init=Constant(0.01))
>>> y = mlp.apply(x)
>>> mlp.push_initialization_config()
>>> mlp.children[0].weights_init = Constant(0.01)
>>> mlp.initialize()
>>> mlp.children[0].parameters[0].get_value() # doctest: +SKIP
array([[ 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
       ...
       [ 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]])


Dernière mise à jour : Vendredi, le 6 juin 2025