심화세션 - NLP.week2
Sep 24, 2024
»
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')
- 색깔 진할수록 빈도 높음
샘플링
- 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 필요
- 서로 다른 크기의 텐서 간의 연산을 가능하게 하는 기능
- each tensor has at least 1 dimension
- 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)
- 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) 
-
- softmax
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. """