My Vision, Computer Vision

[논문 리뷰/요약] Noise-contrastive estimation: A new estimation principle forunnormalized statistical models 본문

Paper

[논문 리뷰/요약] Noise-contrastive estimation: A new estimation principle forunnormalized statistical models

gyuilLim 2025. 1. 15. 16:43
 

Noise-contrastive estimation: A new estimation principle for unnormalized statistical models

We present a new estimation principle for parameterized statistical models. The idea is to perform nonlinear logistic regression to discriminate between the observed data and some artificially gene...

proceedings.mlr.press


이 논문은 대조 학습(Contrastive learning)의 개념을 수학적으로 설명한 논문이다.

또한 Vision Langauge Model에서 주로 사용되는 InfoNCE Loss의 바탕이 되는 개념을 제시한다.

일반적으로 Softmax와 같은 함수를 사용해 확률 밀도 함수를 정규화하여 모델링하는데,

이 논문은 정규화의 Computational cost를 지적하며 노이즈와 대조하는 방법으로 확률 밀도 함수를 모델링한다.


Abstract

  • 매개변수화된 통계 모델(Parameterized statistical model)을 위한 새로운 추정 방법을 제시한다.
  • 이 방법은 정규화되지 않은 모델(Unnormalized models)에 적용할 수 있다.

Introduction

  • 기본적인 추정 문제는 다음과 같다.

 

  • 랜덤 벡터 xRn어떤 확률 밀도 함수(Probability dense function) pd(.) 를 따른다고 할 때, 이 확률 밀도 함수는 매개변수 집합(α)을 이용하여 pm(.;α) 로 모델링할 수 있다.
  • 즉 특정 파라미터 α 에 대해 pd(.)=pm(.;α)만족하는 확률 밀도 함수가 존재하는 것이다.
  • 여기서 문제는 목적 함수를 어떻게 최대화해야 α 를 추정할 수 있는지이다.

 

  • 우선 pm(.;α) 는 확률 밀도 함수이기 때문에, 다음을 만족해야 한다.

pm(u;ˆα)du=1

  • 확률 밀도 함수이기 때문에, 이 함수의 적분값, 즉 그래프의 넓이는 1이어야 한다는 것이다. 이 때 pm(.;ˆα) 는 정규화되었다고 할 수 있다.

 

  • 이를 정규화 상수를 도입하여 다시 나타내면 아래와 같다.

pm(.;α)=p0m(.;α)Z(α),Z(α)=p0m(u;α)du

  • p0m(.;α)정규화되지 않은 확률 밀도 함수를 의미한다.

 

  • 고차원 데이터의 경우, 데이터의 차원이 매우 크기때문에 적분 공간의 부피가 기하급수적으로 증가하므로 정규화 상수 Z(α) 를 계산하는 문제는 매우 어렵다.
  • 정규화 상수를 다루는 가장 간단한 방법은 이를 모델의 추가적인 파라미터로 취급하는 것이다.
  • Contrastive divergenceScore mathcing의 경우 Z(α)근사치로 추정했지만
    Noise-contrastive estimation은 입력 데이터 x 와 인공적으로 생성된 노이즈 y 의 대조 학습으로 Z(α)직접 추정할 수 있다.

Noise-Contrastive Estimation

  • 앞에서 Z(α) 를 모델의 추가적인 파라미터로 취급한다고 했는데, 이 말이 무슨 말이냐면

lnpm(.;θ)=lnp0m(.;α)lnZ(α)=lnp0m(.;α)+c

  • 여기서 θ=(α,c) 이다.
  • 확률 밀도 함수에 로그를 씌운 후 lnZ(α)임의의 상수 c 로 두고 이를 추정한다는 것이다.
  • c 가 특정 값을 가졌을 때만 lnpm(.;θ) 는 1로 적분될 수 있는 것이다.

 

  • αc 를 추정할 수 있는 목적 함수를 만들기 위해, 분류(Classification) 문제로 접근한다.
  • X=(x1,,xT)입력 데이터 집합, Y=(y1,,yT)노이즈 데이터 집합이라고 하자. 이 때 노이즈는 임의의 분포 pn(.) 로부터 생성되었다. u=(X,Y) 이다.

 

  • 모델이 X,Y 를 구별하기 위해 만들어졌다고 했을 때, 로그 비율로 Classification score를 정의할 수 있다.

G(u;θ)=lnpm(u;θ)pn(u)=lnpm(u;θ)lnpn(u)

  • 위 수식이 의미하는 Classification Score는 데이터 upm(.) 에서 나온 데이터(정상)인 경우 증가하고 pn(.) 에서 나온 데이터(잡음)인 경우 감소한다. 입력 데이터와 노이즈를 잘 구별하게끔 학습하는 것이다.

 

  • 다음으로, 이 점수를 확률로 취급하기 위해 [0, 1] 범위로 변환되도록 Logistic function(sigmoid, r)을 사용한다.

h(u,θ)=11+exp[G(u;θ)]

  • 따라서 목적함수(J)를 다음과 같이 정의할 수 있다.

JT=12Ttln[h(xt;θ)]+ln[1h(y;θ)]

  • 위 목적함수는 총 샘플 개수 T 에 대해, 정상 데이터와 잡음 데이터의 h 값의 평균을 나타낸 것이다.

 

  • 만약 T 의 크기가 충분히 크다면 큰수의 법칙에 의해 JT 는 다음과 같이 수렴한다.

J(θ)=12Eln[h(x;θ)]+ln[1h(y;θ)]

  • 이 때, f(.)=lnpm(.;θ) 를 만족하는 어떤 f 가 있다고 하면 아래와 같이 다시 쓸 수 있다.

J(f)=12Eln[r((f(x)lnpn(x))]+ln[1r(f(y)lnpn(y))]

  • 즉 데이터, 잡음 쌍 (x,y) 가 주어졌을 때, 노이즈 데이터(y)가 만들어진 분포(pn(.))만 미리 안다면, 위와 같이 계산하여 로그 확률 밀도 함수 lnpm(.;θ)=lnp0m(.;α)+c 를 학습할 수 있게 되는 것이다.

Data Generation

Noise data(y)

  • 목적 함수를 계산하기 위해 노이즈 데이터에 대한 분포( pn(.))는 미리 알고있어야 하므로, 가우시안 분포나 간단한 확률 분포를 사용한다.

Input data(x)

  • 입력 데이터 xR4 는 ICA(Independent Component Analysis) 모델을 통해 계산된다.

x=As

  • A=(a1,,a4) 이고, s 는 라플라스 분포를 따른다.

구현(Implementation)

1. 데이터 생성

# ICA 모델을 이용한 입력 데이터(x) 생성
def generate_ica_data(batch_size, input_dim):
    A = torch.randn(input_dim, input_dim)
    laplace_dist = Laplace(torch.zeros(input_dim), torch.ones(input_dim))
    s = laplace_dist.sample((batch_size,))
    data = s @ A.T
    return data

# 학습 환경
data_size = 1000
input_dim = 4
num_epochs = 40
learning_rate = 0.005

# Train data, Test data 각각 생성
train_data_samples = generate_ica_data(data_size, input_dim)
test_data_samples = generate_ica_data(data_size, input_dim)

# 가우시안 분포로부터 노이즈 데이터(y) 생성
noise_samples = torch.randn(batch_size, input_dim)

 

2. 데이터 분포 비교

J(f)=12Eln[r((f(x)lnpn(x))]+ln[1r(f(y)lnpn(y))]

  • 아래 코드는 위 식에서 lnpn(.) 을 계산하는 과정이다.
  • 노이즈 데이터가 가우시안 분포로부터 만들어졌기 때문에, x,y 각각에 대한 확률값 pn(x),pn(y) 을 계산한 후 로그를 씌운다.
def compute_log_noise_probs(samples, mean=0.0, std=1.0):
    var = std ** 2
    log_probs = -0.5 * (torch.log(torch.tensor(2 * torch.pi * var)) + (samples - mean)**2 / var)
    return log_probs.sum(dim=1)

# 입력 데이터와 노이즈 데이터를 concat 한 후 계산
log_noise_probs = compute_log_noise_probs(torch.cat([train_data_samples, noise_samples]))
  • 위 코드로부터 만들어진 log prob를 시각화해보면 아래와 같이 왼쪽(입력 데이터)는 낮은 값을, 오른쪽(노이즈 데이터)는 높은 값을 가지고 있는 것을 확인할 수 있다.

입력 데이터와 노이즈 데이터에 대한 Log Prob 시각화

  • 즉 ICA 모델로부터 만들어진 입력 데이터는 노이즈 데이터가 만들어진 가우시안 분포에 속할 확률이 낮다는 것이다.

t-SNE로 나타낸 데이터 분포(파란색 : ICA 입력 데이터, 빨간색 : 노이즈 데이터)

  • t-SNE를 통해 생성된 입력 데이터와 노이즈 데이터를 시각화해보면 위와 같다.

3. 모델 정의

# 간단한 Fully connected layer 정의
class SimpleModel(nn.Module):
    def __init__(self, input_dim):
        super(SimpleModel, self).__init__()
        self.fc1 = nn.Linear(input_dim, 36)
        self.fc2 = nn.Linear(36, 64)
        self.fc3 = nn.Linear(64, 1)

    def forward(self, x):
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x.squeeze()
        
# 모델 및 옵티마이저 초기화
model = SimpleModel(input_dim)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

 

4. NCE Loss Function

J(f)=12Eln[r((f(x)lnpn(x))]+ln[1r(f(y)lnpn(y))]

  • 이제 목적함수를 계산할 수 있다.
# NCE 목적 함수
def nce_loss(f_x, f_y, log_noise_probs):
    term1 = torch.log(sigmoid(f_x - log_noise_probs[:len(f_x)])).mean()
    term2 = torch.log(1 - sigmoid(f_y - log_noise_probs[len(f_x):])).mean()
    loss = -0.5 * (term1 + term2)
    return loss
  • f(x)f(y) 는 모델의 출력을 의미한다.

 

5. 학습

loss_list = []

# 학습 루프
for epoch in range(num_epochs):
    optimizer.zero_grad()

    # 입력 데이터, 노이즈 데이터 각각에 대한 모델 전방 계산
    f_x = model(train_data_samples)
    f_y = model(noise_samples)

    # NCE Loss 계산
    loss = nce_loss(f_x, f_y, log_noise_probs)

    # 학습
    loss.backward()
    optimizer.step()

		# Loss 저장
    loss_list.append(loss.item())

# Loss 시각화
plt.plot(loss_list)
  • 아래 Loss curve를 보면 알 수 있듯이, 학습이 잘 된다.

NCE Loss function을 사용하여 학습한 모델의 Loss curve

 

 

6. 결과 비교

# 모델 Test
model.eval()
with torch.no_grad():
		# Test data에 대한 모델 출력값 계산
    f_test_x = model(test_data_samples)
    
    # Noise sample은 훈련 시 사용한 것과 같음
    f_test_y = model(noise_samples)

    # NCE Loss 계산
    test_log_noise_probs = compute_log_noise_probs(torch.cat([test_data_samples, noise_samples]))
    test_loss = nce_loss(f_test_x, f_test_y, test_log_noise_probs)

# Nce Loss 값 출력
print(f"Test Loss: {test_loss.item():.4f}")

# 예측 결과 시각화
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
sns.kdeplot(test_data_samples.numpy().flatten(), label="True ICA Data", color='blue', fill=True)
sns.kdeplot(f_test_x.numpy().flatten(), label="Prediction of ICA data, f(x)", color='red', fill=True)
sns.kdeplot(f_test_y.numpy().flatten(), label="Prediction of Noise data, f(y)", color='black', fill=True)
plt.xlabel('Value')
plt.ylabel('Density')
plt.legend()
  • 아래 그래프에는 세 가지 분포가 있는데, 다음과 같다.
  1. True ICA data에 대한 분포(파란색)
  2. ICA data에 대한 예측 분포(빨간색)
  3. Noise data에 대한 예측 분포(검은색)

출력 분포(검정색, 빨간색)와 입력 데이터(파란색) 분포 비교

  • 결론은 노이즈 분포에 비해 입력 데이터의 분포가 원래 분포와 더 유사하게 생성하게 되었다는 것을 확인함으로써, Noise-Contrastive Estimation의 효과를 입증할 수 있다는 것이다.
728x90