자연어 처리(NLP)에서 어간 추출(Stemming)과 표제어 추출(Lemmatization)은 텍스트 데이터를 정제하고 분석하기 위한 전처리 과정입니다.
이 두 기법은 단어의 형태를 변환하여 텍스트의 차원 수를 줄이고, 모델의 학습 성능을 향상하는 데 도움을 줍니다.
하지만 어간 추출과 표제어 추출은 각기 다른 방법론과 목적을 가지고 있습니다.
어간 추출은 단어의 접사를 제거하여 기본 형태인 어간을 추출하는 기법으로, 규칙 기반 알고리즘을 사용하여 처리 속도가 빠르지만 의미의 정확성을 보장하지는 않습니다.
반면, 표제어 추출은 단어의 문법적 맥락과 품사를 고려하여 사전에 나오는 기본 형태를 찾는 과정을 포함합니다. 이 과정은 보다 정교하며, 단어의 의미적 일관성을 유지합니다.
이번 포스팅에서는 어간 추출과 표제어 추출의 차이점에 대해 살펴보고, 각 기법의 특징과 예제를 통해 이를 비교해 보겠습니다.
어간 추출(stemming)
어간 추출은 단어의 접사(어미나 접두사)를 제거하여 단어의 기본 형태인 어간을 추출하는 기법입니다.
어간 추출은 규칙 기반의 알고리즘을 사용하여 단어의 어미를 단순히 잘라내기 때문에 처리 속도가 빠르고 간단한 반면, 단어의 정확한 의미를 유지하지는 않습니다.
어간 추출 (stemming)의 특징
1. 어미 제거: 단순한 규칙에 따라 어미를 제거하여 단어의 어간을 찾습니다.
2. 의미 무시: 어간 추출 과정에서 단어의 의미를 고려하지 않으므로, 때로는 의미가 손상될 수 있습니다. 예를 들어, 'relational'은 'relate'와 관련이 있지만 어간 추출 후 'relat'이라는 의미가 없는 형태가 될 수 있습니다.
3. 속도: 어간 추출은 매우 빠르게 처리됩니다.
4. 알고리즘: 가장 많이 사용되는 알고리즘으로는 Porter Stemmer, Snowball Stemmer, Lancaster Stemmer 등이 있습니다.
어간 추출 (stemming)의 예시
- `argue`, `argued`, `arguing` → 모두 `argu`로 정규화됩니다.
- `running`, `run`, `ran` → 모두 `run`로 정규화됩니다.
어간 추출 (stemming)의 장점
- 단순하고 속도가 빠릅니다.
- 규칙 기반으로 구현이 쉬움.
어간 추출 (stemming)의 단점
- 의미론적인 정확성이 떨어집니다.
- 같은 어근에서 온 단어라도 정확한 형태로 변환되지 않을 수 있습니다.
어간 추출 (stemming)의 예시 코드
어간 추출을 실행해보는 코드입니다.
# NLTK 라이브러리 설치
!pip install nltk
# 필요한 라이브러리 로드
import nltk
from nltk.stem import PorterStemmer, WordNetLemmatizer
from nltk.corpus import wordnet
nltk.download('wordnet')
nltk.download('omw-1.4')
# 어간 추출기 및 표제어 추출기 초기화
stemmer = PorterStemmer()
print(stemmer.stem('running'), stemmer.stem('ran'))
print(stemmer.stem('standardizes'), stemmer.stem('standardization'))
print(stemmer.stem('national'), stemmer.stem('nation'))
print(stemmer.stem('tribalical'), stemmer.stem('tribalicalized')) # 사전에 없는 단어
> run ran
> standard standard
> nation nation
> tribal tribalic
위 코드에서 `PorterStemmer`를 사용하여 어간 추출을 실행한 결과는 다음과 같은 특성을 보입니다.
`tribalical`, `tribalicalized` -> `tribal`, `tribalic`
- `tribalical`은 사전에 없는 단어임에도 규칙적으로 접미사 'ical'이 제거되어 `tribal`로 변환됩니다.
- `tribalicalized`는 복합적인 접미사가 붙어있지만, 어간 추출기는 규칙적으로 'ized'를 제거하고 `tribalic`을 반환합니다. 이 결과는 사전에 없는 단어일지라도 추출 규칙을 그대로 적용한 결과입니다.
즉, Porter Stemmer는 단어의 형태를 규칙에 따라 단순화하여 어간을 추출하는 도구입니다.
의미를 고려하지 않고 어미만 제거하기 때문에 'ran'과 같이 이미 간단한 형태인 단어는 그대로 두며, 복잡한 형태의 단어는 접미사 제거를 통해 어간을 추출합니다.
사전에 없는 단어도 규칙에 맞게 처리하지만, 의미와 상관없이 규칙적으로 변환되는 경우가 있습니다.
표제어 추출 (Lemmatization)
표제어 추출은 단어의 문법적 맥락(예: 품사)을 고려하여 사전에 나오는 기본 형태(표제어, Lemma)를 찾는 과정입니다.
표제어 추출은 단순한 어미 제거가 아니라, 단어의 의미적 일관성을 유지하기 위한 정교한 과정을 포함합니다.
이 과정에서는 단어의 품사(POS, Part of Speech)를 파악하고, 그에 맞는 정확한 기본형으로 변환합니다.
표제어 추출 (Lemmatization)의 특징
1. 품사 분석 표제어 추출은 단어의 품사 정보를 고려하여 단어를 변환합니다. 예를 들어, 동사, 명사, 형용사 등의 구분을 통해 단어의 의미를 더 정확하게 추출할 수 있습니다.
2. 사전 기반 처리: 사전에 있는 표제어로 변환하기 때문에 의미를 잃지 않으며, 단어가 사용된 문맥에 따라 다르게 처리될 수 있습니다.
3. 정확성: 표제어 추출은 단어의 의미적 일관성을 유지하여 정확한 결과를 제공합니다.
표제어 추출 (Lemmatization)의 예시
- `running` → `run` (동사로 사용된 경우)
- `better` → `good` (형용사의 비교급인 경우)
- `cars` → `car` (복수형에서 단수형으로)
표제어 추출 (Lemmatization)의 장점
- 단어의 의미를 유지하면서 정규화할 수 있습니다.
- 같은 어근에서 파생된 여러 단어가 문맥에 맞게 일관성 있게 변환됩니다.
표제어 추출 (Lemmatization)의 단점
- 품사 태깅과 같은 추가적인 처리 과정이 필요하므로 속도가 상대적으로 느립니다.
- 구현이 어간 추출에 비해 복잡합니다.
표제어 추출 (Lemmatization)의 예시 코드
표제어 추출을 실행해보는 코드입니다.
from nltk.stem import WordNetLemmatizer
lemma = WordNetLemmatizer()
print(lemma.lemmatize('running'), lemma.lemmatize('ran'))
print(lemma.lemmatize('standardizes'), lemma.lemmatize('standardization'))
print(lemma.lemmatize('national'), lemma.lemmatize('nation'))
print(lemma.lemmatize('tribalical'), lemma.lemmatize('tribalicalized')) # 사전에 없는 단어
> running ran
> standardizes standardization
> national nation
> tribalical tribalicalized
결과를 보면, 원본 단어와 표제어 추출 단어가 동일하네요.🤔
`WordNetLemmatizer`를 사용할 때 품사 정보를 제공하지 않으면, 기본적으로 명사(noun)로 간주하여 표제어 추출을 수행합니다. 이 때문에 문맥에 맞는 품사가 반영되지 않으면 부정확한 결과가 나올 수 있습니다.
예를 들어,
`running`과 `ran`은 둘 다 동사인데, 품사를 지정하지 않으면 동사로 변환되지 않고 명사로 처리됩니다. 이 경우 `running`은 그대로 반환되고, `ran`도 명사 형태로 처리되어 변환되지 않습니다.
`standardizes`와 `standardization` 역시 동사와 명사로 다른 품사를 가지지만, 표제어 추출에서 품사를 지정하지 않으면 올바르게 처리되지 않습니다.
이 경우 모두 기본 품사인 명사로 처리되기 때문에, 문맥에 맞는 정확한 변환이 이루어지지 않습니다.
이를 해결하기 위해서는 품사를 명시적으로 전달하면 정확한 표제어 추출이 가능합니다.
아래는 정확한 품사 정보를 함께 제공한 표제어 추출 코드입니다.
from nltk.stem import WordNetLemmatizer
lemma = WordNetLemmatizer()
# 품사 정보를 추가하여 표제어 추출 수행
# 'v'는 동사, 'n'은 명사, 'a'는 형용사를 의미
print(lemma.lemmatize('running', pos='v'), lemma.lemmatize('ran', pos='v')) # 'run', 'run'
print(lemma.lemmatize('standardizes', pos='v'), lemma.lemmatize('standardization', pos='n')) # 'standardize', 'standardization'
print(lemma.lemmatize('national', pos='a'), lemma.lemmatize('nation', pos='n')) # 'national', 'nation'
print(lemma.lemmatize('tribalical', pos='a'), lemma.lemmatize('tribalicalized', pos='v')) # 'tribalical', 'tribalicalize'
> run run
> standardize standardization
> national nation
> tribalical tribalicalized
이처럼 정확한 품사 정보를 함께 제공해야 표제어 추출이 더 정확하게 이루어집니다.
하지만, 결과를 보시면 `national`, `nation`, `tribalical`, `tribalicalized` 4개의 단어는 그대로 출력되었습니다.
왜일까요?
`WordNetLemmatizer`가 동작하는 방식은 WordNet 사전을 기반으로 하기 때문에, 사전에 없는 단어이거나 이미 사전에서 표제어로 간주되는 단어는 변환되지 않습니다.
위 네 단어가 변환되지 않은 이유를 자세히 설명하자면
1. `national` → `national` (형용사로 처리됨)
- `national`은 이미 표제어 자체입니다. 즉, 이 단어는 WordNet에서 표제어로 간주되므로 변환할 필요가 없습니다.
- `WordNetLemmatizer`는 형용사일 경우 원래의 단어가 이미 표제어일 때, 변환하지 않고 그대로 반환합니다.
2. `nation` → `nation` (명사로 처리됨)
- `nation`은 역시 명사로서 기본 표제어입니다. WordNet에서 이미 표제어로 간주되는 단어는 변환되지 않습니다.
3. `tribalical` → `tribalical` (형용사로 처리됨)
- `tribalical`은 사전에 없는 단어입니다. WordNet 사전에서 단어를 찾을 수 없으면 표제어 추출이 이루어지지 않고, 그대로 반환됩니다.
4. `tribalicalized` → `tribalicalized` (동사로 처리됨)
- `tribalicalized` 역시 WordNet 사전에 없는 단어입니다. 따라서 표제어 추출이 불가능하며, 그대로 반환됩니다.
따라서 `national`과 `nation`은 이미 표제어이기 때문에 변환되지 않았고,
`tribalical`과 `tribalicalized`는 WordNet 사전에 없기 때문에 변환되지 않았습니다.
어간 추출과 표제어 추출의 차이점
어간 추출과 표제어 추출 코드 실습
앞서 제공한 코드에서 어간 추출과 표제어 추출을 실행해 보는 코드를 확장하여, 품사 태깅을 포함한 표제어 추출 예시입니다.
# NLTK 라이브러리 설치
# !pip install nltk
# 필요한 라이브러리 로드
import nltk
from nltk.stem import PorterStemmer, WordNetLemmatizer
from nltk.corpus import wordnet
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
# NLTK 데이터 다운로드 (첫 실행 시)
# nltk.download('punkt')
# nltk.download('wordnet')
# nltk.download('averaged_perceptron_tagger')
# nltk.download('stopwords')
# 어간 추출기 및 표제어 추출기 초기화
stemmer = PorterStemmer() # 어간 추출
lemmatizer = WordNetLemmatizer() # 표제어 추출
# 품사 태깅을 위한 함수
# 품사 태깅 결과를 WordNet에서 사용하는 품사로 변환하는 함수
# (표제어는 WordNet을 기반으로 변환하기 때문임)
def get_wordnet_pos(tag):
# NLTK의 품사 태그가 'J'로 시작하면 형용사(Adjective)로 변환
if tag.startswith('J'):
return wordnet.ADJ
elif tag.startswith('V'): # 'V'로 시작하면 동사(Verb)로 변환
return wordnet.VERB
elif tag.startswith('N'): # 'N'으로 시작하면 명사(Noun)로 변환
return wordnet.NOUN
elif tag.startswith('R'): # 'R'로 시작하면 부사(Adverb)로 변환
return wordnet.ADV
else: # 해당하지 않는 경우(None), WordNet에서는 품사 정보를 사용할 수 없음
return None
# 샘플 텍스트
sentence = "The striped bats are hanging on their feet for best."
# 1️⃣ 토큰화
words = word_tokenize(sentence)
# 2️⃣ 불용어 제거
stop_words = set(stopwords.words('english'))
filtered_words = [word for word in words if word.lower() not in stop_words]
# 3️⃣ 어간 추출 및 표제어 추출
# 어간 추출 결과
print("어간 추출 (Stemming) 결과:")
for word in filtered_words:
print(f"{word} -> {stemmer.stem(word)}")
# 표제어 추출 결과 (품사 태깅 포함)
print("\n표제어 추출 (Lemmatization) 결과:")
tagged_words = nltk.pos_tag(filtered_words) # 품사 태깅
for word, tag in tagged_words:
# get_wordnet_pos(tag)의 결과가 None이라면, wordnet.NOUN을 사용하여 안전하게 "명사(NOUN)"로 처리
wordnet_pos = get_wordnet_pos(tag) or wordnet.NOUN # 품사에 맞게 표제어 추출
print(f"{word} ({tag}) -> {lemmatizer.lemmatize(word, pos=wordnet_pos)}")
> 어간 추출 (Stemming) 결과:
striped -> stripe
bats -> bat
hanging -> hang
feet -> feet
best -> best
. -> .
> 표제어 추출 (Lemmatization) 결과:
striped (VBN) -> strip
bats (NNS) -> bat
hanging (VBG) -> hang
feet (NNS) -> foot
best (RB) -> best
. (.) -> .
그런데 위 코드에서 품사 태깅 결과를 WordNet에서 사용하는 품사로 변환하는 이유가 뭘까요?
-> def get_wordnet_pos(tag)
표제어 추출(Lemmatization)에서 사용하는 WordNet은 품사를 처리할 때 NLTK와는 다른 형식을 사용하기 때문에, 변환이 필요합니다.
NLTK와 WordNet의 차이점
NLTK는 품사 태깅을 할 때 좀 더 세분화된 태그를 사용합니다. 예를 들어, NLTK에서는 명사를 표현할 때 'NN', 'NNS', 'NNP', 'NNPS' 같은 태그를 사용하고, 형용사는 'JJ', 'JJR', 'JJS'처럼 다양하게 표현됩니다.
반면, WordNet은 품사를 조금 더 단순하게 처리합니다. WordNet에서 이해할 수 있는 품사는 크게 4가지입니다.
- 명사 (Noun) → `wordnet.NOUN`
- 동사 (Verb) → `wordnet.VERB`
- 형용사 (Adjective) → `wordnet.ADJ`
- 부사 (Adverb) → `wordnet.ADV`
품사 변환 이유
NLTK의 태그는 WordNet이 이해할 수 있는 태그와 다르기 때문에, WordNet이 표제어 추출을 할 수 있도록 NLTK의 세분화된 태그를 WordNet에서 사용하는 태그로 변환해야 합니다. 그렇지 않으면, WordNet이 표제어 추출 과정에서 올바르게 작동하지 않습니다.
예를 들어
- NLTK에서 'NN'은 명사(Noun)를 의미하지만, WordNet은 `wordnet.NOUN`이라는 태그만 이해할 수 있습니다.
- NLTK에서 'JJ'는 형용사(Adjective)를 의미하지만, WordNet은 `wordnet.ADJ`만 처리할 수 있습니다.
따라서, 변환 함수 `get_wordnet_pos()`가 필요한 이유는 NLTK의 태그를 WordNet에서 이해할 수 있는 간단한 품사 태그로 바꿔주기 위함입니다.