본문 바로가기
머신러닝 with Python

[머신러닝 with Python] TF-IDF를 활용한 텍스트 분류

by CodeCrafter 2024. 11. 29.
반응형

 

 텍스트 데이터 분석에서 TF-IDF(Term Frequency -Inverse Document Frequency)는 단어의 중요도를 측정하는 대표적인 방법 중 하나입니다. 

 이번에는 TF-IDF의 개념과 이를 활용하여 텍스트를 분류하는 방법에 대해서 알아보겠습니다.

 

1. TF-IDF란?

 TF-IDF는 단순한 빈도수 기반의 텍스트 표현 방식인 Bag-of-Words를 개선한 방법으로, 각 단어가 문서와 전체 코퍼스에서 얼마나 중요한지를 측정해 가중치를 부여하는 방법입니다.

 

 이를 통해 자주 등장하지만, 의미가 적은 단어(예를 들어, 조사, 접속사 등)을 걸러내고, 특정 문서에 중요한 단어에 높은 가중치를 부여하게 됩니다.

 

 TF-IDF의 구성요소는 TF(Term Frequency)와 IDF(Inverse Document Frequency)가 있습니다.

 1) TF

  - 특정 단어가 문서에서 얼마나 자주 등장하는지를 측정하는 요소입니다.

 2) IDF

  - 단어가 전체 문서에서 얼마나 드물게 나타나는지를 측정하는 요소입니다.

 

 3) TF-IDF

  - TF와 IDF를 곱하여 단어의 중요도를 전체 문서에서의 등장 빈도를 고려하여 계산합니다.

 

 이러한 TF-IDF를 활용시

  - 문서 간 단어의 중요도를 비교할 수 있고

  - 자주 등장하지만 정보량이 적은 단어 (예를 들어, 조사와 접속사 등)을 자동으로 덜 중요하게 고려할 수 있으며

  - 단순 빈도수 기반 접근 방법인 Bag-of-Wrods보다 성능이 일반적으로 우수하다고 볼 수 있습니다.

 

 

 

 이러한 TF-IDF의 작동 원리에 대해 예제를 통해서 알아보겠습니다.

 

 다음과 같이 3개의 예제 문서가 있다고 가정해보겠습니다.

 - 문서 1 : "오늘 날씨가 정말 좋네요"

 - 문서 2 : "날씨가 좋으면 기분도 좋아요"

 - 문서 3 : "정말 오늘 기분이 좋아요"

 

 먼저 위 데이터를 가지고 TF를 계산해보면 아래와 같습니다.

 

 이제 IDF를 계산해봅니다.

 

 위에서 계산된 TF와 IDF를 이제 곱해주어 최종적인 TF-IDF 스코어를 만들어 줍니다.

 

 결과를 보면 최초에는 단어의 빈도에 따라 오늘 ~ 좋네요까지가 동일한 가중치를 가졌으나, TF-IDF를 고려하니 "좋네요", "좋아요", "기분도" 라는 단어가 더 높은 가중치로 나오게 됩니다.

 

반응형

 

2. TF-IDF를 활용한 텍스트 분류

이번에는 TF-IDF를 활용해서 텍스트 분류를 해보겠습니다. 

 

이번에 사용할 데이터는 Korpora 입니다.

* Korpora 데이터는 다양한 한국어 데이터셋을 쉽고 간편하게 활용할 수 있도록 지원하는 Python 라이브러리입니다. 

* 단순화된 API를 제공하기에 활용이 매우 쉬우며, 감정 분석, 혐오발언 탐지, 문서 요약 등 다양한 테스크에 적합한 데이터셋을 제공하고 있습니다. 

https://github.com/ko-nlp/Korpora

 

GitHub - ko-nlp/Korpora: Korean corpus repository

Korean corpus repository. Contribute to ko-nlp/Korpora development by creating an account on GitHub.

github.com

 

이를 활용해서 파이썬으로 TF-IDF를 활용해 혐오 문서를 구분해보겠습니다.

 

먼저 실습에 필요한 데이터를 다운로드 받기 위해 Korpora package를 설치해줍니다.

 

!pip install Korpora

 

이후 한국어 감성 데이터셋을 로드해줍니다.

# 1. Korpora로 한국어 감성 데이터셋 로드
korpus = Korpora.load("korean_hate_speech")

 

 

이제 학습에 활용한 데이터를 불러와줍니다.

이때 test 데이터에는 혐오에 대한 label이 없으므로 train 데이터만 활용해주겠습니다.

 

train_data = korpus.train

# 학습 데이터 추출
texts = train_data.texts  # 텍스트 데이터
labels = train_data.hates  # 레이블 (hate 상태)

# 데이터프레임 생성
df = pd.DataFrame({'text': texts, 'label': labels})
print("Train DataFrame Head:")
df.head()

 

학습 데이터의 형태는 다음과 같이 text열과 label열로 구성했으며, label 열에는 hate / none / offensive로 구성되어 있습니다.

 

df.label.unique()

 

 

이제 데이터를 전처리해주겠습니다. HTML 태그를 제거하고, 특수 문자 및 숫자를 제거하며, 불용어를 제거하고, 영어는 소문자로 변환을 해줍니다.

 



# 1. 불용어 리스트 정의
stop_words = [
    # 조사
    "이", "그", "저", "것", "수", "등", "을", "를", "은", "는", "이란", "의", "에", "에서", "와", "과", "으로", "도",
    "에게", "한테", "부터", "까지", "만", "든", "걸", "게", "과의", "서", "처럼", "때문에", "거의", "없이", "위해",
    # 연결어
    "그리고", "그래서", "그러나", "하지만", "또", "또한", "그런데", "그렇지만", "이지만", "이므로", "혹은", "또는",
    # 감탄사 및 불필요한 단어
    "아", "야", "오", "어", "응", "음", "에", "여", "어라", "으", "하", "헐", "흠", "휴", "헉", "앗", "꺄", "우와",
    "정말", "너무", "진짜", "뭐", "그냥", "저기", "여기", "뭔가", "이거", "저거", "이건", "저건", "거기", "누가",
    # 시간 관련 단어
    "오늘", "내일", "어제", "지금", "그때", "이때", "저때", "항상", "가끔", "언제", "매일", "매번", "현재",
    # 기타 일반적 단어
    "하나", "둘", "셋", "넷", "다섯", "이상", "미만", "여러", "일부", "모두", "대부분", "각각", "등등", "아래",
]

# 2. 텍스트 전처리 함수 정의
def preprocess_text(text):
    # 2.1. HTML 태그 제거
    text = re.sub(r"<[^>]*>", "", text)

    # 2.2. 특수 문자 및 숫자 제거
    text = re.sub(r"[^ㄱ-ㅎ가-힣a-zA-Z\s]", "", text)

    # 2.3. 불용어 제거
    words = text.split()
    words = [word for word in words if word not in stop_words]
   
    # 2.4. 소문자 변환 (옵션)
    text = " ".join(words).lower()

    return text

# 3. 데이터 로드
train_data = korpus.train
texts = train_data.texts
labels = train_data.hates

# 4. 데이터프레임 생성
df = pd.DataFrame({'text': texts, 'label': labels})

# 5. 전처리 적용
df['processed_text'] = df['text'].apply(preprocess_text)
print("전처리된 데이터프레임:")
df.head()

 

전처리된 데이터프레임을 보시면, 전처리 전인 text에 비해서 processed_text는 불필요한 부분이 많이 사라진 것을 알 수 있습니다. 

 

 

이제 학습 데이터와 검증 데이터로 분리하고 TF-IDF 변환을 진행해주겠습니다. 이때 feature의 수는 5000개로 설정해보겠습니다.

 

# 학습 데이터와 검증 데이터로 분리
X_train, X_val, y_train, y_val = train_test_split(
    df['processed_text'], df['label'], test_size=0.3, random_state=42
)

# TF-IDF 변환
tfidf = TfidfVectorizer(max_features=5000)
X_train_tfidf = tfidf.fit_transform(X_train)
X_val_tfidf = tfidf.transform(X_val)

 

 

TF-IDF로 벡터화된 단어들의 값을 확인해보면 아래와 같습니다.

# 각 문서별 TF-IDF 값을 데이터프레임으로 변환
tfidf_df = pd.DataFrame(X_train_tfidf.toarray(), columns=feature_names)
print("\nTF-IDF 벡터화 결과:")
tfidf_df

 

 

데이터 셋은 5,527개의 샘플과 5,000개의 features를 가진 데이터 셋으로 변환이 되었습니다. 이제 이를 가지고 분류를 진행해보겠습니다. RandomForest Classifier를 활용해서 학습 후 검증을 해보면 

 

# 분류 모델 학습
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train_tfidf, y_train)

# 검증 데이터 평가
y_val_pred = clf.predict(X_val_tfidf)
print("\nClassification Report on Validation Data:")
print(classification_report(y_val, y_val_pred))

 

48% 정도의 분류 정확도를 보였습니다. 

 

 

아래 클래스별 샘플의 수를 확인해보면 none의 샘플 수가 3,486으로 전체 숫자인 5,527 중 약 44.1%를 차지합니다.

# 클래스별 샘플 수 확인
class_counts = df['label'].value_counts()
print("클래스별 샘플 수:")
print(class_counts)

 

 위 모델은 무조건 "None"이라고 외치는 모델보다는 조금은 좋은 성능이지만 신뢰하기에는 너무 낮은 정확도를 가지고 있는 모델이 나왔습니다.

 

 단순한 TF-IDF와 RandomForest의 조합으로는 잘 분류해낼 수 없는 데이터셋이라는 것을 알 수 있었으며, 보다 더 정교한 방법을 활용해야 한다는 것을 알 수 있습니다.

반응형

댓글