В современных нейросетях, включая LLM на базе Transformer, стандартом стали неограниченные функции активации — ReLU и GELU. Их основное преимущество, хорошая прВ современных нейросетях, включая LLM на базе Transformer, стандартом стали неограниченные функции активации — ReLU и GELU. Их основное преимущество, хорошая пр

Стресс-тест функций активации: GELU vs Tanh

В современных нейросетях, включая LLM на базе Transformer, стандартом стали неограниченные функции активации — ReLU и GELU. Их основное преимущество, хорошая проходимость градиентов и быстрое обучение глубоких моделей.

Однако на практике наблюдается проблема: при появлении доминирующих паттернов или высокочастотного шума во входном контексте (длинные диалоги, шумные данные, повторяющиеся или доминирующие токены) модели становятся нестабильными и склонными к деградации генерации и галлюцинациям.

В этой статье я попытался выяснить, может ли быть связан принципиально выбор функции активации с галлюцинациями LLM.

Что такое GELU и Tanh

GELU (Gaussian Error Linear Unit) — гладкая версия ReLU, используемая в большинстве современных LLM. Она пропускает положительные значения без жёсткого потолка и подавляет отрицательные. GELU улучшает обучение и качество на чистых данных, не ограничивая амплитуду активаций.

Tanh (гиперболический тангенс) — ограниченная функция активации с выходом в диапазоне [-1, 1]. При больших входных значениях функция насыщается, что ограничивает влияние отдельных нейронов. Проблема, из-за которой похоже перешли на GELU, более сложное обучение из-за затухания градиентов. Ниже остановлюсь, почему эта проблема сегодня не критична.

Описание эксперимента

Цель эксперимента — изолировать влияние функции активации, не меняя архитектуру и задачу. Для этого использовалась задача классификация MNIST (базовый тест способности сети извлекать и удерживать признаки). Задача выбрана намеренно простой для изоляции эффекта функции активации.

Эксперимент проводился на трёх идентичных MLP

Linear → LayerNorm → Activation → Linear
(Activation ∈ {ReLU, GELU, Tanh})

20 независимых прогонов для каждой конфигурации, при всех искажениях сохраняется суммарная энергия сигнала. Меняется только распределение энергии (энтропия, концентрация), но не её величина.

Проведены три типа стресс-тестов, моделирующих сбои внимания и контекста в LLM.

Код эксперимента под спойлером:

Скрытый текст

import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms import numpy as np import time # --- КОНФИГУРАЦИЯ --- CONFIG = { 'INPUT_SIZE': 784, 'HIDDEN_SIZE': 10, 'OUTPUT_SIZE': 10, 'BATCH_SIZE': 512, 'EPOCHS': 12, 'LR': 0.003, 'NUM_RUNS': 20, 'DEVICE': "cuda" if torch.cuda.is_available() else "cpu", # Добавили 0.0 (Baseline) во все тесты 'LOBOTOMY_LEVELS': [0.0, 0.30, 0.50, 0.70, 0.90], 'SPIKE_LEVELS': [0.0, 0.30, 0.50, 0.70, 0.90], 'NOISE_LEVELS': [0.0, 0.5, 1.0, 2.0, 3.0] } # --- ЗАГРУЗКА ДАННЫХ --- class FastMNIST: def __init__(self, train=True, device='cpu'): transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))]) dataset = datasets.MNIST('./data', train=train, download=True, transform=transform) loader = torch.utils.data.DataLoader(dataset, batch_size=len(dataset)) self.data, self.targets = next(iter(loader)) self.data = self.data.to(device) self.targets = self.targets.to(device) self.n_samples = len(self.data) def get_batches(self, batch_size, shuffle=True): if shuffle: indices = torch.randperm(self.n_samples, device=self.data.device) else: indices = torch.arange(self.n_samples, device=self.data.device) for start_idx in range(0, self.n_samples, batch_size): idx = indices[start_idx : start_idx + batch_size] yield self.data[idx], self.targets[idx] print(f"🔥 DEVICE: {CONFIG['DEVICE']}") train_data = FastMNIST(train=True, device=CONFIG['DEVICE']) test_data = FastMNIST(train=False, device=CONFIG['DEVICE']) # --- МОДЕЛЬ --- class PrismNet(nn.Module): def __init__(self, act_fn, name): super().__init__() self.name = name self.fc1 = nn.Linear(CONFIG['INPUT_SIZE'], CONFIG['HIDDEN_SIZE']) self.ln = nn.LayerNorm(CONFIG['HIDDEN_SIZE']) self.act = act_fn() self.fc2 = nn.Linear(CONFIG['HIDDEN_SIZE'], CONFIG['OUTPUT_SIZE']) def forward(self, x, mask=None): x = x.view(-1, CONFIG['INPUT_SIZE']) pre_latent = self.fc1(x) if mask is not None: pre_latent = pre_latent * mask latent = self.act(self.ln(pre_latent)) return self.fc2(latent) # --- ГЕНЕРАТОРЫ МАСОК --- def get_lobotomy_mask(hidden_size, severity, seed, device): torch.manual_seed(seed) mask = torch.ones(hidden_size, device=device) n_killed = int(hidden_size * severity) if n_killed >= hidden_size: n_killed = hidden_size - 1 perm = torch.randperm(hidden_size, device=device) killed_indices = perm[:n_killed] mask[killed_indices] = 0.0 n_alive = hidden_size - n_killed scale = np.sqrt(hidden_size / n_alive) return mask * scale def get_spike_mask(hidden_size, severity, seed, device): torch.manual_seed(seed) mask = torch.ones(hidden_size, device=device) victim = torch.randint(0, hidden_size, (1,)).item() E_total = float(hidden_size) E_spike = severity * E_total E_noise = (1.0 - severity) * E_total amp_spike = np.sqrt(E_spike) amp_noise = np.sqrt(E_noise / (hidden_size - 1)) mask[:] = amp_noise mask[victim] = amp_spike return mask def get_noise_mask(hidden_size, intensity, seed, device): torch.manual_seed(seed) raw_noise = torch.randn(hidden_size, device=device) * intensity mask = torch.exp(raw_noise) current_E = (mask**2).sum() target_E = float(hidden_size) scale = torch.sqrt(target_E / current_E) return mask * scale # --- ЯДРО ЭКСПЕРИМЕНТА --- print(f"\n=== PRISM FINAL: BASELINE & TRINITY (N={CONFIG['NUM_RUNS']}) ===\n") models_config = [(nn.ReLU, "ReLU"), (nn.Tanh, "Tanh"), (nn.GELU, "GELU")] # Хранилище res_lobo = {name: {lvl: [] for lvl in CONFIG['LOBOTOMY_LEVELS']} for _, name in models_config} res_spike = {name: {lvl: [] for lvl in CONFIG['SPIKE_LEVELS']} for _, name in models_config} res_noise = {name: {lvl: [] for lvl in CONFIG['NOISE_LEVELS']} for _, name in models_config} total_start = time.time() for run in range(CONFIG['NUM_RUNS']): run_start = time.time() print(f"Run {run+1:02d}/{CONFIG['NUM_RUNS']}...", end=" ", flush=True) # 1. Train trained_models = [] for act_fn, name in models_config: model = PrismNet(act_fn, name).to(CONFIG['DEVICE']) opt = optim.Adam(model.parameters(), lr=CONFIG['LR']) for epoch in range(CONFIG['EPOCHS']): model.train() for data, target in train_data.get_batches(CONFIG['BATCH_SIZE']): opt.zero_grad() logits = model(data) loss = nn.CrossEntropyLoss()(logits, target) loss.backward() opt.step() trained_models.append(model) # Helper for running tests def run_test_batch(level_list, result_dict, mask_gen_func): for lvl in level_list: # Special case for Baseline if lvl == 0.0: mask = None else: mask = mask_gen_func(CONFIG['HIDDEN_SIZE'], lvl, seed=1000+run+int(lvl*100), device=CONFIG['DEVICE']) for model in trained_models: model.eval() correct = 0; total = 0 with torch.no_grad(): for data, target in test_data.get_batches(2000, shuffle=False): logits = model(data, mask) correct += logits.argmax(1).eq(target).sum().item() total += target.size(0) result_dict[model.name][lvl].append(100. * correct / total) # 2. Run Tests run_test_batch(CONFIG['LOBOTOMY_LEVELS'], res_lobo, get_lobotomy_mask) run_test_batch(CONFIG['SPIKE_LEVELS'], res_spike, get_spike_mask) run_test_batch(CONFIG['NOISE_LEVELS'], res_noise, get_noise_mask) print(f"Done ({time.time() - run_start:.1f}s)") # --- ОТЧЕТ --- def print_table(title, levels, results_dict, metric_name): print(f"\n\n### {title}") print(f"{metric_name:<10} | {'Model':<6} | {'Accuracy':<9} | {'StdDev':<8} | {'95% CI':<16}") print("|" + "-"*65 + "|") for lvl in levels: label = str(lvl) if lvl == 0.0: label = "0.0 (Base)" print(f"| **{label}** | | | | |") for _, name in models_config: data = results_dict[name][lvl] mean = np.mean(data) std = np.std(data) ci = 1.96 * std / np.sqrt(len(data)) # Simple highlight logic mean_str = f"{mean:.2f}%" if lvl > 0.0 and name == "Tanh" and mean > 60: mean_str = f"**{mean_str}**" print(f"| | {name:<6} | {mean_str:<9} | {std:.2f} | [{mean-ci:.2f}, {mean+ci:.2f}] |") print("|" + "-"*65 + "|") print_table("TEST 1: LOBOTOMY (Потеря информации)", CONFIG['LOBOTOMY_LEVELS'], res_lobo, "Dead %") print_table("TEST 2: SPIKE (Паразитная доминанта)", CONFIG['SPIKE_LEVELS'], res_spike, "Energy %") print_table("TEST 3: NOISE (Энтропия / Хаос)", CONFIG['NOISE_LEVELS'], res_noise, "Noise Lvl") print(f"\nTotal Experiment Time: {time.time() - total_start:.1f}s")

1. Базовый тест

Цель — оценить поведение модели в базовых условиях.

Модель

Точность (Mean)

Стабильность (StdDev)

Комментарий

GELU

92.84%

±0.42

Стандарт индустрии. Лучшая динамика обучения.

ReLU

92.65%

±0.45

Базовая модель.

Tanh

92.06%

±0.28

Самая стабильная, но уступает 0.78% в точности

Tanh отстаёт от GELU примерно на 0.8%. Возможно это и есть плата за ограниченную активацию в условиях чистых данных.

2. Тест на доминирующий нейрон

Искусственно концентрируем часть энергии слоя в одном нейроне. Например, уровень 0.5 означает, что 50% всей энергии слоя приходится на один нейрон, остальная энергия распределена по прочим.

Сила Спайка(% энергии в 1 нейроне)

GELU(Accuracy)

Tanh(Accuracy)

Δ (Tanh - GELU)

Интерпретация

0.0 (Clean)

92.84%

92.06%

-0.78%

В норме GELU лучше.

0.3 (30%)

91.48%

91.77%

+0.29%

Точка перелома.

0.5 (50%)

87.76%

90.94%

+3.18%

Зона риска. Tanh игнорирует атаку.

0.7 (70%)

80.93%

88.41%

+7.48%

GELU теряет стабильность.

0.9 (90%)

66.75%

77.77%

+11.02%

Коллапс GELU.

GELU демонстрирует почти линейное падение точности по мере роста спайка. Tanh деградирует значительно медленнее и сохраняет стабильность.

За счёт насыщения tanh вклад доминирующего нейрона ограничен. Даже при сильной концентрации энергии сеть продолжает использовать остальной контекст.

Дополнительно, при среднем уровне спайка разброс результатов (StdDev) у GELU в несколько раз выше, чем у Tanh, что указывает на повышенную чувствительность GELU к случайным флуктуациям.

Отмечу, что этим экспериментом я хотел продемонстрировать аналогичный механизм, наблюдаемый в LLM. При подавлении повторов энергия концентрируется в альтернативных токенах. В длинных контекстах внимание схлопывается на небольшое число позиций.

Предполагаю, что Tanh в FFN-слоях может сгладить эти артефакты.

3. Тест на рост энтропии

На активации накладывается мультипликативный шум при сохранении общей энергии.
Это моделирует длинный, зашумлённый или плохо структурированный контекст.

Уровень Шума(σ)

GELU(Accuracy)

Tanh(Accuracy)

Δ (Tanh - GELU)

Интерпретация

0.0 (Clean)

92.84%

92.06%

-0.78%

Бэйслайн.

0.5 (Low)

87.29%

90.25%

+2.96%

Начало деградации контекста.

1.0 (High)

62.68%

77.68%

+15.00%

Tanh работает как фильтр.

2.0 (Chaos)

42.64%

58.70%

+16.06%

GELU генерирует хаос.

  • При слабом шуме разница умеренная.

  • При высоком шуме точность GELU резко падает.

  • Tanh сохраняет существенно более высокий уровень корректной классификации.

Фактически, GELU продолжает интерпретировать шум как полезный сигнал.
Tanh ограничивает вклад случайных флуктуаций и по сути выполняет роль порогового фильтра.

4. Тест удаление нейронов

Случайно удаляется часть нейронов слоя, а оставшиеся усиливаются так, чтобы суммарная энергия сохранялась.

% Удаленных нейронов

GELU(Accuracy)

Tanh(Accuracy)

Δ (Tanh - GELU)

Интерпретация

0.0 (Clean)

92.84%

92.06%

-0.78%

Все нейроны на месте.

0.3 (30%)

67.61%

81.27%

+13.66%

Tanh сохранил образ.

0.5 (50%)

50.44%

62.65%

+12.21%

Tanh держится на половине сети

0.7 (70%)

31.33%

38.52%

+7.19%

Критическая потеря для всех.

  • При удалении 30–50% нейронов Tanh сохраняет значительно более высокую точность.

  • GELU деградирует быстрее.

В сетях с Tanh информация распределена более равномерно. В сетях с GELU признаки кодируются более локально, поэтому потеря нейронов критичнее.

Итоговые выводы

Эксперимент выявляет инженерный компромисс, а не лучшую функцию активации.

GELU / ReLU (неограниченные)

Плюсы:

  • Быстрое обучение

  • Лучшие результаты на чистых бенчмарках

Минусы:

  • Высокая чувствительность к доминирующим активациям

  • Низкая устойчивость при росте энтропии

  • Повышенный риск деградации и нестабильного поведения

Tanh (ограниченная)

Плюсы:

  • Высокая устойчивость к шуму и спайкам

  • Более равномерное распределение информации

  • Предсказуемая деградация

Минусы:

  • Сложнее обучать

  • Небольшое отставание в идеальных условиях

Причины отказа от Tanh

В начале развития LLM Tanh считался стандартом, но проблема затухания градиентов, мотивировала к переходу на GELU. Сейчас уже разработаны методики, в значительной степени эту проблему решающие или обходящие — LayerNorm / RMSNorm, корректная инициализация (Xavier / orthogonal), residual-соединения.

Так что вопрос уже стоит не в невозможности использования Tanh, а в выборе места и способа его использования. Даже если сети с Tanh немного сложнее обучать, потенциальный выигрыш в устойчивости должен это полностью компенсировать.

Практические предложения

Для систем, где важна надёжность, устойчивость к шумному контексту и отказоустойчивость (safety-critical или reasoning-ориентированные модели), отказ от Tanh требует пересмотра.

Перспективный подход — гибридная архитектура:

  • использовать GELU на ранних слоях для извлечения признаков,

  • использовать Tanh в узких местах сети (bottlenecks, attention-контуры, memory-блоки) для стабилизации и фильтрации аномалий.

Эксперимент показывает, что выбор функции активации существенно влияет на поведение нейросетей в части их устойчивости.

Источник

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно

Ethereum создает команду для защиты от квантовой угрозы

Ethereum создает команду для защиты от квантовой угрозы

Сообщается, что Ethereum Foundation создал новую команду для подготовки сети к возможным атакам квантовых компьютеров. Эти машины однажды смогут взломать
Поделиться
Bitcoinist2026/01/25 22:00
Пилотный проект Visa позволяет банкам использовать стейблкоины для беспрепятственных глобальных выплат

Пилотный проект Visa позволяет банкам использовать стейблкоины для беспрепятственных глобальных выплат

В значимом развитии для будущего трансграничных платежей, Visa запустила пилотный проект, который позволяет банкам и финансовым учреждениям предварительно финансировать международные транзакции с использованием стейблкоинов. Эта инициатива является частью постоянных усилий Visa по модернизации глобальной платежной инфраструктуры, предлагая более быстрые и эффективные решения, использующие растущее принятие криптовалютных активов. Visa's [...]
Поделиться
Crypto Breaking News2025/09/30 16:59
Эксперт по оружию встревожен «совершенно безумным заявлением» прокурора США о стрельбе в DHS

Эксперт по оружию встревожен «совершенно безумным заявлением» прокурора США о стрельбе в DHS

«Это безумное заявление», — сказал эксперт по политике в области оружия в ответ на комментарий назначенного Трампом прокурора о недавнем убийстве агентами DHS. Назначен Трампом
Поделиться
Rawstory2026/01/25 22:22