아래 그림은 업로드된 강의 PDF의 주요 페이지를 그대로 넣은 것입니다.
아래 코드는 업로드된 BiLSTM 실습(초급) 노트북에서 핵심 부분만 뽑아 설명합니다.
영어 문장을 정규식으로 단어만 골라서 리스트로 만들어요.
# [간단 토크나이저 정의(정규식): 영문 단어 분할
# 영어 소문자/숫자 단어만 추출, 나머지는 공백 처리
token_pattern = re.compile(r"[a-z0-9']+")
def simple_tokenize(text: str):
# (한국어 주석) 소문자 변환 후 정규식 매칭
# list형태로 변환
return token_pattern.findall(text.lower())
자주 나오는 단어부터 번호를 줍니다. 너무 희귀한 단어는 <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"]))
문장 → [단어들] → [번호들] → 길이 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"]))
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())
Embedding은 단어 번호를 “의미 벡터”로 바꿉니다. BiLSTM은 앞/뒤 문맥을 같이 보고, 마지막에 Linear가 긍/부정을 맞혀요.
# 모델 정의: 임베딩 + 양방향 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)
model.train() : 드롭아웃/배치정규화가 “학습 모드”로loss.backward() : 오차를 기준으로 기울기 계산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()
BiLSTM은 우리가 전처리/모델/학습을 많이 직접 만들었죠.
DistilBERT는 이미 큰 데이터로 미리 공부한 모델을 가져와서, 우리 라벨(IMDb 긍/부정)에 맞게 조금만 조정합니다.
# 준비: 라이브러리 & 시드
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)
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)
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)
딥러닝 전에 많이 쓰던 “단어 점수” 방법이 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()
| 비교 | BiLSTM | DistilBERT |
|---|---|---|
| 장점 | 구조 이해 쉬움, 직접 구현 학습에 좋음 | 성능 좋고, 적은 데이터에서도 잘 됨(전이학습) |
| 단점 | 전처리/모델/학습을 많이 직접 해야 함 | 메모리/자원 더 필요, 내부가 “블랙박스”처럼 느껴질 수 있음 |
| 추천 상황 | 기본기/흐름 학습, 작은 실습 | 실제 성능 목표, 빠르게 좋은 결과 필요 |
[batch, seq_len] (토큰 id)[batch]padding_idx=0 쓰면 안정적model.train() / model.eval()attention_mask가 패딩을 “무시”하도록 도와줌