6P by GN⁺ 6시간전 | ★ favorite | 댓글과 토론
  • karpathy가 공개한 아트 프로젝트. 200줄 단일 파일로 외부 의존성없이 GPT 전체 알고리듬을 구현
  • 프로덕션 LLM과의 차이는 규모와 효율성일 뿐 핵심은 동일, 이 코드를 이해하면 GPT의 알고리듬적 본질을 이해한 것
  • 데이터셋, 토크나이저, autograd 엔진, GPT-2 유사 Transformer 아키텍처, Adam 옵티마이저, 학습·추론 루프까지 포함
  • micrograd, makemore, nanogpt 등 기존 프로젝트들과 10년간의 LLM 단순화 작업의 결정체로, 더 이상 단순화할 수 없는 최소 형태로 GPT의 본질을 담음
  • 32,000개의 이름 데이터를 학습해 그럴듯한 새로운 이름을 생성하며, 모든 계산을 스칼라 단위 autograd로 직접 수행
  • 학습 과정은 손실 계산 → 역전파 → Adam 업데이트로 구성되며, 약 1분 내 실행 가능

microgpt 개요

  • microgpt는 200줄짜리 Python 스크립트로, GPT 모델의 학습과 추론 과정을 완전하게 구현
    • 외부 라이브러리 없이 데이터셋, 토크나이저, autograd, 모델, 옵티마이저, 학습 루프를 모두 포함
  • 기존의 micrograd, makemore, nanogpt 등의 프로젝트를 통합해 단일 파일로 정리
  • “더 이상 단순화할 수 없음” 수준으로 알고리듬적 핵심만 남긴 구현
  • 전체 코드는 GitHub Gist, 웹페이지, Google Colab에서 제공

데이터셋 구성

  • 대규모 언어 모델의 연료는 텍스트 데이터 스트림이며, 프로덕션에서는 인터넷 웹페이지를 사용하지만 microgpt에서는 32,000개의 이름을 한 줄씩 담은 단순한 예제 사용
  • 각 이름이 하나의 "문서"로 취급되며, 모델은 데이터 내의 통계적 패턴을 학습해 유사한 새 문서를 생성하는 것이 목표
  • 학습 완료 후 모델은 "kamon", "karai", "vialan" 같은 그럴듯한 새 이름을 "환각(hallucinate)"
  • ChatGPT 관점에서 사용자와의 대화도 "특이하게 생긴 문서"일 뿐이며, 프롬프트로 문서를 초기화하면 모델의 응답은 통계적 문서 완성에 해당

토크나이저

  • 신경망은 문자가 아닌 숫자로 작동하므로 텍스트를 정수 토큰 ID 시퀀스로 변환하고 다시 복원하는 방법 필요
  • tiktoken(GPT-4 사용) 같은 프로덕션 토크나이저는 효율을 위해 문자 청크 단위로 작동하지만, 가장 단순한 토크나이저는 데이터셋의 각 고유 문자에 하나의 정수를 할당
  • 소문자 a-z를 정렬해 각 문자에 인덱스로 ID 부여하며, 정수 값 자체는 의미가 없고 각 토큰은 별개의 이산 심볼
  • BOS(Beginning of Sequence) 특수 토큰을 추가해 "새 문서가 시작/종료됨"을 알리며, "emma"는 [BOS, e, m, m, a, BOS]로 래핑
  • 최종 어휘 크기는 27개(소문자 26개 + BOS 1개)

자동 미분(Autograd)

  • 신경망 학습에는 그래디언트가 필요: 각 파라미터에 대해 "이 값을 살짝 올리면 손실이 올라가는가, 내려가는가, 얼마나?"를 알아야 함
  • 연산 그래프는 많은 입력(모델 파라미터와 입력 토큰)을 가지지만 단일 스칼라 출력인 손실(loss) 로 수렴
  • 역전파(Backpropagation) 는 출력에서 시작해 그래프를 역방향으로 따라가며 미적분의 체인 룰에 의존해 모든 입력에 대한 손실의 그래디언트 계산
  • Value 클래스로 구현: 각 Value는 단일 스칼라(.data)를 감싸고 어떻게 계산되었는지 추적
    • 덧셈, 곱셈 등의 연산 시 새 Value가 입력(_children)과 해당 연산의 국소 도함수(_local_grads) 를 기억
    • 예: __mul__은 ∂(a·b)/∂a=b, ∂(a·b)/∂b=a를 기록
  • 지원되는 연산 블록: 덧셈, 곱셈, 거듭제곱, log, exp, ReLU
  • backward() 메서드가 역위상 순서로 그래프를 순회하며 각 단계에서 체인 룰 적용
    • 손실 노드에서 self.grad = 1로 시작(∂L/∂L=1)
    • 국소 그래디언트를 경로 따라 곱해가며 파라미터까지 전파
  • +=로 누적(할당이 아님): 그래프가 분기될 때 각 분기에서 독립적으로 그래디언트가 흘러와 합산되어야 함(다변수 체인 룰의 결과)
  • PyTorch의 .backward()알고리듬적으로 동일하나, 텐서 대신 스칼라 단위로 작동해 훨씬 단순하지만 효율성은 낮음

파라미터 초기화

  • 파라미터는 모델의 지식으로, 무작위로 시작해 학습 중 반복적으로 최적화되는 부동소수점 숫자의 대규모 집합
  • 가우시안 분포에서 작은 무작위 값으로 초기화
  • state_dict로 명명된 행렬들로 구성: 임베딩 테이블, 어텐션 가중치, MLP 가중치, 최종 출력 프로젝션
  • 하이퍼파라미터 설정:
    • n_embd = 16: 임베딩 차원
    • n_head = 4: 어텐션 헤드 수
    • n_layer = 1: 레이어 수
    • block_size = 16: 최대 시퀀스 길이
  • 소형 모델 기준 4,192개 파라미터(GPT-2는 16억 개, 현대 LLM은 수천억 개)

아키텍처

  • 모델 아키텍처는 무상태 함수: 토큰, 위치, 파라미터, 이전 위치의 캐시된 키/값을 받아 다음 토큰에 대한 로짓(점수) 반환
  • GPT-2를 따르되 약간 단순화: RMSNorm(LayerNorm 대신), 바이어스 없음, ReLU(GeLU 대신)
  • 헬퍼 함수

    • linear: 행렬-벡터 곱셈으로 가중치 행렬의 각 행에 대해 하나의 내적 계산, 신경망의 기본 구성 요소인 학습된 선형 변환
    • softmax: 원시 점수(로짓)를 확률 분포로 변환, 모든 값이 [0,1] 범위에 들어가고 합이 1이 됨, 수치적 안정성을 위해 최대값 먼저 빼기
    • rmsnorm: 벡터를 단위 제곱평균제곱근을 갖도록 재조정해 활성화가 네트워크를 통과하며 커지거나 줄어드는 것 방지, 학습 안정화
  • 모델 구조

    • 임베딩: 토큰 ID와 위치 ID가 각각의 임베딩 테이블(wte, wpe)에서 행을 참조, 두 벡터를 더해 토큰이 무엇인지와 시퀀스에서 어디에 있는지 동시 인코딩
      • 현대 LLM은 위치 임베딩을 건너뛰고 RoPE 같은 상대 기반 위치 지정 기법 사용
    • 어텐션 블록: 현재 토큰을 Q(쿼리), K(키), V(값) 세 벡터로 프로젝션
      • 쿼리: "내가 찾는 것은?", 키: "내가 담고 있는 것은?", 값: "선택되면 제공하는 것은?"
      • 예: "emma"에서 두 번째 "m"이 다음을 예측할 때 "최근 어떤 모음이 있었나?" 같은 쿼리 학습 가능, 앞의 "e"가 이 쿼리와 잘 맞아 높은 어텐션 가중치 획득
      • 키와 값은 KV 캐시에 추가되어 이전 위치 참조 가능
      • 각 어텐션 헤드가 쿼리와 모든 캐시된 키 사이의 내적(√d_head로 스케일)을 계산, softmax로 어텐션 가중치 얻고 캐시된 값의 가중 합 계산
      • 모든 헤드 출력을 연결해 attn_wo로 프로젝션
      • 어텐션 블록은 위치 t의 토큰이 과거 0..t-1의 토큰을 "볼" 수 있는 유일한 곳, 어텐션은 토큰 통신 메커니즘
    • MLP 블록: 2층 피드포워드 네트워크: 임베딩 차원의 4배로 확장 → ReLU 적용 → 다시 축소
      • 위치별 "사고"의 대부분이 이루어지는 곳
      • 어텐션과 달리 시간 t에 완전히 로컬한 계산
      • Transformer는 통신(어텐션)과 계산(MLP)을 교차 배치
    • 잔차 연결: 어텐션과 MLP 블록 모두 출력을 입력에 다시 더함
      • 그래디언트가 네트워크를 직접 통과하게 해 깊은 모델의 학습 가능하게 함
    • 출력: 최종 은닉 상태를 lm_head로 어휘 크기에 프로젝션해 토큰당 하나의 로짓 생성(여기서는 27개 숫자), 높은 로짓 = 해당 토큰이 다음에 올 가능성 높음
    • KV 캐시 특이점: 학습 중에도 KV 캐시 사용은 드문 경우이나, microgpt가 한 번에 하나의 토큰만 처리하므로 명시적으로 구축, 캐시된 키와 값이 연산 그래프의 라이브 Value 노드로 역전파 대상

학습 루프

  • 학습 루프는 반복적으로: (1) 문서 선택 → (2) 토큰에 대해 모델 순방향 실행 → (3) 손실 계산 → (4) 역전파로 그래디언트 획득 → (5) 파라미터 업데이트
  • 토큰화

    • 각 학습 스텝에서 하나의 문서를 선택해 양쪽에 BOS 래핑: "emma" → [BOS, e, m, m, a, BOS]
    • 모델의 목표는 이전 토큰들이 주어졌을 때 각 다음 토큰을 예측
  • 순방향 패스와 손실

    • 토큰을 한 번에 하나씩 모델에 공급하며 KV 캐시 구축
    • 각 위치에서 모델이 27개 로짓 출력, softmax로 확률 변환
    • 각 위치의 손실은 올바른 다음 토큰의 음의 로그 확률: −log p(target), 이를 교차 엔트로피 손실이라 함
    • 손실은 모델이 실제로 오는 것에 얼마나 놀랐는지 측정: 확률 1.0 할당 시 손실 0, 확률 0 근처 시 손실 +∞
    • 문서 전체의 위치별 손실을 평균해 단일 스칼라 손실 획득
  • 역방향 패스

    • loss.backward() 한 번 호출로 전체 연산 그래프에 대해 역전파 실행
    • 이후 각 파라미터의 .grad가 손실을 줄이기 위해 어떻게 변경해야 하는지 알려줌
  • Adam 옵티마이저

    • 단순 경사 하강(p.data -= lr * p.grad) 대신 Adam 사용
    • 파라미터당 두 개의 이동 평균 유지:
      • m: 최근 그래디언트의 평균(모멘텀)
      • v: 최근 그래디언트 제곱의 평균(파라미터별 학습률 적응)
    • m_hat, v_hat은 0으로 초기화된 m, v의 바이어스 보정
    • 학습률은 학습 중 선형 감소
    • 업데이트 후 .grad = 0으로 초기화
  • 학습 결과

    • 1,000 스텝 동안 손실이 약 3.3(27개 토큰 중 무작위 추측: −log(1/27)≈3.3)에서 약 2.37로 감소
    • 낮을수록 좋고 최저는 0(완벽한 예측)이므로 개선 여지 있으나 모델이 이름의 통계적 패턴을 학습 중임이 명확

추론

  • 학습 완료 후 모델에서 새 이름 샘플링 가능, 파라미터 고정 후 순방향 패스를 루프로 실행, 각 생성 토큰을 다음 입력으로 피드백
  • 샘플링 과정

    • 각 샘플을 BOS 토큰으로 시작("새 이름 시작")
    • 모델이 27개 로짓 생성 → 확률로 변환 → 해당 확률에 따라 무작위로 하나의 토큰 샘플링
    • 해당 토큰을 다음 입력으로 피드백, 모델이 다시 BOS 생성("완료") 또는 최대 시퀀스 길이 도달까지 반복
  • 온도(Temperature)

    • softmax 전에 로짓을 온도로 나눔
    • 온도 1.0: 모델이 학습한 분포에서 직접 샘플링
    • 낮은 온도(예: 0.5): 분포를 날카롭게 해 모델이 더 보수적으로 상위 선택을 할 가능성 높임
    • 온도 0 근처: 항상 가장 확률 높은 단일 토큰 선택(탐욕적 디코딩)
    • 높은 온도: 분포를 평평하게 해 더 다양하지만 덜 일관된 출력

실행 방법

  • Python만 필요(pip install 없음, 의존성 없음): python train.py
  • MacBook에서 약 1분 소요
  • 각 스텝에서 손실 출력: ~3.3(무작위)에서 ~2.37로 감소
  • 학습 완료 후 환각된 새 이름 생성: "kamon", "ann", "karai" 등
  • Google Colab 노트북에서도 실행 가능, Gemini에게 질문 가능
  • 다른 데이터셋 시도, num_steps 증가로 더 오래 학습, 모델 크기 증가로 더 나은 결과 가능

코드 진행 단계

파일 추가 내용
train0.py 바이그램 카운트 테이블 — 신경망 없음, 그래디언트 없음
train1.py MLP + 수동 그래디언트(수치적 & 해석적) + SGD
train2.py Autograd(Value 클래스) — 수동 그래디언트 대체
train3.py 위치 임베딩 + 단일 헤드 어텐션 + rmsnorm + 잔차
train4.py 멀티헤드 어텐션 + 레이어 루프 — 전체 GPT 아키텍처
train5.py Adam 옵티마이저 — 이것이 train.py
  • build_microgpt.py Gist의 Revisions에서 모든 버전과 각 스텝 간 diff 확인 가능

프로덕션 LLM과의 차이

  • microgpt는 GPT 학습 및 실행의 완전한 알고리듬적 본질 포함, ChatGPT 같은 프로덕션 LLM과의 차이는 핵심 알고리듬을 바꾸지 않으며 규모에서 작동하게 하는 요소
  • 데이터

    • 32K 짧은 이름 대신 수조 개의 인터넷 텍스트 토큰(웹페이지, 책, 코드 등)으로 학습
    • 데이터 중복 제거, 품질 필터링, 도메인 간 신중한 혼합
  • 토크나이저

    • 단일 문자 대신 BPE(Byte Pair Encoding) 같은 서브워드 토크나이저 사용
    • 자주 함께 나타나는 문자 시퀀스를 단일 토큰으로 병합, "the" 같은 일반 단어는 단일 토큰, 희귀 단어는 조각으로 분리
    • ~100K 토큰 어휘, 위치당 더 많은 콘텐츠를 보므로 훨씬 효율적
  • Autograd

    • 순수 Python의 스칼라 Value 객체 대신 텐서(대규모 다차원 숫자 배열) 사용, 초당 수십억 부동소수점 연산 수행하는 GPU/TPU에서 실행
    • PyTorch가 텐서에 대한 autograd 처리, FlashAttention 같은 CUDA 커널이 여러 연산 융합
    • 수학은 동일, 많은 스칼라가 병렬 처리
  • 아키텍처

    • microgpt: 4,192개 파라미터, GPT-4급 모델: 수천억 개
    • 전반적으로 매우 유사한 Transformer 신경망이나 훨씬 넓고(임베딩 차원 10,000+) 훨씬 깊음(100+ 레이어)
    • 추가 레고 블록 유형과 순서 변경:
      • RoPE(회전 위치 임베딩) — 학습된 위치 임베딩 대신
      • GQA(그룹화된 쿼리 어텐션) — KV 캐시 크기 감소
      • 게이트 선형 활성화 — ReLU 대신
      • MoE(전문가 혼합) 레이어
    • 잔차 스트림 위에 어텐션(통신)과 MLP(계산)가 교차하는 핵심 구조는 잘 보존
  • 학습

    • 스텝당 하나의 문서 대신 대규모 배치(스텝당 수백만 토큰), 그래디언트 누적, 혼합 정밀도(float16/bfloat16), 신중한 하이퍼파라미터 튜닝
    • 프론티어 모델 학습에 수천 개의 GPU가 수개월간 실행
  • 최적화

    • microgpt: Adam + 단순 선형 학습률 감소
    • 대규모에서 최적화는 독자적 분야: 감소된 정밀도(bfloat16, fp8), 대규모 GPU 클러스터에서 학습
    • 옵티마이저 설정(학습률, 가중치 감쇠, 베타 파라미터, 워밍업/감쇠 스케줄)을 정밀하게 튜닝 필요, 올바른 값은 모델 크기, 배치 크기, 데이터셋 구성에 따라 다름
    • 스케일링 법칙(예: Chinchilla)이 고정 컴퓨팅 예산을 모델 크기와 학습 토큰 수 사이에 어떻게 할당할지 안내
    • 대규모에서 이 세부사항을 잘못하면 수백만 달러의 컴퓨팅 낭비 가능, 팀들이 전체 학습 실행 전 광범위한 소규모 실험 수행
  • 후학습(Post-training)

    • 학습에서 나온 기본 모델("사전학습" 모델)은 문서 완성기이지 챗봇이 아님
    • ChatGPT로 만드는 과정은 두 단계:
      • SFT(지도 미세조정): 문서를 큐레이션된 대화로 교체하고 학습 계속, 알고리듬적으로 변화 없음
      • RL(강화학습): 모델이 응답 생성 → 점수 부여(인간, "심판" 모델, 알고리듬) → 피드백으로 학습
    • 근본적으로 여전히 문서에 대해 학습하나, 이제 문서가 모델 자체에서 나온 토큰으로 구성
  • 추론

    • 수백만 사용자에게 모델 서빙에 자체 엔지니어링 스택 필요: 요청 배칭, KV 캐시 관리 및 페이징(vLLM 등), 속도를 위한 추측적 디코딩, 메모리 감소를 위한 양자화(int8/int4로 실행), 여러 GPU에 모델 분산
    • 근본적으로 여전히 시퀀스의 다음 토큰을 예측하나 더 빠르게 만드는 엔지니어링에 많은 노력

FAQ

  • 모델이 무언가를 "이해"하는가?

    • 철학적 질문이나 기계적으로: 마법은 일어나지 않음
    • 모델은 입력 토큰을 다음 토큰에 대한 확률 분포로 매핑하는 큰 수학 함수
    • 학습 중 파라미터는 올바른 다음 토큰을 더 확률 높게 만들도록 조정
    • "이해"를 구성하는지는 개인에게 달렸으나 메커니즘은 위 200줄에 완전히 담김
  • 왜 작동하는가?

    • 모델에 수천 개의 조정 가능한 파라미터가 있고, 옵티마이저가 각 스텝에서 손실을 낮추도록 조금씩 이동
    • 많은 스텝에 걸쳐 파라미터가 데이터의 통계적 규칙성을 포착하는 값으로 안정
    • 이름의 경우: 자음으로 시작하는 경우 많음, "qu"가 함께 나타나는 경향, 자음 3개 연속은 드묾 등
    • 모델은 명시적 규칙이 아닌 이를 반영하는 확률 분포를 학습
  • ChatGPT와 어떤 관련이 있는가?

    • ChatGPT는 이 동일한 핵심 루프(다음 토큰 예측, 샘플링, 반복)를 엄청나게 확장하고 대화형으로 만드는 후학습 추가
    • 채팅 시 시스템 프롬프트, 사용자 메시지, 응답 모두 시퀀스의 토큰일 뿐
    • 모델은 microgpt가 이름을 완성하는 것과 동일하게 한 번에 하나의 토큰으로 문서를 완성
  • "환각"은 무엇인가?

    • 모델은 확률 분포에서 샘플링해 토큰 생성
    • 진실 개념이 없으며 학습 데이터에 비추어 통계적으로 그럴듯한 시퀀스만 앎
    • microgpt가 "karia" 같은 이름을 "환각"하는 것은 ChatGPT가 거짓 사실을 자신 있게 말하는 것과 동일한 현상
    • 둘 다 실제가 아닌 그럴듯하게 들리는 완성
  • 왜 이렇게 느린가?

    • microgpt는 순수 Python에서 한 번에 하나의 스칼라 처리, 단일 학습 스텝에 수 초 소요
    • GPU에서 동일한 수학이 수백만 스칼라를 병렬 처리해 수 자릿수 더 빠르게 실행
  • 더 좋은 이름을 생성하게 할 수 있는가?

    • 가능: 더 오래 학습(num_steps 증가), 모델 크기 증가(n_embd, n_layer, n_head), 더 큰 데이터셋 사용
    • 대규모에서도 중요한 동일한 조절 요소
  • 데이터셋을 바꾸면?

    • 모델은 데이터에 있는 어떤 패턴이든 학습
    • 도시 이름, 포켓몬 이름, 영어 단어, 짧은 시 파일로 교체하면 대신 그것들을 생성하도록 학습
    • 나머지 코드는 변경 불필요