헷개정 - 헷갈리는 개념 정리

stemming, lemmatization

cch8ii 2025. 10. 24. 18:28

텍스트 데이터를 모델에 입력하기 전에 불용어(stopword) 제거, 소문자 변환, 토큰화(tokenization) 등 여러 전처리 과정을 거쳐야 한다.

 

이때 자연어 데이터에는 같은 의미를 가진 단어라도 여러 가지 형태로 표현되는 경우가 많다. 예를 들어서 “run”, “running”, “runs”, “ran”은 모두 ‘달리다’라는 의미를 가진다. 하지만 컴퓨터는 이들을 완전히 다른 단어로 인식한다. 이렇게 단어가 불필요하게 분리되면 모델이 단어 간 관계를 학습하기 어려워지고 데이터가 sparse해진다는 문제가 생긴다.

이러한 문제를 해결하기 위해 단어의 형태를 정규화하는 과정을 거치는데 바로 Stemming(어간 추출) 과 Lemmatization(표제어 추출)이다.

 

이전에 알아야 할 것은 단어가 형태소 단위로 나뉜다는 것이다. 형태소는 단어가 의미를 가지는 가장 작은 단위인데 이때 형태소의 종류로 어간(stem)과 접사(affix)가 존재한다. 어간은 단어의 의미를 담고 있는 단어의 핵심 부분이고 접사는 단어에 추가적인 의미를 주는 부분이다.

 

Stemming(어간 추출)

어간(Stem)을 추출하는 작업을 어간 추출(stemming)이라고 한다. 어간 추출의 경우 형태학적인 분석을 조금 단순화한 작업인데 정해진 규칙을 기반으로 단어의 어미를 잘라내는 섬세하지 않은 작업이다.

뒤에서 설명할 Lemmatization과 달리 단순히 어미를 잘라서 단어가 어떤 품사인지, 문맥에서 어떤 의미로 쓰였는지 고려하지 않는다. 때문에 stemming 후 나오는 결과 단어가 사전에 존재하지 않는 단어일 수 있다.

from nltk.stem import PorterStemmer
stemmer = PorterStemmer()

print(stemmer.stem('studies'))   # studi
print(stemmer.stem('ran'))       # ran
print(stemmer.stem('happier'))   # happi
print(stemmer.stem('women'))     # women

 

예시를 보면 둘 다 의미는 남지만 형태는 사전에 없는 단어가 된다.

이처럼 정확하진 않지만 속도가 빠르며 PorterStemmer의 경우에는 정밀하게 설계되어있어 영어 자연어 처리에서 어간 추출을 하고 싶을 때 사용하면 좋은 선택이 된다고 한다. PorterStemmer외에도 LancasterStemmer 알고리즘도 있는데 둘을 비교한 예시는 다음과 같다. (두 알고리즘의 어간 추출 규칙이 다르다. )

## 참고 사이트: https://wikidocs.net/21707

from nltk.stem import PorterStemmer
from nltk.stem import LancasterStemmer

porter_stemmer = PorterStemmer()
lancaster_stemmer = LancasterStemmer()

words = ['policy', 'doing', 'organization', 'have', 'going', 'love', 'lives', 'fly', 'dies', 'watched', 'has', 'starting']
print('어간 추출 전 :', words)
print('포터 스테머의 어간 추출 후:',[porter_stemmer.stem(w) for w in words])
print('랭커스터 스테머의 어간 추출 후:',[lancaster_stemmer.stem(w) for w in words])

어간 추출 전 : ['policy', 'doing', 'organization', 'have', 'going', 'love', 'lives', 'fly', 'dies', 'watched', 'has', 'starting']
포터 스테머의 어간 추출 후: ['polici', 'do', 'organ', 'have', 'go', 'love', 'live', 'fli', 'die', 'watch', 'ha', 'start']
랭커스터 스테머의 어간 추출 후: ['policy', 'doing', 'org', 'hav', 'going', 'lov', 'liv', 'fly', 'die', 'watch', 'has', 'start']

 

앞서 언급한 바와 같이 서로 다른 규칙을 사용하기 때문에 다른 결과가 나오고 사용하고자 하는 Corpus에 각각 적용해본 후 적합한 Stemmer 알고리즘을 사용해야 한다.

Lemmatization(표제어 추출)

표제어(Lemma)는 한글로는 '표제어' 또는 '기본 사전형 단어' 라는 의미이다. Lemmatization은 말 그대로 단어들의 표제어 자체를 찾아간다고 생각하면 된다. 즉, Lemmatization은 단순히 어미를 자르는 게 아니라 사전에 존재하는 원형(lemma) 으로 변환하려고 하는 방법이다.

사전과 문법을 이용해서 단어의 원형을 찾아가기 때문에 Stemming에 비해 정확도가 높다. 예를 들어서 am, are, is는 서로 다른 스펠링이지만 그 뿌리 단어는 be라는 것을 알고 있는 것이다. (am, are, is의 표제어 = be)

## 예시
dies → die
has → have
watched → watch

하지만 여기서 우리가 알아야 할 것은 단어가 품사에 따라서 다른 lemma를 가지고 있다는 것이다. 때문에 Stemming은 품사를 고려할 필요가 없지만 Lemmatization은 품사를 고려하는 것이 반드시 필요하다.

## 단어가 품사에 따라서 다른 lemma를 가지는 예시

from nltk.stem import WordNetLemmatizer
lem = WordNetLemmatizer()

words = ["dies", "watched", "has", "better", "saw", "meeting"]

# 품사 미지정: 기본 pos='n'(명사)로 처리 → 문맥이 동사/형용사여도 잘 안 바뀜
print([lem.lemmatize(w) for w in words])
# 예) ['die', 'watched', 'has', 'better', 'saw', 'meeting']   # 환경에 따라 다를 수 있음

# 품사 지정: 동사(v), 형용사(a) 등을 알려주면 정확해짐
print([lem.lemmatize("dies",   "v"),   # 'die'
       lem.lemmatize("watched","v"),   # 'watch'
       lem.lemmatize("has",    "v")])  # 'have'

print(lem.lemmatize("better", "a"))    # 'good' (형용사일 때)
print(lem.lemmatize("saw",    "v"))    # 'see'  (동사일 때)
print(lem.lemmatize("meeting","v"))    # 'meet' (동사 ing 형태)

 

즉, Lemmatization을 하기 위해선 해당 문장에서 어떤 품사로 쓰였는지 알아야 원형을 결정할 수 있기 때문에 Lemmatizer는 내부에 사전을 사용한다.

NLTK에서는 표제어 추출을 위한 도구인 WordNetLemmatizer를 지원한다. WordNetLemmatizer는 내부적으로 WordNet이라는 사전을 사용한다. 이 사전은 단어 별로 명사(noun), 동사(verb), 형용사(adjective) 등에 따라 다른 lemma를 저장하고 있다.

예를 들어 다음과 같이 품사에 따라 다른 표제어가 추출되는 것이다.

단어  품사  Lemma
dies 동사(V) die
dies 명사(N) dye

 

때문에 Lemmatization을 할 때 품사 정보를 주지 않으면 기본값이 명사로 처리가 되어 잘못된 결과가 나올 수 있다.

## 품사 정보를 주지 않아 잘못된 결과가 나오는 예시
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()

word = "dies"
print("기본값(Pos=명사) 사용:", lemmatizer.lemmatize(word))
# 기본값(Pos=명사) 사용: dye

 

때문에 품사 정보를 활용해 Lemmatization을 진행해보면 다음과 같은 결과가 나온다.

from nltk.stem import WordNetLemmatizer

lemmatizer = WordNetLemmatizer()

lemmatizer.lemmatize('studies', 'v')  # study
lemmatizer.lemmatize('ran', 'v')  # run
lemmatizer.lemmatize('happier', 'a')  # happy
lemmatizer.lemmatize('women', 'n')  # woman

 

Lemmatization을 진행하면 Stemming과 달리 사전에 존재하는 단어가 추출되는 것을 볼 수 있다.

 

Stemming(어간 추출) 과 Lemmatization(표제어 추출)을 비교해보자

간단하게 표로 비교해보면 다음과 같다.

구분  Stemming (어간 추출) Lemmatization (표제어 추출)
정의 단어의 어미(접사)를 단순 규칙 기반으로 자름 품사와 의미 정보를 활용해 사전에 존재하는 원형(lemma)으로 변환
기반 방식 규칙 기반 문자열 처리 (단순 자르기) 사전 기반 처리 (WordNet 등 활용)
품사 고려 여부 고려하지 않음 품사 정보 필요 (POS tagging 필요)
결과 단어 사전에 존재하지 않는 단어가 될 수 있음 항상 사전에 존재하는 실제 단어
정확도 낮음 — 형태만 자름 높음 — 의미와 문법 고려
속도 빠름 느림
장점 단순하고 계산량이 적음 문법적, 의미적 정확도가 높음
단점 불규칙 변화·품사 반영 불가 품사 정보가 없으면 부정확해질 수 있음

 

한국어는 어떻게 처리하지?

한국어는 영어처럼 띄어쓰기 단위의 단어가 아니기 때문에 형태소 분석을 통해 단어의 구성 요소를 분리해야 한다.

한국어는 다음과 같은 5언 9품사의 구조를 가지고 있다.

언  품사
체언 명사, 대명사, 수사
수식언 관형사, 부사
관계언 조사
독립언 감탄사
용언 동사, 형용사

이 중 용언에 해당되는 동사와 형용사는 어간(stem)과 어미(ending)의 결합으로 구성된다.

  • 어간(stem) : 용언(동사, 형용사)을 활용할 때 원칙적으로 모양이 변하지 않는 부분
    • 예: 먹다 → 먹, 가다 → 가
    • 변할 수도 있음 (불규칙 활용) → 예: 긋다 → 그어라
  • 어미(ending): 용언의 어간 뒤에 붙어서 활용하면서 변하는 부분 (문법적인 기능 수행)
    • 예: 먹는다, 먹었다, 먹어요 — 모두 어간 먹 + 다양한 어미로 구성

활용은 어간이 어미를 취할 때 어간의 모습이 일정하다면 규칙 활용, 어간이나 어미의 모습이 변하는 불규칙 활용으로 나뉜다고 한다.

때문에 한국어에서는 영어의 Lemmatization과 달리 형태소 분석기를 사용해 단어를 “어간 + 어미”로 분리하는 방식으로 원형을 얻는다.

예를 들어 KoNLPy 라이브러리의 Okt 형태소 분석기를 살펴보면 다음과 같이 나뉘어 진다.

from konlpy.tag import Okt

okt = Okt()
print(okt.morphs("그녀는 달리고 있었다."))
# ['그녀', '는', '달리', '고', '있', '었다', '.']

print(okt.pos("그녀는 달리고 있었다."))
# [('그녀', 'Noun'), ('는', 'Josa'), ('달리', 'Verb'), ('고', 'Eomi'), ('있', 'Verb'), ('었다', 'Eomi'), ('.', 'Punctuation')]

 

즉, 형태소 분석 결과의 어간(stem) 을 이용하면 영어의 lemma(표제어) 와 유사한 역할을 할 수 있다는 것이다.

 

때문에 위에서 영어에서는 품사 정보를 통해 lemma를 찾는 과정(Lemmatization) 이 필요하지만 한국어에서는 형태소 분석기를 통해 어간을 추출하는 것이 사실상 Lemmatization의 역할을 대신한다고 볼 수 있다.