23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
'감성 분류' 라고 하면 사용자가 작성한 글을 "좋음","싫음"으로 나누는 것을 의미합니다.
즉, 긍정/부정 2가지로 분류할 수 있습니다.
작은 샘플 데이터로 한국어 감성 분류 파이프라인을 구축하는 샘플 코드를 작성해 보았습니다.
아래 코드는 형태소 분석(Okt), 토큰화/패딩, Keras Embedding → BiLSTM → Dense 모델, 그리고 학습 안정화를 위한 콜백까지 한 번에 볼 수 있도록 구성하였습니다.
문장 텍스트를 Deep Learning 모델에 Input 으로 전달하기 전에 전처리 과정을 거쳐야 되며, 전처리 과정은 아래 단계로 구성을 하였습니다.
클렌징 → 불필요한 문자 제거.
형태소 분석 + 불용어 제거 → 핵심 단어만 추출.
토크나이저 → 단어 사전 작성 + 숫자 인코딩.
패딩 → 문장 길이를 동일하게 맞춤.
어휘집 크기 계산 → Embedding 레이어 입력 차원 결정.
이 과정을 거치면 텍스트가 신경망에 투입 가능한 숫자 배열로 변환됩니다.
`df['document'] = df['document'].str.replace(r'[^A-Za-z가-힣 ]', '', regex=True)`텍스트에서 한글, 영문, 공백만 남기고 특수문자나 불필요한 기호는 모두 제거합니다.
이렇게 해야 토큰화할 때 의미 없는 기호 때문에 단어 사전이 불필요하게 커지는 것을 방지할 수 있습니다.
예: “이 영화!!! 최고@@@” → “이 영화 최고”
okt = Okt()
stop_words = ['은','는','이','가','을','를','에','의','와','과','도','으로','하다']
def word_tokenization(text: str):
return [w for w in okt.morphs(text) if w not in stop_words]Okt 형태소 분석기를 이용해 문장을 단어 단위(형태소)로 나눕니다.
한국어는 조사, 어미 등이 많아 그대로 쓰면 학습이 어려움 → 불용어(stop words)로 제거.
예: “이 영화는 재미있어요” → [영화, 재미있다]
num_words = 10000
tokenizer = Tokenizer(oov_token='<OOV>', num_words=num_words)
tokenizer.fit_on_texts(data_tokenized)
seqs = tokenizer.texts_to_sequences(data_tokenized)Tokenizer는 단어에 고유 번호(인덱스)를 부여해 사전을 만듭니다.
num_words=10000 → 상위 1만 개 단어만 사용 (희귀 단어 제외).
oov_token='' → 사전에 없는 단어는 로 처리.
tokenizer.fit_on_texts(data_tokenized)를 호출해, 토큰화된 전체 텍스트(data_tokenized)를 바탕으로 단어사전(vocabulary)을 생성하고, 각 단어에 고유한 정수 인덱스 부여
texts_to_sequences() 메서드를 통해 텍스트를 정수 인덱스 시퀀스로 변환
예: [영화, 재미있다] → [15, 203]
max_length = max(len(s) for s in seqs)
x_data = pad_sequences(seqs, maxlen=max_length, padding='pre', truncating='post')
y_data = df['label'].to_numpy()문장마다 길이가 제각각 → 모델 입력 크기를 맞추기 위해 패딩 필요.
pad_sequences는 부족한 부분을 0으로 채워 길이를 통일.
padding='pre' → 앞쪽에 0을 채움.
truncating='post' → 너무 긴 문장은 뒤를 잘라냄.
결과: 모든 문장이 동일한 길이의 정수 배열로 변환.
vocab_size = min(num_words, len(tokenizer.word_index) + 1)실제 단어 사전 크기를 계산.
+1 이유 → Keras에서 인덱스 0은 패딩 전용으로 예약되어 있기 때문.
최종 vocab_size는 Embedding 레이어 입력 차원으로 사용.
체크포인트(ModelCheckpoint): 모델이 학습되는 동안 매 epoch마다 검증 성능을 모니터링합니다. 성능이 최고일 때의 가중치를 저장해 두면, 학습이 끝난 뒤 가장 좋은 성능의 모델을 바로 복원할 수 있습니다.
얼리스톱(EarlyStopping): 검증 손실(val_loss)이 일정 횟수(patience) 동안 개선되지 않으면 학습을 조기 종료합니다. 불필요한 epoch를 줄이고 과적합을 예방하는 효과가 있습니다.
텐서보드(TensorBoard): 학습 과정을 웹 대시보드 형태로 시각화할 수 있게 해줍니다. 손실, 정확도 곡선, 학습 시간, 레이어별 가중치 분포 등을 한눈에 확인할 수 있어 디버깅과 모델 개선에 유용합니다.
모델은 크게 3가지 층(layer)으로 이루어져 있습니다.
정리하면, Embedding은 단어를 의미 벡터로 바꾸는 단계, BiLSTM은 문맥을 이해하는 단계, Dense는 최종 감정을 분류하는 단계입니다.
이 세 가지가 합쳐져 하나의 모델을 이루며, 각 레이어가 역할을 분담해 전체 파이프라인을 완성합니다.
Embedding = "단어 사전을 숫자 대신 의미있는 좌표로 바꿔주는 번역기"
BiLSTM = "앞에서 읽는 사람 + 뒤에서 읽는 사람, 두 명의 독해 전문가가 주고받아 핵심을 뽑아내는 과정"
Dense = "최종 판정관(심판) — 독해 결과를 보고 ‘긍정’ 또는 ‘부정’ 결정을 내림"
이 세 부분을 조합하여 모델(모듈의 집합)입니다. 각 부분은 레이어(부품)이고, 이 부품들을 연결한 전체가 모델입니다.
Embedding
- 무엇을 하는가?: 단어의 정수 인덱스(예: 23, 124, 3)를 받아서 각 단어를 고정 길이의 실수 벡터(예: 128차원)로 바꿉니다.
- 왜 필요한가?: 원-핫 벡터처럼 희소하고 비의미적인 표현 대신, 비슷한 의미의 단어는 가까운 벡터로 놓이는 '의미 공간'을 학습할 수 있어서 모델 성능이 좋아집니다.
- 입출력 형태: (batch_size, seq_len) → (batch_size, seq_len, embedding_dim)
BiLSTM
- 무엇을 하는가?: 문장을 앞→뒤(정방향)와 뒤→앞(역방향) 두 방향으로 읽어서 각각의 정보를 합칩니다. 결과적으로 각 시점의 정보가 앞뒤 문맥을 모두 반영하게 됩니다.
- 왜 중요한가?: 한국어는 어미와 조사 등 문장 끝부분에 중요한 정보가 오기 때문에, 역방향 문맥도 같이 보는 것이 감성 파악에 도움이 됩니다.
- units 파라미터 해석: units=16이라면 "방향당" 16개의 hidden unit을 갖습니다. 양방향(Bidirectional)일 경우 기본적으로 출력 차원은 16×2 = 32가 됩니다(연결 방식에 따라 다름).
- return_sequences 옵션:
return_sequences=False(기본): 시퀀스 전체를 하나의 벡터(예: 마지막 타임스텝의 요약)로 압축하여 반환 → 분류 문제에 흔히 사용
return_sequences=True: 각 타임스텝별로 출력을 유지 → 시퀀스 라벨링(품사 태깅, 개체명 인식 등)에 사용
. 입출력 형태(예): Embedding 출력이 (B, L, 128)이고 units=16, return_sequences=False라면 출력은 (B, 32)
.추가 옵션: dropout, recurrent_dropout으로 과적합을 줄이거나 return_sequences=True로 중간에 Attention 층을 붙이는 등 확장이 가능합니다.
Dense(완전연결층)
- 무엇을 하는가?: BiLSTM이 만든 특징 벡터를 받아서 최종 출력값(로그잇 또는 확률)을 계산합니다.
- 왜 필요한가?: BiLSTM은 문맥 특징을 추출하지만, 그 자체로 이유(긍정/부정)를 '결정'하지는 않습니다. Dense는 이 특징들을 선형 결합하고(그리고 비선형 활성화를 통해) 최종 클래스를 예측합니다.
- 설정 예시:
. 이진분류: Dense(1, activation='sigmoid') + binary_crossentropy
. 다중클래스: Dense(num_classes, activation='softmax') + categorical_crossentropy
. 다중레이블: Dense(num_labels, activation='sigmoid') + binary_crossentropy
중간에 Dense(64, activation='relu') 같은 은닉층을 추가하면 표현력이 늘어나지만, 샘플 수가 적으면 과적합 위험도 커집니다.
입력(문장: 토큰 ID) : (B, L) e.g. (4, 7)
Embedding 출력 : (B, L, 128) e.g. (4, 7, 128)
BiLSTM(units=16, bi) : (B, 32) e.g. (4, 32) # 양방향이라 16*2
Dense(1, sigmoid) : (B, 1) e.g. (4, 1) # 확률 출력B (Batch size, 배치 크기)
한 번에 신경망 모델에 입력으로 주는 샘플 데이터의 개수입니다.
예를 들어 B=4라면, 동시에 4개의 문장이나 데이터 샘플이 모델에 입력됩니다.
배치 처리는 연산 효율과 학습 안정성 측면에서 매우 중요합니다.
일반적으로 학습 시 매개변수 업데이트는 배치 단위로 이루어집니다.
L (Sequence length, 시퀀스 길이)
각 입력 문장이 가지는 토큰(단어, 형태소 등)의 길이입니다.
예를 들어 L=7이라면, 한 문장은 7개의 토큰으로 구성되어 있다는 뜻입니다.
만약 문장 길이가 다르면, 일반적으로 패딩(padding)을 통해 일정한 길이(L)로 맞춥니다.
아래 작성된 코드는 Keras의 Sequential 모델(순차형 모델)을 사용하였습니다.
즉, 레이어들을 한 줄로 차례대로 쌓아 올린 구조이며, Embedding → BiLSTM → Dense 순서로 구성된 단순하고 직관적인 신경망 모델입니다.
모델 타입: Sequential
구성 레이어:
Embedding (단어를 벡터로 표현),
Bidirectional LSTM (BiLSTM) (문맥을 앞뒤 양방향으로 학습)
Dense (완전연결층) (출력층, 이진 분류 수행)
- Keras로 한국어 감성 분류 (Okt + BiLSTM)
- 개요: 텍스트 정제 → 형태소 단위 토큰화(+불용어) → 정수 시퀀스 → 패딩 →Embedding → BiLSTM → Dense → 콜백(ModelCheckpoint/EarlyStopping/TensorBoard)
10개 샘플만을 사용하였습니다.
# 0) 라이브러리 임포트 -----------------------------------------------------------------
import os, re, datetime, warnings # 운영체제, 정규식, 시간, 경고 제어
import numpy as np # 수치 연산 (배열/난수 등)
import pandas as pd # 테이블 데이터 구조(DataFrame)
import tensorflow as tf # 딥러닝 프레임워크 (Keras 포함)
from tensorflow.keras.preprocessing.text import Tokenizer # 텍스트→정수 인덱스
from tensorflow.keras.preprocessing.sequence import pad_sequences # 시퀀스 패딩
from konlpy.tag import Okt # 한국어 형태소 분석기 (Okt)
from tqdm import tqdm # 진행률 바
# 품질/재현성 설정 ----------------------------------------------------------------------
# warnings.filterwarnings('ignore') # 깔끔한 로그를 위해 경고 숨김
tqdm.pandas() # pandas의 apply와 함께 진행률 표시
# np.random.seed(42); tf.random.set_seed(42) # (선택) 실험 재현성 확보를 위한 시드 고정
# 1) 샘플 데이터 ------------------------------------------------------------------------
docs = [
"이 영화 정말 재미있어요!",
"완전 별로였어요. 시간 아까워요.",
"배우들의 연기가 뛰어났어요.",
"스토리가 너무 지루했어요.",
"감동적인 영화였습니다. 추천해요!",
"진짜 재미없고 실망했어요.",
"음악과 내용이 좋았어요.",
"내용이 복잡하고 이해가 안 갔어요.",
"다시 보고 싶은 영화에요.",
"별로 재미 없었어요. 그만 볼래요."
]
labels = [1,0,1,0,1,0,1,0,1,0] # 1=긍정, 0=부정
# DataFrame으로 정리 (학습/전처리에 편리)
df = pd.DataFrame({'document': docs, 'label': labels})
# 2) 텍스트 정제(클렌징) ---------------------------------------------------------------
# - 한글/영문/공백만 남기고 제거.
df['document'] = df['document'].str.replace(r'[^A-Za-z가-힣 ]', '', regex=True)
# 3) 형태소 분석 + 불용어 제거 ---------------------------------------------------------
okt = Okt()
stop_words = ['은','는','이','가','을','를','에','의','와','과','도','으로','하다']
def word_tokenization(text: str):
"""Okt 형태소 분석 후 불용어를 제거해 토큰 리스트를 반환"""
return [w for w in okt.morphs(text) if w not in stop_words]
# 진행률 바가 표시되는 apply로 전체 코퍼스 토큰화
data_tokenized = df['document'].progress_apply(word_tokenization)
# 4) 토크나이저 설정/학습 및 단어사전(vocabulary) 생성 ---------------------------------------------------------------
num_words = 10000
tokenizer = Tokenizer(oov_token='<OOV>', num_words=num_words)
tokenizer.fit_on_texts(data_tokenized)
seqs = tokenizer.texts_to_sequences(data_tokenized)
# 5) 시퀀스 길이 결정 및 패딩 -----------------------------------------------------------
max_length = max(len(s) for s in seqs)
x_data = pad_sequences(seqs, maxlen=max_length, padding='pre', truncating='post')
y_data = df['label'].to_numpy()
# 6) 어휘집 크기 계산 ------------------------------------------------------------------
vocab_size = min(num_words, len(tokenizer.word_index) + 1)
# 7) 모델 정의 -------------------------------------------------------------------------
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Bidirectional, LSTM, Dense
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, TensorBoard
model = Sequential([
Embedding(input_dim=vocab_size, output_dim=128, input_length=max_length),
Bidirectional(LSTM(16, activation='tanh')),
Dense(1, activation='sigmoid')
])
# 모델 컴파일
tf.keras.backend.clear_session()
model.compile(optimizer=Adam(1e-3), loss='binary_crossentropy', metrics=['accuracy'])
# 8) 콜백: 체크포인트/얼리스톱/텐서보드 -----------------------------------------------
checkpoint_cb = ModelCheckpoint(
'./best_model.weights.h5', save_weights_only=True,
save_best_only=True, monitor='val_loss', verbose=1
)
earlystopping_cb = EarlyStopping(
monitor='val_loss', patience=4, restore_best_weights=True, verbose=1
)
log_dir = './logs/' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
tensorboard_cb = TensorBoard(log_dir=log_dir, histogram_freq=1)
# 9) 학습 실행 -------------------------------------------------------------------------
history = model.fit(
x_data, y_data,
epochs=100,
batch_size=4,
validation_split=0.2,
callbacks=[checkpoint_cb, earlystopping_cb, tensorboard_cb],
verbose=1
)모델 테스트
# 테스트용 리뷰 문장 10개 직접 작성
test_sentences = [
"이 영화는 정말 최고였어요. 완전 추천합니다!",
"스토리가 너무 별로였고 지루했어요.",
"배우들의 연기가 인상적이었어요.",
"영화가 너무 재미없어서 졸았어요.",
"감동적이고 마음에 남는 영화였습니다.",
"별로 내용이 없고 실망했어요.",
"음악과 화면이 멋졌어요.",
"구성이 산만하고 이해가 안 됐어요.",
"다시 보고 싶어지는 영화입니다.",
"별로 재미가 없어서 돈이 아까웠어요."
]
# 전처리 과정 (위 학습 때와 동일한 방식 적용)
test_df = pd.DataFrame({'document': test_sentences})
# 특수문자 제거 (한글, 영어, 공백만 남김)
test_df['document'] = test_df['document'].str.replace(r'[^A-Za-z가-힣 ]', '', regex=True)
# 토큰화 및 불용어 제거 함수는 기존과 동일
def word_tokenization(text):
return [word for word in okt.morphs(text) if word not in stop_words]
# 형태소 분석 및 불용어 제거 적용
test_tokenized = test_df['document'].progress_apply(word_tokenization)
# 학습된 토크나이저로 시퀀스 변환
test_seq = tokenizer.texts_to_sequences(test_tokenized)
# 최대 길이 맞추기 (학습 때와 같은 max_length 사용)
x_test = pad_sequences(test_seq, maxlen=max_length, padding='pre', truncating='post')
# 모델 테스트 (평가)
predictions = model.predict(x_test)
# 예측 결과 출력 (확률과 긍정/부정 분류)
for i, pred in enumerate(predictions):
sentiment = '긍정' if pred[0] >= 0.5 else '부정'
print(f"문장: {test_sentences[i]}")
print(f"예측 확률: {pred[0]:.4f} -> 감정: {sentiment}")
print()모델 테스트 결과
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.