AI/Andrej Karpathy

Building makemore Part 2: MLP

Tony Lim 2023. 2. 4. 14:15
728x90

현재는 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 을 하는 것이다.

. 으로 시작해서 .을 만나면 한 이름이 생성된다.

728x90