텍스트 처리 — 토큰화, 어간 추출, 표제어 추출

언어는 연속적입니다. 모델은 이산적입니다. 전처리는 그 둘을 잇는 다리입니다.

유형: Build 언어: Python 선수 조건: Phase 2 · 14(Naive Bayes) 소요 시간: 약 45분

학습 목표

  • 토큰화(Tokenization), 어간 추출(Stemming), 표제어 추출(Lemmatization)의 역할과 실패 양상(failure mode)을 설명합니다.
  • 정규식(regex) 기반 단어 토크나이저(word tokenizer)를 구현하고, URL, 이메일(email), 해시태그(hashtag) 같은 경계 사례(edge case)가 왜 깨지는지 이해합니다.
  • 포터(Porter) 스테머의 일부 규칙(rule)을 구현하고, 규칙 순서(rule ordering)가 결과에 미치는 영향을 확인합니다.
  • 룩업(lookup) 기반 표제어 추출기(lemmatizer)를 만들고, 실서비스(production)에서는 품사 태그(POS tag)와 형태론(morphology) 정보가 왜 필요한지 설명합니다.
  • NLTK와 spaCy가 같은 작업을 어떻게 처리하는지 비교하고, 학습/추론 불일치(training / inference mismatch)를 피하는 기준을 세웁니다.

문제

모델은 "The cats were running."이라는 문장을 그대로 읽지 않습니다. 모델이 실제로 읽는 것은 정수(integer)입니다.

모든 자연어 처리(NLP) 시스템은 같은 세 가지 질문에서 출발합니다. 단어는 어디에서 시작하는가. 단어의 어근(root)은 무엇인가. "run", "running", "ran"을 도움이 될 때는 같은 것으로, 도움이 되지 않을 때는 다른 것으로 어떻게 구분해 다룰 것인가.

토큰화를 잘못하면 모델은 쓰레기 같은 입력에서 학습합니다. 어떤 토크나이저가 don't를 토큰 하나로 다루는데 다른 토크나이저가 do n't 두 토큰으로 다루면, 학습 분포(training distribution)가 갈라집니다. 스테머가 organizationorgan을 같은 어간(stem)으로 줄이면 토픽 모델링(topic modeling)이 무너집니다. 표제어 추출기가 품사(part-of-speech) 문맥을 필요로 하는데 이를 넘기지 않으면, 동사(verb)가 명사(noun)처럼 처리됩니다.

이 레슨에서는 세 가지 전처리(preprocessing) 기본 요소(primitive)를 처음부터 만들고, NLTK와 spaCy가 같은 일을 어떻게 수행하는지 함께 보여 줍니다. 이렇게 비교하면 각 방식의 트레이드오프(trade-off)를 분명하게 볼 수 있습니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.정규식(regex) 기반 단어 토크나이저(word tokenizer)가 'don't'를 ['do', 'n', 't']로 나눈다면, 이것이 일으키는 다운스트림(downstream) 문제는 무엇인가요?

2.어간 추출기(stemmer)가 'organization'을 'organ'으로 줄입니다. 토픽 모델링(topic modeling) 파이프라인에서 이것이 왜 문제가 되나요?

0/2 답변 완료

개념

세 가지 연산(operation)이 있습니다. 각각 고유한 역할과 실패 양상을 가집니다.

Preprocessing pipeline: raw text → tokens → stems or lemmas → model

토큰화(Tokenization)는 문자열(string)을 토큰(token)으로 나눕니다. "토큰"이라는 말이 일부러 넓은 의미로 쓰이는 이유는, 과제(task)마다 적절한 단위 크기(granularity)가 다르기 때문입니다. 고전 NLP(classical NLP)에서는 단어 단위(word-level)를, 트랜스포머(transformer)에서는 서브워드(subword)를, 공백(whitespace)이 없는 언어에서는 문자(character) 단위를 쓸 수 있습니다.

어간 추출(Stemming)은 규칙으로 접미사(suffix)를 잘라냅니다. 빠르고, 공격적이고, 단순합니다. running -> run은 잘 작동합니다. 그러나 organization -> organ은 실패 양상의 대표적인 예입니다.

표제어 추출(Lemmatization)은 문법 지식(grammar knowledge)을 사용해 단어(word)를 사전형(dictionary form)으로 줄입니다. 더 느리지만 정확하고, 룩업 테이블(lookup table)이나 형태소 분석기(morphological analyzer)가 필요합니다. ran -> run"ran""run"의 과거형임을 알아야 가능합니다. better -> good은 비교급 형태(comparative form)를 알고 있어야 가능합니다.

경험칙은 단순합니다. 속도가 중요하고 잡음(noise)을 어느 정도 견딜 수 있다면 어간 추출을 씁니다(검색 색인(search indexing), 거친 분류(rough classification)). 의미가 중요하다면 표제어 추출을 씁니다(질문 응답(question answering), 의미 기반 검색(semantic search), 사용자가 직접 읽게 되는 모든 결과).

만들어 보기

Step 1: 정규식 단어 토크나이저

가장 단순하지만 유용한 토크나이저는 알파벳·숫자가 아닌 문자(non-alphanumeric character)를 기준으로 나누되, 구두점(punctuation)은 별도의 토큰으로 유지합니다. 완벽하지도 최종 형태도 아니지만 한 줄로 실행됩니다.

import re

def tokenize(text):
    return re.findall(r"[A-Za-z]+(?:'[A-Za-z]+)?|[0-9]+|[^\sA-Za-z0-9]", text)

우선순위 순으로 세 가지 패턴(pattern)이 있습니다. 내부에 아포스트로피(apostrophe)를 선택적으로 허용하는 단어(don't, it's), 순수한 숫자, 그리고 공백이 아닌 단일 비알파벳·숫자 문자(구두점)입니다.

>>> tokenize("The cats weren't running at 3pm.")
['The', 'cats', "weren't", 'running', 'at', '3', 'pm', '.']

주의해야 할 실패 양상도 있습니다. 3pm은 글자 연속과 숫자 연속을 번갈아 잡기 때문에 ['3', 'pm']으로 나뉩니다. 대부분의 작업에는 충분하지만 URL, 이메일, 해시태그는 모두 깨집니다. 실서비스에서는 더 일반적인 패턴보다 앞쪽에 전용 패턴을 추가해 사용합니다.

Step 2: 포터 스테머 (Step 1a만 구현)

전체 포터 알고리즘(Porter algorithm)은 다섯 단계의 규칙으로 구성됩니다. Step 1a만으로도 가장 흔한 영어 접미사를 다룰 수 있고, 규칙의 패턴을 이해하기에도 좋습니다.

def stem_step_1a(word):
    if word.endswith("sses"):
        return word[:-2]
    if word.endswith("ies"):
        return word[:-2]
    if word.endswith("ss"):
        return word
    if word.endswith("s") and len(word) > 1:
        return word[:-1]
    return word
>>> [stem_step_1a(w) for w in ["caresses", "ponies", "caress", "cats"]]
['caress', 'poni', 'caress', 'cat']

규칙은 위에서 아래로 읽습니다. ies -> i 규칙 때문에 ponies -> pony가 아니라 ponies -> poni가 됩니다. 이를 보완하는 것이 실제 포터 알고리즘의 step 1b입니다. 여러 규칙은 서로 경쟁하며, 먼저 매칭되는 규칙이 이깁니다. 순서는 개별 규칙 하나하나보다 더 중요합니다.

Step 3: 룩업 기반 표제어 추출기

진짜 표제어 추출에는 형태론 정보가 필요합니다. 교육용 버전에서는 작은 표제어 테이블(lemma table)과 폴백(fallback) 규칙을 사용합니다.

LEMMA_TABLE = {
    ("running", "VERB"): "run",
    ("ran", "VERB"): "run",
    ("runs", "VERB"): "run",
    ("better", "ADJ"): "good",
    ("best", "ADJ"): "good",
    ("cats", "NOUN"): "cat",
    ("cat", "NOUN"): "cat",
    ("were", "VERB"): "be",
    ("was", "VERB"): "be",
    ("is", "VERB"): "be",
}

def lemmatize(word, pos):
    key = (word.lower(), pos)
    if key in LEMMA_TABLE:
        return LEMMA_TABLE[key]
    if pos == "VERB" and word.endswith("ing"):
        return word[:-3]
    if pos == "NOUN" and word.endswith("s"):
        return word[:-1]
    return word.lower()
>>> lemmatize("running", "VERB")
'run'
>>> lemmatize("cats", "NOUN")
'cat'
>>> lemmatize("better", "ADJ")
'good'
>>> lemmatize("watched", "VERB")
'watched'

마지막 예제가 핵심입니다. watched는 테이블에 없고, 폴백 규칙은 ing로 끝나는 경우만 처리합니다. 실제 표제어 추출은 ed로 끝나는 단어, 불규칙 동사(irregular verb), 비교급 형용사(comparative adjective), children -> child처럼 발음 변화가 일어나는 복수형(sound change plural)까지 다룹니다. 그래서 실서비스 시스템에서는 WordNet, spaCy의 형태소 분석기(morphologizer), 또는 전체 형태소 분석기(full morphological analyzer)를 사용합니다.

Step 4: 하나로 연결하기

def preprocess(text, pos_tagger=None):
    tokens = tokenize(text)
    stems = [stem_step_1a(t.lower()) for t in tokens]
    tags = pos_tagger(tokens) if pos_tagger else [(t, "NOUN") for t in tokens]
    lemmas = [lemmatize(word, pos) for word, pos in tags]
    return {"tokens": tokens, "stems": stems, "lemmas": lemmas}

빠진 조각은 품사 태거(POS tagger)입니다. Phase 5 · 07(POS Tagging)에서 직접 만듭니다. 지금은 모든 토큰을 NOUN으로 간주하고 그 한계를 인정한 채로 진행합니다.

사용하기

NLTK와 spaCy는 실서비스용 구현을 제공합니다. 각각 몇 줄이면 됩니다.

NLTK

import nltk
nltk.download("punkt_tab")
nltk.download("wordnet")
nltk.download("averaged_perceptron_tagger_eng")

from nltk.tokenize import word_tokenize
from nltk.stem import PorterStemmer, WordNetLemmatizer
from nltk import pos_tag

text = "The cats were running."
tokens = word_tokenize(text)
stems = [PorterStemmer().stem(t) for t in tokens]
lemmatizer = WordNetLemmatizer()
tagged = pos_tag(tokens)


def nltk_pos_to_wordnet(tag):
    if tag.startswith("V"):
        return "v"
    if tag.startswith("J"):
        return "a"
    if tag.startswith("R"):
        return "r"
    return "n"


lemmas = [lemmatizer.lemmatize(t, nltk_pos_to_wordnet(tag)) for t, tag in tagged]

word_tokenize는 축약형(contraction), 유니코드(Unicode), 그리고 정규식이 놓치는 경계 사례를 처리해 줍니다. PorterStemmer는 다섯 단계 전체를 실행합니다. WordNetLemmatizer는 NLTK의 펜 트리뱅크(Penn Treebank) 품사 태그를 WordNet의 약어 집합(abbreviation set)으로 변환해 주어야 동작합니다. 위의 변환 연결 코드(wiring)가 많은 튜토리얼이 생략하는 부분입니다.

spaCy

import spacy

nlp = spacy.load("en_core_web_sm")
doc = nlp("The cats were running.")

for token in doc:
    print(token.text, token.lemma_, token.pos_)
The      the     DET
cats     cat     NOUN
were     be      AUX
running  run     VERB
.        .       PUNCT

spaCy는 전체 파이프라인을 nlp(text) 뒤에 숨깁니다. 토큰화, 품사 태깅, 표제어 추출이 모두 한 번에 실행됩니다. 대규모 처리에서는 NLTK보다 빠르고, 기본 상태(out of the box)에서의 정확도도 높습니다. 트레이드오프는 개별 구성 요소(component)를 쉽게 교체하기 어렵다는 점입니다.

무엇을 고를까

상황선택
교육, 연구, 구성 요소 교체NLTK
실서비스, 다국어(multi-language), 속도가 중요한 경우spaCy
트랜스포머 파이프라인(모델 자체의 토크나이저를 쓸 예정)tokenizers / transformers를 쓰고 고전적인 전처리는 건너뜀

아무도 잘 말해 주지 않는 두 가지 실패 양상

대부분의 튜토리얼은 알고리즘만 가르치고 끝납니다. 그러나 실제 전처리 파이프라인에서는 다음 두 가지가 자주 문제를 일으키는데, 거의 다뤄지지 않습니다.

재현성 드리프트(Reproducibility drift). NLTK와 spaCy는 버전이 바뀌면서 토큰화와 표제어 추출 동작을 바꿉니다. spaCy 2.x에서 ['do', "n't"]로 나오던 결과가 3.x에서는 ["don't"]가 될 수 있습니다. 모델은 한 분포에서 학습되었는데 추론은 다른 분포에서 이루어지는 셈입니다. 정확도가 조용히 떨어지고도 원인을 모르게 됩니다. 라이브러리 버전을 requirements.txt에 고정(pin)하고, 표본 문장 20개의 기대 토큰화 결과를 박제하는 전처리 회귀 테스트(preprocessing regression test)를 작성하세요. 그리고 업그레이드마다 실행합니다.

학습/추론 불일치(Training / inference mismatch). 학습 단계에서는 공격적인 전처리(소문자화, 불용어(stopword) 제거, 어간 추출)를 적용해 놓고, 배포 단계에서는 사용자 원본 입력에 그대로 모델을 돌리면 성능이 무너집니다. 이는 실서비스 NLP에서 가장 흔한 실패 사례입니다. 학습 시 전처리를 했다면, 추론 시에도 동일한 함수를 그대로 실행해야 합니다. 운영 팀(serving team)이 노트북 셀을 다시 옮겨 적게 두지 말고, 전처리는 모델 패키지 안의 함수 형태로 함께 배포하세요.

산출물 만들기

엔지니어가 교재 세 권을 읽지 않고도 전처리 전략을 고를 수 있도록 돕는 재사용 가능한 프롬프트(prompt)를 만듭니다.

outputs/prompt-preprocessing-advisor.md로 저장합니다.

---
name: preprocessing-advisor
description: Recommends a tokenization, stemming, and lemmatization setup for an NLP task.
phase: 5
lesson: 01
---

You advise on classical NLP preprocessing. Given a task description, you output:

1. Tokenization choice (regex, NLTK word_tokenize, spaCy, or transformer tokenizer). Explain why.
2. Whether to stem, lemmatize, both, or neither. Explain why.
3. Specific library calls. Name the functions. Quote the POS-tag translation if NLTK is involved.
4. One failure mode the user should test for.

Refuse to recommend stemming for user-visible text. Refuse to recommend lemmatization without POS tags. Flag non-English input as needing a different pipeline.

연습문제

  1. 쉬움. tokenize를 확장하여 URL을 하나의 토큰으로 유지하도록 만듭니다. 테스트: tokenize("Visit https://example.com today.")는 URL 토큰 하나를 만들어 내야 합니다.
  2. 중간. 포터 알고리즘의 step 1b를 구현합니다. 단어에 모음(vowel)이 포함되어 있고 ed 또는 ing으로 끝나면 해당 접미사를 제거합니다. 이중 자음 규칙(double-consonant rule)도 함께 처리합니다(hopping -> hop이지 hopp이 아닙니다).
  3. 어려움. WordNet을 룩업 테이블로 사용하되, WordNet에 표제어 항목이 없으면 직접 만든 포터 스테머로 폴백(fallback)하는 표제어 추출기를 구현합니다. 품사 태그가 부여된 말뭉치(tagged corpus)에서 순수 WordNet, 순수 포터와 비교해 정확도를 측정합니다.

핵심 용어

용어흔한 설명실제 의미
토큰(Token)단어모델이 소비하는 단위입니다. 단어, 서브워드, 문자, 바이트일 수 있습니다
어간(Stem)단어의 어근규칙 기반 접미사 제거(suffix stripping)의 결과입니다. 실제 사전에 있는 단어가 아닐 수도 있습니다
표제어(Lemma)사전형사전에서 찾게 되는 형태입니다. 정확히 계산하려면 문법적 문맥이 필요합니다
품사 태그(POS tag)품사(Part of speech)NOUN, VERB, ADJ 같은 범주입니다. 정확한 표제어 추출에 필요합니다
형태론(Morphology)단어 형태 규칙시제(tense), 수(number), 격(case)에 따라 단어 형태가 바뀌는 방식입니다. 표제어 추출은 여기에 의존합니다

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

preprocessing-advisor

Recommends a tokenization, stemming, and lemmatization setup for an NLP task.

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.룩업(lookup) 기반 표제어 추출기(lemmatizer)에서 lemmatize('running', 'NOUN')을 호출했더니 'running'이 그대로 반환됩니다. 가장 가능성 높은 원인은 무엇인가요?

2.팀이 Jupyter 노트북에서 공격적인 전처리(소문자화, 불용어(stopword) 제거, 어간 추출)를 적용해 NLP 모델을 학습했습니다. 실서비스(production)에서는 사용자 원본 입력이 모델에 직접 전달됩니다. 이것은 어떤 실패 패턴인가요?

3.포터 스테머(Porter stemmer) Step 1a 규칙의 순서가 'sses' -> 'ss', 'ies' -> 'i', 'ss' -> 'ss', 's' -> 제거 순서입니다. 왜 규칙 순서(rule ordering)가 개별 규칙보다 더 중요한가요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다