<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>cedis 님의 블로그</title>
    <link>https://cedis.tistory.com/</link>
    <description>cedis 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 5 Jun 2026 09:13:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>cedis</managingEditor>
    <item>
      <title>mini GPT 과제 2편 - GPTDataset과 InputEmbedding 구현 리뷰</title>
      <link>https://cedis.tistory.com/269</link>
      <description>&lt;main style=&quot;max-width: 860px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #0f766e; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;mini GPT 과제 랩 구현 시리즈 2편&lt;/p&gt;
&lt;h1 style=&quot;margin: 0 0 18px; font-size: 30px; line-height: 1.32; color: #111827;&quot;&gt;GPTDataset과 InputEmbedding 구현 리뷰&lt;/h1&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;BPE가 token ID 목록을 만들었다면, `GPTDataset`은 이 목록을 학습 가능한 input/target 쌍으로 잘라낸다. `InputEmbedding`은 그 ID를 Transformer가 계산할 수 있는 벡터로 바꾼다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 단계는 코드 양은 많지 않지만, 뒤의 attention과 model shape를 결정한다. 여기서 shape를 잘못 잡으면 나중에 attention에서 에러가 늦게 터진다.&lt;/p&gt;
&lt;section style=&quot;background: #ecfdf5; border: 1px solid #99f6e4; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #115e59;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 통과 근거&lt;/h2&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;tests/test_dataset.py
- GPTDataset 길이 계산
- __getitem__ input/target shape
- DataLoader batch shape
- InputEmbedding 출력 shape

결과: 4 passed&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. Dataset 길이는 context_length와 stride로 결정된다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;한 샘플은 input `context_length`개와 target `context_length`개를 만들어야 한다. target은 input보다 한 칸 뒤이므로 실제로는 `context_length + 1`개의 token이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.stride = stride if stride is not None else context_length
self._length = max(
    0,
    (len(token_ids) - context_length - 1) // self.stride + 1,
)&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;&lt;b&gt;왜 `- 1`이 들어가나&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0;&quot; data-ke-size=&quot;size16&quot;&gt;input만 만들면 `context_length`개로 충분하다. 하지만 target은 한 칸 뒤까지 필요하므로 마지막 token 하나를 더 읽을 수 있어야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. __getitem__은 input과 target을 한 칸 차이로 만든다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 구현은 단순하지만 의미는 중요하다. 같은 구간을 거의 그대로 쓰되 target만 시작 위치를 하나 뒤로 민다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;start = idx * self.stride
end = start + self.context_length

input_ids = self.token_ids[start:end]
target_ids = self.token_ids[start + 1: end + 1]

return (
    torch.tensor(input_ids, dtype=torch.long),
    torch.tensor(target_ids, dtype=torch.long),
)&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff;&quot;&gt;
&lt;div style=&quot;font-weight: bold; margin-bottom: 12px;&quot;&gt;예시: context_length = 4&lt;/div&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 90px 1fr; gap: 10px; align-items: center;&quot;&gt;
&lt;div style=&quot;font-weight: bold;&quot;&gt;원본&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 8px; flex-wrap: wrap;&quot;&gt;&lt;span style=&quot;background: #f1f5f9; border-radius: 8px; padding: 7px 9px;&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;background: #f1f5f9; border-radius: 8px; padding: 7px 9px;&quot;&gt;11&lt;/span&gt;&lt;span style=&quot;background: #f1f5f9; border-radius: 8px; padding: 7px 9px;&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;background: #f1f5f9; border-radius: 8px; padding: 7px 9px;&quot;&gt;13&lt;/span&gt;&lt;span style=&quot;background: #f1f5f9; border-radius: 8px; padding: 7px 9px;&quot;&gt;14&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;font-weight: bold; color: #1e3a8a;&quot;&gt;input&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 8px; flex-wrap: wrap;&quot;&gt;&lt;span style=&quot;background: #dbeafe; border-radius: 8px; padding: 7px 9px;&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;background: #dbeafe; border-radius: 8px; padding: 7px 9px;&quot;&gt;11&lt;/span&gt;&lt;span style=&quot;background: #dbeafe; border-radius: 8px; padding: 7px 9px;&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;background: #dbeafe; border-radius: 8px; padding: 7px 9px;&quot;&gt;13&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;font-weight: bold; color: #166534;&quot;&gt;target&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 8px; flex-wrap: wrap;&quot;&gt;&lt;span style=&quot;background: #dcfce7; border-radius: 8px; padding: 7px 9px;&quot;&gt;11&lt;/span&gt;&lt;span style=&quot;background: #dcfce7; border-radius: 8px; padding: 7px 9px;&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;background: #dcfce7; border-radius: 8px; padding: 7px 9px;&quot;&gt;13&lt;/span&gt;&lt;span style=&quot;background: #dcfce7; border-radius: 8px; padding: 7px 9px;&quot;&gt;14&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. create_dataloader는 Dataset을 batch 단위로 감싼다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;DataLoader는 학습 루프가 한 번에 여러 샘플을 처리할 수 있게 묶어준다. 이때 반환 shape는 뒤의 모델 입력과 바로 연결된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;dataset = GPTDataset(token_ids, context_length, stride=stride)

return DataLoader(
    dataset,
    batch_size=batch_size,
    shuffle=shuffle,
    drop_last=drop_last,
    num_workers=num_workers,
)&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. InputEmbedding은 token embedding과 position embedding을 더한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;token embedding은 &amp;ldquo;무슨 토큰인가&amp;rdquo;를 표현하고, position embedding은 &amp;ldquo;몇 번째 위치인가&amp;rdquo;를 표현한다. GPT 입력은 이 둘을 더한 값이다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.token_embedding = nn.Embedding(vocab_size, emb_dim)
self.position_embedding = nn.Embedding(context_length, emb_dim)
self.dropout = nn.Dropout(drop_rate)

def forward(self, x):
    batch_size, seq_len = x.shape
    positions = torch.arange(seq_len, device=x.device)
    token_embeddings = self.token_embedding(x)
    position_embeddings = self.position_embedding(positions)
    return self.dropout(token_embeddings + position_embeddings)&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 620px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;값&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;shape&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;x&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;(B, T)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;token ID batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;token_embeddings&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;(B, T, C)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;각 토큰의 벡터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;position_embeddings&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;(T, C)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;각 위치의 벡터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;output&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;(B, T, C)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;TransformerBlock 입력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;이 구현이 통과한 핵심 계약&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dataset 길이가 만들 수 있는 샘플 개수와 일치한다.&lt;/li&gt;
&lt;li&gt;각 샘플의 input과 target은 `context_length` 길이를 가진다.&lt;/li&gt;
&lt;li&gt;DataLoader는 `(batch_size, context_length)` 형태의 batch를 만든다.&lt;/li&gt;
&lt;li&gt;InputEmbedding 출력은 `(batch_size, seq_len, emb_dim)`이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 구현 글에서는 `MultiHeadAttention`을 본다. 여기서부터 모델은 각 위치의 토큰이 이전 문맥을 어떤 비율로 참고할지 계산하기 시작한다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: Dataset은 다음 토큰 예측 문제를 만들고, InputEmbedding은 token ID와 위치 정보를 더해 Transformer가 받을 입력 shape를 만든다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/269</guid>
      <comments>https://cedis.tistory.com/269#entry269comment</comments>
      <pubDate>Fri, 5 Jun 2026 00:24:34 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 과제 1편 - src/bpe.py byte-level BPE 구현 리뷰</title>
      <link>https://cedis.tistory.com/268</link>
      <description>&lt;main style=&quot;max-width: 860px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #0f766e; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;mini GPT 과제 랩 구현 시리즈 1편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 구현의 첫 관문은 토크나이저였다. 과제에서는 외부 tokenizer를 금지했기 때문에, 한국어 리뷰 문자열을 직접 UTF-8 byte-level BPE 방식으로 token ID 목록으로 바꿔야 했다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 실제 구현된 `src/bpe.py`를 기준으로 초기 사전 구성, BPE 학습, 저장/로드, encode/decode 복원을 코드 블록 단위로 본다.&lt;/p&gt;
&lt;section style=&quot;background: #ecfdf5; border: 1px solid #99f6e4; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #115e59;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 통과 근거&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;노트북 실행 결과 기준 `tests/test_bpe.py`는 6개 테스트가 통과했다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;tests/test_bpe.py
- special token ID 고정
- 초기 vocabulary 구성
- save/load 복원
- encode/decode 원문 복원
- get_pad_id/get_bos_id/get_eos_id 확인
- train 이후 vocabulary 증가 확인

결과: 6 passed&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. ID 배치를 먼저 고정한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;토크나이저에서 가장 먼저 정해야 하는 것은 약속이다. 어떤 ID가 padding이고, 어떤 ID가 문장 시작과 끝인지 고정되어 있어야 Dataset과 fine-tuning에서도 같은 기준을 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;PAD_TOKEN = &quot;&amp;lt;pad&amp;gt;&quot;
UNK_TOKEN = &quot;&amp;lt;unk&amp;gt;&quot;
BOS_TOKEN = &quot;&amp;lt;bos&amp;gt;&quot;
EOS_TOKEN = &quot;&amp;lt;eos&amp;gt;&quot;

SPECIAL_TOKENS = [PAD_TOKEN, UNK_TOKEN, BOS_TOKEN, EOS_TOKEN]
SPECIAL_IDS = {token: idx for idx, token in enumerate(SPECIAL_TOKENS)}
BYTE_OFFSET = len(SPECIAL_TOKENS)
NUM_BYTES = 256&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 560px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;ID 범위&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;의미&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;왜 필요한가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;0~3&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;특수 토큰&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;padding, unknown, 문장 시작/끝 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;4~259&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;byte 0~255&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;모든 UTF-8 문자열을 최소 단위로 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;260 이상&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;BPE merge token&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;자주 붙는 byte/token 조합을 압축&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. 초기 vocabulary는 모든 byte를 포함한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;`_init_special_tokens()`는 BPE 학습을 하는 함수가 아니다. 모든 문자를 최소한 byte 단위로 표현할 수 있도록 기본 사전을 까는 함수다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def _init_special_tokens(self):
    self.id_to_token = {}
    self.token_to_id = {}
    self.merges = []

    for idx, token in enumerate(SPECIAL_TOKENS):
        self.id_to_token[idx] = token
        self.token_to_id[token] = idx

    for byte_value in range(NUM_BYTES):
        token_id = BYTE_OFFSET + byte_value
        token = bytes([byte_value])
        self.id_to_token[token_id] = token
        self.token_to_id[token] = token_id&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 `bytes([byte_value])`로 저장하는 이유가 중요하다. 나중에 decode할 때 merge token을 원본 byte까지 재귀적으로 펼쳐야 하기 때문이다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. train은 가장 자주 나온 pair를 하나씩 합친다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;학습은 같은 corpus의 token ID 목록을 계속 압축해 가는 과정이다. 매 반복마다 인접 pair 빈도를 다시 세고, 가장 자주 나온 pair 하나를 새 토큰으로 등록한다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;corpus.encode(&quot;utf-8&quot;)&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;문자열을 byte 값으로 바꿈&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;pair_counts&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;이웃한 token pair 빈도 계산&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #fff7ed; border-left: 5px solid #ea580c; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;best_pair&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;가장 많이 나온 pair를 새 token ID로 등록&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #fff1f2; border-left: 5px solid #be123c; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;ids 갱신&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;해당 pair를 새 ID 하나로 치환&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;while len(self.id_to_token) &amp;lt; self.vocab_size and len(ids) &amp;gt;= 2:
    pair_counts = {}
    for i in range(len(ids) - 1):
        pair = (ids[i], ids[i + 1])
        pair_counts[pair] = pair_counts.get(pair, 0) + 1

    best_pair = max(pair_counts, key=pair_counts.get)
    best_count = pair_counts[best_pair]
    if best_count &amp;lt; 2:
        break

    new_id = len(self.id_to_token)
    self.merges.append(best_pair)
    self.id_to_token[new_id] = best_pair
    self.token_to_id[best_pair] = new_id&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;`best_count &amp;lt; 2`에서 멈추는 선택도 의미가 있다. 한 번만 나온 조합까지 계속 합치면 반복 패턴을 배운다기보다 corpus를 억지로 외우는 쪽에 가까워진다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. encode는 학습된 merge 순서를 재현한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;encode는 새로운 token을 만들면 안 된다. 학습 때 저장한 `self.merges`를 같은 순서로 적용해, 새 문장을 기존 vocabulary 안의 ID 목록으로 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;ids = [BYTE_OFFSET + b for b in text.encode(&quot;utf-8&quot;)]

for pair in self.merges:
    merge_id = self.token_to_id.get(pair)
    if merge_id is None:
        continue
    ids = merge_current_pair(ids, pair, merge_id)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;BPE에서 merge 순서가 바뀌면 결과 ID도 달라질 수 있다. 그래서 `save()`와 `load()`는 vocabulary뿐 아니라 merge rule의 순서도 보존해야 한다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;5. decode는 merge token을 byte까지 펼친 뒤 한 번에 복원한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;가장 위험한 실수는 token ID 하나를 곧바로 문자 하나로 생각하는 것이다. merge token 안에는 다른 token ID 두 개가 들어 있고, 그 안에 또 merge token이 들어 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def decode(self, ids, skip_special=True):
    byte_values = []

    for token_id in ids:
        if skip_special and token_id in SPECIAL_IDS.values():
            continue
        byte_values.extend(self._token_to_bytes(token_id))

    return bytes(byte_values).decode(&quot;utf-8&quot;, errors=&quot;replace&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;&lt;b&gt;실수 방지 포인트&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0;&quot; data-ke-size=&quot;size16&quot;&gt;한글은 여러 byte가 모여 한 글자가 된다. 따라서 byte를 중간에 하나씩 문자열로 바꾸면 깨질 수 있고, 원본 byte를 모두 모은 뒤 마지막에 한 번만 UTF-8로 복원해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;이 구현이 통과한 핵심 계약&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`&amp;lt;pad&amp;gt;`, `&amp;lt;unk&amp;gt;`, `&amp;lt;bos&amp;gt;`, `&amp;lt;eos&amp;gt;` ID가 고정되어 있다.&lt;/li&gt;
&lt;li&gt;모든 byte 0~255가 기본 vocabulary에 들어간다.&lt;/li&gt;
&lt;li&gt;학습한 vocabulary를 저장하고 다시 불러와도 encode/decode가 유지된다.&lt;/li&gt;
&lt;li&gt;한글, 영어, 숫자, 문장부호가 섞인 문장도 decode(encode(text))로 복원된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 구현 글에서는 `GPTDataset`과 `InputEmbedding`을 본다. BPE가 만든 token ID 목록을 &amp;ldquo;다음 토큰 예측&amp;rdquo; 학습 샘플로 자르고, 모델 입력 벡터로 바꾸는 단계다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: 이번 BPE 구현의 핵심은 한글을 byte 단위에서 안전하게 시작하고, 학습된 merge 규칙을 저장해 encode와 decode가 같은 약속을 공유하게 만드는 것이다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/268</guid>
      <comments>https://cedis.tistory.com/268#entry268comment</comments>
      <pubDate>Mon, 1 Jun 2026 11:31:36 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 과제 0편 - 구현 결과와 전체 구조 지도</title>
      <link>https://cedis.tistory.com/267</link>
      <description>&lt;main style=&quot;max-width: 860px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #0f766e; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;mini GPT 과제 랩 구현 시리즈 0편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 과제는 PyTorch만 사용해 작은 GPT 계열 언어 모델을 직접 구현하는 과제였다. 완성 목표는 ChatGPT 같은 대형 모델이 아니라, LLM의 핵심 부품을 직접 만들어 보며 내부 흐름을 이해하는 것이다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 정답 코드를 한 번에 던지는 글이 아니다. 실제 구현 브랜치와 노트북 실행 결과를 기준으로, 어떤 파일이 어떤 책임을 맡았고 어떤 테스트가 그 책임을 확인했는지 먼저 지도처럼 정리한다.&lt;/p&gt;
&lt;section style=&quot;background: #ecfdf5; border: 1px solid #99f6e4; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #115e59;&quot; data-ke-size=&quot;size26&quot;&gt;최종 확인 결과&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;제출 노트북에는 Python 3.11 가상환경과 CUDA 환경에서 실행한 테스트 결과가 저장되어 있었다. 현재 글은 이 실행 결과와 실제 소스 코드를 근거로 작성한다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 560px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #ccfbf1; border: 1px solid #99f6e4; padding: 10px; text-align: left;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;background: #ccfbf1; border: 1px solid #99f6e4; padding: 10px; text-align: left;&quot;&gt;결과&lt;/th&gt;
&lt;th style=&quot;background: #ccfbf1; border: 1px solid #99f6e4; padding: 10px; text-align: left;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;전체 테스트&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;&lt;b&gt;28 passed&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;필수 구현 함수가 단위 테스트 기준으로 연결됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;실행 환경&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;Python 3.11, CUDA&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;과제 권장 환경에서 실행 기록 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;장기 사전학습 로그&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;best val loss 약 4.5587&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #99f6e4; padding: 10px;&quot;&gt;학습 루프와 checkpoint 흐름이 실제로 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. 과제는 여섯 개의 큰 부품으로 나뉜다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;파일 이름만 보면 많아 보이지만 구현 흐름은 선형적이다. 토크나이저가 텍스트를 숫자로 바꾸고, 데이터셋이 다음 토큰 예측 샘플을 만들고, 모델이 logits를 만들고, 학습 루프가 loss를 줄인다.&lt;/p&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #d7dee9; border-radius: 16px; padding: 16px;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 12px; padding: 12px;&quot;&gt;&lt;span style=&quot;background: #2563eb; color: #ffffff; border-radius: 999px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; font-weight: bold;&quot;&gt;1&lt;/span&gt;
&lt;div&gt;&lt;b&gt;BPE tokenizer&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;한글 리뷰 문자열을 UTF-8 byte 기반 token ID로 바꾼다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start; background: #f0fdfa; border: 1px solid #99f6e4; border-radius: 12px; padding: 12px;&quot;&gt;&lt;span style=&quot;background: #0f766e; color: #ffffff; border-radius: 999px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; font-weight: bold;&quot;&gt;2&lt;/span&gt;
&lt;div&gt;&lt;b&gt;Dataset / Embedding&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;입력과 정답을 한 칸 차이로 만들고, token ID를 벡터로 바꾼다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start; background: #faf5ff; border: 1px solid #e9d5ff; border-radius: 12px; padding: 12px;&quot;&gt;&lt;span style=&quot;background: #9333ea; color: #ffffff; border-radius: 999px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; font-weight: bold;&quot;&gt;3&lt;/span&gt;
&lt;div&gt;&lt;b&gt;Multi-Head Attention&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;각 토큰이 이전 문맥 중 어디를 볼지 계산한다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 12px; padding: 12px;&quot;&gt;&lt;span style=&quot;background: #ea580c; color: #ffffff; border-radius: 999px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; font-weight: bold;&quot;&gt;4&lt;/span&gt;
&lt;div&gt;&lt;b&gt;GPTModel&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;LayerNorm, GELU, FeedForward, TransformerBlock을 조립한다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start; background: #fff1f2; border: 1px solid #fecdd3; border-radius: 12px; padding: 12px;&quot;&gt;&lt;span style=&quot;background: #be123c; color: #ffffff; border-radius: 999px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; font-weight: bold;&quot;&gt;5&lt;/span&gt;
&lt;div&gt;&lt;b&gt;Pretraining utilities&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;loss 계산, checkpoint, generate, 학습 루프를 담당한다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start; background: #f8fafc; border: 1px solid #cbd5e1; border-radius: 12px; padding: 12px;&quot;&gt;&lt;span style=&quot;background: #334155; color: #ffffff; border-radius: 999px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; font-weight: bold;&quot;&gt;6&lt;/span&gt;
&lt;div&gt;&lt;b&gt;Fine-tuning&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;GPT backbone 위에 감성 분류용 classification head를 붙인다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. 테스트는 무엇을 검증했나&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 통과 숫자만 적으면 별 의미가 없다. 중요한 것은 각 테스트가 어떤 책임을 확인했는지다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 640px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;테스트 파일&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;통과 수&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;핵심 검증&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;test_bpe.py&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;특수 토큰 ID, save/load, 한글 encode/decode 복원, BPE 학습&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;test_dataset.py&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;input/target 길이, DataLoader batch shape, embedding 출력 shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;test_attention.py&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;MHA 출력 shape, causal mask에서 미래 위치 attention 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;test_model.py&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;LayerNorm, GELU, FFN, Block, GPT forward, loss, greedy generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;test_train.py&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;batch loss, loader loss, checkpoint, temperature/top-k generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;test_finetune.py&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;NSMC 분리, 리뷰 padding, classification head shape, train/eval 함수 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. 전체 테스트 로그는 이렇게 읽는다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 12px;&quot; data-ke-size=&quot;size16&quot;&gt;전체 테스트 통과는 좋은 신호지만, 모델이 좋은 문장을 만든다는 뜻은 아니다. 여기서 보장되는 것은 각 부품이 약속한 shape와 최소 동작 조건을 만족한다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;pytest tests/ -v
collected 28 items

test_attention.py  2 passed
test_bpe.py        6 passed
test_dataset.py    4 passed
test_finetune.py   4 passed
test_model.py      7 passed
test_train.py      5 passed&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이 시리즈는 테스트를 단순히 &amp;ldquo;통과했다&amp;rdquo;로 끝내지 않는다. 각 편에서 &amp;ldquo;이 테스트가 무엇을 막아주는가&amp;rdquo;까지 같이 볼 것이다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. 학습 로그는 결과보다 한계를 같이 봐야 한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;노트북에는 실제 CUDA 환경에서 사전학습을 돌린 기록이 남아 있다. loss는 줄었고 checkpoint를 이어서 학습한 흔적도 있다. 다만 생성 샘플에는 깨진 문자와 어색한 표현이 남아 있었다.&lt;/p&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit,minmax(230px,1fr)); gap: 12px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;확인된 성과&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;train loss와 validation loss가 장기 실행에서 감소했다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;주의할 한계&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;loss 감소가 곧 자연스러운 한국어 생성 품질을 의미하지는 않는다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;다음 분석 지점&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;vocab 크기, 데이터 양, 모델 크기, 학습 시간의 영향을 따로 봐야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;latest epoch: 1390
global_step: 305800
train_loss: 3.9400
val_loss: 4.5616
best_val_loss: 4.5587&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;이번 글에서 기억할 것&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;과제는 BPE부터 fine-tuning까지 GPT 흐름을 작은 부품으로 나누어 구현한다.&lt;/li&gt;
&lt;li&gt;전체 테스트 28개 통과는 필수 부품의 최소 동작 계약을 만족했다는 의미다.&lt;/li&gt;
&lt;li&gt;학습 로그는 구현이 실제로 돌아간 근거지만, 생성 품질까지 자동으로 보장하지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BPE 테스트에서 한글 encode/decode 복원을 확인해야 하는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;attention 테스트에서 causal mask를 확인하지 않으면 어떤 문제가 숨어 있을 수 있는가?&lt;/li&gt;
&lt;li&gt;fine-tuning에서 LM head와 classification head는 왜 역할이 다른가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 첫 구현 항목인 byte-level BPE를 본다. 핵심은 &amp;ldquo;한글을 깨지 않고 숫자 ID로 바꾸고 다시 원문으로 복원하는 법&amp;rdquo;이다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: 이번 과제의 성과는 한 번에 완성된 챗봇이 아니라, GPT를 이루는 부품들이 테스트와 학습 로그로 연결되었다는 점이다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/267</guid>
      <comments>https://cedis.tistory.com/267#entry267comment</comments>
      <pubDate>Mon, 1 Jun 2026 11:31:14 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 7편 - GPT를 감성 분류기로 바꾸는 미세조정</title>
      <link>https://cedis.tistory.com/266</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 7편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;사전학습된 GPT는 기본적으로 다음 토큰을 맞히는 모델이다. 하지만 과제의 마지막 단계에서는 NSMC 리뷰가 긍정인지 부정인지 맞히는 분류 모델로 바꾼다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 GPT 전체를 버리는 것이 아니다. 텍스트를 읽어 hidden state를 만드는 backbone은 그대로 쓰고, 마지막 목적에 맞는 classification head를 새로 붙인다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;LM head와 classification head&lt;/h2&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 620px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #dbeafe; border: 1px solid #bfdbfe; padding: 10px; text-align: left;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;background: #dbeafe; border: 1px solid #bfdbfe; padding: 10px; text-align: left;&quot;&gt;출력&lt;/th&gt;
&lt;th style=&quot;background: #dbeafe; border: 1px solid #bfdbfe; padding: 10px; text-align: left;&quot;&gt;목적&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #bfdbfe; padding: 10px;&quot;&gt;LM head&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #bfdbfe; padding: 10px;&quot;&gt;vocab_size개 점수&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #bfdbfe; padding: 10px;&quot;&gt;다음 토큰 예측&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #bfdbfe; padding: 10px;&quot;&gt;classification head&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #bfdbfe; padding: 10px;&quot;&gt;2개 점수&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #bfdbfe; padding: 10px;&quot;&gt;부정/긍정 분류&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;이번 단계의 입력과 정답&lt;/h2&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #ffffff; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;{&quot;text&quot;: &quot;배우들의 연기가 좋고 끝까지 몰입됐다.&quot;, &quot;label&quot;: 1}
{&quot;text&quot;: &quot;전개가 지루하고 결말도 아쉬웠다.&quot;, &quot;label&quot;: 0}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 12px 0 0;&quot; data-ke-size=&quot;size16&quot;&gt;사전학습에서는 문장 안의 다음 token이 정답이었다. 미세조정에서는 문장 하나에 label 하나가 붙는다. 이 차이 때문에 출력층도 바뀌어야 한다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. 리뷰 전체를 대표하는 벡터가 필요하다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;GPT backbone은 각 token 위치마다 hidden state를 만든다. 하지만 감성 분류는 리뷰 전체에 대해 하나의 label만 필요하다. 그래서 여러 hidden state 중 문장을 대표할 하나를 골라 classifier에 넣는다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;리뷰 token IDs&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr; GPT backbone&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px;&quot;&gt;각 token의 hidden state&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr; 마지막 유효 token 선택&lt;/div&gt;
&lt;div style=&quot;background: #fff7ed; border-left: 5px solid #ea580c; border-radius: 10px; padding: 12px;&quot;&gt;문장 대표 벡터&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr; Linear classifier&lt;/div&gt;
&lt;div style=&quot;background: #fff1f2; border-left: 5px solid #be123c; border-radius: 10px; padding: 12px;&quot;&gt;부정/긍정 logits&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. classification head는 vocab이 아니라 label을 본다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;사전학습의 `lm_head`는 hidden state 하나를 `vocab_size`개의 점수로 바꾼다. 감성 분류에서는 &amp;ldquo;다음 token 후보 전체&amp;rdquo;가 필요하지 않다. 필요한 것은 부정과 긍정 두 개의 점수다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;문장 대표 hidden state: (B, C)
classifier: Linear(C, 2)
logits: (B, 2)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 GPT backbone은 재사용하지만, 마지막 head는 과제 목적에 맞게 바꿔 끼운다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. padding은 분류 위치 선택에서 제외해야 한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;리뷰 길이는 제각각이다. batch로 묶기 위해 짧은 리뷰는 pad token으로 채운다. 이때 마지막 위치를 무조건 고르면 pad의 hidden state를 대표 벡터로 쓰는 실수가 생긴다.&lt;/p&gt;
&lt;div style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 16px;&quot;&gt;&lt;b&gt;핵심 판단&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 token이 아니라, padding이 아닌 마지막 유효 token의 hidden state를 골라야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: 800; color: #111827; margin-bottom: 10px;&quot;&gt;위치 선택 예시&lt;/div&gt;
&lt;pre class=&quot;vim&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #ffffff; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;input_ids:
[좋다, 영화, &amp;lt;eos&amp;gt;, &amp;lt;pad&amp;gt;, &amp;lt;pad&amp;gt;]

대표 위치:
마지막 index 4가 아니라, 마지막 유효 token인 &amp;lt;eos&amp;gt; 위치 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. 미세조정에서 학습되는 것은 &amp;ldquo;분류 기준&amp;rdquo;이다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;backbone은 문장을 hidden state로 바꾸는 역할을 한다. classifier는 그 hidden state를 보고 부정/긍정 기준을 학습한다. 이때 loss는 다음 token 예측용 cross entropy가 아니라 label 0/1에 대한 cross entropy다.&lt;/p&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit,minmax(230px,1fr)); gap: 12px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;사전학습&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;각 위치마다 다음 token ID를 맞힌다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;미세조정&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;리뷰 전체의 label 0/1을 맞힌다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;감성 분류에서 LM head를 그대로 쓰면 왜 맞지 않는가?&lt;/li&gt;
&lt;li&gt;리뷰 전체 label을 예측하려면 token별 hidden state 중 무엇을 선택해야 하는가?&lt;/li&gt;
&lt;li&gt;padding 위치를 대표 벡터로 쓰면 어떤 문제가 생기는가?&lt;/li&gt;
&lt;li&gt;사전학습 loss와 분류 미세조정 loss는 무엇이 다른가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;시리즈 마무리&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지 오면 mini GPT 구현의 큰 흐름은 연결된다. 텍스트는 BPE로 token ID가 되고, Dataset과 Embedding을 거쳐 attention과 Transformer block을 통과하고, 사전학습 또는 분류 미세조정 목표에 맞게 loss를 계산한다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: 미세조정은 GPT backbone의 문장 이해 흐름을 재사용하고, 마지막 목적에 맞는 작은 head를 새로 붙이는 과정이다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/266</guid>
      <comments>https://cedis.tistory.com/266#entry266comment</comments>
      <pubDate>Mon, 1 Jun 2026 11:30:48 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 6편 - 사전학습, loss, 생성, checkpoint</title>
      <link>https://cedis.tistory.com/265</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 6편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;GPTModel이 logits를 만들 수 있게 되면, 이제 학습 루프가 필요하다. 사전학습의 목적은 다음 토큰 예측 loss를 줄이는 것이다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이때 loss 계산, optimizer update, 검증 loss, 텍스트 생성, checkpoint는 따로 떨어진 기능처럼 보이지만 하나의 학습 루프 안에서 연결된다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;사전학습 루프&lt;/h2&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;batch
-&amp;gt; model(input, targets)
-&amp;gt; cross entropy loss
-&amp;gt; loss.backward()
-&amp;gt; optimizer.step()
-&amp;gt; 주기적으로 validation loss 확인
-&amp;gt; 주기적으로 checkpoint 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;학습과 생성은 같은 모델을 다르게 쓰는 일이다&lt;/h2&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 620px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;입력&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;출력 사용법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;사전학습&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;input IDs + target IDs&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;모든 위치의 logits로 loss 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;생성&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;지금까지 만든 input IDs&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;마지막 위치 logits에서 다음 token 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. loss는 다음 토큰 예측이 얼마나 틀렸는지다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;모델 출력 logits의 shape는 `(batch_size, seq_len, vocab_size)`다. 각 위치마다 vocabulary 전체에 대한 점수를 낸다. target은 `(batch_size, seq_len)`이고, 각 위치의 정답 token ID를 가진다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px;&quot;&gt;&lt;b&gt;cross entropy 계산 전 shape 변환&lt;/b&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;margin: 10px 0 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #ffffff; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;logits:  (B, T, V) -&amp;gt; (B*T, V)
targets: (B, T)    -&amp;gt; (B*T)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. 생성은 마지막 위치의 logits에서 다음 token을 고르는 일이다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;텍스트 생성은 학습과 다르게 target이 없다. 현재까지 만든 token ID를 모델에 넣고, 마지막 위치의 logits에서 다음 token을 고른다. 그 token을 뒤에 붙이고 다시 반복한다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;현재 문맥 token IDs&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px;&quot;&gt;모델 forward&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #fff7ed; border-left: 5px solid #ea580c; border-radius: 10px; padding: 12px;&quot;&gt;마지막 위치 logits 선택&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #fff1f2; border-left: 5px solid #be123c; border-radius: 10px; padding: 12px;&quot;&gt;argmax 또는 sampling으로 다음 token 선택&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;temperature가 0이면 가장 높은 점수의 token을 고르는 greedy 방식이 된다. temperature가 크면 확률적으로 더 다양한 token을 고를 수 있다. top-k는 후보를 상위 k개로 제한한다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;&lt;b&gt;생성 설정을 읽는 감각&lt;/b&gt;
&lt;ul style=&quot;margin: 8px 0 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`temperature = 0`: 가장 그럴듯한 token만 고른다. 결과는 안정적이지만 반복적일 수 있다.&lt;/li&gt;
&lt;li&gt;`temperature &amp;gt; 0`: 확률적으로 뽑는다. 다양해지지만 이상한 token도 나올 수 있다.&lt;/li&gt;
&lt;li&gt;`top_k`: 후보를 줄여 너무 낮은 확률의 token이 뽑히는 일을 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. checkpoint는 가중치만 저장하는 것이 아니다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;Colab 학습은 끊길 수 있다. 이어서 학습하려면 모델 가중치뿐 아니라 optimizer 상태, epoch, global step도 함께 저장해야 한다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 560px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;저장 항목&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;model state&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;현재 학습된 가중치 복원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;optimizer state&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;Adam 같은 optimizer 내부 상태 복원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;epoch / global_step&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;어디까지 학습했는지 이어가기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;loss 로그를 읽을 때 주의할 점&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;loss가 줄어드는 것은 모델이 다음 토큰 예측을 더 잘하고 있다는 신호다. 하지만 작은 모델과 제한된 데이터에서는 자연스러운 한국어 생성 품질까지 바로 보장하지 않는다. loss와 생성 샘플을 함께 봐야 한다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. train loss와 validation loss는 서로 다른 질문이다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;train loss는 모델이 학습에 사용한 데이터에 얼마나 맞춰지고 있는지를 본다. validation loss는 학습에 직접 쓰지 않은 데이터에서도 비슷하게 동작하는지를 본다.&lt;/p&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit,minmax(230px,1fr)); gap: 12px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;둘 다 감소&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;학습이 정상적으로 진행 중일 가능성이 높다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;train만 감소&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;훈련 데이터에만 맞춰지는 과적합을 의심한다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 14px; padding: 14px;&quot;&gt;&lt;b&gt;둘 다 정체&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;모델 크기, 학습률, 데이터, tokenizer 설정을 다시 본다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;cross entropy 전에 logits와 targets를 왜 펼치는가?&lt;/li&gt;
&lt;li&gt;temperature와 top-k는 생성 결과에 어떤 영향을 주는가?&lt;/li&gt;
&lt;li&gt;checkpoint에 optimizer state가 필요한 이유는 무엇인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 GPT backbone 위에 classification head를 붙여 감성 분류 모델로 바꾸는 fine-tuning 흐름을 본다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: 사전학습은 다음 토큰 예측 loss를 줄이는 반복이고, 생성과 checkpoint는 그 학습 상태를 관찰하고 이어가기 위한 도구다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/265</guid>
      <comments>https://cedis.tistory.com/265#entry265comment</comments>
      <pubDate>Mon, 1 Jun 2026 11:30:33 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 5편 - GPT Block을 이루는 LayerNorm, GELU, Residual</title>
      <link>https://cedis.tistory.com/264</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 5편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;attention만으로 GPT 모델이 완성되지는 않는다. attention 결과를 안정적으로 쌓고, 각 위치별로 비선형 변환을 수행하고, 깊은 층에서도 신호가 흐르도록 만드는 장치들이 필요하다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 GPT block을 이루는 핵심 부품인 LayerNorm, GELU, FeedForward, residual connection을 한 흐름으로 정리한다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;GPT block의 기본 흐름&lt;/h2&gt;
&lt;pre class=&quot;clean&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;x
-&amp;gt; LayerNorm
-&amp;gt; Causal Multi-Head Attention
-&amp;gt; residual add
-&amp;gt; LayerNorm
-&amp;gt; FeedForward
-&amp;gt; residual add&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 12px 0 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름에서 가장 중요한 규칙은 shape가 계속 `(B, T, C)`로 유지된다는 점이다. 그래야 원래 입력과 새 계산 결과를 residual로 더할 수 있다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. LayerNorm은 각 토큰 벡터를 안정화한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;LayerNorm은 마지막 차원, 즉 한 토큰의 embedding 벡터 내부를 기준으로 평균과 분산을 맞춘다. batch 전체를 기준으로 보는 BatchNorm과 다르게, 문장 길이나 batch 크기가 바뀌어도 토큰 단위로 동작한다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 560px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;정규화&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;기준&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;GPT에서의 감각&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;LayerNorm&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;각 토큰 벡터의 마지막 차원&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;한 토큰의 표현을 안정화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;BatchNorm&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;batch 방향 통계&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;언어 모델보다 CNN 등에서 더 익숙한 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. GELU는 딱 잘라 버리지 않는 비선형 함수다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;FeedForward 안에는 보통 GELU가 들어간다. ReLU가 0보다 작으면 바로 0으로 잘라내는 함수라면, GELU는 값을 더 부드럽게 통과시키거나 줄인다. 여기서 중요한 것은 이름을 외우는 것이 아니라, GPT block 안에서 &amp;ldquo;선형층만 반복되는 구조&amp;rdquo;를 막는 비선형 변환이 필요하다는 점이다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: 800; color: #111827; margin-bottom: 10px;&quot;&gt;ReLU와 GELU를 감으로 구분하기&lt;/div&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit,minmax(220px,1fr)); gap: 12px;&quot;&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 12px; padding: 12px;&quot;&gt;&lt;b&gt;ReLU&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;음수는 0, 양수는 그대로. 단순하고 빠르다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 12px; padding: 12px;&quot;&gt;&lt;b&gt;GELU&lt;/b&gt;
&lt;p style=&quot;margin: 8px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;입력을 확률적으로 부드럽게 통과시키는 느낌에 가깝다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. FeedForward는 각 위치별 작은 MLP다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;attention이 토큰들 사이의 관계를 섞는다면, FeedForward는 각 위치의 벡터를 더 풍부한 표현으로 바꾼다. 보통 `d_model -&amp;gt; 4*d_model -&amp;gt; d_model` 구조를 사용한다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;d_model&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;현재 토큰 표현&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr; Linear&lt;/div&gt;
&lt;div style=&quot;background: #faf5ff; border-left: 5px solid #9333ea; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;4 * d_model&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;표현 공간 확장&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr; GELU&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;d_model&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;다음 block이 받을 크기로 복귀&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. Residual connection은 원래 신호를 살려둔다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;Transformer block은 attention이나 FeedForward 결과를 그대로 다음으로 넘기지 않는다. 원래 입력 `x`에 새로 계산한 결과를 더한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;x = x + attention(layer_norm(x))
x = x + feed_forward(layer_norm(x))&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 깊은 모델에서도 정보와 gradient가 더 잘 흐른다. 처음 공부할 때 residual을 단순한 더하기로만 보면 안 된다. 깊은 층을 안정적으로 쌓기 위한 핵심 통로다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;5. pre-norm 구조로 읽으면 block이 덜 헷갈린다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 구현은 attention이나 FeedForward에 넣기 전에 먼저 LayerNorm을 적용한다. 그래서 흐름을 `정규화 -&amp;gt; 계산 -&amp;gt; 원래 값에 더하기`로 읽으면 된다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 12px; padding: 12px;&quot;&gt;&lt;b&gt;attention branch&lt;/b&gt;&lt;br /&gt;x + attention(layer_norm(x))&lt;/div&gt;
&lt;div style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 12px; padding: 12px;&quot;&gt;&lt;b&gt;feed-forward branch&lt;/b&gt;&lt;br /&gt;x + feed_forward(layer_norm(x))&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 관점으로 보면 GPT block은 낯선 부품 묶음이 아니라, 같은 패턴을 두 번 반복하는 구조가 된다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;이번 글에서 기억할 것&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LayerNorm은 토큰 벡터의 스케일을 안정화한다.&lt;/li&gt;
&lt;li&gt;GELU는 FeedForward 안에서 비선형 변환을 담당한다.&lt;/li&gt;
&lt;li&gt;FeedForward는 각 위치의 표현을 비선형으로 변환한다.&lt;/li&gt;
&lt;li&gt;Residual connection은 깊은 block을 쌓을 수 있게 하는 정보 통로다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;LayerNorm은 어느 차원을 기준으로 평균과 분산을 계산하는가?&lt;/li&gt;
&lt;li&gt;GELU가 없다면 FeedForward는 어떤 종류의 변환만 반복하게 되는가?&lt;/li&gt;
&lt;li&gt;FeedForward가 `4*d_model`로 확장했다가 다시 줄어드는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;Residual connection이 없다면 깊은 모델에서 어떤 문제가 커질 수 있는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 loss 계산, 생성, checkpoint를 포함한 사전학습 흐름을 정리한다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: GPT block은 attention 하나가 아니라, 정규화와 비선형 변환과 residual connection이 함께 묶인 반복 가능한 층이다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/264</guid>
      <comments>https://cedis.tistory.com/264#entry264comment</comments>
      <pubDate>Mon, 1 Jun 2026 11:30:05 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 4편 - Multi-Head Attention은 왜 head를 나누는가</title>
      <link>https://cedis.tistory.com/263</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 4편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;self-attention 하나로도 각 토큰이 문맥을 참고할 수 있다. 그런데 GPT는 보통 attention을 하나만 쓰지 않고 여러 head로 나눈다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 여러 관점이다. 같은 문장을 보더라도 한 head는 가까운 단어 관계를, 다른 head는 문장 전체 분위기를, 또 다른 head는 특정 패턴을 더 잘 보도록 학습될 수 있다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;이번 글에서 다루는 것&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;head를 나눈다는 말의 shape 의미&lt;/li&gt;
&lt;li&gt;`d_model`과 `n_heads`, `head_dim`의 관계&lt;/li&gt;
&lt;li&gt;각 head 결과를 다시 합치는 이유&lt;/li&gt;
&lt;li&gt;Multi-Head Attention을 shape 흐름으로 읽는 법&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. head는 embedding 차원을 나눈 작은 attention 공간이다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;입력 벡터의 마지막 차원이 `d_model`이라고 하자. head를 4개로 나누면 각 head는 `d_model / 4` 크기의 작은 공간에서 attention을 계산한다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: bold; margin-bottom: 12px;&quot;&gt;예: d_model = 128, n_heads = 4&lt;/div&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit,minmax(130px,1fr)); gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 12px;&quot;&gt;head 1&lt;br /&gt;&lt;b&gt;32차원&lt;/b&gt;&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border: 1px solid #99f6e4; border-radius: 10px; padding: 12px;&quot;&gt;head 2&lt;br /&gt;&lt;b&gt;32차원&lt;/b&gt;&lt;/div&gt;
&lt;div style=&quot;background: #faf5ff; border: 1px solid #e9d5ff; border-radius: 10px; padding: 12px;&quot;&gt;head 3&lt;br /&gt;&lt;b&gt;32차원&lt;/b&gt;&lt;/div&gt;
&lt;div style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 10px; padding: 12px;&quot;&gt;head 4&lt;br /&gt;&lt;b&gt;32차원&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 `d_model`은 `n_heads`로 나누어떨어져야 한다. 나누어떨어지지 않으면 각 head가 같은 크기의 `head_dim`을 가질 수 없다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. shape는 이렇게 움직인다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;Multi-Head Attention을 이해할 때는 수식보다 shape를 먼저 보는 편이 좋다. 입력은 `(B, T, C)`이고, head를 나누면 `(B, H, T, D)`가 된다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot; style=&quot;margin: 16px 0; padding: 16px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;x:          (B, T, C)
q, k, v:    (B, T, C)
split head: (B, H, T, D)
score:      (B, H, T, T)
context:    (B, H, T, D)
merge:      (B, T, C)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 `T x T`는 각 위치가 다른 위치를 얼마나 볼지 나타내는 attention weight 행렬이다. causal mask도 바로 이 `T x T` 위에서 적용된다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;&lt;b&gt;숫자를 넣은 예&lt;/b&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 10px 0 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #ffffff; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;B = 2, T = 5, C = 128, H = 4
D = C / H = 32

x:      (2, 5, 128)
split:  (2, 4, 5, 32)
score:  (2, 4, 5, 5)
merge:  (2, 5, 128)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 10px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;head를 나눠도 최종 출력 차원은 원래 C로 돌아온다. 다음 Transformer block이 같은 형식의 입력을 받아야 하기 때문이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. 여러 head 결과는 다시 하나의 벡터로 합쳐진다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;head를 나누는 것은 모델 출력을 여러 갈래로 분리해서 끝내겠다는 뜻이 아니다. 각 head가 계산한 문맥 벡터를 다시 이어 붙여 원래 `d_model` 크기로 되돌린다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;head별 attention&lt;/b&gt; - 각 head가 자기 공간에서 문맥 계산&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;concat&lt;/b&gt; - head 결과를 마지막 차원으로 이어 붙임&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b;&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;output projection&lt;/b&gt; - 다시 다음 layer가 쓰기 좋은 `d_model` 표현으로 섞음&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;head를 나누는 이유를 한 문장으로&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Multi-Head Attention은 하나의 큰 attention으로 모든 관계를 한 번에 보려 하지 않고, embedding 차원을 여러 작은 관점으로 나누어 각 관점에서 문맥을 본 뒤 다시 합치는 방식이다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;구현할 때 자주 나는 실수&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`view` 뒤에 head 차원을 앞으로 옮기지 않으면 score shape가 맞지 않는다.&lt;/li&gt;
&lt;li&gt;`transpose` 이후에는 메모리가 연속적이지 않을 수 있어, 병합 전 `contiguous()`가 필요할 수 있다.&lt;/li&gt;
&lt;li&gt;head를 합친 뒤에는 output projection을 거쳐 head별 결과를 다시 섞는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`d_model`이 `n_heads`로 나누어떨어져야 하는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;attention weight의 shape가 `(B, H, T, T)`가 되는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;head를 나눈 뒤 왜 다시 합쳐야 하는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 LayerNorm, GELU, FeedForward, residual connection을 묶어 GPT Block을 만드는 흐름을 본다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: Multi-Head Attention은 여러 관점으로 문맥을 본 뒤, 그 결과를 다시 하나의 표현으로 합치는 구조다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/263</guid>
      <comments>https://cedis.tistory.com/263#entry263comment</comments>
      <pubDate>Sun, 31 May 2026 00:19:18 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 3편 - Self-Attention과 Causal Mask</title>
      <link>https://cedis.tistory.com/262</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 3편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;embedding까지 끝나면 각 토큰은 벡터가 된다. 하지만 벡터가 되었다고 해서 문맥을 이해하는 것은 아니다. 각 위치의 토큰이 앞뒤 토큰과 어떤 관계를 맺는지 계산해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;self-attention은 같은 문장 안의 토큰들이 서로를 참고하는 방식이다. GPT에서는 여기에 causal mask가 붙어서 현재 토큰이 미래 토큰을 보지 못하게 만든다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;이번 글에서 다루는 것&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;attention이 &amp;ldquo;어떤 토큰을 얼마나 참고할지&amp;rdquo; 정하는 방식&lt;/li&gt;
&lt;li&gt;Query, Key, Value를 너무 어렵게 보지 않는 방법&lt;/li&gt;
&lt;li&gt;causal mask가 왜 다음 토큰 예측에서 필수인지&lt;/li&gt;
&lt;li&gt;attention score, softmax, weighted sum의 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. Attention은 참고 비율을 계산한다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;문장 안의 각 토큰은 혼자 의미를 가지지 않는다. &amp;ldquo;좋았다&amp;rdquo;라는 토큰은 앞에 &amp;ldquo;영화가&amp;rdquo;, &amp;ldquo;정말&amp;rdquo; 같은 토큰이 있을 때 더 분명해진다. attention은 현재 위치가 다른 위치를 얼마나 참고할지 비율을 계산한다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: bold; margin-bottom: 12px;&quot;&gt;현재 토큰: &amp;ldquo;좋았다&amp;rdquo;&lt;/div&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;영화가&lt;/b&gt; &lt;span style=&quot;color: #64748b;&quot;&gt;참고 비율 0.20&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;정말&lt;/b&gt; &lt;span style=&quot;color: #64748b;&quot;&gt;참고 비율 0.35&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;좋았다&lt;/b&gt; &lt;span style=&quot;color: #64748b;&quot;&gt;참고 비율 0.45&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;실제 값은 사람이 정하지 않는다. Query와 Key의 유사도를 계산하고 softmax를 통과시켜 비율로 만든다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 16px; margin: 18px 0;&quot;&gt;&lt;b&gt;작은 숫자로 보면&lt;/b&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 10px 0 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #ffffff; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;현재 토큰의 score
영화가: 1.0
정말:   2.0
좋았다: 2.4

softmax 뒤 참고 비율
영화가: 0.12
정말:   0.33
좋았다: 0.55&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 10px 0 0; color: #475569;&quot; data-ke-size=&quot;size16&quot;&gt;score는 비교용 점수이고, softmax 뒤의 값은 합이 1인 참고 비율이다. 이 비율로 Value를 섞으면 현재 위치의 문맥 벡터가 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. Query, Key, Value는 역할 이름이다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;Query, Key, Value라는 이름 때문에 처음에는 어렵게 느껴진다. 하지만 구현 관점에서는 같은 입력 벡터를 세 개의 다른 선형층에 통과시킨 결과다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 620px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;이름&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;직관&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;계산에서 하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;Query&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;내가 찾고 싶은 기준&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;Key와 점곱해 attention score를 만듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;Key&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;각 토큰이 가진 검색용 표지&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;Query와 얼마나 맞는지 비교됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;Value&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;실제로 가져올 정보&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;attention weight와 곱해져 문맥 벡터가 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. Causal mask는 미래 정답을 가린다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;GPT는 다음 토큰을 맞히는 모델이다. 그런데 학습 중 현재 위치가 미래 토큰을 볼 수 있다면, 정답을 미리 훔쳐보는 셈이 된다. causal mask는 이 부정행위를 막는다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: bold; margin-bottom: 12px;&quot;&gt;볼 수 있는 위치 표시&lt;/div&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; min-width: 520px; width: 100%; text-align: center; font-size: 14px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #f1f5f9;&quot;&gt;토큰 0&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #dcfce7;&quot;&gt;보기 가능&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #fee2e2;&quot;&gt;가림&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #fee2e2;&quot;&gt;가림&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #fee2e2;&quot;&gt;가림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #f1f5f9;&quot;&gt;토큰 1&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #dcfce7;&quot;&gt;보기 가능&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #dcfce7;&quot;&gt;보기 가능&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #fee2e2;&quot;&gt;가림&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #fee2e2;&quot;&gt;가림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #f1f5f9;&quot;&gt;토큰 2&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #dcfce7;&quot;&gt;보기 가능&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #dcfce7;&quot;&gt;보기 가능&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #dcfce7;&quot;&gt;보기 가능&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 8px; background: #fee2e2;&quot;&gt;가림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;구현에서는 보통 현재 위치보다 오른쪽 위 영역을 `-inf`로 채운다. 그러면 softmax를 통과한 뒤 그 위치의 확률이 0에 가까워진다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;attention 흐름 요약&lt;/h2&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;x
-&amp;gt; Q, K, V
-&amp;gt; Q @ K.T / sqrt(head_dim)
-&amp;gt; causal mask
-&amp;gt; softmax
-&amp;gt; attention weight @ V
-&amp;gt; context vector&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;헷갈리기 쉬운 구분&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;score는 아직 확률이 아니다. softmax 전의 비교 점수다.&lt;/li&gt;
&lt;li&gt;mask는 Value를 지우는 것이 아니라 score 단계에서 미래 위치를 막는다.&lt;/li&gt;
&lt;li&gt;최종 출력은 attention weight 자체가 아니라, 그 weight로 Value를 섞은 벡터다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Value는 언제 사용되는가?&lt;/li&gt;
&lt;li&gt;causal mask가 없다면 train loss는 좋아 보일 수 있지만 왜 위험한가?&lt;/li&gt;
&lt;li&gt;attention score를 softmax에 넣는 이유는 무엇인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 여러 개의 attention head를 병렬로 사용하는 Multi-Head Attention을 정리한다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: self-attention은 문맥 안에서 참고 비율을 계산하고, causal mask는 현재 토큰이 미래 정답을 보지 못하게 막는다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/262</guid>
      <comments>https://cedis.tistory.com/262#entry262comment</comments>
      <pubDate>Sun, 31 May 2026 00:18:57 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 2편 - Dataset과 Embedding, 다음 토큰 예측 샘플 만들기</title>
      <link>https://cedis.tistory.com/261</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 2편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;BPE가 문장을 token ID 목록으로 바꾸었다면, 다음 단계는 그 목록을 학습 샘플로 자르는 일이다. GPT는 현재까지의 토큰을 보고 다음 토큰을 맞히도록 학습한다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이 단계에서 자주 헷갈리는 지점은 두 가지다. 첫째, input과 target은 길이가 같지만 한 칸 밀려 있다. 둘째, token ID는 그 자체로 의미 벡터가 아니므로 embedding을 거쳐야 한다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;이번 글에서 다루는 것&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPTDataset이 input과 target을 어떻게 만드는지&lt;/li&gt;
&lt;li&gt;context_length와 stride가 샘플 수에 어떤 영향을 주는지&lt;/li&gt;
&lt;li&gt;token embedding과 position embedding을 왜 더하는지&lt;/li&gt;
&lt;li&gt;최종 입력 shape가 왜 `(batch_size, seq_len, emb_dim)`인지&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. 다음 토큰 예측은 input과 target을 한 칸 어긋나게 만든다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;token ID 목록이 `[10, 11, 12, 13]`이고 context 길이가 3이라고 하자. 모델은 `[10, 11, 12]`를 보고 `[11, 12, 13]`을 맞히도록 훈련된다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: bold; margin-bottom: 12px;&quot;&gt;한 칸 밀린 학습 샘플&lt;/div&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 90px 1fr; gap: 10px; align-items: center;&quot;&gt;
&lt;div style=&quot;font-weight: bold; color: #1e3a8a;&quot;&gt;input&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 8px; flex-wrap: wrap;&quot;&gt;&lt;span style=&quot;background: #e0f2fe; border: 1px solid #7dd3fc; border-radius: 10px; padding: 8px 10px;&quot;&gt;10&lt;/span&gt; &lt;span style=&quot;background: #e0f2fe; border: 1px solid #7dd3fc; border-radius: 10px; padding: 8px 10px;&quot;&gt;11&lt;/span&gt; &lt;span style=&quot;background: #e0f2fe; border: 1px solid #7dd3fc; border-radius: 10px; padding: 8px 10px;&quot;&gt;12&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;font-weight: bold; color: #166534;&quot;&gt;target&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 8px; flex-wrap: wrap;&quot;&gt;&lt;span style=&quot;background: #dcfce7; border: 1px solid #86efac; border-radius: 10px; padding: 8px 10px;&quot;&gt;11&lt;/span&gt; &lt;span style=&quot;background: #dcfce7; border: 1px solid #86efac; border-radius: 10px; padding: 8px 10px;&quot;&gt;12&lt;/span&gt; &lt;span style=&quot;background: #dcfce7; border: 1px solid #86efac; border-radius: 10px; padding: 8px 10px;&quot;&gt;13&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조는 한 번의 forward에서 여러 위치의 다음 토큰 예측을 동시에 학습하게 만든다. 첫 번째 위치는 10 다음 11을, 두 번째 위치는 11 다음 12를, 세 번째 위치는 12 다음 13을 맞히는 식이다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. context_length는 모델이 한 번에 보는 문맥 길이다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;context_length는 한 샘플 안에 들어가는 토큰 수다. 너무 짧으면 긴 문맥을 배우기 어렵고, 너무 길면 계산량이 늘어난다. 이번 과제에서는 처음부터 크게 잡지 않고 작은 설정으로 동작을 확인한 뒤 키우는 방식이 권장된다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 560px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;값&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;의미&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;주의점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;context_length&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;한 샘플의 input 길이&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;target까지 만들려면 실제로는 `context_length + 1`개 토큰이 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;stride&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;다음 샘플로 이동하는 간격&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;작을수록 샘플은 많아지지만 겹침도 늘어남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;batch_size&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;한 번에 묶어 계산하는 샘플 수&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;GPU 메모리와 학습 안정성에 영향&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. token ID는 embedding을 거쳐야 의미 있는 계산 대상이 된다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;token ID 10과 token ID 11은 숫자 크기 자체에 의미가 있는 것이 아니다. 11이 10보다 크다고 해서 더 중요한 토큰이라는 뜻은 아니다. 그래서 ID를 바로 계산하지 않고 embedding table에서 벡터를 꺼낸다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;token ID&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;[10, 11, 12]&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b; font-weight: bold;&quot;&gt;&amp;darr; token embedding table 조회&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;token embedding&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;각 token을 emb_dim 길이의 벡터로 변환&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b; font-weight: bold;&quot;&gt;+ position embedding&lt;/div&gt;
&lt;div style=&quot;background: #faf5ff; border-left: 5px solid #9333ea; border-radius: 10px; padding: 12px;&quot;&gt;&lt;b&gt;모델 입력&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;토큰 의미와 위치 정보를 함께 가진 벡터&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;position embedding이 필요한 이유도 여기서 나온다. 같은 token이라도 문장 안에서 어디에 놓였는지에 따라 역할이 달라질 수 있기 때문이다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;shape로 기억하기&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;이 단계의 최종 출력 shape는 다음처럼 기억하면 된다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: #f8fafc; border: 1px solid #d8e0ea; color: #0f172a; font-size: 14px;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;input token IDs: (batch_size, seq_len)
token embedding: (batch_size, seq_len, emb_dim)
position embedding: (seq_len, emb_dim)
final input: (batch_size, seq_len, emb_dim)&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;target이 input보다 한 칸 뒤로 밀리는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;token ID 숫자 자체를 바로 모델 입력으로 쓰지 않는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;position embedding이 없다면 같은 토큰의 위치 차이를 어떻게 알 수 있을까?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 attention을 다룬다. 핵심은 각 토큰이 문맥 안의 다른 토큰을 어떤 비율로 참고하는지 계산하는 방식이다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: Dataset은 다음 토큰 예측 문제를 만들고, Embedding은 token ID를 모델이 계산할 수 있는 위치 포함 벡터로 바꾼다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/261</guid>
      <comments>https://cedis.tistory.com/261#entry261comment</comments>
      <pubDate>Sun, 31 May 2026 00:18:44 +0900</pubDate>
    </item>
    <item>
      <title>mini GPT 공부 1편 - 한글 토큰화와 byte-level BPE</title>
      <link>https://cedis.tistory.com/260</link>
      <description>&lt;main style=&quot;max-width: 840px; margin: 0 auto; padding: 28px 18px 56px;&quot;&gt;
&lt;article style=&quot;background: #ffffff; border: 1px solid #d9e2ef; border-radius: 18px; padding: 28px 22px; box-shadow: 0 10px 30px rgba(15,23,42,0.06);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2563eb; font-size: 14px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;밑바닥부터 만드는 mini GPT 공부 시리즈 1편&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 16px;&quot; data-ke-size=&quot;size16&quot;&gt;GPT는 문자열을 직접 읽지 않는다. 먼저 문장을 숫자 ID의 목록으로 바꾼다. 이 변환을 담당하는 부품이 토크나이저다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 과제에서 중요한 지점은 교재처럼 이미 만들어진 토크나이저를 쓰지 않는다는 점이다. 특히 한글 리뷰 데이터를 다루기 때문에, 공백 기준 단어 분리보다 더 낮은 단위에서 시작해야 한다.&lt;/p&gt;
&lt;section style=&quot;background: #eef6ff; border: 1px solid #bfdbfe; border-radius: 14px; padding: 18px; margin: 22px 0;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 10px; font-size: 20px; color: #1e3a8a;&quot; data-ke-size=&quot;size26&quot;&gt;이번 글에서 다루는 것&lt;/h2&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰화가 왜 GPT 구현의 첫 단계인지&lt;/li&gt;
&lt;li&gt;한글에서 공백 기준 토큰화가 왜 쉽게 무너지는지&lt;/li&gt;
&lt;li&gt;UTF-8 byte에서 시작하는 BPE가 어떤 문제를 해결하는지&lt;/li&gt;
&lt;li&gt;encode와 decode에서 가장 조심해야 할 복원 원칙&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;1. 토큰화는 문장을 모델의 입력으로 바꾸는 번역기다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;사람은 &amp;ldquo;이 영화는 정말 좋았다&amp;rdquo;를 문장으로 읽는다. 하지만 모델은 이 문장을 그대로 받지 못한다. 모델이 처리할 수 있는 것은 정수 ID이고, 그 ID는 다시 embedding layer를 통해 벡터가 된다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; background: #ffffff; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #eff6ff; border-left: 5px solid #2563eb; border-radius: 10px; padding: 12px 14px;&quot;&gt;&lt;b&gt;문장&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;이 영화는 정말 좋았다&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b; font-weight: bold;&quot;&gt;&amp;darr; tokenizer.encode()&lt;/div&gt;
&lt;div style=&quot;background: #ecfdf5; border-left: 5px solid #0f766e; border-radius: 10px; padding: 12px 14px;&quot;&gt;&lt;b&gt;토큰 ID&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;[2, 431, 128, 87, ... , 3]&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; color: #64748b; font-weight: bold;&quot;&gt;&amp;darr; embedding&lt;/div&gt;
&lt;div style=&quot;background: #faf5ff; border-left: 5px solid #9333ea; border-radius: 10px; padding: 12px 14px;&quot;&gt;&lt;b&gt;벡터&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #475569;&quot;&gt;모델이 계산할 수 있는 실수 배열&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 토크나이저가 깨지면 그 뒤의 모델은 아무리 잘 만들어도 출발점부터 잘못된다. 특히 decode했을 때 원문이 복원되지 않는다면, 학습과 생성 결과를 해석하기 어렵다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;2. 한글은 공백 기준으로만 보기 어렵다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;영어는 공백으로 단어가 비교적 잘 나뉘는 편이다. 하지만 한국어는 조사와 어미가 붙어서 같은 뿌리의 표현도 계속 다른 모양으로 나타난다.&lt;/p&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; min-width: 620px; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;표현&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;사람의 감각&lt;/th&gt;
&lt;th style=&quot;background: #f1f5f9; border: 1px solid #d8e0ea; padding: 10px; text-align: left;&quot;&gt;단순 단어 토큰화의 문제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;재미있다&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;기본 표현&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;별도 단어로 등록 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;재미있었다&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;과거형&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;처음 보면 새로운 단어처럼 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;재미있네요&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;감상 표현&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d8e0ea; padding: 10px;&quot;&gt;어휘에 없으면 알 수 없는 토큰이 늘어남&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;byte-level BPE는 이 문제를 완전히 해결하는 만능 해법은 아니다. 다만 모든 문자열을 최소한 byte 단위로 표현할 수 있게 만들어, 처음 보는 표현도 처리할 수 있는 출발점을 만든다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;3. 한글 한 글자는 byte 여러 개다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;사람에게 &amp;ldquo;한&amp;rdquo;은 한 글자다. 하지만 UTF-8로 저장하면 여러 byte로 표현된다. byte-level 토크나이저는 바로 이 byte 목록에서 시작한다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 16px; padding: 16px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;font-weight: bold; margin-bottom: 10px; color: #0f172a;&quot;&gt;&quot;한&quot;의 byte 관찰&lt;/div&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 10px; align-items: center;&quot;&gt;&lt;span style=&quot;background: #ffffff; border: 1px solid #cbd5e1; border-radius: 10px; padding: 8px 10px;&quot;&gt;문자: 한&lt;/span&gt; &lt;span style=&quot;color: #64748b;&quot;&gt;&amp;rarr;&lt;/span&gt; &lt;span style=&quot;background: #e0f2fe; border: 1px solid #7dd3fc; border-radius: 10px; padding: 8px 10px;&quot;&gt;237&lt;/span&gt; &lt;span style=&quot;background: #e0f2fe; border: 1px solid #7dd3fc; border-radius: 10px; padding: 8px 10px;&quot;&gt;149&lt;/span&gt; &lt;span style=&quot;background: #e0f2fe; border: 1px solid #7dd3fc; border-radius: 10px; padding: 8px 10px;&quot;&gt;156&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이때 byte 값 0~255를 그대로 모델 ID로 쓰지 않고, 앞쪽 ID는 특수 토큰에 배정한다. 과제에서는 0~3을 특수 토큰으로 고정하고, byte 0~255는 ID 4~259에 둔다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 23px; margin: 0 0 14px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;4. BPE는 자주 붙는 조각을 합쳐간다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 14px;&quot; data-ke-size=&quot;size16&quot;&gt;byte에서 시작하면 모든 문자를 표현할 수 있지만, 너무 잘게 쪼개진다. BPE는 여기서 자주 붙어 나오는 이웃한 토큰 쌍을 하나의 새 토큰으로 합쳐간다.&lt;/p&gt;
&lt;div style=&quot;border: 1px solid #d7dee9; border-radius: 16px; padding: 16px; margin: 18px 0;&quot;&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 10px;&quot;&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;1단계&lt;/b&gt; byte token 목록에서 시작한다.&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;2단계&lt;/b&gt; 이웃한 token pair의 빈도를 센다.&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;3단계&lt;/b&gt; 가장 자주 나온 pair를 새 token으로 등록한다.&lt;/div&gt;
&lt;div style=&quot;background: #f8fafc; border-radius: 10px; padding: 12px; border: 1px solid #d9e2ef;&quot;&gt;&lt;b&gt;4단계&lt;/b&gt; 같은 pair를 새 token 하나로 바꾸고 다시 반복한다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 merge 순서다. 학습할 때 어떤 pair를 어떤 순서로 합쳤는지 저장해야 나중에 새로운 문장을 encode할 때 같은 규칙을 재현할 수 있다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; padding: 18px; margin: 30px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #9a3412;&quot; data-ke-size=&quot;size26&quot;&gt;decode에서 가장 조심할 점&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;byte를 하나씩 문자로 바꾸면 한글이 깨질 수 있다. merge token을 원래 byte까지 모두 펼친 뒤, 마지막에 모은 byte 배열 전체를 한 번에 UTF-8로 decode해야 한다.&lt;/p&gt;
&lt;/section&gt;
&lt;section style=&quot;background: #f8fafc; border: 1px solid #d9e2ef; border-radius: 14px; padding: 18px; margin: 24px 0;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;스스로 점검&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한국어에서 공백 기준 토큰화가 쉽게 부족해지는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;byte 0이 token ID 0이 아니라 ID 4가 되는 이유는 무엇인가?&lt;/li&gt;
&lt;li&gt;decode할 때 byte를 마지막에 한 번만 UTF-8로 복원해야 하는 이유는 무엇인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section style=&quot;border-top: 1px solid #e5e7eb; padding-top: 18px; margin-top: 30px;&quot;&gt;
&lt;h2 style=&quot;font-size: 20px; margin: 0 0 10px; color: #111827;&quot; data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 책 공부 글에서는 Dataset과 Embedding을 본다. 핵심은 &amp;ldquo;현재 토큰을 보고 다음 토큰을 맞히는 샘플&amp;rdquo;을 어떻게 만드는지다.&lt;/p&gt;
&lt;/section&gt;
&lt;p style=&quot;margin: 26px 0 0; padding: 14px 16px; background: #ecfeff; border: 1px solid #a5f3fc; border-radius: 14px; color: #155e75; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄 정리: byte-level BPE는 한글을 포함한 모든 문자열을 최소 단위에서 안전하게 표현하고, 자주 붙는 조각을 합쳐 모델이 다루기 좋은 토큰으로 만드는 방법이다.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;</description>
      <author>cedis</author>
      <guid isPermaLink="true">https://cedis.tistory.com/260</guid>
      <comments>https://cedis.tistory.com/260#entry260comment</comments>
      <pubDate>Sun, 31 May 2026 00:18:28 +0900</pubDate>
    </item>
  </channel>
</rss>