어제 있었던 LSTM 과제 실습중 몇가지 수정을 하고, 예측값 63.x를 도출하며 예측 점수까지 짜봤다.
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from torch.nn.utils.rnn import pad_sequence
import numpy as np
df = pd.read_csv("netflix_reviews.csv") # 파일 불러오기
df = df.iloc[:,0:5]
# 전처리 함수
import re
def preprocess_text(text):
if isinstance(text, float):
return ""
text = text.lower() # 대문자를 소문자로
text = re.sub(r'[^\w\s]', '', text) # 구두점 제거
text = re.sub(r'\d+', '', text) # 숫자 제거
text = text.strip() # 띄어쓰기 제외하고 빈 칸 제거
return text
df['reviewId'] = df['reviewId'].apply(preprocess_text)
df['userName'] = df['userName'].apply(preprocess_text)
df['content'] = df['content'].apply(preprocess_text)
# reviews,ratings 설정 시리즈 -> 리스트 형태로(torch에서 다루는 형태)
reviews = df['content'].tolist() # 'content'를 리스트로 변환
ratings = df['score'].tolist() # 'score'를 리스트로 변환
# 라벨을 정수형으로 변환 (필수적인 과정)
label_encoder = LabelEncoder()
ratings = label_encoder.fit_transform(ratings) # 평점 정수형으로 변환 0~4로 바뀜
# 데이터셋 클래스 정의
class ReviewDataset(Dataset):
def __init__(self, reviews, ratings, text_pipeline, label_pipeline):
self.reviews = reviews
self.ratings = ratings
self.text_pipeline = text_pipeline
self.label_pipeline = label_pipeline
def __len__(self):
return len(self.reviews) #데이터셋 크기 반환, 총 몇개의 리뷰와 레이블이 있는지 알려준다. reviews의 길이를 반환하는 이유는 리뷰와 평점의 개수가 같아서
def __getitem__(self, idx):
review = self.text_pipeline(self.reviews[idx]) # 리뷰 인덱스 지정하고 idx에 해당하는 리뷰데이터를 가져오고 text_pipeline을 통해서 리뷰 텍스트를 숫자형 토큰으로 변환
rating = self.label_pipeline(self.ratings[idx]) # 평점 인덱스 지정하고 idx에 해당하는 평점데이터 가져오고 label_pipeline을 통해서 평점을 숫자값으로 변환한다.
return torch.tensor(review), torch.tensor(rating) # 리뷰와 평점을 PyTorch 텐서(tensor)로 변환하여 반환. 텐서로 변환하는 이유: PyTorch 모델이 텐서를 입력으로 받기 때문.
# 토크나이저 정의 (기본 영어 토크나이저)
tokenizer = get_tokenizer('basic_english')
# 어휘 사전 생성 함수
def yield_tokens(data_iter):
for text in data_iter:
yield tokenizer(text)
# 어휘 사전 생성
vocab = build_vocab_from_iterator(yield_tokens(reviews))
# 텍스트 파이프라인 정의 (어휘 사전에 있는 단어만 처리)
def text_pipeline(text):
return [vocab[token] for token in tokenizer(text)]
# 평점 그대로 사용
def label_pipeline(label):
return label # 이미 숫자형이므로 변환 생략
# 데이터를 학습용(train)과 테스트용(test)으로 분리
train_reviews, test_reviews, train_ratings, test_ratings = train_test_split(reviews, ratings, test_size=0.2, random_state=42)
# 데이터셋 정의
train_dataset = ReviewDataset(train_reviews, train_ratings, text_pipeline, label_pipeline)
test_dataset = ReviewDataset(test_reviews, test_ratings, text_pipeline, label_pipeline)
# 패딩을 적용하는 함수 정의
def collate_fn(batch):
reviews, ratings = zip(*batch) # batch는 (review, rating) 쌍으로 이루어진 여러 데이터 포인트가 포함된 리스트고 zip(*batch)을 통해서 각 review와 rating을 분리하여 두 개의 튜플로 반환.
reviews = pad_sequence([torch.tensor(r, dtype=torch.long) for r in reviews], batch_first=True) # 정수형 텐서로 변환
ratings = torch.tensor(ratings, dtype=torch.long) # 평점도 정수형으로 변환
return reviews, ratings
# 데이터 로더 정의
BATCH_SIZE = 64
train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn)
test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, collate_fn=collate_fn)
# LSTM 모델 정의
class LSTMModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim):
super(LSTMModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim) # 단어를 고정된 차원의 벡터로 변환Embedding으로 변경
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True) # LSTM 레이어
self.fc = nn.Linear(hidden_dim, output_dim) # 최종 출력 레이어, hidden_dim -> output_dim(예측할 클래스 수)
def forward(self, text):
embedded = self.embedding(text)
output, (hidden, cell) = self.lstm(embedded)
return self.fc(hidden[-1])
# 하이퍼파라미터 정의
VOCAB_SIZE = len(vocab) # 어휘 사전(단어 집합)의 크기.
EMBED_DIM = 64 # 단어를 임베딩할 벡터의 크기(64차원 벡터로 변환).
HIDDEN_DIM = 128 # LSTM 레이어의 은닉 상태 크기.
OUTPUT_DIM = len(set(ratings)) # 예측할 평점 클래스의 개수(예: 평점이 1~5인 경우 OUTPUT_DIM=5).
# 모델 초기화
model = LSTMModel(VOCAB_SIZE, EMBED_DIM, HIDDEN_DIM, OUTPUT_DIM)
# 손실 함수와 옵티마이저 정의
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01) # SGD 에서 Adam으로 변경 lr : 0.01 - > 0.001 / Accuracy: 63% -> 61.59% 다시
# 모델을 CUDA로 이동 (가능한 경우)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
# 모델 학습 함수 정의
def train_model(model, train_dataloader, criterion, optimizer, num_epochs=10):
model.train() # 학습 모드로 설정
for epoch in range(num_epochs):
total_loss = 0 # 에포크마다 손실을 추적
for i, (reviews, ratings) in enumerate(train_dataloader):
reviews, ratings = reviews.to(device), ratings.to(device) # 데이터를 GPU로 이동
optimizer.zero_grad()
outputs = model(reviews) # 모델에 입력하여 예측값 계산
loss = criterion(outputs, ratings) # 손실 계산
loss.backward() # 역전파
optimizer.step() # 가중치 업데이트
total_loss += loss.item()
# 배치마다 손실 출력
if (i + 1) % 10 == 0:
print(f'Epoch {epoch+1}/{num_epochs}, Batch {i+1}/{len(train_dataloader)}, Loss: {loss.item():.4f}')
print(f'Epoch [{epoch+1}/{num_epochs}], Average Loss: {total_loss/len(train_dataloader):.4f}')
print("Finished Training")
# 모델 학습 실행
train_model(model, train_dataloader, criterion, optimizer, num_epochs=10)
# 모델 평가
correct = 0
total = 0
with torch.no_grad(): # 평가 시 기울기 계산을 하지 않음
for reviews, ratings in test_dataloader:
reviews, ratings = reviews.to(device), ratings.to(device) # 데이터를 GPU로 이동
outputs = model(reviews) # 모델 예측값 계산
_, predicted = torch.max(outputs, 1) # 가장 높은 점수를 가진 클래스 선택
total += ratings.size(0) # 전체 레이블 수
correct += (predicted == ratings).sum().item() # 예측이 맞은 개수 합산
print(f'Accuracy: {100 * correct / total}%') # 정확도 출력
# 결과값
Epoch 1/10, Batch 10/1465, Loss: 1.4572
Epoch 1/10, Batch 20/1465, Loss: 1.4592
Epoch 1/10, Batch 30/1465, Loss: 1.4692
Epoch 1/10, Batch 40/1465, Loss: 1.4214
Epoch 1/10, Batch 50/1465, Loss: 1.4436
Epoch 1/10, Batch 60/1465, Loss: 1.5373
Epoch 1/10, Batch 70/1465, Loss: 1.4278
Epoch 1/10, Batch 80/1465, Loss: 1.3642
Epoch 1/10, Batch 90/1465, Loss: 1.4601
Epoch 1/10, Batch 100/1465, Loss: 1.4164
Epoch 1/10, Batch 110/1465, Loss: 1.3366
.
.
.
Epoch 10/10, Batch 1360/1465, Loss: 0.8361
Epoch 10/10, Batch 1370/1465, Loss: 0.8340
Epoch 10/10, Batch 1380/1465, Loss: 0.8902
Epoch 10/10, Batch 1390/1465, Loss: 0.8727
Epoch 10/10, Batch 1400/1465, Loss: 0.7961
Epoch 10/10, Batch 1410/1465, Loss: 0.8253
Epoch 10/10, Batch 1420/1465, Loss: 1.0503
Epoch 10/10, Batch 1430/1465, Loss: 1.1282
Epoch 10/10, Batch 1440/1465, Loss: 0.8538
Epoch 10/10, Batch 1450/1465, Loss: 0.7876
Epoch 10/10, Batch 1460/1465, Loss: 1.0016
Epoch [10/10], Average Loss: 0.8928
Finished Training
Accuracy: 63.72561574251932%
# 예측 함수
def predict_review(model, review, vocab, tokenizer, device):
# 리뷰를 텐서로 변환
tokens = [vocab[token] for token in tokenizer(review)]
review_tensor = torch.tensor(tokens).unsqueeze(0) # (1, seq_length) 형태로 만듦
# 텐서를 GPU로 이동
review_tensor = review_tensor.to(device)
# 모델에 입력하여 예측값 계산
model.eval() # 평가 모드로 변경
with torch.no_grad(): # 평가 시에는 기울기 계산을 하지 않음
output = model(review_tensor)
_, predicted = torch.max(output, 1)
return predicted.item() + 1 # 예측된 평점 반환 0~4값을 1~5로 바꾸기 위해서 +1
# 새로운 리뷰에 대한 예측
new_review = "This app is great but has some bugs."
predicted_score = predict_review(model, new_review, vocab, tokenizer, device)
print(f'Predicted Score: {predicted_score}')
결과값
Predicted Score: 5
# 새로운 리뷰에 대한 예측2
new_review = "Good app for streaming occasionally, but this is the only app I have that completely malfunctions my user interface, and forces me to restart my phone. No idea why it happens, but it is profoundly annoying."
predicted_score = predict_review(model, new_review, vocab, tokenizer, device)
print(f'Predicted Score: {predicted_score}')
결과값
Predicted Score: 3
정확도 수치가 낮고 로스감소가 제대로 안돼서 튜터님께 여쭤봤는데,
Optimizer로 SGD를 사용하면 로컬 미니멈에 빠지기 쉬운 큰 단점이 있어 학습이 잘 안될 수 있으니, Adam으로 한번 바꿔서 실행 해 보자는 피드백을 받았다.
학습 안정성: Adam은 경사 하강법의 변형 알고리즘으로, 특히 데이터가 복잡하거나 네트워크가 깊을 때 학습을 안정적으로 수행할 수 있습니다. LSTM 모델은 순차적인 데이터를 처리하며, 각 시점에서 기울기 소실 문제나 학습 불안정성이 생길 수 있지만, Adam은 이러한 문제를 완화시킵니다.
빠른 학습 속도: Adam은 빠른 수렴을 돕기 때문에, 여러 에포크 동안 안정적이고 빠르게 학습할 수 있습니다. 특히 LSTM과 같은 구조에서는 많은 파라미터를 학습해야 하므로, 빠른 학습 속도를 지원하는 Adam이 적합합니다.
작은 하이퍼파라미터 튜닝: Adam은 기본 학습률이나 모멘텀 값으로도 적절히 작동합니다. 따라서 특별한 하이퍼파라미터 튜닝 없이도 안정적인 결과를 얻을 수 있어, 학습 설정이 복잡하지 않은 장점이 있습니다.
Adam과 다른 옵티마이저(SGD 등)의 비교:
chatGPT와 열심히 씨름하며 학습 모델 및 전체적인 코드를 한번 짜 봤는데, 쉽지 않았다. 생각지도 못한 라이브러리 환경충돌 문제도 만나고, <사실 이게 젤 컸다.> 여기 저기서 오류가 동시다발적으로 튀어나와 이젠 오류가 나는게 당연시 된거 같다. 오류가 안나서 좋아했는데, 작동을 안하는 상황이 젤 무서웠다. 뭐가 문제인지 알 수 조차 없어서 코드를 다 지우고 새로하기를 10번정도 했다. ㅋㅋㅋ
마지막에 제대로 작동하는 부분에서 희열을 느꼈는데, 에포크마다 로스가 줄어들 기미도 안보이고 어제 포스팅한 게시물처럼 정확도가 30퍼 정도 나오니까 머리에 피가 안통하는 느낌을 받았다. 우여곡절 끝에 유의미한 결과를 도출했을 때는 그만큼 성취감이 있었다. 일기 끝.