🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

뉴스 기사 진위 판별 모델 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 17. · 8 Views

뉴스 기사 진위 판별 모델 완벽 가이드

머신러닝으로 가짜 뉴스를 판별하는 방법을 배웁니다. Python과 scikit-learn을 사용하여 텍스트 분류 모델을 구축하고, 실제 뉴스 기사의 진위를 예측하는 실전 프로젝트를 단계별로 진행합니다.


목차

  1. 가짜_뉴스_데이터셋_소개
  2. 텍스트_데이터_탐색
  3. 전처리_파이프라인_구축
  4. TF-IDF_벡터화
  5. 여러_모델_학습_및_비교
  6. 최적_모델_선택_및_평가
  7. 새_뉴스_기사_예측_테스트

1. 가짜 뉴스 데이터셋 소개

신입 개발자 김개발 씨가 데이터 분석팀에 배치되었습니다. 첫 프로젝트로 "뉴스 기사 진위 판별 시스템"을 만들어야 한다는 미션을 받았습니다.

막막하게 느껴지던 그때, 팀장님이 "먼저 데이터부터 살펴보세요"라고 조언해주셨습니다.

가짜 뉴스 데이터셋은 실제 뉴스와 가짜 뉴스를 구분하는 레이블이 붙어있는 텍스트 모음입니다. 마치 시험 문제집처럼, 정답이 함께 제공되어 모델이 학습할 수 있습니다.

이 데이터를 통해 컴퓨터가 진짜와 가짜를 구분하는 패턴을 배우게 됩니다.

다음 코드를 살펴봅시다.

import pandas as pd
from sklearn.model_selection import train_test_split

# 가짜 뉴스 데이터셋 로드
# label: 0=진짜 뉴스, 1=가짜 뉴스
df = pd.read_csv('fake_news.csv')

# 데이터 구조 확인
print(f"전체 데이터: {len(df)}개")
print(f"진짜 뉴스: {len(df[df['label']==0])}개")
print(f"가짜 뉴스: {len(df[df['label']==1])}개")

# 학습용과 테스트용으로 분리 (8:2 비율)
X_train, X_test, y_train, y_test = train_test_split(
    df['text'], df['label'], test_size=0.2, random_state=42
)

김개발 씨는 회사 서버에 접속해서 데이터 파일을 다운로드했습니다. 파일을 열어보니 수천 개의 뉴스 기사가 텍스트로 저장되어 있고, 각각 진짜인지 가짜인지 표시되어 있었습니다.

"이걸로 어떻게 가짜 뉴스를 구분하는 거지?" 처음엔 막막했지만, 팀장님의 설명을 듣고 나니 이해가 되기 시작했습니다. 데이터셋이란 정확히 무엇일까요?

쉽게 비유하자면, 데이터셋은 마치 문제집과 같습니다. 수험생이 문제를 풀고 정답을 확인하면서 실력을 키우듯이, 컴퓨터도 이 데이터를 보고 "이런 패턴은 가짜 뉴스구나, 저런 패턴은 진짜 뉴스구나"를 학습합니다.

정답이 함께 제공되기 때문에 스스로 판단 기준을 만들어갈 수 있습니다. 왜 데이터를 나누어야 할까요?

초보 개발자들이 흔히 하는 실수가 있습니다. 모든 데이터를 한꺼번에 학습에 사용하는 것입니다.

이렇게 하면 큰 문제가 생깁니다. 마치 시험 문제를 미리 외워버린 학생처럼, 모델이 데이터를 암기만 하고 진짜 실력이 늘지 않는 것입니다.

그래서 우리는 데이터를 두 부분으로 나눕니다. 첫 번째는 학습용 데이터입니다.

전체의 약 80퍼센트를 차지하며, 모델이 패턴을 배우는 데 사용됩니다. 두 번째는 테스트용 데이터입니다.

나머지 20퍼센트로, 모델이 한 번도 본 적 없는 새로운 데이터입니다. 이것으로 실제 성능을 평가합니다.

코드를 하나씩 살펴보겠습니다. 먼저 pandas 라이브러리로 CSV 파일을 읽어옵니다.

이 파일에는 뉴스 기사 텍스트와 레이블이 들어있습니다. 레이블은 0과 1로 표시되는데, 0은 진짜 뉴스, 1은 가짜 뉴스를 의미합니다.

간단한 숫자 코드로 컴퓨터가 이해하기 쉽게 만든 것입니다. 다음으로 데이터 분포를 확인합니다.

진짜 뉴스와 가짜 뉴스가 얼마나 있는지 세어봅니다. 만약 한쪽이 너무 많으면 모델이 편향될 수 있습니다.

예를 들어 진짜 뉴스가 90퍼센트라면, 모델이 "그냥 다 진짜라고 하면 되겠네"라고 배울 수 있습니다. 마지막으로 train_test_split 함수를 사용합니다.

이 함수는 데이터를 무작위로 섞어서 학습용과 테스트용으로 나눠줍니다. test_size=0.2는 20퍼센트를 테스트용으로 쓰겠다는 의미입니다.

random_state=42는 매번 같은 방식으로 섞이도록 하는 설정입니다. 실무에서는 어떻게 활용할까요?

김개발 씨가 속한 회사는 뉴스 플랫폼을 운영합니다. 매일 수천 개의 기사가 올라오는데, 일일이 사람이 확인할 수는 없습니다.

이 데이터셋으로 모델을 학습시키면, 새로 올라오는 기사를 자동으로 검증할 수 있습니다. 의심스러운 기사는 따로 표시해서 편집팀이 검토하도록 합니다.

주의할 점도 있습니다. 데이터의 품질이 무엇보다 중요합니다.

만약 레이블이 잘못 붙어있다면 모델도 잘못 배웁니다. 가짜 뉴스를 진짜로 표시한 데이터가 있다면, 모델은 그것을 진짜라고 믿게 됩니다.

따라서 데이터를 수집할 때 신뢰할 수 있는 출처를 사용해야 합니다. 팀장님이 김개발 씨에게 말했습니다.

"데이터가 전부예요. 좋은 데이터만 있다면 모델은 저절로 좋아집니다." 김개발 씨는 데이터를 꼼꼼히 살펴보며 이상한 부분이 없는지 확인했습니다.

이제 본격적인 분석을 시작할 준비가 되었습니다.

실전 팁

💡 - 데이터 분포를 항상 확인하세요. 한쪽으로 치우치면 모델이 편향됩니다.

  • random_state를 고정하면 실험을 재현할 수 있어 디버깅이 쉬워집니다.
  • 테스트 데이터는 절대 학습에 사용하지 마세요. 성능 평가가 부정확해집니다.

2. 텍스트 데이터 탐색

데이터를 불러온 김개발 씨는 실제 기사 내용을 읽어보기 시작했습니다. 그런데 뭔가 이상했습니다.

진짜 뉴스와 가짜 뉴스가 겉으로 보기엔 비슷해 보였습니다. "어떤 차이가 있는 걸까요?" 선배 박시니어 씨에게 물었더니, "데이터를 시각화해보면 패턴이 보일 거예요"라는 답이 돌아왔습니다.

텍스트 데이터 탐색은 기사의 길이, 단어 빈도, 특징적인 표현을 분석하는 과정입니다. 마치 탐정이 증거를 모으듯이, 진짜와 가짜를 구분하는 단서를 찾아냅니다.

이를 통해 어떤 특징이 중요한지 미리 파악할 수 있습니다.

다음 코드를 살펴봅시다.

import matplotlib.pyplot as plt
from collections import Counter
import re

# 텍스트 길이 비교
real_lengths = [len(text.split()) for text in df[df['label']==0]['text']]
fake_lengths = [len(text.split()) for text in df[df['label']==1]['text']]

# 시각화: 진짜 vs 가짜 뉴스 길이 분포
plt.hist(real_lengths, alpha=0.5, label='진짜 뉴스', bins=50)
plt.hist(fake_lengths, alpha=0.5, label='가짜 뉴스', bins=50)
plt.xlabel('단어 수')
plt.ylabel('기사 수')
plt.legend()
plt.savefig('news_length_distribution.png')

# 자주 등장하는 단어 확인
all_words = ' '.join(df[df['label']==1]['text']).lower()
words = re.findall(r'\w+', all_words)
common = Counter(words).most_common(10)
print("가짜 뉴스에 자주 등장하는 단어:", common)

박시니어 씨가 김개발 씨 옆에 앉아 함께 코드를 작성하기 시작했습니다. "먼저 기사 길이부터 비교해볼까요?" 두 사람은 진짜 뉴스와 가짜 뉴스의 단어 수를 세어봤습니다.

그래프를 그려보니 신기한 패턴이 나타났습니다. 텍스트 탐색이란 무엇일까요?

쉽게 비유하자면, 텍스트 탐색은 마치 범죄 현장을 조사하는 것과 같습니다. 탐정이 지문, 발자국, 증거물을 꼼꼼히 살피듯이, 우리도 텍스트에서 단서를 찾아냅니다.

기사의 길이는 어떤가? 어떤 단어를 자주 쓰는가?

문장 구조는 어떤가? 이런 질문들에 답하면서 패턴을 발견합니다.

왜 이런 탐색이 필요할까요? 모델을 만들기 전에 데이터의 특성을 알아야 합니다.

만약 가짜 뉴스가 대체로 짧다면, 길이가 중요한 특징이 될 수 있습니다. 특정 단어가 가짜 뉴스에만 자주 나온다면, 그것도 유용한 신호입니다.

이런 정보를 미리 알면 더 효과적인 모델을 설계할 수 있습니다. 데이터를 보지 않고 모델부터 만드는 것은 지도 없이 여행을 떠나는 것과 같습니다.

코드를 단계별로 살펴보겠습니다. 먼저 각 기사를 공백 기준으로 나누어 단어 수를 셉니다.

split() 함수가 이 역할을 합니다. 진짜 뉴스와 가짜 뉴스를 각각 따로 세어서 리스트에 담습니다.

이렇게 하면 두 그룹의 길이 분포를 비교할 수 있습니다. 다음으로 히스토그램을 그립니다.

plt.hist() 함수는 데이터의 분포를 막대그래프로 보여줍니다. alpha=0.5 설정은 그래프를 반투명하게 만들어 두 그래프가 겹쳐도 모두 볼 수 있게 합니다.

bins=50은 막대를 50개로 나누라는 의미입니다. 마지막으로 자주 등장하는 단어를 찾습니다.

먼저 모든 텍스트를 소문자로 바꿔서 "News"와 "news"를 같은 단어로 취급합니다. 정규표현식 \w+로 단어만 추출하고, Counter로 빈도를 세어 상위 10개를 뽑습니다.

실제로 어떤 패턴이 발견될까요? 김개발 씨가 그래프를 보고 놀랐습니다.

가짜 뉴스는 평균적으로 더 짧았습니다. 또한 "놀라운", "충격적", "비밀" 같은 감정적인 단어가 자주 등장했습니다.

반면 진짜 뉴스는 구체적인 수치나 출처를 더 많이 언급했습니다. 박시니어 씨가 설명했습니다.

"가짜 뉴스는 사람들의 관심을 끌기 위해 자극적인 표현을 많이 써요. 반면 진짜 뉴스는 사실을 전달하는 데 집중하죠." 주의할 점이 있습니다.

단순히 길이만 보고 판단하면 안 됩니다. 짧은 진짜 뉴스도 있고, 긴 가짜 뉴스도 있습니다.

중요한 것은 여러 특징을 종합적으로 보는 것입니다. 하나의 신호에만 의존하면 오판할 수 있습니다.

또한 데이터셋마다 특성이 다릅니다. 어떤 데이터에서는 길이가 중요하지 않을 수도 있습니다.

따라서 항상 자신의 데이터를 직접 탐색해야 합니다. 김개발 씨는 노트에 발견한 패턴들을 적어두었습니다.

이제 이 인사이트를 바탕으로 모델을 설계할 준비가 되었습니다.

실전 팁

💡 - 시각화는 숫자보다 직관적입니다. 항상 그래프로 확인하세요.

  • 진짜와 가짜를 구분하는 특징적인 단어를 메모해두면 나중에 유용합니다.
  • 데이터 탐색에 충분한 시간을 투자하세요. 좋은 인사이트가 좋은 모델을 만듭니다.

3. 전처리 파이프라인 구축

김개발 씨가 데이터를 모델에 바로 넣으려고 하자, 박시니어 씨가 손을 저었습니다. "잠깐, 그대로 넣으면 안 돼요.

먼저 청소를 해야죠." 텍스트 데이터는 정제되지 않은 날것 그대로입니다. HTML 태그, 특수문자, 불필요한 공백 등이 섞여있어 모델이 학습하기 어렵습니다.

전처리 파이프라인은 텍스트를 모델이 이해할 수 있는 깨끗한 형태로 바꾸는 과정입니다. 마치 요리 전에 재료를 손질하듯이, 불필요한 부분을 제거하고 표준화합니다.

이 단계를 거치면 모델의 성능이 크게 향상됩니다.

다음 코드를 살펴봅시다.

import re
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
import nltk

# 필요한 NLTK 데이터 다운로드
nltk.download('stopwords', quiet=True)

def preprocess_text(text):
    # 1. 소문자 변환
    text = text.lower()

    # 2. HTML 태그와 URL 제거
    text = re.sub(r'<.*?>', '', text)
    text = re.sub(r'http\S+|www\S+', '', text)

    # 3. 특수문자 제거 (문자와 공백만 유지)
    text = re.sub(r'[^a-z\s]', '', text)

    # 4. 불용어 제거 (the, is, at 같은 의미 없는 단어)
    stop_words = set(stopwords.words('english'))
    words = [w for w in text.split() if w not in stop_words]

    # 5. 어간 추출 (running -> run)
    stemmer = PorterStemmer()
    words = [stemmer.stem(w) for w in words]

    return ' '.join(words)

# 전체 데이터에 적용
X_train_clean = X_train.apply(preprocess_text)
X_test_clean = X_test.apply(preprocess_text)

박시니어 씨가 화면에 원본 텍스트를 띄워놓고 말했습니다. "이거 보세요.

HTML 태그도 있고, 링크도 있고, 느낌표도 엄청 많죠? 이런 잡음이 모델을 방해합니다." 전처리가 왜 필요할까요?

쉽게 비유하자면, 전처리는 마치 채소를 다듬는 것과 같습니다. 시금치를 요리하기 전에 흙을 씻어내고, 누런 잎을 떼어내고, 먹기 좋은 크기로 자르듯이, 텍스트도 불필요한 부분을 제거하고 표준화해야 합니다.

날것 그대로 쓰면 맛이 없고 소화도 안 되는 것처럼, 정제되지 않은 데이터는 모델이 학습하기 어렵습니다. 전처리를 하지 않으면 어떤 문제가 생길까요?

예를 들어 "Running"과 "running"과 "run"을 모델이 서로 다른 단어로 인식합니다. 실제로는 같은 의미인데 말이죠.

또한 "!!!"나 "???" 같은 기호는 의미 있는 정보가 아니지만 공간만 차지합니다. 이런 잡음이 쌓이면 모델이 진짜 중요한 패턴을 찾기 어려워집니다.

그래서 전처리 파이프라인이 등장했습니다. 파이프라인이란 여러 단계의 처리 과정을 순서대로 연결한 것입니다.

마치 공장의 조립 라인처럼, 텍스트가 한 단계씩 거치면서 점점 깨끗해집니다. 우리의 파이프라인은 다섯 단계로 구성됩니다.

첫 번째 단계는 소문자 변환입니다. "Apple"과 "apple"을 같은 단어로 취급하기 위해 모두 소문자로 바꿉니다.

간단하지만 효과가 큽니다. 두 번째 단계는 HTML 태그와 URL 제거입니다.

뉴스 기사를 웹에서 크롤링하면 <div>, <p> 같은 태그가 딸려옵니다. 정규표현식 <.*?>로 이런 태그를 모두 지웁니다.

URL도 마찬가지로 httpwww로 시작하는 부분을 제거합니다. 세 번째 단계는 특수문자 제거입니다.

[^a-z\s] 정규표현식은 영문 소문자와 공백만 남기고 나머지를 지웁니다. 숫자, 문장부호, 이모지 등이 모두 사라집니다.

네 번째 단계는 불용어 제거입니다. 불용어란 "the", "is", "at" 같은 문법적인 단어로, 의미 전달에 크게 기여하지 않습니다.

NLTK 라이브러리가 제공하는 불용어 목록을 사용해 이런 단어들을 걸러냅니다. 텍스트 길이가 크게 줄어들어 처리 속도가 빨라집니다.

다섯 번째 단계는 어간 추출입니다. 어간 추출은 단어의 변형을 원형으로 되돌리는 과정입니다.

"running", "runs", "ran"을 모두 "run"으로 바꿉니다. PorterStemmer가 이 작업을 자동으로 수행합니다.

이렇게 하면 같은 의미의 단어들을 하나로 통합할 수 있습니다. 실무에서는 어떻게 활용할까요?

김개발 씨가 전처리 전후의 텍스트를 비교해봤습니다. 원래 500단어였던 기사가 300단어로 줄었습니다.

하지만 핵심 의미는 그대로였습니다. "이렇게 하면 학습 속도도 빠르고, 정확도도 올라가겠네요!" 박시니어 씨가 고개를 끄덕였습니다.

주의할 점도 있습니다. 너무 과도하게 전처리하면 오히려 정보가 손실될 수 있습니다.

예를 들어 숫자를 모두 지우면 "2020년 연봉 상승률 5%"라는 중요한 정보가 사라집니다. 따라서 데이터의 특성에 맞게 전처리 수준을 조절해야 합니다.

또한 전처리는 학습 데이터와 테스트 데이터에 똑같이 적용해야 합니다. 학습할 때는 소문자로 바꾸고, 예측할 때는 대문자 그대로 쓰면 모델이 헷갈립니다.

김개발 씨는 전처리된 데이터를 파일로 저장했습니다. 이제 본격적인 모델 학습을 시작할 차례입니다.

실전 팁

💡 - 전처리 전후의 샘플을 출력해서 제대로 작동하는지 확인하세요.

  • 불용어 목록은 도메인에 맞게 커스터마이징할 수 있습니다.
  • 어간 추출보다 정교한 표제어 추출(Lemmatization)도 있지만, 속도가 느립니다.

4. TF-IDF 벡터화

깨끗하게 정제된 텍스트를 보며 김개발 씨가 물었습니다. "그런데 컴퓨터는 어떻게 텍스트를 이해하나요?

문자는 숫자가 아닌데요." 박시니어 씨가 웃으며 대답했습니다. "바로 그 문제를 해결하는 게 벡터화예요.

텍스트를 숫자로 바꾸는 거죠."

TF-IDF 벡터화는 텍스트를 숫자 벡터로 변환하는 기법입니다. 단어의 중요도를 계산해서 각 문서를 숫자 배열로 표현합니다.

마치 지문을 숫자 코드로 변환하듯이, 텍스트의 특징을 수치화해서 컴퓨터가 계산할 수 있게 만듭니다.

다음 코드를 살펴봅시다.

from sklearn.feature_extraction.text import TfidfVectorizer

# TF-IDF 벡터라이저 생성
# max_features: 가장 중요한 상위 5000개 단어만 사용
# min_df: 최소 5개 문서에 등장해야 포함
# max_df: 전체 문서의 70% 이상에 나오면 제외 (너무 흔한 단어)
vectorizer = TfidfVectorizer(
    max_features=5000,
    min_df=5,
    max_df=0.7,
    ngram_range=(1, 2)  # 단어 1개 & 2개 조합 모두 고려
)

# 학습 데이터로 벡터라이저 학습 및 변환
X_train_tfidf = vectorizer.fit_transform(X_train_clean)

# 테스트 데이터는 변환만 (fit 하지 않음!)
X_test_tfidf = vectorizer.transform(X_test_clean)

# 결과 확인
print(f"벡터 크기: {X_train_tfidf.shape}")
print(f"샘플 하나의 벡터 차원: {X_train_tfidf.shape[1]}")

박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "컴퓨터는 숫자만 이해할 수 있어요.

그래서 텍스트를 숫자로 바꿔야 합니다. 하지만 단순히 'apple=1, banana=2' 이렇게 매기면 안 돼요.

숫자의 크기가 의미를 갖게 되니까요." TF-IDF란 정확히 무엇일까요? 쉽게 비유하자면, TF-IDF는 마치 단어의 중요도를 매기는 채점 시스템입니다.

어떤 단어가 특정 문서에서 자주 나오지만 다른 문서에는 별로 없다면, 그 단어는 그 문서를 특징짓는 중요한 단어입니다. 반대로 모든 문서에 골고루 나오는 단어는 별로 중요하지 않습니다.

TF-IDF는 두 가지 값을 곱해서 만듭니다. 첫 번째는 TF입니다.

Term Frequency의 약자로, 단어가 문서 안에서 얼마나 자주 나오는지를 나타냅니다. "경제"라는 단어가 한 기사에 10번 나왔다면, 이 기사는 경제 관련일 가능성이 높습니다.

두 번째는 IDF입니다. Inverse Document Frequency의 약자로, 단어가 전체 문서에서 얼마나 희귀한지를 나타냅니다.

"the"는 모든 문서에 나오므로 IDF가 낮고, "블록체인"은 특정 문서에만 나오므로 IDF가 높습니다. 이 둘을 곱하면 무슨 일이 일어날까요?

한 문서에 자주 나오면서도 전체적으로는 희귀한 단어가 높은 점수를 받습니다. 바로 그 문서를 대표하는 키워드가 되는 것입니다.

반대로 모든 문서에 흔하게 나오는 단어는 점수가 낮아집니다. 코드를 단계별로 살펴보겠습니다.

TfidfVectorizer를 만들 때 여러 옵션을 설정합니다. max_features=5000은 중요도가 높은 상위 5000개 단어만 사용하겠다는 의미입니다.

모든 단어를 쓰면 차원이 너무 커져서 계산이 느려집니다. min_df=5는 최소 5개 문서에 등장해야 포함한다는 조건입니다.

한 문서에만 나오는 오타나 희귀 단어는 잡음일 가능성이 높으므로 제외합니다. max_df=0.7은 전체 문서의 70퍼센트 이상에 나오면 제외한다는 의미입니다.

너무 흔한 단어는 변별력이 없기 때문입니다. ngram_range=(1, 2)는 흥미로운 설정입니다.

단어 1개(유니그램)뿐만 아니라 연속된 2개 단어 조합(바이그램)도 고려합니다. "fake news"처럼 두 단어가 함께 쓰일 때 의미가 강해지는 경우가 많기 때문입니다.

fit_transform과 transform의 차이를 주의하세요. 학습 데이터에는 fit_transform을 씁니다.

이것은 데이터를 분석해서 어떤 단어가 중요한지 학습하고, 동시에 벡터로 변환합니다. 하지만 테스트 데이터에는 transform만 씁니다.

이미 학습된 기준을 그대로 적용만 하는 것입니다. 만약 테스트 데이터에 fit을 다시 하면 큰 문제가 생깁니다.

학습 때와 다른 기준으로 벡터를 만들게 되어 모델이 제대로 작동하지 않습니다. 결과를 보면 놀라운 일이 일어났습니다.

"This is a fake news article"이라는 텍스트가 [0.0, 0.5, 0.0, 0.8, ...] 같은 5000차원 벡터로 바뀌었습니다. 각 숫자는 해당 단어의 TF-IDF 값입니다.

컴퓨터는 이제 이 숫자들을 계산할 수 있습니다. 실무에서는 어떻게 활용할까요?

김개발 씨의 회사는 수백만 개의 뉴스 기사를 처리합니다. TF-IDF 벡터화를 하면 각 기사를 효율적으로 표현할 수 있습니다.

비슷한 기사끼리 묶거나, 검색 결과를 랭킹하거나, 추천 시스템을 만드는 데 활용됩니다. 주의할 점이 있습니다.

TF-IDF는 단어의 순서를 무시합니다. "dog bites man"과 "man bites dog"가 같은 벡터가 됩니다.

이를 Bag of Words 방식이라고 합니다. 순서가 중요한 경우에는 한계가 있습니다.

또한 희소 행렬(sparse matrix)로 저장됩니다. 대부분의 값이 0이기 때문입니다.

5000개 단어 중 실제로 한 기사에 나오는 건 100개 정도뿐이니까요. 이것을 일반 배열로 바꾸면 메모리가 폭발합니다.

김개발 씨가 감탄했습니다. "텍스트가 숫자로 바뀌니까 드디어 머신러닝을 할 수 있겠네요!" 박시니어 씨가 고개를 끄덕였습니다.

"이제 본격적으로 모델을 학습시킬 차례입니다."

실전 팁

💡 - ngram_range를 (1, 3)까지 늘리면 더 정교하지만 차원이 커져 느려집니다.

  • CountVectorizer는 빈도만 세고, TfidfVectorizer는 중요도까지 계산합니다.
  • 벡터화 객체는 pickle로 저장해두면 나중에 새 데이터에도 같은 기준을 적용할 수 있습니다.

5. 여러 모델 학습 및 비교

벡터화된 데이터를 앞에 두고 김개발 씨가 고민에 빠졌습니다. "어떤 알고리즘을 써야 할까요?" 머신러닝에는 수십 가지 알고리즘이 있습니다.

박시니어 씨가 말했습니다. "여러 개를 동시에 테스트해보고 가장 좋은 걸 고르면 됩니다."

여러 모델 학습 및 비교는 다양한 머신러닝 알고리즘을 한꺼번에 훈련시켜 성능을 비교하는 과정입니다. 마치 여러 요리사에게 같은 재료를 주고 누가 가장 맛있는 요리를 만드는지 경쟁시키는 것과 같습니다.

데이터에 가장 잘 맞는 알고리즘을 찾아냅니다.

다음 코드를 살펴봅시다.

from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report

# 세 가지 모델 준비
models = {
    'Naive Bayes': MultinomialNB(),
    'Logistic Regression': LogisticRegression(max_iter=1000),
    'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42)
}

# 각 모델 학습 및 평가
results = {}
for name, model in models.items():
    # 학습
    model.fit(X_train_tfidf, y_train)

    # 예측
    y_pred = model.predict(X_test_tfidf)

    # 정확도 계산
    accuracy = accuracy_score(y_test, y_pred)
    results[name] = accuracy

    print(f"\n{name} 결과:")
    print(f"정확도: {accuracy:.4f}")
    print(classification_report(y_test, y_pred, target_names=['진짜', '가짜']))

# 최고 성능 모델 선택
best_model = max(results, key=results.get)
print(f"\n최고 성능 모델: {best_model} ({results[best_model]:.4f})")

박시니어 씨가 세 가지 알고리즘을 설명해주었습니다. "각각 장단점이 있어요.

실제로 돌려봐야 어떤 게 우리 데이터에 잘 맞는지 알 수 있습니다." 머신러닝 모델이란 무엇일까요? 쉽게 비유하자면, 머신러닝 모델은 마치 각기 다른 학습 방법을 가진 학생들과 같습니다.

어떤 학생은 암기를 잘하고, 어떤 학생은 논리적 추론을 잘하고, 어떤 학생은 패턴 인식에 강합니다. 같은 교과서로 공부해도 성적이 다르듯이, 같은 데이터로 훈련해도 모델마다 성능이 다릅니다.

우리가 테스트할 세 가지 모델을 살펴보겠습니다. 첫 번째는 Naive Bayes입니다.

순진한 베이즈라는 뜻인데, 각 단어가 독립적이라고 가정합니다. 실제로는 단어들이 서로 연관되어 있지만, 간단하게 처리하기 위해 독립적이라고 "순진하게" 믿는 것입니다.

텍스트 분류에 전통적으로 많이 쓰이며, 빠르고 효율적입니다. 두 번째는 Logistic Regression입니다.

이름에 Regression이 붙어있지만 실제로는 분류 알고리즘입니다. 각 특징에 가중치를 매겨서 선형 조합으로 판단합니다.

해석하기 쉽고, 어떤 단어가 중요한지 확인할 수 있다는 장점이 있습니다. 세 번째는 Random Forest입니다.

여러 개의 결정 트리를 만들어서 투표로 결정하는 앙상블 방법입니다. 마치 여러 전문가의 의견을 종합하는 것처럼, 다양한 관점에서 판단합니다.

복잡한 패턴을 잘 찾아내지만, 학습 시간이 오래 걸립니다. 코드를 단계별로 살펴보겠습니다.

먼저 딕셔너리에 세 모델을 담습니다. 이렇게 하면 반복문으로 한 번에 처리할 수 있습니다.

각 모델 객체를 생성할 때 필요한 파라미터를 설정합니다. LogisticRegression의 max_iter=1000은 최대 반복 횟수입니다.

기본값으로는 수렴하지 않을 수 있어서 늘려줍니다. RandomForestClassifier의 n_estimators=100은 트리를 100개 만든다는 의미입니다.

많을수록 성능이 좋아지지만 느려집니다. random_state=42는 재현 가능하도록 난수 시드를 고정합니다.

반복문 안에서 각 모델을 학습시킵니다. fit 메서드로 학습하고, predict 메서드로 예측합니다.

그런 다음 accuracy_score로 정확도를 계산합니다. 정확도는 전체 예측 중 맞춘 비율입니다.

classification_report는 더 상세한 정보를 보여줍니다. Precision은 가짜라고 예측한 것 중 실제로 가짜인 비율입니다.

Recall은 실제 가짜 뉴스 중 모델이 찾아낸 비율입니다. F1-score는 이 둘의 조화평균입니다.

실제로 어떤 결과가 나올까요? 김개발 씨가 코드를 실행했습니다.

Naive Bayes는 88퍼센트, Logistic Regression은 92퍼센트, Random Forest는 90퍼센트의 정확도를 보였습니다. Logistic Regression이 가장 좋은 성능을 냈습니다.

"왜 Logistic Regression이 가장 좋은 거죠?" 김개발 씨가 물었습니다. 박시니어 씨가 설명했습니다.

"텍스트 데이터는 차원이 높고 희소한 특징이 있어요. Logistic Regression은 이런 데이터에 잘 맞습니다.

하지만 데이터셋이 바뀌면 결과도 달라질 수 있어요." 주의할 점이 있습니다. 정확도만 보면 안 됩니다.

만약 가짜 뉴스가 전체의 10퍼센트밖에 안 된다면, 무조건 "진짜"라고 대답하는 모델도 90퍼센트 정확도를 얻습니다. 따라서 Precision, Recall, F1-score를 함께 봐야 합니다.

또한 과적합(overfitting)을 조심해야 합니다. Random Forest가 학습 데이터에서는 99퍼센트인데 테스트에서 90퍼센트라면, 데이터를 암기했을 가능성이 있습니다.

김개발 씨는 Logistic Regression 모델을 최종 선택하기로 했습니다. 성능도 좋고, 해석도 쉬워서 비즈니스에 설명하기 좋기 때문입니다.

실전 팁

💡 - 항상 여러 모델을 비교하세요. 데이터마다 잘 맞는 알고리즘이 다릅니다.

  • 교차 검증(Cross-validation)을 하면 더 신뢰할 수 있는 평가를 할 수 있습니다.
  • 하이퍼파라미터 튜닝으로 각 모델의 성능을 더 끌어올릴 수 있습니다.

6. 최적 모델 선택 및 평가

Logistic Regression이 가장 좋은 성능을 보였지만, 박시니어 씨는 만족하지 않았습니다. "정확도가 92퍼센트면 나쁘지 않지만, 더 올릴 수 있어요.

하이퍼파라미터를 조정해봅시다." 김개발 씨는 처음 듣는 용어에 고개를 갸우뚱했습니다.

최적 모델 선택 및 평가는 하이퍼파라미터를 튜닝하고 교차 검증으로 성능을 정밀하게 측정하는 과정입니다. 마치 악기를 조율하듯이, 모델의 설정을 미세하게 조정해서 최고의 성능을 끌어냅니다.

혼동 행렬로 오류 유형도 분석합니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt

# Logistic Regression 하이퍼파라미터 탐색
param_grid = {
    'C': [0.1, 1, 10, 100],  # 정규화 강도
    'penalty': ['l1', 'l2'],  # 정규화 방식
    'solver': ['liblinear']  # l1을 위해 필요
}

# 그리드 서치로 최적 파라미터 찾기
grid_search = GridSearchCV(
    LogisticRegression(max_iter=1000),
    param_grid,
    cv=5,  # 5-fold 교차 검증
    scoring='f1',
    n_jobs=-1  # 모든 CPU 코어 사용
)

grid_search.fit(X_train_tfidf, y_train)

# 최적 모델로 예측
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test_tfidf)

# 혼동 행렬 시각화
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('예측')
plt.ylabel('실제')
plt.savefig('confusion_matrix.png')

print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최종 F1 점수: {grid_search.best_score_:.4f}")

박시니어 씨가 화이트보드에 그림을 그렸습니다. "하이퍼파라미터는 모델을 학습하기 전에 우리가 설정하는 값이에요.

이걸 잘 조정하면 성능이 크게 달라집니다." 하이퍼파라미터란 무엇일까요? 쉽게 비유하자면, 하이퍼파라미터는 마치 오븐의 온도와 시간 설정과 같습니다.

같은 재료로 같은 레시피를 써도, 180도에서 20분 굽는 것과 200도에서 15분 굽는 것은 결과가 다릅니다. 머신러닝도 마찬가지입니다.

알고리즘은 같아도 설정값에 따라 성능이 달라집니다. 우리가 조정할 하이퍼파라미터를 살펴보겠습니다.

첫 번째는 C입니다. 정규화 강도를 조절하는 값인데, 역수 개념입니다.

C가 작으면 정규화가 강해져서 모델이 단순해집니다. C가 크면 정규화가 약해져서 모델이 복잡해집니다.

과적합을 방지하려면 적절한 C 값을 찾아야 합니다. 두 번째는 penalty입니다.

L1과 L2 두 가지 정규화 방식이 있습니다. L1은 불필요한 특징의 가중치를 아예 0으로 만들어 특징 선택 효과가 있습니다.

L2는 가중치를 골고루 줄입니다. 데이터에 따라 어느 것이 나은지 다릅니다.

어떻게 최적값을 찾을까요? 손으로 하나하나 바꿔가며 테스트하기엔 조합이 너무 많습니다.

그래서 GridSearchCV를 사용합니다. 이것은 우리가 지정한 파라미터 조합을 모두 시도해보고 가장 좋은 것을 자동으로 찾아줍니다.

param_grid 딕셔너리에 시도할 값들을 나열합니다. C는 0.1, 1, 10, 100 네 가지, penalty는 l1, l2 두 가지입니다.

총 4 × 2 = 8가지 조합이 테스트됩니다. cv=5는 5-fold 교차 검증을 의미합니다.

교차 검증이란 무엇일까요? 데이터를 다섯 부분으로 나누어, 네 부분으로 학습하고 한 부분으로 테스트하는 것을 다섯 번 반복합니다.

매번 다른 부분을 테스트용으로 씁니다. 이렇게 하면 우연히 쉬운 데이터가 테스트에 걸려서 점수가 높게 나오는 것을 방지할 수 있습니다.

scoring='f1'은 F1 점수를 기준으로 최적을 선택한다는 의미입니다. 정확도보다 F1이 더 균형 잡힌 지표이기 때문입니다.

n_jobs=-1은 모든 CPU 코어를 사용해서 병렬로 처리합니다. 8가지 조합을 순차적으로 하면 오래 걸리지만, 병렬로 하면 훨씬 빠릅니다.

혼동 행렬은 무엇일까요? 혼동 행렬은 모델의 오류를 자세히 보여줍니다.

2×2 표로, 실제 진짜를 진짜라고 맞춘 것, 실제 진짜를 가짜라고 틀린 것, 실제 가짜를 진짜라고 틀린 것, 실제 가짜를 가짜라고 맞춘 것을 보여줍니다. 이것을 보면 어디서 실수하는지 알 수 있습니다.

만약 진짜 뉴스를 가짜라고 자주 오판한다면, 정상적인 기사까지 차단할 위험이 있습니다. 반대로 가짜를 진짜라고 놓친다면, 가짜 뉴스 유포를 막지 못합니다.

실무에서는 어떤 오류가 더 치명적인지 판단해야 합니다. 김개발 씨의 회사는 가짜 뉴스를 놓치는 것을 더 위험하게 봅니다.

그래서 Recall을 중요하게 여깁니다. 약간의 오탐(false positive)을 감수하더라도, 가짜를 최대한 많이 잡아내는 것이 목표입니다.

그리드 서치가 끝나자 결과가 나왔습니다. 최적 파라미터는 C=10, penalty='l2'였습니다.

F1 점수는 0.9450으로, 이전의 0.92보다 향상되었습니다. 2.5퍼센트포인트 차이지만, 실제로는 수백 개의 기사를 더 정확하게 분류하는 것입니다.

주의할 점이 있습니다. 그리드 서치는 시간이 오래 걸립니다.

파라미터 조합이 많고, 교차 검증까지 하면 몇 시간씩 걸릴 수 있습니다. 따라서 먼저 넓은 범위로 탐색하고, 좋은 구간을 찾으면 그 주변을 세밀하게 탐색하는 전략이 효율적입니다.

또한 테스트 데이터로 여러 번 평가하면 안 됩니다. 테스트 점수를 보고 모델을 수정하고 다시 테스트하는 것을 반복하면, 테스트 데이터에 과적합됩니다.

검증 데이터와 테스트 데이터를 분리해야 합니다. 김개발 씨는 최적화된 모델을 파일로 저장했습니다.

이제 실제 서비스에 배포할 준비가 되었습니다.

실전 팁

💡 - RandomizedSearchCV는 모든 조합 대신 무작위로 샘플링해서 더 빠릅니다.

  • 혼동 행렬을 보고 비즈니스에 맞게 임계값(threshold)을 조정할 수 있습니다.
  • 모델을 pickle이나 joblib로 저장하면 나중에 다시 학습할 필요 없이 바로 사용할 수 있습니다.

7. 새 뉴스 기사 예측 테스트

드디어 모델이 완성되었습니다. 김개발 씨는 설렘을 감추지 못했습니다.

"이제 진짜 뉴스를 넣어볼까요?" 박시니어 씨가 최근 화제가 된 기사 몇 개를 가져왔습니다. "한번 테스트해봅시다.

실제로 잘 작동하는지 확인해야죠."

새 뉴스 기사 예측 테스트는 학습된 모델을 실제 데이터에 적용하는 과정입니다. 마치 시험을 치르듯이, 한 번도 본 적 없는 새로운 뉴스를 모델에 입력해서 진위를 판별합니다.

예측 확률도 함께 출력해서 얼마나 확신하는지 확인합니다.

다음 코드를 살펴봅시다.

import joblib

# 모델과 벡터라이저 저장
joblib.dump(best_model, 'fake_news_model.pkl')
joblib.dump(vectorizer, 'tfidf_vectorizer.pkl')

# 실제 사용 시: 모델 불러오기
model = joblib.load('fake_news_model.pkl')
vectorizer = joblib.load('tfidf_vectorizer.pkl')

def predict_news(article_text):
    # 1. 전처리
    cleaned = preprocess_text(article_text)

    # 2. 벡터화
    vectorized = vectorizer.transform([cleaned])

    # 3. 예측
    prediction = model.predict(vectorized)[0]
    probability = model.predict_proba(vectorized)[0]

    # 4. 결과 출력
    label = "가짜 뉴스" if prediction == 1 else "진짜 뉴스"
    confidence = probability[prediction] * 100

    return label, confidence

# 테스트
test_article = """
Breaking: Scientists discover cure for all diseases!
This shocking revelation will change humanity forever.
Click here to learn the secret they don't want you to know!
"""

label, confidence = predict_news(test_article)
print(f"판정: {label}")
print(f"확신도: {confidence:.2f}%")

박시니어 씨가 말했습니다. "모델을 만드는 것도 중요하지만, 실제로 사용하는 것이 더 중요해요.

API로 만들어서 서비스에 통합해야 합니다." 모델을 어떻게 배포할까요? 먼저 학습된 모델과 벡터라이저를 파일로 저장합니다.

joblib은 scikit-learn 객체를 효율적으로 저장하는 라이브러리입니다. pickle보다 큰 NumPy 배열을 더 빠르게 처리합니다.

저장해두면 나중에 서버를 재시작해도 다시 학습할 필요가 없습니다. 몇 시간 걸린 학습을 한 번만 하고, 그 결과를 계속 재사용할 수 있습니다.

예측 함수를 만들어봅시다. predict_news 함수는 새로운 기사 텍스트를 받아서 네 단계를 거칩니다.

첫째, preprocess_text로 전처리합니다. 학습할 때와 똑같은 방식으로 청소해야 합니다.

둘째, 저장해둔 vectorizer로 벡터화합니다. 학습 때 fit한 바로 그 벡터라이저를 써야 차원이 일치합니다.

셋째, model.predict로 예측합니다. 0 또는 1이 반환됩니다.

넷째, model.predict_proba로 확률을 구합니다. 이것은 [0.2, 0.8] 같은 형태로, 진짜일 확률과 가짜일 확률을 보여줍니다.

확률이 왜 중요할까요? 단순히 "가짜입니다"라고만 하는 것보다, "95퍼센트 확률로 가짜입니다"라고 하는 게 훨씬 유용합니다.

만약 확률이 51퍼센트라면, 모델이 확신하지 못한다는 뜻입니다. 이런 애매한 케이스는 사람이 직접 검토하도록 할 수 있습니다.

실제로 테스트해봅시다. 예시 기사는 전형적인 가짜 뉴스 패턴을 보입니다.

"Breaking", "shocking", "secret they don't want you to know" 같은 자극적인 표현이 가득합니다. 구체적인 정보는 없고 클릭을 유도하는 문구만 있습니다.

모델에 넣어보니 "가짜 뉴스, 확신도 97.83퍼센트"라는 결과가 나왔습니다. 모델이 정확하게 판단했습니다.

반대로 정상적인 뉴스를 넣어봅시다. "The Federal Reserve announced a 0.25% interest rate increase today, citing inflation concerns.

The decision was made after a two-day policy meeting." 이번엔 "진짜 뉴스, 확신도 89.12퍼센트"가 나왔습니다. 구체적인 수치, 출처, 객관적인 표현이 있어서 진짜로 판단한 것입니다.

실무에서는 어떻게 활용할까요? 김개발 씨의 회사는 이 모델을 Flask API로 만들었습니다.

뉴스가 새로 올라오면 자동으로 API를 호출해서 진위를 확인합니다. 가짜로 판정되면 편집자에게 알림을 보내고, 확률이 낮으면(예: 60퍼센트 미만) 자동으로 검토 대기열에 넣습니다.

또한 사용자들이 의심스러운 기사를 신고하면, 이 모델로 우선순위를 매깁니다. 모델이 가짜일 확률이 높다고 판단한 것부터 먼저 검토합니다.

주의할 점이 있습니다. 모델은 완벽하지 않습니다.

틀릴 수도 있습니다. 따라서 중요한 결정은 사람이 최종 확인해야 합니다.

모델은 보조 도구로, 사람의 판단을 돕는 역할을 합니다. 또한 시간이 지나면 뉴스 패턴이 변합니다.

1년 전 데이터로 학습한 모델은 지금의 가짜 뉴스를 잘 못 잡을 수 있습니다. 정기적으로 새 데이터로 재학습해야 합니다.

김개발 씨는 모니터링 대시보드를 만들었습니다. 매일 모델의 정확도를 추적하고, 성능이 떨어지면 알림을 받습니다.

한 달에 한 번씩 새 데이터로 모델을 업데이트합니다. 프로젝트를 마무리하며 박시니어 씨가 말했습니다.

"잘했어요. 이제 진짜 데이터 사이언티스트가 됐네요." 김개발 씨는 뿌듯함을 느꼈습니다.

처음엔 막막했던 프로젝트였지만, 단계별로 차근차근 진행하니 완성할 수 있었습니다. 데이터 수집, 탐색, 전처리, 벡터화, 모델 학습, 평가, 배포까지 전체 파이프라인을 경험했습니다.

이제 여러분도 자신만의 텍스트 분류 프로젝트를 시작할 수 있습니다. 스팸 메일 필터, 감정 분석, 주제 분류 등 다양한 문제에 같은 접근법을 적용해보세요.

실전 팁

💡 - API로 만들 때는 입력 검증을 꼭 하세요. 빈 문자열이나 너무 긴 텍스트를 처리하지 못하면 에러가 납니다.

  • 로깅을 추가해서 어떤 예측을 했는지 기록하면 나중에 모델 개선에 도움이 됩니다.
  • A/B 테스트로 새 모델과 기존 모델을 비교하면서 점진적으로 배포하세요.

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#MachineLearning#NLP#TextClassification#ScikitLearn

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.