심화세션 - NLP.week5 ~ 7
Building a GPT
# 셰익스피어 파일 다운로드
!wget https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt
with open('input.txt', 'r', encoding='utf-8') as f:
text = f.read()
# 파일 열기
print("length of dataset in characters: ", len(text))
# length of dataset in characters: 1115394
# 파일의 총 길이 1115394 정도
chars = sorted(list(set(text))) # text의 모든 문자 집합 리스트로 만들어서 정렬
vocab_size = len(chars)
print(''.join(chars))
print(vocab_size)
account_circle
# !$&',-.3:;?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
# 65
- small shakespear에서 character들만 추출 한 것이다.
# create a mapping from characters to integers
stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s]
# string -> integer
decode = lambda l: ''.join([itos[i] for i in l])
# integer -> string
print(encode("hii there"))
print(decode(encode("hii there")))
# [46, 47, 47, 1, 58, 46, 43, 56, 43]
# hii there
- one to one encoding
- 하나의 character가 하나의 숫자를 의미
# encoding decoding으로 전체 훈련 세트 토큰화
import torch # we use PyTorch: https://pytorch.org
data = torch.tensor(encode(text), dtype=torch.long) # 인코딩하고 토치로 포장
print(data.shape, data.dtype)
print(data[:1000])# the 1000 characters we looked at earier will to the GPT look like this
"""
torch.Size([1115394]) torch.int64
tensor([18, 47, 56, 57, 58, 1, 15, 47, 58, 47, 64, 43, 52, 10, 0, 14, 43, 44,
53, 56, 43, 1, 61, 43, 1, 54, 56, 53, 41, 43, 43, 42, 1, 39, 52, 63,
1, 44, 59, 56, 58, 46, 43, 56, 6, 1, 46, 43, 39, 56, 1, 51, 43, 1,
57, 54, 43, 39, 49, 8, 0, 0, 13, 50, 50, 10, 0, 31, 54, 43, 39, 49,
6, 1, 57, 54, 43, 39, 49, 8, 0, 0, 18, 47, 56, 57, 58, 1, 15, 47,
58, 47, 64, 43, 52, 10, 0, 37, 53, 59, 1, 39, 56, 43, 1, 39, 50, 50,
1, 56, 43, 57, 53, 50, 60, 43, 42, 1, 56, 39, 58, 46, 43, 56, 1, 58,
53, 1, 42, 47, 43, 1, 58, 46, 39, 52, 1, 58, 53, 1, 44, 39, 51, 47,
57, 46, 12, 0, 0, 13, 50, 50, 10, 0, 30, 43, 57, 53, 50, 60, 43, 42,
8, 1, 56, 43, 57, 53, 50, 60, 43, 42, 8, 0, 0, 18, 47, 56, 57, 58,
1, 15, 47, 58, 47, 64, 43, 52, 10, 0, 18, 47, 56, 57, 58, 6, 1, 63,
53, 59, 1, 49, 52, 53, 61, 1, 15, 39, 47, 59, 57, 1, 25, 39, 56, 41,
47, 59, 57, 1, 47, 57, 1, 41, 46, 47, 43, 44, 1, 43, 52, 43, 51, 63,
1, 58, 53, 1, 58, 46, 43, 1, 54, 43, 53, 54, 50, 43, 8, 0, 0, 13,
50, 50, 10, 0, 35, 43, 1, 49, 52, 53, 61, 5, 58, 6, 1, 61, 43, 1,
49, 52, 53, 61, 5, 58, 8, 0, 0, 18, 47, 56, 57, 58, 1, 15, 47, 58,
47, 64, 43, 52, 10, 0, 24, 43, 58, 1, 59, 57, 1, 49, 47, 50, 50, 1,
46, 47, 51, 6, 1, 39, 52, 42, 1, 61, 43, 5, 50, 50, 1, 46, 39, 60,
43, 1, 41, 53, 56, 52, 1, 39, 58, 1, 53, 59, 56, 1, 53, 61, 52, 1,
54, 56, 47, 41, 43, 8, 0, 21, 57, 5, 58, 1, 39, 1, 60, 43, 56, 42,
47, 41, 58, 12, 0, 0, 13, 50, 50, 10, 0, 26, 53, 1, 51, 53, 56, 43,
1, 58, 39, 50, 49, 47, 52, 45, 1, 53, 52, 5, 58, 11, 1, 50, 43, 58,
1, 47, 58, 1, 40, 43, 1, 42, 53, 52, 43, 10, 1, 39, 61, 39, 63, 6,
1, 39, 61, 39, 63, 2, 0, 0, 31, 43, 41, 53, 52, 42, 1, 15, 47, 58,
47, 64, 43, 52, 10, 0, 27, 52, 43, 1, 61, 53, 56, 42, 6, 1, 45, 53,
53, 42, 1, 41, 47, 58, 47, 64, 43, 52, 57, 8, 0, 0, 18, 47, 56, 57,
58, 1, 15, 47, 58, 47, 64, 43, 52, 10, 0, 35, 43, 1, 39, 56, 43, 1,
39, 41, 41, 53, 59, 52, 58, 43, 42, 1, 54, 53, 53, 56, 1, 41, 47, 58,
47, 64, 43, 52, 57, 6, 1, 58, 46, 43, 1, 54, 39, 58, 56, 47, 41, 47,
39, 52, 57, 1, 45, 53, 53, 42, 8, 0, 35, 46, 39, 58, 1, 39, 59, 58,
46, 53, 56, 47, 58, 63, 1, 57, 59, 56, 44, 43, 47, 58, 57, 1, 53, 52,
1, 61, 53, 59, 50, 42, 1, 56, 43, 50, 47, 43, 60, 43, 1, 59, 57, 10,
1, 47, 44, 1, 58, 46, 43, 63, 0, 61, 53, 59, 50, 42, 1, 63, 47, 43,
50, 42, 1, 59, 57, 1, 40, 59, 58, 1, 58, 46, 43, 1, 57, 59, 54, 43,
56, 44, 50, 59, 47, 58, 63, 6, 1, 61, 46, 47, 50, 43, 1, 47, 58, 1,
61, 43, 56, 43, 0, 61, 46, 53, 50, 43, 57, 53, 51, 43, 6, 1, 61, 43,
1, 51, 47, 45, 46, 58, 1, 45, 59, 43, 57, 57, 1, 58, 46, 43, 63, 1,
56, 43, 50, 47, 43, 60, 43, 42, 1, 59, 57, 1, 46, 59, 51, 39, 52, 43,
50, 63, 11, 0, 40, 59, 58, 1, 58, 46, 43, 63, 1, 58, 46, 47, 52, 49,
1, 61, 43, 1, 39, 56, 43, 1, 58, 53, 53, 1, 42, 43, 39, 56, 10, 1,
58, 46, 43, 1, 50, 43, 39, 52, 52, 43, 57, 57, 1, 58, 46, 39, 58, 0,
39, 44, 44, 50, 47, 41, 58, 57, 1, 59, 57, 6, 1, 58, 46, 43, 1, 53,
40, 48, 43, 41, 58, 1, 53, 44, 1, 53, 59, 56, 1, 51, 47, 57, 43, 56,
63, 6, 1, 47, 57, 1, 39, 57, 1, 39, 52, 0, 47, 52, 60, 43, 52, 58,
53, 56, 63, 1, 58, 53, 1, 54, 39, 56, 58, 47, 41, 59, 50, 39, 56, 47,
57, 43, 1, 58, 46, 43, 47, 56, 1, 39, 40, 59, 52, 42, 39, 52, 41, 43,
11, 1, 53, 59, 56, 0, 57, 59, 44, 44, 43, 56, 39, 52, 41, 43, 1, 47,
57, 1, 39, 1, 45, 39, 47, 52, 1, 58, 53, 1, 58, 46, 43, 51, 1, 24,
43, 58, 1, 59, 57, 1, 56, 43, 60, 43, 52, 45, 43, 1, 58, 46, 47, 57,
1, 61, 47, 58, 46, 0, 53, 59, 56, 1, 54, 47, 49, 43, 57, 6, 1, 43,
56, 43, 1, 61, 43, 1, 40, 43, 41, 53, 51, 43, 1, 56, 39, 49, 43, 57,
10, 1, 44, 53, 56, 1, 58, 46, 43, 1, 45, 53, 42, 57, 1, 49, 52, 53,
61, 1, 21, 0, 57, 54, 43, 39, 49, 1, 58, 46, 47, 57, 1, 47, 52, 1,
46, 59, 52, 45, 43, 56, 1, 44, 53, 56, 1, 40, 56, 43, 39, 42, 6, 1,
52, 53, 58, 1, 47, 52, 1, 58, 46, 47, 56, 57, 58, 1, 44, 53, 56, 1,
56, 43, 60, 43, 52, 45, 43, 8, 0, 0])
"""
# data를 train, validation으로 분리
n = int(0.9*len(data)) # 처음 90% train, 나머지 val
train_data = data[:n]
val_data = data[n:]
# 전체 훈련 data를 다 넘기는 것은 비용이 너무 큼 블록 사이즈로 넘기기
block_size = 8
train_data[:block_size+1]
# tensor([18, 47, 56, 57, 58, 1, 15, 47, 58])
# 훈련 세트 시퀀스에서 9개의 문자
- 8개의 개별 예가 들어가 있음
- 18 → 47
- [18, 47] → 56
- [18, 47, 56] → 57 \(\vdots\)
x = train_data[:block_size]
y = train_data[1:block_size+1]
for t in range(block_size):
context = x[:t+1]
target = y[t]
print(f"when input is {context} the target: {target}")
"""
when input is tensor([18]) the target: 47
when input is tensor([18, 47]) the target: 56
when input is tensor([18, 47, 56]) the target: 57
when input is tensor([18, 47, 56, 57]) the target: 58
when input is tensor([18, 47, 56, 57, 58]) the target: 1
when input is tensor([18, 47, 56, 57, 58, 1]) the target: 15
when input is tensor([18, 47, 56, 57, 58, 1, 15]) the target: 47
when input is tensor([18, 47, 56, 57, 58, 1, 15, 47]) the target: 58
"""
- 다음 char를 예상할때는 input으로 그전의 모든 char들을 context로 삼아서 예측
torch.manual_seed(1337) #데이터 샘플링
batch_size = 4
# how many independent sequences will we process in parallel?
block_size = 8
# what is the maximum context length for predictions?
def get_batch(split):
# generate a small batch of data of inputs x and targets y
data = train_data if split == 'train' else val_data
# "train"이면 훈련 데이터, 아니면 검증 데이터
ix = torch.randint(len(data) - block_size, (batch_size,))
# random 하게 char들을 고를 index를 고름
# 4개의 0~ len(data) - block_size의 범위의 숫자들이 들어감
x = torch.stack([data[i:i+block_size] for i in ix])
y = torch.stack([data[i+1:i+block_size+1] for i in ix])
return x, y
xb, yb = get_batch('train')
print('inputs:')
print(xb.shape)
print(xb)
print('targets:')
print(yb.shape)
print(yb)
print('----')
for b in range(batch_size):
# batch dimensionfor t in range(block_size):
# time dimension
context = xb[b, :t+1]
target = yb[b,t]
print(f"when input is {context.tolist()} the target: {target}")
"""
inputs:
torch.Size([4, 8])
tensor([[24, 43, 58, 5, 57, 1, 46, 43],
[44, 53, 56, 1, 58, 46, 39, 58],
[52, 58, 1, 58, 46, 39, 58, 1],
[25, 17, 27, 10, 0, 21, 1, 54]])
targets:
torch.Size([4, 8])
tensor([[43, 58, 5, 57, 1, 46, 43, 39],
[53, 56, 1, 58, 46, 39, 58, 1],
[58, 1, 58, 46, 39, 58, 1, 46],
[17, 27, 10, 0, 21, 1, 54, 39]])
----
when input is [24] the target: 43
when input is [24, 43] the target: 58
when input is [24, 43, 58] the target: 5
when input is [24, 43, 58, 5] the target: 57
when input is [24, 43, 58, 5, 57] the target: 1
when input is [24, 43, 58, 5, 57, 1] the target: 46
when input is [24, 43, 58, 5, 57, 1, 46] the target: 43
when input is [24, 43, 58, 5, 57, 1, 46, 43] the target: 39
when input is [44] the target: 53
when input is [44, 53] the target: 56
when input is [44, 53, 56] the target: 1
when input is [44, 53, 56, 1] the target: 58
when input is [44, 53, 56, 1, 58] the target: 46
when input is [44, 53, 56, 1, 58, 46] the target: 39
when input is [44, 53, 56, 1, 58, 46, 39] the target: 58
when input is [44, 53, 56, 1, 58, 46, 39, 58] the target: 1
when input is [52] the target: 58
when input is [52, 58] the target: 1
when input is [52, 58, 1] the target: 58
when input is [52, 58, 1, 58] the target: 46
when input is [52, 58, 1, 58, 46] the target: 39
when input is [52, 58, 1, 58, 46, 39] the target: 58
when input is [52, 58, 1, 58, 46, 39, 58] the target: 1
when input is [52, 58, 1, 58, 46, 39, 58, 1] the target: 46
when input is [25] the target: 17
when input is [25, 17] the target: 27
when input is [25, 17, 27] the target: 10
when input is [25, 17, 27, 10] the target: 0
when input is [25, 17, 27, 10, 0] the target: 21
when input is [25, 17, 27, 10, 0, 21] the target: 1
when input is [25, 17, 27, 10, 0, 21, 1] the target: 54
when input is [25, 17, 27, 10, 0, 21, 1, 54] the target: 39
"""
torch.randint(len(data) - block_size, (batch_size,))- 데이터에서 랜덤으로 인덱스를 선택
- 선택된 인덱스로부터
block_size길이만큼의 시퀀스를 잘라냄 len(data) - block_size를 사용하는 이유는,block_size만큼의 공간을 확보하여 샘플이 데이터의 끝을 넘어가지 않도록 하기 위해- 만약
block_size를 빼지 않으면, 마지막 인덱스에서 시퀀스가 초과되어 인덱스 에러가 발생
- 만약
import torch
import torch.nn as nn
from torch.nn import functional as F
torch.manual_seed(1337)
class BigramLanguageModel(nn.Module):
def __init__(self, vocab_size):
super().__init__()
# each token directly reads off the logits for the next token from a lookup table
self.token_embedding_table = nn.Embedding(vocab_size, vocab_size)
# 각 단어(토큰)를 고유한 벡터로 변환하는 임베딩 레이어. vocab_size * vocab_size 크기의 행렬
def forward(self, idx, targets=None):
# idx: input index
# idx and targets are both (B,T) tensor of integers
logits = self.token_embedding_table(idx) # (B,T,C)
# idx 토큰화해서 전달
# c는 문자열의 종류
if targets is None:
loss = None
else:
B, T, C = logits.shape
logits = logits.view(B*T, C)
# logits는 시퀀스의 다음 문자에 대한 점수 = [8*4, 65]
targets = targets.view(B*T)
# target [32]로 각 logit이 어떤 값을 가져야하는지 알려줌
loss = F.cross_entropy(logits, targets)
# target과 -log(softmax(predicted)) 의 합들을 다 더하기 -> loss 작을수록 logit 값과 target 값 근접
return logits, loss
def generate(self, idx, max_new_tokens):
# idx is (B, T) array of indices in the current contextfor _ in range(max_new_tokens):
# get the predictions
logits, loss = self(idx)
# focus only on the last time step
logits = logits[:, -1, :]
# becomes (B, C)# apply softmax to get probabilities
probs = F.softmax(logits, dim=-1) # (B, C)
# sample from the distribution
idx_next = torch.multinomial(probs, num_samples=1)
# 각 시퀀스에서 한 개의 토큰을 선택 후 배치 내 각 시퀀스에 대해 샘플링된 다음 토큰을 나타냄, 결과는 (B, 1) 크기의 텐서
idx = torch.cat((idx, idx_next), dim=1)
# 현재 시퀀스 idx에 샘플링된 다음 토큰 idx_next를 연결 -> (B, T+1) 크기의 시퀀스를 가짐
m = BigramLanguageModel(vocab_size)
logits, loss = m(xb, yb)
print(logits.shape)
print(loss)
print(decode(m.generate(idx = torch.zeros((1, 1), dtype=torch.long), max_new_tokens=100)[0].tolist()))
# m.generate를 사용해 새로운 텍스트를 생성
# torch.Size([32, 65])
# tensor(4.8786, grad_fn=<NllLossBackward0>)
#SKIcLT;AcELMoTbvZv C?nq-QE33:CJqkOKH-q;:la!oiywkHjgChzbQ?u!3bLIgwevmyFJGUGpwnYWmnxKWWev-tDqXErVKLgJ
- generate모델은 현재 전체를 입력하는 중
- ex. 원래는 마지막 문자를 이용해서만 만듦 → k를 만드려면 w만 필요, 현재는 전체 시퀀스를 입력한 다음 가장 마지막 부분만 보고 k를 예측
- 이후 history가 사용될 것이기 때문에 이렇게 구성
# 최적화 객체 생성
optimizer = torch.optim.AdamW(m.parameters(), lr=1e-3)
batch_size = 32
for steps in range(100):
# increase number of steps for good results...
# sample a batch of data
xb, yb = get_batch('train')
# evaluate the loss
logits, loss = m(xb, yb)
optimizer.zero_grad(set_to_none=True)
# 이전 단계 모든 gradient를 0으로
loss.backward()
optimizer.step()
# batch모드로 업데이트
print(loss.item()) # 4.65630578994751
for steps in range(100):100 → 1000 →10000으로 가면서 loss 줄어듦
print(decode(m.generate(idx = torch.zeros((1, 1), dtype=torch.long), max_new_tokens=500)[0].tolist()))
"""
oTo.JUZ!!zqe!
xBP qbs$Gy'AcOmrLwwt
p$x;Seh-onQbfM?OjKbn'NwUAW -Np3fkz$FVwAUEa-wzWC -wQo-R!v -Mj?,SPiTyZ;o-opr$mOiPJEYD-CfigkzD3p3?zvS;ADz;.y?o,ivCuC'zqHxcVT cHA
rT'Fd,SBMZyOslg!NXeF$sBe,juUzLq?w-wzP-h
ERjjxlgJzPbHxf$ q,q,KCDCU fqBOQT
SV&CW:xSVwZv'DG'NSPypDhKStKzC -$hslxIVzoivnp ,ethA:NCCGoi
tN!ljjP3fwJMwNelgUzzPGJlgihJ!d?q.d
pSPYgCuCJrIFtb
jQXg
pA.P LP,SPJi
DBcuBM:CixjJ$Jzkq,OLf3KLQLMGph$O 3DfiPHnXKuHMlyjxEiyZib3FaHV-oJa!zoc'XSP :CKGUhd?lgCOF$;;DTHZMlvvcmZAm;:iv'MMgO&Ywbc;BLCUd&vZINLIzkuTGZa
D.?
"""
The mathematical trick in self-attention
# consider the following toy example:
torch.manual_seed(1337)
B,T,C = 4,8,2
# batch, time, channels
# 현재 통신하지 않는 상태
x = torch.randn(B,T,C)
x.shape # torch.Size([4, 8, 2])
- 어떻게 통신해야 하는지
- 미래의 토큰으로부터 정보를 얻어선 안됨
- 내 과거의 토큰들과만 통신
- weighted aggregation 사용(음의 평균 구하기)
# We want x[b,t] = mean_{i<=t} x[b,i]
xbow = torch.zeros((B,T,C))
for b in range(B):
for t in range(T):
xprev = x[b,:t+1]# (t,C)
xbow[b,t] = torch.mean(xprev, 0)
- xbow에는 각 token 의 과거(전 위치) token값들의 평균이 들어가 있음
version2
# toy example illustrating how matrix multiplication can be used for a "weighted aggregation"
torch.manual_seed(42)
a = torch.tril(torch.ones(3, 3))
a = a / torch.sum(a, 1, keepdim=True)
b = torch.randint(0,10,(3,2)).float()
c = a @ b
print('a=')
print(a)
print('--')
print('b=')
print(b)
print('--')
print('c=')
print(c)
"""
a=
tensor([[1.0000, 0.0000, 0.0000],
[0.5000, 0.5000, 0.0000],
[0.3333, 0.3333, 0.3333]])
--
b=
tensor([[2., 7.],
[6., 4.],
[6., 5.]])
--
c=
tensor([[2.0000, 7.0000],
[4.0000, 5.5000],
[4.6667, 5.3333]])
"""
- tril
- 정사각형 행렬에 밑 삼각형 반에만 값을 채워줌
- a @ b
- a: sum한 것에 대한 평균
- b: random
- c: 행렬 곱
- 기존방식보다 더 빠르게 계산 가능
# version 2: using matrix multiply for a weighted aggregation
wei = torch.tril(torch.ones(T, T))
wei = wei / wei.sum(1, keepdim=True)
xbow2 = wei @ x
# (B, T, T) @ (B, T, C) ----> (B, T, C)
torch.allclose(xbow, xbow2) # True
version3
# version 3: use Softmax
tril = torch.tril(torch.ones(T, T))
wei = torch.zeros((T,T))
# T×T의 제로 행렬
# == affinity
wei = wei.masked_fill(tril == 0, float('-inf'))
# == cannot communicate through past
wei = F.softmax(wei, dim=-1)
# 소프트맥스로 확률 변환
xbow3 = wei @ x
torch.allclose(xbow, xbow3) # True
wei = wei.masked_fill(tril == 0, float('-inf'))- 상삼각행렬에 해당하는 부분(대각선 위의 요소)을 -∞로 채우기
- 나중에 소프트맥스를 적용했을 때 -∞ 부분 0이 됨
- 미래의 값이 현재에 영향을 주지 않도록 마스킹
torch.allclose(xbow, xbow3)- 이전 버전의 Self-Attention 출력
- xbow와 현재 버전의 출력 xbow3가 같은지 확인
minor code clean up, positional embedding
class BigramLanguageModel(nn.Module):
def __init__(self, vocab_size):
super().__init__()
# each token directly reads off the logits for the next token from a lookup table
self.token_embedding_table = nn.Embedding(vocab_size, n_embd)
self.position_embedding_table = nn.Embedding(block_size, n_embd)
self.lm_head = nn.Linear(n_embd, vocab_size)
def forward(self, idx, targets=None):
B, T = idx.shape
# idx and targets are both (B,T) tensor of integers
tok_emb = self.token_embedding_table(idx) # (B, T, C)
# 각 토큰을 임베딩 벡터로 변환
pos_emb = self.position_embedding_table(torch.arange(T, device = device)) # (T, C)
# 각 토큰이 시퀀스 내에서 어떤 위치에 있는지에 대한 위치 정보를 임베딩
x = tok_emb + pos_emb # (B, T, C)
# C: vocab size
# 두 임베딩을 더하여 위치와 토큰 정보를 결합
logits = self.lm_head(x) # (B,T,C)
# 선형 계층을 통해 모델의 출력 크기를 조정하여 logits 계산
- indirection을 위해 한번에 logit을 계산하지 않음
- token 자체의 embedding 도 구하고 index (position)에 대한 embedding도 구함
- Linear layer를 통해 logit에 대한 사이즈를 맞춘다.
version4
# version 4: self-attention!
torch.manual_seed(1337)
B,T,C = 4,8,32 # batch, time, channels
x = torch.randn(B,T,C)
# let's see a single Head perform self-attention
head_size = 16
key = nn.Linear(C, head_size, bias=False)
query = nn.Linear(C, head_size, bias=False)
value = nn.Linear(C, head_size, bias=False)
k = key(x) # (B, T, 16)
q = query(x) # (B, T, 16)
wei = q @ k.transpose(-2, -1) # (B, T, 16) @ (B, 16, T) ---> (B, T, T)
tril = torch.tril(torch.ones(T, T))
#wei = torch.zeros((T,T))
wei = wei.masked_fill(tril == 0, float('-inf'))
wei = F.softmax(wei, dim=-1) # softmax로 하삼각형 정규화
v = value(x)
out = wei @ v
#out = wei @ x
out.shape # torch.Size([4, 8, 16])
wei[0]
"""
tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.1574, 0.8426, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.2088, 0.1646, 0.6266, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5792, 0.1187, 0.1889, 0.1131, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0294, 0.1052, 0.0469, 0.0276, 0.7909, 0.0000, 0.0000, 0.0000],
[0.0176, 0.2689, 0.0215, 0.0089, 0.6812, 0.0019, 0.0000, 0.0000],
[0.1691, 0.4066, 0.0438, 0.0416, 0.1048, 0.2012, 0.0329, 0.0000],
[0.0210, 0.0843, 0.0555, 0.2297, 0.0573, 0.0709, 0.2423, 0.2391]],
grad_fn=<SelectBackward0>)
"""
- key 와 query를 내적해서 어떤 token에 집중해야 할지 파악
wei.masked_fill을 통해 query 이전의 position 들의 key들에게 dot product를 하지 않게 하고 그 값을 softmax를 통해 0~1의 표준화된 값으로 보여줌- 이후 값들은 무시함
- 8번째 row를 보면 query가 8번째 token이고 1~8 까지의 key(token) 들과 내적해서 가장 큰 0.2297 값이 나온 4번째 token 이 8번째 token과 관계가 깊다는것을 알 수 있음
- value는 다음 word를 예측하기위한 embdding matrix
- 최종적으로 주어진 관계를 통해 전달해야 할 정보의 내용
Attention은 정보 전달 메커니즘
노드들이 상호 간의 정보를 전달하는 그래프로 볼 수 있음
각 노드는 다른 노드들로부터 정보를 가중합방식으로 수집하며, 이 가중치는 각 노드 간의 관계를 표현
공간 개념의 부재와 위치 인코딩 필요성
Attention 메커니즘에는 공간적인 개념이 없음
Attention은 단순히 벡터들의 집합에서 상호작용이 일어나므로, 벡터가 텍스트의 앞부분에 위치하는지 뒷부분에 위치하는지에 대한 정보를 알지 못함
-> 포지션 인코딩 필요, 포지션 인코딩을 통해 각 토큰의 순서를 모델에 알려줘야 시퀀스의 순서 정보가 반영됨
배치 차원 간 독립성
배치 차원에서 각 예시는 서로 상호작용하지 않으며, 독립적인 Attention 계산이 이루어짐
Encoder와 Decoder Attention Block
- Encoder Attention Block
- 모든 토큰이 서로 자유롭게 정보를 주고받도록 마스킹 없이 동작
- 주로 번역 모델이나 정보 압축과 같은 비자기회귀 작업에 사용
- Decoder Attention Block
- 삼각 마스킹 적용
- 각 토큰은 자기 자신과 그 이전의 토큰들만 참고할 수 있음
- 자기회귀 모델에서 주로 사용
Self-Attention과 Cross-Attention
Self-Attention: 쿼리, 키, 값이 모두 같은 소스에서 생성 Cross-Attention: 쿼리는 주 시퀀스(예: 텍스트 시퀀스)에서 생성되지만, 키와 값은 다른 외부 소스에서 가져옴
Scaled Attention (스케일 조정된 Attention)
- Scaled Attention
-
쿼리와 키 간 내적 결과를 벡터 크기(차원 수)의 제곱근으로 나눈 값으로 조정 -> softmax 계산 안정적
-
소프트맥스 출력이 지나치게 특정 값에 집중하지 않도록 만들고, 고르게 분포될 수 있게 함
-
single self attention
class Head(nn.Module):
"""one head of self-attention"""
def __init__(self, head_size):
super().__init__()
self.key = nn.Linear(n_embd, head_size, bias=False)
self.query = nn.Linear(n_embd, head_size, bias=False)
self.value = nn.Linear(n_embd, head_size, bias=False)
# key query value 생성
self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))
# trill은 버퍼
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B,T,C = x.shape
k = self.key(x) # (B,T,C)
q = self.query(x) # (B,T,C)
# compute attention scores ("affinities")
wei = q @ k.transpose(-2,-1) * C**-0.5 # (B, T, C) @ (B, C, T) -> (B, T, T)
# 내적 계산하고 정규화
wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # (B, T, T)
# 디코더 역할, 과거와 통신하지 못하게 함
wei = F.softmax(wei, dim=-1) # (B, T, T)
# 소프트맥스 정규화
wei = self.dropout(wei)
# perform the weighted aggregation of the values
v = self.value(x) # (B,T,C)
out = wei @ v # (B, T, T) @ (B, T, C) -> (B, T, C)
return out
- 추가 변경 사항
- BigramLanguageModel
self.lm_head = nn.Linear(n_embd, vocab_size): 언어모델에서 생성자에 헤드 만들고 헤드 사이즈 동일하게 설정x = tok_emb + pos_emb: 토큰 임베딩과 위치 임베딩 더하기logits = self.lm_head(x): 출력 디코더 언어 모델링 헤드에 넣어서 로짓 생성idx_cond = idx[:, -block_size:]: idx가 블록 크기보다 크면 위치임베딩 테이블이 범위를 벗어나게 되므로 자름
- hyperparameters
learning_rate = 1e-3: attention은 높은 걸 견딜 수 없어서 학습속도 매우 작게 줄이기max_iters = 5000: 학습률이 낮으므로 반복 많이
- BigramLanguageModel
multi head attention
여러 개의 attention 병렬로 적용하고 결과 연결
class MultiHeadAttention(nn.Module):
""" multiple heads of self-attention in parallel """
def __init__(self, num_heads, head_size):
super().__init__()
self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
self.proj = nn.Linear(n_embd, n_embd)
self.dropout = nn.Dropout(dropout)
# 헤드 수와 사이즈만 정해서 여러 개의 헤드 만들기
def forward(self, x):
out = torch.cat([h(x) for h in self.heads], dim=-1)
out = self.dropout(self.proj(out))
return out
# 목록으로 다중 헤드 병렬 실행
# dim=-1 로 모든 출력 연결
feedforward layers
Transformer 모델의 인코더 또는 디코더 블록 내부에서 사용되는 피드포워드 서브레이어
class FeedFoward(nn.Module):
""" a simple linear layer followed by a non-linearity """
def __init__(self, n_embd):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_embd, 4 * n_embd),
nn.ReLU(),
nn.Linear(4 * n_embd, n_embd),
nn.Dropout(dropout),
)
# linear layer 앞에 오고 ReLU(비선형) layer가 뒤따름
# Dropout으로 과적합 방지
def forward(self, x):
return self.net(x)
residual connection
통신과 계산 분산 통신은 multi head attention으로 수행 계산은 모든 토큰에 대해 feedforward 네트워크를 사용해 수행
class Block(nn.Module):
""" Transformer block: communication followed by computation """
def __init__(self, n_embd, n_head):
# n_embd: embedding dimension, n_head: the number of heads we'd like
super().__init__()
head_size = n_embd // n_head
self.sa = MultiHeadAttention(n_head, head_size)
self.ffwd = FeedFoward(n_embd)
self.ln1 = nn.LayerNorm(n_embd)
self.ln2 = nn.LayerNorm(n_embd)
def forward(self, x):
x = x + self.sa(self.ln1(x))
x = x + self.ffwd(self.ln2(x))
# 분기하고 돌아와서 연산 수행하고의 반복
return x
잔여연결
x = x + self.sa(self.ln1(x))- 입력 x를 LayerNorm으로 정규화하고 Self-Attention에 통과시켜 나온 결과에 x를 더해줌
x = x + self.ffwd(self.ln2(x))- 첫 번째 결과를 LayerNorm으로 정규화하고 FeedForward에 통과시켜 나온 결과에 기존 결과를 더해줌
layernorm
배치의 각 샘플에 대해 독립적으로 평균과 분산을 계산하여 정규화
class LayerNorm1d: # (used to be BatchNorm1d)
def __init__(self, dim, eps=1e-5, momentum=0.1):
self.eps = eps
self.gamma = torch.ones(dim)
self.beta = torch.zeros(dim)
def __call__(self, x):
# calculate the forward pass
xmean = x.mean(1, keepdim=True) # batch mean 계산
xvar = x.var(1, keepdim=True) # batch variance 계산
xhat = (x - xmean) / torch.sqrt(xvar + self.eps) # 정규화
self.out = self.gamma * xhat + self.beta
# 정규화된 xhat에 gamma 곱하고 beta 더하기
return self.out
def parameters(self):
return [self.gamma, self.beta]
torch.manual_seed(1337)
module = LayerNorm1d(100)
x = torch.randn(32, 100) # batch size 32 of 100-dimensional vectors
x = module(x)
x.shape #torch.Size([32, 100])
x[:,0].mean(), x[:,0].std() # mean,std of one feature across all batch inputs
#(tensor(0.1469), tensor(0.8803))
x[0,:].mean(), x[0,:].std() # mean,std of a single input from the batch, of its features
#(tensor(-9.5367e-09), tensor(1.0000))
self.blocks = nn.Sequential(*[Block(n_embd, n_head=n_head) for _ in range(n_layer)]): n_layer로 블록의 레이어 수 지정
전체 코드
import torch
import torch.nn as nn
from torch.nn import functional as F
# hyperparameters
batch_size = 16 # how many independent sequences will we process in parallel?
block_size = 32 # what is the maximum context length for predictions?
max_iters = 5000
eval_interval = 100
learning_rate = 1e-3
device = 'cuda' if torch.cuda.is_available() else 'cpu'
eval_iters = 200
n_embd = 64
n_head = 4
n_layer = 4
dropout = 0.0
# ------------
torch.manual_seed(1337)
# wget https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt
with open('input.txt', 'r', encoding='utf-8') as f:
text = f.read()
# here are all the unique characters that occur in this text
chars = sorted(list(set(text)))
vocab_size = len(chars)
# create a mapping from characters to integers
stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s] # encoder: take a string, output a list of integers
decode = lambda l: ''.join([itos[i] for i in l]) # decoder: take a list of integers, output a string
# Train and test splits
data = torch.tensor(encode(text), dtype=torch.long)
n = int(0.9*len(data)) # first 90% will be train, rest val
train_data = data[:n]
val_data = data[n:]
# data loading
def get_batch(split):
# generate a small batch of data of inputs x and targets y
data = train_data if split == 'train' else val_data
ix = torch.randint(len(data) - block_size, (batch_size,))
x = torch.stack([data[i:i+block_size] for i in ix])
y = torch.stack([data[i+1:i+block_size+1] for i in ix])
x, y = x.to(device), y.to(device)
return x, y
@torch.no_grad()
def estimate_loss():
out = {}
model.eval()
for split in ['train', 'val']:
losses = torch.zeros(eval_iters)
for k in range(eval_iters):
X, Y = get_batch(split)
logits, loss = model(X, Y)
losses[k] = loss.item()
out[split] = losses.mean()
model.train()
return out
class Head(nn.Module):
""" one head of self-attention """
def __init__(self, head_size):
super().__init__()
self.key = nn.Linear(n_embd, head_size, bias=False)
self.query = nn.Linear(n_embd, head_size, bias=False)
self.value = nn.Linear(n_embd, head_size, bias=False)
self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B,T,C = x.shape
k = self.key(x) # (B,T,C)
q = self.query(x) # (B,T,C)
# compute attention scores ("affinities")
wei = q @ k.transpose(-2,-1) * C**-0.5 # (B, T, C) @ (B, C, T) -> (B, T, T)
wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # (B, T, T)
wei = F.softmax(wei, dim=-1) # (B, T, T)
wei = self.dropout(wei)
# perform the weighted aggregation of the values
v = self.value(x) # (B,T,C)
out = wei @ v # (B, T, T) @ (B, T, C) -> (B, T, C)
return out
class MultiHeadAttention(nn.Module):
""" multiple heads of self-attention in parallel """
def __init__(self, num_heads, head_size):
super().__init__()
self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
self.proj = nn.Linear(n_embd, n_embd)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
out = torch.cat([h(x) for h in self.heads], dim=-1)
out = self.dropout(self.proj(out))
return out
class FeedFoward(nn.Module):
""" a simple linear layer followed by a non-linearity """
def __init__(self, n_embd):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_embd, 4 * n_embd),
nn.ReLU(),
nn.Linear(4 * n_embd, n_embd),
nn.Dropout(dropout),
)
def forward(self, x):
return self.net(x)
class Block(nn.Module):
""" Transformer block: communication followed by computation """
def __init__(self, n_embd, n_head):
# n_embd: embedding dimension, n_head: the number of heads we'd like
super().__init__()
head_size = n_embd // n_head
self.sa = MultiHeadAttention(n_head, head_size)
self.ffwd = FeedFoward(n_embd)
self.ln1 = nn.LayerNorm(n_embd)
self.ln2 = nn.LayerNorm(n_embd)
def forward(self, x):
x = x + self.sa(self.ln1(x))
x = x + self.ffwd(self.ln2(x))
return x
# super simple bigram model
class BigramLanguageModel(nn.Module):
def __init__(self):
super().__init__()
# each token directly reads off the logits for the next token from a lookup table
self.token_embedding_table = nn.Embedding(vocab_size, n_embd)
self.position_embedding_table = nn.Embedding(block_size, n_embd)
self.blocks = nn.Sequential(*[Block(n_embd, n_head=n_head) for _ in range(n_layer)])
self.ln_f = nn.LayerNorm(n_embd) # final layer norm
self.lm_head = nn.Linear(n_embd, vocab_size)
def forward(self, idx, targets=None):
B, T = idx.shape
# idx and targets are both (B,T) tensor of integers
tok_emb = self.token_embedding_table(idx) # (B,T,C)
pos_emb = self.position_embedding_table(torch.arange(T, device=device)) # (T,C)
x = tok_emb + pos_emb # (B,T,C)
x = self.blocks(x) # (B,T,C)
x = self.ln_f(x) # (B,T,C)
logits = self.lm_head(x) # (B,T,vocab_size)
if targets is None:
loss = None
else:
B, T, C = logits.shape
logits = logits.view(B*T, C)
targets = targets.view(B*T)
loss = F.cross_entropy(logits, targets)
return logits, loss
def generate(self, idx, max_new_tokens):
# idx is (B, T) array of indices in the current context
for _ in range(max_new_tokens):
# crop idx to the last block_size tokens
idx_cond = idx[:, -block_size:]
# get the predictions
logits, loss = self(idx_cond)
# focus only on the last time step
logits = logits[:, -1, :] # becomes (B, C)
# apply softmax to get probabilities
probs = F.softmax(logits, dim=-1) # (B, C)
# sample from the distribution
idx_next = torch.multinomial(probs, num_samples=1) # (B, 1)
# append sampled index to the running sequence
idx = torch.cat((idx, idx_next), dim=1) # (B, T+1)
return idx
model = BigramLanguageModel()
m = model.to(device)
# print the number of parameters in the model
print(sum(p.numel() for p in m.parameters())/1e6, 'M parameters')
# create a PyTorch optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
for iter in range(max_iters):
# every once in a while evaluate the loss on train and val sets
if iter % eval_interval == 0 or iter == max_iters - 1:
losses = estimate_loss()
print(f"step {iter}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
# sample a batch of data
xb, yb = get_batch('train')
# evaluate the loss
logits, loss = model(xb, yb)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
# generate from the model
context = torch.zeros((1, 1), dtype=torch.long, device=device)
print(decode(m.generate(context, max_new_tokens=2000)[0].tolist()))