일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 컴파일
- 주간회고
- zero-shot
- #include
- Langchain
- sk네트웍스familyai캠프
- few-shot
- 12기
- sk네트웍스family
- 중복인클루드
- ai캠프
- FastAPI
- 21주차
- 회고록
- one-shot
- 전처리
- Docker
- 배포
- 헤더가드
- sk네트웍스familyai캠프12기
- openai
- 어셈블
- Rag
- 임베딩
- AWS
- C++
- Fine-tuning
- 최종프로젝트
- sk네트웍스ai캠프
- 소스코드
- Today
- Total
ansir 님의 블로그
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 자연어 딥러닝 기초 2025-04-25 본문
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 자연어 딥러닝 기초 2025-04-25
ansir 2025. 4. 25. 19:04시퀀스 데이터와 임베딩 벡터
시퀀스 데이터: 순서가 중요한 데이터
시계열: 시간에 따라서 변하는 데이터
텍스트: 단어 순서가 의미를 결정
임베딩 벡터의 필요성
고정된 길이의 숫자 배열
비슷한 의미의 단어는 비슷한 벡터를 가짐
동작: 임베딩 레이어가 단어 번호를 벡터로 변환
Word2Vec vs. Embedding Layer
Word2Vec: 사전학습모델( 대량의 데이터 ), 단어를 고정된 벡터로 제공, 사전 학습된 의미를 바로 사용 가능, 단점은 새로운 단어는 처리가 어려움
Embedding Layer: 모델 내부에서 학습되는 벡터, 데이터에 특화됨, 초기 벡터는 무작위라서 학습시간이 오래 걸림
시퀀스 데이터란?
순서가 중요한 데이터를 의미합니다.
이런 데이터에서는 각 요소의 순서가 의미에 영향을 줍니다.
예시:
시계열 데이터: 시간 순서에 따라 변화하는 데이터
👉 예: 주가, 기온, 센서 데이터 등
텍스트 데이터: 문장 내에서 단어의 순서가 문장의 의미를 결정
👉 예:
"나는 밥을 먹었다."
"밥을 나는 먹었다."
**두 문장은 단어는 같지만, 순서에 따라 강조나 의미가 달라짐**
임베딩 벡터의 필요성
딥러닝 모델은 텍스트를 직접 이해하지 못합니다.
그래서 텍스트를 숫자(벡터)로 변환해서 처리할 수 있어야 합니다.
이를 위해 사용하는 것이 임베딩(embedding) 입니다.
임베딩 벡터란?
고정된 길이의 숫자 배열(vector)
같은 의미나 비슷한 맥락에서 사용되는 단어는 비슷한 벡터값을 가지도록 학습됩니다.
예를 들어:
- "강아지"와 "개" → 비슷한 임베딩 벡터
- "사과"와 "의자" → 서로 다른 의미 → 벡터도 멀어짐
임베딩 동작 방식
단어 → 숫자 벡터로 변환
텍스트 데이터는 일반적으로 먼저 단어 번호(index)로 바뀌고,
그 번호가 임베딩 레이어(Embedding Layer)를 거쳐 벡터로 변환됩니다.
입력: [4, 6, 2]
↓ 임베딩 레이어
출력: [[0.1, 0.3, ...], [0.4, 0.8, ...], [0.9, 0.2, ...]]
Word2Vec vs. Embedding Layer
구분 | Word2Vec | Embedding Layer |
---|---|---|
설명 | 사전 학습된 임베딩 모델 | 모델 내부에서 직접 학습 |
학습 데이터 | 대규모 일반 텍스트 (뉴스, 위키 등) | 우리가 사용하는 특정 데이터셋 |
특징 | 이미 의미가 반영된 벡터 제공 | 모델이 학습하면서 의미를 반영 |
장점 | 빠르게 적용 가능, 일반적인 의미 반영 | 현재 작업에 특화된 의미 반영 가능 |
단점 | 새로운 단어 처리 어려움 | 처음엔 무작위 벡터 → 학습 시간이 필요 |
요약
- 시퀀스 데이터는 순서가 중요하며, 텍스트와 시계열 데이터가 대표적.
- 딥러닝은 텍스트를 이해 못 하므로, 숫자 벡터로 변환(임베딩)해야 함.
- Word2Vec은 사전 학습된 임베딩, Embedding Layer는 모델 내부에서 직접 학습되는 임베딩.
시퀀스 데이터 임베딩 실습 (Keras & PyTorch 비교)
import numpy as np
import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import torch
import torch.nn as nn
# 데이터
sample_sequence = [
"영화가 정말 재밌어요",
"이 영화 별로에요",
"스토리가 최고입니다.",
"배우 연기가 좋아요.",
"너무 지루한 영화"
]
# 토큰화 및 정수 인코딩
# Out-Of_Vocabulary: 데이터에 없는 단어가 테스트나 추론에 등장할 수 있어서 특수 토큰을 매핑해준다.
tokenizer = Tokenizer(oov_token="<OOV>")
tokenizer.fit_on_texts(sample_sequence)
sequences = tokenizer.texts_to_sequences(sample_sequence)
print(f"sequences: {sequences}")
# 단어 크기 확인
print(f"tokenizer.word_index: {tokenizer.word_index}")
vocab_size = len(tokenizer.word_index) + 1 # 패딩을 위해서 자리 하나 확보
print(f"vocab_size: {vocab_size}")
# 패딩( 시퀀스의 길이 통일 )
max_length = 5
padded_sequences = pad_sequences(sequences, maxlen=max_length, padding="post", truncating="post")
print(f"padded_sequences: \n{padded_sequences}")
# Embedding 레이어 사용
embedding_dim = 10
keras_model = tf.keras.Sequential([
tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim),
# tf.keras.layers.Flatten(),
# tf.keras.layers.Dense(1, activation="sigmoid")
])
# 임베딩 출력 확인
embedding_output = keras_model.predict(padded_sequences)
print(f"embedding_output: {embedding_output.shape}")
# 임베딩 레이어만 이용한 모델
class EmbeddingModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(EmbeddingModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
def forward(self, x):
return self.embedding(x)
# 모델 생성
model = EmbeddingModel(vocab_size, embedding_dim)
# 출력
output = model(torch.tensor(padded_sequences))
print(f"output: {output.shape}") # ( 샘플 수, max_len, embedding_dim )
# embedding_dim 벡터 공간에 얼마나 많은 정보를 표현하는지 설정 100~300 선호 커지면 표현력이 높아지고 학습 오래 걸리고, 과적합
# 적은 차원에서 늘려가면서 성능을 비교하는게 좋다.
위 코드는 자연어 문장을 숫자 시퀀스로 바꾸고, 임베딩 벡터로 변환하는 과정을 Keras와 PyTorch 양쪽에서 연습하는 예제입니다.
1. 데이터 준비
sample_sequence = [
"영화가 정말 재밌어요",
"이 영화 별로에요",
"스토리가 최고입니다.",
"배우 연기가 좋아요.",
"너무 지루한 영화"
]
간단한 한글 문장 5개를 준비합니다.
2. 텍스트 토큰화 & 정수 인코딩
tokenizer = Tokenizer(oov_token="<OOV>")
tokenizer.fit_on_texts(sample_sequence)
sequences = tokenizer.texts_to_sequences(sample_sequence)
Tokenizer: 단어를 숫자로 바꾸는 도구
fit_on_texts
: 단어 사전을 만들고 각 단어에 번호를 매김texts_to_sequences
: 실제 문장을 숫자 리스트로 변환
💡 oov_token?
"Out-of-Vocabulary"
- 학습 시 없던 단어가 추론 시 등장할 경우를 대비해 특수 토큰 으로 대체
실제 배포 모델에서는 꼭 넣는 게 좋습니다.
3. 단어 사전과 시퀀스 확인
print(tokenizer.word_index)
print(sequences)
word_index: 각 단어와 번호가 매핑된 딕셔너리
vocab_size: 단어 개수 + 1
👉 Keras의 Embedding은 0번 인덱스를 패딩 용도로 사용하기 때문!
4. 시퀀스 길이 맞추기 (패딩)
padded_sequences = pad_sequences(sequences, maxlen=5, padding="post", truncating="post")
문장마다 단어 수가 다르다 → 신경망은 고정된 입력만 받음
그래서 모든 문장의 길이를 맞춰줘야 함 → 패딩
padding="post": 뒤에 0을 붙임
truncating="post": 너무 긴 문장은 뒤에서 잘림
5. Keras 임베딩 레이어 사용
keras_model = tf.keras.Sequential([
tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim),
])
input_dim: 단어 사전 크기 (vocab_size)
output_dim: 벡터의 차원 (embedding_dim)
embedding_output = keras_model.predict(padded_sequences)
print(embedding_output.shape)
출력 형태: (샘플 수, 시퀀스 길이, 임베딩 차원)
→ (5, 5, 10)
6. PyTorch 임베딩 모델 정의
class EmbeddingModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(EmbeddingModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
def forward(self, x):
return self.embedding(x)
nn.Embedding: Keras와 동일한 역할
텐서 x를 넣으면 해당하는 임베딩 벡터들을 반환
output = model(torch.tensor(padded_sequences))
print(output.shape)
7. 임베딩 차원 설정 팁
"embedding_dim 벡터 공간에 얼마나 많은 정보를 표현하는지 설정 100~300 선호"
임베딩 차원 수가 클수록 표현력은 높아짐
👉 더 미세한 의미 차이까지 표현 가능
그러나 너무 크면:
학습 시간 증가
과적합 가능성 증가
그래서 적절한 차원을 찾기 위해 작은 값부터 늘려가며 실험하는 게 좋습니다.
요약
단계 | 설명 |
---|---|
텍스트 → 숫자 | Tokenizer로 단어를 숫자로 변환 |
시퀀스 정리 | pad_sequences로 길이 맞추기 |
임베딩 | 단어 번호 → 고정 길이 벡터로 변환 |
Keras vs PyTorch | 임베딩 구현 방식만 다르고 동작은 유사 |
RNN, LSTM, GRU — 순환 신경망(Recurrent Neural Networks)
RNN: 이전 단계를 기억하면서 다음 단계를 처리한다.
구조: 각 시간단계( timestemp )에서 입력( 단어, 숫자 )을 받고 은닉상태( hidden state )를 업데이트 -> 과거 정보를 요약
Recurrent: 같은 가중치를 모든 시간대 단계에 공유해서 시퀀스를 순차적으로 처리
LSTM / GRU: 장기 의존성 문제
RNN은 긴 시퀀스에서 초기 정보가 점점 희석되는 기울기 소실 기억을 저장하는 gate를 추가해 중요한 정보는 오래 기억
입력 게이트: 새로운 정보를 추가
망각 게이트: 불필요한 정보 삭제
출력 게이트: 현재 출력 결정
GRU: LSTM에서 게이트 수를 줄임
RNN (Recurrent Neural Network)
이전의 정보를 기억하면서 다음 입력을 처리하는 구조
핵심 아이디어
문장처럼 순서가 중요한 데이터는 단순한 신경망으로 처리하기 어려움
그래서 이전 단계의 정보를 기억해서 다음 단계를 처리하는 모델이 RNN
구조 설명
입력: 시퀀스의 각 요소 (예: 단어 또는 숫자)
매 단계에서:
현재 입력 + 이전의 은닉 상태(hidden state) 를 이용해
새로운 은닉 상태를 계산하고 업데이트
이 은닉 상태는 과거 정보 요약본처럼 작용
Recurrent(반복되는) 구조
모든 시간 단계에서 같은 가중치를 사용
즉, 하나의 RNN 셀을 복사해서 여러 시간 단계에 반복 사용
➡️ 장점: 파라미터 수가 일정해서 효율적
➡️ 단점: 긴 시퀀스에서는 과거 정보가 점점 희석됨
RNN의 한계:
장기 의존성 문제
긴 문장에서 앞부분의 정보가 뒤로 갈수록 기억이 약해지는 현상
기울기 소실(Gradient Vanishing)
학습 과정에서 역전파할 때, 초반 입력의 영향이 뒤로 갈수록 점점 작아짐
결국 초기 정보가 사라져버리는 문제
LSTM (Long Short-Term Memory)
기억을 잘 유지하고 잊을 수 있는 능력이 있는 RNN의 확장 버전
해결 방법: "게이트(Gate)" 구조 추가
게이트 종류 | 역할 |
---|---|
입력 게이트 | 새로운 정보를 기억할지 결정 |
망각(Forget) 게이트 | 이전 기억 중 어떤 걸 잊을지 결정 |
출력 게이트 | 어떤 정보를 다음 단계로 출력할지 결정 |
게이트들은 모두 0~1 사이의 값을 가진 시그모이드 함수로 구성됨
→ 각 게이트가 정보 흐름을 조절하는 필터 역할을 함
➡️ 중요한 정보는 오래 기억, 덜 중요한 건 잊음
GRU (Gated Recurrent Unit)
LSTM보다 간단한 구조로 게이트 수를 줄인 버전
GRU의 특징
입력 게이트와 망각 게이트를 하나로 통합 → 업데이트 게이트
구조가 더 단순해서 학습 속도가 빠르고 성능도 비슷
LSTM | GRU |
---|---|
게이트 3개 (입력, 망각, 출력) | 게이트 2개 (업데이트, 리셋) |
더 세밀한 제어 가능 | 계산량 적고 빠름 |
긴 시퀀스에 강함 | 일반적인 상황에서 효율적 |
요약
모델 | 특징 | 장단점 |
---|---|---|
RNN | 순서대로 기억 & 처리 | 긴 시퀀스에서 정보가 사라짐 (기울기 소실) |
LSTM | 게이트로 기억 제어 | 오래된 정보도 잘 유지, 계산 복잡 |
GRU | 간소화된 LSTM | 빠르고 간단하지만 LSTM만큼 정밀하진 않음 |
[1, 2, 3] → 4 예측 LSTM 실습
숫자 시퀀스 데이터를 기반으로 다음 숫자를 예측하는 모델 학습
예시: [1, 2, 3] → 4를 예측
모델: PyTorch 기반 LSTM 모델
# ex [1,2,3] - 4 예측
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader
# 랜덤한 시퀀스 생성하는 데이터셋
class SequenceDataset(Dataset):
def __init__ (self, sq_length=3, data_size=1000):
self.sq_length = sq_length
self.data = []
for _ in range(data_size):
start = np.random.randint(1, 100)
seq = list(range(start, start + sq_length+1))
self.data.append(seq)
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
seq = self.data[idx]
input_seq = seq[:-1]
target = seq[-1]
return torch.FloatTensor(input_seq).unsqueeze(-1), torch.FloatTensor([target]).unsqueeze(-1)
# 데이터셋과 데이터로더
dataset = SequenceDataset()
dataloader = DataLoader(dataset, batch_size=16, shuffle=True)
# 데이터셋 형태 확인
# input,target = next(iter(dataset))
# print(f"input: {input}, target: {target}")
# LSTM 모델 정의
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x ):
out, (hn, cn) = self.lstm(x)
out = self.fc(out[:, -1:, :]) # 마지막 타임스텝의 출력( batch, output_size )
return out
# 모델 생성
input_size = 1 # 입력 차원( 숫자 )
hidden_size = 16
output_size = 1
model = LSTMModel(input_size, hidden_size, output_size)
# 학습 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
num_epochs = 200
# 학습 및 손실기록
losses = []
for epoch in range(num_epochs):
epoch_loss = 0
for inputs,targets in dataloader:
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
losses.append(epoch_loss / len(dataloader))
if (epoch+1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss / len(dataloader):.4f}')
# 예측 테스트 및 시각화
model.eval()
# 1, 2, 3 -> 4
test_seq = torch.FloatTensor([[1, 2, 3]]).unsqueeze(-1).to(device)
with torch.no_grad():
prediction = model(test_seq)
print(f'prediction : {prediction.item()}')
# 시각화
plt.plot([0, 1, 2], [1, 2, 3], label='input sequence', marker='o')
plt.plot([2, 3], [3, prediction.item()], label='prediction', marker='x', linestyle='--')
plt.xlabel('Time step')
plt.ylabel('Value')
plt.title('Sequence Prediction')
plt.legend()
plt.grid(True)
plt.show()
1. 데이터 생성
class SequenceDataset(Dataset):
init: 랜덤한 시작 숫자에서 [start, start+1, ..., start+3] 형태의 시퀀스를 만듦
예: [42, 43, 44, 45]
앞의 3개는 입력
마지막 숫자는 정답(label)
getitem:
입력 시퀀스: [42, 43, 44] → (3, 1) 텐서로 반환
타깃: [45] → (1, 1) 텐서
2. LSTM 모델 구조
class LSTMModel(nn.Module):
구성:
nn.LSTM(input_size, hidden_size)
→ 숫자 한 개씩 들어오고 (input_size = 1), 은닉 상태는 16차원
out[:, -1:, :]:
→ 시퀀스 마지막 단계 출력만 사용 (미래 예측에 필요)
nn.Linear(hidden_size, output_size)
→ 예측 결과는 숫자 하나 (output_size = 1)
3. 학습 과정
for epoch in range(num_epochs):
옵티마이저: Adam
손실함수: MSELoss (평균제곱오차, 회귀 문제에 적합)
에폭마다 손실 기록 후 출력
4. 예측
test_seq = torch.FloatTensor([[1,2,3]])
[1, 2, 3] 시퀀스를 넣고 4가 나오는지 확인
model.eval() 모드로 예측
예측 결과 출력 및 시각화
plt.plot([0, 1, 2], [1, 2, 3], label='input sequence', marker='o')
plt.plot([2, 3], [3, prediction.item()], label='prediction', marker='x', linestyle='--')
파란 선: 입력 시퀀스
주황 선: 마지막 숫자부터 예측한 값까지 연결
요약
구성 요소 | 설명 |
---|---|
데이터 | 연속 숫자 시퀀스: [n, n+1, n+2, n+3] |
모델 | LSTM → 마지막 hidden → 선형 → 숫자 하나 예측 |
목적 | 입력 [a, b, c] → 다음 숫자 d 예측 |
평가 | MSELoss, 예측 결과 시각화 |
추가 설명
- LSTM은 시퀀스 데이터에서 과거 정보를 잘 유지하면서 미래를 예측하는 데에 탁월합니다.
- 이 예제는 숫자지만, 텍스트(단어 시퀀스)로 확장하면 언어모델, 문장 생성기가 될 수 있습니다.
- input.unsqueeze(-1): 시퀀스를 (batch, seq_len, input_size) 모양으로 만들기 위함
- 실제 자연어에서는 임베딩 레이어로 단어 → 벡터로 바꾸고 그 벡터를 LSTM에 넣음
텍스트 분류 모델( LSTM 기반 )
감상분류, 카테고리 분류, 스펨여부
1. 시퀀스 -> 벡터 -> 클래스 분류 구조
텍스트 분류모델은 다음단계를 거친다
- 시퀀스 입력: 문장을 단어 시퀀스로 변환
- 임베딩: 각 단어를 벡터로 변환
- LSTM 처리: 처리
- 분류: 최종 벡터를 Dense 레이어로 변환
2. 그 밖의 레이어
- Dropout, Dense, Activation
텍스트 분류란 문장을 분석해서 감정, 주제, 스팸 여부 같은 클래스를 예측하는 작업입니다.
예를 들어:
감상 분류: "너무 재미있어요" → 긍정
카테고리 분류: "이 영화는 스릴러다" → 장르: 스릴러
스팸 여부: "무료 문자입니다" → 스팸
- 이런 작업을 하기 위해 텍스트를 숫자로 변환하고 LSTM 모델에 넣는 구조입니다.
1. 전체 구조 흐름
"시퀀스 → 벡터 → 클래스 분류" 순으로 진행됩니다.
단계별 설명:
시퀀스 입력 (텍스트 → 시퀀스)
문장을 단어 단위로 자르고 정수로 인코딩
→ 예: "너무 재밌어요" → [3, 15, 29]
임베딩 (Embedding Layer)
정수로 표현된 단어를 고정된 크기의 벡터로 변환
→ [3, 15, 29] → [[0.1, -0.3, ...], [...], [...]]
- 임베딩은 단어 간 의미 유사성을 벡터로 학습하는 과정
LSTM 처리
단어 시퀀스를 순서대로 처리하며 문장의 의미 요약
시간 순서에 따라 정보를 기억하고, 문맥을 이해함
출력은 전체 문장의 정보를 담은 벡터
분류 (Dense Layer)
LSTM이 생성한 벡터를 Dense 레이어에 통과시켜 클래스 확률로 변환
출력 예: [0.1, 0.9] → 부정 10%, 긍정 90%
2. 그 밖에 사용되는 레이어들
Dropout
과적합을 막기 위해 일부 뉴런을 랜덤하게 끄는 레이어
학습할 때만 작동하며, 일반화 성능 향상
Dense (전결합층)
LSTM의 출력을 받아 분류 결과를 만들어내는 마지막 단계
출력 노드 수 = 클래스 수
Activation (활성화 함수)
예측 결과를 확률 형태로 만들기 위해 사용
이진 분류: sigmoid
다중 분류: softmax
전체 흐름 요약 다이어그램
[문장]
↓
[토큰화 → 시퀀스]
↓
[Embedding Layer]
↓
[LSTM Layer]
↓
[Dropout (선택)]
↓
[Dense + Activation (분류 결과)]
한국어 영화 리뷰 데이터를 이용한 감성 분류(LSTM 기반)
!pip install konlpy -q
import torch
import torch.nn as nn
import numpy as np
from konlpy.tag import Okt
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from tensorflow.keras.preprocessing.sequence import pad_sequences
import re
!wget http://skt-lsl-nlp-model.s3.amazonaws.com/KoBERT/datasets/nsmc/ratings_train.txt
!wget http://skt-lsl-nlp-model.s3.amazonaws.com/KoBERT/datasets/nsmc/ratings_test.txt
# 학습에서 만든 vocab 재사용하고, 테스트셋에서 UNK 처리
def wordtoindex(wordlists, vocab):
return [vocab.get(word, vocab['<UNK>']) for word in wordlists]
train_df = pd.read_csv('ratings_train.txt', sep='\t')[:10000]
train_df.dropna(inplace=True)
train_df.reset_index(drop=True, inplace=True)
# 전처리 함수
# 텍스트를 토큰화(형태소) 정수인코딩, 패딩수행
# 특수토큰 <PAD>,<UNK> 0, 1
# 리턴값 : 패딩이 완료된 데이터, 단어사전
okt = Okt()
# 한글과 공백만 추출
X = train_df['document'].apply(lambda x: re.sub(r'[^ㄱ-ㅎ가-힣\s]+', '', x))
X = X.apply(lambda x : okt.morphs(x, stem=True))
vocab = {word:i for i, word in \
enumerate(['<PAD>','<UNK>' ] + list(set( [word for words in X.to_list() \
for word in words]))
)}
X = X.apply(lambda x: wordtoindex(x,vocab))
max_len = 10
X = X.apply(lambda x : pad_sequences([x],maxlen=max_len, padding='post',truncating='post').tolist()[0])
# 데이터셋
class NaverMovieDataset(Dataset):
def __init__(self, X, y):
self.X = X
self.y = y
def __len__(self):
return len(self.X)
def __getitem__(self, index):
return torch.LongTensor( self.X[index]).clone().detach().squeeze(), \
torch.LongTensor( [self.y[index]]).clone().detach().squeeze()
dataset = NaverMovieDataset(X, train_df['label'].to_list() )
dataloader = DataLoader(dataset,batch_size=32,shuffle=True)
# LSTM 모델 정의
class NaverLSTMModel(nn.Module):
def __init__(self,vocab_size, ebedding_dim, hidden_size,output_size):
super().__init__()
self.embedding = nn.Embedding(vocab_size, ebedding_dim)
self.lstm = nn.LSTM(ebedding_dim, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size,output_size)
def forward(self,x):
embedding = self.embedding(x) # batch, max_len, ebedding_dim
out, (hn,cn) = self.lstm(embedding) # batch, max_len, hidden_state
out = self.fc(out[:,-1,:]) # 마지막 타임 스탬프
return out
embedding_dim = 100
hidden_size = 128
output_size = 2 # 긍정부정
vocab_size = len(vocab)
model = NaverLSTMModel(vocab_size,embedding_dim,hidden_size,output_size)
# 학습설정 손실함수, 옵티마이져, 학습루프
from tqdm import tqdm
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = model.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
train_loss = []
for epoch in range(10):
epoch_loss = 0
iterator = tqdm(dataloader)
for sequence, labels in iterator:
sequence, labels = sequence.to(device), labels.to(device)
outputs = model(sequence)
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
epoch_loss += loss.item()
iterator.set_description(f'epoch : {epoch+1}, loss : {epoch_loss:.4f}')
train_loss.append(epoch_loss / len(dataloader) )
print(f'epoch : {epoch+1}, epoch loss : {epoch_loss / len(dataloader):.4f}' )
# 평가
test_df = pd.read_csv('ratings_test.txt', sep='\t')[:10000]
test_df.dropna(inplace=True)
test_df.reset_index(drop=True, inplace=True)
# 테스트셋 전처리 (vocab은 학습 시 사용한 vocab 사용!)
X = test_df['document'].apply(lambda x: re.sub(r'[^ㄱ-ㅎ가-힣\s]+', '', x))
X = X.apply(lambda x : okt.morphs(x, stem=True))
X = X.apply(lambda x : wordtoindex(x, vocab))
X = X.apply(lambda x : pad_sequences([x], maxlen=max_len, padding='post', truncating='post').tolist()[0])
# 텐서 데이터로 변환
dataset = NaverMovieDataset(X, test_df['label'].to_list())
dataloader = DataLoader(dataset, batch_size=32, shuffle=False)
model.eval()
with torch.no_grad():
correct = 0
total = 0
for sequence, labels in dataloader:
sequence, labels = sequence.to(device), labels.to(device)
outputs = model(sequence)
correct += (torch.argmax(outputs,dim=1) == labels).sum().item()
correct / len(dataloader.dataset)
1. 라이브러리 설치 및 임포트
!pip install konlpy -q
!wget http://skt-lsl-nlp-model.s3.amazonaws.com/KoBERT/datasets/nsmc/ratings_train.txt
!wget http://skt-lsl-nlp-model.s3.amazonaws.com/KoBERT/datasets/nsmc/ratings_test.txt
konlpy: 한국어 형태소 분석기 (이 코드에서는 Okt 사용)
- wget: NSMC 데이터셋을 다운로드하는 데 사용됨 (네이버 영화 리뷰 감성 분류용)
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import numpy as np
import re
from konlpy.tag import Okt
from tensorflow.keras.preprocessing.sequence import pad_sequences
2. 학습 데이터 로드 및 전처리
train_df = pd.read_csv('ratings_train.txt', sep='\t')[:10000]
train_df.dropna(inplace=True)
train_df.reset_index(drop=True, inplace=True)
- 학습 데이터셋을 불러오고, 결측치를 제거함.
- 10,000개 샘플만 사용하여 간단한 실습 형태로 구성.
전처리 작업
okt = Okt()
X = train_df['document'].apply(lambda x: re.sub(r'[^ㄱ-ㅎ가-힣\s]+', '', x))
X = X.apply(lambda x : okt.morphs(x, stem=True))
- 특수문자 제거 후, 형태소 단위로 분해 (e.g. "재밌다" → "재밌", "다")
- 형태소 분석은 한국어의 문법 구조를 분석하기 위해 꼭 필요함.
단어 인덱싱
vocab = {word:i for i, word in enumerate(['<PAD>', '<UNK>'] + list(set(word for words in X for word in words)))}
def wordtoindex(wordlists, vocab):
return [vocab.get(word, vocab['<UNK>']) for word in wordlists]
X = X.apply(lambda x: wordtoindex(x, vocab))
- 단어 사전을 만들어 각 단어를 고유한 정수로 변환.
- 사전에 없는 단어는 (Unknown)으로 처리.
패딩 처리
max_len = 10
X = X.apply(lambda x : pad_sequences([x], maxlen=max_len, padding='post', truncating='post').tolist()[0])
- 각 문장의 길이를 동일하게 맞추기 위해 패딩 적용 (문장 길이 10 기준)
- LSTM은 고정된 길이의 입력만 받으므로 필수 과정.
3. PyTorch Dataset 정의
class NaverMovieDataset(Dataset):
def __init__(self, X, y):
self.X = X
self.y = y
def __len__(self):
return len(self.X)
def __getitem__(self, index):
return torch.LongTensor(self.X[index]), torch.LongTensor([self.y[index]]).squeeze()
- PyTorch에서 데이터를 쉽게 불러오도록 custom Dataset 클래스를 정의.
- 텍스트와 라벨을 텐서 형태로 변환.
dataset = NaverMovieDataset(X, train_df['label'].to_list())
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
- DataLoader는 미니배치 단위로 데이터를 모델에 전달함.
4. LSTM 모델 정의
class NaverLSTMModel(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_size, output_size):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
embedding = self.embedding(x)
out, _ = self.lstm(embedding)
out = self.fc(out[:, -1, :]) # 마지막 시점의 hidden state만 사용
return out
- 임베딩 레이어: 정수 인덱스를 실수 벡터로 변환 (단어 의미 표현)
- LSTM 레이어: 순서를 가진 시퀀스 데이터를 처리 (시간의 흐름 반영)
- FC 레이어: 마지막 hidden state를 이용해 최종 출력 (긍정/부정 예측)
embedding_dim = 100
hidden_size = 128
output_size = 2
vocab_size = len(vocab)
model = NaverLSTMModel(vocab_size, embedding_dim, hidden_size, output_size)
5. 학습 루프
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = model.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
train_loss = []
for epoch in range(10):
epoch_loss = 0
iterator = tqdm(dataloader)
for sequence, labels in iterator:
sequence, labels = sequence.to(device), labels.to(device)
outputs = model(sequence)
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
epoch_loss += loss.item()
iterator.set_description(f'epoch : {epoch+1}, loss : {epoch_loss:.4f}')
train_loss.append(epoch_loss / len(dataloader))
- 손실 함수는 CrossEntropy (이진 분류용)
- 에폭마다 평균 손실을 저장
- tqdm으로 진행 상황 시각화
6. 평가 (Test Set)
test_df = pd.read_csv('ratings_test.txt', sep='\t')[:10000]
test_df.dropna(inplace=True)
test_df.reset_index(drop=True, inplace=True)
테스트 데이터 로딩
X = test_df['document'].apply(lambda x: re.sub(r'[^ㄱ-ㅎ가-힣\s]+', '', x))
X = X.apply(lambda x : okt.morphs(x, stem=True))
X = X.apply(lambda x : wordtoindex(x, vocab))
X = X.apply(lambda x : pad_sequences([x], maxlen=max_len, padding='post', truncating='post').tolist()[0])
학습과 동일한 방식으로 전처리 (vocab은 새로 만들지 않고 그대로 사용!)
dataset = NaverMovieDataset(X, test_df['label'].to_list())
dataloader = DataLoader(dataset, batch_size=32, shuffle=False)
model.eval()
with torch.no_grad():
correct = 0
for sequence, labels in dataloader:
sequence, labels = sequence.to(device), labels.to(device)
outputs = model(sequence)
correct += (torch.argmax(outputs, dim=1) == labels).sum().item()
accuracy = correct / len(dataloader.dataset)
- 학습된 모델을 이용해 정확도 평가 진행 (전체 샘플 중 맞춘 비율 계산)
전체 요약
단계 | 설명 |
---|---|
데이터 로딩 | 네이버 영화 리뷰 긍정/부정 데이터셋 |
전처리 | 형태소 분석, 정수 인코딩, 패딩 |
데이터 준비 | PyTorch Dataset, DataLoader 구성 |
모델 정의 | 임베딩 + LSTM + Linear (이진 분류) |
학습 루프 | 손실 계산, 역전파, 옵티마이저 적용 |
평가 | 테스트셋 정확도 계산 |
성능 개선
Bidirectional 구조
양방향 LSTM( BiLSTM ): 시퀀스를 순방향과 역방향 동시에
Overfitting: Dropout
정해진 확률로 임의의 뉴런을 무효화( 가중치를 0 )
Batch Normalization
배치정규화: 각 레이어의 출력 평균 0 분산 1로 정규화
학습을 안정적, 속도 증가
1. Bidirectional LSTM (양방향 LSTM)
✔ 개념
일반 LSTM은 시퀀스를 앞에서 뒤로 한 번만 처리함.
BiLSTM은 앞에서 뒤로 + 뒤에서 앞으로 두 번 처리하여 더 풍부한 문맥 정보를 학습함.
왜 좋은가?
특히 자연어처럼 앞뒤 문맥이 중요한 경우에 효과적.
예: "이 영화는 정말 재미없다." → "재미"를 보고 긍정으로 판단할 수 있지만, "없다"까지 봐야 부정임을 정확히 알 수 있음.
✔ 코드 적용 예
self.lstm = nn.LSTM(embedding_dim, hidden_size, batch_first=True, bidirectional=True)
self.fc = nn.Linear(hidden_size * 2, output_size) # 두 방향을 합치므로 x2
2. Dropout
✔ 개념
학습 중 일부 뉴런을 무작위로 꺼서 (출력을 0으로 만들어) 과적합 방지.
학습 시에만 적용되고, 테스트 시에는 전체 뉴런 사용.
✔ 왜 좋은가?
특정 뉴런에만 의존하는 걸 방지해 일반화 능력 향상.
특히 작은 데이터셋이나 복잡한 모델에서 효과적.
✔ 코드 적용 예
self.dropout = nn.Dropout(p=0.5)
...
out = self.dropout(out)
- 보통 LSTM의 출력이나 FC layer 앞에 붙여줌.
3. Batch Normalization (배치 정규화)
✔ 개념
각 미니배치의 출력값을 평균 0, 분산 1로 정규화.
학습을 빠르고 안정적으로 함 (그래디언트 폭발/소실 방지).
✔ 왜 좋은가?
학습 속도 향상 (더 큰 학습률 가능)
드랍아웃처럼 약간의 정규화 효과도 있음.
✔ 코드 적용 예
self.batchnorm = nn.BatchNorm1d(hidden_size * 2)
...
out = self.batchnorm(out)
- LSTM은 시퀀스 데이터라 BatchNorm1d를 시퀀스 축을 평균 내거나 마지막 hidden state에 적용하는 식으로 사용.
🔧 통합 적용 예시
LSTM 블록을 아래처럼 개선할 수 있습니다.
요약 정리
기법 | 목적 | 적용 위치 |
---|---|---|
BiLSTM | 문맥 정보 강화 | LSTM 레이어 정의 시 bidirectional=True, FC input 크기 x2 |
Dropout | 과적합 방지 | LSTM 출력 후나 FC 전 |
BatchNorm | 학습 안정화 및 속도 향상 | FC 입력 직전 혹은 LSTM 마지막 출력에 적용 |
def wordtoindex(wordlists,vocab):
return [vocab.get(word,1) for word in wordlists]
def getDataLoader(filepath,vocab=None):
df = pd.read_csv(filepath, sep='\t')[:10000]
df.dropna(inplace=True)
df.reset_index(drop=True, inplace=True)
okt = Okt()
# 한글과 공백만 추출
X = df['document'].apply(lambda x: re.sub(r'[^ㄱ-ㅎ가-힣\s]+', '', x))
X = X.apply(lambda x : okt.morphs(x, stem=True))
if vocab == None:
vocab = {word:i for i, word in \
enumerate(['<PAD>','<UNK>' ] + list(set( [word for words in X.to_list() \
for word in words]))
)}
X = X.apply(lambda x: wordtoindex(x,vocab))
max_len = 10
X = X.apply(lambda x : pad_sequences([x],maxlen=max_len, padding='post',truncating='post'))
dataset = NaverMovieDataset(X, df['label'].to_list() )
return DataLoader(dataset,batch_size=32,shuffle=True), vocab
train_loader,vocab = getDataLoader('ratings_train.txt')
test_loader, _ = getDataLoader('ratings_test.txt', vocab=vocab)
# LSTM 모델 정의
class NaverBiLSTMModel(nn.Module):
def __init__(self,vocab_size, ebedding_dim, hidden_size,output_size):
super().__init__()
self.embedding = nn.Embedding(vocab_size, ebedding_dim)
self.lstm = nn.LSTM(ebedding_dim, hidden_size, batch_first=True, bidirectional=True)
self.fc = nn.Linear(hidden_size*2,output_size)
self.dropout = nn.Dropout(0.3)
self.batch_norm = nn.BatchNorm1d(hidden_size*2) # BiLSTM은 hidden_size*2출력
def forward(self,x):
embedding = self.embedding(x) # batch, max_len, ebedding_dim
out, (hn,cn) = self.lstm(embedding) # batch, max_len, hidden_state
out = self.batch_norm(out[:,-1,:] )# 마지막 타임 스탬프
out = self.dropout(out)
out = self.fc(out)
return out
embedding_dim = 100
hidden_size = 128
output_size = 2 # 긍정부정
vocab_size = len(vocab)+1
print(vocab_size)
model = NaverBiLSTMModel(vocab_size,embedding_dim,hidden_size,output_size)
# 학습
# 학습설정 손실함수, 옵티마이져, 학습루프
from tqdm import tqdm
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = model.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
train_loss = []
for epoch in range(10):
epoch_loss = 0
iterator = tqdm(train_loader)
for sequence, labels in iterator:
sequence, labels = sequence.to(device), labels.to(device)
outputs = model(sequence)
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
epoch_loss += loss.item()
iterator.set_description(f'epoch : {epoch+1}, loss : {epoch_loss:.4f}')
train_loss.append(epoch_loss / len(train_loader) )
print(f'epoch : {epoch+1}, epoch loss : {epoch_loss / len(train_loader):.4f}' )
# 평가용 데이터 로드
model.eval()
with torch.no_grad():
correct = 0
total = 0
for sequence, labels in test_loader:
sequence, labels = sequence.to(device), labels.to(device)
outputs = model(sequence)
correct += (torch.argmax(outputs,dim=1) == labels).sum().item()
correct / len(test_loader.dataset)
1. wordtoindex 함수
def wordtoindex(wordlists, vocab):
return [vocab.get(word, 1) for word in wordlists]
- 각 단어 리스트를 정수 인덱스 리스트로 변환해줘.
- vocab.get(word, 1) → 사전에 없는 단어는 토큰의 인덱스 1로 처리함.
- 왜?: 딥러닝 모델은 텍스트 자체가 아니라 숫자를 입력받아야 하므로 모든 단어를 숫자로 바꿔야 함.
2. getDataLoader 함수
def getDataLoader(filepath, vocab=None):
...
X = df['document'].apply(lambda x: re.sub(r'[^ㄱ-ㅎ가-힣\s]+', '', x))
X = X.apply(lambda x: okt.morphs(x, stem=True))
- 데이터셋을 로드하고, 텍스트 전처리 진행.
- [^ㄱ-ㅎ가-힣\s]+ → 한글과 공백만 남기고 제거.
- okt.morphs(x, stem=True) → 형태소 단위로 분리 + 원형 복원 (예: "좋았어요" → "좋다")
if vocab is None:
vocab = {word: i for i, word in enumerate(['<PAD>','<UNK>'] + list(set(...)))}
- 처음 실행 시 단어 사전(vocab)을 생성
- : 패딩용(0), : 모르는 단어(1)
X = X.apply(lambda x: pad\_sequences(\[x\], maxlen=10, ...))
dataset = NaverMovieDataset(X, df\['label'\].to\_list())
- 문장을 최대 길이 10으로 잘라내고(또는 0으로 채움)
- 최종적으로 PyTorch Dataset 객체로 만듦
- DataLoader는 이 Dataset에서 배치 단위로 데이터를 꺼내는 역할
3. NaverBiLSTMModel 클래스
class NaverBiLSTMModel(nn.Module):
- PyTorch 모델 클래스. BiLSTM 구조와 정규화, 드롭아웃이 포함되어 있어.
주요 구성 요소:
구성 요소 | 설명 |
---|---|
nn.Embedding | 단어 인덱스를 밀집된 벡터로 변환 |
nn.LSTM(..., bidirectional=True) | 양방향 LSTM |
nn.Linear(hidden*2, output_size) | 2개의 방향 결과를 모두 사용 |
Dropout(0.3) | 30% 확률로 뉴런 제거 (과적합 방지) |
BatchNorm1d | 출력 정규화로 학습 안정화 |
out, (hn, cn) = self.lstm(embedding)
out = self.batch_norm(out[:, -1, :])
out = self.dropout(out)
out = self.fc(out)
out[:, -1, :]
: 마지막 타임스탬프의 출력- BatchNorm → Dropout → FC 순으로 통과시킴
4. 학습 루프
device = 'cuda' if torch.cuda.is\_available() else 'cpu'
...
for epoch in range(10):
for sequence, labels in train\_loader:
...
outputs = model(sequence)
loss = loss\_fn(outputs, labels)
...
- GPU가 있으면 cuda, 없으면 CPU로 학습
- CrossEntropyLoss: 분류 문제에서 자주 쓰이는 손실함수
- tqdm: 진행률 보여주는 바
각 에폭마다 평균 손실을 출력하며 학습 진행
5. 평가 루프
model.eval()
with torch.no\_grad():
for sequence, labels in test\_loader:
...
correct += (torch.argmax(outputs, dim=1) == labels).sum().item()
eval()
은 드롭아웃/배치정규화를 평가 모드로 전환torch.no_grad()
: 평가 시엔 그래디언트 계산 생략 (속도↑, 메모리↓)- 정답 개수를 세서 정확도 계산
최종 출력
correct / len(test\_loader.dataset)
- 전체 데이터 중 정답 맞춘 비율 → 정확도
요약
- Tokenizer / Okt 문장을 단어 단위로 자르는 도구
- Padding 문장 길이를 맞추기 위해 0으로 채움
- Embedding 단어를 숫자로 표현한 벡터
- LSTM / BiLSTM 시퀀스를 처리하는 RNN 계열 모델
- Dropout 일부 뉴런 끄기 → 과적합 방지
- BatchNorm 출력값 정규화 → 학습 안정화
- FC (Fully Connected) 마지막 분류기 역할
- Softmax 출력값을 확률처럼 해주는 함수 (암묵적으로 CrossEntropyLoss에 포함됨)
'SK 네트웍스 family AI 캠프 > 수업 내용 복습' 카테고리의 다른 글
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 자연어 딥러닝 응용(신경망 기계번역) 2025-04-29 (0) | 2025.05.01 |
---|---|
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 자연어 딥러닝 응용(언어 모델링) 2025-04-28 (0) | 2025.04.28 |
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 자연어 임베딩 이해 2025-04-24 (0) | 2025.04.24 |
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 자연어 처리 기법 2025-04-22 (1) | 2025.04.22 |
[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] 모델 검증 2025-04-15 (0) | 2025.04.15 |