Supplementary Materials

요약

  • 텍스트를 (한국어는 형태소 단위로) 토큰화
  • → 불필요한 요소 제거
  • → 벡터화(One-Hot → BoW → TF-IDF) 순으로,
  • 점점 더 단어의 중요도를 잘 반영하는 방향으로 발전한다.

1. 자연어처리 개요

  • 자연어처리(NLP; Natural Language Processing)
    • 컴퓨터가 인간처럼 텍스트를 이해할 수 있도록 하는 기술

  • cf. NLP의 두 가지 영역

    1. NLU (Natural Language Understanding): 자연어 이해
    2. NLG (Natural Language Generation): 자연어 생성
  • 대표적인 NLP 태스크

    • 기계 번역(Machine Translation)
    • 대화 시스템(Dialog System)
    • 문서 요약(Summarization)
    • 스토리 생성(Story Generation) 등

1.1. NLP 시스템 구축 단계

  • NLP 시스템은 다음 세 단계로 구성된다.
    1. 전처리 (Preprocessing): 입력 텍스트를 토큰 단위로 분할 (토큰화)
    2. 임베딩 (Embedding): 단어를 수치 벡터 공간으로 표현 (N x D 벡터)
    3. 모델링 (Modeling): 대규모 텍스트 코퍼스로 학습하여 예측·분류 수행

1.2. NLP의 기본 단위

  • NLP의 기본 단위들을 작은 것부터 큰 것까지 정리하면 이렇다.
    1. 토큰(Token): 가장 작은 단위로, 문맥에 따라 단어·형태소·서브워드가 될 수 있다.
    2. 문장: 마침표·느낌표·물음표로 구분되는 최소 독립 단위
    3. 단락: 문장 집합
    4. 문서: 단락 집합
    5. 말뭉치(corpus): 수집된 텍스트 데이터 전체

2. 텍스트 전처리

2.1. 토큰

  • 토큰 (Token)
    • 문장이나 문서를 구성하는 가장 작은 단위의 텍스트 조각

    • 토큰이 무엇인지 정의하는 기준은 분석 목적이나 알고리즘에 따라 다르다.

    • 단어가 될 수도 있고, 형태소, 글자(문자), 혹은 단어의 일부(서브워드, Subword)가 될 수도 있다.

    • 예: “안녕하세요”라는 단어가 있을 때, 토큰은 다음과 같이 나뉠 수 있다.

      • 단어 단위 토큰: ["안녕하세요"]
      • 글자 단위 토큰: ["안", "녕", "하", "세", "요"]
      • 서브워드 단위 토큰: ["안녕", "##하세요"] (알고리즘에 따라 다름)

2.2. 토큰화

  • 토큰화 (Tokenization)

    • 입력된 가공되지 않은 텍스트(Raw Text; corpus)를
    • 의미 있는 최소 단위인 토큰 단위로 잘게 쪼개는 과정(Process)
  • 컴퓨터는 텍스트를 인간처럼 곧바로 이해하지 못한다.

    • 따라서 텍스트를 숫자로 변환(임베딩)하기 전에, 컴퓨터가 처리하기 가장 적절한 크기의 조각으로 나누어 주는 이 토큰화 과정이 반드시 필요하다.

(1) 문장 토큰화

  • 문장 토큰화

    • 문서를 문장 단위로 분리
  • 마침표가 항상 문장 경계는 아니라는 점이 어려움이다.

    • “애들아 A. I. 수업 이해했다고 해줘.”에서 A.I.의 마침표를 문장 끝으로 잘못 처리하면 안 된다.
    • 결국 언어와 코퍼스 특성에 맞는 규칙을 직접 정의해야 하므로 전처리는 까다로운 작업이다.

(2) 어절 기반

  • 단어 토큰화
    • 공백/구두점 기준으로 단어 분리
    • "나는 밥을 먹었다"["나는", "밥을", "먹었다"]

  • 구두점이나 특수문자를 단순히 제외해서는 안 된다.
  • 단어 토큰화에서 띄어쓰기와 구두점 제거만으로 자르면 문제가 생기는 경우가 많다.
  • 예를 들어
    • “Ph.D”, “A.I.”처럼 의미 있는 약어가 깨지고,
    • $45.55(금액),
    • 01/02/06(날짜),
    • 123,456,789(콤마),
    • New York(띄어쓰기 합성어) 같은 경우는 잘못 분리될 수 있다.

  • 영어는 띄어쓰기 기반 단어 토큰화가 잘 통하지만,

    • 한국어는 어절(띄어쓰기) 기반 토큰화를 지양한다.
    • 한국어는 교착어(첨가어)라서 조사·어미가 단어에 그대로 붙기 때문이다.
  • 성호 = Sung is, 성호 = with Sung, 성호에게 = to Sung

    • → 같은 단어(성호)가 조사 때문에 매번 다른 단어로 인식되는 문제 발생

(3) 형태소 기반

한국어는 교착어라 어절 기반 토큰화로는 부족하고, 형태소(morpheme) 기반으로 토큰화한다.

  • 형태소는 의미(어휘+문법)를 가진 가장 작은 단위로 두 종류가 있다.

    1. 자립 형태소
      • 혼자 쓰일 수 있는 형태소
      • (명사·대명사·수사, 관형사·부사, 감탄사 등)
    2. 의존 형태소
      • 다른 형태소와 결합해야 쓰이는 형태소
      • (접사, 어미, 조사, 어간)
          • 동사·형용사의 어간+어미(먹-다/먹-고/먹-어서),
          • 접두사(햇과일, 맨손),
          • 접미사(나무꾼, 선생님) 등도 의존 형태소에 속한다.
  • 예시 “성호가 자연어처리 수업을 한다.”를 분석하면:

    1. 자립: 성호, 자연어, 처리, 수업
    2. 의존: -가(조사), 을-(조사), 하-(어간), -ㄴ다(어미)

(4) 실습: Okt 유무에 따른 vocab 차이

위 예문을 그대로 코드로 — 어절 기반과 형태소 기반의 차이를 vocab으로 확인한다.

from konlpy.tag import Okt   # KoNLPy — Java 필요
from sklearn.feature_extraction.text import CountVectorizer
 
okt = Okt()
sample = "성호가 자연어처리 수업을 한다."
 
# Case A: raw 문장 그대로 → 어절(공백) 단위로 잘림
vec_A = CountVectorizer()
vec_A.fit([sample])
print("Case A (Okt 없이):", sorted(vec_A.get_feature_names_out()))
# → ['성호가', '수업을', '자연어처리', '한다'] — 조사가 붙은 채 한 단어 취급
 
# Case B: Okt 형태소 분석 후 → 조사가 분리됨
tokens = okt.morphs(sample)
print("Okt 토큰:", tokens)   # ['성호', '가', '자연어', '처리', '수업', '을', '한다', '.']
vec_B = CountVectorizer()
vec_B.fit([' '.join(tokens)])
print("Case B (Okt 사용):", sorted(vec_B.get_feature_names_out()))
  • ‘성호가’(조사마다 다른 단어 취급) vs ‘성호’+‘가’(분리)
    • → 교착어인 한국어에서 okt.morphs()가 필요한 이유가 그대로 드러난다.

cf. 서브워드 토큰화

  • 단어를 더 작은 조각으로 (BPE, WordPiece, SentencePiece)
  • → 신경망/LLM에서 표준

2.3. 토크나이저

  • 토크나이저 (Tokenizer)
    • 토큰화 과정을 실제로 수행하는 도구 또는 알고리즘
언어도구특징
영어NLTKword_tokenize, sent_tokenize 등 교육·기본용
영어spaCy빠르고 정확, 실무용 파이프라인
한국어KoNLPy (Okt, Mecab, Komoran, Hannanum)형태소 분석 기반
한국어Mecab속도·정확도 우수 (설치 약간 번거로움)
공통Hugging Face tokenizers, SentencePiece서브워드, LLM/딥러닝용

2.4. 불용어 제거

  • 불용어 제거 (Stopword Removal)

    • 분석에 큰 의미가 없는 흔한 단어(불용어)를 제거해
    • 노이즈를 줄이고 핵심 단어에 집중하는 작업이다.
  • 불용어 예시

    • 영어: the, a, is, in, of, and ...
    • 한국어: 이, 그, 저, 은, 는, 을, 를, 하다, 있다 ... (조사·접속사 등)
  • 방법

    • 사전 기반: NLTK stopwords, spaCy 내장 불용어 목록 사용
    • 커스텀 목록: 도메인에 맞게 직접 추가/삭제 (한국어는 표준 사전이 없어 직접 구축하는 경우가 많음)
  • cf. 주의할 점

    • 불용어 제거가 항상 좋은 건 아님: 감정분석(“not good”)이나 LLM 기반 작업에서는 의미가 사라질 수 있어 생략하기도 한다.
    • 최신 딥러닝/LLM에서는 보통 서브워드 토크나이저만 쓰고 불용어 제거는 잘 하지 않는다.
    • 불용어 제거는 주로 고전적 NLP(TF-IDF, 키워드 추출, 토픽 모델링)에서 유용하다.

3. 워드 임베딩

  • 워드 임베딩 (Word Embedding)

    • 단어를 실수 벡터로 매핑하는 표현 방식이다.
  • 텍스트 벡터화로 넘어가는 이유

    • 컴퓨터가 단어의 의미를 파악하려면 텍스트를 숫자(벡터)로 바꿔야 한다.
    • 이미지가 RGB 3-channel 행렬(0~255 숫자)로 표현되듯,
    • 텍스트도 벡터로 표현해야 코멘트(극성) 분석, 문서 분류, 문서 요약, 스팸 필터링 같은 작업이 가능해진다.
    • 사람의 말을 컴퓨터에게 이해시키는 것이 목표이다.

3.1. 정수 인코딩

  • 가장 단순한 방법은 각 단어에 ID를 부여하는 정수 인코딩이다.

예시 성호:0, 자연어:1, 처리:2, 수업:3

3.2. One-Hot Encoding

  • 이를 One-Hot 벡터로 바꾸면 해당 위치만 1이고 나머지는 0인 벡터가 된다.

예시

  • 하지만 두 가지 큰 문제가 있다.
    1. 고차원 문제: 어휘가 백만 개면 벡터 차원도 백만이 된다.
    2. 유사도(의미) 표현 불가 문제: 고양이와 강아지처럼 의미가 비슷한 단어라도 서로 다른 위치에 1이 찍히므로, 유클리디안 거리나 코사인 유사도로 계산하면 유사성을 전혀 잡아내지 못한다.

  • 유사도 측정에는 두 방식이 쓰인다.
    1. 유클리디안 거리: 두 점 사이 직선 거리 √Σ(xᵢ−yᵢ)²
    2. 코사인 유사도: 두 벡터의 각도(cosθ) 기반, −1(반대)~0(무관)~1(동일)

3.3. Bag of Words (BoW)

  • Bag of Words (BoW)

    • 원-핫 인코딩처럼 단어를 단순한 벡터로 표현하지만,
    • 의미적 유사성을 반영하지 못한다.
  • BoW 가정

    • 단어 순서를 무시하고 각 문서 내 단어 빈도만 고려해 벡터를 만드는 방식이다. (Term Frequency 기반 벡터화)
    • 윤동주 “별 헤는 밤” 예시처럼 단어별 등장 횟수를 세어 문서×단어 행렬(Document-Term Matrix)을 만들고, 이 행렬에 코사인 유사도를 적용하면 논문 검색이나 도서·영화(넷플릭스) 추천 시스템에 활용할 수 있다.
  • 한계:

    • 단순 빈도라 모든 단어에 동일한 중요도를 준다.
    • 즉 의미 없는 조사 ‘이/가’가 10번 나오는 것과 핵심 형용사 ‘재미있는/훌륭한’이 10번 나오는 것을 똑같이 취급하는 문제가 있다.

(1) 예시

  • 두 문장을 BoW로 벡터화해 보자.
    • 전체 단어 사전 = [성호, 수업, 한다, 듣는다] 라고 하면, 각 문서를 단어별 등장 횟수로 표현한다.
    1. 문서1: "성호가 수업을 한다" → 문서1 =
    2. 문서2: "성호가 수업을 듣는다" → 문서2 =
단어성호수업한다듣는다
문서11110
문서21101
  • 순서는 무시하고 빈도만 세므로, “성호”와 “수업”을 공유하는 두 문서는 코사인 유사도가 높게 나온다.

3.4. TF-IDF (가중치 부여)

  • 빈도만으로는 부족한 점을 보완하기 위해 단어에 가중치를 준다.

    1. TF (Term Frequency): 특정 단어 t가 문서 d에 등장한 횟수
    2. DF (Document Frequency): 단어 t가 등장한 문서의 수
    3. IDF = log(N/DF): 전체 문서 수 N을 DF로 나눈 값에 로그 (N이 커질수록 값이 기하급수적으로 커지기 때문에 로그로 완화)
    4. TF, IDF 둘을 곱한 것이 TF-IDF = TF × log(N/DF) 이다.
  • 핵심 아이디어는

    • 모든 문서에 두루 등장하는 단어는 중요도를 낮게, 특정 문서에만 자주 등장하는 단어는 중요도를 높게 판단하는 것이다.
    • 빈도(TF)와 희소성(IDF)을 모두 고려한다.
    • 셰익스피어 작품 예시에서 단순 빈도표가 TF-IDF 가중치 표로 바뀌면서 흔한 단어의 값은 줄고 희귀한 단어의 값이 부각된다.
    • 정보 검색에서 가장 널리 알려진 가중치 기법이다.

(1) 예시

단어”이”(조사)“자연어”
전체 문서가 3개 → 전체 문서가 3개 →
어떤 문서에서 5번 등장 → 어떤 문서에서 5번 등장 →
3개 문서 모두에 등장 → 이 문서에만 등장 →
  • → 결과
    • 모든 문서에 흔한 “이”는 가중치가 0으로 소멸하고,
    • 특정 문서에만 나오는 “자연어”는 높은 가중치를 받는다.
    • 빈도는 같아도 희소성으로 중요도가 갈린다.

(2) sklearn 구현: CountVectorizer vs TfidfVectorizer

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
 
tf_vec    = CountVectorizer(max_features=2000)   # TF (단순 빈도) = BoW
tfidf_vec = TfidfVectorizer(max_features=2000)   # TF × IDF (희소성 가중치)
 
# ★ fit_transform은 train에만! test는 transform만 ★ (거꾸로 쓰면 data leakage)
# (X_train_text/X_test_text 정의는 §4.3 실습 참고)
X_train_tf = tf_vec.fit_transform(X_train_text).toarray()   # vocab 학습 + 변환
X_test_tf  = tf_vec.transform(X_test_text).toarray()        # 변환만!
  • 결과 행렬 shape = (문서 수 × vocab 크기) — §3.3의 Document-Term Matrix 그 자체

4. 실습: 네이버 영화 리뷰 감성 분류

1장의 NLP 3단계(전처리 → 임베딩 → 모델링)를 끝까지 실행하는 종합 실습. 벡터화까지 끝나면 07_Classification·08_Image Data & CNN완전히 동일한 골격(CrossEntropyLoss + argmax)이다.

4.1. (참고) ngram_range에 따른 vocab 변화

sample_tokens = ['성호 가 자연어 처리 재미있다']
 
for ng in [(1, 1), (1, 2), (1, 3)]:
    vec = CountVectorizer(ngram_range=ng)
    vec.fit(sample_tokens)
    print(f"ngram_range={ng}: {sorted(vec.get_feature_names_out())}")
# (1,2)면 1-gram + 2-gram('성호 가', '자연어 처리' 등)이 모두 vocab에 포함된다

4.2. 전처리: 형태소 토큰화

import pandas as pd
from tqdm.auto import tqdm
SEED = 42
 
df = pd.read_table('naver-review.txt', on_bad_lines='skip')
df = df.dropna(how='any').reset_index(drop=True)
 
# 클래스별 균형 샘플 (수업에서는 클래스당 10,000건)
N_PER_CLASS = 2_500
pos = df[df['label'] == 1].sample(n=N_PER_CLASS, random_state=SEED)
neg = df[df['label'] == 0].sample(n=N_PER_CLASS, random_state=SEED)
df_s = pd.concat([pos, neg]).sample(frac=1, random_state=SEED).reset_index(drop=True)
 
# 토큰화: 형태소 분리 → 공백으로 다시 연결 (sklearn 입력 형식)
def preprocess(text):
    if not isinstance(text, str) or not text.strip():
        return []
    return okt.morphs(text)
 
tqdm.pandas(desc='Okt')
df_s['tokens'] = df_s['document'].progress_apply(preprocess)
df_s['joined'] = df_s['tokens'].apply(' '.join)
df_s = df_s[df_s['tokens'].str.len() > 0].reset_index(drop=True)

4.3. 임베딩: split → 벡터화

from sklearn.model_selection import train_test_split
 
X_train_text, X_test_text, y_train, y_test = train_test_split(
    df_s['joined'].values, df_s['label'].values,
    test_size=0.2, random_state=SEED, stratify=df_s['label']
)
 
X_train_tf    = tf_vec.fit_transform(X_train_text).toarray()     # §3.4 (2)와 동일
X_test_tf     = tf_vec.transform(X_test_text).toarray()
X_train_tfidf = tfidf_vec.fit_transform(X_train_text).toarray()
X_test_tfidf  = tfidf_vec.transform(X_test_text).toarray()

4.4. 모델링: MLP 분류

import torch, torch.nn as nn, torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from sklearn.metrics import accuracy_score
 
class TextMLP(nn.Module):
    """텍스트 벡터(2000차원)용 MLP — 구조는 ch08 SimpleMLP와 동일"""
    def __init__(self, input_dim, hidden1=64, hidden2=32, num_classes=2):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden1), nn.ReLU(),
            nn.Linear(hidden1, hidden2), nn.ReLU(),
            nn.Linear(hidden2, num_classes),     # raw logits
        )
 
    def forward(self, x):
        return self.net(x)
 
 
def run_text_experiment(X_tr, X_te, y_tr, y_te, label):
    """벡터화 방식(TF/TF-IDF)만 바꿔 동일 골격으로 학습+평가"""
    X_tr_t = torch.tensor(X_tr, dtype=torch.float32)
    y_tr_t = torch.tensor(y_tr, dtype=torch.long)      # CrossEntropy → long!
    X_te_t = torch.tensor(X_te, dtype=torch.float32)
 
    loader = DataLoader(TensorDataset(X_tr_t, y_tr_t), batch_size=64, shuffle=True)
    model = TextMLP(input_dim=X_tr.shape[1])
    train_model(model, loader, nn.CrossEntropyLoss(),   # train_model: [[07_Classification]] §8.4
                optim.Adam(model.parameters(), lr=0.001), num_epochs=10)
 
    model.eval()
    with torch.no_grad():
        preds = model(X_te_t.to(DEVICE)).argmax(dim=1).cpu()
    acc = accuracy_score(y_te, preds)
    print(f">>> [{label}] Test Accuracy: {acc:.4f}")
    return acc
 
acc_tf    = run_text_experiment(X_train_tf, X_test_tf, y_train, y_test, 'TF')
acc_tfidf = run_text_experiment(X_train_tfidf, X_test_tfidf, y_train, y_test, 'TF-IDF')
  • 핵심 비교 포인트
    • 입구(전처리·벡터화)만 텍스트용으로 바뀌었을 뿐, 학습·평가 골격은 ch07·08과 동일하다.
    • TF vs TF-IDF: 같은 모델에서 벡터화 방식만 바꿔 성능을 비교하는 실험 설계.