728x90
SMALL
- 오늘은 국민 청원 데이터와 다산콜센터 데이터를 가지고 여러 기법들, 딥러닝 모델들을 적용해보며 실습해보았습니다!
☁️ 국민 청원 데이터로 워드 클라우드 만들기
✨ 데이터를 먼저 불러와봅시다!
- 먼저, pandas를 이용하여 CSV 파일로부터 국민 청원 데이터를 불러옵니다.
- 불러올 때, article_id를 인덱스로 설정해서 각 청원을 고유하게 관리하고,
- start와 end 컬럼은 날짜형 데이터로 파싱하여 나중에 시간 기반 분석에 활용할 수 있도록 합니다.
import pandas as pd
base_path = 'data'
file_name = 'petition.csv'
df = pd.read_csv(f'{base_path}/{file_name}',
index_col='article_id',
parse_dates=['start', 'end'])
✨ 관심가는 키워드로 데이터를 골라내보자!
- 데이터 전체에서 '돌봄', '육아', '초등', '보육' 등과 같이 특정 관심 분야에 해당하는 단어를 포함한 청원만 추출할 수 있었습니다.
- 그래서 match() 함수와 r'.*(돌봄|육아|초등|보육).*' 패턴을 사용해서 제목 또는 내용 중 어느 부분에라도 해당 단어가 포함되면 매치가 되게끔 설정했습니다.
- flags=re.MULTILINE 옵션은 텍스트가 여러 줄로 이루어진 경우에도 각 줄마다 패턴을 탐색할 수 있도록 도와주는 역할을 합니다.
import re
p = r'.*(돌봄|육아|초등|보육).*'
care = df[df['title'].str.match(p) |
df['content'].str.match(p, flags=re.MULTILINE)]
✨ 토큰화를 진행해보자!
💡 Soynlp의 RegexTokenizer 활용해서 토큰화하기
- 한국어 텍스트 처리를 위해 전에 사용했었던 Soynlp 라이브러리의 RegexTokenizer를 사용합니다.
- 전에 사용만 해보고 특징을 따로 정리해두진 않았었는데, KoNLPy와 비교해서는
- 파이썬만으로 작성되어 있어 설치와 활용이 간편하며, 비지도학습 기반으로 단어 경계를 추출해줍니다.
- 특히, 도메인 특화 텍스트에서 신조어나 특수 단어를 잘 처리할 수 있지만,
- 단어 추출에 집중하다 보니 문맥 정보는 상대적으로 반영이 부족할 수 있습니다.
- 이에 비해 KoNLPy는 다양한 형태소 분석기를 지원하며, 품사 태깅이나 개체명 인식 등 보다 풍부한 기능을 제공하지만, 환경 설정이나 대용량 데이터 처리 시 속도가 저하될 수 있는 단점이 있습니다.
from soynlp.tokenizer import RegexTokenizer
tokenizer = RegexTokenizer()
tokened_title = tokenizer.tokenize(sample_title)
print(tokened_title)
# ['공공기관', '무조건적인', '정규직전환을', '반대합니다', '.']
tokened_content = tokenizer.tokenize(sample_content)
💡 텍스트에 방해되는 문자들 전처리하기
- 개행 문자 제거
- 텍스트 내 개행(\\n) 문자는 시각화에 불필요한 정보를 주므로 공백으로 대체했습니다.
- 정규표현식에서는 \\n이 개행 문자를 의미하므로, 실제 문자열 내에서는 '\\\\n'으로 표현합니다.
- 특수 문자 및 불필요한 기호 제거
- 한글과 영문자를 제외한 다른 문자는 제거하여 데이터의 순수 텍스트만 남깁니다.
- 토큰 단위 분리
- apply() 함수를 사용해 전체 텍스트를 공백 기준으로 토큰화함으로써 단어 리스트를 생성해냈습니다.
content_text = care['content'].str.replace('\\\\\\\\n', ' ', regex=True)
content_text = content_text.str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣 a-zA-Z]', ' ', regex=True)
tokens = content_text.apply(tokenizer.tokenize)
☁️ 워드 클라우드로 마무리!
- 워드 클라우드를 통해 텍스트 데이터에서 자주 등장하는 단어들을 시각적으로 한눈에 파악할 수 있었습니다.
💡 기본적인 워드 클라우드 먼저 생성해보자
- 자주 등장하지만 분석에 큰 의미를 주지 않는 단어(불용어)들을 미리 지정해서 제거했고,
- WordCloud 클래스에 폰트 경로, 이미지 크기, 배경색, 불용어 리스트, 그리고 재현성을 위한 random_state 값을 설정해주었습니다.
from wordcloud import WordCloud
import koreanize_matplotlib
import matplotlib.pyplot as plt
stopwords = ['하지만', '그리고', '그런데', '저는', '제가', '그럼', '이런',
'저런', '합니다', '많은', '많이', '정말', '너무']
def display_word_cloud(data, width=1200, height=500):
word_draw = WordCloud(font_path=r'/usr/share/fonts/truetype/nanum/NanumGothic.ttf',
width=width, height=height,
stopwords=stopwords,
background_color='white',
random_state=42)
word_draw.generate(data)
plt.figure(figsize=(15, 7))
plt.imshow(word_draw)
plt.axis('off')
plt.show()
display_word_cloud(' '.join(content_text))
💡 명사만 보고 싶었는데...
- 데이터의 핵심 의미를 보다 명확하게 파악하기 위해, Soynlp의 LRNounExtractor를 사용하여 명사만 추출하는 작업을 진행했습니다.
- LRNounExtractor는 전체 텍스트를 기반으로 단어 후보들을 추출한 후, 그 중 명사로 판단되는 단어들만 선별하는 방식으로 동작합니다.
from soynlp.noun import LRNounExtractor
import time
noun_extractor = LRNounExtractor(verbose=True)
noun_extractor.train(content_text)
nouns = noun_extractor.extract()
nouns_text = ' '.join(list(nouns.keys()))
start = time.time()
display_word_cloud(nouns_text)
print(f"Processing Time: {time.time()-start:.4f} sec")
- 이렇게 워드 클라우드를 만들어봤는데! 워드 클라우드가 데이터의 전반적인 키워드를 한눈에 파악할 수 있게 해주지만, 세부적인 정보는 부족할 수 있기 때문에
- 추가적인 키워드 추출이나 개체명 인식 등의 방법과 병행해서 사용하면 시너지 효과가 더 날 수 있을 것 같습니다.
🛤️ 똑같은 데이터로 이번엔 이진 분류!
✨ 대상 데이터를 선정해보자!
- 투표 수의 분포 데이터를 살펴본 결과,
- 전체 데이터에서 투표 수가 500건 이하인 경우는 너무 많아서 일정한 패턴을 찾기 어렵고,
- 20만 건 이상은 극소수의 이상치로 평균에 왜곡을 줄 수 있을 것 같아, 투표수가 500건 초과이면서 20만 건 미만인 데이터만 선택해서 진행했습니다.
remove_outlier = df.loc[(df['votes'] > 500) & (df['votes'] < 200000)]
df = remove_outlier.copy()
print(df.shape)
- 또한 원래는 answered라는 응답 여부 컬럼을 기준으로 분류하려 했지만,
- 답변 여부(answered)에 대해 분류하기에는 표준편차가 너무 크고 모수가 적으므로, 투표 수(votes)로 분석하기로 결정!
- 따라서 투표수를 기준으로 평균을 넘으면 1, 그렇지 않으면 0으로 이진 분류할 새로운 변수 votes_pos_neg를 생성해주었습니다.
# 기본값 0 설정
df['votes_pos_neg'] = 0
# 평균 투표수 구하기
votes_mean = df['votes'].mean()
# 투표수가 평균을 넘으면 1, 그렇지 않으면 0 (Boolean → int로 변환)
df['votes_pos_neg'] = (df['votes'] > votes_mean).astype(int)
- 여기서 평균 투표수를 기준으로 1과 0을 나누는 이유는, 일정 기준(평균)을 넘는 청원은 상대적으로 '응답(또는 주목) 대상'으로 판단할 수 있기 때문이었습니다.
✨ 전처리 한 번 해볼까
- 두 가지의 전처리 함수를 만들어서 텍스트 데이터를 전처리했습니다.
💡 preprocessing()
- \\\\n 패턴을 공백으로 대체하여 줄바꿈 정보를 제거하고,
- 특수문자나 이모티콘을 제거하기 위해 "[?.,;:|\\)*~’!^-_+<>@#$%&-=#}※]"와 같은 패턴을 제거해냈습니다.
- 또, 한글, 영문, 숫자만 남기기 위해서 이외의 문자들은 모두 제거해냈고, 그 후 연속된 공백도 단일 공백으로 치환했습니다.
import re
def preprocessing(text):
# 개행문자 제거
text = re.sub('\\\\\\\\n', ' ', text)
# 특수문자, 이모티콘 제거
text = re.sub('[?.,;:|\\)*~`’!^\\-_+<>@\\#$%&-=#}※]', '', text)
# 한글, 영문, 숫자만 남기기
text = re.sub('[^가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9]', ' ', text)
# 중복 공백 제거
text = re.sub(' +', ' ', text)
return text
💡 remove_stopwords()
- 이번에도 불용어를 처리하기 위해서 미리 정의한 불용어 리스트에 포함된 단어들을 함수에 포함시켜 처리했습니다.
def remove_stopwords(text):
tokens = text.split(' ')
stops = ['수', '현', '있는', '있습니다', '그', '년도', '합니다',
'하는', '및', '제', '할', '하고', '더', '대한', '한',
'그리고', '월', '저는', '없는', '입니다', '등', '일',
'많은', '이런', '것은', '왜', '같은', '같습니다', '없습니다',
'위해', '한다']
meaningful_words = [w for w in tokens if w not in stops]
return ' '.join(meaningful_words)
df['content_preprocessing'] = df['content'].apply(preprocessing)
df['content_preprocessed'] = df['content_preprocessing'].apply(remove_stopwords)
✨ 데이터 분할 후 벡터화 작업하기!
- 모델 구축 전, 먼저 전체 데이터를 70%의 Train 데이터와 30%의 Test 데이터로 분할했습니다.
split_count = int(df.shape[0] * 0.7)
df_train = df[:split_count].copy()
df_test = df[split_count:].copy()
print(df_train.shape, df_test.shape)
- 벡터화 작업에서는 텍스트 데이터를 수치 데이터로 변환하기 위해 sklearn의 CountVectorizer를 사용했습니다.
- 주요 파라미터는 min_df, ngram_range, max_features가 있는데,
- min_df=2는 최소 2개 이상의 문서에서 등장하는 단어만 고려해 오타나 드문 단어를 제거해주고,
- ngram_range=(1, 3)은 단어 단위(1-gram)부터 3-gram까지의 조합을 생성해줍니다.
- 마지막으로 max_features=2000은 상위 2000개의 피처(단어)를 사용해 모델 복잡도를 조절해주는 역할을 합니다.
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(analyzer='word',
tokenizer=None,
preprocessor=None,
stop_words=None,
min_df=2,
ngram_range=(1, 3),
max_features=2000)
# 학습 데이터에 대해 단어-문서 행렬 생성
train_feature_vector = vectorizer.fit_transform(df_train['content_preprocessed'])
test_feature_vector = vectorizer.transform(df_test['content_preprocessed'])
✨ TF-IDF로 정규화 작업하기!
- CountVectorizer로 만든 단어-문서 행렬에 TF-IDF 가중치를 적용해 단어의 중요도를 반영해주는 단계입니다.
- 여기서는 TfidfTransformer를 사용하고, smooth_idf=False 옵션을 주어 0 값의 피처에 작은 값을 추가하지 않도록 설정합니다.
from sklearn.feature_extraction.text import TfidfTransformer
transformer = TfidfTransformer(smooth_idf=False)
train_feature_tfidf = transformer.fit_transform(train_feature_vector)
test_feature_tfidf = transformer.transform(test_feature_vector)
✨ 여기서는 처음 나오는 LightGBM
- 여기서는 빠른 학습과 확장성을 가진 Gradient Boosting 프레임워크인 LightGBM을 사용했습니다.
- 모델에 n_jobs=1 값을 준 것은 디버깅과 작은 데이터 세트에서 안정적인 실행을 하기 위해 단일 스레드로 설정한다는 뜻입니다.
from lightgbm import LGBMClassifier
y_label = df_train['votes_pos_neg']
model = LGBMClassifier(random_state=42, n_jobs=1)
model = model.fit(train_feature_tfidf, y_label)
✨ 모델 평가는 교차 검증으로!
- 과적합을 방지하고 모델의 일반화 성능을 평가하기 위해 KFold를 사용해 5-fold 교차 검증을 진행했습니다.
from sklearn.model_selection import KFold, cross_val_score
k_fold = KFold(n_splits=5, shuffle=True, random_state=42)
scoring = 'accuracy'
score = cross_val_score(model, train_feature_tfidf, y_label, cv=k_fold, n_jobs=1, scoring=scoring)
print("교차 검증 정확도:", round(np.mean(score) * 100, 2))
- 위 결과로는 교차 검증 결과가 약 79.62%로 나타났으며,
- 모델이 훈련 데이터에 대해 비교적 안정적인 예측 성능을 보인다는 것을 의미합니다.
✨ 예측하고 마지막 결과까지 평가해보자!
- 테스트 데이터에 대해 예측을 진행하고, 예측값과 실제값 간의 차이를 계산해서 모델의 최종 정확도를 평가했습니다.
# 예측 수행
y_pred = model.predict(test_feature_tfidf)
output = pd.DataFrame({'votes_pos_neg_pred': y_pred})
print(output['votes_pos_neg_pred'].value_counts())
# 테스트 데이터프레임에 예측 결과 추가
df_test['votes_pos_neg_pred'] = y_pred
# 예측과 실제값의 차이(절댓값) 계산
df_test['pred_diff'] = np.abs(df_test['votes_pos_neg'] - df_test['votes_pos_neg_pred'])
pred_diff = df_test['pred_diff'].value_counts()
print(f'전체 {y_pred.shape[0]}건 중 {pred_diff[0]}건이 정확하게 예측되었습니다.')
# 최종 정확도 계산
acc = (pred_diff[0] / y_pred.shape[0]) * 100
print(f'최종 모델 정확도: {acc:.6f}%')
- 예측 결과, 대부분의 데이터가 0으로 분류되었고(실제값과 예측값이 일치하는 경우가 많음), 최종 모델 정확도는 약 79.6%로 평가되었습니다.
- 학습과 테스트의 정확도 차이가 아주 미세했고, 이 값은 난수 시드나 라이브러리 버전에 따라서도 약간의 변동이 있을 수 있다고 합니다.
📞 120다산콜재단(센터) 데이터에 LDA 적용하기
- 120다산콜재단의 행정 상담 민원 데이터를 활용해서 두 가지 분석 기법을 적용해보았는데,
- 먼저 첫 번째는 LDA(잠재 디리클레 할당)를 통해 문서에 내재된 주제(토픽)를 찾아내는 과정을 진행했고,
- 두 번째는 TF-IDF와 코사인 유사도를 활용해 문서 간의 유사도를 측정해보았습니다.
- 데이터는 서울시의 24시간 상담 서비스를 제공하는 120다산콜재단(센터)의 질문과 답변 데이터를 사용했고,
- 이 결과로 추후 RNN이나 LSTM 등 딥러닝 모델로 학습해 분류하는 작업의 기본 전처리 및 탐색 단계로 활용할 수 있었습니다.
✨ 기본적인 데이터 탐색
- 먼저 데이터는 온라인 URL에서 가져왔고, 불러들인 뒤에는 결측치 여부까지 확인해보았습니다.
import pandas as pd
import numpy as np
import koreanize_matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# 데이터 로드: 120다산콜재단 데이터 (제목과 내용 포함)
df = pd.read_csv('<https://bit.ly/seoul-120-text-csv>')
print(df.shape)
# 결측치 확인
print(df.isnull().sum())
✨ 제목과 내용 결합해서 문서 만들기
- 분석 대상 문서를 생성하기 위해, 제목과 내용 컬럼을 하나로 결합해서 새 컬럼인 문서를 생성했습니다.
- 이렇게 결합된 문서를 대상으로 토픽 모델링과 유사도 분석을 진행할 예정입니다.
# 문서 생성: 제목과 내용을 결합
df['문서'] = df['제목'] + ' ' + df['내용']
✨ CountVectorizer로 전에 배웠던 DTM 생성하기
- 문서에 포함된 단어들의 출현 빈도를 기반으로 문서-단어 행렬(Document-Term Matrix, DTM)을 생성합니다.
- 여기서는 CountVectorizer를 사용했고, 분석에 방해가 되는 불용어는 미리 지정해두었습니다.
from sklearn.feature_extraction.text import CountVectorizer
# 불용어를 지정하여 불필요한 단어 제거 ('돋움', '경우', '또는'으로 우선 설정)
cv = CountVectorizer(stop_words=['돋움', '경우', '또는'])
dtm_cv = cv.fit_transform(df['문서'])
# CountVectorizer로 만들어진 단어 사전 확인
print(cv.vocabulary_)
print(cv.get_feature_names_out())
# 문서-단어 행렬을 DataFrame으로 변환하여 각 단어의 전체 빈도 합계 확인
cv_cols = cv.get_feature_names_out()
df_word_freq = pd.DataFrame(dtm_cv.toarray(), columns=cv_cols).sum().sort_values()
print(df_word_freq)
✨ LDA를 활용한 토픽 모델링
- LDA(잠재 디리클레 할당)는 주어진 문서 내에서 어떤 주제(토픽)들이 존재하는지, 그리고 각 주제와 단어의 연관성을 확률적으로 파악하는 모델입니다.
- 특히 두 확률값을 주로 사용하는데,
- 먼저, P(단어 | 주제)는 특정 주제에서 어떤 단어들이 주로 등장하는지를 나타내고,
- P(주제 | 문서)는 문서가 어떤 주제들로 구성되어 있는지를 나타내는 확률값입니다.
- LDA는 sklearn의 LatentDirichletAllocation을 사용해서 구현할 수 있고, import 후 전체 문서를 10개의 주제로 분해할 수 있도록 설정했습니다.
from sklearn.decomposition import LatentDirichletAllocation
NUM_TOPICS = 10
LDA_model = LatentDirichletAllocation(n_components=NUM_TOPICS, random_state=42)
# LDA 모델 학습: CountVectorizer로 생성한 dtm_cv를 입력
LDA_model.fit(dtm_cv)
✨ TF-IDF 벡터화로 단어 가중치 분석해보기
- TF-IDF는 아시다시피 단어 빈도에 가중치를 적용해, 문서 내에서 상대적으로 중요한 단어를 부각시키는 방법입니다.
from sklearn.feature_extraction.text import TfidfVectorizer
# 불용어에 '있습니다', '있는', '합니다' 등 추가
tfidf = TfidfVectorizer(stop_words=['돋움', '경우', '또는', '있습니다', '있는', '합니다'])
dtm_tfidf = tfidf.fit_transform(df['문서'])
cols_tfidf = tfidf.get_feature_names_out()
# 각 단어의 TF-IDF 가중치를 전체 문서에 대해 합산
dist = np.sum(dtm_tfidf, axis=0)
df_tfidf = pd.DataFrame(dist, columns=cols_tfidf).T.sort_values(by=0)
print(df_tfidf.tail(10))
- 가중치를 계산한 뒤에 TF-IDF로 벡터화된 문서를 DataFrame 형태로 변환해서 각 문서에서 단어들의 가중치 분포를 확인할 수 있습니다.
# 희소 행렬을 Numpy 배열로 변환하여 확인
df_tfidf_array = pd.DataFrame(dtm_tfidf.toarray(), columns=cols_tfidf)
print(df_tfidf_array.head())
✨ 마지막으로 문서 간의 유사도 분석까지
- 전에 배웠던 코사인 유사도는 두 개의 벡터 사이의 코사인 각도를 계산하여, 두 문서가 얼마나 유사한지(내적 공간에서의 유사도)를 평가하는 방법입니다.
- 예를 들어, 첫 번째 문서(여기서는 '아빠 육아 휴직 장려금'으로 출력)와 나머지 문서들의 유사도를 계산하고, 가장 유사한 문서를 찾아 추천 시스템 등에 활용할 수도 있습니다.
from sklearn.metrics.pairwise import cosine_similarity
# 첫 번째 문서와 모든 문서 간의 코사인 유사도 계산
similarity_simple_pair = cosine_similarity(dtm_tfidf[0], dtm_tfidf)
result_list = similarity_simple_pair.tolist()[0]
# 유사도 값을 데이터프레임에 새로운 컬럼으로 추가
df['유사도'] = result_list
# '분류', '제목', '유사도'를 기준으로 유사도가 높은 상위 10개 문서 확인
print(df[['분류', '제목', '유사도']].sort_values(by='유사도', ascending=False).head(10))
- 코사인 유사도는 유클리드 거리나 자카드 유사도와 달리, 크기보다는 방향(패턴) 차이에 주목하는 특징이 있습니다.
🐑 똑같은 데이터로 이번엔 BiLSTM 모델 구축하기
- 아까 사용했던 120다산콜재단의 질문/답변 데이터를 대상으로 이번엔 RNN의 한 종류인 BiLSTM(양방향 LSTM)을 활용하여 텍스트 분류 모델을 만드는 과정을 진행했습니다.
✨ RNN? BiLSTM?
- RNN은 순환 신경망을 뜻하는 친구로, 시퀀스 데이터(문장, 음성 등)에서 이전 시점의 정보를 현재 처리에 반영할 수 있도록 설계된 모델입니다.
- 입출력 유형에 따라서
- 일대일(one-to-one): 일반적인 분류 문제
- 다대일(many-to-one): 영화 리뷰 감정 분석 등, 전체 문장을 하나의 레이블로 분류
- 일대다(one-to-many) / 다대다(many-to-many): 번역, 텍스트 생성 등에서 활용하는 방식으로 분류됩니다.
- 또한, LSTM은 입력, 출력, 삭제 게이트를 통해 장기적인 의존성을 학습하지만, 계산 비용이 크다는 단점이 있고,
- GRU는 업데이트 게이트와 리셋 게이트만 사용해 구조를 단순화한 버전이라고 보시면 되겠습니다.
- 그래서 일반 LSTM은 한 방향(보통 순방향)으로 시퀀스를 처리하지만, BiLSTM은 입력 시퀀스를 앞뒤 양 방향으로 처리해서 문맥 정보를 더 풍부하게 반영할 수 있습니다.
- 특히 이 양방향 LSTM은 텍스트 분류나 감정 분석, 기계 번역 등에서 자주 사용되는 편입니다.
✨ 클래스에 불균형이 있을 땐?
- 원본 데이터에서는 분류별(행정, 건강, 여성가족 등) 데이터 수 차이가 심해 불균형 문제가 발생할 수도 있을 듯 싶었습니다.
- 그래서 상위 3개 분류인 '행정', '경제', '복지' 데이터만 선택하여 사용하게 되었습니다.
# 상위 3개 분류만 선택하여 데이터 불균형 문제를 어느 정도 완화
df = df[df['분류'].isin(['행정', '경제', '복지'])]
print(df['분류'].value_counts())
✨ 벡터화하고, 분할하고
- 문서(X)와 분류(y)를 분리하고, 분류는 one-hot 인코딩을 진행해 행렬 형태로 변환해냈습니다.
- train_test_split()으로 분할할 때, stratify 옵션을 사용해서 학습 데이터와 테스트 데이터의 클래스 비율을 동일하게 유지할 수도 있습니다.
# 독립변수(문서)와 종속변수(분류) 분리
X = df['문서']
y = df['분류']
# One-hot 인코딩으로 레이블 변환
y_onehot = pd.get_dummies(y)
# train/test 데이터 분리 (20% 테스트)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y_onehot, test_size=0.2, random_state=42, stratify=y_onehot)
✨ 토큰화하고, 패딩 작업 들어가기
- 먼저 keras의 Tokenizer를 사용해서 각 텍스트를 단어 인덱스의 시퀀스로 변환했고,
- 변환된 시퀀스의 길이를 동일하게 맞춰주는 패딩 작업으로 RNN 모델에 입력할 수 있도록 해주었습니다.
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
vocab_size = 1000 # 단어 사전의 최대 크기
oov_tok = '<oov>'
tokenizer = Tokenizer(num_words=vocab_size, oov_token=oov_tok)
# 학습 데이터로 토큰 사전 구축
tokenizer.fit_on_texts(X_train)
word_to_index = tokenizer.word_index
# 텍스트를 정수 시퀀스로 변환
train_sequences = tokenizer.texts_to_sequences(X_train)
test_sequences = tokenizer.texts_to_sequences(X_test)
# 시퀀스 길이 맞추기 (max_length=500, 후방 패딩)
max_length = 500
X_train_sp = pad_sequences(train_sequences, maxlen=max_length, padding='post')
X_test_sp = pad_sequences(test_sequences, maxlen=max_length, padding='post')
✨ 실제로 BiLSTM 모델을 만들어보자!
- 저희가 구축한 모델은 크게 세 가지 종류의 층으로 구성되어 있습니다.
- 먼저 Embedding Layer는 단어 인덱스를 고정된 길이의 벡터로 변환하는 역할을 하고,
- 가장 중요한 Bidirectional LSTM 층!에서는 두 개의 LSTM 레이어를 사용해서 양방향으로 시퀀스를 처리했습니다.
- 첫 번째 LSTM 레이어에서는 return_sequences=True 옵션으로 시퀀스 전체의 출력을 반환해서 다음 레이어에 전달해주고,
- BatchNormalization과 Dropout을 적용해 과적합을 완화해주었습니다!
- 마지막 Dense Layer를 이용한 최종 출력층에서는 softmax 함수를 통해 클래스 확률을 출력해주었습니다.
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Embedding, LSTM, Bidirectional, Dropout, BatchNormalization
n_class = y_train.shape[1]
model = Sequential([
Embedding(vocab_size, 64, input_length=max_length),
Bidirectional(LSTM(64, return_sequences=True)),
BatchNormalization(),
Bidirectional(LSTM(32)),
Dropout(0.2),
Dense(16, activation='relu'),
Dense(n_class, activation='softmax')
])
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
model.summary()
✨ 조기 종료로 모델을 잘 학습시켜보자!
- 검증 손실(val_loss)이 개선되지 않으면 학습을 조기에 종료해서 과적합을 방지하는 방법을 사용했습니다.
- 그리고 학습 결과를 히스토리로 저장한 후에, 정확도와 손실 곡선을 시각화하는 작업도 진행했습니다.
from tensorflow.keras.callbacks import EarlyStopping
early_stop = EarlyStopping(monitor='val_loss', patience=5)
history = model.fit(X_train_sp, y_train,
epochs=100,
batch_size=64,
callbacks=[early_stop],
validation_split=0.2)
# 학습 결과 시각화
import matplotlib.pyplot as plt
import pandas as pd
df_hist = pd.DataFrame(history.history)
df_hist[['accuracy', 'val_accuracy']].plot(title="Accuracy")
plt.show()
df_hist[['loss', 'val_loss']].plot(title="Loss")
plt.show()
- 학습 도중 과적합이 발생했을 가능성이 있었기 때문에, 조기 종료와 모델 구축에서 사용된 Dropout 등을 통해 이를 완화하고자 하였고,
- 또한 데이터 불균형 문제로 인해 특정 분류에서 예측 성능이 낮을 수 있으므로, 이후 데이터 보강이나 모델 구조 개선을 고려하는 방향도 생각해볼 수 있을 것 같습니다!
✨ 마지막이다! 예측도 하고 평가도 해보고
- argmax로 모델이 출력한 각 클래스에 대한 확률값 중, 가장 큰 값을 예측값으로 선정하였고,
- 이 값을 토대로 test 데이터에 대한 손실 및 정확도를 계산해서 모델의 최종 성능을 확인해보았습니다.
# 예측: 각 클래스에 대한 확률값 출력
y_pred = model.predict(X_test_sp)
# 예측값: 확률이 가장 높은 클래스의 인덱스를 선택
import numpy as np
y_predict = np.argmax(y_pred, axis=1)
y_test_val = np.argmax(y_test.values, axis=1)
# 실제값과 예측값의 일치율 (정확도)
accuracy = (y_test_val == y_predict).mean()
print("예측 정확도:", accuracy)
# 모델 평가: 손실과 정확도 출력
test_loss, test_acc = model.evaluate(X_test_sp, y_test)
print("테스트 손실:", test_loss, "테스트 정확도:", test_acc)
🤔 35일차 회고
- 슬슬 딥러닝 과정의 중반 정도에 돌입했다는 것이 느껴지는 게,
- 데이터 전처리, 토큰화, 정규화, 모델 구축, 평가의 과정이 계속 반복되는 것 같습니다.
- 이 워크플로우를 잘 숙지하고 익혀내서 추후에 회사에 들어가 혹여나 이 과정을 진행한다 하더라도 순서에 대한 착오없이 깔끔히 진행해보고 싶습니다.
- 사실 LDA나 BiLSTM 관련해서는 수식이나 복잡한 그림을 보면서 이해하는 것보다 이렇게 간단하게 배우니 오히려 이해가 더 빠른 것 같기도 하고... 다시 한 번 관련 개념을 깊게 들여다봐야할 것 같습니다.
728x90
LIST
'부트캠프 > LG U+' 카테고리의 다른 글
🤔 타이타닉에서 살아남기 (0) | 2025.03.27 |
---|---|
🤔 요즘 어떤 강의가 가장 핫해...? (1) | 2025.03.19 |
🤔 형태가 바뀌면 의미도 바뀔까? (0) | 2025.03.17 |
🤔 이번엔 좀 더 깊게 달려보자 (2) | 2025.03.13 |
🤔 왜 달려도 달려도 끝이 없을까... (8) | 2025.03.12 |