Fonctions d’activation : de la 1D à la 3D
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
et produit une sortie comprise dans l’intervalle , ce qui en faisait une interprétation naturelle en termes de probabilité.
Voir le code
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 s’écrase rapidement vers zéro dès que 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 :
La sortie appartient cette fois à l’intervalle . 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.
Voir le code
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 a la même allure que celui de : la dérivée reste proche de zéro dès que 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 :
Tant que est positif, la dérivée vaut exactement , 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 , mais on lui attribue en pratique la valeur ou , sans incidence notable sur l’apprentissage.
Voir le code
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 : la dérivée bascule instantanément de à , sans la zone de saturation progressive observée pour sigmoïde et tanh. Cette simplicité a toutefois une contrepartie : pour , 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 :
où est un coefficient petit, typiquement .
Voir le code
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 a ici été agrandi à uniquement pour que la pente reste visible sur le graphique ; en pratique on retient une valeur bien plus faible, autour de , 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 , contrairement à ReLU.
GELU
La fonction GELU (Gaussian Error Linear Unit) a été introduite par Hendrycks et Gimpel, qui la définissent comme le produit de l’entrée par la fonction de répartition de la loi normale centrée réduite :
Voir le code
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
def gelu(x):
return x * norm.cdf(x)
def gelu_derivative(x):
return norm.cdf(x) + x * norm.pdf(x)
x = np.linspace(-3, 3, 100)
y = gelu(x)
dy = gelu_derivative(x)
plt.axhline(0, color='black', linewidth=0.8)
plt.axvline(0, color='black', linewidth=0.8)
plt.plot(x, y, 'b-', label=r"$\mathrm{GELU}(x)$")
plt.plot(x, dy, 'r--', label=r"$\mathrm{GELU}'(x)$")
plt.title("GELU et sa dérivée")
plt.xlabel("x")
plt.legend(loc="upper left")
plt.show()
Les auteurs présentent cette construction comme une alternative à ReLU qui ne coupe pas l’entrée selon son signe mais la pondère selon sa valeur, en partant d’un régularisateur stochastique qui applique aléatoirement à l’entrée d’un neurone soit l’identité, soit l’application nulle, et en montrant que cette pondération se fait par la magnitude de l’entrée plutôt que par son seul signe comme le fait ReLU. Pour très positif, et GELU se confond avec l’identité ; pour très négatif, et GELU s’annule, comme ReLU. La différence avec ReLU est sa régularité : GELU est dérivable partout, y compris en , et n’écrase pas brutalement les entrées légèrement négatives.
2. En trois dimensions
Toutes les fonctions précédentes s’appliquent à un neurone à une seule entrée . Mais un neurone reçoit en réalité une combinaison linéaire de plusieurs entrées : , avant que l’activation ne s’applique à ce scalaire . Pour visualiser cela, prenons le cas le plus simple d’un neurone à deux entrées et , avec des poids égaux à et un biais nul : . La surface , tracée ci-dessous en trois dimensions (, et la sortie de l’activation), 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 trois dimensions
La coupe en S de la sigmoïde devient une marche continue : plate et proche de loin dans la direction , plate et proche de loin dans la direction , avec une transition douce le long de la droite .
Voir le code
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 trois dimensions
Même principe, mais la transition se fait cette fois entre et plutôt qu’entre et : la surface est symétrique par rapport à l’origine.
Voir le code
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 trois dimensions
La cassure nette de ReLU en devient une arête : la surface reste exactement plate (à ) sur toute la moitié du plan où , 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.
Voir le code
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 trois dimensions
La même arête que ReLU, mais la moitié du plan où n’est plus totalement plate : elle descend légèrement, avec la même pente que dans le cas 1D. Aucun point de la surface n’a de gradient nul, contrairement à ReLU.
Voir le code
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()
GELU en trois dimensions
L’arête vive de ReLU est remplacée par un pli arrondi : la surface s’annule progressivement du côté et rejoint l’identité de l’autre côté, sans cassure. On distingue aussi le léger creux sous zéro autour de la transition, caractéristique de GELU, impossible avec ReLU dont la sortie est toujours positive ou nulle.
Voir le code
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
x = np.linspace(-4, 4, 100)
y = np.linspace(-4, 4, 100)
X, Y = np.meshgrid(x, y)
Z = (X + Y) * norm.cdf(X + Y)
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='cividis', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='GELU(x+y)')
ax.set_title('GELU en 2 dimensions : $\\mathrm{GELU}(x+y)$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()
3. Propriétés mathématiques et propriétés empiriques
Cette distinction sépare deux façons de juger une fonction d’activation. Les propriétés mathématiques se démontrent directement sur la formule : c’est ce qu’on a fait pour sigmoïde, tanh et ReLU dans les sections précédentes, et une étude récente conduit la même analyse pour GELU en examinant sa dérivabilité, son caractère borné, ses points stationnaires et sa régularité par une investigation mathématique rigoureuse portant sur ces propriétés.
Les propriétés empiriques, en revanche, ne se déduisent pas de la formule : elles s’observent en entraînant effectivement des réseaux et en comparant leurs performances. Hendrycks et Gimpel ont ainsi comparé GELU à ReLU et à ELU sur plusieurs tâches, et ont constaté des améliorations de performance pour l’ensemble des tâches de vision par ordinateur, de traitement du langage naturel et de reconnaissance vocale considérées. Cet écart entre la théorie et l’expérience est net : rien dans la formule de GELU ne garantit a priori sa supériorité sur ReLU, et c’est uniquement l’évaluation empirique qui l’établit. C’est cette performance empirique qui explique l’adoption de GELU dans des architectures comme BERT ou les transformeurs de vision, où elle est devenue une non-linéarité largement répandue en raison de ses performances supérieures à celles d’autres fonctions comme ReLU.
Le même écart entre garantie mathématique et observation empirique apparaît déjà avec sigmoïde et tanh. Glorot et Bengio ont montré expérimentalement que l’activation sigmoïde, à cause de sa valeur moyenne non centrée en zéro, est mal adaptée aux réseaux profonds avec initialisation aléatoire, car sa moyenne peut conduire la couche cachée supérieure à saturer. Or rien dans la seule formule de la sigmoïde n’impose ce comportement : c’est l’observation de réseaux réellement entraînés, couche par couche, qui a révélé le phénomène et conduit à la recommandation de fonctions moins saturantes pour les architectures profondes.
4. Choix de la fonction d’activation selon les couches
Le choix d’une fonction d’activation n’est pas le même selon la position de la couche dans le réseau.
Dans les couches cachées, ReLU et ses variantes (Leaky ReLU, GELU) sont aujourd’hui privilégiées pour leur résistance à la saturation du gradient. Glorot, Bordes et Bengio ont notamment montré que les réseaux à base de rectifieurs (ReLU) peuvent s’entraîner efficacement même sans les techniques de pré-entraînement non supervisé jusque-là jugées nécessaires pour les réseaux profonds, grâce à la sparsité induite par cette fonction. Sigmoïde et tanh restent en revanche utilisées dans certaines architectures spécifiques, notamment les réseaux récurrents à mécanismes de porte (LSTM, GRU), où leur intervalle de sortie borné sert précisément à moduler un flux d’information entre 0 et 1 ou entre -1 et 1.
Dans la couche de sortie, le choix dépend de la nature du problème plutôt que des considérations de gradient : sigmoïde pour une classification binaire, dont la sortie dans s’interprète comme une probabilité ; softmax pour une classification multi-classe, qui produit une distribution de probabilité sur l’ensemble des classes ; et l’absence de fonction d’activation, c’est-à-dire une sortie linéaire, pour une tâche de régression.
Le compromis qui gouverne ce choix reste donc le même que celui dégagé plus haut, entre saturation du gradient et extinction du gradient, à ceci près que la décision se prend désormais aussi à l’aune de résultats empiriques publiés sur des tâches concrètes, et non plus seulement à partir des propriétés mathématiques de chaque fonction.
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 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 trois 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.