심화세션 - NLP.week2

» AI

2주차

  • make more
    • character level language model
    • 모든 character는 sequences of individual characters
    • 주어진 문자들의 연속에서 다음 문자를 예측하는 모델

빅램(Bigram) 언어 모델

  • 두 문자만을 기반으로 작동
  • 이전 문자를 기반으로 다음 문자를 예측

  •   words = open('names.txt', 'r').read().splitlines()
      words[:10]
      """
      ['emma',
      'olivia',
      'ava',
      'isabella',
      'sophia',
      'charlotte',
      'mia',
      'amelia',
      'harper',
      'evelyn']
      """
    
  •   b = {} # 딕셔너리에 bigram이랑 count 저장
      for w in words:
      chs = ['<S>'] + list(w) + ['<E>'] 
      # 이름들에 <S> start , <E> end를 추가
      for ch1, ch2 in zip(chs, chs[1:]):
          bigram = (ch1, ch2) # 붙어있는 문자들끼리 pair
          b[bigram] = b.get(bigram, 0) + 1 # bigram 초기값 0, 등장할 때마다 값을 1씩 증가시켜 발생 횟수를 기록
                
          sorted(b.items(), key = lambda kv: -kv[1])
          # 딕셔너리 key value를 특정 pair가 몇 번 나타났는지를 기준으로 내림차순 정렬
    
  •   import torch # pytorch 임포트
      N = torch.zeros((27, 27), dtype=torch.int32) # 3행 5열 0벡터, type=int
      # S는 항상 처음 , E는 항상 마지막이라 28로 하면 모든 값이 0인 행과 열 생김
    
      chars = sorted(list(set(''.join(words)))) # 문자 리스트에서 중복 제거하고 남은 문자 정렬 -> a ~ z 까지 26개 알파벳 정렬
      stoi = {s:i+1 for i,s in enumerate(chars)} # string int로 변환
      stoi['.'] = 0
      itos = {i:s for s,i in stoi.items()} # 숫자랑 알파벳 순서 바꿈
    
      for w in words:
          chs = ['.'] + list(w) + ['.']
          for ch1, ch2 in zip(chs, chs[1:]):
              ix1 = stoi[ch1]
              ix2 = stoi[ch2]
              N[ix1, ix2] += 1 # 벡터 값 +1
              """
              . e
              em
              mm
              ma
              a.
              """
    
    
  •   import matplotlib.pyplot as plt # 시각화
      %matplotlib inline
    
      plt.figure(figsize=(16,16))
      plt.imshow(N, cmap='Blues')
      for i in range(27):
          for j in range(27):
              chstr = itos[i] + itos[j]
              plt.text(j, i, chstr, ha="center", va="bottom", color='gray')
              plt.text(j, i, N[i, j].item(), ha="center", va="top", color='gray')
      plt.axis('off')
    
  • alt text
    • 색깔 진할수록 빈도 높음

샘플링

  • dot을 사용하여 시작
  • 각 문자가 얼마나 자주 단어의 시작 문자로 등장하는지를 나타내는 빈도 확인

  •   N[0] # 첫 번째 행
      p = N[0].float()
      p = p / p.sum() # 확률로 변환
      p
      """tensor([0.0000, 0.1377, 0.0408, 0.0481, 0.0528, 0.0478, 0.0130, 0.0209, 0.0273,
          0.0184, 0.0756, 0.0925, 0.0491, 0.0792, 0.0358, 0.0123, 0.0161, 0.0029,
          0.0512, 0.0642, 0.0408, 0.0024, 0.0117, 0.0096, 0.0042, 0.0167, 0.0290])"""
    
  • torch.multinomial로 샘플링
    • 주어진 확률에 따라 정수를 샘플링해줌
  •   g = torch.Generator().manual_seed(2147483647) # 제네레이터 오브젝트로 결과 동일하게 나오도록 설정
      ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item() # 확률 분포 p에 따라 한 개의 샘플을 추출
      itos[ix] # 추출된 인덱스를 문자로 변환 -> 'm'
      # 두 번째 문자를 샘플링하기 위해 m으로 시작하는 행을 참조
    
      P.sum(1, keepdim=True).shape
      # P.sum(0, keepdims=True) 로 하게 되면 0차원=row를 기준으로 아래로 쭈욱 더하게 되어 [1,27] 의 tensor가 튀어나옴
      # keepdims = False로 하게 되면 0번째 차원을 유지 시켜주지 않고 "Squeeze out" 하게 되어 [27] tensor가 튀어 나온다.
      # 1차원=col 기준으로 오른쪽으로 더해서 [27,1]의 tensor가 나옴
      # torch.Size([27, 1])
    
  • Broadcasting 필요
    • 서로 다른 크기의 텐서 간의 연산을 가능하게 하는 기능
    1. each tensor has at least 1 dimension
    2. dimension size must be equalr or one of them is 1 , or one of them doesn’t exist
  •       # 27, 27
          # 27,  1
          P = (N+1).float() # 모델 스무딩
          P /= P.sum(1, keepdims=True)
          # [27,27] 을 [27,1] 로 나누기
          # 내부적으로 [27,1] 을 copy해서 [27,27] 로 만든뒤에 element wise division
    


    가능한 에러

    keepdims=False면 맨 오른쪽으로 align 시키고 비교하다보니
    27, 27
    __ 27 로 비교

          P.sum(1).shape
          # torch.Size([27])
          # broadcasting으로 27 -> [1,27]을 만듦
    

    27 , 27
    1, 27 로 자동으로 broadcasting
    -> colum으로 긴 vector를 수직으로 복사하여 27,27로 만듦
    -> row가 아닌 각 column의 probabilty distribution이 생김


결과 출력

  •   g = torch.Generator().manual_seed(2147483647)
    
      for i in range(5):
            
      out = [] # 결과를 담을 빈 리스트
      ix = 0
      while True:
          p = P[ix]
          ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()
          out.append(itos[ix]) # 문자를 리스트에 추가
          if ix == 0: # end token이면 break
          break
      print(''.join(out)) # 문자 결합해서 출력
      """
      mor.
      axx.
      minaymoryles.
      kondlaisah.
      anchshizarie."""
    

품질 평가

  • 우도(likelihood)를 사용
    • 모델의 품질을 단일 숫자로 요약하는 방법
    • 모델의 품질을 측정하는 지표
    • 확률의 곱은 매우 작은 숫자이므로 로그 우도(log likelihood) 사용
      • 각 확률의 로그를 취한 값들의 합
    • 음의 로그 우도(Negative Log Likelihood) = loss function
      • 로그 우도의 음수 값
      • 값이 낮을수록 모델이 데이터를 잘 예측하는 것, 값이 높을수록 모델이 잘못 예측하는 것
        -> 음의 로그 우도를 최소화
  •   log_likelihood = 0.0
      n = 0 #count
    
      for w in words:
      #for w in ["andrejq"]:
      chs = ['.'] + list(w) + ['.']
      for ch1, ch2 in zip(chs, chs[1:]):
          ix1 = stoi[ch1]
          ix2 = stoi[ch2]
          prob = P[ix1, ix2]
          logprob = torch.log(prob)
          log_likelihood += logprob # 로그의 합은 진수의 곱
          n += 1
          print(f'{ch1}{ch2}: {prob:.4f} {logprob:.4f}')
    
      print(f'{log_likelihood=}')
      nll = -log_likelihood # 보기 편하게 양수로 변환
      print(f'{nll=}')
      print(f'{nll/n}') # 평균 = 훈련 set loss function
    
  • 모델 스무딩(model smoothing)
    • 모든 조합에 가짜 카운트를 추가
    • 더 많이 더할수록 모델은 더 균등해지고, 덜 더할수록 모델은 특정 문자에 더 집중
    •   P = (N+1).float()
      
    • 확률 행렬에서 0이 없어짐 -> 어느 조합도 확률이 0이 되는 상황이 없어짐 = 완전히 불가능한 케이스는 없다

neural network approach

  •   xs, ys = [], [] # 인풋과 타겟
    
      for w in words[:1]:
      chs = ['.'] + list(w) + ['.']
      for ch1, ch2 in zip(chs, chs[1:]):
          ix1 = stoi[ch1]
          ix2 = stoi[ch2]
          print(ch1, ch2)
          xs.append(ix1)
          ys.append(ix2)
                
      xs = torch.tensor(xs)
      ys = torch.tensor(ys)
      # torch.Tensor와 torch.tensor가 있음
      # torch.tensor는 dytpe을 바로 캐치해서 새로운 tensor를 생성함
      """
      . e
      e m
      m m
      m a
      a .
      """
      xs # tensor([ 0,  5, 13, 13,  1])
      ys # tensor([ 5, 13, 13,  1,  0])
    


    one hot encoding

    해당 인덱스만 1이고 나머지 모두 0
    5
    [0 0 0 0 1 0 0 0 …]


  •   import torch.nn.functional as F
      xenc = F.one_hot(xs, num_classes=27).float() # 세세한 weight value조정을 원하기 때문에 float로 변환
      xenc
      """
      tensor([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0.],
          [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0.]])
      """
      xenc.shape # torch.Size([5, 27])
    
      plt.imshow(xenc)
    

    alt text

    • one hot encoding
    • 노란색 1, 나머지 0
  •   W = torch.randn((27, 27), generator=g) # 정규 분포에 따라 랜덤하게 값 생성
      xenc @ W # @: 행렬 곱셈
      # [5,27] @ [27,1] = [5,27]
    
      logits = xenc @ W # log-counts
      counts = logits.exp() # equivalent N 
      # e^logits로 변환
      probs = counts / counts.sum(1, keepdims=True) # 1로 정규화된 확률 분포로 변환
      probs # 다음 character가 뭐가 올지에 대한 확률표
      """
      tensor([[0.0205, 0.0023, 0.0097, 0.0428, 0.0228, 0.0042, 0.0101, 0.0568, 0.0500,
           0.0253, 0.0054, 0.0460, 0.0075, 0.1609, 0.0068, 0.0243, 0.0029, 0.0237,
           0.0247, 0.0174, 0.0423, 0.0248, 0.0799, 0.1822, 0.0412, 0.0522, 0.0132],
          [0.0154, 0.0397, 0.0928, 0.0160, 0.0110, 0.0068, 0.0152, 0.0278, 0.0154,
           0.0860, 0.0127, 0.0456, 0.0137, 0.0177, 0.1286, 0.0055, 0.0016, 0.0354,
           0.0357, 0.1141, 0.0197, 0.0209, 0.0065, 0.0369, 0.0844, 0.0535, 0.0414],
          [0.0212, 0.0213, 0.0439, 0.0287, 0.0586, 0.0994, 0.0104, 0.0332, 0.0301,
           0.0158, 0.0192, 0.0120, 0.0047, 0.0458, 0.0048, 0.0123, 0.0029, 0.0733,
           0.0095, 0.0379, 0.0216, 0.0299, 0.0705, 0.1450, 0.0950, 0.0314, 0.0215],
          [0.0212, 0.0213, 0.0439, 0.0287, 0.0586, 0.0994, 0.0104, 0.0332, 0.0301,
           0.0158, 0.0192, 0.0120, 0.0047, 0.0458, 0.0048, 0.0123, 0.0029, 0.0733,
           0.0095, 0.0379, 0.0216, 0.0299, 0.0705, 0.1450, 0.0950, 0.0314, 0.0215],
          [0.0289, 0.0077, 0.0613, 0.0126, 0.0153, 0.0124, 0.0459, 0.0036, 0.1801,
           0.0839, 0.0042, 0.0869, 0.0119, 0.0161, 0.0522, 0.0106, 0.0422, 0.0140,
           0.0131, 0.0274, 0.0692, 0.0199, 0.0167, 0.0139, 0.0275, 0.1140, 0.0084]])
      """
      probs[0].sum() # tensor(1.)
    
    • softmax
      •   counts = logits.exp()
          probs = counts / counts.sum(1, keepdims=True)
        
      • alt text

Optimization

  •   g = torch.Generator().manual_seed(2147483647)
      W = torch.randn((27, 27), generator=g, requires_grad=True)
      # 기울기 계산
      # 역전파를 통해 W의 값을 학습
    
      # forward pass
      xenc = F.one_hot(xs, num_classes=27).float()
      logits = xenc @ W
      counts = logits.exp() 
      probs = counts / counts.sum(1, keepdims=True) 
      loss = -probs[torch.arange(5), ys].log().mean()
      # 각 예측된 확률 중에서 실제 정답에 해당하는 확률을 선택, 그 값을 로그로 변환해 음의 로그 우도 계산
      # 이 값들을 평균내어 손실 함수로 사용
    
      print(loss.item()) # 3.6891887187957764
    
      # backward pass
      W.grad = None # 기울기 초기화 ()= 0)
      loss.backward() # 역전파를 통해 기울기를 계산
    
      W.data += -0.1 * W.grad
      # 기울기를 사용해 가중치 업데이트(-0.1은 학습률)
    
  •   xs, ys = [], []
      for w in words:
      chs = ['.'] + list(w) + ['.']
      for ch1, ch2 in zip(chs, chs[1:]):
          ix1 = stoi[ch1]
          ix2 = stoi[ch2]
          xs.append(ix1)
          ys.append(ix2)
      xs = torch.tensor(xs)
      ys = torch.tensor(ys)
      num = xs.nelement()
      print('number of examples: ', num)
    
      # initialize the 'network'
      g = torch.Generator().manual_seed(2147483647)
      W = torch.randn((27, 27), generator=g, requires_grad=True)
      # number of examples:  228146
    
      # gradient descent
      for k in range(1):
        
      # forward pass
      xenc = F.one_hot(xs, num_classes=27).float() 
      logits = xenc @ W 
      counts = logits.exp() 
      probs = counts / counts.sum(1, keepdims=True) 
      loss = -probs[torch.arange(num), ys].log().mean() + 0.01*(W**2).mean() # 0이 되지 않도록 정규화
      print(loss.item())
        
      # backward pass
      W.grad = None 
      loss.backward()
        
      # update
      W.data += -50 * W.grad
    

최종

  •   g = torch.Generator().manual_seed(2147483647)
    
      for i in range(5):
        
      out = []
      ix = 0
      while True:
            
          # ----------
          # BEFORE:
          #p = P[ix]
          # ----------
          # NOW:
          xenc = F.one_hot(torch.tensor([ix]), num_classes=27).float()
          logits = xenc @ W 
          counts = logits.exp() 
          p = counts / counts.sum(1, keepdims=True) 
          # ----------
            
          ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()
          out.append(itos[ix])
          if ix == 0:
          break
      print(''.join(out))
      """
      mor.
      axx.
      minaymoryles.
      kondlaisah.
      anchthizarie.
      """