본 게시물은 캐글 노트북을 바탕으로 작성되었습니다.
노트북을 보며 필사하면서 간단한 번역, 코드 리뷰를 작성했습니다.
이 노트북은 제가 쓴 Getting started with NLP Notebooks 시리즈의 첫 시작입니다. 이 노트북은 현재의 대회와 관련된 NLP의 개념을 설명합니다. NLP는 인간의 언어와 컴퓨터 사이의 상호 작용에 초점을 맞춘 연구 분야입니다. NLP는 컴퓨터 과학, 인공지능, 그리고 컴퓨터 언어학의 교점입니다. NLP는 컴퓨터가 똑똑하고 유용한 방식으로 인간의 언어로부터 의미를 이끌어내고, 이해하고, 분석하게 하는 방법입니다.
여기선 크게 두개의 부분으로 나뉩니다.
- Part 1 : Getting started with NLP : A general Introduction
- Part 2 : Getting started with NLP : CountVectorizers | TFIDF | Hashing Vectorizer
나머지 문자 처리부분에 대한 노트북은 기본 전처리, EDA, 감성 추출에 대한 내용입니다.
Dataset
여기서 사용되는 데이터는 훈련, 평가 데이터셋으로 나눠져있는 트윗들을 포함하고 있습니다. 훈련 데이터에는 타겟 컬럼이 있는데, 이는 트윗들이 실제 재해와 연관이 있는지 없는지를 판단하는 용도로 쓰입니다.
우리가 할 일은 평가 데이터셋에 있는 트윗들이 재해와 연관이 있는지 없는지를 1또는 0으로 예측하는 ML 모델을 만드는 것입니다. 이는 이진 분류 문제의 기본입니다.
평가 지표에 대한 이해
평가 지표는 통계 또는 머신 러닝 모델의 성능을 측정하는 데 쓰입니다. 모델을 테스트하기 위한 평가 지표는 매우 다양합니다. 여기에는 분류 정확도, 로그 손실등이 포함됩니다. 이번 문제에서는, 예측값과 기댓값 사이에 F1 score를 이용해서 평가하여 제출할것입니다.
F score( F1 score 또는 F measure 라고도 불리는)는 테스트의 정확도 측정입니다.
F score는 테스트의 precision(정밀도)와 recall(재현률)의 가중 조화 평균으로 정의됩니다.
- Precision(정밀도)는 정답이라고 예측된 것 중에 진짜로 정답인 것들에 대한 비율입니다.
- Recall(재현율)은 정답 중에서 실제로 모델이 정답이라고 예측한 비율을 나타냅니다.
F score는 완벽한 Precision과 Recall값을 가지면 최고의 값인 1로 수렴합니다. 반대로 최저 Precision과 Recall값을 가지면 0으로 수렴합니다.
왜 이게 유용할까요?
F score는 테스트의 정확도를 측정하며, precision과 recall의 균형을 맞춰줍니다. F score는 precision과 recall을 모두 사용하여 테스트의 성능에 대한 측정을 좀 더 현실적으로 해줍니다. F score는 검색, 문서 분류 및 쿼리 분류의 성능을 측정하기 위해 정보 검색에 자주 사용됩니다.
목차
- 필요한 라이브러리 불러오기
- 데이터 살펴보기
- 기본적인 EDA
- 텍스트 데이터 전처리
- 토큰 벡터화
- 텍스트 분류 모델 생성
1. 필요한 라이브러리 불러오기
import numpy as np
import pandas as pd
# text processing libraries
import re
import string
import nltk
from nltk.corpus import stopwords
# XGBoost
import xgboost as xgb
from xgboost import XGBClassifier
# sklearn
from sklearn import model_selection
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import f1_score
from sklearn import preprocessing, decomposition, model_selection, metrics, pipeline
from sklearn.model_selection import GridSearchCV, StratifiedKFold, RandomizedSearchCV
# matplotlib and seaborn for plotting
import matplotlib.pyplot as plt
import seaborn as sns
# Suppress warnings
import warnings
warnings.filterwarnings('ignore')
2. 데이터 살펴보기
# Training data
train = pd.read_csv('.../train.csv')
print('Training data shape : ', train.shape)
train.head()
훈련 데이터는 7613개의 행이 있고 타겟(우리가 예측 하고자 하는 라벨)이 있는 다섯개의 피쳐로 구성되어 있습니다.
# Testing data
test = pd.read_csv('.../test.csv')
print('Testing data shape : ', test.shape)
test.head()
테스트 데이터에는 타겟 컬럼이 없는 것을 알 수 있습니다.
3. 기본적인 EDA
결측치 확인
# Missing values in training set
train.isnull().sum()
각 컬럼들은 다음을 의미합니다.
- text : 트윗 내용
- keyword : 트윗에서의 주요 내용
- location : 트윗을 한 지역
# Missing values in test set
test.isnull().sum()
훈련 데이터와 테스트 데이터 모두 location에서 많은 수의 결측치가 있는것을 확인 할 수 있습니다.
타겟 컬럼 탐색
- 타겟 컬럼 분포 확인
우리는 주어진 트윗이 실제 재해와 관련이 있다면 1을 예측하고, 그게 아니라면 0을 예측 해야합니다.
train['target'].value_counts()
sns.barplot(train['target'].value_counts().index, train['target'].value_counts(), palette='rocket')
재해와 관련된 트윗과 그렇지 않은 트윗을 한 번 뽑아서 확인 해보겠습니다.
# A disaster tweet
disaster_tweets = train[train['target']==1]['text']
disaster_tweets.values[1]
# output
'Forest fire near La Ronge Sask. Canada'
# non a disaster tweet
non_disaster_tweets = train[train['target']==0]['text']
non_disaster_tweets.values[1]
# output
'I love fruits'
'keyword' 컬럼 탐색
키워드 컬럼은 트윗에서의 키워드를 의미합니다. 훈련 데이터에서 상위 20개의 키워드를 한 번 보겠습니다.
sns.barplot(y=train['keyword'].value_counts()[:20].index, x=train['keyword'].value_counts()[:20], orient='h')
이번에는 데이터셋에서 'disaster'란 단어가 얼마나 자주 나오는지 확인해보고, 그게 우리가 트윗에 대한 내용이 재해에 관한 것인지 판단하는 데에 도움을 주는지를 보겠습니다.
train.loc[train['text'].str.contains('disaster', na=False, case=False)].target.value_counts()
'location' 컬럼 탐색
비록 location 컬럼에 결측치가 많지만 현재 데이터에 있는 상위 20개의 location에 대해 보겠습니다. 몇몇 지역들이 반복되기 때문에, 약간의 정리가 필요합니다.
# Replacing the ambigious locations name with Standard names
train['location'].replace({'United States':'USA',
'New York':'USA',
"London":'UK',
"Los Angeles, CA":'USA',
"Washington, D.C.":'USA',
"California":'USA',
"Chicago, IL":'USA',
"Chicago":'USA',
"New York, NY":'USA',
"California, USA":'USA',
"FLorida":'USA',
"Nigeria":'Africa',
"Kenya":'Africa',
"Everywhere":'Worldwide',
"San Francisco":'USA',
"Florida":'USA',
"United Kingdom":'UK',
"Los Angeles":'USA',
"Toronto":'Canada',
"San Francisco, CA":'USA',
"NYC":'USA',
"Seattle":'USA',
"Earth":'Worldwide',
"Ireland":'UK',
"London, England":'UK',
"New York City":'USA',
"Texas":'USA',
"London, UK":'UK',
"Atlanta, GA":'USA',
"Mumbai":"India"}, inplace=True)
sns.barplot(y=train['location'].value_counts()[:5].index, x=train['location'].value_counts()[:5], orient='h')
4. 텍스트 데이터 전처리
1. 데이터 정리
우리는 NLP 프로젝트를 진행하기 전에 모든 데이터를 일관된 형식으로 전처리를 해주는 과정이 필요합니다. 예를 들어 데이터 정리, 토큰화 그리고 데이터를 매트릭스로 변환하는 등의 과정이 있습니다. 몇몇 기본적인 텍스트 데이터 전처리 기술은 다음이 있습니다.
- 알고리즘이 같은 단어를 다른 의미로 해석하는걸 방지하기 위해 모든 텍스트를 소문자로 바꾸거나 대문자로 바꾸는 방법
- 노이즈 제거, 즉 구두점, 숫자 값, 일반적인 비감각 텍스트(?)(/n)과 같은 표준 숫자나 문자에 없는 모든 항목을 제거하는 방법
- 토큰화 : 토큰화는 일반적인 문자열을 토큰 목록으로 변환시키는 과정을 설명할 때 사용되는 용어 입니다. Sentence tokenizer는 문장 목록을 찾을 때 사용될 수 있고, Word tokenizer는 단어 목록을 찾는데 사용 될 수 있습니다.
- 불용어 제거 : 가끔씩, 사용자가 필요로 하는 문서와 일치하는 문서를 찾는데에 도움을 거의 주지 않지만 아주 많이 등장하는 단어가 있습니다. 그것을 불용어(stop words)라고 합니다.
토큰화 이후에 진행되는 데이터 정리에 대한 방법이 있습니다.
- Stemming(어간 추출) : Stemming은 일반적으로 쓰이는 단어의 형식으로부터 변형된 단어를 어간, 기본형 등으로 줄이는 과정을 말합니다. 예를 들어 'Stems', 'Stemming', 'Stemmed' and 'Stemtization'을 어간 추출한 결과로는 아마 'stem'이 될 것입니다.
- Lemmatization(표제어 추출) : 어간 추출을 약간 변형한 것이 표제어 추출 입니다. 두 방법의 가장 큰 차이점은 어간 추출은 가끔 존재하지 않는 단어를 만들어내지만, 표제어 추출은 실제 단어를 뽑아냅니다. 따라서, 어간(여러분이 찾아낸 단어에서 의미있는 부분)은 사전에서 찾아볼 수 있는 것이 아닙니다. 하지만 표제어는 찾을 수 있습니다. 표제어 추출의 예로는 'run'은 'running'과 'ran'의 기본 형태이기 때문에 또는 'better'과 'good'은 같은 표제어에 있기 때문에 같은 단어라고 간주하는 것입니다.
- 품사 분류
- bi-gram 또는 tri-gram을 만들거나 그 이상의 n-gram을 만드는 방법.
하지만, 이 모든 방법을 여러분이 사용할 필요는 없습니다. 여러분이 다루는 문제에 따라 사용할 방법을 정하면 됩니다. 불용어 제거가 어떨땐 도움이 되지만 또 어떨때는 도움이 되지 않기도 합니다. 여기에 한 블로그에서 찾은 아주 nice한 표가 있습니다. 이 표에는 여러분이 가지고 있는 텍스트 데이터에대해 얼마나 전처리를 해야하는지에 대한 요약이 있습니다.
# A quick glance over the existing data
train['text'][:5]
# output
0 Our Deeds are the Reason of this #earthquake M...
1 Forest fire near La Ronge Sask. Canada
2 All residents asked to 'shelter in place' are ...
3 13,000 people receive #wildfires evacuation or...
4 Just got sent this photo from Ruby #Alaska as ...
Name: text, dtype: object
# Applying a first round of text cleaning techniques
def clean_text(text):
''' 소문자화, 대괄호 안 문자 제거, 링크 제거, 구두점 제거, 숫자를 포함한문자 제거'''
text = text.lower()
text = re.sub('\[.*?\]', '', text)
text = re.sub('https?://\S+|www\.\S+', '', text)
text = re.sub('<.*?>+', '', text)
text = re.sub('[%s]' % re.escape(string.punctuation), '', text)
text = re.sub('\n', '', text)
text = re.sub('\w*\d\w*', '', text)
return text
# Applying the cleaning function to both test and training datasets
train['text'] = train['text'].apply(lambda x : clean_text(x))
test['text'] = test['text'].apply(lambda x : clean_text(x))
# Let's take a look at the updated text
train['text'].head()
이번엔 그냥 재미로 정제된 텍스트에 대해서 wordcloud를 만들어 트윗에서 가장 많이 쓰이는 단어를 보겠습니다.
from wordcloud import WordCloud
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(26,8))
wordcloud1 = WordCloud(background_color = 'white', width=600, height=400).generate(' '.join(disaster_tweets))
ax1.imshow(wordcloud1)
ax1.axis('off')
ax1.set_title('Disaster Tweets', fontsize=40);
wordcloud2 = WordCloud(background_color='white', width=600, height=400).generate(' '.join(non_disaster_tweets))
ax2.imshow(wordcloud2)
ax2.axis('off')
ax2.set_title('Disaster Tweets', fontsize=40);
'''노트북에선 진행할 때 disaster_tweets와 non_disaster_tweets에 정제된 데이터를 할당하는 과정이 없음'''
2. 토큰화
토큰화는 입력 문장을 토큰이라 불리는 것을 기준으로 자르는 과정을 말합니다. 토큰은 단어, 문장, 단락 등이 될 수 있습니다. 우리가 원하는 토큰 유형에 따라, 토큰화는 다양한 방식으로 진행할 수 있습니다.
text = "Are you coming, aren't you"
tokenizer1 = nltk.tokenize.WhitespaceTokenizer() # 문장에서 공백을 기준으로 토큰화 진행
tokenizer2 = nltk.tokenize.TreebankWordTokenizer() # 정규 표현식을 사용하여 Penn Treebank와 같은 방법으로 토큰화 진행
tokenizer3 = nltk.tokenize.WordPunctTokenizer() # 구두점을 기준으로 토큰화 진행
tokenizer4 = nltk.tokenize.RegexpTokenizer(r'\w+') # 정규 표현식을 이용해서 토큰화 진행
print('Example Text : ', text)
print('---'* 25)
print('Tokenization by whitespace : - ', tokenizer1.tokenize(text))
print('Tokenization by words using Treebank Word Tokenizer : - ', tokenizer2.tokenize(text))
print('Tokenization by punctuation : - ', tokenizer3.tokenize(text))
print('Tokenization by regular expression : - ', tokenizer4.tokenize(text))
# Tokenizing the training and the test set
tokenizer = nltk.tokenize.RegexpTokenizer(r'\w+')
train['text'] = train['text'].apply(lambda x : tokenizer.tokenize(x))
test['text'] = test['text'].apply(lambda x : tokenizer.tokenize(x))
train['text'].head()
3. 불용어 제거
이제 불용어를 제거해 보겠습니다. a, an, the, are과 같이 자주 등장하지만 별 다른 의미가 없는 단어를 제거하겠습니다.
import nltk
nltk.download('stopwords')
def remove_stopwords(text):
'''
Removing stopwords belonging to english language
'''
words = [w for w in text if w not in stopwords.words('english')]
return words
train['text'] = train['text'].apply(lambda x : remove_stopwords(x))
test['text'] = test['text'].apply(lambda x : remove_stopwords(x))
train.head()
4. 토큰 정규화
토큰 정규화는 서로 다른 모양의 토큰들을 기본 형태로 바꿔주는 것을 말합니다. 두 가지 방법으로 토큰 정규화가 가능합니다.
- Stemming(어간 추출) : 접미사를 제거하고 대체하여 stem이라 불리는 단어의 기본 형태를 얻는 방법 ( cats - cat, wolves - wolv)
- Lemmatization(표제어 추출) : 단어를 기본형이나 사전적인 모양으로 바꿔주는 방법
import nltk
nltk.download('wordnet')
nltk.download('omw=1.4')
# Stemming and Lemmatization examples
text = 'feet cats wolves talked'
tokenizer = nltk.tokenize.TreebankWordTokenizer()
tokens = tokenizer.tokenize(text)
# Stemmer
stemmer = nltk.stem.PorterStemmer()
print('Stemming the sentence : ', ' '.join(stemmer.stem(token) for token in tokens))
# Lemmatizer
lemmatizer = nltk.stem.WordNetLemmatizer()
print('Lemmatizing the sentence : ', ' '.join(lemmatizer.lemmatize(token) for token in tokens))
# outputs
Stemming the sentence : feet cat wolv talk
Lemmatizing the sentence : foot cat wolf talked
여기서 우리가 알아야 할 점은 우리는 단어를 자르지 않고 원래의 모습을 그대로 유지하기를 바랄때가 있기 때문에 Stemming과 Lemmatizationdms 은 항상 결과를 향상시키는 것은 아니라는 것입니다. 따라서 우리가 다루는 문제에 따라서 위의 방법을 사용할지를 판단해야 합니다. 이 문제에서 저는 위의 방법을 사용하지 않겠습니다.
# After preprocessing, the text format
def combine_text(list_of_text):
''' Takes a list of text and combines them into one large chunk of text.'''
combined_text = ' '.join(list_of_text)
return combined_text
train['text'] = train['text'].apply(lambda x : combine_text(x))
test['text'] = test['text'].apply(lambda x : combine_text(x))
train['text'].head()
# outputs
0 deeds reason earthquake may allah forgive us
1 forest fire near la ronge sask canada
2 residents asked shelter place notified officer...
3 people receive wildfires evacuation orders cal...
4 got sent photo ruby alaska smoke wildfires pou...
Name: text, dtype: object
모든 과정을 한 번에 - 텍스트 전처리 함수 정의
나중에 다시 사용하기 편하게 하기 위해서 지금까지 전처리 과정을 담은 함수를 정의하는 것이 좋을 것 같습니다.
# text preprocessing function
def text_preprocessing(text):
'''
Cleaning and parsing the text.
'''
tokenizer = nltk.tokenize.RegexpTokenizer(r'\w+')
nopunc = clean_text(text)
tokenized_text = tokenizer.tokenize(nopunc)
remove_stopwords = [w for w in text if w not in stopwords.words('english')]
combined_text = ' '.join(remove_stopwords)
return combined_text
5. 토큰 벡터화
초기 전처리 과정이 끝난 후에 우리는 텍스트를 의미가 있는 숫자들의 벡터나 배열로 바꿔야 합니다. 벡터화를 진행하는 방법은 다양합니다.
Bag of Words
bag-of-words는 문장에서 단어의 빈도수를 설명하는 방법을 말합니다. 여기에는 두가지 내용이 포함 되어있습니다.
- 단어장 ( 전체 문장에서 발견되는 단어 )
- 단어의 존재 유무 측정 ( 원-핫 벡터로 문장에 단어가 존재하는지 표현 )
왜 이 방법이 'bag' of words라고 불릴까요?
그 이유는 문서내에서 단어의 순서나 구조에 대한 정보가 버려지기 때문이고 모델은 문서에서 단어가 어디에 위치하냐가 아니라 존재 하는지에만 관심이 있기 때문입니다.
우리는 사이킷런의 CountVectorizer를 이용해 bag-of-words를 만들 수 있습니다. bag-of-words의 모든 행은 각각의 트윗들을 나타내고, 모든 열은 각각 다른 단어들을 나타냅니다
Bag of Words - CountVectorizer Features
Countvectorizer는 텍스트 문서들의 모음을 토큰 개수에 대한 행렬로 바꿉니다. CountVectorizer는 자동으로 전처리, 토큰화, 불용어 제거를 진행해주는 다양한 옵션을 가지고 있습니다. 하지만 우리는 위에서 이해를 돕기위해 단계별로 과정을 진행했습니다. 따라서 우리는 따로 매개변수를 지정해주지 않고 countvectorizer의 기본 모형을 사용 하겠습니다.
count_vectorizer = CountVectorizer()
train_vectors = count_vectorizer.fit_transform(train['text'])
test_vectors = count_vectorizer.fit_transform(test['text'])
## Keeping only non-zero elements to preserve space
print(train_vectors[0].todense())
# outputs
[[0 0 0 ... 0 0 0]]
TFIDF Features
Bag of Words의 문제점 중 하나는 매우 자주나오는 단어가 문서를 지배하지만, 그 단어에 많은 정보가 있지 않을 수도 있다는 것입니다. 또한 짧은 문서보다 긴 문서에 더 많은 가중치를 준다는 문제가 있습니다.
문제 해결을 위한 한가지 방법은 단어의 빈도를 모든 문서에서 나타나는 빈도로 재조정하여 모든 문서에서 자주 나타나는 'the'와 같은 단어에 대한 점수가 불이익을 받도록 하는 것입니다. 이렇게 점수를 매기는 방법은 Term Frequency_Inverse Document Frequency 라고 불리며 간단하게 TF-IDF라고도 부릅니다.
Term Frequency : 현재 문서에서의 단어 빈도에 대한 점수
TF = (Number of times term t appears in a document) / (Number of terms in the documents)
Inverse Document Frequency : 전체 문서에서 단어가 얼마나 희귀한지에 대한 점수
IDF = 1+log(N/n), N은 문서의 숫자, n은 t가 나타난 문서의 숫자
tfidf = TfidfVectorizer(min_df=2, max_df=0.5, ngram_range=(1,2))
train_tfidf = tfidf.fit_transform(train['text'])
test_tfidf = tfidf.fit_transform(test['text'])
6. 텍스트 분류 모델 생성
이제 분류 모델에 사용하기 위한 데이터 준비는 끝났습니다. 이제 보편적으로 사용되는 분류 알고리즘을 이용해 기본적인 분류 모델을 만들어보고 모델이 어떻게 작동하는지 보겠습니다.
Logistic Regression Classifier
# Fitting a simple Logistic Regression on Counts
clf = LogisticRegression(C=1.0)
scores = model_selection.cross_val_score(clf, train_vectors, train['target'], cv=5, scoring='f1')
scores
# outputs
array([0.59865255, 0.49611063, 0.57166948, 0.56290774, 0.68789809])
clf.fit(train_vectors, train['target'])
# Fitting a simple Logistic Regression on TFIDF
clf_tfidf = LogisticRegression(C=1.0)
scores = model_selection.cross_val_score(clf_tfidf, train_tfidf, train['target'], cv=5, scoring='f1')
scores
# outputs
array([0.57229525, 0.49673203, 0.54277829, 0.46618106, 0.64768683])
이 경우에는 countvectorizer가 TFIDF보다 더 좋은 성능을 보입니다.
Naive Bayes Classifier
위 점수 정도면 괜찮은 점수입니다. 이번에는 텍스트 데이터에서 성능이 잘 나온다는 Naive Bayes모델을 이용해 보겠습니다.
# Fitting a simple Naive Bayes on Counts
clf_NB = MultinomialNB()
scores = model_selection.cross_val_score(clf_NB, trian_vectors, train['target'], cv=5, scoring='f1')
scores
# outputs
array([0.63149079, 0.60675773, 0.68575519, 0.64341085, 0.72505092])
clf_NB.fit(train_vectors, train['target'])
# Fitting a simple Naive Bayes on TFIDF
clf_NB_TFIDF = MultinomialNB()
scores = model_selection.cross_val_score(clf_NB_TFIDF, train_tfidf, train['target'], cv=5, scoring='f1')
scores
# outputs
array([0.57590597, 0.57092511, 0.61135371, 0.5962963 , 0.7393745 ])
원래 노트북에서는 TFIDF를 이용한 naive bayes모델의 점수가 logistic regression모델보다 훨씬 좋다고 하는데 아무리봐도 그냥 logistic regression모델이 더 좋아보이는데...
XGBoost
import xgboost as xgb
clf_xgb = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1)
scores = model_selection.cross_val_score(clf_xgb, train_vectors, train['target'], cv=5, scoring='f1')
scores
# outputs
array([0.47379913, 0.37379576, 0.43988816, 0.38900634, 0.53142857])
clf_xgb_TFIDF = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1)
scores = model_selection.cross_val_score(clf_xgb_TFIDF, train_tfidf, train['target'], cv=5, scoring='f1')
scores
# outputs
array([0.48947951, 0.34406439, 0.43140965, 0.40084388, 0.53014354])
- 참고
https://en.wikipedia.org/wiki/Precision_and_recall
https://deepai.org/machine-learning-glossary-and-terms/f-score
https://kavita-ganesan.com/text-preprocessing-tutorial/#.Yx7kI-xBw-S
'Kaggle 필사 & 리뷰 > NLP' 카테고리의 다른 글
[NLP] Kaggle 필사 커리큘럼(진행중) (0) | 2022.10.04 |
---|---|
your first NLP competition submission (0) | 2022.09.18 |
End-to-End NLP(EDA & ML) with Sentiment Analysis (0) | 2022.09.11 |