긴 글을 나누어 다음 문자 예측하기

본인 사본을 만들어 실행·수정·기록하세요.

1. 긴 글에서 짧은 학습 예제 만들기

hello!를 h → e → l → l → o → !처럼 순서대로 읽어 보세요. 이렇게 순서가 있는 나열이 Sequence입니다. 문자 하나가 한 시점이며 공백도 한 문자입니다.

원문에서 일정한 길이로 잘라 보는 구간을 Window라고 합니다. 길이 3으로 hel을 읽을 때 각 위치의 다음 문자 정답은 ell입니다. h 다음은 e, he 다음은 l, hel 다음은 l입니다. 정답은 마지막 하나만이 아니라 각 위치마다 있습니다.

예제 원문 시작 위치 Input Target
1 0 hel ell
2 2 llo lo!

Window Size=3은 입력 길이, Stride=2는 다음 예제의 시작점을 옮기는 간격입니다. Target은 언제나 Input보다 한 글자 뒤입니다. Stride가 2여도 두 글자 뒤를 예측하지 않습니다. 예제 1과 2는 원문 위치 2를 함께 포함합니다.

두 예제를 행으로 쌓으면 Batch가 됩니다. 행 수 N=2, 행 안의 문자 수 S=3이며, 문자마다 길이 E의 One-hot 벡터를 넣으면 Input shape는 (2,3,E)입니다. 한 행 안에서 State는 다음 문자로 전달되지만, 다른 행으로 넘기지는 않습니다.

A-1. 짧은 문자열로 자르는 규칙 확인하기

# 한 예제의 입력 길이와 다음 예제로 이동하는 간격을 구별하세요.
demo_text = "hello!"
demo_size, demo_stride = 3, 2
for start in range(0, len(demo_text) - demo_size, demo_stride):
    inp = demo_text[start:start + demo_size]
    target = demo_text[start + 1:start + demo_size + 1]
    print(f"시작 위치 {start}: Input={inp!r}, Target={target!r}")

A-2. 긴 글에 같은 규칙 적용하기

이제 입력 길이를 20, 이동 간격을 5로 정합니다. 시작 위치는 0·5·10·…이며, 각 Target은 Input보다 한 글자 뒤에서 시작합니다. 마지막 정답까지 있으려면 입력 길이보다 한 글자 더 필요합니다. 만든 예제를 모두 한 Batch로 묶으며, 실제 N·S·E는 출력으로 확인하세요.

import sys, numpy as np, matplotlib.pyplot as plt
import torch
from torch import nn
import torch.nn.functional as F
torch.manual_seed(42)
torch.set_num_threads(2)
print('Python:', sys.version.split()[0], '| PyTorch:', torch.__version__)
sentences=["if you want to build a ship, don't drum up people together to", "collect wood and don't assign them tasks and work, but rather", "teach them to long for the endless immensity of the sea."]
sentence=' '.join(sentences);chars=sorted(set(sentence));vocab={c:i for i,c in enumerate(chars)}
window_size = 20
window_stride = 5
starts = range(0, len(sentence) - window_size, window_stride)
ids = torch.tensor([[vocab[c] for c in sentence[i:i+window_size]] for i in starts])
Y = torch.tensor([[vocab[c] for c in sentence[i+1:i+window_size+1]] for i in starts])
X = F.one_hot(ids, num_classes=len(chars)).float()
print('X:', X.shape, 'Y:', Y.shape)
for i in range(2):
    print('input:', ''.join(chars[j] for j in ids[i]))
    print('target:', ''.join(chars[j] for j in Y[i]))
assert X.shape[:2] == Y.shape

B. 직접 수행

  1. 첫 예제 셀에서 hello!를 길이 3·Stride 2로 자른 두 Input·Target 쌍을 출력하세요. 각 쌍의 Input과 Target을 위아래 두 행으로 놓고 같은 열에서 한 글자 차이가 나는지 대조하세요.
  2. 긴 글을 처리하는 데이터 셀의 첫 두 Input·Target 쌍에도 같은 표시를 하세요. 두 예제의 시작 위치가 몇 글자씩 이동하는지 적으세요.
  3. X.shape와 Y.shape의 각 축에 Window 수 NN, 길이 SS, 문자 표현 크기 EE를 대응시키세요.

예상은 정확한 수치 대신 shape나 증가·감소 방향으로 적어도 됩니다. 설명은 아래 Text Cell에 기록하세요.

# ✍ 직접 작성: 이 셀 아래에 본인의 코드를 추가하세요.
# 코드가 길어지면 Code Cell을 추가해도 됩니다.

C. 직접 기록 · 이 Text Cell을 더블클릭해서 수정

2. RNN·LSTM·GRU 비교

같은 Window 데이터와 학습 조건으로 RNN·LSTM·GRU를 비교합니다. 각 모델은 Hidden Size=32이며, 모든 시점의 output을 Linear에 넣어 문자 점수를 만듭니다. LSTM만 Hidden State 외에 Cell State를 반환합니다. 학습 결과는 results에 저장되므로 표와 State 확인을 위해 다시 학습할 필요는 없습니다.

A. 기준 예제 · 먼저 읽고 실행

class CharModel(nn.Module):
    def __init__(self,kind,vocab_size,hidden=32):
        super().__init__();self.rnn=getattr(nn,kind)(vocab_size,hidden,batch_first=True);self.fc=nn.Linear(hidden,vocab_size)
    def forward(self,x):
        output,state=self.rnn(x);return self.fc(output)
def fit_char(kind,X,Y,epochs=300,lr=.01):
    torch.manual_seed(42);m=CharModel(kind,X.shape[-1]);opt=torch.optim.Adam(m.parameters(),lr=lr);losses=[]
    for epoch in range(epochs):
        logits=m(X);loss=F.cross_entropy(logits.reshape(-1,X.shape[-1]),Y.reshape(-1))
        opt.zero_grad();loss.backward();opt.step();losses.append(loss.item())
        if epoch%100==0:print(kind,epoch,loss.item())
    with torch.no_grad():pred=m(X).argmax(-1)
    return m,losses,pred
results={}
for kind in ['RNN','LSTM','GRU']:
    m,h,p=fit_char(kind,X,Y,epochs=300,lr=.01);results[kind]=(m,h,p)
    print(kind,'accuracy:',(p==Y).float().mean().item());print(''.join(chars[i] for i in p[0]))
    plt.plot(h,label=kind)
plt.legend();plt.xlabel('Epoch');plt.ylabel('Loss');plt.grid();plt.show()
with torch.no_grad():output,(h_n,c_n)=results['LSTM'][0].rnn(X)
print('LSTM output:',output.shape,'h_n:',h_n.shape,'c_n:',c_n.shape)
for kind in ['RNN', 'GRU']:
    with torch.no_grad(): output, h_n = results[kind][0].rnn(X)
    print(kind, 'output:', output.shape, 'h_n:', h_n.shape)

B. 직접 수행

  1. 모델 학습 셀을 실행하세요. RNN·LSTM·GRU별 마지막 Loss·Training Accuracy·첫 Window의 예측 문자열을 한 표로 적으세요. 사용한 데이터·epoch·Learning Rate는 세 모델에서 동일하게 둡니다.
  2. State 출력 셀을 실행하고, 모델별 output·h_n·c_n의 shape를 표로 적으세요. 반환하지 않는 State는 ‘없음’으로 표시하세요.
  3. output의 Sequence 축과 h_n의 마지막 시점 표현을 구분해 설명하고, LSTM이 추가로 반환하는 c_n의 역할을 적으세요.

예상은 정확한 수치 대신 shape나 증가·감소 방향으로 적어도 됩니다. 설명은 아래 Text Cell에 기록하세요.

# ✍ 직접 작성: 이 셀 아래에 본인의 코드를 추가하세요.
# 코드가 길어지면 Code Cell을 추가해도 됩니다.

C. 직접 기록 · 이 Text Cell을 더블클릭해서 수정

반복 숙달 · 실행 전에 판단하기

앞서 비교한 RNN·LSTM·GRU의 결과를 이 표에 정리하세요. 같은 모델을 세 번 더 학습할 필요는 없습니다. 같은 데이터·epoch·Learning Rate를 사용했는지 확인합니다.

변경 조건 예상 shape/수치/결과 실제 결과 오류가 있었다면 원인
⟦직접 입력 1⟧ ⟦직접 입력⟧ ⟦직접 입력⟧ ⟦직접 입력⟧
⟦직접 입력 2⟧ ⟦직접 입력⟧ ⟦직접 입력⟧ ⟦직접 입력⟧
⟦직접 입력 3⟧ ⟦직접 입력⟧ ⟦직접 입력⟧ ⟦직접 입력⟧
# ✍ 반복 실험 코드

선택 실습 · 오류의 원인을 찾아 코드 고치기

문자별 Logits와 Target을 펼치는 축이 맞지 않는 경우

  1. LSTM 학습까지 마친 뒤, 노트북 맨 아래에 Code Cell을 추가하고 다음 코드를 실행하세요.

    with torch.no_grad():
        debug_logits = results['LSTM'][0](X)
    debug_flat_logits = debug_logits.reshape(-1, debug_logits.shape[-1])
    print("Logits:", debug_logits.shape, "flattened:", debug_flat_logits.shape)
    for debug_target in [Y, Y.reshape(-1)]:
        print("Target:", debug_target.shape)
        try:
            print("Loss:", F.cross_entropy(debug_flat_logits, debug_target).item())
        except (RuntimeError, ValueError) as error:
            print(type(error).__name__, str(error))
  2. Text Cell에 두 Target shape와 실행 성공 여부를 표로 적으세요. 펼친 Logits의 한 행이 무엇을 예측하며, 그 행에 대응하는 Target을 어느 축 순서로 나열해야 하는지 설명하세요.

저장 전 확인

  1. 작성란과 출력·그래프를 확인하고 새 런타임에서 전체 셀을 순서대로 실행하세요.
  2. 본인 Drive에 저장한 뒤 .ipynb로 내려받으세요.

코드, 실제 Output·그래프, 직접 작성한 해석을 포함하세요. 제출은 KLAS를 이용하며 기한은 학기 일정과 최신 공지를 확인합니다.

파일명: 이름_학번_13주차.ipynb.