최근 생성형 AI와 대형 언어 모델(LLM)이 급격하게 발전하면서 자연어 처리(NLP) 기술에 대한 관심이 그 어느 때보다 뜨겁습니다. 하지만 화려한 LLM 아키텍처를 이해하고 자유롭게 활용하기 위해서는 결국 가장 기본이 되는 '텍스트 분류(Text Classification)' 메커니즘을 정확히 파악해야 합니다. 텍스트 분류는 스팸 메일 차단, 고객 문의 자동 분류, 영화 리뷰나 SNS 글의 감정 분석 등 이미 우리 일상 속 수많은 소프트웨어에 깊숙이 자리 잡고 있는 핵심 기술입니다.
본 포스팅에서는 파이토치(PyTorch)를 활용해 가장 대표적인 NLP 입문 데이터셋인 IMDb 영화 리뷰 데이터를 다루어 봅니다. 텍스트 전처리부터 데이터로더 구성, 그리고 순환 신경망(LSTM)을 이용한 감정 분석 모델의 설계와 학습, 평가까지 한 단계씩 차근차근 살펴보겠습니다. NLP의 기본기를 탄탄하게 다지고 싶은 개발자분들에게 좋은 출발점이 되기를 바랍니다.

📌 핵심 요약 3줄
- 텍스트 분류의 본질: 자연어 데이터를 토큰화 및 정수 인덱싱 과정을 거쳐 컴퓨터가 이해할 수 있는 텐서 형태로 변환하는 것이 첫걸음입니다.
- 효율적인 아키텍처: 본 실습에서는 문맥의 흐름과 장기 의존성을 효과적으로 학습할 수 있는 순환 신경망 구조인 **LSTM(Long Short-Term Memory)**을 사용합니다.
- 성공적인 모델링의 핵심: 자연어 처리는 모델의 깊이만큼이나 패딩(Padding), 배치 처리, 어휘 사전(Vocabulary) 관리 등 데이터 전처리 파이프라인 구축이 성능을 좌우합니다.
1. 텍스트 분류(Text Classification) 개요
텍스트 분류는 자연어로 된 문장이나 문서를 입력받아 미리 정의된 특정 범주(Class)로 분류하는 작업입니다. 전체적인 작업 흐름은 아래 표와 같이 요약할 수 있습니다.
| 단계 | 주요 작업 내용 | 핵심 목표 및 역할 |
| 1. 데이터 준비 & 전처리 | 텍스트 토큰화, 어휘 사전(Vocab) 구축 | 텍스트 문장을 최소 의미 단위(토큰)로 쪼개고 고유 번호 부여 |
| 2. 데이터로더 구성 | 미니배치 생성, 문장 길이 맞추기(Padding) | 가변적인 문장 길이를 통일하여 GPU 연산 효율 극대화 |
| 3. 모델 설계 및 임베딩 | Embedding 레이어 및 LSTM 레이어 정의 | 단어의 의미를 고차원 밀집 벡터로 변환하고 문맥 정보 추출 |
| 4. 학습 및 평가 | 손실 함수 계산, 역전파, 정확도 측정 | 손실을 최소화하도록 가중치를 갱신하고 테스트 데이터로 검증 |
이번 예제에서는 IMDb 영화 리뷰 데이터를 사용하여 리뷰 본문이 긍정(Positive)인지 부정(Negative)인지 이진 분류(Binary Classification)하는 모델을 구축해 보겠습니다.
2. 데이터 준비 및 환경 설정
가장 먼저 필요한 라이브러리를 불러오고 IMDb 데이터셋을 로드합니다. 영문 텍스트를 단어 단위로 쪼개기 위해 기본적인 영어 토크나이저도 함께 설정합니다.
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.datasets import IMDB
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
from torch.utils.data import DataLoader
# 기본 영어 토크나이저 설정
tokenizer = get_tokenizer("basic_english")
# IMDb 데이터셋 로드 (반복자 객체 반환)
train_iter, test_iter = IMDB(split=("train", "test"))
3. 텍스트 전처리 및 어휘 사전(Vocabulary) 구축
컴퓨터는 텍스트를 있는 그대로 이해할 수 없습니다. 따라서 단어들을 고유한 정수 인덱스로 매핑해주는 '어휘 사전'을 만들어야 합니다. 사전에 없는 낯선 단어 처리를 위해 <unk> 토큰을 특수 토큰으로 지정합니다.
# 토큰 생성용 제너레이터 함수 정의
def yield_tokens(data_iter):
for _, text in data_iter:
yield tokenizer(text)
# 학습 데이터를 순회하며 어휘 사전 구축
vocab = build_vocab_from_iterator(yield_tokens(train_iter), specials=["<unk>"])
# 사전에 없는 단어(OOV)는 기본적으로 <unk> 인덱스로 처리하도록 설정
vocab.set_default_index(vocab["<unk>"])
# 텍스트 및 레이블 전처리 파이프라인 함수
def text_pipeline(text):
return vocab(tokenizer(text))
def label_pipeline(label):
# IMDb 데이터셋의 레이블은 원본 데이터에 따라 다를 수 있으므로 매핑 확인 필요
# 보통 'pos' 또는 2는 1(긍정)로, 'neg' 또는 1은 0(부정)으로 변환합니다.
return 1 if label == "pos" or label == 2 else 0
4. 효율적인 미니배치 구성을 위한 데이터로더(DataLoader)
리뷰마다 문장의 길이가 제각각 다르기 때문에, 하나의 배치를 묶으려면 가장 긴 문장을 기준으로 길이를 맞춰주는 패딩(pad_sequence) 작업이 필수적입니다.
def collate_batch(batch):
text_list, label_list = [], []
for label, text in batch:
text_list.append(torch.tensor(text_pipeline(text), dtype=torch.int64))
label_list.append(torch.tensor(label_pipeline(label), dtype=torch.int64))
# 가변 길이의 텐서들을 가장 긴 문장 길이에 맞춰 0(또는 패딩 토큰 인덱스)으로 채움
padded_text = torch.nn.utils.rnn.pad_sequence(text_list, batch_first=True, padding_value=0)
return padded_text, torch.tensor(label_list)
batch_size = 16
# 주의: train_iter가 위에서 소모되었을 수 있으므로 다시 생성해주는 것이 안전합니다.
train_iter, test_iter = IMDB(split=("train", "test"))
train_list = list(train_iter) # 반복자를 리스트로 변환하여 재사용 가능하게 만듦
test_list = list(test_iter)
train_dataloader = DataLoader(train_list, batch_size=batch_size, shuffle=True, collate_fn=collate_batch)
test_dataloader = DataLoader(test_list, batch_size=batch_size, shuffle=False, collate_fn=collate_batch)
5. LSTM 기반 텍스트 분류 모델 설계
단어 인덱스를 밀집 벡터로 바꿔주는 nn.Embedding과 문장의 맥락을 파악하는 nn.LSTM, 그리고 최종 출력을 위한 nn.Linear 레이어로 모델을 구성합니다. LSTM의 마지막 타임스텝 Hidden State를 추출해 분류에 사용합니다.
class TextClassificationModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim):
super(TextClassificationModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
# 장기 의존성 학습에 유리한 LSTM 채택
self.rnn = nn.LSTM(embed_dim, hidden_dim, batch_first=True, num_layers=1, bidirectional=False)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text):
# text shape: [batch_size, seq_len]
embedded = self.embedding(text) # shape: [batch_size, seq_len, embed_dim]
output, (hidden, _) = self.rnn(embedded)
# hidden shape: [num_layers * num_directions, batch_size, hidden_dim]
# 단방향 LSTM이므로 마지막 레이어의 마지막 은닉 상태(hidden[-1])를 사용합니다.
return self.fc(hidden[-1])
# 하이퍼파라미터 설정
vocab_size = len(vocab)
embed_dim = 100
hidden_dim = 128
output_dim = 1 # 이진 분류이므로 출력 차원은 1
model = TextClassificationModel(vocab_size, embed_dim, hidden_dim, output_dim)
6. 모델 학습(Training)
이진 분류에 적합한 BCEWithLogitsLoss를 손실 함수로 설정하고, Adam 옵티마이저를 사용해 모델을 학습시킵니다.
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
def train_model(model, dataloader, criterion, optimizer, epochs=3):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
for epoch in range(epochs):
model.train()
total_loss = 0
for text, labels in dataloader:
text, labels = text.to(device), labels.to(device)
optimizer.zero_grad()
predictions = model(text).squeeze(1) # 차원 맞추기
loss = criterion(predictions, labels.float())
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1} | 평균 손실(Loss): {total_loss/len(dataloader):.4f}")
# 모델 학습 실행 (빠른 실습을 위해 3에폭만 진행)
train_model(model, train_dataloader, criterion, optimizer, epochs=3)
7. 모델 평가(Evaluation)
학습이 끝난 모델이 새로운 데이터(Test Data)에 대해 얼마나 잘 예측하는지 정확도를 검증해 봅니다.
def evaluate_model(model, dataloader):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.eval()
correct = 0
total = 0
with torch.no_grad():
for text, labels in dataloader:
text, labels = text.to(device), labels.to(device)
outputs = model(text).squeeze(1)
# 시그모이드를 통과한 값이 0.5 이상이면 1, 아니면 0으로 판단
predictions = torch.round(torch.sigmoid(outputs))
correct += (predictions == labels).sum().item()
total += labels.size(0)
print(f"테스트 데이터 최종 정확도(Accuracy): {correct / total:.4f}")
# 검증 및 평가 실행
evaluate_model(model, test_dataloader)
🛠️ 개발을 위한 실전 팁 (Tips for Development)
- 사전 학습된 임베딩(Pre-trained Embedding) 활용하기: 단어 가짓수가 많고 데이터가 부족할 때는 고정된 Embedding 레이어 대신 FastText나 GloVe, 또는 Word2Vec 같은 사전 학습된 가중치를 로드해서 사용하면 초기 학습 속도와 모델 성능이 크게 향상됩니다.
- 가변 길이 처리를 위한 Pack Padded Sequence: 패딩 토큰(0)이 너무 많으면 LSTM이 쓸데없는 연산을 하게 되어 성능이 저하될 수 있습니다. torch.nn.utils.rnn.pack_padded_sequence를 사용하면 패딩을 제외한 실제 문장 길이만큼만 효율적으로 연산할 수 있습니다.
- GPU 가속화 확인: NLP 모델은 데이터 사이즈가 커질 수 있으므로 반드시 text.to(device) 형태로 Tensor를 GPU 메모리에 올려 연산 속도를 확보해야 합니다.
⚠️ 흔히 하는 실수 (Common Mistakes)
- Iterator 일회성 소모 실수: torchtext.datasets에서 제공하는 데이터셋 객체는 일종의 '반복자(Iterator)'입니다. 이를 한 번 루프 돌려 어휘 사전을 만들고 나면 객체 내부가 비어버려서 DataLoader를 만들 때 데이터가 들어가지 않는 실수를 자주 합니다. 반드시 list(train_iter) 형태로 리스트화해두거나 필요할 때마다 다시 로드해 써야 합니다.
- 패딩 토큰 인덱스 불일치: nn.Embedding을 생성할 때 문장 길이를 맞추기 위해 넣은 패딩 토큰의 인덱스(예: padding_idx=0)를 명시하지 않으면, 모델이 패딩용 의미 없는 숫자조차 하나의 의미 있는 단어로 간주하고 학습을 진행하여 오버핏이 발생할 수 있습니다.
- Squeeze 차원 오류: 배치의 크기가 1이 되거나 최종 출력을 계산할 때 squeeze(1)를 무조건 적용하다가 가끔 텐서의 꼴이 망가져 Target size와 Input size가 맞지 않는다는 다차원 행렬 에러(Shape Mismatch)를 겪기 쉽습니다. 연산 직전에 항상 각 텐서의 .shape를 출력해 보며 디버깅하는 습관을 지니는 것이 좋습니다.
8. 결론
이번 포스팅에서는 파이토치를 활용해 자연어 처리의 핵심인 IMDb 영화 리뷰 감정 분석 모델을 직접 구현해 보았습니다. 자연어 텍스트 데이터를 토큰화하고 컴퓨터가 이해할 수 있는 숫자로 변환하는 파이프라인의 개념부터, LSTM 순환 신경망을 거쳐 긍정과 부정을 예측하는 일련의 과정을 다루었습니다.
단순히 이론으로만 접하던 딥러닝 컴포넌트들을 직접 코드로 엮어보며 감을 잡으셨기를 바랍니다. 여기서 한 걸음 더 나아가고 싶다면 LSTM 대신 양방향 구조인 Bi-LSTM을 적용해 보거나, 드롭아웃(Dropout) 레이어를 추가해 과적합을 방지하는 실험을 해보시는 것을 추천합니다. 오늘 다룬 기초가 훗날 트랜스포머(Transformer)와 대형 언어 모델을 학습하는 든든한 밑거름이 될 것입니다. 다음에도 유익한 개발 팁으로 찾아뵙겠습니다!
'Python for AI, Embedded > Deep Learning: PyTorch & AI Modeling' 카테고리의 다른 글
| RNN을 넘어선 혁신, 트랜스포머(Transformer) 모델 구조와 핵심 개념 완벽 정리 (0) | 2026.05.19 |
|---|---|
| PyTorch TorchText 활용한 NLP 데이터셋 로딩 및 전처리 가이드 (IMDB 예제) (0) | 2026.05.17 |
| PyTorch RNN, LSTM, GRU 개념 완벽 정리: 구조부터 차이점까지 한눈에 보기 (0) | 2026.05.16 |
| PyTorch 이미지 분류 완벽 가이드: MNIST부터 CIFAR-10까지 한 번에 끝내기 (0) | 2026.05.15 |
| PyTorch로 시작하는 CNN 이미지 분류: 기초부터 실전 예제까지 (0) | 2026.05.14 |