임베딩, 어텐션 연산에 이어서 인코더의 마지막 단계인 정규화와 피드 포워드 층에 대해 알아보자.
층 정규화
트랜스포머 구조에서 정규화는 머신러닝에서 입력 데이터에 대한 스케일링을 적용할 때와 거의 비슷한 이유로 시행된다고 할 수 있다. 머신러닝에서 입력 데이터의 스케일링은 데이터를 특정 범위로 조정하는 것인데, 이를 통해 데이터의 상대적 크기 차이가 모델 성능에 악영향을 미치지 않도록 한다. 이와 비슷하게 정규화란 층의 출력 값을 일정한 분포로 맞추어 학습을 안정화하는 역할을 한다. 보통 각 층의 출력을 평균 0, 분산 1로 맞추는 경우가 많고, 이로 인해 기울기 소실이나 폭주 문제를 완화시킬 수 있다.
층 정규화 말고도 배치 정규화도 있다. 배치 정규화는 말 그대로 배치 단위로 정규화를 수행하는 것이다. 그러나 배치 정규화는 자연어 처리에는 적합하지 않다. 그 이유는 문장 길이의 다양성 때문이다. 문장 길이가 다른 것끼리 배치 정규화를 시행하게 된다면 어떤 배치에는 패딩 토큰이 들어가 평균과 분산이 왜곡될 수도 있고, 또 자연어 처리와 같은 문장 내 순서나 의존 관계가 중요한 데이터에 대해서는 이러한 방식이 올바르지 않다고 판단된다.
이어서 배울 피드 포워드 층을 층 정규화 앞에 둘지, 뒤에 둘 지에 따라서 사후 정규화와 사전 정규화로 나뉜다. 현재는 층 정규화 후에 어텐션 연산과 피드 포워드 층을 통과하는 사전 정규화가 주로 활용된다고 한다.
norm = nn.LayerNorm(embedding_dim)
norm_x = norm(input_embeddings)
norm_x.shape # torch.Size([1, 5, 16])
norm_x.mean(dim=-1).data, norm_x.std(dim=-1).data
# (tensor([[ 2.2352e-08, -1.1176e-08, -7.4506e-09, -3.9116e-08, -1.8626e-08]]),
# tensor([[1.0328, 1.0328, 1.0328, 1.0328, 1.0328]]))
역시 우리는 Pytorch 덕을 본다. Pytorch에서 제공하는 nn.LayerNorm을 통해 정규화 층을 만들 수 있다. 위 코드는 입력 임베딩 벡터를 층 정규화에 통과시켜 나온 결과를 확인한다. 결과에서 알 수 있듯이, 평균 0과 표준 편차 1에 매우 근접함을 알 수 있다.
피드 포워드 층
인코딩의 마지막 단계이다. 우리는 지금까지 단어 사이의 관계를 파악하는 역할의 어텐션 연산을 알아보았다. 그렇다면 피드 포워드 층은 무슨 역할을 할까? 피드 포워드 층은 입력 텍스트 전체를 이해하는 역할을 담당하는 완전 연결층을 의미한다. 다시 말해 어텐션이 단어 간 관계를 학습한다면, 피드 포워드 층은 그 관계를 바탕으로 전체 텍스트를 더욱 통합된 정보로 압축하여 표현하는 역할을 한다. 합성곱 신경망 실습과 비슷하게 선형 층, 드롭아웃 층, 층 정규화, 활성 함수로 구성된다.
class PreLayerNormFeedForward(nn.Module):
def __init__(self, d_model, dim_feedforward, dropout):
super().__init__()
self.linear1 = nn.Linear(d_model, dim_feedforward) # 선형 층 1
self.linear2 = nn.Linear(dim_feedforward, d_model) # 선형 층 2
self.dropout1 = nn.Dropout(dropout) # 드랍아웃 층 1
self.dropout2 = nn.Dropout(dropout) # 드랍아웃 층 2
self.activation = nn.GELU() # 활성 함수
self.norm = nn.LayerNorm(d_model) # 층 정규화
def forward(self, src):
x = self.norm(src)
x = x + self.linear2(self.dropout1(self.activation(self.linear1(x))))
x = self.dropout2(x)
return x
위 코드에서 처음 선형 층 둘은 차원을 확장시켰다가 다시 줄이는데, 이는 합성곱 신경망에서와 같은 원리로, 모델이 더 풍부한 표현을 학습할 수 있도록 비선형 변환 공간을 확장하기 위해서이다. 이어서 forward() 메서드를 살펴보자.
x = self.norm(src)
- 입력 텐서 src를 층 정규화함
- 사전 정규화(pre-layer normalization)를 적용하였기에, 다른 층에 들어가기 전에 정규화를 수행
x = x + self.linear2(self.dropout1(self.activation(self.linear1(x))))
- 첫 번째 선형 변환: self.linear1(x)는 입력 x를 dim_feedforward 크기로 확장하여, 추가적인 비선형 학습 공간을 제공
- 활성 함수 적용: self.activation()을 통해 비선형 변환을 추가하여, 데이터에 복잡한 패턴을 학습할 수 있도록 함
- 드롭아웃 적용: self.dropout1()는 활성 함수의 출력을 일부 무작위로 0으로 설정하여 과적합을 방지
- 두 번째 선형 변환: self.linear2()는 차원을 다시 줄여서 모델이 중요한 정보만 남기도록 도와줌
- 잔차 연결(Residual Connection): 입력 x와 결과를 더해 줌. 잔차 연결은 모델이 더 깊어질 때 발생할 수 있는 기울기 소실 문제를 완화하고 학습을 안정화함
x = self.dropout2(x)
두 번째 드롭아웃 층을 적용. 최종 출력의 일부 값을 무작위로 0으로 설정하여 모델이 특정 노드에 과도하게 의존하지 않도록 함
return x
최종적으로 피드 포워드 층을 통과한 결과를 반환. 이 결과는 정규화, 비선형 변환, 차원 확장 및 축소, 잔차 연결을 거친 정보가 담긴 텐서를 의미
그런데 사실 이러한 과정은 굉장히 귀납적이다. 일단 해보니까 성능이 좋아서 하는 거고, 이후에 그럴 만한 이유를 찾아서 붙인 것이다. 복잡한 관계를 학습한다던지, 차원을 줄이면서 중요한 정보를 요약할 수 있게 된다던지 하는 것들 말이다.
이렇게 해서 인코더의 구성 요소를 모두 알아보았다. 다음 시간에는 이를 종합하여 인코더의 전체 구조를 확인하고, 디코더는 이와 어떤 점이 다른지를 살펴보며 트랜스포머 구조에 대한 분석을 마칠 예정이다.
'AI' 카테고리의 다른 글
[LLM] RAG로 Hallucination 방지하기 with Llama-index (7) | 2024.11.12 |
---|---|
[LLM] 트랜스포머 구조 파헤치기 (4) - 인코더와 디코더 (0) | 2024.11.08 |
[LLM] 트랜스포머 구조 파헤치기 (2) - 어텐션 (Attention) 이해하기 (2) | 2024.11.04 |
[LLM] OpenAI API (3) - 파이썬 코드로 호출해 보기 (2) | 2024.11.02 |
[LLM] OpenAI API (2) - Playground를 사용해 보자 (1) | 2024.11.01 |