기간 : 10/27 ~ 10/31

Transformer와 LLM 시작. 진짜 자연어 처리를 배우기 시작했다.


이번 주에 배운 내용은 크게 3가지다.

1. Attention을 활용한 Transformer 아키텍처

  • RNN 및 Seq2Seq 기반 구조의 문제점(장기 의존성, 정보 손실 등)을 해결하기 위해 각 단어의 관계성에 주목하는 기법
  • 문장 내 단어의 위치와 의미적 유사성을 병렬적으로 N번 학습하여(Multi-Head) 문맥을 파악
  • Seq2Seq 구조를 탈피하고 Attention만을 활용한 Transformer 구조

2. Hugging Face에서 모델 가져오기

  • GitHub와 비슷하게 개발자들이 학습된 모델을 레포지토리에 올리고 공유하는 곳
  • 필요한 Task에 따라 적절한 모델을 찾아 로드

3. OpenAI와 Ollama를 활용한 LLM 프롬프팅


🔨Transformer 구현

https://github.com/WindyAle/Transformer

 

GitHub - WindyAle/Transformer: 공부하면서 구현해본 Transformer 구조

공부하면서 구현해본 Transformer 구조. Contribute to WindyAle/Transformer development by creating an account on GitHub.

github.com

 

글로 보면 언뜻 알것 같으면서도 내가 이해한 내용이 코드로 연결이 잘 안되는 것 같아서

그냥 직접 구현해보면서 부딪혀봤다.

Transformer/
├── main.ipynb            # main 파일 (테스트용)
└── modules/
    ├── MyTransformer.py  # 인코더-디코더
    └── common.py         # 여러 번 재사용되는 구조들
                          ## attention, Embedding, MultiHeadAttention, FeedForward

 

수업에서 실습으로 진행한 코드를 기반으로 함수끼리 좀 더 직관적으로 서로 연결되도록 노력해봤는데 잘 됐는지는 모르겠다.

  • common
    핵심 메커니즘인 attention 함수 그리고 임베딩, 멀티헤드, 피드포워드는 트랜스포머 내부에서 여러 번 재사용되고 여기저기서 호출되는 도구들
  • MyTransformer
    common의 도구들을 사용해 인코더와 디코더 그리고 그 둘을 연결하는 메인 클래스
  • (최상위) main
    import를 테스트하는 파일. 초기값 설정하고 입력값만 넣으면 끝이다.

 

class Config:
    def __init__(self, vocab_size, pad_token_id=0):
        self.vocab_size = vocab_size        # 단어사전 크기
        self.hidden_size = 768              # 은닉층 차원수 (attention_eads의 배수)
        self.max_position_embeddings = 768  # 최대 시퀀스 길이
        self.num_attention_heads = 12       # 어텐션 헤드 개수
        self.num_hidden_layers = 12         # 레이어 개수
        self.intermediate_size = 2048       # FeedForward 레이어의 중간 차원
        self.hidden_dropout_prob = 0.1      # dropout 비율
        self.layer_norm_eps = 1e-12         # epsilon
        self.pad_token_id = pad_token_id    # 패딩 토큰

테스트를 위한 Config 객체

대부분의 숫자는 자유롭게 주면 되지만 hidden_sizenum_attention heads의 배수여야 한다.

멀티 헤드 어텐션 시 임베딩 차원을 헤드 수만큼 나눠야 하기 때문.

 

source_ids = torch.tensor([
    [101, 2054, 2064, 2106, 102, 0, 0, 0, 0, 0, 0, 0],
    [101, 3000, 4000, 102, 2000, 102, 0, 0, 0, 0, 0, 0]
], dtype=torch.long)

target_ids = torch.tensor([
    [201, 3054, 3064, 3106, 202, 0, 0, 0, 0, 0],
    [201, 4000, 5000, 202, 6000, 202, 0, 0, 0, 0]
], dtype=torch.long)

테스트를 위해 더미로 만든 데이터

원시 텍스트에서 벡터화까지 진행되었다 가정하고 아무 숫자를 넣었다.

 

import torch
from modules.MyTransformer import Transformer

model = Transformer(config_enc, config_dec)
model.eval() # 평가 모드

# ... (생략)

logits = model(source_ids, target_ids)
print(f"Output Logits shape: {logits.shape}")

# 예상 출력 (VOCAB_SIZE == 15000)
# [2, 10, 15000]

다행히도 예상과 동일한 결과가 나온다.

 

batch_size(2)개의 문장이 만들어졌고

각 문장은 seq_len(10)개의 단어로 이루어져 있으며

단어사전에 있는 vocab_size(15000)개의 단어들이 각 위치에 올 수 있는 확률이다.

  • [2, 10, 768]로 계산된 아웃풋이
  • 디코더에서 Linear(hidden_size, vocab_size)를 거쳐
  • 단어사전에 매핑된 각 단어들에 대한 확률분포로 최종값이 나온다.

💥Trouble Shooting

🚨 RuntimeError: view size is not compatible with input tensor's size and stride

상황

 - transpose를 적용한 텐서에 view를 사용할 때

원인

expand, narrow, transpose 등 배열의 형태를 바꾸는 메서드를 적용하면 메모리의 stride가 바뀐다.

배열의 저장 형태가 non-unit stride일 경우 view가 읽어오지 못한다.

해결

context = context.transpose(1, 2).contiguous()
context = context.view(batch_size, -1, self.embed_dim)

contiguous()를 추가로 적용

 

Q. 동일한 동작을 하는 reshape와의 차이점

  • view는 얕은 복사. 원본 배열을 특정한 모양으로 조회하는 메커니즘이다. unit stride를 만족해야 한다.
  • reshape는 기본적으로는 view와 동일, view를 못하는 상황에는 그냥 새로운 배열을 만드는 스마트한 버전.

Q. 근본적인 의문으로, view는 왜 stride를 체크하는가?

→ view에 dtype 파라미터가 있는게 힌트였다. 데이터타입 레벨에서 값을 읽어오려면 원본 배열을 바이트 단위로 읽어야 하기 때문

 

 

🚨 AttributeError: 'Embedding' object has no attribute 'self.position_ids'

상황

- 포지션 임베딩 중 Embedding 클래스가 호출될 때

원인

Embedding의 클래스 속성으로 self.position_ids을 정의했는데 이것을 forward 메서드가 참조하지 못한다.

파라미터에 device 속성을 적용한게 원인이었다.

다른 파라미터들은 GPU로 이동했는데 텐서만 여전히 CPU에 남아있어서 서로 connected 되지 못했다.

→ torch.tensor()는 model.to()가 적용되지 않는다.

해결

self.register_buffer("position_ids", torch.arange(...))

torch.tensor는 GPU로 이동하기 위해 별도의 명시가 필요하다.

register_buffer()라는 메서드를 새로 알았다.

 

Q. position_ids를 forward에서 선언하면 되지 않을까?

→ 이 경우 메서드가 호출될 때마다 새로운 position_ids가 매번 생성되고, 불필요한 오버헤드가 반복되어 별로다.

 

Q. 왜 텐서만 특수하게 취급할까?

모델의 속성이지만 학습 대상은 아닌 파라미터를 다루기 위한 PyTorch의 규칙이라고 한다. 단순 계산 or 임시저장용 텐서는 모델 state에 저장될 필요가 없으므로 이런 경우를 구분하기 위함인듯.

+ Recent posts