화요일, 9월 10, 2024
HomeDL네거티브 샘플링이란? : 다중 분류를 Binary화, 부정응답학습.

네거티브 샘플링이란? : 다중 분류를 Binary화, 부정응답학습.

위 포스팅에 이어지는 내용입니다.

CBOW라는 word2vec 신경망을 구현해보았습니다. 하지만 corpus의 어휘수가 늘어남에 따라 신경망의 파라미터가 기하급수적으로 늘어나 GPU메모리와 연산속도가 느려지게 되었습니다.

입력층의 연산을 줄여주기 위해 이전 포스팅에서 Embedding계층을 도입하여 해결하였고,

출력층과 Softmax층의 연산을 줄이기 위해 Negative Sampling이라는 방법을 알아보겠습니다.

네거티브 샘플링이란?

Softmax층

일단 Softmax층을 봅시다.

출력층에서 softmax연산을 해서 확률를 계산하는 연산은 언뜻보기에 그렇게 많은 연산이 필요한가? 의문이 들 수 있습니다.

하지만 아래 softmax식을 보면 대번에 이해가 가실겁니다.

Softmax층은 다중분류 모델에 사용하는 layer 입니다.

네거티브샘플링이라는 기법은 Softmax층의 연산(다중분류)을 이진분류로 만들어 신경망을 단순화 하는 것입니다.

you, goodbye라는 맥락이 입력되면 say라는 답(target)이 맞냐? 틀리냐?라는 이진분류로 바꿔서 연산용의성을 높이는 것이 포인트입니다.

위 그림은 Embedding 계층은 생략한 버전입니다.

Sigmoid 함수와 Cross entropy error

sigmoid 함수를 복습해봅시다. 아래와 같은 식이고, 이는 확률로 해석할 수 있습니다.

시그모이드 그래프입니다.

이렇게 확률 y를 얻으면 y와 정답지를 비교해서 loss를 구합니다.

Loss는 Cross entropy error입니다. y는 확률값, t는 정답레이블 입니다.

식이 복잡해보이지만, 이진분류에서 정답이 1(true)일때, 위의 식은 -logy가되고, 정답이 0(false)면 -log(1-y)가 됩니다.

다중분류문제를 이진분류로 구현하는 법

지금까지 설명한 CBOW의 개선법을 그림으로 한눈에 보면 아래와 같습니다.

Win은 Embedding계층으로 개선되었고, Wout에 say에 해당하는 부분과 h를 dot하여 나온 값으로 Sigmoid함수를 통과시키고, Cross entropy loss를 구하게 됩니다. 구현상으로는 h를 받아온 뒤 Embedding Dot 이라는 계층을 만들어 Sigmoid 계층 전까지의 연산을 한번에 수행합니다.

Embedding dot 계층의 구현입니다.

Python
class EmbeddingDot:
    def __init__(self, W):
        self.embed = Embedding(W)
        self.params = self.embed.params
        self.grads = self.embed.grads
        self.cache = None # forward()시 계산결과를 잠시 저장

    def forward(self, h, idx):
        target_W = self.embed.forward(idx)
        out = np.sum(target_W * h, axis=1) # dot product

        self.cache = (h, target_W)
        return out

    def backward(self, dout):
        h, target_W = self.cache
        dout = dout.reshape(dout.shape[0], 1)

        dtarget_W = dout * h
        self.embed.backward(dtarget_W)
        dh = dout * target_W
        return dh

부정응답(틀린응답)도 학습해야한다.

지금 까지 배운것으로는 긍정응답에대해 학습할 수 있지만 틀린정답은 학습하지 않습니다.

you, ???, goodbye 라는 맥락이 들어가면 say라는 답변에 해당하는 것은 학습하나. ???에 hello가 나왔을 때처럼 부정응답에 대해서는 학습하지 않습니다.

따라서 hello같은 부정응답도 학습데이터를 만들어 줘야 합니다.

그렇다면 코퍼스에 있는 모든 단어를 기준으로 부정응답데이터를 만들어주면 될까요? 감당하기 어렵겠죠?

그래서 부정적인 예를 5~10개 정도를 샘플링합니다. 그래서 Negative Sampling 기법입니다~

위와 같이 학습하게 됩니다. 샘플링으로 5~10개 정도를 샘플링 한다고 했죠?

어떻게 하면 좋을까요? 무작위? 코퍼스의 데이터를 기초로 샘플링 합니다.

자주 등장하는 단어를 많이 추출하게 됩니다. 코퍼스의 단어출현 확률분포를 구해서 확률로 샘플링하게 됩니다.

word2vec에서 네거티브 샘플링은 단어의 확률에 0.75제곱을 하라고 권고합니다.

출현확률이 낮은 단어를 좀 더 확률을 높여주는 것이고, 확률이 높은 단어는 낮춰주게 됩니다.

Negative Sampling 구현

Python
class NegativeSamplingLoss:
    def __init__(self, W: np.ndarray, corpus: List[int], power: float = 0.75, sample_size: int = 5) -> None:
        """
        NegativeSamplingLoss 초기화 메서드.
        
        매개변수:
        W (np.ndarray): 출력 쪽 가중치.
        corpus (List[int]): 말뭉치의 단어 ID 리스트.
        power (float): 단어 확률 분포의 계수.
        sample_size (int): 네거티브 샘플의 수.
        """
        self.sample_size = sample_size
        self.sampler = UnigramSampler(corpus, power, sample_size)  # 유니그램샘플러 init
        
        self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)]  
        # 손실 계산을 위한 시그모이드 손실 레이어 보관, sample_size + 1 개의 계층 생성, +1 하는 이유는 긍정예도 학습해야 하므로
        
        self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]  
        # 임베딩 벡터와의 내적 계산 레이어 보관

        self.params, self.grads = [], []  # 파라미터와 그래디언트를 저장할 리스트 초기화
        for layer in self.embed_dot_layers:  # 각 임베딩에 대해서 실행. layer[0]가 긍정예가 됨.
            self.params += layer.params  # 파라미터를 self.params에 추가
            self.grads += layer.grads  # 그래디언트를 self.grads에 추가

    def forward(self, h: np.ndarray, target: np.ndarray) -> float:
        """
        순전파 메서드.
        
        매개변수:
        h (np.ndarray): 은닉층의 출력.
        target (np.ndarray): 긍정적 예 타겟 단어 인덱스.
        
        반환값:
        float: 손실 값.
        """
        batch_size = target.shape[0]  # 배치 크기 추출
        negative_sample = self.sampler.get_negative_sample(target)  # 네거티브 샘플 생성

        # 긍정적 예 순전파
        score = self.embed_dot_layers[0].forward(h, target)  # [0]가 긍정 예
        correct_label = np.ones(batch_size, dtype=np.int32)  # 긍정적 예의 정답 레이블 (1)
        loss = self.loss_layers[0].forward(score, correct_label)  # 긍정적 예의 손실 계산

        # 부정적 예 순전파
        negative_label = np.zeros(batch_size, dtype=np.int32)  # 부정적 예의 레이블 (0)
        for i in range(self.sample_size):  # 각 부정적 샘플에 대해
            negative_target = negative_sample[:, i]  # 부정적 타겟 추출
            score = self.embed_dot_layers[1 + i].forward(h, negative_target)  # 부정적 예에 대한 계산
            loss += self.loss_layers[1 + i].forward(score, negative_label)  # 부정적 예의 손실 계산을 손실 값에 추가

        return loss

    def backward(self, dout: float = 1) -> np.ndarray:
        """
        역전파 메서드.
        
        매개변수:
        dout (float): 상위 레이어로부터 전달된 미분값.
        
        반환값:
        np.ndarray: 은닉층으로 전달될 미분값.
        """
        dh = 0
        for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):  # 각 손실 레이어와 임베드 닷 레이어에 대해
            dscore = l0.backward(dout)  # 손실 레이어의 역전파 수행
            dh += l1.backward(dscore)  # 임베드 닷 레이어의 역전파 수행 후 dh에 더함

        return dh
     

위에서 설명했던 네거티브 샘플링의 과정들을 보면서 코드 한줄한줄 따라가다보면 이해가 될 거라 생각합니다. 다음 포스팅에서는 만들어낸 네거티브샘플링으로 학습코드를 구현하여 네거티브샘플링을 실제로 학습해보겠습니다.

RELATED ARTICLES

Leave a reply

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments