module.py

import torch.nn as nn
import torch.nn.functional as F

# 활성화 함수들 정의
def gelu(x):
    """GELU 활성화 함수 구현
    OpenAI GPT에서 사용된 버전과는 약간 다름
    
    0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) *
    (x + 0.044715 * torch.pow(x, 3))))

    논문 참조: https://arxiv.org/abs/1606.08415
    """
    return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0)))

def swish(x):
    # Swish 활성화 함수: x * sigmoid(x)
    return x * torch.sigmoid(x)

# 활성화 함수들을 딕셔너리로 매핑
ACT2FN = {"gelu": gelu, "relu": F.relu, "swish": swish}

우선 이 아이들은 딕셔너리로 매핑되어 아래의 Intermediate class에 사용된다. 그래서 run에 관련된 파일들에서

    parser.add_argument("--hidden_act", default="gelu", type=str)  # gelu relu

위와 같이 선택되어 사용된다.

여기서 의아한 점 두가지

  1. 왜 gelu와 swish는 직접 함수를 구현했는데 relu는 없지?

    하고 찾아보니까 import torch.nn.functional as F에 gelu와 relu가 import 되어있어서 그렇단다. 그럼 왜 gelu만 직접 구현하신거지? 라는 의문은 OpenAI GPT 버전과 다르다는 이야기가 주석논문에 써 있으니 넘기자. 영어로만 8페이진데 저걸 하나하나 탐구하긴 좀.

  2. 왜 gelu와 relu는 parser에 사용가능하게 되어있는데 swish는 없지? 걍 주석에 빼놓으신건가?

  3. GELU (Gaussian Error Linear Unit)

def gelu(x):
    return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0)))
  • 특징: 음수 입력도 부드럽게 일부 통과시킴
  • BERT, GPT 등 현대 트랜스포머 모델에서 주로 사용
  • 사용 가능: Yes (코드에 직접 구현되어 있음)
  1. ReLU (Rectified Linear Unit)
# F.relu로 PyTorch에서 import
# 음수는 0, 양수는 그대로 통과
  • 특징: 간단하고 계산이 빠름, 음수를 완전히 차단
  • 딥러닝에서 가장 널리 사용되는 활성화 함수
  • 사용 가능: Yes (PyTorch에서 F.relu로 import됨)
  1. Swish
def swish(x):
    return x * torch.sigmoid(x)
  • 특징: GELU와 비슷하게 부드러운 곡선, 최신 활성화 함수
  • 일부 상황에서 GELU/ReLU보다 좋은 성능
  • 사용 가능: Yes (코드에 직접 구현되어 있음)

음. 걍 주석에서 빼놓은건가. 한번 테스트 나중에 해봐야 할듯.

class LayerNorm(nn.Module):

class LayerNorm(nn.Module):
    def __init__(self, hidden_size, eps=1e-12):
        """TF 스타일의 Layer Normalization"""
        super(LayerNorm, self).__init__()
        # 가중치와 편향 파라미터 정의
        self.weight = nn.Parameter(torch.ones(hidden_size))
        self.bias = nn.Parameter(torch.zeros(hidden_size))
        self.variance_epsilon = eps

    def forward(self, x):
        # 평균과 분산 계산
        u = x.mean(-1, keepdim=True)
        s = (x - u).pow(2).mean(-1, keepdim=True)
        # 정규화 후 스케일링과 이동
        x = (x - u) / torch.sqrt(s + self.variance_epsilon)
        return self.weight * x + self.bias

딥러닝코드만 보다 보니까 머신러닝이 그립다. 아래 처럼 진행된다.

# hidden_size가 4인 LayerNorm
layer_norm = LayerNorm(hidden_size=4)

# 입력 데이터 (배치 크기=2, 시퀀스 길이=3, 특성 차원=4)
x = torch.tensor([
    [[1, 2, 3, 4],     # 첫 번째 배치, 첫 번째 시퀀스
     [2, 3, 4, 5],     # 첫 번째 배치, 두 번째 시퀀스
     [3, 4, 5, 6]],    # 첫 번째 배치, 세 번째 시퀀스
    
    [[2, 3, 4, 5],     # 두 번째 배치, 첫 번째 시퀀스
     [3, 4, 5, 6],     # 두 번째 배치, 두 번째 시퀀스
     [4, 5, 6, 7]]     # 두 번째 배치, 세 번째 시퀀스
])

# LayerNorm 적용
normalized = layer_norm(x)
# 각 시퀀스의 각 위치에서 평균=0, 분산=1이 되도록 정규화

class Embeddings(nn.Module):

# args 설정 예시
class Args:
    item_size = 10000        # 전체 아이템(영화) 개수
    hidden_size = 64         # 임베딩 차원
    max_seq_length = 50      # 최대 시퀀스 길이
    hidden_dropout_prob = 0.1  # 드롭아웃 비율

args = Args()
embedding_layer = Embeddings(args)

# 입력 데이터 예시 (배치 크기=2, 시퀀스 길이=5)
input_ids = torch.tensor([
    [4643, 170, 531, 616, 2140],  # 첫 번째 사용자의 영화 시청 시퀀스
    [1591, 2600, 8169, 2572, 0]   # 두 번째 사용자의 영화 시청 시퀀스 (마지막은 패딩)
])

# Embeddings 계산 과정:
# 1. 아이템 임베딩
item_emb = embedding_layer.item_embeddings(input_ids)
# shape: (2, 5, 64)
# 각 영화 ID가 64차원 벡터로 변환됨

# 2. 위치 임베딩
position_ids = torch.tensor([
    [0, 1, 2, 3, 4],  # 첫 번째 시퀀스의 위치 정보
    [0, 1, 2, 3, 4]   # 두 번째 시퀀스의 위치 정보
])
pos_emb = embedding_layer.position_embeddings(position_ids)
# shape: (2, 5, 64)
# 각 위치가 64차원 벡터로 변환됨

# 3. 임베딩 합산
embeddings = item_emb + pos_emb
# shape: (2, 5, 64)
# 아이템과 위치 정보가 결합됨

# 4. Layer Normalization
normalized = embedding_layer.LayerNorm(embeddings)
# shape: (2, 5, 64)
# 결합된 임베딩을 정규화

# 5. Dropout 적용
final_embeddings = embedding_layer.dropout(normalized)
# shape: (2, 5, 64)
# 일부 값들이 랜덤하게 0으로 설정됨

final_embeddings[0][0]  # 첫 번째 사용자의 첫 번째 영화(4643)의 임베딩 벡터
# 64차원 벡터에는:
# - 영화 4643의 특성 정보
# - 시퀀스에서의 위치(0) 정보
# 가 모두 포함되어 있음

SelfAttention 클래스

# 예시 설정
args = {
    'hidden_size': 64,          # 임베딩 차원
    'num_attention_heads': 4,   # 어텐션 헤드 수
    'attention_probs_dropout_prob': 0.1
}

# 입력 예시: 사용자가 본 3개의 영화 시퀀스
input_tensor = [
    [영화1_임베딩],  # [64차원]
    [영화2_임베딩],  # [64차원]
    [영화3_임베딩]   # [64차원]
]

# 작동 과정:
1) Query, Key, Value 생성:
   영화1 -> Q1, K1, V1
   영화2 -> Q2, K2, V2
   영화3 -> Q3, K3, V3

2) 어텐션 점수 계산:
   점수_1_1 = Q1·K1  # 영화1이 영화1에 주목하는 정도
   점수_1_2 = Q1·K2  # 영화1이 영화2에 주목하는 정도
   점수_1_3 = Q1·K3  # 영화1이 영화3에 주목하는 정도
   # 나머지 영화들도 마찬가지

3) 결과:
   새로운_영화1 = (점수_1_1×V1 + 점수_1_2×V2 + 점수_1_3×V3)
   # 다른 영화들과의 관계를 고려한 새로운 표현

Intermediate 클래스

여기에 아까 활성화 함수들이 선택되어 사용된다.

# Feed-Forward Network (FFN)
# 각 영화의 특징을 더 깊게 처리

# 예시:
영화_임베딩 = [64차원 벡터]

1) 확장:
dense_1: 64 -> 256 차원으로 확장
[64] -> [256]

2) 활성화 함수 적용 (GELU):
비선형성 추가

3) 축소:
dense_2: 256 -> 64 차원으로 복원
[256] -> [64]

# 이를 통해 각 영화의 특징을 더 복잡하게 표현

Layer 클래스 (트랜스포머 레이어)

# 하나의 완전한 트랜스포머 레이어
# SelfAttention + Intermediate 결합

입력: [영화1, 영화2, 영화3] 시퀀스

1) Self-Attention:
   - 영화들 간의 관계 파악
   예: 영화1이 액션영화면, 다른 액션영화들과 강한 연관성

2) Intermediate:
   - 각 영화의 특징 강화
   예: 액션+SF 같은 복합적인 특성 파악

출력: [강화된_영화1, 강화된_영화2, 강화된_영화3]

Encoder 클래스

# 여러 개의 Layer를 쌓은 전체 구조

# 예시: 3개의 레이어를 사용할 경우
입력 시퀀스: [영화1, 영화2, 영화3]

Layer 1:
- 기본적인 장르 관계 파악
- 예: 액션영화끼리의 연관성

Layer 2:
- 더 복잡한 패턴 파악
- 예: 감독, 배우 등의 관계

Layer 3:
- 가장 추상적인 패턴 파악
- 예: 스토리, 분위기 등의 유사성

최종 출력:
- 각 영화가 다른 영화들과의 관계와 
  자신의 특성이 모두 반영된 표현으로 변환됨

아래처럼 진행된다.

# 사용자의 영화 시청 기록
시청_기록 = [
    "아이언맨",    # 액션, SF
    "인셉션",      # SF, 스릴러
    "다크나이트"   # 액션, 범죄
]

# 트랜스포머 모델 처리 후
강화된_시청_기록 = [
    "아이언맨+SF_히어로물",
    "인셉션+복잡한_구조",
    "다크나이트+다크_히어로물"
]

# 이를 바탕으로 다음에 볼 만한 영화 추천
추천_영화 = [
    "어벤져스",    # 비슷한 SF 히어로물
    "메멘토",      # 복잡한 구조의 스릴러
    "배트맨 비긴즈" # 다크 히어로물
]