- PostgreSQL과 pgvector만으로 GPT-2 추론 흐름을 SQL에 옮겨, 토큰화부터 Transformer 블록과 다음 토큰 생성까지 재현함
- 생성형 LLM은 같은 입력에 같은 후보 토큰 확률을 내는 결정적 함수에 가깝고, 응답이 달라지는 지점은 후보 토큰을 뽑는 확률적 선택 단계임
- 구현은 GPT-2의 BPE 토크나이저, 50257개 토큰, 768차원 임베딩, 1024 토큰 컨텍스트, 12개 블록, 12개 attention head, GELU 기반 feedforward를 SQL 쿼리와 테이블로 표현함
- PostgreSQL 정규식의 Unicode 속성 미지원과
EXP의 작은 값 처리 한계처럼, 데이터베이스 환경에 맞춘 현실적인 우회가 필요했음 - 예제는
"Happy New Year! I wish you"에서 10개 토큰을 생성해"Happy New Year! I wish you all the best in your new year!"를 출력했고, 작성자 환경에서 2분 44초 걸림
SQL로 GPT-2 추론 파이프라인 만들기
- ChatGPT는 SQL이 대규모 언어 모델 구현에 적합하지 않다고 답했지만, PostgreSQL SQL로 GPT-2 추론 파이프라인을 구현함
- 참고한 구현 설명은 Jay Mody의 GPT in 60 Lines of NumPy이며, 같은 구성요소를 데이터베이스 테이블과 쿼리로 옮김
- 생성형 LLM은
llm(prompt: str) -> list[tuple[str, float]]형태의 함수처럼 볼 수 있음- 입력은 텍스트 프롬프트
- 출력은 다음에 올 문자열 후보와 확률 배열
- 내부 수학과 파라미터가 같다면 같은 입력에 같은 결과를 반환함
- ChatGPT 같은 제품이 같은 질문에 다른 답을 줄 수 있는 이유는 모델 자체보다 다음 토큰 선택 단계의 확률적 선택에 있음
텍스트 생성 루프
- 생성 과정은 프롬프트를 토큰 배열로 바꾼 뒤, 모델을 반복 호출해 다음 토큰을 고르고 프롬프트 뒤에 붙이는 구조임
- 기본 흐름은 다음 단계로 구성됨
tokenize(prompt)로 문자열을 토큰 ID 배열로 변환gpt2(tokens)가 50257개 토큰에 대한 확률을 계산select_next_token(candidates)가 다음 토큰을 선택- 선택한 토큰을 배열에 추가
- 정해진 토큰 수, 타임아웃, stopword 같은 조건에서 중단
detokenize(tokens)로 토큰 배열을 문자열로 복원
- 이렇게 누적된 토큰 시퀀스는 문법, 구문, 의미, 추론처럼 보이는 특성을 가진 자연어 텍스트가 될 수 있음
BPE 토크나이저를 SQL로 구현
- 신경망 입력 전 텍스트는 숫자 목록으로 바뀌어야 하지만, Unicode 코드포인트를 그대로 쓰면 토큰 공간과 길이가 비효율적임
- GPT-2는 Byte pair encoding의 변형을 사용함
- 토큰 사전은 50257개 코드포인트를 사용
- UTF-8 바이트 시퀀스와 “end of text” 토큰을 포함
- 처음에는 256개 바이트 토큰에서 시작해, 자주 등장하는 인접 토큰 쌍을 새 토큰으로 추가함
- 이 병합을 50000번 반복해 50256개 토큰을 만들고, 마지막에 end-of-text 토큰을 추가함
- GPT-2 토크나이저에는 바이트를 문자열 문자로 매핑하는 추가 계층이 있으며, 이 매핑은 OpenAI GPT-2의
encoder.py에 정의돼 있음 - SQL 구현에서는 OpenAI에서 내려받은 토큰 사전을
tokenizer테이블에 넣고, 바이트-문자 매핑을encoder테이블에 저장함 "Mississippilessly"예제는 recursive CTE로 단일 바이트에서 시작해 병합 가능한 가장 좋은 인접 쌍을 반복 병합함- 예제에서 토큰 수는 17개에서 5개로 줄어듦
- Unicode의 약 150k 코드포인트 공간 대신 GPT-2의 약 50k 토큰 공간을 사용함
- 여러 단어를 처리할 때 GPT-2는 정규식으로 텍스트를 나눠 각 단어 내부에서 병합함
- PostgreSQL은 정규식에서 Unicode 문자 속성을 지원하지 않아 원래 GPT-2 정규식을 일부 수정함
- 이 수정은 적절한 Unicode 지원을 손상시켰을 가능성이 있음
"PostgreSQL is great"는 SQL 토크나이저에서[6307, 47701, 318, 1049]로 변환됨- 토큰 클러스터는
Post,greSQL,Ġis,Ġgreat Ġ는 공백을 나타냄
- 토큰 클러스터는
임베딩과 컨텍스트 윈도우
- 토큰 ID는 곧바로 모델 계산에 쓰이지 않고 임베딩 벡터로 변환됨
- GPT-2는 토큰과 위치를 각각 임베딩함
WTE는 word token embedding이며 50257×768 행렬WPE는 word position embedding이며 1024×768 행렬
- 각 토큰 위치마다
WTE벡터와WPE벡터를 더해 다음 단계의 입력 벡터를 만듦 WPE가 1024개 위치만 갖기 때문에 GPT-2 프롬프트에 사용할 수 있는 최대 토큰 수는 1024개임- 이 수가 LLM의 컨텍스트 윈도우에 해당함
- 모델 설계 시 정해지는 하이퍼파라미터이며 학습으로 바뀌지 않음
- SQL 구현은 pgvector를 사용함
- 배열에 벡터 연산을 직접 정의해 순수 SQL로도 가능하지만 성능이 낮아짐
- 초기 버전은 순수 SQL 함수로 동작했지만 느렸음
self-attention을 SQL 쿼리로 전개
- Transformer의 핵심은 self-attention 메커니즘이며, 2017년 논문 Attention is all you need에 기반함
- attention은 토큰 벡터들이 서로 영향을 주도록 만들어, 프롬프트의 앞부분 정보가 마지막 벡터로 전달되게 함
- GPT-2 구현은 12세트의
Q,K,V행렬을 사용함- 각 세트는 하나의 attention head
- 각 head는 64차원
c_attn은 768×2304 선형 변환이며, 결과는Q,K,V가 가로로 쌓인 2304차원 벡터임- 가중치와 bias는
c_attn_w,c_attn_b테이블에 저장됨
- attention 계산 전에는 layer normalization이 적용됨
- scale과 shift 파라미터는
ln_1_g,ln_1_b테이블에 저장됨
- scale과 shift 파라미터는
- causal self-attention에서는 뒤쪽 토큰이 앞쪽 토큰에 영향을 주지 못하도록 causal mask를 적용함
- 모델의 다음 토큰 후보는 최종적으로 마지막 임베딩에서 결정됨
- 정보 흐름은 마지막 벡터 쪽으로 향해야 하며, 마지막 벡터의 중간값이 이전 벡터에 영향을 주면 안 됨
- SQL 구현은 softmax 계산에서 PostgreSQL
EXP가 매우 작은 수에 실패하는 문제를 피하기 위해 입력이 -745.13보다 작으면 0으로 처리함 - causal mask 덕분에 새 토큰을 프롬프트에 붙여도 이전 토큰들에 대한 계산 결과는 변하지 않음
- 원래 GPT-2 구현은 이 성질을 활용함
- SQL 구현은 단순성을 위해 이 재사용을 쓰지 않음
multi-head attention과 잔차 연결
- 12개 head의 attention 결과는 각각 64차원이며, 이를 가로로 쌓아 다시 768차원으로 만듦
- 이어서
c_proj_w,c_proj_b에 저장된 학습된 선형 변환으로 attention 출력을 투영함 - multi-headed attention 결과에는 원래 입력이 다시 더해짐
- 이 잔차 연결은 원래 Transformer 논문에 포함된 기법
- 학습 중 vanishing gradient와 exploding gradient 문제를 완화하기 위한 설계로 소개됨
feedforward 단계와 Transformer 블록
- attention 뒤에는 feedforward 신경망이 이어짐
- GPT-2의 feedforward 단계는 3개 층의 multi-layer perceptron으로 구성됨
- 차원은 768 → 3072 → 768
- 활성화 함수는 GELU를 사용함
- 선형 변환 파라미터는 다음 테이블에 저장됨
mlp_c_fc_w,mlp_c_fc_bmlp_c_proj_w,mlp_c_proj_b
- feedforward 입력도 먼저
ln_2파라미터로 정규화됨ln_2_g,ln_2_b가 scale과 shift를 저장함
- feedforward 출력에도 입력을 다시 더하는 잔차 연결이 적용됨
- 이 attention + feedforward 조합이 하나의 block이며, GPT-2는 12개 block을 파이프라인처럼 연결함
- 각 block은 자체 학습 파라미터 세트를 가짐
- SQL에서는 recursive CTE로 block들을 연결함
- 마지막 block 출력은
ln_f파라미터로 다시 정규화됨
다음 토큰으로 되돌리기
- 최종 출력 중 마지막 위치의 768차원 벡터가 다음 토큰의 임베딩임
- 이 벡터를 다시 토큰으로 바꾸기 위해 처음 토큰 임베딩에 사용한
WTE행렬을 다시 사용함 - 정확한 역변환은 보통 불가능함
- 예측 임베딩이
WTE의 특정 행과 정확히 같지 않을 수 있음 - 따라서 각 토큰 임베딩과의 dot product를 계산해 가까운 토큰을 찾음
- 예측 임베딩이
WTE와 예측 임베딩의 dot product 결과는 50257개 점수, 즉 logit이 됨- 이 점수들은 softmax를 거쳐 확률로 바뀜
- top 후보 수는
top_n - 확률 분포를 조절하는 값은 temperature
- temperature가 높을수록 1순위가 아닌 토큰이 선택될 가능성이 커지고, 추론이 덜 예측 가능해짐
- top 후보 수는
"PostgreSQL is great"예제에서 상위 5개 다음 토큰 후보는 다음과 같음Ġfor,.ĠatĠto
- temperature가 0.5, 1, 2로 바뀌면 같은 후보들의 softmax 확률 분포가 달라짐
실제 추론 결과와 코드
- 최종 SQL은 토큰을 확률에 따라 선택하고 프롬프트에 붙이는 과정을 반복함
- 모델 자체는 결정적이며, 비결정적 요소는 토큰 선택에 포함된 난수뿐임
- 예제 설정은 다음과 같음
- 프롬프트:
"Happy New Year! I wish you" - 생성 토큰 수: 10
- temperature: 2
- top_n: 1
SETSEED(0.20231231)사용
- 프롬프트:
- 작성자 환경에서 쿼리는 2분 44초 실행됨
- 출력 결과는
"Happy New Year! I wish you all the best in your new year!" - 쿼리와 설치 코드는 GitHub 저장소 quassnoi/explain-extended-2024에 있음