Lecture17: NLP

작성일: 2026-01-30 · 자료: 업로드된 강의 PDF + 실습 노트북

0) 오늘 수업이 “왜 어려웠는지”부터 정리

한 줄 요약: 글(문장)은 컴퓨터가 바로 못 알아듣기 때문에, 숫자로 바꾼 뒤 모델에 넣어야 해요.

강의 슬라이드 미리보기(요점)

아래 그림은 업로드된 강의 PDF의 주요 페이지를 그대로 넣은 것입니다.

강의 표지
슬라이드 더 보기 (1~12페이지)
p.2
slide 2
p.3
slide 3
p.4
slide 4
p.5
slide 5
p.6
slide 6
p.7
slide 7
p.8
slide 8
p.9
slide 9
p.10
slide 10
p.11
slide 11
p.12
slide 12
p.13
slide 13

2) BiLSTM 실습 코드: “한 줄씩 왜 필요한지”

아래 코드는 업로드된 BiLSTM 실습(초급) 노트북에서 핵심 부분만 뽑아 설명합니다.

2-1) 토큰화(단어 쪼개기)

영어 문장을 정규식으로 단어만 골라서 리스트로 만들어요.

# [간단 토크나이저 정의(정규식): 영문 단어 분할
# 영어 소문자/숫자 단어만 추출, 나머지는 공백 처리
token_pattern = re.compile(r"[a-z0-9']+")
def simple_tokenize(text: str):
    # (한국어 주석) 소문자 변환 후 정규식 매칭
    # list형태로 변환
    return token_pattern.findall(text.lower())

2-2) 단어사전(vocab) 만들기

자주 나오는 단어부터 번호를 줍니다. 너무 희귀한 단어는 <unk>로 처리해요.

# 어휘사전(Vocab) 구축
# 상위 N개 토큰만 사용하여 희소 단어는 <unk> 처리
from collections import Counter
MAX_VOCAB = 30000
PAD, UNK = "<pad>", "<unk>"

counter = Counter()
for ex in raw["train"]:
    counter.update(simple_tokenize(ex["text"]))

2-3) 인코딩 + 패딩

문장 → [단어들] → [번호들] → 길이 MAX_LEN에 맞춰 자르거나(Truncate) 0을 채웁니다(Pad).

# 어휘사전(Vocab) 구축
# 상위 N개 토큰만 사용하여 희소 단어는 <unk> 처리
from collections import Counter
MAX_VOCAB = 30000
PAD, UNK = "<pad>", "<unk>"

counter = Counter()
for ex in raw["train"]:
    counter.update(simple_tokenize(ex["text"]))

2-4) Dataset (DataLoader가 읽기 쉬운 형태로 포장)

PyTorch는 __len__, __getitem__이 있는 Dataset을 좋아해요. DataLoader가 자동으로 배치를 만들어 줍니다.

# PyTorch Dataset 래핑
class IMDBTensor(torch.utils.data.Dataset):
    def __init__(self, hf_split):
        self.data = hf_split
    def __len__(self): return len(self.data)
    def __getitem__(self, idx):
        text = self.data[idx]["text"]
        label = self.data[idx]["label"]
        x = torch.tensor(encode(text), dtype=torch.long)
        y = torch.tensor(encode_label(label), dtype=torch.long)
        return x, y

train_ds = IMDBTensor(raw["train"])
test_ds  = IMDBTensor(raw["test"])

train_loader = DataLoader(train_ds, batch_size=64, shuffle=True,  num_workers=2, pin_memory=torch.cuda.is_available())
test_loader  = DataLoader(test_ds,  batch_size=128, shuffle=False, num_workers=2, pin_memory=torch.cuda.is_available())

2-5) 모델: Embedding → BiLSTM → Linear

Embedding은 단어 번호를 “의미 벡터”로 바꿉니다. BiLSTM은 앞/뒤 문맥을 같이 보고, 마지막에 Linear가 긍/부정을 맞혀요.

BiLSTM (직접 만든 모델) Embedding 단어→벡터 BiLSTM 앞/뒤 문맥 Linear 긍/부정 DistilBERT (사전학습 Transformer) Tokenizer WordPiece + 특수토큰 DistilBERT Encoder Self-Attention Classifier Head 라벨에 맞게 미세조정
# 모델 정의: 임베딩 + 양방향 LSTM + FC
class BiLSTM(nn.Module):
    def __init__(self, vocab_size, emb=128, hidden=128, num_layers=1, num_classes=2, pad_idx=0, dropout=0.2):
        super().__init__()
        self.emb = nn.Embedding(vocab_size, emb, padding_idx=pad_idx)
        self.lstm = nn.LSTM(emb, hidden, num_layers=num_layers, batch_first=True, bidirectional=True, dropout=0.0)
        self.dropout = nn.Dropout(dropout)
        self.fc = nn.Linear(hidden*2, num_classes)  # 양방향 → 2배
        # (한국어 주석) Kaiming 초기화
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.kaiming_normal_(m.weight)
                if m.bias is not None: nn.init.zeros_(m.bias)

    def forward(self, x):
        # x: (B, T)
        e = self.emb(x)                 # (B, T, E)
        out, (h, c) = self.lstm(e)      # h: (num_layers*2, B, H)
        # (한국어 주석) 마지막 레이어의 forward/backward hidden state 결합
        last_f = h[-2]                  # (B, H)
        last_b = h[-1]                  # (B, H)
        h_cat = torch.cat([last_f, last_b], dim=1)  # (B, 2H)
        h_cat = self.dropout(h_cat)
        logits = self.fc(h_cat)         # (B, C)
        return logits

model = BiLSTM(len(itos), emb=128, hidden=128, num_layers=1, pad_idx=PAD_IDX).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=1e-3)

2-6) 학습 루프(진짜 자주 틀리는 부분)

  1. model.train() : 드롭아웃/배치정규화가 “학습 모드”로
  2. loss.backward() : 오차를 기준으로 기울기 계산
  3. optimizer.step() : 가중치 업데이트
# 학습/평가 루프
def train_one_epoch(epoch):
    model.train()
    total_loss = total_correct = total = 0
    for X, y in train_loader:
        X, y = X.to(device), y.to(device)
        optimizer.zero_grad()
        logits = model(X)
        loss = criterion(logits, y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * y.size(0)
        total_correct += (logits.argmax(1) == y).sum().item()
        total += y.size(0)
    print(f"[Train] Epoch {epoch} | loss={total_loss/total:.4f} | acc={total_correct/total:.4f}")

@torch.no_grad()
def evaluate():
    model.eval()
    total_loss = total_correct = total = 0
    for X, y in test_loader:
        X, y = X.to(device), y.to(device)
        logits = model(X)
        loss = criterion(logits, y)
        total_loss += loss.item() * y.size(0)
        total_correct += (logits.argmax(1) == y).sum().item()
        total += y.size(0)
    print(f"[Test ] loss={total_loss/total:.4f} | acc={total_correct/total:.4f}")

EPOCHS = 3  # 2~3 에폭 권장
for ep in range(1, EPOCHS+1):
    train_one_epoch(ep)
    evaluate()

3) DistilBERT 파인튜닝: “코드가 갑자기 쉬워지는 이유”

BiLSTM은 우리가 전처리/모델/학습을 많이 직접 만들었죠.
DistilBERT는 이미 큰 데이터로 미리 공부한 모델을 가져와서, 우리 라벨(IMDb 긍/부정)에 맞게 조금만 조정합니다.

3-1) 토크나이저 + 모델 불러오기

# 준비: 라이브러리 & 시드
import random, numpy as np, torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPadding
from transformers import TrainingArguments, Trainer
import evaluate

SEED = 2025
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
if torch.cuda.is_available(): torch.cuda.manual_seed_all(SEED)

3-2) 전처리(토큰화) 함수

Transformer는 보통 input_ids, attention_mask 같은 입력을 만듭니다(토크나이저가 자동 생성).

# 전처리 함수: 토큰화
MAX_LEN = 256
def preprocess(batch):
    return tokenizer(batch["text"], truncation=True, max_length=MAX_LEN)

encoded = dataset.map(preprocess, batched=True, remove_columns=["text"])
print(encoded)

3-3) Trainer로 학습

Trainer는 학습 루프를 “자동화”해줍니다. 그래서 코드가 확 줄어들어요.

# 준비: 라이브러리 & 시드
import random, numpy as np, torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPadding
from transformers import TrainingArguments, Trainer
import evaluate

SEED = 2025
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
if torch.cuda.is_available(): torch.cuda.manual_seed_all(SEED)

4) 보강: TF-IDF(왜 나오나?)

딥러닝 전에 많이 쓰던 “단어 점수” 방법이 TF-IDF예요.
간단히 말해, 자주 나오지만(중요) + 모든 문서에 흔하진 않은(특별) 단어에 점수를 높게 줍니다.

import pandas as pd # 데이터프레임 사용을 위해
from math import log # IDF 계산을 위해

docs = [
  '먹고 싶은 사과',
  '먹고 싶은 바나나',
  '길고 노란 바나나 바나나',
  '저는 과일이 좋아요'
]
vocab = list(set(w for doc in docs for w in doc.split()))
vocab.sort()

5) BiLSTM vs DistilBERT: 언제 뭘 쓰나?

비교BiLSTMDistilBERT
장점구조 이해 쉬움, 직접 구현 학습에 좋음성능 좋고, 적은 데이터에서도 잘 됨(전이학습)
단점전처리/모델/학습을 많이 직접 해야 함메모리/자원 더 필요, 내부가 “블랙박스”처럼 느껴질 수 있음
추천 상황기본기/흐름 학습, 작은 실습실제 성능 목표, 빠르게 좋은 결과 필요
: 오늘처럼 “전체 흐름이 안 잡히는 날”은
(1) 토큰화→정수화→패딩을 손으로 한 번 써보고 →
(2) BiLSTM으로 한 번 돌려보고 →
(3) 그 다음 DistilBERT로 “자동화된 버전”을 보는 순서가 제일 이해가 빨라요.

6) 빠른 체크리스트 (실습 에러 줄이기)