콘텐츠로 이동
Data Prep
상세

텍스트 마이닝 분석

비정형 텍스트 데이터에서 구조화된 인사이트를 추출하는 방법론 가이드. 전처리부터 토픽 모델링, 감성 분석, NER까지 핵심 기법과 LLM 시대의 새로운 접근법을 다룬다.


개요

텍스트 마이닝은 대량의 비정형 텍스트에서 패턴, 주제, 감성, 관계를 자동으로 추출하는 분석 방법이다. 기업 데이터의 80% 이상이 비정형이며, 이 중 상당 부분이 텍스트다.

비즈니스 가치

활용 분야 데이터 소스 분석 기법 비즈니스 가치
VoC 분석 고객 문의, 리뷰 감성 분석, 토픽 모델링 제품 개선, CS 효율화
소셜 리스닝 SNS, 뉴스, 커뮤니티 트렌드 분석, 감성 추적 브랜드 모니터링, 위기 감지
내부 문서 분석 보고서, 이메일, 회의록 분류, 요약, 검색 지식 관리, 의사결정 지원
특허 분석 특허 문서 키워드 추출, 클러스터링 R&D 전략, 경쟁 분석
규제/법률 법률 문서, 판례 NER, 관계 추출 컴플라이언스, 리스크 관리

전처리 파이프라인

텍스트 전처리 흐름

원본 텍스트
[정규화] ──── 소문자 변환, 유니코드 정규화, 특수문자 처리
[토큰화] ──── 문장 분리, 단어 분리
[형태소 분석] ─ 품사 태깅 (한국어: KoNLPy)
[불용어 제거] ─ 조사, 어미, 일반 불용어
[정규화] ──── 어간 추출 / 표제어 추출
전처리된 토큰 리스트

한국어 전처리 (KoNLPy)

from konlpy.tag import Mecab
import re

class KoreanTextPreprocessor:
    """한국어 텍스트 전처리"""

    def __init__(self):
        self.mecab = Mecab()
        self.stopwords = self._load_stopwords()

    def preprocess(self, text: str, 
                   pos_filter: list = None) -> list[str]:
        """전처리 파이프라인

        Args:
            text: 원본 텍스트
            pos_filter: 추출할 품사 태그 (기본: 명사, 동사, 형용사)
        """
        if pos_filter is None:
            pos_filter = ['NNG', 'NNP', 'VV', 'VA']  # 일반명사, 고유명사, 동사, 형용사

        # 1. 정규화
        text = self._normalize(text)

        # 2. 형태소 분석 + 품사 필터
        morphs = self.mecab.pos(text)
        tokens = [word for word, pos in morphs 
                  if pos in pos_filter and len(word) > 1]

        # 3. 불용어 제거
        tokens = [t for t in tokens if t not in self.stopwords]

        return tokens

    def _normalize(self, text: str) -> str:
        """텍스트 정규화"""
        text = re.sub(r'[^\w\s가-힣]', ' ', text)
        text = re.sub(r'\s+', ' ', text).strip()
        return text

    def _load_stopwords(self) -> set:
        """한국어 불용어 사전"""
        return {
            '것', '수', '등', '및', '이', '그', '저', 
            '때', '더', '중', '위', '약', '또', '만',
            '년', '월', '일', '번', '개', '대', '차',
        }

영어 전처리 비교

단계 영어 한국어
토큰화 공백 + 구두점 기반 형태소 분석기 필수
소문자화 필수 해당 없음
어간 추출 Porter/Snowball Stemmer 형태소 분석기가 처리
불용어 NLTK 기본 제공 도메인별 직접 구축 필요
복합어 드뭄 매우 빈번 (복합명사)
도구 NLTK, spaCy KoNLPy (Mecab, Okt, Komoran)

분석 기법

1. TF-IDF + 키워드 추출

문서에서 중요한 단어를 수치적으로 추출한다.

TF-IDF(t, d) = TF(t, d) x IDF(t)

TF(t, d) = (단어 t가 문서 d에 출현한 횟수) / (문서 d의 총 단어 수)
IDF(t)   = log(전체 문서 수 / 단어 t가 출현한 문서 수)
from sklearn.feature_extraction.text import TfidfVectorizer

def extract_keywords(documents: list[str], top_n: int = 10) -> dict:
    """TF-IDF 기반 키워드 추출"""
    vectorizer = TfidfVectorizer(
        max_features=5000,
        max_df=0.85,
        min_df=2,
    )
    tfidf_matrix = vectorizer.fit_transform(documents)
    feature_names = vectorizer.get_feature_names_out()

    # 전체 코퍼스 기준 상위 키워드
    avg_tfidf = tfidf_matrix.mean(axis=0).A1
    top_indices = avg_tfidf.argsort()[-top_n:][::-1]

    return {feature_names[i]: avg_tfidf[i] for i in top_indices}

2. 토픽 모델링

LDA (Latent Dirichlet Allocation)

전통적인 확률 기반 토픽 모델. 각 문서를 토픽의 혼합으로, 각 토픽을 단어의 분포로 표현한다.

from gensim.models import LdaMulticore
from gensim.corpora import Dictionary

def run_lda(tokenized_docs: list[list[str]], 
            num_topics: int = 10) -> dict:
    """LDA 토픽 모델링"""
    dictionary = Dictionary(tokenized_docs)
    dictionary.filter_extremes(no_below=5, no_above=0.5)
    corpus = [dictionary.doc2bow(doc) for doc in tokenized_docs]

    model = LdaMulticore(
        corpus=corpus,
        id2word=dictionary,
        num_topics=num_topics,
        passes=10,
        workers=4,
        random_state=42,
    )

    topics = []
    for idx, topic in model.print_topics(-1, num_words=10):
        topics.append({
            "topic_id": idx,
            "words": topic,
        })

    # Coherence score
    from gensim.models import CoherenceModel
    coherence = CoherenceModel(
        model=model, texts=tokenized_docs, 
        dictionary=dictionary, coherence='c_v'
    )

    return {
        "model": model,
        "topics": topics,
        "coherence": coherence.get_coherence(),
    }

BERTopic

BERT 임베딩 + UMAP + HDBSCAN을 결합한 신경망 기반 토픽 모델. LDA 대비 의미적으로 더 일관된 토픽을 추출한다.

from bertopic import BERTopic
from sentence_transformers import SentenceTransformer

def run_bertopic(documents: list[str], 
                 language: str = "korean") -> BERTopic:
    """BERTopic 토픽 모델링"""
    # 한국어 임베딩 모델
    embedding_model = SentenceTransformer(
        "jhgan/ko-sroberta-multitask"
    )

    topic_model = BERTopic(
        embedding_model=embedding_model,
        language="multilingual",
        calculate_probabilities=True,
        verbose=True,
        min_topic_size=10,
        nr_topics="auto",
    )

    topics, probs = topic_model.fit_transform(documents)

    # 결과 확인
    print(topic_model.get_topic_info())

    # 토픽별 대표 문서
    for topic_id in range(min(5, len(topic_model.get_topics()))):
        print(f"\nTopic {topic_id}:")
        print(topic_model.get_topic(topic_id))

    return topic_model

LDA vs BERTopic 비교:

항목 LDA BERTopic
원리 확률적 생성 모델 임베딩 + 클러스터링
입력 BoW (전처리 필수) 원본 텍스트 (전처리 선택)
토픽 수 사전 지정 필수 자동 결정 가능
의미 일관성 보통 높음
속도 빠름 느림 (임베딩 계산)
해석성 높음 (단어 분포) 높음 (대표 단어 + 문서)
동적 토픽 제한적 시간별 토픽 변화 지원

3. 감성 분석

접근법 비교

접근법 방법 장점 단점
규칙 기반 감성 사전 + 규칙 해석 가능, 도메인 적용 쉬움 정확도 제한, 맥락 무시
ML 기반 BERT fine-tuning 높은 정확도, 맥락 이해 학습 데이터 필요
LLM 기반 프롬프트로 분류 학습 데이터 불필요, 유연 비용, 속도

LLM 기반 감성 분석

from openai import OpenAI

client = OpenAI()

def analyze_sentiment_llm(texts: list[str], 
                          batch_size: int = 10) -> list[dict]:
    """LLM 기반 배치 감성 분석"""
    results = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        numbered = "\n".join(f"{j+1}. {t}" for j, t in enumerate(batch))

        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{
                "role": "system",
                "content": """텍스트의 감성을 분석하세요.
각 텍스트에 대해 JSON 형식으로 응답:
{"id": 번호, "sentiment": "positive|negative|neutral", 
 "confidence": 0.0-1.0, "aspect": "주요 측면"}"""
            }, {
                "role": "user",
                "content": numbered,
            }],
            temperature=0,
            response_format={"type": "json_object"},
        )

        batch_results = json.loads(response.choices[0].message.content)
        results.extend(batch_results["results"])

    return results

4. 명명 개체 인식 (NER)

텍스트에서 인명, 지명, 기관명, 날짜 등 개체를 식별한다.

개체 유형 태그 예시
인명 PER 홍길동, Elon Musk
기관 ORG 삼성전자, OpenAI
지명 LOC 서울, Silicon Valley
날짜 DATE 2026년 3월, 지난 주
금액 MONEY 1,000만원, $50B
제품 PRODUCT iPhone 16, Galaxy S26
# Few-shot NER with LLM
def extract_entities_llm(text: str, 
                         entity_types: list[str]) -> list[dict]:
    """LLM 기반 NER (few-shot)"""
    examples = """
예시:
입력: "삼성전자가 2026년 1월 서울에서 AI 칩을 공개했다."
출력: [
  {"text": "삼성전자", "type": "ORG"},
  {"text": "2026년 1월", "type": "DATE"},
  {"text": "서울", "type": "LOC"},
  {"text": "AI 칩", "type": "PRODUCT"}
]
"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "system",
            "content": f"""텍스트에서 다음 유형의 개체를 추출하세요: {entity_types}
{examples}
JSON 리스트로 응답하세요."""
        }, {
            "role": "user", 
            "content": text,
        }],
        temperature=0,
    )

    return json.loads(response.choices[0].message.content)

5. 문서 유사도 / 클러스터링

from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
import numpy as np

def cluster_documents(documents: list[str], 
                      n_clusters: int = None) -> dict:
    """문서 임베딩 + 클러스터링"""
    model = SentenceTransformer("jhgan/ko-sroberta-multitask")
    embeddings = model.encode(documents, show_progress_bar=True)

    # 최적 클러스터 수 탐색 (미지정 시)
    if n_clusters is None:
        scores = {}
        for k in range(2, min(20, len(documents) // 5)):
            km = KMeans(n_clusters=k, random_state=42, n_init=10)
            labels = km.fit_predict(embeddings)
            scores[k] = silhouette_score(embeddings, labels)
        n_clusters = max(scores, key=scores.get)

    # 클러스터링
    kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
    labels = kmeans.fit_predict(embeddings)

    # 클러스터별 대표 문서 (centroid에 가장 가까운 문서)
    representatives = {}
    for cluster_id in range(n_clusters):
        mask = labels == cluster_id
        cluster_embeddings = embeddings[mask]
        centroid = kmeans.cluster_centers_[cluster_id]
        distances = np.linalg.norm(cluster_embeddings - centroid, axis=1)
        rep_idx = np.where(mask)[0][distances.argmin()]
        representatives[cluster_id] = documents[rep_idx]

    return {
        "labels": labels,
        "n_clusters": n_clusters,
        "representatives": representatives,
        "silhouette": silhouette_score(embeddings, labels),
    }

6. 텍스트 요약

방식 설명 장점 단점
Extractive 원문에서 핵심 문장 선택 사실 정확, 빠름 자연스러움 떨어짐
Abstractive 새로운 문장으로 재작성 자연스러움, 간결 Hallucination 위험
LLM 기반 프롬프트로 요약 고품질, 유연 비용, 길이 제한

비즈니스 적용

VoC (Voice of Customer) 분석 파이프라인

고객 데이터 수집
├── 고객센터 문의 (전화 STT, 채팅)
├── 온라인 리뷰 (앱스토어, 쇼핑몰)
├── 설문 자유 응답
└── SNS 멘션
[전처리] → [분류: 카테고리] → [감성 분석]
                              ┌─────┴─────┐
                              ▼           ▼
                        [긍정 분석]  [부정 분석]
                        - 강점 파악   - 불만 토픽
                        - 충성 요인   - 이탈 위험
                              │           │
                              └─────┬─────┘
                            [인사이트 리포트]
                            - 주간 트렌드
                            - 우선순위 액션
                            - 경쟁사 비교

소셜 리스닝

분석 항목 방법 시각화
브랜드 언급량 키워드 카운팅 시계열 그래프
감성 트렌드 감성 분석 + 시계열 감성 비율 추이
이슈 탐지 이상 탐지 (언급량 급증) 알림
경쟁 비교 교차 분석 레이더 차트
인플루언서 네트워크 분석 소셜 그래프

LLM 시대의 텍스트 분석

LLM의 등장으로 텍스트 분석의 패러다임이 변화하고 있다.

기존 vs LLM 기반 비교

작업 기존 방식 LLM 방식 비교
분류 BERT fine-tuning (학습 데이터 필요) Zero/few-shot 프롬프트 LLM: 빠른 시작, ML: 높은 처리량
NER 시퀀스 태깅 모델 학습 Few-shot 프롬프트 LLM: 유연, ML: 대량 처리
감성 분석 감성 사전 / 분류 모델 프롬프트 분류 LLM: 뉘앙스 이해
요약 Extractive + T5 프롬프트 요약 LLM: 압도적
토픽 추출 LDA / BERTopic 프롬프트 기반 혼합 사용 권장

권장 전략

데이터 규모가 작고 (< 1000건) + 카테고리 불명확
    → LLM 기반 탐색적 분석

데이터 규모가 크고 (> 10,000건) + 반복 처리
    → 기존 ML/DL 파이프라인

탐색 후 확정 + 대규모 적용
    → LLM으로 레이블링 → ML 모델 학습 → 배치 처리

시각화

주요 시각화 유형

시각화 용도 라이브러리
워드클라우드 핵심 키워드 한눈에 wordcloud
토픽 맵 토픽 간 관계, 크기 pyLDAvis, BERTopic
감성 트렌드 시간별 감성 변화 matplotlib, plotly
네트워크 그래프 단어/개체 관계 networkx, pyvis
히트맵 문서-토픽 분포 seaborn
from wordcloud import WordCloud
import matplotlib.pyplot as plt

def create_korean_wordcloud(word_freq: dict, 
                            font_path: str = "/usr/share/fonts/truetype/nanum/NanumGothic.ttf"):
    """한국어 워드클라우드 생성"""
    wc = WordCloud(
        font_path=font_path,
        width=800,
        height=400,
        background_color="white",
        max_words=100,
        colormap="viridis",
    )
    wc.generate_from_frequencies(word_freq)

    fig, ax = plt.subplots(figsize=(16, 8))
    ax.imshow(wc, interpolation="bilinear")
    ax.axis("off")
    plt.tight_layout()
    return fig

참고 자료

자료 링크
KoNLPy 문서 https://konlpy.org/
BERTopic 문서 https://maartengr.github.io/BERTopic/
Sentence Transformers (한국어) https://huggingface.co/jhgan/ko-sroberta-multitask
RAGAS (RAG 평가) https://docs.ragas.io/

최종 업데이트: 2026-03-25