반응형

💬 한국어 텍스트 데이터 전처리

  • 텍스트 데이터는 보통 그 자체로 사용하기보다, 의미의 단위로 나눠서 활용 여부나 사이 연관 관계를 찾는다.
  • 저번 장에서 확인한 대로, 데이터셋의 텍스트 데이터는 한국어 문장으로 구성되어 있다.
  • 학습할 때마다 한국어 형태소 분리를 실행해도 되지만, 시간이 너무 많이 걸려서, 텍스트 데이터를 전처리 해놓기로 한다. 

한국어 텍스트 데이터 전처리 방법

  • 텍스트 데이터의 전처리 단계는 다음과 같다.
1. 텍스트 데이터를 형태소 단위로 분리한다.
2. 분리된 데이터 중, 불용어를 제거한다. (Optional)
3. 동의어를 mapping 할 수 있다면, 동의어를 mapping한다. (Optional)
4. word dictionary를 생성
5. word를 정수 인코딩한다.

1. 한국어 형태소 분리 (Tokenization)

  • 한국어의 경우에는 영어와 달리, 의미의 단위가 띄어쓰기와 정확히 일치하지 않는다.
  • 과거부터 한국어 형태소 분리가 연구되어 여러 방법들이 존재한다.
  • 파이썬은 "konlpy" 라이브러리에서 다양한 형태소 분석기를 제공한다. (자세한 내용은 https://konlpy.org/ko/latest/index.html 참조)
  • konlpy에서는 다양한 형태소 분석기를 제공해 주지만, 그중 "Okt"(Twitter에서 개발한 오픈소스 한국어 처리기)를 사용하여 한국어 형태소 분리 처리를 하기로 한다. (선택한 이유는 속도 때문이다.)
처리 전 처리 후
"아버지가 방에 들어가신다."  ["아버지","가","방","에","들어가신다","."]

2. 불용어 처리

  • 불용어(Stopword)는 분석에 필요하지 않거나, 분석 결과에 영향을 미치는 단어를 말한다. (예시: 은,는,이,가)
  • 자연어 처리에서는 불용어 제거가 매우 중요하다. 불용어는 실제 분석에는 이용되지 않지만, 일반적으로 문장에서 등장하는 빈도수가 높아져서, 해당 단어들이 중요한 단어로 인식될 수가 있다. 
  • 또한, 불용어 제거는 전처리 과정에서 처리할 텍스트 데이터의 양이 줄어서 처리 속도 향상을 위해 꼭 필요하다. 
  • 다만, 불용어는 분석의 목적에 따라 달라지는 상대적인 개념이기 때문에, 분석에 영향을 미치지 않는 단어만 넣도록 한다. 예를 들어, 일반적으로 '?'는 Elastic Search 등의 텍스트 검색을 위한 처리에서 불용어 처리가 될 수 있지만, 우리가 다루고자 하는 비윤리적 텍스트 검출에서는 상대를 비꼬는 문장을 찾을 수 있는 중요한 단서가 되기도 한다. 
  • 일반적으로 특수문자는 문장에서 제거한 후에, 한국어 형태소 분리하지만, 해당 Task에서는 특수문자가 분류에 많이 사용될 것으로 생각되어서, 실제 사용되지 않을 것으로 보이는 특수문자만 불용어로 처리하였다.  
불용어 예시 : 

stopwords = ['','','','','','','','','','','으로','','하다','!','?','<','>','(',')','[',']','|','#','.']

3. 동의어 처리

  • 동의어 처리는 일반적으로 텍스트 검색 기능에서 유사한 단어를 찾기 위해 자주 사용된다. 
  • 비윤리적 텍스트 문장 검출 데이터셋에서는 일반적인 단어를 특수문자나 축약어로 표현해 놓은 데이터가 많다. (예시: ㅇㅋ, Ok, 오키, 옿키)
  • 좋은 성능의 검출 모델을 만들기 위해서 동의어 처리도 매우 중요할 것으로 생각되지만, 현실적으로 공수가 너무 많이 소요되어, 별도의 동의어 처리는 하지 않았다. 

4. Word Dictionary 생성 

  • 학습 데이터셋을 형태소 분리하고, 불용어와 동의어 처리까지 처리한 후, 학습 데이터를 기반으로 워드 딕셔너리를 생성한다.
  • 실사용 시, Word Dictionary에 포함되지 않는 단어가 등장했을 경우에는 모델은 해당 단어에 대한 정보를 사용할 수 없다. (자연어 처리 시, 다양하고 풍부한 학습 데이터가 필요한 이유이기도 하다.) 
  • 각 단어에 대한 고유의 번호를 지정한다. 
  • 모든 단어를 사용하는 것이 다양성 측면에서는 좋겠지만, 속도나 성능을 고려하면, word dictionary의 크기를 무작정 늘리는 것은 좋지 않다. (일반적으로 최대 단어 개수를 지정한다. )
  • 정수 인코딩과 모델 테스트 시 빠른 사용을 위해, 인덱싱(key: 숫자, value: word)과 역인덱싱(key: word, value: 숫자)을 모두 진행해 놓는 것이 좋다.  
문장 형태소 분리 Word Dictionary 인덱싱
"아버지가 방에 들어가신다." ["아버지","가","방","에","들어가신다","."]  {<unk>:0,<pad>:1,"아버지":2, "방":3, "들어가신다":4} {'0':<unk>,'1':<pad>,'2': "아버지", '3':"방", '4':"들어가신다"}

5. 텍스트의 Word를 정수 인코딩

  • 모델의 학습을 위해, 텍스트를 정수로 인코딩해주는 작업이 필요하다. 
  • Word Dictionary를 기반으로 학습 데이터의 각 단어들을 정수로 변환한다.
  • Word Dictionary에 포함되지 않은 불용어 등은 "<unk>"라는 wildcard로 치환한다. 이를 통해, 모델이 입력받는 word 데이터의 종류는 Word Dictionary에 존재하는 단어 개수로 제한된다. 
  • 정수 인코딩을 진행하면, 문장마다의 인코딩 벡터의 길이는 전부 다르다. 학습을 위해 데이터를 정형화하는 편이 좋기 때문에, 인코딩 벡터의 끝부분에 "<pad>"값을 넣어, 벡터의 길이를 모두 같게 만들어준다. 
처리 전 처리 후
"아버지가 방에 들어가신다."  2,0,3,0,4,0,1,1,1,1

 

데이터 전처리 코드 구현

  • 전처리 과정을 사전에 진행해 놓기 위한, 코드를 구현하였다. 다만, 학습과 테스트 시 코드 통일성을 위해, 정수 인코딩 부분은 모델 데이터셋 정의 과정에 넣었다. (테스트 데이터도 정수 인코딩은 진행해야 하기 때문에, Dataset 구성 파트에서 설명 예정)
  • 자연어 Dataset 처리를 용이하게 하기 위해, "torchtext"라는 pytorch에서 제공해 주는 자연어 처리용 패키지를 사용하였다. (하지만, pytorch는 데이터셋의 종류에 따른 전처리를 모두 통일하기 위해 현재는 torch를 사용하도록 권장하고 있다. )
  • Okt가 빠르긴 하지만, 처리 데이터가 많기 때문에 전처리에 시간이 많이 소요된다. 
  • 데이터 전처리 단계를 대략적으로 구성하였지만, 실사용에서는 불용어 지정이나 어느 종류의 형태소 분석기를 사용할 것인지, word dictionary를 어떻게 구성할 것인지 등이 성능을 결정하는 매우 중요한 단계이다. 
from konlpy.tag import *
from torchtext import data 
import json

import pandas as pd

tokenizer = Okt()
stopwords = ['의','가','에','들','는','잘','걍','과','도','를','으로','한','하다','!','?','<','>','(',')','[',']','|','#','.']

# 텍스트 전처리 함수
def norm_morphs(x):
    x = tokenizer.normalize(x) # 텍스트 Normalization
    x = tokenizer.morphs(x) # 형태소 분리
    x = [word for word in x if not word in stopwords] #불용어 처리
    return x


if __name__ =='__main__':
    # 데이터셋 위치 지정
    data_dir = r"..\korean_language\data"

    # ID: 문서의 번호, TEXT: 문장 데이터(전처리 함수를 지정할 수 있음), LABEL: 윤리성 유무를 나타내는 LABEL 
    ID = data.Field(sequential=False, use_vocab=False)
    TEXT = data.Field(sequential=True, use_vocab=True, tokenize = norm_morphs, batch_first=True, tokenizer_language='ko')
    LABEL = data.Field(sequential=False, use_vocab=False, is_target=True)

    # Torch Text의 splits를 이용해서, 데이터를 한번에 불러올 수 있다. 
    train_data, test_data = data.TabularDataset.splits(path=data_dir, train='train', test='test', format='tsv', fields=[('id',ID), ('label',LABEL),('temp1',None),('temp2',None),('temp3',None),('text',TEXT)], skip_header=True)

    # word dictionary를 만듬 (최대 크기와, 최소 빈도수를 지정)
    TEXT.build_vocab(train_data, min_freq=2, max_size=100000)

    # word dictionary를 저장
    with open('./dictionary.json','w') as f:
        json.dump(TEXT.vocab.stoi, f, ensure_ascii=False, indent=4)

    # Index dictionary를 저장 
    index_dict = {v: k for k, v in TEXT.vocab.stoi.items()}
    with open('./index_dictionary.json','w') as f:
        json.dump(index_dict, f, ensure_ascii=False, indent=4)


    id_list = []
    text_list = []
    label_list = []
    df = pd.DataFrame()

    for id, data_dict in enumerate(train_data):
        id_list.append(id)
        text_list.append('|'.join(data_dict.text))
        label_list.append(data_dict.label)

    df['id'] = id_list
    df['text'] = text_list
    df['label'] = label_list
    df.to_csv('train.csv', index=False,sep = '#')

    id_list = []
    text_list = []
    label_list = []
    df = pd.DataFrame()

    for id, data_dict in enumerate(test_data):
        id_list.append(id)
        text_list.append('|'.join(data_dict.text))
        label_list.append(data_dict.label)

    df['id'] = id_list
    df['text'] = text_list
    df['label'] = label_list

    df.to_csv('test.csv', index=False,sep = '#')


    print("Train Data :",len(train_data))
    print("Test Data :",len(test_data))

 

전처리를 진행하였으니, Dataset을 정의하고, 모델을 만들어볼 차례이다!

+ Recent posts