ReLU・GELU・SiLUを数式から比較して理解する

活性化関数はニューラルネットワークに非線形性を導入する重要な要素です。ReLUが長らく標準でしたが、近年はGELUやSiLU(Swish)など新しい活性化関数が登場し、特にTransformerモデルで広く使われています。

本記事では、主要な活性化関数の数学的定義から実験による比較まで詳しく解説します。

本記事の内容

  • 各活性化関数の数学的定義と導関数
  • 関数形状と勾配特性の比較
  • Pythonでの実装と実験

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

活性化関数の役割

なぜ活性化関数が必要か

活性化関数がないと、ニューラルネットワークは単なる線形変換の合成になります:

$$ f(x) = W_L \cdots W_2 W_1 x = W’ x $$

これでは複雑な非線形関数を学習できません。活性化関数により非線形性を導入することで、任意の関数を近似できるようになります。

良い活性化関数の条件

  1. 非線形性: 線形では表現力がない
  2. 微分可能性: 勾配降下法のため(少なくともほぼ至る所で)
  3. 適切な勾配: 勾配消失・爆発を避ける
  4. 計算効率: 順伝播・逆伝播が高速

ReLU(Rectified Linear Unit)

数学的定義

$$ \text{ReLU}(x) = \max(0, x) = \begin{cases} x & \text{if } x > 0 \\ 0 & \text{if } x \leq 0 \end{cases} $$

導関数

$$ \frac{d}{dx} \text{ReLU}(x) = \begin{cases} 1 & \text{if } x > 0 \\ 0 & \text{if } x < 0 \end{cases} $$

$x = 0$ では微分不可能ですが、実用上は0か1のどちらかに設定します。

特徴

利点 欠点
計算が非常に高速 Dying ReLU問題($x < 0$ で勾配0)
勾配消失が起きにくい($x > 0$ で) 出力が非有界
スパースな表現を学習 0中心ではない

Dying ReLU問題

入力が常に負になるユニットは、勾配が常に0になり学習が停止します。これを「Dying ReLU問題」と呼びます。

対策として、Leaky ReLU(負の領域にも小さい傾きを持たせる)が提案されています:

$$ \text{LeakyReLU}(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha x & \text{if } x \leq 0 \end{cases} $$

$\alpha$ は小さな正の値(例: 0.01)です。

GELU(Gaussian Error Linear Unit)

数学的定義

GELUは入力を標準正規分布の累積分布関数(CDF)で重み付けします:

$$ \text{GELU}(x) = x \cdot \Phi(x) = x \cdot P(X \leq x), \quad X \sim \mathcal{N}(0, 1) $$

正規分布のCDFは誤差関数を使って書くと:

$$ \Phi(x) = \frac{1}{2} \left[ 1 + \text{erf}\left( \frac{x}{\sqrt{2}} \right) \right] $$

したがって:

$$ \text{GELU}(x) = \frac{x}{2} \left[ 1 + \text{erf}\left( \frac{x}{\sqrt{2}} \right) \right] $$

近似式

計算効率のため、以下の近似がよく使われます:

$$ \text{GELU}(x) \approx 0.5x \left[ 1 + \tanh\left( \sqrt{\frac{2}{\pi}} (x + 0.044715 x^3) \right) \right] $$

または、シグモイドを使った近似:

$$ \text{GELU}(x) \approx x \cdot \sigma(1.702 x) $$

導関数

$$ \frac{d}{dx} \text{GELU}(x) = \Phi(x) + x \cdot \phi(x) $$

ここで $\phi(x) = \frac{1}{\sqrt{2\pi}} e^{-x^2/2}$ は標準正規分布の確率密度関数です。

特徴

利点 欠点
滑らかで連続的 ReLUより計算コストが高い
確率的な解釈が可能 近似を使わないと遅い
Transformerで高性能 数学的にやや複雑

直感的な理解

GELUは「入力の値に応じて、その入力をどれだけ通すかを確率的に決める」と解釈できます:

  • $x$ が大きい(正)→ 高確率で通す($\Phi(x) \approx 1$)
  • $x$ が小さい(負)→ 低確率で通す($\Phi(x) \approx 0$)
  • $x \approx 0$ → 50%の確率で通す

SiLU / Swish

数学的定義

SiLU(Sigmoid Linear Unit)、またはSwishは以下のように定義されます:

$$ \text{SiLU}(x) = x \cdot \sigma(x) = \frac{x}{1 + e^{-x}} $$

ここで $\sigma(x) = \frac{1}{1 + e^{-x}}$ はシグモイド関数です。

より一般的なSwishは学習可能なパラメータ $\beta$ を持ちます:

$$ \text{Swish}_\beta(x) = x \cdot \sigma(\beta x) $$

$\beta = 1$ のときSiLUと一致します。

導関数

$$ \frac{d}{dx} \text{SiLU}(x) = \sigma(x) + x \cdot \sigma(x)(1 – \sigma(x)) = \sigma(x)(1 + x(1 – \sigma(x))) $$

整理すると:

$$ \frac{d}{dx} \text{SiLU}(x) = \sigma(x) + \text{SiLU}(x)(1 – \sigma(x)) $$

特徴

利点 欠点
滑らかで連続的 ReLUより計算コストが高い
自己ゲーティング 勾配計算がやや複雑
EfficientNetで高性能 出力が非有界

自己ゲーティングの解釈

SiLUは入力 $x$ 自身でゲーティングを行います: – $\sigma(x)$ がゲート(0〜1の値) – $x$ がゲートされる値

これにより、入力の「重要度」を入力自身が決定する、という直感的な解釈ができます。

3つの活性化関数の比較

数学的な関係

興味深いことに、GELU と SiLU は似た形をしています。どちらも「$x \times$ (0から1の値)」という形で、$x$ をスケーリングしています:

  • GELU: $x \cdot \Phi(x)$
  • SiLU: $x \cdot \sigma(x)$

$\Phi(x)$ と $\sigma(x)$ はどちらもS字型の関数で、負の極限で0、正の極限で1に近づきます。

勾配の特性

関数 $x \to -\infty$ $x = 0$ $x \to +\infty$
ReLU 0 0 or 1 1
GELU 0 0.5 1
SiLU 0 0.5 1

GELUとSiLUは負の領域でも小さな勾配を持つため、Dying ReLU問題が軽減されます。

Pythonでの実装と可視化

活性化関数の実装

import numpy as np
import matplotlib.pyplot as plt
from scipy.special import erf

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

def relu_derivative(x):
    """ReLUの導関数"""
    return (x > 0).astype(float)

def gelu(x):
    """GELU(厳密版)"""
    return x * 0.5 * (1 + erf(x / np.sqrt(2)))

def gelu_approx(x):
    """GELU(tanh近似)"""
    return 0.5 * x * (1 + np.tanh(np.sqrt(2/np.pi) * (x + 0.044715 * x**3)))

def gelu_derivative(x):
    """GELUの導関数"""
    phi = np.exp(-x**2 / 2) / np.sqrt(2 * np.pi)  # 標準正規分布のPDF
    Phi = 0.5 * (1 + erf(x / np.sqrt(2)))  # 標準正規分布のCDF
    return Phi + x * phi

def silu(x):
    """SiLU / Swish"""
    return x / (1 + np.exp(-x))

def sigmoid(x):
    """シグモイド関数"""
    return 1 / (1 + np.exp(-x))

def silu_derivative(x):
    """SiLUの導関数"""
    sig = sigmoid(x)
    return sig + silu(x) * (1 - sig)

# 可視化
x = np.linspace(-4, 4, 1000)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 関数のプロット
ax1 = axes[0]
ax1.plot(x, relu(x), label='ReLU', linewidth=2)
ax1.plot(x, gelu(x), label='GELU', linewidth=2)
ax1.plot(x, silu(x), label='SiLU', linewidth=2)
ax1.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
ax1.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
ax1.set_xlabel('x')
ax1.set_ylabel('f(x)')
ax1.set_title('Activation Functions')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax1.set_xlim(-4, 4)
ax1.set_ylim(-1, 4)

# 導関数のプロット
ax2 = axes[1]
ax2.plot(x, relu_derivative(x), label="ReLU'", linewidth=2)
ax2.plot(x, gelu_derivative(x), label="GELU'", linewidth=2)
ax2.plot(x, silu_derivative(x), label="SiLU'", linewidth=2)
ax2.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
ax2.axhline(y=1, color='gray', linestyle='--', alpha=0.5)
ax2.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
ax2.set_xlabel('x')
ax2.set_ylabel("f'(x)")
ax2.set_title('Derivatives of Activation Functions')
ax2.legend()
ax2.grid(True, alpha=0.3)
ax2.set_xlim(-4, 4)
ax2.set_ylim(-0.5, 1.5)

plt.tight_layout()
plt.show()

GELUの近似精度の検証

import numpy as np
import matplotlib.pyplot as plt
from scipy.special import erf

def gelu_exact(x):
    """GELU(厳密版)"""
    return x * 0.5 * (1 + erf(x / np.sqrt(2)))

def gelu_tanh_approx(x):
    """GELU(tanh近似)"""
    return 0.5 * x * (1 + np.tanh(np.sqrt(2/np.pi) * (x + 0.044715 * x**3)))

def gelu_sigmoid_approx(x):
    """GELU(sigmoid近似)"""
    return x / (1 + np.exp(-1.702 * x))

x = np.linspace(-4, 4, 1000)

gelu_e = gelu_exact(x)
gelu_t = gelu_tanh_approx(x)
gelu_s = gelu_sigmoid_approx(x)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 関数の比較
ax1 = axes[0]
ax1.plot(x, gelu_e, label='GELU (exact)', linewidth=2)
ax1.plot(x, gelu_t, '--', label='GELU (tanh approx)', linewidth=2)
ax1.plot(x, gelu_s, ':', label='GELU (sigmoid approx)', linewidth=2)
ax1.set_xlabel('x')
ax1.set_ylabel('GELU(x)')
ax1.set_title('GELU: Exact vs Approximations')
ax1.legend()
ax1.grid(True, alpha=0.3)

# 誤差の比較
ax2 = axes[1]
ax2.plot(x, np.abs(gelu_t - gelu_e), label='tanh approx error', linewidth=2)
ax2.plot(x, np.abs(gelu_s - gelu_e), label='sigmoid approx error', linewidth=2)
ax2.set_xlabel('x')
ax2.set_ylabel('Absolute Error')
ax2.set_title('Approximation Error')
ax2.legend()
ax2.grid(True, alpha=0.3)
ax2.set_yscale('log')

plt.tight_layout()
plt.show()

print(f"Max error (tanh approx): {np.max(np.abs(gelu_t - gelu_e)):.6f}")
print(f"Max error (sigmoid approx): {np.max(np.abs(gelu_s - gelu_e)):.6f}")

PyTorchでの比較実験

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np
import matplotlib.pyplot as plt

def create_dataset(n_samples=5000, input_dim=20, complexity='medium'):
    """異なる複雑さのデータセットを生成"""
    np.random.seed(42)

    X = np.random.randn(n_samples, input_dim).astype(np.float32)

    if complexity == 'linear':
        W = np.random.randn(input_dim)
        y = (X @ W > 0).astype(np.int64)
    elif complexity == 'medium':
        y = ((X[:, 0]**2 + X[:, 1]**2) > 1).astype(np.int64)
    elif complexity == 'hard':
        y = (np.sin(X[:, 0] * 3) + np.cos(X[:, 1] * 3) + X[:, 2] > 0).astype(np.int64)
    else:
        raise ValueError(f"Unknown complexity: {complexity}")

    # Train/Val/Test split
    train_end = int(0.6 * n_samples)
    val_end = int(0.8 * n_samples)

    return {
        'train': (X[:train_end], y[:train_end]),
        'val': (X[train_end:val_end], y[train_end:val_end]),
        'test': (X[val_end:], y[val_end:])
    }

class MLP(nn.Module):
    """活性化関数を指定できるMLP"""

    def __init__(self, input_dim, hidden_dims, num_classes, activation='relu'):
        super().__init__()

        # 活性化関数の選択
        if activation == 'relu':
            act_fn = nn.ReLU
        elif activation == 'gelu':
            act_fn = nn.GELU
        elif activation == 'silu':
            act_fn = nn.SiLU
        elif activation == 'leaky_relu':
            act_fn = lambda: nn.LeakyReLU(0.01)
        else:
            raise ValueError(f"Unknown activation: {activation}")

        layers = []
        prev_dim = input_dim
        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            layers.append(act_fn())
            prev_dim = hidden_dim
        layers.append(nn.Linear(prev_dim, num_classes))

        self.net = nn.Sequential(*layers)

    def forward(self, x):
        return self.net(x)

def train_and_evaluate(activation, data, input_dim, hidden_dims=[64, 32],
                       n_epochs=50, lr=0.001, verbose=False):
    """モデルの学習と評価"""
    train_dataset = TensorDataset(
        torch.tensor(data['train'][0]),
        torch.tensor(data['train'][1])
    )
    val_dataset = TensorDataset(
        torch.tensor(data['val'][0]),
        torch.tensor(data['val'][1])
    )
    test_dataset = TensorDataset(
        torch.tensor(data['test'][0]),
        torch.tensor(data['test'][1])
    )

    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
    test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

    model = MLP(input_dim, hidden_dims, num_classes=2, activation=activation)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=lr)

    train_losses = []
    val_accuracies = []

    for epoch in range(n_epochs):
        # Training
        model.train()
        epoch_loss = 0
        for X_batch, y_batch in train_loader:
            optimizer.zero_grad()
            outputs = model(X_batch)
            loss = criterion(outputs, y_batch)
            loss.backward()
            optimizer.step()
            epoch_loss += loss.item()

        train_losses.append(epoch_loss / len(train_loader))

        # Validation
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for X_batch, y_batch in val_loader:
                outputs = model(X_batch)
                _, predicted = torch.max(outputs, 1)
                total += y_batch.size(0)
                correct += (predicted == y_batch).sum().item()
        val_accuracies.append(correct / total)

    # Test
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for X_batch, y_batch in test_loader:
            outputs = model(X_batch)
            _, predicted = torch.max(outputs, 1)
            total += y_batch.size(0)
            correct += (predicted == y_batch).sum().item()
    test_accuracy = correct / total

    return {
        'train_losses': train_losses,
        'val_accuracies': val_accuracies,
        'test_accuracy': test_accuracy
    }

# 実験
activations = ['relu', 'gelu', 'silu', 'leaky_relu']
complexities = ['linear', 'medium', 'hard']

results = {}

for complexity in complexities:
    print(f"\nDataset complexity: {complexity}")
    data = create_dataset(complexity=complexity)

    results[complexity] = {}
    for activation in activations:
        result = train_and_evaluate(activation, data, input_dim=20)
        results[complexity][activation] = result
        print(f"  {activation}: Test Accuracy = {result['test_accuracy']:.4f}")

# 可視化
fig, axes = plt.subplots(2, 3, figsize=(15, 10))

for idx, complexity in enumerate(complexities):
    # 学習曲線
    ax1 = axes[0, idx]
    for activation in activations:
        ax1.plot(results[complexity][activation]['train_losses'],
                label=activation, alpha=0.8)
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Training Loss')
    ax1.set_title(f'{complexity.capitalize()} - Training Loss')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    # 検証精度
    ax2 = axes[1, idx]
    for activation in activations:
        ax2.plot(results[complexity][activation]['val_accuracies'],
                label=activation, alpha=0.8)
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('Validation Accuracy')
    ax2.set_title(f'{complexity.capitalize()} - Validation Accuracy')
    ax2.legend()
    ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# テスト精度の比較
print("\n" + "="*50)
print("Test Accuracy Summary")
print("="*50)
print(f"{'Activation':<15} {'Linear':>10} {'Medium':>10} {'Hard':>10}")
print("-"*50)
for activation in activations:
    linear_acc = results['linear'][activation]['test_accuracy']
    medium_acc = results['medium'][activation]['test_accuracy']
    hard_acc = results['hard'][activation]['test_accuracy']
    print(f"{activation:<15} {linear_acc:>10.4f} {medium_acc:>10.4f} {hard_acc:>10.4f}")

活性化関数の使い分け

一般的な推奨

モデル 推奨活性化関数
CNN (一般) ReLU
Transformer (NLP) GELU
Transformer (Vision) GELU or SiLU
EfficientNet SiLU
一般的なMLP ReLU or GELU

選択の指針

  1. 速度重視: ReLU(最も高速)
  2. 精度重視: GELU or SiLU(滑らかで勾配が安定)
  3. Dying ReLU対策: Leaky ReLU, GELU, SiLU
  4. Transformerベース: GELU(GPT, BERTで使用)

まとめ

本記事では、主要な活性化関数(ReLU, GELU, SiLU)について解説しました。

  • ReLUは最も高速だが、Dying ReLU問題がある
  • GELUは正規分布のCDFでゲーティングし、Transformerで広く使用される
  • SiLUはシグモイドでゲーティングし、EfficientNetで高性能
  • GELUとSiLUは滑らかで、負の領域でも小さな勾配を持つ

次のステップとして、以下の記事も参考にしてください。