• mksong8

[데이터 분석] FastText를 이용한 Twitter 감성 분석



사용하기 쉽고 빠르게 학습 할 수 있는 FastText를 라이브러리를 사용하여 트위터 감정성 분석을 해 보겠습니다. ​ ​ | FastText란? ​ FastText는 페이스북에서 개발한 NLP 라이브러리이며 무료 오픈소스 텍스트 분석기입니다. ​



| 왜 FastText인가? ​ 신경망 모델의 단점은 모델링과 테스트에 시간이 많이 걸린다는 점입니다. 그러나 FastText는 모델링에 시간이 많이 걸리지 않습니다. 그러나 분석 정확도는 신경망 모델이 뒤지지 않습니다.





FastText를 감성 분석에 어떻게 적용하는지 알아보도록 하겠습니다. ​

| 데이터세트 가져오기 ​ betsentiment.com 사이트의 데이터를 사용하여 분석을 해 보겠습니다. 레이블은 긍정, 부정, 중립, 혼합 이 네 가지로 구분되어 있으며 혼합 레이블의 트윗은 분석에서 제외하였습니다. 팀 관련 트윗을 훈련 데이터 셋으로 사용하고 선수 관련 트윗을 테스트 셋으로 사용하였습니다. ​ ​ | 데이터 정제 ​ 데이터를 분석 가능하게 정리하는 작업입니다. 다음과 같은 순서로 작업을 진행합니다. ​ 1. 모든 해시 태그를 삭제합니다. 2. 가중치를 두어 분석을 하지 않을 예정이므로 멘션은 삭제합니다. 3. 이모티콘을 의미가 같은 텍스트로 수정합니다. 4. 줄임말을 줄이기 전으로 되돌립니다. 5. URL을 제거합니다. 6. 문장부호를 제거합니다. 7. 오타를 수정합니다. 8. 모두 소문자로 수정합니다. 9. HTML 태그를 제거합니다. ​ ​ 다음의 트윗 데이터를 정제해 보겠습니다. ​ tweet = '<html> bayer leverkusen goalkeeeeper bernd leno will not be #going to napoli. his agent uli ferber to bild: "I can confirm that there were negotiations with napoli, which we have broken off. napoli is not an option." Atletico madrid and Arsenal are the other strong rumours. #b04 #afc </html>' ​ ​ ● HTML태그 제거 ​ HTML를 제거하기 위해 Beautifulsoup 라이브러리를 사용합니다. ​ tweet = BeautifulSoup(tweet).get_text() ​ #output 'bayer leverkusen goalkeeeeper bernd leno will not be #going to napoli. his agent uli ferber to bild: "I can confirm that there were negotiations with napoli, which we have broken off. napoli is not an option." Atletico madrid and Arsenal are the other strong rumours. #b04 #afc' ​ 상황에 따라 정규식을 사용하여 제거하거나 수정할 수 있습니다. 이 경우에는 re 패키지를 사용합니다. ​ ​ ● 해시태그 제거 ​ 정규식 @[A-Za-z0-9]+는 멘션을 나타내고, #[A-Za-z0-9]+는 해시태그를 의미합니다. 멘션과 해시태그는 삭제합니다. ​ tweet = ' '.join(re.sub("(@[A-Za-z0-9]+)|(#[A-Za-z0-9]+)", " ", tweet).split()) ​ #output 'bayer leverkusen goalkeeeeper bernd leno will not be to napoli. his agent uli ferber to bild: "I can confirm that there were negotiations with napoli, which we have broken off. napoli is not an option." Atletico madrid and Arsenal are the other strong rumours.' ​ ​ ● URL 삭제 ​ 정규식 \w+:\/\/\S+는 http:// 또는 https:// 로 시작하는 URL을 삭제해 줍니다. ​ tweet = ' '.join(re.sub("(\w+:\/\/\S+)", " ", tweet).split()) ​ #output 'bayer leverkusen goalkeeeeper bernd leno will not be to napoli. his agent uli ferber to bild: "I can confirm that there were negotiations with napoli, which we have broken off. napoli is not an option." Atletico madrid and Arsenal are the other strong rumours.' ​ ​ ● 문장 부호 제거 ​ .,!?:;-=를 삭제합니다. ​ tweet = ' '.join(re.sub("[\.\,\!\?\:\;\-\=]", " ", tweet).split()) ​ #output 'bayer leverkusen goalkeeeeper bernd leno will not be napoli his agent uli ferber to bild "I can confirm that there were negotiations with napoli which we have broken off napoli is not an option " Atletico madrid and Arsenal are the other strong rumours' ​ ​ ● 소문자 변환 ​ 대문자를 모두 소문자로 수정합니다. ​ tweet = tweet.lower() ​ #output 'bayer leverkusen goalkeeeeper bernd leno will not be napoli his agent uli ferber to bild "i can confirm that there were negotiations with napoli which we have broken off napoli is not an option " atletico madrid and arsenal are the other strong rumours' ​ ​ ● 줄임말 수정 ​ 줄임말을 줄이기 전의 용어로 수정합니다. 이와 관련된 DB가 없었기 대문에 직접 메타데이터 DB를 생성하였습니다. ​ CONTRACTIONS = {"mayn't":"may not", "may've":"may have",......} ​ tweet = tweet.replace("’","'") words = tweet.split() reformed = [CONTRACTIONS[word] if word in CONTRACTIONS else word for word in words] tweet = " ".join(reformed) ​ #input 'I mayn’t like you.' ​ #output 'I may not like you.' ​ ​ ● 오타 수정 ​ 작업을 간단하게 하기 위해 같은 문자가 2번 반복되는 단어를 찾아냅니다. ​ tweet = ''.join(''.join(s)[:2] for _, s in itertools.groupby(tweet)) ​ #output 'bayer leverkusen goalkeeper bernd leno will not be napoli his agent uli ferber to bild "i can confirm that there were negotiations with napoli which we have broken off napoli is not an option " atletico madrid and arsenal are the other strong rumours' ​ ​ ● 이모티콘 수정 ​ 이모티콘을 의미하는 단어로 수정합니다. 이모티콘 패키지를 사용하고 없는 이모티콘의 경우 직접 DB를 만들었습니다. ​ SMILEYS = {":‑(":"sad", ":‑)":"smiley", ....} ​ words = tweet.split() reformed = [SMILEY[word] if word in SMILEY else word for word in words] tweet = " ".join(reformed) ​ #input 'I am :-(' ​ #output 'I am sad' ​ 이모티콘 패키지는 :flushed_face: 를 반환하기 때문에 ":"를 삭제하는 처리를 추가로 하였습니다. ​ tweet = emoji.demojize(tweet) tweet = tweet.replace(":"," ") tweet = ' '.join(tweet.split()) ​ #input 'He is 😳' ​ #output 'He is flushed_face' ​ 데이터 분석 결과에 유의미한 결과가 나타날 수 있기 때문에 중지 단어(stop words)를 사용하지 않았습니다. ​ ​ | 데이터 형식 변환 FastText 지도학습에 적합하도록 데이터의 형식을 수정합니다. ​ FastText에서 레이블은 __label__로 시작합니다. 입력값은 다음과 같습니다. ​ __label__NEUTRAL _d i 'm just fine i have your fanbase angry over __label__POSITIVE what a weekend of football results & hearts ​ 다음과 같이 데이터를 변환합니다. ​ def transform_instance(row): cur_row = [] #Prefix the index-ed label with __label__ label = "__label__" + row[4] cur_row.append(label) cur_row.extend(nltk.word_tokenize(tweet_cleaning_for_sentiment_analysis(row[2].lower()))) return cur_row def preprocess(input_file, output_file): i=0 with open(output_file, 'w') as csvoutfile: csv_writer = csv.writer(csvoutfile, delimiter=' ', lineterminator='\n') with open(input_file, 'r', newline='', encoding='latin1') as csvinfile: # encoding='latin1' csv_reader = csv.reader(csvinfile, delimiter=',', quotechar='"') for row in csv_reader: if row[4]!="MIXED" and row[4].upper() in ['POSITIVE','NEGATIVE','NEUTRAL'] and row[2]!='': row_output = transform_instance(row) csv_writer.writerow(row_output ) # print(row_output) i=i+1 if i%10000 ==0: print(i) ​ ​ 긍정, 부정, 중립 외의 트윗은 분석에서 제외하였습니다. ​ nltk.word_tokenize()는 문자열을 독립적인 단어들로 변환합니다. ​ ​ nltk.word_tokenize('hello world!') ​ #output ['hello', 'world', '!'] ​

| 데이터셋 샘플링 ​ 레이블마다 트윗 개수가 다릅니다. 중립 레이블의 데이터가 전체 데이터의 약 72%입니다. ​ ​ import pandas as pd import seaborn as sns ​ df = pd.read_csv('betsentiment-EN-tweets-sentiment-teams.csv',encoding='latin1') df['sentiment'].value_counts(normalize=True)*100 ​ sns.countplot(x="sentiment", data=df)​




각 레이블의 데이터 수가 동일해야 합니다. 이 조건을 맞추기 위해 반복해서 업 샘플링 작업을 수행하였습니다. ​ ​ def upsampling(input_file, output_file, ratio_upsampling=1): # Create a file with equal number of tweets for each label # input_file: path to file # output_file: path to the output file # ratio_upsampling: ratio of each minority classes vs majority one. 1 mean there will be as much of each class than there is for the majority class ​ i=0 counts = {} dict_data_by_label = {} ​ # GET LABEL LIST AND GET DATA PER LABEL with open(input_file, 'r', newline='') as csvinfile: csv_reader = csv.reader(csvinfile, delimiter=',', quotechar='"') for row in csv_reader: counts[row[0].split()[0]] = counts.get(row[0].split()[0], 0) + 1 if not row[0].split()[0] in dict_data_by_label: dict_data_by_label[row[0].split()[0]]=[row[0]] else: dict_data_by_label[row[0].split()[0]].append(row[0]) i=i+1 if i%10000 ==0: print("read" + str(i)) ​ # FIND MAJORITY CLASS majority_class="" count_majority_class=0 for item in dict_data_by_label: if len(dict_data_by_label[item])>count_majority_class: majority_class= item count_majority_class=len(dict_data_by_label[item]) ​ # UPSAMPLE MINORITY CLASS data_upsampled=[] for item in dict_data_by_label: data_upsampled.extend(dict_data_by_label[item]) if item != majority_class: items_added=0 items_to_add = count_majority_class - len(dict_data_by_label[item]) while items_added<items_to_add: data_upsampled.extend(dict_data_by_label[item][:max(0,min(items_to_add-items_added,len(dict_data_by_label[item])))]) items_added = items_added + max(0,min(items_to_add-items_added,len(dict_data_by_label[item]))) ​ # WRITE ALL i=0 ​ with open(output_file, 'w') as txtoutfile: for row in data_upsampled: txtoutfile.write(row+ '\n' ) i=i+1 if i%10000 ==0: print("writer" + str(i)) ​ ​ 업 샘플링을 반복하면 모델이 오버핏 될 가능성이 있으나 데이터세트가 충분히 크기 때문에 문제가 되지 않을 것입니다. ​

| 트레이닝 (모델링) ​ 지도 학습을 이용하였습니다. ​ hyper_params = {"lr": 0.01, "epoch": 20, "wordNgrams": 2, "dim": 20} ​ print(str(datetime.datetime.now()) + ' START=>' + str(hyper_params) ) ​ # Train the model. model = fastText.train_supervised(input=training_data_path, **hyper_params) print("Model trained with the hyperparameter \n {}".format(hyper_params)) ​ ​ lr은 학습률, epoch는 epoch 개수, wordNgrams은 Ngram 단어 중 최대 길이, dim는 단어 벡터의 크기를 말합니다. ​ train_supervised는 지도학습 모델링을 하기 위해 사용되는 함수입니다. ​ ​

| 평가 ​ 모델의 정확도를 평가합니다. ​ ​ model_acc_training_set = model.test(training_data_path) model_acc_validation_set = model.test(validation_data_path) ​ # DISPLAY ACCURACY OF TRAINED MODEL ​ text_line = str(hyper_params) + ",accuracy:" + str(model_acc_training_set[1]) + ",validation:" + str(model_acc_validation_set[1]) + '\n' ​ print(text_line) ​ ​ test는 모델의 정확도를 나타냅니다. 테스트 셋의 정확도는 79.7%, 트레이닝 세트의 정확도는 97.5%입니다. ​

| 예측 ​ 생성된 모델에 트윗을 적용하여 레이블을 예측해 보겠습니다. ​ model.predict(['why not'],k=3) model.predict(['this player is so bad'],k=1) ​ predict는 문자열의 감정 레이블을 예측하고 k는 반환할 레이블의 개수를 지정합니다. ​

| 모델 수량화 ​ model.quantize(input=training_data_path, qnorm=True, retrain=True, cutoff=100000) ​ ​

| 모델 저장 ​ model.save_model(os.path.join(model_path,model_name + ".ftz")) ​ ​ ​ | 결론

트윗 데이터를 정제하는 과정부터 FastText를 이용하여 모델링, 예측하는 과정까지 설명해 보았습니다. 감성 분석이 필요하시면 유펜솔루션과 상의해 주세요. : )

조회 124회

​고객센터

Tel: 02-596-8900  Fax : 02-6930-5709

10시 - 오후 7시(토, 일요일 및 공휴일은 휴무)

개인정보관리책임자 : 황재준   상호 : 유펜솔루션   대표자 : 김재훈   사업자등록번호 : 426-86-00939
주소 : (본사)대전광역시 유성구 엑스포로446번길 38, 3층 302호 / (지사 및 연구소)서울시 성동구 연무장 15길 11, B동 2층
​ⓒ 2019 UpennSolution Co., Ltd. All rights reserved.