본문 바로가기

카테고리 없음

numpy만으로 가장 간단한 LLM을 만들어보자 #2 (파이썬 초보 가능)

반응형

아래 소스코드는 내가 정말 할 수 있는한 가장 짧고 간단하게만 만들어본 LLM 기본 코드이다. 물론 사이즈가 작으니 LLM(대형 언어 모델)이 아니라 SLM이나 Tiny LM이라고 불러야 할 것 이다.

 

학습 데이터는 첨부해두었다. 그냥 맛뵈기이므로 테일러 스위프트의 노래 가사 하나를 넣었다. 정제코드 작성을 생략하기 위해 특수문자나 눌나눔 등이 모두 정제된, 정말 처리하기 쉬운 텍스트를 사용하겠다.

 

lyrics.txt
0.00MB

 

import numpy as np

with open("lyrics.txt", "r", encoding="utf-8") as f:
    text = f.read()

words = text.strip().split()
vocab = sorted(set(words))
vocab_size = len(vocab)

block_size = 4
embed_size = 8
lr = 0.05

W_embed = np.random.normal(0, 0.01, size=(vocab_size, embed_size))
W_proj = np.random.normal(0, 0.01, size=(embed_size, vocab_size))

for step in range(100000):
    ix = np.random.randint(0, len(words) - block_size - 1)
    x = words[ix : ix + block_size]
    y = words[ix + block_size]

    x_indices = [vocab.index(w) for w in x]
    y_index = vocab.index(y)

    emb = W_embed[x_indices]  
    context = emb.mean(axis=0)
    logits = context @ W_proj
    probs = np.exp(logits) / np.exp(logits).sum()

    loss = 1.0 - probs[y_index]

    probs[y_index] -= 1
    dW_proj = np.outer(context, probs)
    dcontext = probs @ W_proj.T

    for idx in x_indices:
        W_embed[idx] -= lr * dcontext
    W_proj -= lr * dW_proj

    if step % 10000 == 0:
        pred_idx = np.argmax(probs)
        pred_word = vocab[pred_idx]
        print(f"Step {step:04d} | Input: {' '.join(x)} | Target: {y} | Pred: {pred_word} | Loss: {loss:.4f}")

def generate(start, length=8):
    out = start.strip().split()
    for _ in range(length):
        context = out[-block_size:]
        context_indices = [vocab.index(w) for w in context]
        emb = W_embed[context_indices]
        context_vec = emb.mean(axis=0)
        logits = context_vec @ W_proj
        probs = np.exp(logits) / np.exp(logits).sum()
        next_idx = np.random.choice(vocab_size, p=probs)
        out.append(vocab[next_idx])
    return ' '.join(out)

print("\nGenerated:")
print(generate("romeo take me somewhere", length=10))

 

LLM을 만드는 다른 튜토리얼을 보면 바로 PyTorch를 이용해서 트랜스포머 구조를 만들어버리는 경우가 많은데, 사실 그렇게 해도 소스코드의 길이는 크게 더 길어지지 않는다. 다만 파이토치에서 제공하는 각종 딥 러닝용 편의함수를 쓰게 되면 그 내막에서 무엇이 이루어지는지 모르기 때문에 기본 원리에 대해선 제대로 이해하지 못할 확률이 높다. 특히 트랜스포머의 경우 결코 직관적으로 이해하기 쉬운 구조는 아니다. 그래서 이렇게 기본 언어 모델을 만들어 놓고 어탠션부터 하나씩 추가해서 결국 트랜스포머 구조를 만드는 것이 LLM을 제대로 공부해보고자 하는 사람에겐 권장된다.

 

코드 해석

파일 로드

파이썬에 익숙한 사람이라면 코드만 보고서도 "아 이렇게 돌아가는 거구나"라고 이해할 수 있다. 그렇지 않더라도 상심할 필요는 없다. 생소한 함수들이 많아서 그런데 하나씩 짚어보면 누구나 다 이해할 수 있는 코드라고 자신할 수 있다. 한 파트씩 살펴보며, 구현된 내용을 설명하고 이것이 실제 상용 LLM과 어떤 차이가 있는지 비교해보겠다.

 

with open("lyrics.txt", "r", encoding="utf-8") as f:
    text = f.read()

words = text.strip().split()
vocab = sorted(set(words))
vocab_size = len(vocab)

 

파일을 열고 띄어쓰기 기준으로 잘라 단어들을 vocab에 넣어둔다. 내가 준 학습 데이터를 이용했다면 데이터를 아래와 같을 것이다. 총 141개이다. 우린 이 단어만을 이용해서 문장을 만들 것이다. 

['a', 'afraid', 'air', 'all', 'alone', 'am', 'and', 'are', 'around', 'away', 'baby', 'balcony', 'ball', 'be', 'because', 'been', 'begging', 'both', 'but', 'can', 'close', 'come', 'coming', 'crowd', 'crying', 'dad', 'daddy', 'dead', 'did', 'difficult', 'do', 'dress', 'escape', 'ever', 'everything', 'eyes', 'fading', 'faith', 'feel', 'feeling', 'first', 'flashback', 'for', 'from', 'garden', 'go', 'got', 'gowns', 'ground', 'have', 'he', 'head', 'hello', 'how', 'i', 'if', 'in', 'is', 'it', 'juliet', 'just', 'keep', 'knelt', 'knew', 'know', 'left', 'letter', 'lights', 'little', 'love', 'make', 'marry', 'me', 'mess', 'met', 'my', 'never', 'not', 'of', 'oh', 'on', 'out', 'outskirts', 'party', 'pebbles', 'pick', 'please', 'prince', 'princess', 'pulled', 'quiet', 'real', 'really', 'ring', 'romeo', 'run', 'said', 'save', 'saw', 'say', 'scarlet', 'see', 'sneak', 'so', 'somewhere', 'staircase', 'standing', 'starts', 'stay', 'story', 'summer', 'take', 'talked', 'tell', 'that', 'the', 'there', 'they', 'think', 'this', 'through', 'throwing', 'tired', 'to', 'town', 'trying', 'waiting', 'was', 'way', 'we', 'were', 'what', 'when', 'while', 'white', 'will', 'wondering', 'yes', 'you', 'young', 'your']

 

여기서 우리는 가장 쉬운 예제를 위해 '단어'를 기본 단위로 사용하지만 실무적으로는 '토큰'을 사용한다. 토큰은 단어와 유사하지만 좀 더 의미단위로 잘게 쪼개놓은 sub-word 들도 포함한다. 예를 들어 영어에선 un-break-able 처럼 자주 쓰이는 접두사, 접미사를 별도 토큰으로 저장해서 단어 수를 줄일 수도 있고, 한글에서는 "한글" "에서는"과 같이 띄어쓰기 기준으로 나누기 애매한 어절들을 구분해두고 조합할 수 있다. 따라서 진짜 그럴듯한 언어 능력을 위해선 사실 토큰을 사용하는게 필수적이다.

 

하지만 단어를 토큰 단위로 분해하는 것은 꽤나 어렵다. 별도의 "토크나이저"를 구현하거나 누가 만들어 놓은 토큰 딕셔너리를 가져다 쓰는데, 후자가 훨씬 일반적이다. 하지만 이는 보통 사이즈가 수만 단위이므로, 단 100여개의 단어만 사용하는 이 미니 모델에선 그마저도 일단은 생략한다. 참고삼아 여기서 구경은 해볼 수 있다.

 

초기화 (하이퍼파라미터 설정 및 가중치 행렬 생성)

block_size = 4
embed_size = 8
lr = 0.05

W_embed = np.random.normal(0, 0.01, size=(vocab_size, embed_size))
W_proj = np.random.normal(0, 0.01, size=(embed_size, vocab_size))

 

각종 기준 값들 설정 및 초기화 영역이다. 정말 타이니한 모델이므로 수치가 아주 귀엽다. 블록 사이즈 4는 한번에 학습할 사이즈이다. 이 모델에선 4개 단어를 보고 그 다음 한개의 단어를 예측할 것이다. 그리고 임베딩 벡터의 차원은 8로 한다.

 

※ "왜 단어 예측을 하지?" 혹은 "임베딩 벡터가 뭐지?"라는 의문이 든다면 이전글을 먼저 읽고 오는 것을 추천한다.

 

그리고 우리의 모델 그 자체인 두개 행렬(W_embed, W_proj)을 우선 랜덤으로 초기화해준다. 학습이 끝나고 나면 이 행렬 두개가 우리의 모델을 대표하게 된다. 많은 상용 모델들이 소스 코드는 공개를 안해도 이 가중치만 "모델"이라고 지칭하며 공개할만큼 인공지능 모델의 핵심 자산이다.

 

행렬의 사이즈에 주목하자. 임베딩 행렬(W_embed)는 한줄 한줄이 단어의 임베딩 벡터이다. 그래서 단어 수를 행 사이즈로 하고, 임베딩 벡터의 차원 수를 열 사이즈로 하는 것이다. 그리고 프로젝션 행렬(W_proj)는 예측에 사용되는 가중치인데, 사이즈가 임베딩 행렬을 뒤집어 놓은 사이즈다. 여기선 각각의 행이 하나의 단어에 대응이 된다.

 

아래 그림은 단순화를 위해 임베딩 벡터를 8차원이 아닌 4차원으로 그려보았다.

 

 

 

여기서 만약 afraid인 0.2, 0.9, 0.6, 0.3을 W_proj에 곱하면 어떻게 될까? a에도 곱해지고 afraid에도 곱해지고, air에도 곱해지고, 결국 your까지 모든 단어에 곱해지게 된다. 

 

예측의 기본 개념과 학습

 

a       → 0.2×0.3 + 0.9×0.6 + 0.6×0.5 + 0.3×0.8 = 1.14  
afraid  → 0.2×0.1 + 0.9×0.9 + 0.6×0.4 + 0.3×0.1 = 1.10  
air     → 0.2×0.7 + 0.9×0.2 + 0.6×0.1 + 0.3×0.4 = 0.50  
your    → 0.2×0.1 + 0.9×0.7 + 0.6×0.2 + 0.3×0.1 = 0.80

 

즉 [1 x 141]의 행렬, 즉 숫자들이 죽 나열된 1차원 배열이 결과로 나오게 되고, 이 중 가장 큰 값이 나온 단어를 선택하는 것이 기본 골자이다. 어떻게 가장 큰 값이 옮은 답이 되냐고? 당연히 처음에는 그렇게 안 된다. 그렇게 될 수 있도록 수만, 수백만번 실행을 하면서 값을 조정해나가는 것이다.

for step in range(100000):
    ix = np.random.randint(0, len(words) - block_size - 1)
    x = words[ix : ix + block_size]
    y = words[ix + block_size]

    x_indices = [vocab.index(w) for w in x]
    y_index = vocab.index(y)

    emb = W_embed[x_indices]  
    context = emb.mean(axis=0)
    logits = context @ W_proj
    probs = np.exp(logits) / np.exp(logits).sum()

    (이하 생략)

 

가장 핵심이 되는 학습 루프이다. 먼저 가사에서 랜덤으로 4단어씩 추출한다. 여기선 단순한 예제이니 단순 4단어씩 무작위로 추출하지만 실제 좀 더 고도화된 LLM에선 이렇게 단순히 랜덤을 취하진 않는다. 이러면 확률적으로 아무리 많이 돌려도 학습을 못하고 지나치는 부분이 생길 수 있기 때문이다. 여하튼, 우리의 모델은 이 추출된 4단어 다음에 올 1단어를 학습 대상으로 한다.

 

그런데 아까는 afraid 한 단어 [ 1 x 8 ] 를 가지고 곱했으니 행렬의 사이즈가 딱 맞았는데, 4단어 [ 4 x 8 ]라니 이걸 어떻게 프로젝션에 곱할까? 그래서 우리는 4단어로 이루어지 문장 조차도 8차원의 임베딩 벡터로 만들어 줘야 한다. 이를 위한 다양한 기법이 있는데 여기서는 그냥 mean을 사용했다. 즉 4개의 숫자의 평균을 취해서 이를 벡터 값으로 사용하기로 한 것이다. 

 

문맥 생성

emb = W_embed[x_indices]  # 각 단어의 임베딩을 가져와서
context = emb.mean(axis=0)  # 각 열별로 평균을 취한다.

afraid → [0.2, 0.9, 0.6, 0.3, 0.1, 0.5, 0.8, 0.4]  
air    → [0.8, 0.3, 0.1, 0.7, 0.2, 0.9, 0.3, 0.5]  
all    → [0.4, 0.2, 0.8, 0.6, 0.7, 0.1, 0.5, 0.6]  
alone  → [0.5, 0.6, 0.9, 0.1, 0.3, 0.4, 0.2, 0.7]  
↓ (mean)   ↓     ↓     ↓     ↓     ↓     ↓     ↓  
context→ [0.5, 0.5, 0.6, 0.4, 0.3, 0.5, 0.4, 0.6]

 

이렇게 여러개의 단어 조합을 하나의 임베딩 벡터로 만든 것을 context, 즉 문맥이라고 한다. 하지만 여기서 영특한 사람은 "이게 대표성 있는 문맥이 될 수 있나요? 단어들이 뭉개져 버리고 순서도 무시가 되는데요?"라고 반문할 수 있다. 맞는 말이다! 사실 순서 보전 등을 위한 여러 트릭들이 있지만 여기선 그냥 간단한 예제를 위해 무시한 것이다. 이어지는 강의에서는 더 좋은 문맥을 만들기 위한 기법도 다를 것이다.

 

여튼 지금 처럼 단순한 데이터, 쉬운 학습에서는 이렇게 문맥을 만들어도 결과는 나름 잘 나온다.

 

 

아까 우리가 afraid를 곱한 것처럼 번엔 문맥(context)을 W_proj에 곱해보자. 결과는 [ 1 x 141 ]의 행렬, 즉 141개의 숫자가 나올 것이다. 이걸 "로짓(Logit)" 이라고 부른다. 그리고 이 로짓을 probs = np.exp(logits) / np.exp(logits).sum() 또 이렇게 확률값으로 처리하는 걸 볼 수 있는데 이런 기법을 소프트맥스(softmax)라고 부른다.

 

소프트 맥스

사실상 가장 큰 값을 선택하면 되긴 하지만 softmax를 취하는 이유는, 항상 가장 큰 값만이 정답은 아니기 때문이다. 예를 들어 "한국인들이 좋아하는 과일은 [    ], [    ], [    ] 등이 있다."라는 말을 만들어 낼 때 저 안에 들어갈 과일은 딸기가 될 수도 있고 수박이 될 수도 있고 사과가 될 수도 있다. 어떤 것도 틀린 것은 아니며, 다른 과일이 나올 수도 있다. 언어 모델의 핵심은 항상 같은 말만 반복하지 않는 것이다. 그렇게 때문에 우리가 같은 질문을 해도 ChatGPT는 항상 다른 답변을 내놓고, 대화도 좀 더 자연스럽게 할 수 있다. 그렇기에 언어 모델은 확률에 따라 어느정도 무작위성 선택을 한다. 그런데 만약 예를 들어 딸기 1.05, 수박 2.71, 사과 1.66이라는 계산 결과가 나왔다면 어떤 기준으로 선택을 해야 할까? 그 기준 중 하나가 소프트맥스이다. 아래와 같이 계산된다.

 

exp(1.05) ≈ 2.857  
exp(2.71) ≈ 15.036  
exp(1.66) ≈ 5.260  

합계 ≈ 23.153

딸기: 2.857 / 23.153 ≈ 0.12  
수박: 15.036 / 23.153 ≈ 0.65  
사과: 5.260 / 23.153 ≈ 0.23

 

도합 1 즉, 100%가 되도록 계산을 해주는데, 모델이 각각 12%, 65%, 23%의 확률로 다음 단어를 선택하도록 기준을 제시해주는 것이다. 근데 왜 꼭 지수함수(exp)를 취하는가? 그냥 숫자 그대로 합계를 내서 나눠도 되지 않나? 물론, 안 될 건 없다.

딸기: 1.05  
수박: 2.71  
사과: 1.66  

합계 ≈ 5.42

딸기: 1.05 / 5.42 ≈ 0.19  
수박: 2.71 / 5.42 ≈ 0.50  
사과: 1.66 / 5.42 ≈ 0.31

 

하지만 exp를 취함으로써 얻는 이점이 있다. 우선은 마이너스 값이 있더라도 양수로 치환이 되므로 계산이 쉬워진다. 그리고 작은 차를 더 크게 만들어주므로 높은 값의 비중을 더 크게 만드는 것이다. 어찌되었든 연구자들이 오랜 기간 다양한 시도를 해 본 결과 이렇게 하는 것이 품질이 더 좋다고 판단했기 때문에 이렇게 하는 것일 뿐이지, 당신만의 소프트맥스를 개발해도 전혀 문제 없다. 그게 당신의 모델에는 더 좋은 결과를 가져온다면 말이다.

 

틀린 방향과 정도를 계산

이젠 예측을 한번 시도해봤으니 학습, 즉 가중치를 조절할 차례이다.

for step in range(100000):
    (생략)
    probs[y_index] -= 1
    dW_proj = np.outer(context, probs)
    dcontext = probs @ W_proj.T
    (생략)

 

로그 출력을 위한 소스코드는 생략하고 보자. 앞서 우리는 context 다음에 나올 단어를 구하기 위해, 141개 모든 단어에 대한 점수와 확률을 구하였다. 이 141개의 확률 중에서 정답이 되는 확률에만 -1을 한다. 이는 해당 확률만 반전을 시킨다고 보면 되겠다. 예를 들어 만약 우리가 구한 확률이 아래와 같은데, 정답은 alone이라고 하자. 1.0이어야 하는데 0.2가 구해졌으니 0.8만큼 틀린 것이다. 나머지는 모두 0.0이어야 하는데 0.05, 0.1. 0.2 등의 값이 나왔으니 0.05, 0.1, 0.2만큼 틀린 것이다. 틀린 방향이 반대이므로 0.8만 부호가 다르다.

 

 

여기서 벡터의 외적 곱(outer product)이라는 개념이 나온다.

 

dW_proj = np.outer(context, probs)

 

행 벡터(context)와 열 백터(probs)를 곱해서 펼치는 것이다. 말은 어렵지만 아래 그림을 보면 쉽다. 각 행과 열에 해당하는 수의 곱을 나열하면 행 벡터 사이즈 x 열 벡터 사이즈(8x141) 의 행렬이 나온다. 아래 그림은 쉬운 이해를 위해 행 수를 4로 하였다. 우리의 코드 상에서는 8이다.

 

 

 

이것은 우리가 프로젝션 가중치를 변화시킬 양이다. 앞서 우린 prob에 -1을 한 "정답과의 차이"를 만들어냈다. 이를 diff라고 하겠다. 여기에 context를 내적하는 것은 프로젝션을 구성하는 각 단어들에 대해, 틀린 정도를 구하는 것이다. 미분의 개념을 고려하면 "기울기(gradient)"라는 표현이 쓰이는데 이 글에선 어려운 수학점 개념은 일단 배제하도록 하겠다.

 

틀린 방향 반대로 학습

for step in range(100000):
    (생략)
    for idx in x_indices:
        W_embed[idx] -= lr * dcontext
    W_proj -= lr * dW_proj

 

여튼 틀린 정도를 구했으니 이걸 현재 가충치에 반영해주면 된다. 여기서 학습률(Learning Rate)이 등장하는데, 우리의 코드에선 lr = 0.05라는 값을 초기에 정의하였다. 이 값은 하이퍼파라미터로, 어떤 값이 가장 좋을지 알 수 없어서 여러 값을 넣어보며 테스트를 해봐야 한다. 내 코드에서도 결과가 좋은 임의의 값을 사용한 것이다.

 

이와 같은 맥락으로 우리는 단어의 임베딩 값도 수정을 해준다.

 

dcontext = probs @ W_proj.T 

 

우리는 앞서 probs가 정답과의 차이라고 설명하였다. 이 정답과의 차이에 8x141의 프로젝션을 141x8로 뒤집어서 곱해주면 프로젝션 입장에서 context가 어느 방향으로 수정되어야 하는지가 계산된다. 앞서 context를 W_proj와 곱해서 logits을 만들고 이것을 '틀린 정도'로 표현한 것이 probs라는 걸 기억하자. 계산을 역으로 함으로써 context의 틀린 정도를 구하는 것이다. 그리고 이를, context를 구성한 단어의 임베딩 벡터에 학습률(lr)만큼 조금씩 반영을 해주는 것이다.

 

직관적인 예시

혹시라도 숫자적으로 안되는 독자를 위해 조금만 더 직관적으로 설명하면 이렇다. 윗 내용이 이해가 되었다면 아래 내용은 그냥 넘어가 된다. 하지만 윗 내용이 이해가 안되었다면 아래 내용은 한줄 한줄 천천히 읽어보길 바란다.

 

"i love" 다음 "you"를 예측하고자 한다.

→ 그런데 "i love go"가 나와버렸다!?

  그러면 우리는 계산에 사용되는 숫자를 "i love" 다음 "you"가 나오는 방향으로 살짝 조정한다. 

  그리고 "i" 와 "love"에 해당하는 숫자 또한 "you"가 나오는 방향으로 조금 조정한다.

 

예를 들어 "i"의 embed가 [20, 50], "love"의 embed [10, 90]이라서 "i love"라는 문맥(context)은 [15, 70]로 계산되었다. (mean)

 

여기에 가중치 [3, 0.5]가 곱해져서 [45, 35]가 나왔는데, 이는 "go"에 해당하는 숫자였다고 하자. (15 × 3, 70 × 0.5)

그런데 정답은 [80, 10]이라는 embed값을 가진 "you"였다.

그럼 우리는 가중치 [3, 0.5]3은 좀 더 커지고, 0.5는 조금 더 작아져야 한다는 것을 안다!

 

그리고 문맥 [15, 70]을 구성하는 [20, 50] [10, 90]의 값들도 수정한다. 우리는 첫번째 값들(20, 10)이 조금 더 커지고, 두번째 값들(50, 90)은 조금 더 작아져야 한다는 것을 안다!

 

그런데 여기서 "어느정도로 커지고, 어느정도로 작아져야 하는가?"를 softmax를 통해 계산을 한 것이다.

 

이렇게 "i love you", "you like me", "i hate Kate", "i miss you", "i want to go", "you need to come" 등의 다양한 경우를 학습하다 보면 자연스럽게 "i", "you", "Kate"는 좀 더 가까운 거리(임베딩 벡터의 숫자가 가까움)로 모이게 된다. 이것이 사람이기 때문에 그렇다는 인지까지는 못하더라도, 적어도 목적어 자리에 온다는 어렴풋한 감을 가지는 것이다. 동사인 love와 miss, like 등도 가까운 거리로 모이게 된다. 그래서 적어도 "i love go"와 같이 말이 안되는 말을 할 확률은 줄어들게 되는 것이다.

 

그런데 임베딩 벡터의 차원 수가 너무 작으면 you와 Kate가 가까운 거리에 있다 보니 "i love you"가 나와야 하는데 "i love Kate"가 나오는 불륜 상황이 연출될 수 있다. 이 방지하기 위해선 임베딩 벡터를 [80, 10] 같은 2차원보단 [20, 52, 12, 55, 19, 91, 15, 32, 3, 75]와 같은 더 높은 차원으로 만들어주면 된다. 그럼 학습 과정에서 훨씬 다방면으로 저 숫자들이 업데이트 된다. 어떤 숫자들은 용법이 같은 것을 인지하고 가까워지겠지만, 어떤 숫자들은 "love" 뒤에 오는 것과 "hate" 뒤에 오는 것을 다르게 인지하고 멀어지기도 할 것이다.

 

사실 이번엔 학습 데이터가 무척 간단하므로 8차원을 사용했지만 실제론 백차원은 되어야 진짜 다양한 단어를 적절하게 이해할 수 있다. 

 

실행을 시켜보자

너무나도 간단한 데이터이고, 복잡한 트릭은 모두 생략된 코드이므로 정말 빠르게 돌아갈 것이다. 10만번, 20만번 학습을 충분히 시키면 아래와 같이 결과가 나온다. (난수 때문에 돌릴 때 마다 결과가 다르니 아래와는 내용이 다를 수 있다.)

Step 0000 | Input: baby just say yes | Target: romeo | Pred: this | Loss: 0.9929
Step 10000 | Input: you will be the | Target: prince | Pred: and | Loss: 0.9419
Step 20000 | Input: to tell me how | Target: to | Pred: run | Loss: 0.4110
Step 30000 | Input: tired of waiting wondering | Target: if | Pred: wondering | Loss: 0.1371
Step 40000 | Input: story baby just say | Target: yes | Pred: say | Loss: 0.0404
Step 50000 | Input: run you will be | Target: the | Pred: make | Loss: 0.0345
Step 60000 | Input: see you we keep | Target: quiet | Pred: quiet | Loss: 0.0487
Step 70000 | Input: met you on the | Target: outskirts | Pred: of | Loss: 0.0517
Step 80000 | Input: run you will be | Target: the | Pred: the | Loss: 0.0115
Step 90000 | Input: trying to tell me | Target: how | Pred: how | Loss: 0.0491

Generated:
romeo take me somewhere we can be alone i will be waiting all there

 

즉 가사를 거의 외워 버린 것이나 마찬가지다. 그럴 수 밖에 없는 것이, 이 가사로만 언어를 배운다면 누가 romeo take me somewhere이라고 했을때 "we"말고는 다른 단어를 떠올릴 수가 없다. 그리고 "take me some where we"이라고 하면 "can"을 떠올릴 수 밖에 없다. 만약 romeo take me somwhere 다음에 다른 단어가 나오는 텍스트들이 많았다면 좀 더 다양한 문장이 나왔겠지만 지금의 한정된 학습 데이터로는 어쩔 수 없다. 하지만 만약 학습을 좀 덜 시키면?

 

아래는 3만번만 학습을 시킨 결과이다.

Step 0000 | Input: alone i will be | Target: waiting | Pred: gowns | Loss: 0.9929
Step 5000 | Input: oh oh oh oh | Target: oh | Pred: because | Loss: 0.1306
Step 10000 | Input: you will be the | Target: prince | Pred: the | Loss: 0.8320
Step 15000 | Input: you on the outskirts | Target: of | Pred: town | Loss: 0.9181
Step 20000 | Input: out of this mess | Target: it | Pred: this | Loss: 0.3343
Step 25000 | Input: alone i keep waiting | Target: for | Pred: know | Loss: 0.7311

Generated:
romeo take me somewhere are can be alone i will waiting waiting begging you

 

뭔가 애절한 노래 가사 같지만 문법도 살짝 틀렸고 다른 결과가 나왔다. 아직 임베딩이나 가중치가 충분히 자리잡지 못한 것이다.

 

다음에 배울 것

다음 시간엔 이번에 다루지 못했던 여러 LLM 기법들을 다뤄보겠다. 어탠션 메커니즘이나 순서 인코딩 등이다. 그리고 여러개의 레이어가 포함된 딥러닝 매커니즘을 적용해볼 수도 있다. 지금은 학습을 너무 많이 시키면 가사를 외워 버리는 식이고, 학습을 덜 시키면 문법적으로 말이 안되는 말을 만들어내지만, 우리가 좀 더 LLM을 현대 LLM 답게 만들면 그냥 "romeo"만 던져줘도 뭔가 그럴듯한 다른 가사를 만들어낸다. 문법적으로도 완벽하고 내용도 노래 가사랑 결이 맞는! 

 

마치며 

이번 강의는 우리가 앞으로 두고두고 사용할 베이스가 되는 코드에 대해 하나하나 짚고 넘어가다 보니 다소 길어졌습니다. 다음 강의는 최소한 저번 인트로와 이번 회차의 내용은 모두 이해하였다고 가정하고 진행을 할 예정입니다. 따라서 이번 내용은 꼭 숙지해 주시고 모르는게 있으면 댓글로 질문을 올려주시기 바랍니다.

반응형