자연어 처리(자연어 이해, 자연어 생성), 벡터화, SpaCy, 토큰화, 불용어, 통계적 트리밍, 어간추추출, 표제어 추출, Counter-based Representation(Bag of words - TF / TF-IDF, 코사인 유사도
NOTE
각 과정마다 파이썬으로 어떻게 구현하는지 다 적으면 너무 길어져서 개념은 가능한 설명만 남기려고 했다. 상세한 코드가 궁금하면실습한 것
부분을 보면 된다.
자연어 이해(NLU, NL Understanding)
자연어 생성(NLG, NL Generation)
NLU + NLG
: 둘이 같이 쓰이는 것.자연어 처리의 개념에 대해선 그리 어렵지 않았다. 그러면 이제부터 본격적으로 자연어 처리 작업을 하기 위해 무엇을 해야하는지 프로세스 순서에 맞게 설명을 이어가겠다. 지금부터가 시작이다.
텍스트 데이터를 전처리하는 건 자연어처리의 시작이자 절반 이상을 차지하는 중요한 과정이라고 한다.
벡터화(Vectorize)
에 대해서 말할건데, 컴퓨터가 이해할 수 있도록 벡터화를 해주기 전에 이 텍스트 전처리를 해주는 거다. 머릿속에 그림 잘 그려놔야 나중에 안 헷갈린다!!토큰화
할건데, 해보면 어떤 의미인지 알 수 있다.오늘 텍스트 전처리를 하며 다양한 걸 해보게 되었는데 간단히 요약하면 아래와 같다. 이거 잘 기억해!!!
토큰화(tokenize), 대소문자 통일(
lower()
등), 정규표현식을 사용해 필요없는 부분 제거, 불용어(Stop Words) 처리, 통계적 트리밍(Trimming), 어간추출(Stemming), 표제어 추출(Lemmatization)
아이고 많다 많아! 몇개만 좀 더 자세히 보자.
정규표현식(Regular Expression)
# 정규표현식 패키지 import
import re
# 정규식 지정
regex = r"[^a-zA-Z0-9 ]"
'''
참고로 ^는 not을 의미한다. 영소문자, 영대문자, 숫자0-9, 공백이 아닌 것(not)은 다 변경하겠는 표현이다.
'''
# 정규식을 적용할 문자열 할당하기
test_str = "(Natural Language Processing) is easy!, AI!\n"
# 해당되는 패턴의 문자를 어떤 문자로 바꿀 지를 지정하기
subst = "" # 없애겠다는 것
result = re.sub(regex, subst, test_str)
result # => Natural Language Processing is easy AI
Counter
클래스.update
메서드를 사용하면 각 행에 적용되는 결과를 업데이트 할 수 있다..most_common
메서드를 적용하여 상위 n개 결과를 리스트 형태로 출력할 수 있다.from collections import Counter
# Counter 객체 생성
word_counts = Counter()
'''
- Counter 객체는 리스트 요소의 값과 요소의 갯수를 카운트 하여 저장하고 있는다.
- 카운터 객체는 .update 메소드로 계속 업데이트 가능합니다.
'''
# 토큰화된 각 리뷰 리스트를 카운터 객체에 업데이트하기
df['tokens'].apply(lambda x: word_counts.update(x))
'''
토큰으로 쪼개져 있는(= 단어)걸 하나씩 꺼내서 Counter 객체에 업데이트 하면 같은 단어는 계속 카운트가 올라가며 저장되어 있는거라고 생각하면 된다.
'''
# Top_10 보기
word_counts.most_common(10)
SpaCy
import spacy
from spacy.tokenizer import Tokenizer
nlp = spacy.load("en_core_web_sm")
tokenizer = Tokenizer(nlp.vocab)
tokens = []
for doc in tokenizer.pipe(df['reviews.text']): #데이터프레임의 분석할 열이다.
doc_tokens = [re.sub(r"[^a-z0-9]", "", token.text.lower()) for token in doc]
tokens.append(doc_tokens)
df['tokens'] = tokens
df['tokens'].head()
squarify
라는 시각화 라이브러리 이용하면 아래와 같이도 표현할 수 있다. 신기했다!import squarify
import matplotlib.pyplot as plt
wc_top20 = wc[wc['rank'] <= 20]
squarify.plot(sizes=wc_top20['percent'], label=wc_top20['word'], alpha=0.6)
plt.axis('off')
plt.show()
불용어(Stop Words) 처리
통계적 트리밍(Trimming)
어간 추출(stemming) / 표제어 추출(Lemmatization)
이건 중요하니까 문단 구분을 좀 해서 적어둬야겠다.
어간 추출(stemming)
from nltk.stem import PorterStemmer
ps = PorterStemmer()
words = ["wolf", "wolves"]
for word in words:
print(ps.stem(word)) #=> wolf wolv
표제어 추출(Lemmatization)
lem = "The social wolf. Wolves are complex."
nlp = spacy.load("en_core_web_sm")
doc = nlp(lem)
for token in doc:
print(token.text, token.lemma_, sep=" ")
'''
[출력 결과]
The the
social social
wolf wolf
. .
Wolves wolf
are be
complex complex
. .
'''
자, 지금까지 벡터화를 하기 전 데이터 전처리를 할 때 무엇을 하는지에 대해서 적어봤다. 내용이 너무 많아 헷갈릴 것 같긴 한데,, 그래도 무작정 외우려고 하기보단 내가 자연어 처리 프로젝트 와중에 있다고 생각하고 보면 좀 더 각 과정들의 필요성과 의미가 잘 기억될 것이다.
문서-단어 행렬(DTM, Document-Term Matrix)
의 형태로 나타내진다. 아래와 같다고 생각하면 된다. 등장 횟수 기반의 단어 표현(Count-based Representation)
: 단어가 문서(혹은 문장)에 등장하는 횟수를 기반으로 벡터화하는 방법Bag-of-Words (CounterVectorizer)
TF-IDF (TfidfVectorizer)
분포 기반의 단어 표현(Distributed Representation)
: 타겟 단어 주변에 있는 단어를 기반으로 벡터화하는 방법Word2Vec(CBoW, Skip-gram)
fastText
Count-based Representation)
에 대해 배웠다.Bag-of-Words(BoW) - TF(Term Frequency)
CounterVectorizer
를 사용하면 TF 방식으로 문서를 벡터화 할 수 있다. Bag-of-Words(BoW) - TF-IDF(Term Frequency-Inverse Document Frequency)
TF(w)*IDF(w)
인데 IDF(w)의 수식은 다음과 같다. TfidfVectorizer
를 사용하면 TF-IDF 벡터화도 사용할 수 있다.코드의 활용을 잘 기억하자. 내가 적은 건 아무래도 내 흐름대로 진행되었기 때문에 일반화하기에는 좀 덜 깔끔할 수도 있다. (실제로 오늘 노트에서는 함수 만들어서 전처리 벡터화까지 한번에 했지만 나는 다 쪼개서 직접 했다) 하지만 각 흐름과 구현을 이해하고 있다면 나중에 써먹는데 큰 문제는 없을 것이다.
[오늘 분석할 것]
- indeed.com 에서 Data Scientist 키워드로 Job descrition을 찾아 스크래핑한 데이터를 이용해 과제를 진행해 보겠습니다.
Data_Scienties.csv
파일에는 1300여개의 Data Scientist job description 정보가 담겨 있습니다.
[중복행 제거]
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/indeed/Data_Scientist.csv')
# title, company, description 에 해당하는 Column만 남기기
df = df[['title', 'company', 'description']]
print('중복행 제거 전', df.shape)
# 중복행 제거
df = df.drop_duplicates(ignore_index=True) # 인덱스 재설정 / 레퍼런스 https://mizykk.tistory.com/93
print('중복행 제거 후', df.shape)
df.head()
[토큰 정제하기]
import spacy
from spacy.tokenizer import Tokenizer
nlp = spacy.load("en_core_web_sm")
tokenizer = Tokenizer(nlp.vocab)
# 함수 만들기 (3개 칼럼 해줘야 하니까 편의상)
def to_token(df_col):
tokens = []
for doc in tokenizer.pipe(df_col):
doc_tokens = [re.sub(r"[^a-z0-9]", "", token.text.lower()) for token in doc]
tokens.append(doc_tokens)
return tokens
# 토큰화 하기
df['title'] = to_token(df['title'])
df['company'] = to_token(df['company'])
df['description'] = to_token(df['description'])
df.tail()
#근데 tl;dr의 토큰화가 'tl', 'dr'이 아니라 'tldr'이 되는지 궁금하다. 아마도 tokenizer 기준에 ;, : 등의 특수문자는 알아서 분리 단위로 인식하지 않도록 되어있는듯?
# 토큰 리스트에 공백값 들어가 있는 것 없애자. (ex - [tldr, , spring, is, accelerating, the, discov...]) // 의미없는 값이라 굳이 필요 없어보이긴 하지만 연습삼아 해보기.
def remove_value(list):
list = [v for v in list if v] # 레퍼런스 https://jinmay.github.io/2019/06/30/python/python-how-to-delete-empty-string-in-list/
return list
df['title'] = df['title'].apply(lambda x: remove_value(x))
df['company'] = df['company'].apply(lambda x: remove_value(x))
df['description'] = df['description'].apply(lambda x: remove_value(x))
df.tail() # 깔끔! 속이 편해졌다.
[정제한 토큰 시각화]
# 세 칼럼에 각각 있는 토큰 리스트 한 곳에 모으기 (token 기준)
all_tokens = []
for doc in df['title']:
for v in doc:
all_tokens.append(v)
for doc in df['company']:
for v in doc:
all_tokens.append(v)
for doc in df['description']:
for v in doc:
all_tokens.append(v)
print('토큰 총 갯수 = ', len(all_tokens), '\n')
# 데이터 프레임으로 만들기
all_tokens_df = pd.DataFrame(all_tokens, columns = ['token'])
# 단어별 전체 카운트
from collections import Counter
word_counts = Counter()
all_tokens_df.apply(lambda x: word_counts.update(x))
word_counts.most_common(10) # Top10
# 세 칼럼에 각각 있는 토큰 리스트 한 곳에 모으기 (doc 기준)
all_tokens_doc = []
for doc in df['title']:
all_tokens_doc.append(doc)
for doc in df['company']:
all_tokens_doc.append(doc)
for doc in df['description']:
all_tokens_doc.append(doc)
print('doc 총 갯수 = ', len(all_tokens_doc), '\n')
# 데이터 프레임으로 만들기
all_tokens_doc_df = pd.DataFrame([all_tokens_doc]).T.rename(columns = {0:'docs'})
# 단어별 존재하는 문장 카운트
word_doc_counts = Counter()
for doc in all_tokens_doc:
word_doc_counts.update(set(doc))
# 참고로 이렇게 해보려고 했는데 잘 안되서 걍 for문 돌림 - all_tokens_doc.apply(lambda x: word_doc_counts.update(set(x)))
word_doc_counts.most_common(10)
## 토큰의 수, 빈도 순위, 존재 문서 수, 비율 등 정보 계산. (여기선 한번만 작업할 거니 함수 따로 안 만들고 위 셀에서 진행한 것 가지고 활용) ##
# word & counts 먼저 데이터프레임으로 만들기
word_n_counts = zip(word_counts.keys(), word_counts.values()) # zip() 레퍼런스 https://ooyoung.tistory.com/60
wc = pd.DataFrame(word_n_counts, columns = ['word', 'counts'])
# rank 칼럼 추가
wc['rank'] = wc['counts'].rank(method = 'first', ascending = False).astype(int) #rank() 레퍼런스 https://rfriend.tistory.com/461
# 전체 대비 비율 칼럼 추가
len_total = len(all_tokens)
wc['percent'] = wc['counts'].apply(lambda x: x / len_total)
# 누적 비율 칼럼 추가하기 전 rank 별로 소팅하기
wc = wc.sort_values(by='rank')
# 누적 비율 칼럼 추가
wc['cul_percent'] = wc['percent'].cumsum() # cumsum() 레퍼런스 https://runebook.dev/ko/docs/numpy/reference/generated/numpy.cumsum
wc.head(10)
## word_in_docs 등 더 넣을 수 있는 건 굳이 지금 안 넣겠다.
# 누적 비율 시각화
import seaborn as sns
sns.lineplot(x='rank', y='cul_percent', data=wc);
# => 여기까지 해본 소감 - 이번이야 함수 안 만들고 이렇게 단계별로 해보며 원리 이해하기 좋았음. 여러 번 작업해야하면 함수 만들어서 하는게 1000000만배 낫겠다.
[불용어 처리]
# 기본 불용어 사전에 두 단어("data", "work")를 추가
STOP_WORDS = nlp.Defaults.stop_words.union(['data', 'work'])
all_tokens_df.head(10) # 아까 단어별로 데이터프레임 만들어두었던 것. 여기서 STOP_WORDS에 있는 단어는 빼줄거다.
# 토큰 정제하기
tokens_not_stop_words = []
for v in all_tokens_df['token']:
if v not in STOP_WORDS:
tokens_not_stop_words.append(v)
tokens_not_stop_words_df = pd.DataFrame(tokens_not_stop_words, columns = ['token'])
tokens_not_stop_words_df.head(10)
# 단어별 전체 카운트
word_counts2 = Counter()
tokens_not_stop_words_df.apply(lambda x: word_counts2.update(x))
word_counts2.most_common(10) # Top10
[Lemmatization]
# 불용어 제거된 위에꺼 그대로 가져다 써서 해볼건데, 그러면 일반 스트링이가 lemma_를 적용 못함. 적절한 조치를 해서 사용할 것이다. (AttributeError: 'str' object has no attribute 'lemma_')
def get_lemmas(text):
lemmas = []
doc = nlp(text) #이게 그 조치임.
for v in doc:
lemmas.append(v.lemma_)
return lemmas[0]
tokens_not_stop_words_df['lemma'] = tokens_not_stop_words_df['token'].apply(get_lemmas)
tokens_not_stop_words_df.head(10)
# Top10
lemma_df = pd.DataFrame(tokens_not_stop_words_df['lemma'], columns = ['lemma'])
word_counts4 = Counter()
lemma_df.apply(lambda x: word_counts4.update(x))
word_counts4.most_common(10) # Top10
TfidfVectorizer
를 이용해 각 문서들을 벡터화 한 후 KNN 모델을 만들고, 내가 원하는job description
을 질의해 가장 가까운 검색 결과들을 가져오고 분석합니다.
[TfidfVectorizer]
job description
와 5개의 가장 유사한 job description
이 있는 index를 입력하세요.max_features = 3000
으로 설정합니다.# 위에서 중복행 제거, 소문자 변환, 정규표현식까지는 반영한 데이터 사용해볼 것이다.
df_des = pd.DataFrame(df['description'], columns = ['description'])
df_des['description'] = df_des['description'].apply(lambda x: " ".join(v for v in x)) # 레퍼런스 https://codechacha.com/ko/python-convert-list-to-string/
df_des
# TfidfVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vect = TfidfVectorizer(stop_words=STOP_WORDS, max_features=3000) #STOP_WORDS는 위에서 지정했던거랑 동일하게.
dtm_tfidf = tfidf_vect.fit_transform(df_des['description'])
dtm_tfidf = pd.DataFrame(dtm_tfidf.todense(), columns=tfidf_vect.get_feature_names())
dtm_tfidf # 숫자가 들어가 있넹 으음..
# KNN
from sklearn.neighbors import NearestNeighbors
nn = NearestNeighbors(n_neighbors=5, algorithm='kd_tree')
nn.fit(dtm_tfidf)
# 88번 인덱스와 유사한 것 5개 찾기
nn.kneighbors([dtm_tfidf.iloc[88]]) #[88, 40, 121, 240, 68]
- TF-IDF를 이용해 문장 혹은 문서를 벡터화한 경우, 이 벡터값을 이용해 문서 분류 태스크를 진행할 수 있습니다.
- 현재 다루고 있는 데이터셋에는 label이 존재하지 않으므로, title 컬럼에 "Senior"가 있는지 없는지 여부를 통해 Senior 직무 여부를 분류하는 작업을 진행해보겠습니다.
[준비]
# 다시 df 중복행 제거한 것만 불러와서 해보자.
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/indeed/Data_Scientist.csv')
# title, company, description 에 해당하는 Column만 남기기
df = df[['title', 'company', 'description']]
# 중복행 제거
df = df.drop_duplicates(ignore_index=True)
# 조건에 따라 추가
df['senior'] = df['title'].apply(lambda x: 1 if 'Senior' in x else 0)
#확인
df['senior'].value_counts() # 참고로 내가 앞에서 lower로 다 바꾼 df로 봤을 때는 senior가 98개였음. 원래 df에 senior 3개가 있었겠다.
[분류기 생성]
train_test_split
을 통해 train 데이터와 valid 데이터로 나눈 후, sklearn
의 DecisionTreeClassifier
를 이용해 분류를 진행해주세요. from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
X_train, X_test, y_train, y_test = train_test_split(dtm_tfidf, df['senior'].values, test_size=0.1, random_state=42)
model = DecisionTreeClassifier(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(classification_report(y_test,y_pred))
# 디스크립션으로 이 업무가 시니어 업무인지 아닌지를 예측한다..?