현재는 bigram이니 27개의 row만 존재하지만 2, 3개의 input 기준으로 다음 단어를 예측하게 되면 27^2 , 27^3 의 row가 생겨서 점점 W.shape이 말도 안되게 커지게 된다.
https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf
논문은 word modeling 우린 character modeling 이지만 아이디어는 똑같이 적용할 수 있다.
word를 30차원으로 embedding한다. 비슷한 의미들은 embedding space에서 비슷한 곳에 분포하게 된다.
3개의 word 를 기준으로 다음 word를 예측하는 모델이다.
C.shape는 (17000,30) 으로 17000은 총 word갯수이고 30은 우리가 embbeding 할 차원을 의미한다. 모든 단어들이 30개의 차원(특징)으로 구분되어진다.
C(wt-1).shape은 (1,30) 일것이다. 3개의 단어를 더하고 tanh 바로 전 layer는 이 총 90개와 fully connected 되어있고 softmax 전 layer는 17000개의 logit으로 되어있을 것이다. 나중에 훈련이 끝나면 이 마지막 layer를 기준으로 가장 확률 높은놈을 샘플링해서 다음 word를 추출해 낼것이다.
embedding 구현
# build the dataset
block_size = 3 # context length: how many characters do we take to predict the next one?
X, Y = [], []
for w in words:
print(w)
context = [0] * block_size
for ch in w + '.':
ix = stoi[ch]
X.append(context)
Y.append(ix)
print(''.join(itos[i] for i in context), '--->', itos[ix])
context = context[1:] + [ix] # crop and append
X = torch.tensor(X)
Y = torch.tensor(Y)
import random
random.seed(42)
random.shuffle(words)
n1 = int(0.8*len(words))
n2 = int(0.9*len(words))
Xtr, Ytr = build_dataset(words[:n1])
Xdev, Ydev = build_dataset(words[n1:n2])
Xte, Yte = build_dataset(words[n2:])
yuheng
... ---> y
..y ---> u
.yu ---> h
yuh ---> e
uhe ---> n
hen ---> g
eng ---> .
X.shape, X.dtype, Y.shape, Y.dtype
(torch.Size([228146, 3]), torch.int64, torch.Size([228146]), torch.int64)
block size = 3이니 emm -> a 처럼 3개 기반으로 다음 char를 훈련시킨다.
training : dev : test 를 80: 10 : 10 의 비율로 가져고 dev는 hyper parameter tuning , test는 굉장히 가끔 써야한다. 아니면 test 조차 overfitting 해버릴 수 있으니까
C = torch.randn((27, 2))
우리는 embedding 을 2차원으로 할것이고 굳이 one hot encoding을 거치지 않고 indexing으로 할것이다. C[5] 이러면 [0,0,0,0,0,1,0 ..] (1,27) one hot vector를 C랑 @한거니까
C[X].shape 은 (228146,3,2) 이다. 모든 X안의 char들이 2차원으로 embedding 이 된다는 것을 알 수 있다. pytorch에서 indexing은 굉장히 자유롭다.
X[13,0]
tensor(14)
C[X][13,0]
tensor([ 0.0022, -0.6903, -0.5648, -0.6775, -0.2468, 0.3441, 0.3444, 0.3433,
-0.1936, -0.4070], grad_fn=<SelectBackward0>)
C[14]
tensor([ 0.0022, -0.6903, -0.5648, -0.6775, -0.2468, 0.3441, 0.3444, 0.3433,
-0.1936, -0.4070], grad_fn=<SelectBackward0>)
C를 10차원 embedding (27,10) 일때 14란 숫자가 10차원으로 vector로 표현이 된것이다. C[X] 에서 228146 개중 13번째 는 (3,10) vector일 것이고 그중 0번째 는 (10) vector 일것이다.
이 값이 X[13,0]에있는 14라는 값을 (10) vector로 표현한것임으로 C[14] 랑 값이 같아진다. n이 10차원으로 표현되었다.
hidden layer 구현
W1 = torch.randn((6, 100))
b1 = torch.randn(100)
h = torch.tanh(emb.view(-1, 6) @ W1 + b1)
h
tensor([[-0.9593, -0.9898, 0.9987, ..., -0.6509, -0.9997, 0.9978],
[-0.9995, -0.9882, 0.7896, ..., 0.5566, -0.7900, 0.8329],
[-0.9769, 0.9975, -0.4099, ..., -0.0782, -0.9998, 0.0702],
...,
[ 0.9977, -0.4416, -0.5036, ..., -0.1040, 0.9661, -0.2288],
[ 0.9991, -0.7121, -0.6554, ..., -0.0184, 0.6605, 0.1505],
[ 0.9944, 0.2288, 0.8216, ..., -0.8122, 0.5646, -0.3119]])
2차원 embedding일때 3개의 char로 predict하니 input= 6이된다. h.shape는 (32,100)
torch.cat( [[emb:,0,:], emb[:,1:,:], emb[:,2,:]] , 1).shape
torch.Size([32,6])
torch.cat(torch.unbind(emb,1),1).shape
torch.Size([32,6])
emb @ W를 바로 할 수는 없다. (32,3,2) @ (6,100) 을 할 수 없으니까 그래서 3개의 vector를 concatenate 해야한다.
첫번째는 manual 하게 한것이고 이는 다형성이 없다.
unbind를 통해 1차원을 Returns a tuple of all slices along a given dimension, already without it. 해줌으로 첫번쨰와 동일한결과를 얻게 된다.
하지만 view 함수를 통해 emb(-1,6)으로 동일한 결과를 얻을 수 있고 가장 efficient 하다.
또한 W = (32,100) +b (100) 이니 broadcasting 이 일어난다.
32,100
1, 100
vertically copy 가 일어나서 (32,100) 되어 element wise adding을 하게 된다. 모든 row에 같은 b를 더하길 원하는것이니 원하는 동작이다. 32개의 Wx + b를 원하는거니까
W2 = torch.randn((100, 27))
b2 = torch.randn(27)
softmax이전에 logit을 계산하는 layer는 output size = 27 이다. char(a~z) 이니까
g = torch.Generator().manual_seed(2147483647) # for reproducibility
C = torch.randn((27, 10), generator=g)
W1 = torch.randn((30, 200), generator=g)
b1 = torch.randn(200, generator=g)
W2 = torch.randn((200, 27), generator=g)
b2 = torch.randn(27, generator=g)
parameters = [C, W1, b1, W2, b2]
emb = C[X]
h = torch.tanh(emb.view(-1,6) @ W1 + b1) # (32,100)
logits = h @ W2 + b2 # (32,27)
#counts = logits.exp()
#prob = counts/ counts.sum(1, keepdims=True)
#loss = -prob[torch.arange(32),Y].log().mean()
F.cross_entropy(logits,Y)
마지막 3줄은 cross_entropy 함수의 구현체이지만 항상 이걸 사용하는 것이 좋다. 왜냐하면
1. 3줄에 적힌 operator들의 graident 들을 실제로 만들지도 계산하지도 않는다.
예를들면
def tanh(self):
x = self.data
t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)
out = Value(t, (self, ), 'tanh')
def _backward():
self.grad += (1 - t**2) * out.grad
out._backward = _backward
return out
tanh 에서도 gradient를 구할때 간단히 1-t제곱에 관해서만 backprop이 일어나지 2*x -1 나누기 등등을 하지 않는다.
logits = torch.tensor([-100,-3,0,100])
counts = logits.exp()
probs = counts / counts.sum()
probs
tensor([0., 0., 0., nan])
2. counts = [~ ,~, ~, inf] 가 나오게된다. e^100 이여서 float의 범위를 넘은것이다.
pytorch가 내부적으로 logits 중 최대값을 0으로 만들어버린다. 어차피 counts.sum()으로 나누니까 probs는 동일하지만 float의 범위를 넘을 일이 생기지 않는다.
batch
for p in parameters:
p.requires_grad = True
lre = torch.linspace(-3, 0, 1000)
lrs = 10**lre
lri = []
lossi = []
stepi = []
for i in range(200000):
# minibatch construct
ix = torch.randint(0, Xtr.shape[0], (32,))
# forward pass
emb = C[Xtr[ix]] # (32, 3, 10)
h = torch.tanh(emb.view(-1, 30) @ W1 + b1) # (32, 200)
logits = h @ W2 + b2 # (32, 27)
loss = F.cross_entropy(logits, Ytr[ix])
#print(loss.item())
# backward pass
for p in parameters:
p.grad = None
loss.backward()
# update
#lr = lrs[i]
lr = 0.1 if i < 100000 else 0.01
for p in parameters:
p.data += -lr * p.grad
# track stats
#lri.append(lre[i])
stepi.append(i)
lossi.append(loss.log10().item())
#print(loss.item())
Xtr은 228146 개 임으로 이중에 32개만 minibatch로 고르는것이다. 훨씬 빠르게 iteration을 진행할 수 있다.
actual gradient direction이랑 다르지만 approximate gradient direction 도 충분하고 더 많은 iteration을 돌리는것 좋다.
learning rate 또한 training 이 진행될수록 점점 감소하게 한다.
plt.plot(stepi, lossi)
lossi 에 log 를 해준이유는 hockey stick 을 방지하기위해서이다. log를 씌우면 squashing 을 해주기 떄문이다.
# visualize dimensions 0 and 1 of the embedding matrix C for all characters
plt.figure(figsize=(8,8))
plt.scatter(C[:,0].data, C[:,1].data, s=200)
for i in range(C.shape[0]):
plt.text(C[i,0].item(), C[i,1].item(), itos[i], ha="center", va="center", color='white')
plt.grid('minor')
vowel 끼리 모여있는것을 확인할 수 있다. 이 그림 embedding을 2차원으로 했을떄의 예시이다.
위에서 진행했던 것은 10차원이니 plot으로 보여줄 수 없다.
# sample from the model
g = torch.Generator().manual_seed(2147483647 + 10)
for _ in range(20):
out = []
context = [0] * block_size # initialize with all ...
while True:
emb = C[torch.tensor([context])] # (1,block_size,d)
h = torch.tanh(emb.view(1, -1) @ W1 + b1)
logits = h @ W2 + b2
probs = F.softmax(logits, dim=1)
ix = torch.multinomial(probs, num_samples=1, generator=g).item()
context = context[1:] + [ix]
out.append(ix)
if ix == 0:
break
print(''.join(itos[i] for i in out))
carmahzati.
hariffinleigelty.
halani.
emmahnen.
net에서 sampling으로 이름을 만들어봄 , 도잉ㄹ하게 loss 전까지 하지만 실제로 backprop을 안하기에 net을 그대로 유지하고 sampling 을 하는 것이다.
. 으로 시작해서 .을 만나면 한 이름이 생성된다.
'AI > Andrej Karpathy' 카테고리의 다른 글
Building makemore Part 5: Building a WaveNet (0) | 2023.08.15 |
---|---|
Building makemore Part 4: Becoming a Backprop Ninja (0) | 2023.02.26 |
Building makemore Part 3: Activations & Gradients, BatchNorm (0) | 2023.02.19 |
The spelled-out intro to language modeling: building makemore (0) | 2023.01.24 |
The spelled-out intro to neural networks and backpropagation: building micrograd (0) | 2023.01.23 |