본문 바로가기
C.W.K.
Stream
Lesson 03 of 04 · published

vmap + grad: 예제별 그래디언트

~9 min · vmap, jax, tutorial

Level 0호기심
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

vmap과 grad를 합성하는 가장 흔한 패턴은 예제별 그래디언트야. 표준 학습은 배치 평균 그래디언트를 쓰지만, 가끔 예제마다 그래디언트를 따로 보고 싶을 때가 있어 (그래디언트 노이즈 규모, 차등 프라이버시, 영향 함수).

import jax
import jax.numpy as jnp

# 단일 example 의 loss
def loss_one(params, x, y):
    '''x: (D,), y: (), params: (D,)'''
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

# grad — 단일 example 의 gradient
g_one = jax.grad(loss_one)
grad_for_one_example = g_one(params, x_single, y_single)  # shape: (D,)

# vmap — batch 의 모든 example 의 gradient
g_each = jax.vmap(g_one, in_axes=(None, 0, 0))
per_grads = g_each(params, batch_x, batch_y)  # shape: (B, D)

# 비교: 표준 batch loss 의 gradient
def batch_loss(params, X, Y):
    pred = X @ params
    return jnp.mean((pred - Y) ** 2)

batch_grad = jax.grad(batch_loss)(params, batch_x, batch_y)  # shape: (D,)

# 검증: per_grads.mean(0) ≈ batch_grad
print(jnp.allclose(per_grads.mean(0), batch_grad))  # True (거의)

이게 PyTorch에서 훅이나 Functorch 없이는 어려웠던 일. JAX에서는 한 줄이면 돼.

활용 1: 그래디언트 분포 분석

@jax.jit
def per_example_grads(params, X, Y):
    return jax.vmap(jax.grad(loss_one), in_axes=(None, 0, 0))(params, X, Y)

g_each = per_example_grads(params, X_batch, Y_batch)  # (B, D)

# gradient norm 분포
norms = jnp.linalg.norm(g_each, axis=1)  # (B,)
print(f"평균 norm: {norms.mean():.4f}")
print(f"최대 norm: {norms.max():.4f} (outlier 가능성)")
print(f"norm std: {norms.std():.4f}")

# outlier example 식별
outlier_idx = jnp.argmax(norms)
print(f"가장 큰 gradient 의 example: {outlier_idx}, norm={norms[outlier_idx]}")

실전에서 학습이 불안정할 때는 이상치 예제를 찾는 게 첫 디버깅 단계야.

활용 2: Differential Privacy (DP-SGD)

def dp_sgd_step(params, X, Y, lr, clip_norm, noise_scale, key):
    # per-example gradients
    g_each = jax.vmap(jax.grad(loss_one), in_axes=(None, 0, 0))(params, X, Y)

    # 각 example 의 norm clipping
    g_each = jax.vmap(
        lambda g: g * jnp.minimum(1, clip_norm / jnp.linalg.norm(g))
    )(g_each)

    # 평균 + noise
    g_clipped_mean = g_each.mean(0)
    noise = jax.random.normal(key, g_clipped_mean.shape) * noise_scale
    return params - lr * (g_clipped_mean + noise)

학술 알고리즘인 DP-SGD를 30줄 이하로 구현할 수 있어. 다른 프레임워크에선 별도 라이브러리가 필요해.

활용 3: 영향 함수

"이 한 예제를 빼면 모델이 어떻게 바뀔까?"에 대한 근사야. 예제별 그래디언트가 기본 재료야.

# 매우 간단화된 influence
def influence_score(params, X, Y, x_test, y_test):
    g_each = jax.vmap(jax.grad(loss_one), in_axes=(None, 0, 0))(params, X, Y)
    g_test = jax.grad(loss_one)(params, x_test, y_test)
    return -jnp.dot(g_each, g_test)  # train example 마다 한 score

🔬 vmap + grad가 푸는 문제 카테고리

"각 예제별 무언가"가 필요한 모든 작업, 예제별 그래디언트, 예제별 Hessian (vmap of grad of grad), 예제별 특성 기여도. JAX의 합성 덕에 한 줄로 표현할 수 있어. 예전에는 박사 학위 논문에서나 보던 계산을 이제는 연구 노트 한 줄로 표현할 수 있어.

Code

import jax
import jax.numpy as jnp

def loss_single(params, x, y):
    """Loss for a SINGLE example — keep it simple!"""
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

# Regular gradient: gradient of average loss
def batch_loss(params, X, y):
    return jnp.mean(jax.vmap(loss_single, in_axes=(None, 0, 0))(params, X, y))

avg_grad = jax.grad(batch_loss)

# Per-example gradients: separate gradient for each example
per_example_grad = jax.vmap(jax.grad(loss_single), in_axes=(None, 0, 0))

# Test
params = jnp.array([1.0, 2.0, 3.0])
X = jax.random.normal(jax.random.PRNGKey(0), (32, 3))
y = jax.random.normal(jax.random.PRNGKey(1), (32,))

# Average gradient: shape (3,)
g_avg = avg_grad(params, X, y)
print(f"Average gradient shape: {g_avg.shape}")  # (3,)

# Per-example gradients: shape (32, 3)
g_per = per_example_grad(params, X, y)
print(f"Per-example gradient shape: {g_per.shape}")  # (32, 3)

# Verify: mean of per-example gradients ≈ average gradient
print(f"Match: {jnp.allclose(jnp.mean(g_per, axis=0), g_avg, atol=1e-5)}")
import jax
import jax.numpy as jnp

def loss_single(params, x, y):
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

def dp_gradient(params, X, y, clip_norm=1.0, noise_scale=0.1, key=None):
    """Differentially private gradient computation."""
    # 1. Get per-example gradients
    per_ex_grads = jax.vmap(
        jax.grad(loss_single), in_axes=(None, 0, 0)
    )(params, X, y)

    # 2. Clip each gradient to the norm bound
    grad_norms = jnp.linalg.norm(per_ex_grads, axis=-1, keepdims=True)
    clip_factor = jnp.minimum(1.0, clip_norm / (grad_norms + 1e-8))
    clipped_grads = per_ex_grads * clip_factor

    # 3. Average and add noise
    avg_grad = jnp.mean(clipped_grads, axis=0)
    noise = noise_scale * clip_norm * jax.random.normal(key, avg_grad.shape)
    return avg_grad + noise / X.shape[0]

# Usage
key = jax.random.PRNGKey(42)
params = jnp.array([1.0, 2.0, 3.0])
X = jax.random.normal(jax.random.PRNGKey(0), (64, 3))
y = jax.random.normal(jax.random.PRNGKey(1), (64,))

dp_grad = dp_gradient(params, X, y, key=key)
print(f"DP gradient: {dp_grad}")

External links

Exercise

예제 256개로 이루어진 배치에서 예제별 그래디언트를 계산해. 그 평균이 평균 손실의 그래디언트와 같은지 검증해. 그래디언트 norm의 히스토그램도 그려서 실제 연구에서 쓸 수 있는 진단 자료로 만들어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.