머신러닝을 활용한 가짜뉴스 분류 튜토리얼
  • 등록자 윤지우
  • 등록일 2020-11-26 07:00 (2021-09-15 11:33)
  • 조회수 2743
좋아요
튜토리얼 영상

 

Fake News Classification

목표

  • Fake News를 잘 분류하는 것이 목표임.
  • 머신러닝/딥러닝을 활용하여 주어진 News가 True/False 인지 예측함.
  • 활발한 연구가 진행중임.

한계점

  • 하지만, 대부분의 연구가 특정 Rumor나 Fake news에 초점을 맞춤. (주로 Politics)
  • 다양한 종류의 fakenews가 어떻게 생산되고 확대되는지에 대한 지식이 없음
  • 최근 연구들도 6~7년 전 데이터를 주로 사용함

Fake News Dataset

  • 위의 문제점을 해결하기 위해서는 다양한 분야에서 발생하는 Fake News 관련 데이터셋 및 이를 분류하는 모델이 필요함.
  • 따라서 다양한 주제에 대해서 팩트체킹이 이루어지는 snopes(https://www.snopes.com/) 를 타겟서비스로 설정하고 데이터를 수집하였음.
라이브러리 및 데이터 로드
  1. import pandas as pd
  2. import seaborn as sns
  3. import numpy as np
  4. from sklearn.feature_extraction.text import CountVectorizer
  5. from sklearn.linear_model import RidgeClassifier
  6. from sklearn.metrics import classification_report
  7. from sklearn.model_selection import train_test_split
  8. from nltk.stem.snowball import EnglishStemmer
  9. from imblearn.over_sampling import SMOTE

  10. from sklearn.metrics import confusion_matrix
  11. import matplotlib.pyplot as plt
  12. %matplotlib inline
  13. import numpy as np

  14. df = pd.read_csv("snopes.tsv", sep="\t") # load data
  15. print(df.shape) # how many articles?

판다스 라이브러리의 read_csv 함수를 사용하여 데이터 셋을 로드하고 데이터의 크기를 확인하기 위해서 shape를 프린트해 봅니다.

튜토리얼에서 사용하는 데이터 셋의 경우 총 12410 row가 있으며 각 row는 16개의 피처를 가집니다.

데이터 구조 확인

데이터명

설명

게시글 번호

기사의 고유 번호

제목

기사의 제목

URL

기사의 링크

요약글

기사의 내용에 대한 요약글

발행일

기사 발행일

수정일

기사 수정일

카테고리

루머의 주제에 대한 카테고리

클레임(Claim)

검증하는 루머의 내용하나의 문장으로 표현.

루머 평가(Veracity)

검증한 루머에 대한 평가 결과거짓 외에도 대부분 참대부분 거짓 등의 값을 가질 수 있음.

정보처

루머를 검증하는 데 사용한 정보 출처

태그

루머의 주제를 나타낼 수 있는 키워드


자세한 데이터 구조는 다음과 같습니다.

데이터 셋은 온라인 팩트체킹 서비스인 Snopes (https://snopes.com) 에서 수집되었습니다.

본 튜토리얼에서는 루머의 내용을 한 문장으로 표현한 클레임(Claim) 과 루머에 대한 평가(Veracity) 만을 다룰 예정입니다.

데이터 확인
  1. df.head()

df.head 함수는 판다스 데이터프레임 내부에서 앞선 5개의 데이터들을 보여줍니다.

True / False 데이터만 걸러내기
  1. df = df[(df["veracity"]=="true")|(df["veracity"]=="false")] # veracity true or false
  2. df.shape

앞선 데이터를 확인하면 veracity (루머의 평가) 부분이 NaN, false, true가 섞여있는 것을 확인할 수 있습니다.

Fake News Classification 문제를 조금 더 단순화하기 위해서 이번 튜토리얼에서는 True 혹은 False인 뉴스만 다룰 것이기 때문에 위와 같이 코드를 작성하여 실행해줍니다. 

True와 False로만 구성된 데이터 셋은 총 6266 개의 뉴스를 가지고 있음을 확인할 수 있습니다.

이후, df.head()로 한번 더 확인해보면 veracity가 NaN인 row가 없어졌음을 확인할 수 있습니다.

year, month column 추가하기
  1. df['year'] = df.published_date.apply(lambda x: int(x.split(" ")[0].split("-")[0])) # create year column
  2. df['month'] = df.published_date.apply(lambda x: int(x.split(" ")[0].split("-")[1])) # create month column
  3. print(df.head())

추후에 분석하기 위해 published_date 에서 year와 month를 따로 저장하는 작업을 해줍니다. 

새로운 year, month 피처는 월별, 연도별 뉴스의 수를 분석할 때 사용합니다. 

마지막으로 df.head를 통해 데이터를 확인해줍니다. 

데이터프레임의 끝에 year와 month가 생긴 것을 확인할 수 있습니다.

카테고리별 뉴스의 개수 확인
  1. categories = df.category.value_counts()
  2. categories = pd.DataFrame(categories)
  3. categories = categories.reset_index()
  4. categories.columns = ['category', 'count']
  5. categories.tail()

지금부터는 본격적인 모델 학습에 앞서서 Fake News 데이터를 알아보겠습니다. 

우선 어떠한 카테고리별 뉴스의 분포를 확인하기 위해서 다음과 같이 카테고리별 뉴스의 개수를 dataframe으로 만듭니다.

플롯을 위한 함수 작성
  1. def plot_bargraph(x, y, data, ax, title, ylim, rotation, ylabel="", xlabel=""):
  2. sns.barplot(x=x, y=y, data=data, ax=ax)
  3. ax.set_title(title, fontsize=16)
  4. for tick in ax.get_xticklabels():
  5. tick.set_rotation(rotation)
  6. ax.set_ylim(0, ylim)
  7. ax.set_ylabel(ylabel, fontsize=14)
  8. ax.set_xlabel(xlabel, fontsize=14)
  9. ax.tick_params(labelsize=13)

다음은 플롯을 그리기 위한 함수입니다. 

Seaborn 라이브러리를 활용했습니다.

카테고리별 뉴스의 개수 플롯
  1. # plot categories
  2. fig, ax = plt.subplots()
  3. fig.set_size_inches(14,5)

  4. plot_bargraph("category", "count", categories, ax, "Number of Articles per Category", 1500, 90, "Number of Articles", "Categories")

플롯을 그려보니 Junk News가 가장 많은 뉴스의 양을 차지하는 것을 확인할 수 있고, Politics도 있는 것을 확인할 수 있습니다. 

이는 정치분야에서 가장 Fake News가 많이 나오기 때문인 것 같습니다. 

또한, 현재 사용하고 있는 Snopes 기반의 Fake News 데이터 셋은 정치 분야 외에도 꽤 다양한 분야의 뉴스를 포함하고 있음을 확인할 수 있습니다.

연도별 뉴스의 개수 확인
  1. years = df.groupby('year')['id'].count()
  2. years = pd.DataFrame(years)
  3. years = years.reset_index()
  4. years.columns = ['year', 'count']
  5. years.tail()

다음은 연도별 뉴스의 개수를 확인하기 위해 dataframe을 생성하는 과정입니다.

연도별 뉴스의 개수 플롯
  1. fig, ax = plt.subplots()
  2. fig.set_size_inches(8,5)
  3. plot_bargraph("year", "count", years[years['year']>=2009], ax, "Number of Articles per Year", 2000, 0, "Number of Articles", "Year")

시각화를 진행해보면 미국 대선이 있었던 2016년에 가장 많은 뉴스가 있음을 확인할 수 있습니다.

n

월별 뉴스의 개수 확인
  1. months = df[df['year'] == 2016].groupby('month')['id'].count()
  2. months = pd.DataFrame(months)
  3. months = months.reset_index()
  4. months.columns = ['month', 'count']
  5. months.head()

다음은 월별 기사 수를 알아보고자 합니다. 

이전 플롯에서 2016년에 가장 많은 기사가 제보 되었음을 확인하였으므로, 2016년을 특정하여 월별 기사 수를 확인해보겠습니다.

월별 뉴스의 개수 플롯
  1. fig, ax = plt.subplots()
  2. fig.set_size_inches(8,4)
  3. plot_bargraph("month", "count", months, ax, "Number of Articles per Month", 200, 0, "Number of Articles", "Months")

플롯을 확인해보면 6월을 기준으로 상반기보다는 하반기에 뉴스가 많은 것을 확인할 수 있습니다.

모델 학습 준비
  1. fake_news = df[["claim", "veracity"]]
  2. fake_news = fake_news.dropna()

이제 기본적인 분석은 완료했고, 실제로 모델을 학습시켜 claim만을 가지고 veracity를 예측할 것 입니다. 

그러기 위해서 기존의 dataframe을 조금 더 간단한 형태로 다음과 같이 변환해줍니다.

Train/Test 데이터 스플릿
  1. from sklearn.model_selection import train_test_split
  2. X_train, X_test, y_train, y_test = train_test_split(fake_news['claim'], fake_news['veracity'],random_state = 1, test_size=0.3)

Train 데이터는 학습 중에만 사용되며 Test 데이터는 학습 도중에는 모델이 알 수 없습니다. 

이렇게 학습을 하고 평가를 해야만 모델이 real data를 만났을 때 어느 정도의 성능을 보일지에 대한 정확한 평가가 가능합니다. 

튜토리얼에서는 Test 데이터 셋의 비율을 30%로 진행했습니다.

  1. stemmer = EnglishStemmer()
  2. analyzer = CountVectorizer(min_df = 0.5, ngram_range = (1, 3), stop_words='english').build_analyzer()
  3. def stemmed_words(doc):
  4. return (stemmer.stem(w) for w in analyzer(doc))
  5. vect = CountVectorizer(analyzer=stemmed_words).fit(X_train)
  6. X_train_vectorized = vect.transform(X_train)
  7. len(vect.get_feature_names())

우리가 가지고 있는 데이터는 자연어 형태 입니다. 모델을 학습시키기 위해서는 수치적인 형태로 변환해주어야 합니다.

이때 모델이 조금 더 학습을 하기 쉽도록 단어의 어간을 추출하는 작업도 같이 진행하면 좋습니다.

*Stemming은 어간을 추출하는 작업입니다. Stemming에 대한 자세한 내용은 다음의 사이트에서 확인할 수 있습니다. (https://wikidocs.net/21707)


모델 훈련 및 평가
  1. # without smote
  2. model = RidgeClassifier(class_weight='balanced')
  3. model.fit(X_train_vectorized, y_train)
  4. predictions = model.predict(vect.transform(X_test))
  5. print(classification_report(y_test, predictions))

이제 모델을 학습시키고 Test 셋을 통해서 평가를 진행합니다. 

본 튜토리얼에서는 따로 모델의 파라미터를 튜닝하지 않고 class_weight만 balanced로 두었습니다. 

그리고 최종적으로 classification_report 함수를 통해 모델의 성능을 확인했습니다.

  1. precision recall f1-score support
  2. false 0.81 0.66 0.72 1515
  3. true 0.18 0.33 0.24 356

  4. accuracy 0.59 1871
  5. macro avg 0.50 0.49 0.48 1871
  6. weighted avg 0.69 0.59 0.63 1871

하지만, 클래스 불균형으로 인해 모델의 성능이 그렇게 높지 않습니다. 

오버샘플링을 진행해보도록 하겠습니다.

오버샘플링으로 데이터 불균형 해결하기
  1. X_train_vectorized_smote, y_train_smote = SMOTE(random_state=1).fit_sample(X_train_vectorized, y_train) # over-sampling

오버샘플링은 클래스의 불균형이 있을 경우 소수 클래스에 대해서 데이터를 생성해주는 작업입니다.

현재 데이터 분포와 유사한 데이터 포인터들을 생성해서 모델학습에 도움을 줍니다.

단, 언더/오버샘플링은 반드시 Train 데이터에만 적용합니다!

본 튜토리얼에서는 오버샘플링 기법 중 SMOTE를 사용했습니다.

SMOTE 적용 후 모델 평가
  1. # with smote
  2. model = RidgeClassifier(class_weight='balanced')
  3. model.fit(X_train_vectorized_smote, y_train_smote)
  4. predictions = model.predict(vect.transform(X_test))
  5. print(classification_report(y_test, predictions))
  1. precision recall f1-score support
  2. false 0.84 0.77 0.80 1515
  3. true 0.27 0.35 0.30 356

  4. accuracy 0.69 1871
  5. macro avg 0.55 0.56 0.55 1871
  6. weighted avg 0.73 0.69 0.71 1871

SMOTE를 적용하니 성능이 조금 향상되었습니다. 

여전히 성능이 낮기는 하지만, 모델 셀렉션, 파라미터 튜닝 과정을 거치거나 다른 딥러닝 모델을 활용하면 성능이 올라갈 것이라 기대합니다.

실제 데이터로 예측


마지막으로 실제 snopes.com 사이트에 있는 claim에 대해서 예측을 진행해보도록 하겠습니다.

문장 하나를 벡터화 시킨 다음 학습된 모델에 인풋으로 넣어주면 다음과 같이 예측값을 반환해줍니다.

그 결과, 정답과 일치하는 TRUE가 나왔습니다.

  1. def prediction(sentence):
  2. return(model.predict(vect.transform([sentence])))
  3. prediction("The U.S. presidential limo is equipped with Goodyear tires.")[0]
  1. 'true'