index

Fonctions d’activation : de la 1D à la 2D

La fonction d’activation est une fonction mathématique appliquée à un signal en sortie d’un neurone artificiel. Le terme vient de l’équivalent biologique « potentiel d’activation », seuil de stimulation qui, une fois atteint, entraîne une réponse du neurone. Elle est le plus souvent non linéaire : c’est cette non-linéarité qui permet à un réseau de couches empilées de représenter des relations complexes, plutôt qu’une simple combinaison linéaire des entrées, comme le rappellent Goodfellow, Bengio et Courville dans leur ouvrage de référence sur l’apprentissage profond.

Le choix de la fonction d’activation conditionne fortement le comportement du réseau lors de l’apprentissage, en particulier sa capacité à propager le gradient à travers les couches successives. On présente ici les fonctions les plus couramment utilisées, en suivant la logique historique qui a conduit de l’une à l’autre, d’abord pour un neurone à une seule entrée, puis pour un neurone à deux entrées.

1. En une dimension

La fonction sigmoïde

La fonction sigmoïde a été l’une des premières fonctions utilisées dans les couches cachées des premiers réseaux de neurones. Elle est définie par

σ(x)=11+ex, et de deˊriveˊσ(x)=σ(x)(1σ(x)),\sigma(x) = \frac{1}{1+e^{-x}} \text{, et de dérivée } \sigma'(x) = \sigma(x)(1-\sigma(x)),

et produit une sortie comprise dans l’intervalle [0,1][0,1], ce qui en faisait une interprétation naturelle en termes de probabilité.

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.linspace(-10, 10, 100)
y = sigmoid(x)
dy = sigmoid(x) * (1 - sigmoid(x))

plt.axhline(0, color='black', linewidth=0.8)
plt.axvline(0, color='black', linewidth=0.8)
plt.plot(x, y, 'b-', label=r"$\sigma(x)$")
plt.plot(x, dy, 'r--', label=r"$\sigma'(x)$")
plt.title("Fonction sigmoïde et sa dérivée")
plt.xlabel("x")
plt.legend(loc="upper left")
plt.show()

On observe sur ce graphique que la dérivée σ(x)\sigma'(x) s’écrase rapidement vers zéro dès que x|x| dépasse 4 ou 5. C’est ce phénomène, le problème du gradient qui s’annule, que Glorot et Bengio ont mis en évidence expérimentalement : la sortie de la sigmoïde n’étant pas centrée en zéro, sa moyenne non nulle peut conduire les couches supérieures à saturer dès les premières étapes de l’apprentissage, en particulier avec une initialisation aléatoire standard des poids. Les couches éloignées de la sortie reçoivent alors un gradient de plus en plus faible au fil de la rétropropagation, et finissent par ne presque plus apprendre.

La tangente hyperbolique

Pour corriger en partie ce défaut, on a introduit la tangente hyperbolique, qui partage la même forme en S mais recentre la sortie autour de zéro :

tanh(x)=exexex+ex, et de deˊriveˊtanh(x)=1tanh2(x).\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} \text{, et de dérivée } \tanh'(x) = 1 - \tanh^2(x).

La sortie appartient cette fois à l’intervalle [1,1][-1,1]. Ce recentrage facilite généralement la convergence, car les activations en sortie d’une couche ne sont plus systématiquement positives, ce qui évite le biais de saturation décrit par Glorot et Bengio pour la sigmoïde.

import numpy as np
import matplotlib.pyplot as plt

def tanh(x):
    return np.tanh(x)

x = np.linspace(-10, 10, 100)
y = tanh(x)
dy = 1 - tanh(x)**2

plt.axhline(0, color='black', linewidth=0.8)
plt.axvline(0, color='black', linewidth=0.8)
plt.plot(x, y, 'b-', label=r"$\tanh(x)$")
plt.plot(x, dy, 'r--', label=r"$\tanh'(x)$")
plt.title("Tangente hyperbolique et sa dérivée")
plt.xlabel("x")
plt.legend(loc="upper left")
plt.show()

Le graphique de tanh(x)\tanh'(x) a la même allure que celui de σ(x)\sigma'(x) : la dérivée reste proche de zéro dès que x|x| s’éloigne de l’origine. Le problème de saturation n’est donc pas résolu par tanh, seulement atténué par le recentrage en zéro. C’est ce constat qui a motivé l’adoption progressive de la fonction ReLU dans les réseaux profonds modernes.

ReLU

La fonction ReLU (rectified linear unit) abandonne la forme en S au profit d’une fonction linéaire par morceaux :

ReLU(x)=max(0,x), et de deˊriveˊReLU(x)={1si x>0,0si x<0.\mathrm{ReLU}(x) = \max(0,x) \text{, et de dérivée } \mathrm{ReLU}'(x) = \begin{cases} 1 & \text{si } x > 0, \\ 0 & \text{si } x < 0. \end{cases}

Tant que xx est positif, la dérivée vaut exactement 11, sans aucune saturation : le gradient se propage donc sans atténuation à travers les couches actives. Nair et Hinton ont montré que ces unités, contrairement aux unités binaires utilisées jusque-là dans les machines de Boltzmann restreintes, préservent l’information sur les intensités relatives au fil des couches successives, et permettent d’entraîner des réseaux profonds efficacement sans recourir au pré-entraînement non supervisé jusqu’alors jugé nécessaire. La dérivée n’est pas définie en x=0x=0, mais on lui attribue en pratique la valeur 00 ou 11, sans incidence notable sur l’apprentissage.

import numpy as np
import matplotlib.pyplot as plt

def relu(x):
    return np.maximum(0, x)

def relu_derivative(x):
    return np.where(x > 0, 1, 0)

x_zoom = np.linspace(-3, 3, 200)
y = relu(x_zoom)
dy = relu_derivative(x_zoom)

plt.axhline(0, color='black', linewidth=0.8)
plt.axvline(0, color='black', linewidth=0.8)
plt.plot(x_zoom, y, 'b-', label=r"$\mathrm{ReLU}(x)$")
plt.plot(x_zoom, dy, 'r--', label=r"$\mathrm{ReLU}'(x)$")
plt.title("ReLU et sa dérivée")
plt.xlabel("x")
plt.legend(loc="upper left")
plt.show()

On voit nettement sur ce graphique le coude en x=0x=0 : la dérivée bascule instantanément de 00 à 11, sans la zone de saturation progressive observée pour sigmoïde et tanh. Cette simplicité a toutefois une contrepartie : pour x<0x<0, le gradient est exactement nul, et un neurone qui se retrouve durablement dans cette zone n’est plus mis à jour. On parle alors de neurone mort.

Leaky ReLU

Pour éviter qu’un gradient nul n’arrête complètement l’apprentissage d’un neurone, on utilise parfois la version dite leaky ReLU, qui conserve une pente faible mais non nulle pour les entrées négatives :

f(x)={xsi x>0,αxsi x0,, et de deˊriveˊf(x)={1si x>0,αsi x0,f(x) = \begin{cases} x & \text{si } x > 0, \\ \alpha x & \text{si } x \le 0, \end{cases} \text{, et de dérivée } f'(x) = \begin{cases} 1 & \text{si } x > 0, \\ \alpha & \text{si } x \le 0, \end{cases}

α\alpha est un coefficient petit, typiquement 0,010{,}01.

import numpy as np
import matplotlib.pyplot as plt

alpha = 0.1  # valeur agrandie pour la lisibilité du graphique (0.01 en pratique)

def leaky_relu(x):
    return np.where(x > 0, x, alpha * x)

def leaky_relu_derivative(x):
    return np.where(x > 0, 1, alpha)

x_zoom = np.linspace(-3, 3, 200)
y = leaky_relu(x_zoom)
dy = leaky_relu_derivative(x_zoom)

plt.axhline(0, color='black', linewidth=0.8)
plt.axvline(0, color='black', linewidth=0.8)
plt.plot(x_zoom, y, 'b-', label=r"$f(x)$")
plt.plot(x_zoom, dy, 'r--', label=r"$f'(x)$")
plt.title("Leaky ReLU et sa dérivée (alpha=0,1 pour la lisibilité)")
plt.xlabel("x")
plt.legend(loc="upper left")
plt.show()

Le coefficient α\alpha a ici été agrandi à 0,10{,}1 uniquement pour que la pente reste visible sur le graphique ; en pratique on retient une valeur bien plus faible, autour de 0,010{,}01, pour rester proche du comportement de ReLU tout en évitant les neurones morts. On distingue bien la dérivée qui ne s’annule plus pour x<0x<0, contrairement à ReLU.

2. En deux dimensions

Toutes les fonctions précédentes s’appliquent à un neurone à une seule entrée xx. Mais un neurone reçoit en réalité une combinaison linéaire de plusieurs entrées : z=w1x1+w2x2++bz = w_1 x_1 + w_2 x_2 + \dots + b, avant que l’activation ne s’applique à ce scalaire zz. Pour visualiser cela, prenons le cas le plus simple d’un neurone à deux entrées xx et yy, avec des poids égaux à 11 et un biais nul : z=x+yz = x + y. La surface f(x+y)f(x+y) montre alors comment l’activation transforme tout le plan des entrées, pas seulement une droite.

Ces graphiques sont interactifs : cliquez-glissez pour les faire pivoter.

Sigmoïde en deux dimensions

La coupe en S de la sigmoïde devient une marche continue : plate et proche de 00 loin dans la direction (x+y)-(x+y), plate et proche de 11 loin dans la direction (x+y)(x+y), avec une transition douce le long de la droite x+y=0x+y=0.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-6, 6, 100)
y = np.linspace(-6, 6, 100)
X, Y = np.meshgrid(x, y)
Z = 1 / (1 + np.exp(-(X + Y)))

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='sigmoïde(x+y)')
ax.set_title('Sigmoïde en 2 dimensions : $\\sigma(x+y)$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Tangente hyperbolique en deux dimensions

Même principe, mais la transition se fait cette fois entre 1-1 et 11 plutôt qu’entre 00 et 11 : la surface est symétrique par rapport à l’origine.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-2, 2, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z = np.tanh(X + Y)

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='tanh(x+y)')
ax.set_title('Tangente hyperbolique en 2 dimensions : $\\tanh(x+y)$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

ReLU en deux dimensions

La cassure nette de ReLU en x=0x=0 devient une arête : la surface reste exactement plate (à 00) sur toute la moitié du plan où x+y0x+y \le 0, puis remonte linéairement de l’autre côté. C’est précisément cette arête, reproduite à chaque neurone d’une couche, qui permet à un réseau ReLU d’approximer des fonctions par morceaux linéaires de plus en plus fines à mesure que les couches s’empilent.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.maximum(0, X + Y)

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='plasma', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='ReLU(x+y)')
ax.set_title('ReLU en 2 dimensions : $\\mathrm{ReLU}(x+y)$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Leaky ReLU en deux dimensions

La même arête que ReLU, mais la moitié du plan où x+y0x+y \le 0 n’est plus totalement plate : elle descend légèrement, avec la même pente α\alpha que dans le cas 1D. Aucun point de la surface n’a de gradient nul, contrairement à ReLU.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.where(X + Y > 0, X + Y, 0.1 * (X + Y))

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='magma', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='leaky ReLU(x+y)')
ax.set_title('Leaky ReLU en 2 dimensions')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Conclusion

Le choix d’une fonction d’activation se résume à un compromis entre deux écueils : la saturation du gradient, qui touche sigmoïde et tanh dès que x|x| s’éloigne de l’origine, et l’extinction du gradient, qui touche ReLU pour les entrées négatives. Les couches cachées des réseaux profonds privilégient aujourd’hui ReLU ou ses variantes pour cette raison, tandis que sigmoïde reste réservée à la couche de sortie pour les problèmes de classification binaire, où son interprétation probabiliste reste utile. Les surfaces en deux dimensions rendent ce compromis visible directement dans la géométrie : les zones plates correspondent aux zones de gradient nul ou quasi nul, et c’est leur étendue qui distingue les quatre fonctions.

Bibliographie

Goodfellow, I., Bengio, Y. et Courville, A. (2016). Deep Learning. MIT Press.

Glorot, X. et Bengio, Y. (2010). Understanding the difficulty of training deep feedforward neural networks. Proceedings of the 13th International Conference on Artificial Intelligence and Statistics (AISTATS), PMLR 9

.

Nair, V. et Hinton, G. E. (2010). Rectified Linear Units Improve Restricted Boltzmann Machines. Proceedings of the 27th International Conference on Machine Learning (ICML-10), pp. 807-814.

Glorot, X., Bordes, A. et Bengio, Y. (2011). Deep Sparse Rectifier Neural Networks. Proceedings of the 14th International Conference on Artificial Intelligence and Statistics (AISTATS), pp. 315-323.