Loading the catalog…
Loading the catalog…
1. 프로젝트 개요 이번 실습에서는 네이버 영화 리뷰 데이터(NSMC, Naver Sentiment Movie Corpus)를 활용해 리뷰가 긍정인지 부정인지 분류하는 감성 분석 모델을 구현했다. 감성 분석(Sentiment Analysis)은 문장이나 문서에 담긴 감정·평가를 분류하는 자연어 처리 작업이다. 영화 리뷰에서는 일반적으로 다음과 같이 이진 분류 문제로 설정한다. 레이블 의미 0 부정 리뷰 1 긍정 리뷰 데이터는 리뷰 내용이 담긴 document , 감성 레이블인 label , 데이터 식별자인 id 로 구성된다. 모델 학습에는 document 와 label 을 사용하고, id 는 분류에 직접적인 의미가 없으므로 제외한다. wikidocs 2. 데이터 구성 NSMC 데이터는 총 200,000개의 네이버 영화 리뷰로 구성된다. 데이터 구분 리뷰 수 훈련 데이터 150,000개 테스트 데이터 50,000개 전체 데이터 200,000개 훈련 데이터의 예시는 다음과 같은 구조를 가진다. id document label 9976970 아 더빙.. 진짜 짜증나네요 목소리 0 3819312 흠... 포스터보고 초딩영화줄... 1 10265843 너무재밓었다그래서보는것을추천한다 1 한국어 리뷰 데이터에는 띄어쓰기 오류, 이모티콘, 반복 문자, 숫자, 영어, 특수문자, 신조어 등이 포함될 수 있다. 따라서 모델 학습 전 데이터 정제와 형태소 기반 토큰화가 중요하다. wikidocs 3. 데이터 전처리 중복 데이터 제거 먼저 같은 리뷰 문장이 여러 번 존재할 수 있으므로 document 열을 기준으로 중복 데이터를 제거한다. train_data.drop_duplicates(subset=['document'], inplace=True) 원본 훈련 데이터는 150,000개였지만, 중복 제거 후에는 146,183개가 남는다. document 의 고유값 수는 146,182개이며, 여기에는 결측값 1개가 포함되어 있다. wikidocs 결측값 제거 리뷰 내용이 없는 행은 감성 분석에 사용할 수 없으므로 제거한다. train_data = train_data.dropna(how='any') 결측값을 제거한 뒤 텍스트 전처리를 수행한다. 한글 이외 문자 처리 실습에서는 한글 자음·모음·완성형 한글과 공백을 제외한 문자를 제거한다. train_data['document'] = train_data['document'].str.replace( "[^ᄀ-하-ᅵ가-힣 ]", "", regex=True ) 정규표현식의 의미는 다음과 같다. 표현 의미 ᄀ-ᄒ 한글 자음 ᅡ-ᅵ 한글 모음 가-힣 완성형 한글 음절 공백 띄어쓰기 유지 [^ ... ] 대괄호 안에 포함되지 않는 문자 특수문자 제거 이후에는 영어, 숫자, 이모티콘만으로 작성된 리뷰가 빈 문자열이 될 수 있다. 이런 데이터는 NaN 으로 바꾼 뒤 다시 제거한다. train_data['document'] = train_data['document'].str.replace( '^ +', '', regex=True ) train_data['document'].replace('', np.nan, inplace=True) train_data = train_data.dropna(how='any') 전처리 후 훈련 데이터는 145,393개, 테스트 데이터는 48,852개가 남는다. wikidocs 4. 형태소 토큰화와 불용어 제거 한국어는 조사와 어미가 단어에 결합하는 교착어이므로, 단순히 공백 기준으로 나누는 방식보다 형태소 분석 기반 토큰화가 유용하다. 실습에서는 KoNLPy의 Mecab 형태소 분석기를 사용한다. from konlpy.tag import Mecab mecab = Mecab() sentence = "와 이런 것도 영화라고 차라리 뮤직비디오를 만드는 게 나을 뻔" print(mecab.morphs(sentence)) [ '와', '이런', '것', '도', '영화', '라고', '차라리', '뮤직', '비디오', '를', '만드', '는', '게', '나을', '뻔' ] 이후 감성 분류에 상대적으로 도움이 적을 수 있는 조사, 어미, 접속 표현 등을 불용어로 정의해 제거한다. stopwords = [ '도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '지', '임', '게' ] X_train = [] for sentence in train_data['document']: tokens = mecab.morphs(sentence) tokens = [word for word in tokens if word not in stopwords] X_train.append(tokens) 예를 들어 다음 리뷰는 형태소 토큰화와 불용어 제거 후 여러 토큰으로 분리된다. 원문: 아 더빙.. 진짜 짜증나네요 목소리 토큰화 결과: ['아', '더', '빙', '진짜', '짜증', '나', '네요', '목소리'] 다만 불용어는 고정된 정답 목록이 아니다. 분석 목적, 도메인, 모델 구조에 따라 중요한 단어가 달라질 수 있으므로 실제 프로젝트에서는 데이터 탐색과 성능 평가를 바탕으로 조정해야 한다. wikidocs 5. 학습·검증·테스트 데이터 분리 모델은 훈련 데이터로 학습하고, 검증 데이터로 하이퍼파라미터와 과적합 여부를 점검하며, 마지막으로 테스트 데이터에서 최종 성능을 측정한다. 훈련 데이터의 20%를 검증 데이터로 분리한다. from sklearn.model_selection import train_test_split X_train, X_valid, y_train, y_valid = train_test_split( X_train, y_train, test_size=0.2, random_state=0, stratify=y_train ) 여기서 stratify=y_train 은 분리 이후에도 긍정·부정 레이블의 비율이 최대한 유지되도록 한다. 데이터 부정 비율 긍정 비율 학습 데이터 약 50.238% 약 49.762% 검증 데이터 약 50.239% 약 49.761% 테스트 데이터 약 49.808% 약 50.192% 레이블이 거의 균형을 이루므로 정확도(Accuracy)를 기본 평가 지표로 사용할 수 있다. 다만 실제 서비스에서 한쪽 감성이 압도적으로 많다면 정확도만으로 성능을 판단하지 말고 Precision, Recall, F1-score, ROC-AUC 등을 함께 확인해야 한다. wikidocs 6. 단어 집합과 정수 인코딩 딥러닝 모델은 문자열이 아니라 숫자를 입력으로 받는다. 따라서 토큰화된 단어에 고유한 정수 인덱스를 부여하는 정수 인코딩(Integer Encoding) 을 수행한다. 먼저 학습 데이터에서 단어의 등장 빈도를 계산한다. from collections import Counter word_list = [] for sentence in X_train: for word in sentence: word_list.append(word) word_counts = Counter(word_list) 학습 데이터에는 총 45,296개의 서로 다른 단어가 등장한다. 이 중 등장 횟수가 2회 이하인 희귀 단어는 26,105개로, 전체 단어 종류의 약 57.63%를 차지한다. 하지만 전체 등장 빈도에서 차지하는 비중은 약 2.28%에 불과하다. wikidocs 따라서 등장 빈도 3회 미만의 희귀 단어를 단어 사전에서 제외한다. threshold = 3 vocab_size = total_cnt - rare_cnt vocab = vocab[:vocab_size] 희귀 단어 제거 후 실제 단어 사전 크기는 19,191개가 된다. 특수 토큰 시퀀스 길이를 맞추기 위한 패딩과 단어 사전에 없는 단어 처리를 위해 특수 토큰을 추가한다. word_to_index = {} word_to_index['<PAD>'] = 0 word_to_index['<UNK>'] = 1 토큰 인덱스 역할 <PAD> 0 길이가 짧은 문장을 채우는 패딩 토큰 <UNK> 1 단어 사전에 없는 미지의 단어 최종 단어 사전 크기는 19,193개가 된다. for index, word in enumerate(vocab): word_to_index[word] = index + 2 토큰을 정수 시퀀스로 바꾸는 함수는 다음과 같다. def texts_to_sequences(tokenized_X_data, word_to_index): encoded_X_data = [] for sent in tokenized_X_data: index_sequences = [] for word in sent: index_sequences.append( word_to_index.get(word, word_to_index['<UNK>']) ) encoded_X_data.append(index_sequences) return encoded_X_data 예를 들어 토큰 시퀀스가 다음과 같다고 가정해보자. ['이야', '어쩜', '이렇게', '나', '지루', '할', '수'] 정수 인코딩 후에는 다음처럼 변환된다. [924, 1866, 128, 7, 80, 48, 34] 7. 패딩 리뷰마다 길이가 다르기 때문에, 배치 단위 연산을 하려면 모든 입력 시퀀스의 길이를 동일하게 맞춰야 한다. 이 과정을 패딩(Padding) 이라고 한다. 훈련 데이터에서 리뷰의 최대 길이는 74, 평균 길이는 약 12.3이다. 실습에서는 max_len = 30 으로 설정하며, 이 길이는 훈련 샘플의 약 92.5%를 포함한다. wikidocs max_len = 30 def pad_sequences(sentences, max_len): features = np.zeros((len(sentences), max_len), dtype=int) for index, sentence in enumerate(sentences): features[index, :len(sentence)] = np.array(sentence)[:max_len] return features 패딩 후 첫 번째 리뷰는 다음과 같은 형태가 된다. [ 924, 1866, 128, 7, 80, 48, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 문장 뒤쪽에 붙은 0 은 <PAD> 토큰이다. 데이터 패딩 후 크기 훈련 데이터 (116,314, 30) 검증 데이터 (29,079, 30) 테스트 데이터 (48,852, 30) 각 샘플은 길이 30의 정수 시퀀스로 변환된다. wikidocs 8. LSTM 감성 분류 모델 모델은 다음 순서로 구성된다. 정수 인코딩된 리뷰 → Embedding → LSTM → 마지막 은닉 상태 → Fully Connected Layer → 긍정/부정 분류 PyTorch 구현은 다음과 같다. import torch import torch.nn as nn class TextClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim): super(TextClassifier, self).__init__() self.embedding = nn.Embedding(vocab_size, embedding_dim) self.lstm = nn.LSTM( input_size=embedding_dim, hidden_size=hidden_dim, batch_first=True ) self.fc = nn.Linear(hidden_dim, output_dim) def forward(self, x): # x: (batch_size, sequence_length) embedded = self.embedding(x) # embedded: (batch_size, sequence_length, embedding_dim) lstm_out, (hidden, cell) = self.lstm(embedded) # hidden: (num_layers, batch_size, hidden_dim) last_hidden = hidden.squeeze(0) # logits: (batch_size, output_dim) logits = self.fc(last_hidden) return logits 입력과 출력의 텐서 형태는 다음과 같이 변한다. 단계 텐서 형태 설명 입력 (batch_size, sequence_length) 정수 인코딩된 단어 인덱스 임베딩 (batch_size, sequence_length, embedding_dim) 각 단어가 벡터로 변환됨 LSTM 마지막 은닉 상태 (batch_size, hidden_dim) 문장 전체를 요약한 벡터 출력층 (batch_size, 2) 부정·긍정 클래스별 점수 예를 들어 배치 크기 32, 문장 길이 30, 임베딩 차원 100, 은닉 상태 차원 128이라면 텐서 흐름은 다음과 같다. (32, 30) → (32, 30, 100) → (32, 128) → (32, 2) 9. 학습 설정 실습에서 사용한 주요 하이퍼파라미터는 다음과 같다. 항목 값 단어 사전 크기 19,193 최대 문장 길이 30 임베딩 차원 100 LSTM 은닉 상태 차원 128 출력 클래스 수 2 배치 크기 32 옵티마이저 Adam 학습률 0.001 손실 함수 Cross Entropy Loss 에포크 5 embedding_dim = 100 hidden_dim = 128 output_dim = 2 model = TextClassifier( vocab_size=vocab_size, embedding_dim=embedding_dim, hidden_dim=hidden_dim, output_dim=output_dim ) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam( model.parameters(), lr=0.001 ) 이진 분류 문제이지만 출력 노드를 2개로 두고 nn.CrossEntropyLoss() 를 사용한다. 모델은 각 클래스의 점수(logits)를 출력하며, 가장 높은 점수를 가진 클래스를 최종 예측으로 선택한다. wikidocs 10. 학습과 검증 학습 과정에서는 다음 순서가 반복된다. 배치 데이터를 모델에 입력한다. 예측 결과인 logits를 계산한다. 실제 레이블과 예측값의 손실을 계산한다. loss.backward() 로 역전파를 수행한다. optimizer.step() 으로 모델 파라미터를 업데이트한다. 각 에포크가 끝날 때 검증 데이터 성능을 확인한다. 검증 손실이 가장 낮은 모델을 저장한다. best_val_loss = float('inf') for epoch in range(num_epochs): model.train() for batch_X, batch_y in train_dataloader: batch_X = batch_X.to(device) batch_y = batch_y.to(device) logits = model(batch_X) loss = criterion(logits, batch_y) optimizer.zero_grad() loss.backward() optimizer.step() val_loss, val_accuracy = evaluate( model, valid_dataloader, criterion, device ) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model_checkpoint.pth') 모델 평가 시에는 model.eval() 과 torch.no_grad() 를 사용한다. model.eval() with torch.no_grad(): logits = model(batch_X) 코드 역할 model.train() 학습 모드 활성화 model.eval() 평가 모드 활성화 torch.no_grad() 평가 중 기울기 계산 비활성화 optimizer.zero_grad() 이전 배치의 기울기 초기화 loss.backward() 손실 기준 역전파 수행 optimizer.step() 모델 파라미터 업데이트 특히 model.eval() 은 드롭아웃, 배치 정규화처럼 학습·평가 동작이 다른 레이어가 포함된 모델에서 중요하다. torch.no_grad() 는 평가 시 기울기 계산을 생략해 메모리 사용량과 연산 시간을 줄여준다. wikidocs 11. 모델 성능 검증 손실이 가장 낮았던 모델을 불러와 최종 성능을 평가한다. 평가 데이터 Loss Accuracy 검증 데이터 0.3392 84.90% 테스트 데이터 0.3435 84.92% 훈련에 참여하지 않은 테스트 데이터에서도 약 84.92%의 정확도를 보였다. 검증 성능과 테스트 성능 차이가 크지 않기 때문에, 예시 결과에서는 극단적인 과적합 징후가 크지 않다고 해석할 수 있다. wikidocs 12. 새로운 리뷰 예측 학습한 모델은 새로운 영화 리뷰에 대해 긍정 또는 부정을 예측할 수 있다. index_to_tag = { 0: '부정', 1: '긍정' } def predict(text, model, word_to_index, index_to_tag): model.eval() tokens = mecab.morphs(text) tokens = [word for word in tokens if word not in stopwords] token_indices = [ word_to_index.get(token, word_to_index['<UNK>']) for token in tokens ] input_tensor = torch.tensor( [token_indices], dtype=torch.long ).to(device) with torch.no_grad(): logits = model(input_tensor) predicted_index = torch.argmax(logits, dim=1) return index_to_tag[predicted_index.item()] 예측 결과는 다음과 같다. 입력 리뷰 예측 결과 이 영화 개꿀잼 ᄏᄏᄏ 긍정 이딴게 영화냐 ᄍᄍ 부정 감독 뭐하는 놈이냐? 부정 와 개쩐다 정말 세계관 최강자들의 영화다 긍정 모델은 토큰화, 불용어 제거, 정수 인코딩, 임베딩, LSTM 연산을 거쳐 입력 문장에 포함된 감성 정보를 바탕으로 최종 클래스를 예측한다. 다만 신조어, 비꼬는 표현, 복합 감정, 문맥 의존적 표현에서는 오분류가 발생할 수 있다. wikidocs 13. 정리 NSMC는 긍정·부정 레이블을 가진 총 20만 건의 네이버 영화 리뷰 데이터셋이다. 중복값과 결측값을 제거하고, 정규표현식으로 텍스트를 정제했다. 한국어는 교착어이므로 Mecab 형태소 분석기를 이용해 토큰화했다. 희귀 단어를 제거하고 <PAD> , <UNK> 특수 토큰을 추가해 단어 사전을 구성했다. 토큰을 정수로 인코딩하고, 모든 리뷰 길이를 30으로 맞추기 위해 패딩을 적용했다. Embedding → LSTM → Fully Connected Layer 구조의 감성 분류 모델을 구현했다. 예시 모델은 검증 정확도 84.90%, 테스트 정확도 84.92%를 기록했다. 실제 성능 개선을 위해서는 양방향 LSTM, GRU, 사전학습 임베딩, Transformer 기반 언어 모델, 하이퍼파라미터 튜닝 등을 추가로 실험할 수 있다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
4.5 LSTM으로 네이버 영화 리뷰 분류하기. 1. 프로젝트 개요 이번 실습에서는 네이버 영화 리뷰 데이터(NSMC, Naver Sentiment Movie Corpus)를 활용해 리뷰가 긍정인지 부정인지 분류하는 감성 분석 모델을 구현했다. 감성 분석(Sentiment Analysis)은 문장이나 문서에 담긴 감정·평가를 분류하는 자연어 처리 작업이다. 영화 리뷰에서는 일반적으로 다음과 같이 이진 분류 문제로 설정한다. 레이블 의미 0 부정 리뷰 1 긍정 리뷰 데이터는 리뷰 내용이 담긴 document , 감성 레이블인 label , 데이터 식별자인 id 로 구성된다. 모델 학습에는 document 와 label 을 사용하고, id 는 분류에 직접적인 의미가 없으므로 제외한다. wikidocs 2.…
Open source