ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [NLP] 자연어처리 - 한국어 전처리를 위한 기법들
    ✨ AI/NLP 2020. 10. 22. 17:10

     

     

    한국어는 너무 어렵다. 띄어쓰기 차이, 한 글자 차이 등으로 의미가 달라진다. 많은 자연어 처리 책이나 논문등에서 영어에 대한 전처리 기법은 많지만 한국어에 대한 전처리 및 임베딩이 약했습니다. 하지만 최근에는 오픈프로젝트나 개인이 라이브러리나 오픈소스를 개발해주셔서..! 많이 좋아졌습니다.

     

     

     

    데이터 전처리?

    텍스트 데이터 관련 신경망 모델을 만들어야 한다.

    일단 그러면 많은 양의 텍스트, 즉 코퍼스(Corpus)가 필요할 것이다. 크롤링이나 오픈 데이터 등을 통해 일단 얻는다. 하지만 띄어쓰기가 잘못되 있는 것도 있을 것이고, 맞춤법이 틀린 것도 있을 것이다. 이렇게 사소한 차이도 임베딩 벡터로 보면 큰 차이일 수 있기 때문에 처음에 전처리를 잘하는 것이 중요하다.

     

     

    그래서 오늘은 한국어 전용 텍스트 전처리에는 어떤 것이 있는지 알아보겠습니다.

     

     

    • Basic
    • Tokenize
    • Spell Check
    • Pos Tag
    • Stemming
    • Stopwords
    • Negation

     

     

     


     

    0. Basic

    • 기초적인 전처리,
    • html tag 제거(크롤링한 데이터일경우)
    • 숫자, 영어, 특수문자 등 필요하지 않은 언어 제거
    • Lowercasing
    • "@%*=()/+ 와 같은 punctuation(문장부호) 제거

     

     

    punct = "/-'?!.,#$%\'()*+-/:;<=>@[\\]^_`{|}~" + '""“”’' + '∞θ÷α•à−β∅³π‘₹´°£€\×™√²—–&'
    punct_mapping = {"‘": "'", "₹": "e", "´": "'", "°": "", "€": "e", "™": "tm", "√": " sqrt ", "×": "x", "²": "2", "—": "-", "–": "-", "’": "'", "_": "-", "`": "'", '“': '"', '”': '"', '“': '"', "£": "e", '∞': 'infinity', 'θ': 'theta', '÷': '/', 'α': 'alpha', '•': '.', 'à': 'a', '−': '-', 'β': 'beta', '∅': '', '³': '3', 'π': 'pi', }
    
    
    def clean_punc(text, punct, mapping):
        for p in mapping:
            text = text.replace(p, mapping[p])
        
        for p in punct:
            text = text.replace(p, f' {p} ')
        
        specials = {'\u200b': ' ', '…': ' ... ', '\ufeff': '', 'करना': '', 'है': ''}
        for s in specials:
            text = text.replace(s, specials[s])
        
        return text.strip()

     

    import re
    
    
    def clean_text(texts):
        corpus = []
        for i in range(0, len(texts)):
            review = re.sub(r'[@%\\*=()/~#&\+á?\xc3\xa1\-\|\.\:\;\!\-\,\_\~\$\'\"]', '',str(texts[i])) #remove punctuation
            review = re.sub(r'\d+','', str(texts[i]))# remove number
            review = review.lower() #lower case
            review = re.sub(r'\s+', ' ', review) #remove extra space
            review = re.sub(r'<[^>]+>','',review) #remove Html tags
            review = re.sub(r'\s+', ' ', review) #remove spaces
            review = re.sub(r"^\s+", '', review) #remove space from start
            review = re.sub(r'\s+$', '', review) #remove space from the end
            corpus.append(review)
        return corpus

     

     


     

     

     

     

    1. Tokenize

     

    자연어 처리에서는 텍스트를 '토큰 단위'로 나눈다.

    'I am a student' 영어에서 토큰은 단순히 띄어쓰기를 해서 보면 되지만, 'i', 'am', 'a', 'student'

    한국어에서 띄어쓰기는 텍스트 의미를 구분하는데 큰 영향을 준다.

     

    문맥이 없이 단순 띄어쓰기로만 토큰을 구분한다면, 그 의미의 구분이 명확하지가 않다.

    -> '학교종이 땡땡땡' -> '학교 종이' or '학교종이' ..?

     

     

    띄어쓰기에 따라 분석이 제대로 되지 않을 수 있다.

    -> '너무기대안하고갔나 재밌게봤다' -> '너', '무기', '대안', '하고', '갔나', '재밌게' ....

     

    띄어쓰기를 해주는 오픈소스 라이브러리가 있지만 문제점이 있다.

    대체로 수집한 데이터에 학습된 알고리즘으로 구성되었기 때문에 코퍼스 도메인에 따라 띄어쓰기 성능이 다를 수 있다. 예를 들어 라이브러리1은 지식인 같은 대화체 데이터이고 라이브러리2는 위키피디아같은 문어체라면,

    대화체에 라이브러리2를 사용하면 잘 안먹힐 수 있다는 얘기다.

     

     

    애초에 모든 공백을 없앤 후, 문맥에 따라 띄어쓴 문장을 만드는 것이 좋은 방법

    -> '너무기대를 안했나봐' -> '너무기대를안했나봐' -> '너무 기대를 안 했나 봐'

     

     


     

    띄어쓰기 방식(출처)

    1. 경계 인식 방식

    : 띄어 쓰기 전 토큰의 영향을 많이 받는다.

     

     

     

    2. 영역 인식 방식

    : 띄어 쓰는 지점 주변 토큰의 영향을 고르게 받는다.

     

     


     

     

    - 한국어 Tokenizer

     

    1. ADAMS.ai 의 띄어쓰기 API (Saltlux)
    2. RAWS (Real-time Automatic Word Segmentation)
    3. KoSpacing (w/ pre-trained embedding and weights)
    4. 부산대 맞춤법 검사기 (띄어쓰기만 반영)
    5. Daum 맞춤법 검사기 (띄어쓰기만 반영)
    6. 네이버 맞춤법 검사기 (띄어쓰기만 반영)
    7. 핑퐁 ChatSpace

     

     

    여기서 3번 PyKoSpacing을 사용해보겠습니다.

     

    pip install git+https://github.com/haven-jeon/PyKoSpacing.git

     

    from pykospacing import spacing
    
    
    print(spacing("김형호영화시장분석가는'1987'의네이버영화정보네티즌10점평에서언급된단어들을지난해12월27일부터올해1월10일까지통계프로그램R과KoNLP패키지로텍스트마이닝하여분석했다."))

     

    // 결과
    
    김형호 영화시장 분석가는 '1987'의 네이버 영화 정보 네티즌 10점 평에서 언급된 단어들을 지난해 12월 27일부터 올해 1월 10일까지 통계 프로그램 R과 KoNLP 패키지로 텍스트마이닝하여 분석했다.

     

    띄어쓰기가 될 때, 문장부호의 역할이 중요한 듯 하니 띄어쓰기 먼저 하고 문장부호 제거를 해야겠다. 문장부호 제거를 어느 순서로 하느냐도 좀 고려해 봐야할 필요가 있을 것 같다.

     

     


     

     

    ※ 그리고 띄어쓰기 말고 문장 분리라는 것이 있다.

     

    한국어 문장분리 파이썬 라이브러리 kss

     

    문장 분리의 경우 형태소 분석으로 종결어미를 구분한다던지, 문장의 CRF(Conditional Random Field) 결과로 판단하는 방법이 있다. kss - Korean Sentence Splitter 은 pip으로 빠르게 설치가 가능하며, 형태소분석기(KoNLPy)가 성능이 비슷비슷한 것을 감안했을 때, kss로 성능을 높일 수도 있다.

     

    kss는 정교한 패턴 기반의 문장 분리기라는 장점과 함께 통계 기반에 비해 월등히 뛰어난 속도를 보인다.

     

    알고리즘(출처)

    여기서는 성능을 획기적으로 개선할 수 있는 다른 방식의 접근이 필요하다. 이에 따라 종결형에 사용되는 음절을 골라내어 이전/이후 음절을 매칭하여 문장을 구분했다. 여기서 골라낸 음절은 다/요/.!? 이며, 이전/이후 패턴을 미리 정의해 두고 이후 패턴은 2음절까지 확장해 나가며, 어느 부위에서 정확히 끊어낼지 Sentence Boundary를 지정했다. 해당 알고리즘은 내가 알고 있는 국내 최고의 NLP 엔지니어 중 한 명이자 카카오 NLP 리더인 김응균님의 알고리즘에 큰 영향을 받았다.

     

    pip install kss

     

    import kss
    
    
    s = "회사 동료 분들과 다녀왔는데 분위기도 좋고 음식도 맛있었어요 다만, 강남 토끼정이 강남 쉑쉑버거 골목길로 쭉 올라가야 하는데 다들 쉑쉑버거의 유혹에 넘어갈 뻔 했답니다 강남역 맛집 토끼정의 외부 모습."
    for sent in kss.split_sentences(s):
        print(sent)

     

    // 결과
    회사 동료 분들과 다녀왔는데 분위기도 좋고 음식도 맛있었어요
    다만, 강남 토끼정이 강남 쉑쉑버거 골목길로 쭉 올라가야 하는데 다들 쉑쉑버거의 유혹에 넘어갈 뻔 했답니다
    강남역 맛집 토끼정의 외부 모습.

     

     


     

     

    2. Spell Check

     

    py-hanspell은 네이버 맞춤법 검사기를 이용한 파이썬용 한글 맞춤법 검사 라이브러리이다.

     

     

    pip install py-hanspell

    pip으로 설치할 수도 있고, 깃저장소에서 직접 내려받아 설치할 수도 있다.

    필요한 의존 라이브러리는 requests

     

     

    사용법1 - dict로 출력

    from hanspell import spell_checker
    
    
    result = spell_checker.check(u'안녕 하세요. 저는 한국인 입니다. 이문장은 한글로 작성됬습니다.')
    result.as_dict()  # dict로 출력
    {'checked': '안녕하세요. 저는 한국인입니다. 이 문장은 한글로 작성됐습니다.',
     'errors': 4,
     'original': '안녕 하세요. 저는 한국인 입니다. 이문장은 한글로 작성됬습니다.',
     'result': True,
     'time': 0.07065701484680176,
     'words': {'안녕하세요.': 2,
               '저는': 0,
               '한국인입니다.': 2,
               '이': 2,
               '문장은': 2,
               '한글로': 0,
               '작성됐습니다.': 1}}
    >>> result
    Checked(result=True, original='안녕 하세요. 저는 한국인 입니다. 이문장은 한글로 작성됬습니다.', checked='안녕하세요. 저는 한국인입니다. 이 문장은 한글로 작성됐습니다.', errors=4, words=OrderedDict([('안녕하세요.', 2), ('저는', 0), ('한국인입니다.', 2), ('이', 2), ('문장은', 2), ('한글로', 0), ('작성됐습니다.', 1)]), time=0.10472893714904785)

     

     

    사용법2 - list로 주고받기

    from hanspell import spell_checker
    
    
    spell_checker.check([u'안녕 하세요.', u'저는 한국인 입니다.'])
    [Checked(result=True, original='안녕 하세요.', checked='안녕하세요.', errors=1, words=OrderedDict([('안녕하세요.', 2)]), time=0.03297615051269531),
     Checked(result=True, original='저는 한국인 입니다.', checked='저는 한국인입니다.', errors=1, words=OrderedDict([('저는', 0), ('한국인입니다.', 2)]), time=0.029018878936767578)]

     

     

    파라미터들

    result 맞춤법 검사 성공여부를 나타냅니다.
    original 검사 전의 문장입니다.
    checked 맞춤법 검사 후의 문장입니다.
    errors 맞춤법 오류 수를 나타냅니다.
    words Checked.words
    time 총 요청 시간을 나타냅니다.

     

     

    Checked.words

    위 사용 방법에 나와있는 words 부분은 교정된 최종 문장을 공백으로 나눈(split) 결과입니다.

    결과는 key가 단어, value가 CheckResult를 나타냅니다.

    아래 코드를 참고하세요.

    for key, value in result.words.items():
    ...    print(key, value)
    안녕하세요. 2
    저는 0
    한국인입니다. 2
    이 2
    문장은 2
    한글로 0
    작성됐습니다. 1
    

     

     

    CheckResult

    아래 코드로 import 하신 후 비교에 사용할 수 있는 상수입니다.

    from hanspell.constants import CheckResult
    .PASSED 맞춤법 검사 결과 문제가 없는 단어 또는 구절
    .WRONG_SPELLING 맞춤법에 문제가 있는 단어 또는 구절
    .WRONG_SPACING 띄어쓰기에 문제가 있는 단어 또는 구절
    .AMBIGUOUS 표준어가 의심되는 단어 또는 구절
    .STATISTICAL_CORRECTION 통계적 교정에 따른 단어 또는 구절

     


     

     

    ※ 그리고, 반복되는 이모티콘이나 자모 ㅎㅎㅎㅎ 하하하하 ㅋㅋㅋ 같은 것을 nomalize하는 라이브러리가 있습니다.

     

    !pip install soynlp
    from soynlp.normalizer import *
    print(repeat_normalize('와하하하하하하하하하핫', num_repeats=2))

     

     

     

    ※ 그리고 외래어 사전을 다운받아 사용하실 수도 있습니다.

     

    !curl -c ./cookie -s -L "https://drive.google.com/uc?export=download&id=1RNYpLE-xbMCGtiEHIoNsCmfcyJP3kLYn" > /dev/null
    !curl -Lb ./cookie "https://drive.google.com/uc?export=download&confirm=`awk '/download/ {print $NF}' ./cookie`&id=1RNYpLE-xbMCGtiEHIoNsCmfcyJP3kLYn" -o confused_loanwords.txt

     

    lownword_map = {}
    lownword_data = open('/content/confused_loanwords.txt', 'r', encoding='utf-8')
    
    lines = lownword_data.readlines()
    
    for line in lines:
        line = line.strip()
        miss_spell = line.split('\t')[0]
        ori_word = line.split('\t')[1]
        lownword_map[miss_spell] = ori_word

     

    def spell_check_text(texts):
        corpus = []
        for sent in texts:
            spaced_text = spacing(sent)
            spelled_sent = spell_checker.check(sent)
            checked_sent = spelled_sent.checked
            normalized_sent = repeat_normalize(checked_sent)
            for lownword in lownword_map:
                normalized_sent = normalized_sent.replace(lownword, lownword_map[lownword])
            corpus.append(normalized_sent)
        return corpus
        
    spell_preprocessed_corpus = spell_check_text(basic_preprocessed_corpus)

    3. Pos Tag

    konlpy의 형태소 분석기의 성능은 비슷비슷 하다고 하고 그나마 mecab이 가장 뛰어난 것으로 알려져있다. 하지만 mecab은 윈도우에서 사용하지 못한다. 하지만 Python기반의 형태소 분석기 중 카카오에서 Khaiii라는 오픈 소스를 제공하였고 성능이 가장 좋은 것 중하나가 되었다.

     

     

    설치

    !git clone https://github.com/kakao/khaiii.git
    !pip install cmake
    !mkdir build
    !cd build && cmake /content/khaiii
    !cd /content/build/ && make all
    !cd /content/build/ && make resource
    !cd /content/build && make install
    !cd /content/build && make package_python
    !pip install /content/build/package_python

     

    from khaiii import KhaiiiApi
    api = KhaiiiApi()
    
    test_sents = ["나도 모르게 사버렸다."]
    
    for sent in test_sents:
        for word in api.analyze(sent):
            for morph in word.morphs:
                print(morph.lex + '/' + morph.tag)
        print('\n')

     

    // 결과
    
    나/NP
    도/JX
    모르/VV
    게/EC
    사/VV
    아/EC
    버리/VX
    었/EP
    다/EF
    ./SF

     

     

     

    significant_tags = ['NNG', 'NNP', 'NNB', 'VV', 'VA', 'VX', 'MAG', 'MAJ', 'XSV', 'XSA']
    
    def pos_text(texts):
        corpus = []
        for sent in texts:
            pos_tagged = ''
            for word in api.analyze(sent):
                for morph in word.morphs:
                    if morph.tag in significant_tags:
                        pos_tagged += morph.lex + '/' + morph.tag + ' '
            corpus.append(pos_tagged.strip())
        return corpus

     

    // 결과 예시
    
    제임스/NNP 얼/NNG 지/NNP 미/NNG 카터/NNP 주니/NNG 어/NNP 민주당/NNP 출신/NNG 미국/NNP 번/NNB 대통령/NNG

     

     

     


    4. Stemming

     

    동사를 원형으로 복원한다..!

     

    p1 = re.compile('[가-힣A-Za-z0-9]+/NN. [가-힣A-Za-z0-9]+/XS.')
    p2 = re.compile('[가-힣A-Za-z0-9]+/NN. [가-힣A-Za-z0-9]+/XSA [가-힣A-Za-z0-9]+/VX')
    p3 = re.compile('[가-힣A-Za-z0-9]+/VV')
    p4 = re.compile('[가-힣A-Za-z0-9]+/VX')
    def stemming_text(text):
        corpus = []
        for sent in text:
            ori_sent = sent
            mached_terms = re.findall(p1, ori_sent)
            for terms in mached_terms:
                ori_terms = terms
                modi_terms = ''
                for term in terms.split(' '):
                    lemma = term.split('/')[0]
                    tag = term.split('/')[-1]
                    modi_terms += lemma
                modi_terms += '다/VV'
                ori_sent = ori_sent.replace(ori_terms, modi_terms)
            
            mached_terms = re.findall(p2, ori_sent)
            for terms in mached_terms:
                ori_terms = terms
                modi_terms = ''
                for term in terms.split(' '):
                    lemma = term.split('/')[0]
                    tag = term.split('/')[-1]
                    if tag != 'VX':
                        modi_terms += lemma
                modi_terms += '다/VV'
                ori_sent = ori_sent.replace(ori_terms, modi_terms)
    
            mached_terms = re.findall(p3, ori_sent)
            for terms in mached_terms:
                ori_terms = terms
                modi_terms = ''
                for term in terms.split(' '):
                    lemma = term.split('/')[0]
                    tag = term.split('/')[-1]
                    modi_terms += lemma
                if '다' != modi_terms[-1]:
                    modi_terms += '다'
                modi_terms += '/VV'
                ori_sent = ori_sent.replace(ori_terms, modi_terms)
    
            mached_terms = re.findall(p4, ori_sent)
            for terms in mached_terms:
                ori_terms = terms
                modi_terms = ''
                for term in terms.split(' '):
                    lemma = term.split('/')[0]
                    tag = term.split('/')[-1]
                    modi_terms += lemma
                if '다' != modi_terms[-1]:
                    modi_terms += '다'
                modi_terms += '/VV'
                ori_sent = ori_sent.replace(ori_terms, modi_terms)
            corpus.append(ori_sent)
        return corpus

     

     


    5. Stopwords

     

    불용어 처리

    - 갖고 있는 데이터에서 유의미한 단어 토큰만을 선별하기 위해서는 큰 의미가 없는 단어 토큰을 제거하는 작업

     

    stopwords = ['데/NNB', '좀/MAG', '수/NNB', '등/NNB']
    
    def remove_stopword_text(text):
        corpus = []
        for sent in text:
            modi_sent = []
            for word in sent.split(' '):
                if word not in stopwords:
                    modi_sent.append(word)
            corpus.append(' '.join(modi_sent))
        return corpus

     

    참고

     

     


     

     

     

    정리

     

    • 띄어쓰기 - py-spacing
    • 문장분리 - kss
    • 문장부호제거 - python의 re이용
    • 맞춤법 검사 - py-hanspell ( + 반복 이모티콘, 자모 정규화/외래어처리)
    • 형태소 분리 - konlpy

     

     


     

     

    References

     

    한국어 전처리

    kss(Korean Sentence Splitter)

    대화체에 유연한 띄어쓰기 모델 만들기

    한국어 띄어쓰기 프로그램 도전기

    댓글

Designed by Tistory.