| """ | |
| sentiment.py | |
| 모듈화된 감성 분류기 | |
| 함수: | |
| - load_sentiment_model: 저장된 KcELECTRA 모델/토크나이저 로드 | |
| - predict_sentiment_df: 분리된 문장 DataFrame에 감성 추론 후 라벨 및 확률 컬럼 추가 | |
| CLI: | |
| --input : 입력 CSV 경로 (ID, 작성시간, cleaned, divided_comment 포함) | |
| --output : 출력 CSV 경로 | |
| --model-dir : HuggingFace 포맷 모델 디렉토리 (config.json, tf_model.h5, vocab 등) | |
| --text-col : 분류할 텍스트 컬럼 (기본: 'divided_comment') | |
| --output-col : 예측 라벨 컬럼명 (기본: 'sentiment') | |
| --max-length : 토큰 최대 길이 (기본: 64) | |
| --batch-size : 배치 크기 (기본: 16) | |
| --preview : True일 때 샘플 10개 확인 | |
| """ | |
| import os | |
| import time | |
| import argparse | |
| import pandas as pd | |
| import numpy as np | |
| import tensorflow as tf | |
| from transformers import ElectraTokenizer, TFElectraForSequenceClassification | |
| from ace_tools_open import display_dataframe_to_user | |
| def load_sentiment_model(model_dir: str): | |
| """ | |
| 저장된 KcELECTRA 모델/토크나이저 로드 | |
| model_dir 내부에 config.json, tf_model.h5, vocab.txt, tokenizer_config.json, | |
| special_tokens_map.json 등이 있어야 함 | |
| """ | |
| tokenizer = ElectraTokenizer.from_pretrained(model_dir) | |
| model = TFElectraForSequenceClassification.from_pretrained( | |
| model_dir, | |
| num_labels=2 | |
| ) | |
| return tokenizer, model | |
| def predict_sentiment_df( | |
| df: pd.DataFrame, | |
| tokenizer, | |
| model, | |
| text_col: str = 'divided_comment', | |
| output_col: str = 'sentiment', | |
| max_length: int = 64, | |
| batch_size: int = 16 | |
| ) -> pd.DataFrame: | |
| """ | |
| 분리된 문장 DataFrame(df)에 대해 감성 분류 수행 | |
| - output_col: 예측 라벨 컬럼명 | |
| - output_col_prob_0, output_col_prob_1: softmax 확률 컬럼 추가 | |
| """ | |
| texts = df[text_col].fillna('').astype(str).tolist() | |
| enc = tokenizer( | |
| texts, | |
| return_tensors='tf', | |
| padding='max_length', | |
| truncation=True, | |
| max_length=max_length | |
| ) | |
| ds = tf.data.Dataset.from_tensor_slices(dict(enc)).batch(batch_size) | |
| start = time.perf_counter() | |
| preds = model.predict(ds) | |
| elapsed = time.perf_counter() - start | |
| probs = tf.nn.softmax(preds.logits, axis=1).numpy() | |
| labels = np.argmax(probs, axis=1) | |
| df[output_col] = labels | |
| df[f'{output_col}_prob_0'] = probs[:, 0] | |
| df[f'{output_col}_prob_1'] = probs[:, 1] | |
| print(f"[SENTI] {len(texts)} samples → {elapsed:.2f}s total, {elapsed/len(texts):.4f}s per sample") | |
| return df | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser(description='감성 분류 모듈') | |
| parser.add_argument('--input', '-i', required=True, help='입력 CSV 파일 경로') | |
| parser.add_argument('--output', '-o', required=True, help='출력 CSV 파일 경로') | |
| parser.add_argument('--model-dir', '-m', required=True, help='모델 디렉토리 경로') | |
| parser.add_argument('--text-col', default='divided_comment', help='분류할 텍스트 컬럼') | |
| parser.add_argument('--output-col', default='sentiment', help='예측 라벨 컬럼명') | |
| parser.add_argument('--max-length', type=int, default=64, help='토큰 최대 길이') | |
| parser.add_argument('--batch-size', type=int, default=16, help='배치 크기') | |
| parser.add_argument('--preview', action='store_true', help='샘플 확인') | |
| args = parser.parse_args() | |
| df = pd.read_csv(args.input, encoding='utf-8-sig') | |
| tokenizer, model = load_sentiment_model(args.model_dir) | |
| df_out = predict_sentiment_df( | |
| df, | |
| tokenizer, | |
| model, | |
| text_col=args.text_col, | |
| output_col=args.output_col, | |
| max_length=args.max_length, | |
| batch_size=args.batch_size | |
| ) | |
| if args.preview: | |
| display_dataframe_to_user('감성 분류 예시', df_out.head(10)) | |
| os.makedirs(os.path.dirname(args.output), exist_ok=True) | |
| df_out.to_csv(args.output, index=False, encoding='utf-8-sig') | |
| print(f"[SENTI] 저장 완료: {args.output}") | |