[딥러닝] GPT 모델 정리(Decoder Only Transformer, Pytorch GPT-1 자연어 생성 모델 구현)

2025. 5. 25. 11:30·Study/NLP

- 이번 포스팅에서는 앞서 정리한 Transformer 모델에서 파생되어 만들어진 GPT 모델에 대해 정확히 이해하고 Pytorch 로 GPT-1 모델을 구현할 것입니다.

앞서 트랜스포머 모델에서 설명했던 내용을 기반으로 설명을 하며, 그렇기에 트랜스포머 모델을 이해하신다면 이해하는 것이 어렵지 않을 것입니다.

중복된 내용은 자세한 설명을 생략하겠습니다.

 

- 본 게시글의 의의는,

현재 LLM 분야에서 가장 앞서가고 있는 GPT 모델을 처음부터 이해하여 생성형 AI 를 직접 만들고 응용할 수 있는 능력을 기르는 것입니다. GPT 시리즈의 첫 모델의 내용뿐만이 아닌, 그 이후 GPT 모델들이 개선되고 발전한 흐름을 살펴보고 다음 공부 방향을 어떻게 정해야 하는지까지 알아보겠습니다.

 

(GPT 란?)

- GPT(Generative Pre-trained Transformer)는 2018년 OpenAI 에서 발표한 트랜스포머 기반 언어 모델입니다.

논문 제목은  "Improving Language Understanding by Generative Pre-Training".

트랜스포머 모델의 디코더 부분만을 사용한 모델이라 생각하시면 됩니다.

 

(GPT 모델의 의의)

- GPT 모델이 왜 트랜스포머 모델에서 디코더 부분만을 사용한 것인지,

디코더 부분만을 쓴 것이 어떤 장점이 있는지를 정리하겠습니다.

 

- 먼저, 디코더보다 이해하기 쉬운 인코더 부분에 대해 알아봅시다.

트랜스포머 인코더의 의미는, 시계열 데이터에서 각 요소간 관계(주목도)를 수치화하여 반영하고, 그렇게 반영된 데이터를 축약하여 벡터 공간 안에 전체 데이터(=문장) 의미를 녹여내는 것을 의미합니다.

단어 자체 의미 + 위치 의미 + 단어간 관계의 의미를 품고 있는 벡터를 녹여낸 컨텍스트 벡터를 얻기 위한 작업이라 생각하면 되죠.

 

기존 트랜스포머 모델에서도 인코더는 독립적인 단위로 떼어놓고 볼 수 있었으며,

여기서 얻어낸 컨텍스트 벡터를 언어 모델의 Memory(번역 모델일 시 이를 기반으로 문장 생성) 개념으로 활용할 수 있었습니다.

 

- 인코더는 데이터의 축약, 디코더는 데이터의 생성입니다.

번역기와 같이 정답이 존재하지 않는 생성형 모델을 만들기 위해서 필요한 것은 인코더 모델의 축약된 의미가 아닌 디코더만으로 충분합니다.

다음 단어의 예측만 제대로 이루어진다면 기반이 되는 인코딩 벡터가 필요없기 때문입니다.

 

예를 들어 보겠습니다.

요즘 흔히 보이는 대화형 AI 의 경우에는 어떨까요?

물론 상대방의 말을 기반으로 대화를 생성하기에, 이전 대화에 대한 의미 벡터가 필요할 수도 있습니다.

하지만 이전 데이터 역시 한줄로 표현할 수 있습니다.

 

예를들어,

"안녕?"

"안녕. 잘지냈니?"

"어, 잘 지냈어."

 

라는 대화가 있을 때, 모델은 화자를 명확히 구분할 것 없이,

 

"안녕? <next> 안녕, 잘 지냈니? <next> 어, 잘 지냈어."

 

이렇게 대화 전체를 한 문장으로 다룰 수 있으며, 이렇게 될 경우 문제 해결은 '다음 단어 생성'이라는 것으로 보다 간단해집니다.

(위와 같이 대화형, 질문 응답형으로 생성 모델을 학습시키는 방식을 Instruction Tuning 이라 부르며, LLaMA2 게시글에서 설명합니다.)

 

트랜스포머 모델을 이해하셨다면 아실 수 있듯,

사실 인코더는 디코더가 '어떤 것을 생성할지'에 대한 규제 역할을 하는 것이지, 생성시점에 필요한 정보는 디코더 내부에서 반환되는 벡터만으로 충분하며, 디코더 레이어 최종 부분에서 Generator 역할을 하는 레이어 이전에 준비되는 벡터에는 '다음 단어 추론에 필요한 최종 의미 정보'가 다 들어있는 상태입니다.

 

즉, 번역이 아니라 생성만의 관점에서 보았을 때 디코더는 그 자체로 완벽한 구조이며, 설령 번역 모델로 치더라도, 애초에 문장을 그렇게 학습시키면 되므로,("다음을 번역해 주세요 <번역대상> i am a boy <번역 결과> 나는 소년입니다." 이렇게 질문과 번역 과정을 통째로 입력하여 추론하도록 학습) 문제가 없습니다.

 

- 여기까지 파악한 제 생각을 말씀드리자면,

기존 트랜스포머 구조에서 생성형 AI 를 만들기 위해 필요한 요소에서 불필요한 부분을 제거하여 모델을 단순화하고, 그만큼 가볍게 하여 성능을 높인 것이 GPT 라고 이해하였습니다.

 

그렇다고 결과값을 제약하는 인코더라는 것도 나쁘지는 않다고 생각하였습니다.

이에 대해 찾아보니, 제 생각과 다르지 않게, 다중 입력 조건(이전 출력 데이터뿐 아니라 인간이 감정이나 그때그때의 감정 상태 등에 영향을 받듯 다양한 인코딩 벡터를 받기), 다중 모달, 세밀한 조건 제어가 필요한 경우는 인코더 정보를 활용하는 최신 연구 결과 모델들도 있다고 합니다.

추후 이에 대해서도 공부하고 정리하고 응용할 것입니다.

 

(GPT 구조 설명)

GPT 구조도

Input Tokens
   │
[1] Token Embedding + Positional Encoding
   │
[2] Repeated N layers of:
       ┌──────────────────────────────────────────┐
       │ Masked Multi-Head Self-Attention (causal mask)
       │ └─ Residual Connection + LayerNorm
       │
       │ Position-wise Feed Forward Network (MLP)
       │ └─ Residual Connection + LayerNorm
       └──────────────────────────────────────────┘
   │
[3] Final LayerNorm
   │
[4] Linear layer (projection to vocab size)
   │
[5] Softmax (next-token probabilities)

 

GPT 모델의 구조는 위와 같습니다.

 

트랜스포머 모델의 디코더와 차이점으로 쉽게 알아보자면,

1. 인코더에서 값을 받는 Cross-Attention 삭제 (위 도표의 빨간 사각형 부분)

2. Final LayerNorm 추가

 

위와 같습니다.

 

1번 내용의 경우는 인코더가 사라졌기 때문에 당연히 삭제한 것이고,

2번 내용은 모델 규모가 커질때 학습 안정성을 높이기 위해서입니다.

 

- 이미 토대가 되는 Transformer 모델을 자세히 알아봤기 때문에 크게 어려울 것 없이 이해할 수 있습니다.

 

(GPT-1 모델 학습 방식)

- GPT-1 의 학습 방식은 두 단계를 거쳐 학습되었습니다.

1. Pre-training

GPT-1 의 학습은 UnSupervised Learning 입니다.

트랜스포머 모델에서는 번역 원문과 그에대한 번역 정답을 준비함으로써 지도학습을 수행했지만,

디코더의 경우는 따로 정답을 준비할 것 없이 문장 자체가 정답이 되기 때문입니다.

온전한 문장이 하나 있다면 마스킹을 통해 각 문장 위치별 어떤 단어가 예측되어야 하는지를 알 수 있으니까요.

이에 대해서는 트랜스포머 정리글에서 정리를 했으니 생략합니다.

사용하는 손실 함수 역시 트랜스포머와 동일한 Cross Entropy 입니다.

 

이와 같은 학습 방식으로,

학습 데이터는 약 7GB 의 양질의 문장 데이터가 저장된 데이터셋인 BooksCorpus 를 사용.

특정 도메인에 특화되어 있지 않는 범용적인 언어를 사용하도록 훈련시키는 것이 특징입니다.

GPT 에서 시작된 LLM 모델은 어마어마하게 많은 파라미터를 가지고 있으며, 이를 훈련시키기 위한 데이터와 시간 역시 어마어마합니다.

 

특화 언어 모델을 만들기 위해 처음부터 모든 파라미터를 학습시키는 것 보다는,

기초적이고 범용적인 단어를 말하는 법을 배운 후 특화된 언어를 사용하도록 튜닝하는 것이 GPT 모델이 제시한 학습 방식이며,

이는 실제적으로 효과적이고 효율적이라는 것이 증명되었습니다.

 

마치 어린아이에게 말을 가르치는 단계에 변호사나 의사의 말하는 방식을 가르치지 않고, 일단 올바른 말하는 방식을 익힌 후에 각자 필요에 맞는 언어를 배운다는 예시로 이해하시면 됩니다.

 

2. Fine-tuning

앞서 사전학습된 범용 LLM 모델을 기반으로 특정 태스크에 특화된 데이터들을 모아서 학습시키는 것입니다.

허깅 페이스와 같은 곳에 공개된 LLM 모델을 가지고 각 산업별 특화 모델을 만드는 것으로 이해하시면 됩니다.(허깅 페이스가 이러한 이유로 나온 것만 같네요.)

 

단순히 문장만을 입력하는게 아니라, 이를 통해 특수 목적을 달성하도록 할 수도 있는데, 예를들어,

[문장] : this movie was fantastic!, [감정] : Positive

 

이렇게 구성하여 [문장] 태그 뒤에 오는 문장에 대하여, [감정] 태그 뒤로 그 문장의 감정을 반환하도록 훈련하거나,

[전문]: The pandemic affected many countries globally. ... [요약]: The pandemic had global impact.

 

이렇게 전체 문장이 있을 때, 요약 문장을 반환하도록 할 수도 있고,

[Context]: The Eiffel Tower is located in Paris. [Question]: Where is the Eiffel Tower? [Answer]: Paris

 

조건, 질문, 답변 의 형태로 문장을 생성하게도 할 수 있습니다.

 

파인튜닝의 유형을 몇가지로 정리 하자면,

1. 감정분석

2. 요약

3. Q&A

4. 코드 생성

5. 말투 변환

6. 도메인 특화

 

이러합니다.

 

이렇게 확인하니 GPT 모델의 파인 튜닝이 다른 딥러닝 모델의 파인튜닝보다 재밌는 점으로는,

방대한 데이터로 범용 언어 모델을 만든 후 위와 같이 단순 생성과는 전혀 달라 보이는 작업까지도 커버가 가능하도록 간편하고 효과적으로 훈련시킬 수 있다는 것이네요.

 

(GPT 모델 생성 및 학습 실습)

- OpenAI가 2018년에 발표한 "Improving Language Understanding by Generative Pre-Training" 논문(GPT-1)을 기반으로 하여 GPT-1 모델을 Pytorch 로 만들어 학습시킨 후 테스트까지 진행하겠습니다.

 

- GPT-1 모델의 논문상 모델 구조는 아래와 같습니다.

기반 구조: Transformer Decoder
레이어 수: 12개
히든 사이즈: 768
헤드 수: 12
파라미터 수: 약 1.17억 개 (117M)

토크나이저 : Byte Pair Encoding (BPE)
입력 토큰 수: 최대 512 tokens

 

- GPT-1 모델 Pytorch 코드는 아래와 같습니다.

gpt.py

import math
import torch
from torch import nn
import torch.nn.functional as F


class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size, emb_size):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, emb_size)
        self.scale = math.sqrt(emb_size)

    def forward(self, tokens):
        return self.embedding(tokens.long()) * self.scale


class PositionalEmbedding(nn.Module):
    def __init__(self, d_model, max_len=512):
        super().__init__()
        self.pos_embedding = nn.Parameter(torch.zeros(1, max_len, d_model))
        nn.init.trunc_normal_(self.pos_embedding, std=0.02)

    def forward(self, x):
        seq_len = x.size(1)
        return self.pos_embedding[:, :seq_len, :]


class InputEmbedding(nn.Module):
    def __init__(self, vocab_size, d_model, dropout=0.1):
        super().__init__()
        self.token = TokenEmbedding(vocab_size, d_model)
        self.position = PositionalEmbedding(d_model=d_model)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x):
        token_emb = self.token(x)
        pos_emb = self.position(x)
        return self.dropout(token_emb + pos_emb)


class Attention(nn.Module):
    def __init__(self, dropout=0.1):
        super().__init__()

        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        scores = torch.matmul(query, key.transpose(-2, -1)) \
                 / math.sqrt(query.size(-1))

        if mask is not None:
            scores = scores.float()
            scores = scores.masked_fill(mask == 0, -1e9)

        p_attn = F.softmax(scores, dim=-1)

        p_attn = self.dropout(p_attn)

        return torch.matmul(p_attn, value), p_attn


class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        super().__init__()
        assert d_model % h == 0

        self.d_k = d_model // h
        self.h = h

        self.linear_layers = nn.ModuleList([nn.Linear(d_model, d_model) for _ in range(3)])
        self.output_linear = nn.Linear(d_model, d_model)
        self.attention = Attention(dropout)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)

        query, key, value = [l(x).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)
                             for l, x in zip(self.linear_layers, (query, key, value))]

        x, attn = self.attention(query, key, value, mask=mask)

        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.h * self.d_k)

        return self.output_linear(x)


class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.gamma = nn.Parameter(torch.ones(features))
        self.beta = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True, unbiased=False)
        return self.gamma * (x - mean) / (std + self.eps) + self.beta


class SublayerConnection(nn.Module):
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))


class ReLU(nn.Module):
    def forward(self, x):
        return torch.clamp(x, min=0)


class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.activation = ReLU()
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        x = self.linear1(x)
        x = self.activation(x)
        x = self.linear2(x)
        return self.dropout(x)


class DecoderBlock(nn.Module):
    def __init__(self, d_model, heads, d_ff, dropout=0.1):
        super().__init__()
        self.attn = MultiHeadedAttention(heads, d_model, dropout)
        self.ff = PositionwiseFeedForward(d_model, d_ff, dropout)
        self.sublayers = nn.ModuleList([
            SublayerConnection(d_model, dropout),
            SublayerConnection(d_model, dropout)
        ])

    def forward(self, x, mask=None):
        x = self.sublayers[0](x, lambda _x: self.attn(_x, _x, _x, mask))
        x = self.sublayers[1](x, self.ff)
        return x


class GPT(nn.Module):
    def __init__(self, vocab_size, d_model, n_layers, heads, d_ff, max_len=512, dropout=0.1):
        super().__init__()
        self.inputEmbedding = InputEmbedding(vocab_size, d_model, dropout)

        self.blocks = nn.ModuleList([
            DecoderBlock(d_model, heads, d_ff, dropout) for _ in range(n_layers)
        ])
        self.ln_f = LayerNorm(d_model)
        self.head = nn.Linear(d_model, vocab_size, bias=False)

        self.head.weight = self.inputEmbedding.token.embedding.weight

        self.max_len = max_len

    def generate_square_subsequent_mask(self, sz):
        mask = torch.tril(torch.ones(sz, sz)).bool()
        return mask

    def forward(self, x):
        batch_size, seq_len = x.size()
        if seq_len > self.max_len:
            raise ValueError(f"Sequence length {seq_len} exceeds max length {self.max_len}")

        mask = self.generate_square_subsequent_mask(seq_len).to(x.device)
        mask = mask.unsqueeze(0).unsqueeze(0)

        x = self.inputEmbedding(x)

        for block in self.blocks:
            x = block(x, mask)

        x = self.ln_f(x)
        logits = self.head(x)
        return logits

 

트랜스포머 모델을 기준으로 보자면 컨셉적인 부분을 제외하고  크게 달라진 것이 없습니다.

 

변경된 부분만 보자면,

DecoderLayer 에서 CrossAttention 을 삭제하였으며,

각 레이어들을 조합한 GPT 모델에서는 인코더를 제거하고 재배치 한 것, decoder 벡터 생성 후 norm 을 적용한 것 정도입니다.

 

와중에 generator 의 weight 를 token_emb 의 embedding 레이어의 weight 로 공유해 사용하도록 하였습니다.

weight tying 이라는 기법으로, 입력 임베딩과 출력 레이어 가중치를 서로 공유함으로써, 파라미터 수 감소, 과적합 방지, 학습 안정성 향상 등의 효과가 있다고 합니다.

이것이 가능한 이유는, 임베딩 레이어가

단어 -> 의미 벡터

변환 기능을 수행하고, 출력 레이어가

의미 벡터 -> 단어

변환 기능을 수행하기 때문이라고 합니다.

왜 변환 방향이 달라지는데 동일 가중치가 사용되는지에 대해서는 저도 더 자세히 알아보고 정리하겠습니다.

 

- 학습 코드

import torch
from torch import nn, optim
from torch.utils.data import DataLoader
from datasets import load_dataset
from transformers import GPT2TokenizerFast
from tqdm.auto import tqdm
from gpt import GPT  # 앞서 정의한 GPT 모델 클래스

# ------------------------------
# Hyperparameters and Config
# ------------------------------
VOCAB_SIZE = None  # 나중에 토크나이저 로딩 후 설정
D_MODEL = 768
N_LAYERS = 12
HEADS = 12
D_FF = 3072
MAX_LEN = 512
DROPOUT = 0.1
BATCH_SIZE = 8
BLOCK_SIZE = 128
LR = 5e-5
EPOCHS = 3
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# ------------------------------
# 데이터셋 준비 (Wikitext-2)
# ------------------------------
# 3.1. HuggingFace Dataset 로드
raw_datasets = load_dataset("wikitext", "wikitext-2-raw-v1")
# split: "train", "validation", "test"

# 3.2. GPT-2 토크나이저 불러오기
tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")
tokenizer.add_special_tokens({"pad_token": "[PAD]"})
VOCAB_SIZE = len(tokenizer)


# 3.3. 텍스트를 토큰화하여 input_ids 컬럼 생성
def tokenize_function(examples):
    return tokenizer(examples["text"])


tokenized_datasets = raw_datasets.map(
    tokenize_function,
    batched=True,
    remove_columns=["text"]
)


# 3.4. 토큰 시퀀스를 BLOCK_SIZE 길이로 묶기
def group_texts(examples):
    concatenated = []
    for ids in examples["input_ids"]:
        concatenated.extend(ids)
    total_length = (len(concatenated) // BLOCK_SIZE) * BLOCK_SIZE
    concatenated = concatenated[:total_length]

    result = [
        concatenated[i: i + BLOCK_SIZE]
        for i in range(0, total_length, BLOCK_SIZE)
    ]
    return {"input_ids": result}


# ← 여기서 "input_ids"와 "attention_mask"를 함께 제거하도록 수정
lm_datasets = tokenized_datasets.map(
    group_texts,
    batched=True,
    batch_size=1000,
    remove_columns=["input_ids", "attention_mask"]
)


# 3.5. PyTorch DataLoader 준비
def collate_fn(batch):
    input_ids = torch.stack(
        [torch.tensor(example["input_ids"], dtype=torch.long) for example in batch]
    )
    return input_ids


train_dataset = lm_datasets["train"]
val_dataset = lm_datasets["validation"]

train_dataloader = DataLoader(
    train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn
)
val_dataloader = DataLoader(
    val_dataset, batch_size=BATCH_SIZE, shuffle=False, collate_fn=collate_fn
)

# ------------------------------
# 모델, 손실함수, 옵티마이저
# ------------------------------
model = GPT(
    vocab_size=VOCAB_SIZE,
    d_model=D_MODEL,
    n_layers=N_LAYERS,
    heads=HEADS,
    d_ff=D_FF,
    max_len=MAX_LEN,
    dropout=DROPOUT
).to(DEVICE)

optimizer = optim.Adam(model.parameters(), lr=LR)
criterion = nn.CrossEntropyLoss()


# ------------------------------
# 학습 함수 정의
# ------------------------------
def train_one_epoch(model, dataloader, optimizer, criterion, device):
    model.train()
    total_loss = 0.0
    for batch in tqdm(dataloader, desc="Training", leave=False):
        batch = batch.to(device)  # (batch_size, BLOCK_SIZE)

        logits = model(batch)  # (batch, BLOCK_SIZE, vocab_size)

        shift_logits = logits[:, :-1, :].contiguous()
        shift_labels = batch[:, 1:].contiguous()
        loss = criterion(
            shift_logits.view(-1, VOCAB_SIZE),
            shift_labels.view(-1)
        )

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    return total_loss / len(dataloader)


def evaluate(model, dataloader, criterion, device):
    model.eval()
    total_loss = 0.0
    with torch.no_grad():
        for batch in tqdm(dataloader, desc="Validation", leave=False):
            batch = batch.to(device)
            logits = model(batch)
            shift_logits = logits[:, :-1, :].contiguous()
            shift_labels = batch[:, 1:].contiguous()
            loss = criterion(
                shift_logits.view(-1, VOCAB_SIZE),
                shift_labels.view(-1)
            )
            total_loss += loss.item()
    return total_loss / len(dataloader)


# ------------------------------
# 전체 학습 루프
# ------------------------------
for epoch in range(1, EPOCHS + 1):
    train_loss = train_one_epoch(model, train_dataloader, optimizer, criterion, DEVICE)
    val_loss = evaluate(model, val_dataloader, criterion, DEVICE)
    print(f"Epoch {epoch}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}")
    torch.save(model, "model.pth")

 

학습 데이터는 wikitext 데이터셋을 사용 했습니다.

 

GPT-1 모델의 논문 내용에 맞춰 파라미터를 설정하였으며, transformer 와 같이 model 의 logits 와 정답 값이 되는 입력 문장과의 cross entropy 를 사용해서 구한 loss 값으로 학습을 하였습니다.

파인 튜닝을 하려면 기존 모델을 불러와서 새로운 데이터로 추가 학습을 시키면 됩니다.

학습 진행

 

학습은 위와 같이 진행됩니다.

 

- 테스트 코드

import torch
from transformers import GPT2TokenizerFast
import torch.nn.functional as F


def generate_text(model, tokenizer, device, start_prompt="", max_new_tokens=1000, temperature=1.0):
    model.eval()
    generated = []

    # 시작 토큰 시퀀스 생성
    if start_prompt:
        input_ids = tokenizer.encode(start_prompt, return_tensors="pt").to(device)
    else:
        # 시작 문구 없으면 임의 토큰 하나 생성 (예: <BOS> 또는 eos_token)
        input_ids = torch.tensor(
            [[tokenizer.bos_token_id if tokenizer.bos_token_id is not None else tokenizer.eos_token_id]], device=device)

    with torch.no_grad():
        for _ in range(max_new_tokens):
            # 입력 시퀀스가 너무 길면 뒤쪽 일부만 자르기 (모델 최대 길이 내로)
            if input_ids.size(1) > model.max_len:
                input_ids = input_ids[:, -model.max_len:]

            logits = model(input_ids)  # (batch=1, seq_len, vocab_size)
            logits = logits[:, -1, :] / temperature  # 마지막 토큰의 로짓만 사용 (1, vocab_size)
            probs = F.softmax(logits, dim=-1)

            # 샘플링: 확률 분포에서 다음 토큰 선택
            next_token_id = torch.multinomial(probs, num_samples=1)

            # 시퀀스에 다음 토큰 추가
            input_ids = torch.cat([input_ids, next_token_id], dim=-1)

            generated.append(next_token_id.item())

    generated_text = tokenizer.decode(generated, clean_up_tokenization_spaces=True)
    return generated_text


def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # 토크나이저 로드 (학습 시 사용한 것과 동일)
    tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # 모델 로드 (학습 시 저장한 경로)
    model = torch.load("model.pth", map_location=device, weights_only=False)
    model.to(device)

    # 시작 문구 설정 (빈 문자열 가능)
    start_prompt = "The meaning of life is"

    generated_text = generate_text(model, tokenizer, device, start_prompt=start_prompt, max_new_tokens=1000)
    print("Generated Text:\n")
    print(generated_text)


if __name__ == "__main__":
    main()

 

위에서 학습 시킨 모델을 불러와 데이터를 생성하도록 하였습니다.

1000 단어를 생성합니다.

테스트 결과

 

앞서 1000 에포크만 학습한 모델로 생성한 문장은 위와 같습니다.

보시다시피 학습이 모자라 제대로 생성은 되지 않습니다.

 

(GPT-1 모델의 성과와 한계)

- GPT-1 모델이 만들어져 이뤄낸 성과는,

1. NLP 범용 모델을 뿌리로하여(Pre-Train) 특정 도메인 지식을 보유한 데이터를 학습해(Fine-Tune) 특화 모델을 만들어내는 것이 가능함을 증명하였습니다.

2. 트랜스포머 모델의 어텐션에 대한 우수성을 다시한번 증명하였으며, 디코더만 사용하더라도 단어 생성에 전혀 문제가 없음을 증명하였습니다.

3. 모델 완성도 및 효과성으로 인해 GPT 시리즈를 만들어 발전하는 첫번째 발판이 되었습니다.

4. 당시 표준 기법보다 텍스트 분류, 의미 유사도 파악, 질의응답 등 모든면에서 성과를 내었습니다.

5. 사람이 설계한 복잡한 구조 없이 단순한 딥러닝 모델만으로 언어의 이해가 가능하다는 것을 증명하였습니다.

즉, 생물 신경망을 흉내낸 인공 신경망이 사이즈만 충분하다면 정말 생물과 같은 유연한 사고력을 지님을 증명한 것이죠.

 

- GPT-1 모델의 한계는,

1. 파라미터 수가 상대적으로 작아 복잡한 언어 이해에 대한 성능에는 한계가 있었습니다.

117M 파라미터는 지금 기순에서는 작고, 복잡한 추론엔 한계가 있습니다.

성능상으로는 우수성이 검증되었으므로 이후 GPT 사이즈 경쟁을 불러일으키죠.

2. 단방향 : 좌->우 방향만 보기에 문맥의 전후 관계를 동시에 이해하는데에 제약이 있습니다.

3. 입력 토큰 수 제한과 함께 긴 문맥 추론에 약합니다.

4. 언어 이해에는 약합니다. 기본 컨셉부터가 다음 단어 추론에 중점을 둔 모델이므로 당시에는 단어를 뱉어내는 앵무새라는 평가도 받았을 정도로 이해하지 않고 말을 한다는 성향이 강했습니다.(할루시네이션 등의 문제도 있었음)

5. few-shot 학습 불가능 : 쉽게 말하면 프롬프트 기반 학습이 지원되지 않는다는 뜻으로, 별도의 파인튜닝 없이도 프롬프트 안에서 규칙을 파악해 문제를 해결하는 능력을 의미합니다. 지금은 추론 능력이 부족하다는 것으로 이해하면 되며, 나중에 이를 개선된 모델을 정리시 few-shot, zero-shot 등의 설명도 같이 하겠습니다.

 

(GPT 시리즈 발전 흐름)

- 위에서 정리한 GPT-1 모델의 한계를 바탕으로 이후 관련 모델이 어떻게 발전했는지를 간결하게 살펴보겠습니다.

 

GPT-1(2018) : 매개 변수 수 1.17억, Transformer 모델에서 디코더를 제거한 생성형 모델의 모범적인 형태를 만들어냈습니다.

모델이 감당 가능한 최대 컨텍스트 길이는 512 입니다.

 

GPT-2(2019) : 전 버전에 비해 매개변수를 대폭 증가시켜 15억으로 증량되었습니다. 모델 규모의 성능 상승을 증명하여 본격적인 LLM 규모 경쟁이 시작되었습니다. 활성화 함수로 RELU 가 아닌 당시 효과성을 입증한 GELU 를 사용하기 시작하였고, 고정 함수로 사용하였던 위치 인코딩 방식을 학습 가능한 위치 인코딩 방식을 적용하였습니다.

또한 모델이 커짐에 따라 '학습 안정성'을 중시하게 되었으며, 결과 Layer Norm 의 위치를 Residual 뒤에 적용하는 것이 아닌 앞에 적용하여 Residual 의 효과를 높였습니다.

최대 컨텍스트 길이는 1024 토큰입니다.

 

GPT-3(2020) : 매개 변수가 1750억으로 비교도 할 수 없이 커졌습니다. 학습 방식에 Few-shot 방식을 적용하였습니다.

최대 컨텍스트 길이는 2048 토큰입니다.

이 모델의 한계는, 매개변수가 커진만큼 성능도 높아졌지만, 더불어 학습에 필요한 데이터도 많아졌다는 것입니다.

일반적인 사람이 평생 보는 정보보다 많은 데이터를 학습함으로도 사람 수준의 능력을 아직 구현 못했으며, 게임과 같은 것을 실행시켰을 때 이전에 둔 수를 기억하지 못하는 등 학습 효율이 나쁘며, 단순한 GPT 모델 자체로는 한계가 존재함을 알게 되었습니다.

(= 단순한 문장 생성기로서의 단점)

 

GPT-3.5(2022) : GPT-3 의 단점을 조금이라도 해소하고자 나온 모델입니다.

ChatGPT 출시와 함께 나왔다고 하네요.

이때부터 GPT 모델 세부 내용이 공개되지 않았으며,

파라미터수는 GPT-3 보다 증가되었을 것이라 보이며, 컨텍스트 길이는 4096 토큰입니다.

자세한 내용은 추후 게시글을 정리할 것입니다.

 

GPT-4(2023) : 매개변수는 비공개. 아마 수조로 추정되며, 기반 구조 역시 변화가 있을 것으로 예상되지만 알려지지 않았습니다.

멀티모달이 적용되었습니다. GPT-4 Turbo, GPT-4o, GPT-4o mini, GPT-4.1, GPT-4.5 와 같이 파생 모델이 많으며, 아마 OpenAI 가 GPT 모델을 본격적으로 사업의 일환으로 본 여파로 제품으로서 특화된 모델을 개발한 결과로 보입니다.

GPT 다음 모델은 2025 년 5월인 현재 GPT-5 모델이 개발중입니다.

저작자표시 비영리 변경금지 (새창열림)

'Study > NLP' 카테고리의 다른 글

[딥러닝] ViT 모델 정리(BERT 기반 Image Encoder, Pytorch ViT 이미지 분류 모델 구현)  (2) 2025.05.28
[딥러닝] BERT 모델 정리(Encoder Only Transformer, Pytorch BERT 자연어 인코딩 모델 구현)  (0) 2025.05.27
[딥러닝] Transformer 모델 정리(LLM 모델 이해, Multi Head Attention, Pytorch 딥러닝 번역기 구현)  (0) 2025.05.22
[LLM] LLM 모델 Multi-Modal 에 대한 기본 이해와 적용(이미지 + 텍스트 해석 AI 구현)  (1) 2025.05.21
[Langchain] 생성형 AI 서버 구축 (LLM 모델 네트워크 서빙 방식 정리)  (0) 2025.05.21
'Study/NLP' 카테고리의 다른 글
  • [딥러닝] ViT 모델 정리(BERT 기반 Image Encoder, Pytorch ViT 이미지 분류 모델 구현)
  • [딥러닝] BERT 모델 정리(Encoder Only Transformer, Pytorch BERT 자연어 인코딩 모델 구현)
  • [딥러닝] Transformer 모델 정리(LLM 모델 이해, Multi Head Attention, Pytorch 딥러닝 번역기 구현)
  • [LLM] LLM 모델 Multi-Modal 에 대한 기본 이해와 적용(이미지 + 텍스트 해석 AI 구현)
Railly Linker
Railly Linker
IT 지식 정리 및 공유 블로그
  • Railly Linker
    Railly`s IT 정리노트
    Railly Linker
  • 전체
    오늘
    어제
  • 공지사항

    • 분류 전체보기 (111) N
      • Programming (34)
        • BackEnd (18)
        • FrontEnd (3)
        • DBMS (1)
        • ETC (12)
      • Study (76) N
        • Computer Science (21)
        • Data Science (21) N
        • Computer Vision (16)
        • NLP (15)
        • ETC (3)
      • Error Note (1)
      • ETC (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 태그

    network_mode: "host"
    list
    데이터베이스 제약
    unique
    docker compose
    논리적 삭제
    MacOS
    kotlin arraylist
    localhost
    kotlin linkedlist
    Kotlin
    지리 정보
    jvm 메모리 누수
    docker 배포
    kotlin mutablelist
    단축키
    springboot 배포
  • 링크

    • RaillyLinker Github
  • hELLO· Designed By정상우.v4.10.0
Railly Linker
[딥러닝] GPT 모델 정리(Decoder Only Transformer, Pytorch GPT-1 자연어 생성 모델 구현)
상단으로

티스토리툴바