Codong's Development Diary RSS 태그 관리 글쓰기 방명록
python/자연어처리 (11)
2021-04-11 14:59:16

개요


살다보니 생각보다 자연어처리가 재밌기도 하고, 실제로도 많이 이용하게 되는 것 같아서 지대로 공부를 해보고 싶어졌다. 너무 수박 겉핥기 식으로만 알고 있었던 것 같아서 하나씩 정리하면서 차근차근 공부해보자! 원래는 종이 책을 하나 뗄까도 싶었지만, 페이지 넘기는 것도 귀찮기에 갓키독스(wikidocs)에 있는 갓(유)원준님의 딥러닝을 이용한 자연어 처리 입문으로 정했다! 예제도 잘 되어있어서 정말 좋다!

그럼 하나씩 정독하면서 중요한 부분을 정리하면서 내 생각과 이해한 것을 적어보도록 하자~~😆


시작하기에 앞서, 전처리란?!

자연어 처리에서 크롤링 등으로 얻어낸 코퍼스 데이터를 필요에 맞게 사용하기 위해서는 전처리를 진행해야 한다. 요리로 비유를 하자면, 재료를 날 것 그대로 사용한다면 맛을 보장할 수 없을 것이다. 우리는 성능을 보장할 수 없지 않겠는가? 그렇다면 어떻게 해야할까. 데이터를 용도에 맞게 사용하고자 토큰화, 정제, 정규화를 진행해야 한다.


1. 토큰화(Tokenization)


첫번째 토큰화는 주어진 코퍼스(corpus)에서 토큰(token)이라 불리는 단위로 나누는 작업을 토큰화라고 한다. 이 토큰의 단위는 상황에 따라 다르지만, 보통 의미있는 단위로 토큰을 정의한다.


* 토큰화에서 고려해야할 사항

토큰화 작업을 단순하게 코퍼스에서 구두점을 제외하고 공백 기준으로 잘라내는 작업이라고 간주할 수는 없다. 그 이유에 대해 살펴보자.

  1. 구두점이나 특수 문자를 단순 제외할 때
    • ex. 21/02/06 -> 날짜 , $100,000 -> 돈을 나타낼 때
  2. 줄임말과 단어 내 띄어쓰기
    • ex. we're -> we are 의 줄임말./ rock n roll -> 하나의 단어지만 띄어쓰기가 존재.
  3. 문장 토큰화 : 단순 마침표를 기준으로 자를 수 없음.
    • ex. IP 192.168.56.31 서버에 들어가서 로그 파일 저장해서 ukairia777@gmail.com로 결과 좀 보내줘. 그러고나서 점심 먹으러 가자.

* 한국어 토큰화의 어려움

영어는 New York과 같은 합성어나 he's 와 같이 줄임말에 대한 예외처리만 한다면, 띄어쓰기(whitespace)를 기준으로 하는 띄어쓰기 토큰화를 수행해도 단어 토큰화가 잘 작동한다.

  • 영어와는 달리 한국어에는 조사라는 것이 존재
    • ex. '그가', '그에게', '그를', '그와', '그는'과 같이 다양한 조사가 붙음. 같은 단어임에도 서로 다른 조사가 붙어서 다른 단어로 인식이
  • 한국어는 띄어쓰기가 영어보다 잘 지켜지지 않는다.
    • ex. 띄어쓰기를안해도사람들은이해를합니다.

🤔 그럼 어쩌란거지?

한국어 토큰화에서는 형태소(morpheme)란 뜻을 가진 가장 작은 말의 단위인 이 개념을 반드시 이해해야 한다. 이 형태소에는 두 가지 형태소가 있는데 자립 형태소와 의존 형태소가 존재 한다.

  1. 자립 형태소 : 접사, 어미, 조사와 상관없이 자립하여 사용할 수 있는 형태소. 그 자체로 단어가 된다. 체언(명사, 대명사, 수사), 수식언(관형사, 부사), 감탄사 등이 있다.
  2. 의존 형태소 : 다른 형태소와 결합하여 사용되는 형태소. 접사, 어미, 조사, 어간를 말한다.
  • ex. 문장 : 에디가 딥러닝책을 읽었다.
    1. 자립 형태소 : 에디, 딥러닝책
    2. 의존 형태소 : -가, -을, 읽-, -었, -다

한국어 토큰화를 도와주는 형태소 분석기

konlpy의 Kkma, Okt, mecab 또는 Pykomoran 등이 있다. 형태소 분석기 마다 성능이 다르기에 결과가 다르다.

  • 대표적 형태소 분석기의 기능
    1) morphs : 형태소 추출
    2) pos : 품사 태깅(Part-of-speech tagging)
    3) nouns : 명사 추출

3가지 분석기의 형태소 추출(morphs)를 실행했는데 시간도 다르고 결과도 다른 것을 알 수 있다. 그렇기에 필요 용도에 따라 적절한 분석기를 사용하면 된다. 예시에는 없지만 속도가 중요하다면 mecab을 이용할 수 있다.


2. 정제(Cleaning)와 정규화(Normalization)


토큰화 작업 전, 후에는 텍스트 데이터를 용도에 맞게 정제 및 정규화하는 일이 항상 함꼐한다. 목적은 다음과 같다.

  1. 정제 : 갖고 있는 코퍼스로부터 노이즈 데이터를 제거한다.
  2. 정규화 : 표현 방법이 다른 단어들을 통합시켜서 같은 단어로 만들어준다.

2.1 정제(Cleaning)


2.1.1 정규 표현식(Regular Expression)

  • 얻어낸 코퍼스에서 노이즈 데이터의 특징 및 패턴을 잡아낼 수 있다면, 정규 표현식을 통해서 이를 제거할 수 있는 경우가 많다. 코퍼스 내에 계속해서 등장하는 글자들을 규칙에 기반하여 한 번에 제거하는 방식으로서 사용 가능.
    ex. 뉴스 기사를 크롤링 -> 기사 게재 시간 등

2.1.2 불필요한 단어 제거 (Removing Unnecessary Words)

자연어가 아니면서 아무 의미도 갖지 않는 글자들(특수 문자 등) 뿐만아니라 분석하고자 하는 목적에 맞지 않는 불필요 단어들을 노이즈 데이터라고 하기도 한다.

  1. 등장 빈도가 적은 단어
    • ex. 100,000개의 메일 데이터에서 총 합 5번 밖에 등장하지 않은 단어의 경우 직관적으로 분류에 거의 도움이 되지 않을 것
  2. 길이가 짧은 단어(Removing words with very a short length)
    영어는 길이가 2~3 이하인 단어를 제거하는 것만으로도 크게 의미를 갖지 못하는 단어를 줄이는 효과를 갖고 있지만, 한국어 단어는 한자어가 많고, 한 글자만으로도 이미 의미를 가진 경우가 많다
    • ex. 영어 : 2~3 글자 이하 it, at, to, on, in, by 불용어 제거 가능.
      한국어 : 용(龍) 한국어로는 한 글자 영어에서는 d, r, a, g, o, n 6글자.

➕ 한국어에서 불용어 제거하기

간단하게는 토큰화 후에 조사, 접속사 등을 제거하기. 조사나 접속사와 같은 단어들뿐만 아니라 명사, 형용사와 같은 단어들 중에서 불용어로서 제거하고 싶은 단어들이 생기기도 한다. 결국에는 사용자가 직접 불용어 사전을 만들게 되는 경우가 많다.

  • 예를 들어 문장에서 의도를 파악하는 것을 하려고 할 때
from konlpy.tag import Okt, Kkma, Komoran
okt=Okt()

text='시원한 콜라, 그리고 맛있는 햄버거 포장해 주세요.'
stop_words=['시원한', '맛있는', '그리고', '해', '주세요', ',', '.']

word_token = okt.morphs(text)
print(word_token)
# ['시원한', '콜라', ',', '그리고', '맛있는', '햄버거', '포장', '해', '주세요', '.']

result=[]
for word in word_token:
    if word not in stop_words:
        result.append(word)

print(result)
# ['콜라', '햄버거', '포장']

2.2 정규화(Normalization)

규칙에 기반한 표기가 다른 단어들의 통합을 생각해 볼 수 있다. 종류에는 어간 추출(stemming)과 표제어 추출(lemmatizaiton) 등이 있다. 자연어 처리에서 전처리, 더 정확히는 정규화의 지향점은 언제나 갖고 있는 코퍼스로부터 복잡성을 줄이는 일이다.

➕ 형태소의 두 가지 종류 : 어간(stem)과 접사(affix)

1) 어간(stem) : 단어의 의미를 담고 있는 단어의 핵심 부분.
2) 접사(affix) : 단어에 추가적인 의미를 주는 부분. ex. cat(어간)와 -s(접사)


2.2.1 표제어 추출(Lemmatization)

한글로는 '표제어' 또는 '기본 사전형 단어' 정도의 의미

  • ex. am, are, is는 서로 다른 스펠링이지만 그 뿌리 단어인 be는 이 단어들의 표제어라고 할 수 있다.

2.2.2 어간 추출(Stemming)

어간 추출은 형태학적 분석을 단순화한 버전, 정해진 규칙만으로 단어의 어미를 자르는 어림짐작의 작업이라고 볼 수도 있다.

  • ex. formalize → formal / allowance → allow / electricical → electric 이와 같이 단순 어미 자름.

한국어에서의 어간 추출 : 용언에 해당되는 '동사'와 '형용사'는 어간(stem)과 어미(ending)의 결합으로 구성

품사
체언 명사, 대명사, 수사
수식언 관형사, 부사
관계언 조사
독립언 감탄사
용언 동사, 형용사

➕ 활용(conjugation) : 용언의 어간(stem)이 어미(ending)를 가지는 일을 말한다.

  1. 규칙활용 : 어간의 모습이 일정.
    ex. 잡/어간 + 다/어미
  2. 불규칙활용 : 어간이나 어미의 모습이 변함. 단순한 분리만으로 어간 추출이 되지 않고 좀 더 복잡한 규칙을 필요로 함.
    ex. ‘듣-, 돕-, 곱-, 잇-, 오르-, 노랗-’ 등이 ‘듣/들-, 돕/도우-, 곱/고우-, 잇/이-, 올/올-, 노랗/노라-’와 같이 어간의 형식이 달라지는 일이 있거나 ‘오르+ 아/어→올라, 하+아/어→하여, 이르+아/어→이르러, 푸르+아/어→푸르러’와 같이 일반적인 어미가 아닌 특수한 어미를 취하는 경우

➕ 한국어 자연어처리 파이썬 라이브러리 soynlp의 normalization

from soynlp.normalizer import *

emoticon_normalize('ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ쿠ㅜㅜㅜㅜㅜㅜ', num_repeats=3)
# 'ㅋㅋㅋㅜㅜㅜ'

repeat_normalize('와하하하하하하하하하핫', num_repeats=2)
# '와하하핫'

only_hangle('가나다ㅏㅑㅓㅋㅋ쿠ㅜㅜㅜabcd123!!아핫')
# '가나다ㅏㅑㅓㅋㅋ쿠ㅜㅜㅜ 아핫'

only_hangle_number('가나다ㅏㅑㅓㅋㅋ쿠ㅜㅜㅜabcd123!!아핫')
# '가나다ㅏㅑㅓㅋㅋ쿠ㅜㅜㅜ 123 아핫'

only_text('가나다ㅏㅑㅓㅋㅋ쿠ㅜㅜㅜabcd123!!아핫')
# '가나다ㅏㅑㅓㅋㅋ쿠ㅜㅜㅜabcd123!!아핫'

3. 정수 인코딩(Integer Encoding)


컴퓨터는 텍스트보다는 숫자를 더 잘 처리 할 수 있다. 이를 위해 텍스트를 숫자로 바꾸는 여러가지 기법들이 있다. 그 전에 첫 단계로 각 단어를 고유한 정수에 맵핑(mapping)시키는 전처리 작업이 필요할 때가 있다. 인덱스를 부여하는 방법랜덤으로 부여하기도 하지만, 보통은 전처리 또는 단어 빈도수를 기준으로 정렬한 뒤에 부여한다.
ex. 텍스트에 단어가 5,000개 존재시 각각 1번부터 5,000번까지 단어와 맵핑되는 고유한 정수, 다른 표현으로는 인덱스를 부여. 가령, book은 150번, dog는 171번과 같이 숫자가 부여

결론 : 컴퓨터가 알아먹기 쉽게 바꿔주는 것!


➕ 실습해보기

단어에 정수를 부여하는 방법 중 하나로 단어를 빈도수 순으로 정렬한 단어 집합(vocabulary)을 만들고, 빈도수가 높은 순서대로 차례로 낮은 숫자부터 정수를 부여하는 방법이 있다. 구현하는 방법은 다양하다.

  1. dictionary 사용하기
  2. 내장 모듈 Counter 사용하기
  3. 내장 함수 enumerate 사용하기
  4. Keras Tokenizer 사용하기

>> 예제 코드 보러가기


4. 패딩(Padding)


자연어 처리를 하다보면 문장(또는 문서)의 길이가 서로 다를 수 있다. 그런데 기계는 길이가 전부 동일한 문서들에 대해서는 하나의 행렬로 보고, 한꺼번에 묶어서 처리할 수 있다. 다시 말해 병렬 연산을 위해서 여러 문장의 길이를 임의로 동일하게 맞춰주는 작업이 필요할 때가 있다.

쉽게 말해 병렬 연산을 위해 문장(또는 문서)의 길이를 동일하게 맞춰 주는 작업이다. 길면 자르고, 짧으면 특정 값으로 채워준다.


➕ 실습해보기

패딩을 할 때 가장 긴 길이를 가진 문서의 길이를 기준으로 패딩을 한다고 능사는 아니다. 가령, 모든 문서의 평균 길이가 20인데 문서 1개의 길이가 5,000이라고 해서 굳이 모든 문서의 길이를 5,000으로 패딩할 필요는 없을 수 있다. 반대로 너무 짧게 잡으면 잘려나가는 데이터들이 많이 존재하므로, 문서 길이의 분포를 보고 결정하는 것이 좋다.

  1. Numpy로 패딩
  2. Keras 전처리 도구로 패딩

>> 예제 코드 보러가기


5. 원-핫 인코딩(One-Hot Encoding)


단어 집합의 크기를 벡터의 차원으로 하고, 표현하고 싶은 단어의 인덱스에 1의 값을 부여하고, 다른 인덱스에는 0을 부여하는 단어의 벡터 표현 방식입니다. 두 가지 과정으로 정리 할 수 있다. 1) 정수 인코딩 2) 해당 단어 1부여, 나머지 0부여

➕ 예제

label = ['한식', '중식', '일식', '양식']
word2index={}
for idx, word in enumerate(label):
    word2index[word]=idx

print(word2index)
# {'한식' : 0, '중식' : 1, '일식' : 2, '양식' : 3}

정수 인코딩을 진행한 후,

# 원-핫 인코딩 함수 정의
def one_hot_encoding(word, word2index):
    one_hot_vector = [0]*(len(word2index))
    index=word2index[word]
    one_hot_vector[index]=1
    return one_hot_vector

vec=one_hot_encoding("한식",word2index)
print(vec)
# [1,0,0,0]

또는 Keras 의 to_categorical을 이용해서 정수 인코딩 된 리스트를 인풋으로 넣으면 쉽게 얻을 수 있다.

from tensorflow.keras.utils import to_categorical

# 아까 한식중식 정수 인코딩 된 것.
encoded=[0,1,2,3]
one_hot = to_categorical(encoded)
print(one_hot)
#[[1, 0, 0, 0] #인덱스 0의 원-핫 벡터
#[0, 1, 0, 0] #인덱스 1의 원-핫 벡터
#[0, 0, 1, 0] #인덱스 2의 원-핫 벡터
#[0, 0, 0, 1]] #인덱스 3의 원-핫 벡터

원-핫 인코딩의 한계

  1. 단어의 개수가 늘어날 수록, 벡터를 저장하기 위해 필요한 공간이 계속 늘어난다는 단점
    • ex. 단어 1000개일 경우 1의 값을 가지는 1개 빼곤 999개의 값은 0을 가짐.
  2. 단어 유사도 표현 못함.

이를 보완하기 위한 벡터화 기법

  1. 카운트 기반의 벡터화 방법 : LSA, HAL 등
  2. 예측 기반으로 벡터화 방법 : NNLM, RNNLM, Word2Vec, FastText 등
  3. 두 가지 방법을 모두 사용 : GloVe

⚖️ 데이터의 분리 (Splitting Data)


이 파트는 머신 러닝(딥 러닝) 모델에 데이터를 훈련시키기 위해 데이터를 분리하는 작업은 꼭 필요하기에 남겨두었다. 기본적이지만 모르면 안되는 부분이기에 혹시나 유용히 쓰이는 것이 있을 수 있다.


1️⃣ X,Y 분리하기

  1. zip 함수 이용
sequences=[['a', 1], ['b', 2], ['c', 3]] # 리스트의 리스트 또는 행렬 또는 2D 텐서.
X,y = zip(*sequences)
# 또는 (위 아래 결과 똑같음)
X,y = zip(['a', 1], ['b', 2], ['c', 3])

print(X) # ('a', 'b', 'c')
print(y) # (1, 2, 3)
  1. pandas 데이터프레임 이용
import pandas as pd

values = [['당신에게 드리는 마지막 혜택!', 1],
['내일 뵐 수 있을지 확인 부탁드...', 0],
['도연씨. 잘 지내시죠? 오랜만입...', 0],
['(광고) AI로 주가를 예측할 수 있다!', 1]]
columns = ['메일 본문', '스팸 메일 유무']

df = pd.DataFrame(values, columns=columns)
X=df['메일 본문']
Y=df['스팸 메일 유무']
print(X) # ['당신에게 드리는 마지막 혜택!', '내일 뵐 수 있을지 확인 부탁드...', ...]
print(Y) # [1, 0, 0, 1]
  1. numpy 이용
import numpy as np
ar = np.arange(0,16).reshape((4,4))
print(ar)
# [[ 0  1  2  3]
# [ 4  5  6  7]
# [ 8  9 10 11]
# [12 13 14 15]]

X=ar[:, :3]
y=ar[:,3]
print(X) # [[ 0  1  2], [ 4  5  6], [ 8  9 10], [12 13 14]]
print(y) # [3 7 11 15]

2️⃣ 테스트 데이터 분리하기

이건 정말 필요하다. 이미 분리된 X,y 셋에서 어느정도 비율을 가지고 훈련 셋과 테스트 셋을 분리할 떄 유용하다.

  1. scikit-learn 이용하기
# test_size에 테스트 셋을 몇 퍼센트 넣을 것인지 지정해준다. ex) 0.2 => 8:2 비율로 나누겠다.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2, random_state=1234)
  1. 직접 분리하기
import numpy as np
X, y = np.arange(0,24).reshape((12,2)), range(12)
# 실습을 위해 임의로 X와 y가 이미 분리 된 데이터를 생성

# 몇개까지 자를 건지 지정
n_of_train = int(len(X) * 0.8) # 데이터의 전체 길이의 80%에 해당하는 길이값을 구한다.
n_of_test = int(len(X) - n_of_train) # 전체 길이에서 80%에 해당하는 길이를 뺀다.
print(n_of_train) # 9
print(n_of_test) # 3

# 위의 값 기준으로 자르기.
X_test = X[n_of_train:] #전체 데이터 중에서 20%만큼 뒤의 데이터 저장
y_test = y[n_of_train:] #전체 데이터 중에서 20%만큼 뒤의 데이터 저장
X_train = X[:n_of_train] #전체 데이터 중에서 80%만큼 앞의 데이터 저장
y_train = y[:n_of_train] #전체 데이터 중에서 80%만큼 앞의 데이터 저장

reference

2021-03-16 18:49:36

개요


정규표현식으로 자연어 처리를 할 때에 있던 일이다. 분명 한글인데, 인식을 못하는 경우가 있다. 인코딩 문제가 아닐까 싶다. 정확한 원인 규명은 못했지만 내 나름대로 해결한 문제를 기록해본다.


문제점


내가 정규표현식을 통해 한글을 검색하려 했었는데, None을 뱉어내는 경우가 있어서 이상하게 여겨 한번 테스트를 해봤다.

subs=[
  '대법원 2016. 10. 13. 선고 2016두42449 판결',
  '대법원 2017. 11. 23. 선고 2015다1017, 1024, 1031, 1048 판결'
  ]

com=re.compile(r"[가-힣]")
for sub in subs:
  search_word=com.search(sub)
  print(f'{sub}에서 검색 결과 : {search_word}')

# 출력 결과
# 대법원 2016. 10. 13. 선고 2016두42449 판결에서 검색 결과 : None
# 대법원 2017. 11. 23. 선고 2015다1017, 1024, 1031, 1048 판결에서 검색 결과 : <re.Match object; span=(0, 3), match='대법원'>

??? 이게 무슨 일이지? subs 안에 들어있는 두 문장은 영락없는 한글인데 하나는 되고, 다른 하나는 왜 안될까?
나는 내 정규식이 틀렸나 싶어서 https://regexr.com/ 이 사이트에서 확인을 해보았다.

??? 왜 또 안될까? 어이가 없어서 저 안되는 부분을 똑같이 복붙해봤다.

이번엔 위에껀 잡히는데 밑에껀 또 안잡힌다;;;; 뭔 차이가 있을까 싶어서 밑에 결과를 살펴보니 알 수 있었다.

뭔 차이인지 알겠는가? 보시다시피 code가 다른 것을 알 수 있다! 인코딩이 잘못된 것 같다는 생각이 들었다...

그리고 마우스를 가져다 대면,

저렇게 나뉘어지는 것을 보고 인코딩이 잘못되었다는 것을 확신할 수 있다.

# 원본 텍스트
original='대법원 2016. 10. 13. 선고 2016두42449 판결'
# 그대로 직접 타이핑한 텍스트
copy='대법원 2016. 10. 13. 선고 2016두42449 판결'

print(len(original),len(copy))

# 출력 결과
# 47, 34

심지어 길이도 달랐다. 그러니 내가 단어를 찾을 수가 없었던 것이었다... 이런걸 어떻게 사용해야할까...


내가 생각한 해결법


어차피 정규식도 유니코드로 찾아내는 거니까 코드 번호로 명시해주면 찾아지지 않을까? 라는 생각을 했다. 그래서 실제로 시도한 결과..

잘 잡혔다! 대충 어림잡아 4200~4800사이로 바꿔서 \u(16진수) 이와 같이 넣어주었다.


21/04/01 업데이트 내용.


이후에 업무를 하다가, 윈도우에서 만든 텍스트 파일을 mac에서 사용하려니 문제가 생겼다. cp949로 인코딩해서 내용물을 활용한 것 까지는 좋았다. 하지만 내가 코드 중에, 파일 제목을 변수에 담아서 내용과 비교를 하는 로직을 짠 적이 있었다. 또 찾을 수 없다고 뜨길래 확인해보니 위의 상황들과 같았다.

import glob

file_list=glob.glob('./data/*.txt')

for file in file_list:
    title=file.split('/')[2]
    load_file(title)

    ...

# 결과    
KeyError: 'title'

오잉? 분명 있는 것인데 왜 없다고 하지??? 그래도 이번에 문제점은 확실히 알았다(사실 몇시간 끙끙 앓았음...). 내용은 파이썬 내부함수로 파일을 읽으면서 인코딩하면 되지만, 파일 제목은 인코딩이 되지 않는다는 점이다. 그래서 이것을 어떻게 다시 돌려놓았는가 함은...!!!

from unicodedata import normalize
import glob

# 여기서 파일 명을 들고오면서 인코딩이 안된 것을 가지고 사용하다보니 에러가 난것이다..
file_list=glob.glob('./data/*.txt')


for file in file_list:
    title=file.split('/')[2]
    title = normalize('NFC', title)
    load_file(title)

    ...

from unicodedata import normalize 를 임포트 해와서!!
title = normalize('NFC', title) NFC 로 맹글어주면 된닷 ㅎㅎ

정말 간단했다.. 코드 몇줄로 끝나다니 살짝 허무하긴 했는데, 해결되서 다행이다 😩
여기서 NFC가 뭔지 궁금하신 분들은 이 곳 에서 설명이 잘 되있으니 참고하시면 될 것 같다!


마무리


DB에서 데이터를 가져오다보니 어쩌다 이렇게까지 하게 되었는지 모르겠지만, 원인이 정확히 어디서부터 그렇게 된지 찾기가 쉽지가 않다. window와 mac 사이를 파일로 왔다갔다해서 그런가 인코딩이 엉킨거 같기도 하다. 어떻게 통일화할지는 아직 생각을 못해봤는데, 그것도 언젠가 찾아서 하게 되면 안까먹게 포스팅 해야겠다 😋

reference

2021-03-11 21:34:31

👋 개요


한글 자연어처리 라이브러리로 konlpy나 mecab을 사용하여 형태소 분석이나, 명사추출을 할 때, 신조어나 복합명사들이 제대로 추출되지 않는 경우가 있다. 그런 경우 따로 분석기에 사용자 사전을 추가해서 그러한 문제를 보완할 수 있다.

하지만 매번 사람이 일일이 다 찾아서 작성할 순 없는 노릇이다. 그러면 어떻게 하면 좋을까? 🤔

 

👍 soynlp


한국어 분석을 위한 한국어 자연어처리 라이브러리다. 학습데이터를 이용하지 않으면서 데이터에 존재하는 단어를 찾거나, 문장을 단어열로 분해, 혹은 1품사 판별을 할 수 있는 비지도학습 접근법을 지향한다. 여러가지 버전의 명사 추출기를 제공하고 있다.

from soynlp.noun import NewsNounExtractor

noun_extractor_news = NewsNounExtractor(
    max_left_length=10, 
    max_right_length=7,
    predictor_fnames=None,
    verbose=True
)
nouns_news = noun_extractor_news.train_extract(sentences)

# 출력
used default noun predictor; Sejong corpus based logistic predictor
/Users/dong/opt/anaconda3/envs/cow_word/lib/python3.7/site-packages/soynlp
local variable 'f' referenced before assignment
local variable 'f' referenced before assignment
scan vocabulary ... 
done (Lset, Rset, Eojeol) = (518797, 290268, 289869)
predicting noun score was done                                        
before postprocessing 138637
_noun_scores_ 30616
checking hardrules ... done0 / 30616+(이)), NVsubE (사기(당)+했다) ... done
after postprocessing 21026
extracted 985 compounds from eojeolss ... 45000 / 45737

 

그 중에서 나는 많은 기능을 포함하고 있는 NewsNounExtractor 를 사용했다. 왜냐하면 뉴스 데이터를 사용하기도 하고, 여러가지 속성들을 가지고 있기 때문이다.

아직 감이 안오니 어서 출력해보자. score를 기준으로 내림차순 정렬을 해서 200개만 출력해봤다.

 

sort_nouns_news = sorted(nouns_news.items(), key=lambda x:-x[1].score)[:200]
print(tmp_)

# 출력
[('기초수급자',
  NewsNounScore(score=1.0, frequency=16, feature_proportion=0.25, eojeol_proportion=0.5, n_positive_feature=1, unique_positive_feature_proportion=1.0)),
 ...]

 

출력결과를 보면 7개 가량의 속성이 있음을 알 수 있다. 그 중에서 나는 score, frequency, feature_proportion 이 세 가지에 집중해봤다.

 

1️⃣ score : 명사 가능성을 점수로 표현했으며, 튜토리얼 문서에 따르면 한국어는 L + [R] 구조이며, 명사 뒤에 나오는 R set을 모아 명사 가능 점수를 학습 시켜놨다. R set에 '있게'는 1.0점, '있는'은 0.3 점이라 등록되었다 가정하고, '재미 + 있게' 3번, '재미 + 있는' 2번 등장하였다면 재미의 명사 가능 점수는 (3 x 1.0 + 2 x 0.33) / 5 = 0.732점 이라고 한다.
2️⃣ frequency : 딱 보면 느낌오겠지만, 그 단어가 나온 빈도수이다.
3️⃣ feature_proportion : 이것에 대해 제대로 나오진 않았지만, 번역기 돌려보면 특성이 있는지(?) 정도로 추측이 된다.

 

🤩 아이디어


soynlp를 활용하여 미등록단어 문제를 해결할 수 있지 않을까? 라는 생각을 해보았다.

그래서 떠오른 아이디어는 뉴스데이터를 크롤링하여 soynlp의 명사추출기로 추출된 명사를 10개씩 ' '(공백)으로 이어 붙여 형태소 분석기(사용한 것은 mecab)을 통해 명사 분석한다. mecab을 통해 나온 output을 input으로 넣었던 명사와 비교하여 분석되지 않은 명사가 어떤 것인지 살펴보는 것이다!

글로 적으니까 햇갈리니 허접하지만 직관적인 플로우 차트를 그려봤다.

아무튼 느낌은 왔으니 실제로 해봐야 알 것 아닌가??

 

🤔 진행과정


데이터는 우선 네이버 뉴스데이터에서 크롤링을 했고, soynlp로 추출된 명사는 score * frequency * feature_proportion 계산을 하여 높은 순서대로 상위 200개를 사용했다.(➕ 글자수가 2개 이하인 것들은 제외했다.)

nouns_news_tmp = {key: value for key, value in nouns_news.items() if len(key) > 2} 
top_news = sorted(nouns_news_tmp.items(), 
    key=lambda x:-x[1].frequency * x[1].score * x[1].feature_proportion)[:200]
for i, (word, score) in enumerate(top_news):
    if i % 4 == 0:
        print()
    print('%6s (%.2f)' % (word, score.score), end='')

# 출력
오마이뉴스 (0.99)   이재명 (0.96) 코로나19 (0.95)  페이스북 (0.89)
   상대적 (1.00)   시민들 (0.82)최고위원회의 (0.97)   간담회 (0.88)
   김현정 (1.00)   그동안 (0.82)  세금으로 (1.00) 정책협의회 (0.99)
   마스크 (0.96)  제3지대 (0.94)수원컨벤션센터 (1.00)  CCTV (0.84)
  거리두기 (0.82)  민주주의 (0.87)   바람직 (0.99)   공동체 (0.95)
   대체재 (0.98)  장례식장 (0.96)  포퓰리즘 (0.80)   일자리 (0.80)
   여배우 (0.99)   취재진 (0.98)   불가능 (0.90) 글래드호텔 (1.00)
   있었기 (1.00)한국사회여론연구소 (1.00)   아이들 (0.72)  경기지사 (0.73)
   더불어민주당 (0.61)   재보선 (0.80)   부적절 (1.00)연합뉴스TV (1.00)
   ...

 

이렇게 추출된 명사들을 10개씩 문장으로 만들어서 mecab에 넣어서 비교를 해봤다. 정확도는 전체 개수 중 맞춘 개수이다.

 

# 추출된 명사를 단어만 뽑아 리스트화 시킨후
word_list=[i[0] for i in top_news]

str_dic={}
# 10개씩 나눠서 끝에 마침표를 찍어 딕셔너리에 담는다.
for n in range(0,len(word_list),10):
        word_dic[n]=word_list[n:n+10]
        tmp_str=' '.join(word_dic[n])
        str_dic[n]=tmp_str+'.'

...

# 출력
입력텍스트(soy_nlp 명사) : ['아이들 경기지사 단일화 어떠한 리얼미터 실시간 더불어민주당 재보선 부적절 연합뉴스TV.']
출력텍스트(mecab 통과) : ['실시간', '리얼미터', '경기지사', '단일', '아이', '연합', '부적', '보선', '뉴스', '민주당']
정확도 : 0.3
맞춘명사 : ['실시간', '리얼미터', '경기지사']
없는명사 : ['아이들', '단일화', '어떠한', '더불어민주당', '재보선', '부적절', '연합뉴스TV']

 

코드가 좀 더럽고 길어서 다 올리긴 좀 그래서 초반부만 올렸다.. 생각보다 결과는 처참했다..

 

😳 결과


  • 첫번째 문제점은 추출된 명사를 공백으로 이어 붙여서 완전한 문장이 아니라 그냥 명사로만 이루어진 문장이라 그런지 mecab이 제대로 인식하지 못하는 경우가 많았다. 이 부분은 mecab의 명사 추출 과정을 이해하지 못했기에 신경쓰지 못했다.
  • 그리고 추출된 명사도 200개를 뽑았을 때 더불어민주당, 거리두기, 재보선과 같이 복합명사나 신조어(줄임말 등)가 보이긴 하지만, 있었기, 세금으로와 같이 이상한 결과들도 많다..

 

🙌 마무리


그래도 나름 하면서 재밌다고 생각했던 시도였다. 다만 생각보다 결과가 좋지 않아서 문제다... 저것만 사용해서 미등록단어를 자동으로 등록하게 한다면 결과가 좋지 않을게 뻔하다... 다음엔 우선적으로 내가 사용하는 모델의 원리에 대한 이해하는 과정을 충분히 거친 뒤, 이러한 작업을 시도하면 더 좋은 결과를 낼 수 있지 않을까 싶다. 앞으로 더 분발하자 ~~ 🔥

 

reference

2021-03-08 21:58:06

개요


대게는 데이터를 csv 파일이나 excel파일, txt파일로 접하게 되었다. 하지만 이 데이터들을 파이썬에 옮겨서 지지든, 볶든, 어떻게든 요리하고 싶다면 어쩌면 좋을까? 엑셀처럼 다룰 수 있으면 얼마나 좋을까? 그래서 준비한게 PANDAS 라이브러리다!

pandas 란?

pandas는 데이터 조작 및 분석을 위해 Python 프로그래밍 언어로 작성된 소프트웨어 라이브러리다. 특히 숫자 테이블과 시계열을 조작하기위한 데이터 구조와 연산을 제공힌다. 그리고 pandas의 DataFrame은 여러 개의 Series들의 조합으로 구성되어 있다. 뒤에 예제를 통해 알 수 있다.

글로만 봐선 잘 모르겠다... 어서 시작해보자!

 

Pandas 요리하기


1. DataFrame 생성하기

생성하는 것은 상당히 쉽다. 제일 먼저 pandas 라이브러리를 pip install pandas로 설치한 후 import 하고 생성하자!

 import pandas as pd # pd로 줄여 사용하는게 국룰

 # 1) 딕셔너리로 생성하기 dic의 key가 컬럼이 되고, value가 값이 된다.
 data={'name':['철수','영희'],'Phone_num':['01033334444','01011112222']}
 df = pd.DataFrame(data,index=['1번','2번'])

 # 2) 이중 리스트로 만들기
 data=[
     ['철수','01033334444'],
    ['영희','01011112222']
         ]
 # 또는 np.array로 만들기
 data=np.array([
    ['철수','01033334444'],
    ['영희','01011112222']
    ])

 df = pd.DataFrame(data,columns=['name','phone_num'],index=['1번','2번'])

 print(df)
 # 출력  name     phone_num
 # 1번   철수        01033334444
 # 2번   영희        01011112222

DataFrame 에는 인풋으로 다음과 같이 넣어줘야 한다. pd.DataFrame(value, index, columns) 에서 value의 shape이 (n,m)인 행열이 있다면, index의 길이(len(indax))와 n이 같아야 하고, columns의 길이(len(columns))가 m과 같아야 한다. 어찌보면 당연한 건데 헷갈린다,,

2. 열 / 행 추출

df의 매력적인 부분이 원하는 행, 또는 열을 쉽게 가져올 수 있다. 위 예제를 그대로 사용하여 이름 _열 데이터_만 가져오고 싶다면,

 print(df.name)
 # 또는
 print(df['name'])
 # 또는 iloc은 숫자로 접근. 컴마로 열부분임을 명시
 print(df.iloc[:,0])
 # 또는 loc은 값으로 접근
 print(df.loc[:,'name'])

 # 출력
 # 1번    철수
 # 2번     영희
 # Name:name, dtype: object

반대로 행의 데이터에 접근하고 싶다면,

 # iloc은 그 행의 번호(숫자)로 접근한다.
 print(df.iloc[0])
 # 또는 리스트처럼 인덱스 슬라이싱한다.
 print(df[:1])
 # 또는 loc은 index의 값으로 접근한다.
 print(df.loc['1번'])


 # 출력
 # name             철수
 # phone_num    영희
 # Name: 1번, dtype: object

이 둘의 공통점은 Series 객체로 반환된다. 이 Series를 이용하여 더 멋진 인덱싱이 가능하다!

3. 조건에 맞는 데이터 추출

실제 데이터들은 정말 많고 많을 것이다. 위의 예제의 경우 이름이 철수인 데이터만 보고 싶을 땐 어떻게 하면 좋을까? Series를 이용하면된다!

print(df['name']=='철수')
# 출력
# 1번    True
# 2번    False
# Name: name. dtype: bool

# 본 df의 인덱스로 넣어주면 철수에 해당한 값만 가진 dataframe 출력
df[df['name']=='철수']

# 출력   name     phone_num
# 1번    철수        01033334444

위 코드를 실행하면 DataFrame에서 철수가 포함되는 행의 데이터는 True로 이루어진 boolean형태의 시리즈를 반환한다. 이 시리즈를 이용하여 본 데이터 프레임안에 넣어주면 True인 행, 즉 철수인 행만 출력이 된다.

4. 열 / 행 추가 및 수정

열 추가는 상당히 간단하다. 열을 추출할 때처럼 추가 시킬 column명을 적고, index의 길이에 맞춰 값을 리스트로 추가해주면 된다.

# 추가
df['sex']=['남','여']
print(df)
# 출력    name    phone_num    sex
# 1번    철수        01033334444    남
# 2번     영희        01011112222    여

# 수정 
df['phone_num']=['01034567890','01012345678']
print(df)
# 출력    name    phone_num    sex
# 1번    철수        01034567890    남
# 2번     영희        01012345678    여

행의 수정도 같은 맥락이다.

# 추가
df = df.append({'name':'코린','phone_num':'01025552223','sex':'남'},ignore_index=True)

# 인덱스까지 추가하고 싶다면 series로 만들어 추가
add_row=pd.Series({'name':'코린','phone_num':'01025552223','sex':'남'},name='3번')
df=df.append(add_row)

# 출력    name    phone_num    sex
# 1번    철수        01034567890    남
# 2번     영희        01012345678    여
# 3번    코린        01025552223    남

# 수정
df.iloc[1]=['희영','폰없음','남']

# 출력    name    phone_num    sex
# 1번    철수        01034567890    남
# 2번     희영        폰없음           남
# 3번    코린        01025552223    남

append를 이용하면, index가 상관없으면 위 처럼 딕셔너리 형태로 적용, ignore_index옵션 True로 해줘야함. 그렇지만, append만 하면 리스트처럼 바로 적용되지 않는다. 그래서 위와 같이 df = df.append()처럼 값을 재정의 해준다.

5. 자주쓰는 속성

데이터들을 표로 관리하는 것도 좋지만, 다른 모델의 인풋으로 넣어줘야 할 때가 있다. 그래서 자주 쓰는 메서드들을 적어보았다.

  1. index 가져오기
print(df.index)

# 출력
Index(['1번','2번','3번'], dtype='object')

# 리스트로 가져오기
index_list = df.index.to_list
print(index_list)

# 출력
['1번','2번','3번']
  1. columns 가져오기
print(df.index)

# 출력
Index(['name','phone_num','sex'], dtype='object')

# 리스트로 가져오기
columns_list = df.index.to_list
print(columns_list)

# 출력
['name','phone_num','sex']
  1. values 가져오기 array 객체로 다른 모델의 인풋으로 많이 사용한다.
print(df.values)

# 출력
array([['철수','01034567890','남'],
        ['희영','폰없음','남'],
        ['코린','01025552223','남']], dtype='object')

마무리


항상 편리한 도구들은 많다고 생각한다. 내가 어떤 작업을 할것인지, 그 작업에 가장 효율적인 도구를 찾아 사용하는 것이 제일 중요하다고 생각한다. 그렇지만 모르면 못쓴다^^ 찾아보고 까먹지 않게 한 번 썼을 때 정리 잘해놓자 !

2021-02-24 00:30:41

🧐 정규표현식이란?


정규 표현식(regular expression, 간단히 regexp 또는 regex, rational expression) 또는 정규식(正規式)은 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어이다. 정규 표현식은 많은 텍스트 편집기와 프로그래밍 언어에서 문자열의 검색과 치환을 위해 지원하고 있으며, 특히 펄과 Tcl은 언어 자체에 강력한 정규 표현식을 구현하고 있다.

즉, text에서 패턴을 파악해 찾고, 바꾸고, 지우고 등등을 할 수 있다! 전처리 단계에서 빠질 수 없는 부분! (언제까지 .split,.strip 등 만 쓸래??!!)


📝 사용법

아주 간단하다. import re를 한다음 여러가지 메서드를 사용하면 된다~~😆 자세한 설명은 밑에서..


💱메타문자


  1. 문자 클래스 [ ]
    문자 클래스로 만들어진 정규식은 "[ ] 사이의 문자들과 매치"라는 의미를 갖는다. 주로 [0-9] : 숫자, [가-힣] : 한글, [a-z] 이런식으로 많이 사용한다. 이 괄호 사이에 들어가는 다른 메타 문자들은 다 문자로 인식되므로 주의하기! 그리고 ^ 이 들어가면 not의 의미가 되므로 주의!

자주 사용하는 문자 클래스

  • \d : 숫자 [0-9]와 같다.
  • \D : 비숫자 [^0-9]와 같다.
  • \w : 숫자 + 문자 [a-zA-Z0-9]와 같다.
  • \W : 숫자 + 문자가 아닌 것 [^a-zA-Z0-9]와 같다.
  • \s : 공백 [ \t\n\r\f\v]와 같다.
  • \S : 비공백 [^ \t\n\r\f\v]와 같다.
  • \b : 단어 경계 ('\w'와 '\W'의 경계)
  • \B : 비단어 경계
  • .(dot) : \n을 제외한 모든 문자와 매칭

이것들은 한번만 쓰면 한 글자를 매칭한다. 그렇다고 3자리 숫자를 표현하는데 \d\d\d 이렇게 쓸 순없지 않는가? (사실 가능) 100자리 숫자면? \d\d\d\ ... \d\d? 말이 안된다. 그렇기에 등장한 것이...

  1. 반복
    문자 클래스 뒤에 붙이면 반복 된다.(ex. \d*) 종류는 다양하다
  • * : 0번째부터 반복된다.
  • + : 1번째부터 반복된다. ex) txt = aadd 일때 'aaddc*' (o) 'aaddc+' (x)
  • {m,n} : m부터 n까지 반복된다.
  • ? : 0번 아니면 1번 일때. {0,1} 이랑 같지만 이게 생각보다 쓸모있는게 태그 같은건 <h1> </h1> 이런식으로 /가 있을 수도 없을 수도 있기에 편리함.

제공되는 기능

search, findall,match, sub 등등 다양한 것이 있다.


👀 내가 자주 썻던 부분


한 줄로 re.sub( , , ) 로 간단하게 사용할 수 있지만, re.compile( 'Pattern' )을 사용하여 변수로 저장해두면 재사용 가능!

1️⃣ re.search('검색할 텍스트','패턴') : 텍스트 내에서 내가 원하는 패턴 위치 찾기

import re

text='지금 날짜는 2021. 02. 23 이래용 ㅎㅎㅎ'
p = re.compile(r'\d{4}.\s\d{1,2}.\s\d{1,2}')

# search 함수로 해당 텍스트의 시작 위치, 끝 위치 검색
# .span은 start index와 end index를 튜플로 반환
idx=p.search(text).span()

# 원래 text에서 위의 인덱스를 이용해 발췌
date = text[idx[0]:idx[1]]

print(date) # 출력 결과 >> 2021. 02. 23

2️⃣ re.sub('바꿀텍스트', '입력텍스트', '패턴') : '입력텍스트' 에서 '패턴'에 해당하는 부분을 '바꿀텍스트'로 바꾼다.

text='<h1>지금 날짜는 2021. 02. 23 이래용 ㅎㅎㅎ</h1>'

# [/]?는 /는 있어도 되고 없어도 되고, 알파벳 하나와 숫자하나 그리고 제일 밖 괄호
p = re.compile(r'\<[/]?[a-z][0-9]\>')

# 패턴에 해당하는 부분을 ''으로 바꿈 곧 없앰
pre_text=p.sub('',text)

print(pre_text) # 출력 결과 >> 지금 날짜는 2021. 02. 23 이래용 ㅎㅎㅎ

마무리

간만에 사용하게 되면서 생각보다 재밌음을 느꼈다. 물론 내가 적은 것 말고 그룹핑, 컴파일 옵션, 다양한 기능들... 등등 많지만 내가 사용하기엔 쓸만한 부분들은 적어뒀다고 생각한다. 다음에 다시 만났을 때 내가 안까먹길 바라면서 ... 이만 ... 👋

아참! 간단하게 테스트를 해볼 수 있는 사이트가 있다! 이곳에서 연습해서 적용시키는 것을 추천!

>> 실습하러가기 <<

reference