본문 바로가기
Project/🚀 AI Finance 2026

#9 [모듈1-3] 뉴스 센티먼트 분석 & 일간 리포트 시스템

by 로띠 2026. 2. 19.
NLP

HuggingFace transformers + KR-FinBert-SC

한국어 금융 뉴스 감성분석 파이프라인

News Collect NLP Sentiment Feature Eng. ML Training

이 글은 Alchemetric 프로젝트에서 한국어 금융 뉴스의 감성(긍정/부정/중립)을 자동 분석하는 데 사용하는 HuggingFace transformers 라이브러리와 KR-FinBert-SC 모델을 NLP를 처음 접하는 분도 이해할 수 있도록 설명합니다. 이론보다는 “이 라이브러리가 뭐고, 실제 코드에서 어떻게 돌아가는가”에 초점을 맞춥니다.

1. 주가 예측에 뉴스가 왜 중요한가?

뉴스 = 시장 심리의 바로미터

주식 시장에서 가격이 움직이는 이유는 크게 두 가지입니다. 하나는 과거 가격 패턴(기술적 지표), 다른 하나는 미래에 대한 기대(뉴스, 공시, 시장 심리)입니다.

RSI, MACD, 볼린저밴드 같은 기술적 지표는 "과거 가격"만 봅니다. 어제까지의 주가 흐름을 분석해서 패턴을 찾죠. 하지만 주가는 과거만 보고 움직이지 않습니다. "삼성전자가 역대 최대 실적을 달성했다"는 뉴스가 나오면, 아무리 기술적으로 과매수 구간이라도 주가가 뛸 수 있습니다. 뉴스는 이렇게 “미래 기대”를 반영하는 중요한 신호입니다.

문제: 사람이 다 읽을 수 없다

KOSPI 시장에만 종목이 수백 개이고, 하루에 종목별로 수십 건의 뉴스가 쏟아집니다. 사람이 이 모든 뉴스를 읽고 “이 뉴스가 긍정적인가, 부정적인가”를 판단하는 것은 물리적으로 불가능합니다. 그래서 NLP(자연어 처리) 모델을 사용해 이 과정을 자동화합니다.

예시:
“삼성전자 역대 최대 실적 달성” → 긍정 (+0.95)
“삼성전자 반도체 적자 전환” → 부정 (-0.88)

NLP 모델은 이런 판단을 초 단위로, 수백 건씩 해냅니다.

2. HuggingFace transformers란?

“AI 모델계의 앱스토어”

HuggingFace는 AI/NLP 분야에서 가장 유명한 오픈소스 플랫폼입니다. 이 플랫폼에서 제공하는 transformers 라이브러리를 사용하면, 전 세계 연구자들이 만들어놓은 수만 개의 사전학습된 AI 모델pip install 한 번으로 다운받아 바로 사용할 수 있습니다.

스마트폰 앱스토어에서 앱을 검색해서 다운받아 쓰는 것처럼, HuggingFace에서 원하는 AI 모델을 검색해서 다운받아 쓸 수 있습니다. 텍스트 분류, 번역, 요약, 질의응답 등 다양한 작업에 특화된 모델들이 올라와 있죠.

pipeline() API — 3줄이면 NLP 모델 사용 가능

transformers 라이브러리의 가장 강력한 장점은 pipeline() API입니다. 복잡한 모델 아키텍처, 토크나이저, 전처리를 전혀 몰라도 단 3줄로 NLP 모델을 사용할 수 있습니다.

from transformers import pipeline

# 1. 모델 로드 (한국어 금융 감성 분류 모델)
analyzer = pipeline("sentiment-analysis", model="snunlp/KR-FinBert-SC")

# 2. 분석 실행
result = analyzer("삼성전자 역대 최대 실적 달성")

# 3. 결과 확인
print(result)
# [{"label": "positive", "score": 0.95}]

이 3줄 뒤에서 일어나는 일은 실제로 꽤 복잡합니다. 텍스트를 토큰(단어 조각)으로 자르고, 숫자 벡터로 변환하고, 12개의 Transformer 레이어를 통과시키고, 최종적으로 positive/negative/neutral 확률을 계산합니다. 하지만 pipeline()이 이 모든 과정을 추상화해주기 때문에, 우리는 “텍스트 넣고 결과 받기”만 하면 됩니다.

왜 직접 학습 안 하고 “가져다 쓰나”? — Transfer Learning의 힘

처음부터 AI 모델을 만들려면 수억 개의 텍스트 데이터수백 시간의 GPU 학습이 필요합니다. 이건 Google, Meta 같은 빅테크가 하는 일이지, 개인 프로젝트에서 할 수 있는 일이 아닙니다.

대신 우리는 Transfer Learning(전이학습)을 사용합니다. 이미 대량의 텍스트로 “언어를 이해”하도록 학습된 모델(BERT)을 가져와서, 금융 분야에 특화된 버전(KR-FinBert-SC)을 바로 활용하는 것이죠.

비유: 한국어를 이미 유창하게 하는 사람이 있다고 생각해보세요. 이 사람에게 “금융 뉴스 읽고 긍정/부정 판단하는 법”만 추가로 가르치면 됩니다. 한국어 자체를 처음부터 가르칠 필요가 없죠. BERT가 “한국어를 이미 아는 사람”이고, KR-FinBert-SC가 “금융 뉴스 읽는 법을 배운 상태”입니다.

3. KR-FinBert-SC — 한국어 금융 특화 모델

BERT → FinBERT → KR-FinBert-SC 진화 과정

Alchemetric에서 사용하는 snunlp/KR-FinBert-SC 모델은 하루아침에 만들어진 것이 아닙니다. 세 단계의 진화를 거쳐 탄생했습니다.

단계 모델 제작 특징
1단계 BERT Google (2018) 범용 언어 이해 모델. 영어 위키피디아 등으로 학습
2단계 FinBERT 연구자 커뮤니티 BERT를 금융 텍스트(영어 재무보고서 등)로 추가 학습
3단계 KR-FinBert-SC 서울대 NLP Lab 한국어 금융 뉴스 센티먼트 분류에 특화

KR-FinBert-SC의 SCSentiment Classification(감성 분류)의 약자입니다. 이 모델은 한국어 금융 뉴스 텍스트를 입력받아 3개 클래스 중 하나로 분류합니다.

분류 의미 예시
positive 주가에 긍정적 영향 “삼성전자 영업이익 사상 최대”
negative 주가에 부정적 영향 “반도체 업황 악화로 실적 급감”
neutral 주가에 영향 없음 “삼성전자 정기 주주총회 개최”

왜 일반 감성 분석이 아닌 금융 특화 모델인가?

금융 뉴스에는 일상 언어와 정반대의 감성을 가지는 표현들이 많습니다. 일반 감성 분석 모델(영화 리뷰 분석용 등)로는 이런 뉘앙스를 잡을 수 없습니다.

뉴스 제목 일반 모델 판단 금융 모델 판단 이유
“금리 인상 결정” 중립 부정 금리 상승 = 주가 하락 요인
“적자 폭 축소” 부정 (“적자”라는 단어) 긍정 적자가 줄어듦 = 실적 개선 시그널
“대규모 유상증자 결정” 중립 부정 유상증자 = 주식 가치 희석

입력 제한: 512 토큰

BERT 계열 모델은 한 번에 처리할 수 있는 텍스트 길이에 제한이 있습니다. KR-FinBert-SC의 최대 입력은 512 토큰입니다. 토큰은 단어보다 작은 단위(단어 조각)로, 한국어 기준으로 대략 200~300단어 정도에 해당합니다.

뉴스 기사가 이보다 길면 뒷부분이 잘립니다. 그래서 프로젝트에서는 truncation=True를 설정해 자동으로 잘리도록 처리하고 있습니다.

# sentiment_analyzer.py — 모델 로드 시 truncation 설정
self._pipeline = hf_pipeline(
    "sentiment-analysis",
    model=self.model_name,        # "snunlp/KR-FinBert-SC"
    tokenizer=self.model_name,
    max_length=512,               # 최대 512 토큰
    truncation=True,              # 초과 시 자동 잘림
)

4. 센티먼트 점수 변환

모델 출력 → 프로젝트 점수

KR-FinBert-SC 모델의 원시 출력은 {"label": "positive", "score": 0.87} 형태입니다. label은 분류 결과이고, score는 모델이 얼마나 확신하는지(0~1)를 나타냅니다.

Alchemetric에서는 이것을 -1.0 ~ +1.0 범위의 단일 점수로 변환합니다.

# sentiment_analyzer.py — 라벨별 기본 방향
_LABEL_SCORE = {
    "positive":  1.0,   # 긍정 → +방향
    "negative": -1.0,   # 부정 → -방향
    "neutral":   0.0,   # 중립 → 0
}

# 최종 점수 = 방향 x 확신도
sentiment_score = _LABEL_SCORE[label] * confidence

변환 예시를 표로 보겠습니다.

모델 출력 계산 최종 점수
positive, score=0.87 (+1.0) × 0.87 +0.87
negative, score=0.92 (-1.0) × 0.92 -0.92
neutral, score=0.75 (0.0) × 0.75 0.0

neutral은 확신도에 관계없이 항상 0.0이 됩니다. 중립 뉴스는 주가에 영향을 주지 않는다는 의미입니다.

이 점수가 ML 피처로 들어간다

개별 뉴스의 센티먼트 점수는 피처 엔지니어링(Phase 4) 단계에서 일별로 집계되어 ML 학습에 사용됩니다.

피처명 설명 의미
news_sentiment 일별 평균 센티먼트 점수 그날 뉴스가 전반적으로 긍정인지 부정인지
news_volume 일별 뉴스 건수 뉴스가 많다 = 관심도 높다
news_sentiment_std 센티먼트 표준편차 값이 크면 의견 분분, 작으면 의견 일치
market_sentiment 시장 전체 뉴스 평균 점수 시장 전체 분위기 (강세장 vs 약세장)
market_news_volume 시장 전체 뉴스 건수 시장 전체 관심도
예시: 삼성전자에 대해 어느 날 뉴스 10건이 나왔고, 평균 센티먼트가 +0.6이면 → news_sentiment=0.6, news_volume=10. 이 숫자들이 RSI, PER 등 다른 75개 피처와 함께 ML 모델에 들어가 주가 방향을 예측합니다.

5. Lazy Singleton 패턴 — 무거운 모델을 효율적으로

문제: 모델 로딩이 무겁다

KR-FinBert-SC 모델은 약 400MB 크기입니다. 이 모델을 메모리에 올리는 데 3~5초가 걸립니다. 만약 뉴스를 분석할 때마다 매번 모델을 새로 로드한다면, 종목 100개의 뉴스를 처리하는 데 모델 로딩만으로 300~500초가 소모됩니다.

해결: Lazy Singleton

이 문제를 해결하기 위해 프로젝트에서는 Lazy Singleton 패턴을 사용합니다. 이름을 분해하면:

Singleton(싱글톤): 앱 전체에서 단 하나의 모델 인스턴스만 존재하도록 보장합니다. 누가 어디서 호출하든 항상 같은 모델 객체를 반환합니다.

Lazy(지연): 앱이 시작될 때 바로 로드하지 않고, 처음 실제로 사용할 때 비로소 로드합니다. 뉴스 분석이 필요 없는 작업에서는 모델이 아예 로드되지 않습니다.

비유: 두꺼운 백과사전을 생각해보세요. 처음에 책장에서 꺼내 책상 위에 올려놓는 건 시간이 걸립니다(3~5초). 하지만 한번 책상에 올려놓으면, 이후에는 바로바로 펼쳐서 볼 수 있습니다. 그리고 백과사전이 필요 없는 날에는 아예 꺼내지 않습니다. 이것이 Lazy Singleton입니다.

실제 프로젝트 코드

# sentiment_analyzer.py — 실제 프로젝트 코드

class SentimentAnalyzer:
    _instance = None           # 클래스 변수: 유일한 인스턴스 저장

    @classmethod
    def get_instance(cls):
        # Singleton: 인스턴스가 없으면 생성, 있으면 재사용
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def __init__(self):
        self._pipeline = None    # Lazy: 아직 모델 로드 안 함

    def _load_model(self):
        if self._pipeline is not None:
            return             # 이미 로드됨 → 스킵
        from transformers import pipeline  # Lazy import
        self._pipeline = pipeline(
            "sentiment-analysis",
            model="snunlp/KR-FinBert-SC",
            max_length=512,
            truncation=True,
        )

    def analyze(self, texts, batch_size=32):
        self._load_model()        # 첫 호출 시에만 로드
        return self._pipeline(texts, batch_size=batch_size)

이 패턴 덕분에 뉴스 분석(news_service.py)과 공시 분석(disclosure_service.py)에서 같은 모델 인스턴스를 공유합니다. 두 서비스 모두 SentimentAnalyzer.get_instance()를 호출하지만, 실제 모델 로드는 딱 한 번만 일어납니다.

# news_service.py — 뉴스 분석 시
analyzer = SentimentAnalyzer.get_instance()  # 첫 호출 → 모델 로드 (3~5초)

# disclosure_service.py — 공시 분석 시
analyzer = SentimentAnalyzer.get_instance()  # 이미 로드됨 → 즉시 반환

6. 배치 추론 — 32개씩 묶어서 분석

문제: 하나씩 분석하면 너무 느리다

종목 하나당 뉴스가 50건이고, 종목이 100개면 총 5,000건의 뉴스를 분석해야 합니다. 뉴스를 1건씩 모델에 넣으면, 매번 CPU/GPU와 통신하는 오버헤드가 발생합니다.

해결: batch_size=32

Alchemetric에서는 batch_size=32로 설정하여, 뉴스 32건을 한꺼번에 모델에 전달합니다.

비유: 엘리베이터를 생각해보세요. 1층에서 10층까지 사람을 옮길 때, 한 명씩 태우면 32번 왕복해야 합니다. 하지만 32명을 한번에 태우면 1번 왕복이면 됩니다. 배치 추론은 이 “한번에 태우기”와 같습니다.
# sentiment_analyzer.py — 배치 추론
def analyze(self, texts: list[str], batch_size: int = 32) -> list[dict]:
    self._load_model()
    outputs = self._pipeline(texts, batch_size=batch_size)
    # texts = ["뉴스1", "뉴스2", ..., "뉴스32"]
    # → 32개를 한번에 모델에 전달 → 32개 결과 동시 반환
방식 뉴스 500건 처리 모델 호출 횟수
1건씩 (batch_size=1) 느림 500회
32건씩 (batch_size=32) ~16배 빠름 16회

batch_size를 너무 크게 잡으면 메모리가 부족할 수 있고, 너무 작으면 속도 이점이 줄어듭니다. 32는 CPU 환경에서 메모리와 속도의 균형이 좋은 값입니다.

7. 뉴스 수집 → 분석 → 피처 전체 흐름

지금까지 설명한 개별 기술들이 실제로 어떻게 조합되어 동작하는지, 전체 데이터 흐름을 살펴봅니다.

Step 1. Naver News API로 뉴스 수집

news_fetcher.py가 Naver 검색 API를 호출하여 두 종류의 뉴스를 수집합니다.

종류 검색 키워드 예시
종목 뉴스 {종목명} 주가 “삼성전자 주가”
시장 뉴스 코스피, 코스닥, 증시 등 “코스피”, “한국은행 금리”

Step 2. HTML 태그 정리

네이버 API 응답에는 <b> 태그나 &amp; 같은 HTML 요소가 섞여 있습니다. 정규식으로 이를 제거하여 깨끗한 텍스트만 남깁니다.

# news_fetcher.py — HTML 정리
_TAG_RE = re.compile(r"<[^>]+>")         # HTML 태그 제거
_ENTITY_RE = re.compile(r"&[a-zA-Z]+;|&#\d+;")  # HTML 엔티티 제거

def _clean_html(text):
    text = _TAG_RE.sub("", text)
    text = _ENTITY_RE.sub(" ", text)
    return text.strip()

Step 3. KR-FinBert-SC 배치 분석

news_service.py에서 제목과 설명을 합쳐서 센티먼트 분석을 수행합니다.

# news_service.py — 분석 텍스트 조합
texts = [
    r["title"] + ". " + (r.get("description") or "")
    for r in records
]
# "삼성전자 역대 최대 실적. 삼성전자가 3분기 영업이익..."

sentiments = analyzer.analyze(texts)  # 배치 분석 (batch_size=32)

Step 4. DB 저장

분석된 센티먼트 점수는 뉴스 원문과 함께 news_sentiment 테이블에 저장됩니다. 중복된 뉴스는 upsert(있으면 갱신, 없으면 삽입)로 처리됩니다.

Step 5. 피처 엔지니어링(Phase 4)에서 일별 집계

feature_engineer.py_merge_news_features() 메서드가 DB에 저장된 개별 뉴스 센티먼트를 일별로 집계하여, ML 학습에 사용할 피처로 변환합니다.

# 전체 흐름 요약

Naver API  ─── fetch ───▶  뉴스 원문 (title + description)
                                    │
                          _clean_html()  # HTML 태그 제거
                                    │
                          analyzer.analyze()  # KR-FinBert-SC 배치 분석
                                    │
                          sentiment_score  # -1.0 ~ +1.0
                                    │
                          DB 저장 (news_sentiment 테이블)
                                    │
                          _merge_news_features()  # 일별 집계
                                    │
                          feature_store  # ML 학습용 피처

8. 한계와 개선

한계 1: 종목과 무관한 뉴스 혼재

“삼성전자 주가”로 검색하면, 삼성전자와 직접 관련 없는 뉴스(예: “삼성그룹 관련 뉴스”)도 함께 수집됩니다. 이런 노이즈 뉴스의 센티먼트가 피처에 섞이면 예측 정확도가 떨어질 수 있습니다.

개선: Phase 6B에서 news_relevance_ratio(뉴스 제목에 종목명이 포함된 비율)와 news_sentiment_filtered(종목명 포함 뉴스만의 센티먼트)를 추가 피처로 계산하여, ML 모델이 관련도 높은 뉴스의 영향을 더 크게 반영하도록 했습니다.

한계 2: 512 토큰 제한

긴 심층 분석 기사의 경우 512 토큰 이후의 내용이 잘립니다. 하지만 뉴스의 핵심 내용은 대부분 제목과 첫 몇 문단에 집중되어 있으므로, 실전에서는 title + description(요약)만 사용해도 충분한 정확도를 보입니다.

한계 3: 한국어 금융 도메인 한정

KR-FinBert-SC는 한국어 금융 뉴스에 특화된 모델이라, 영어 뉴스나 금융 외 분야 텍스트에는 정확도가 떨어집니다. 글로벌 시장 뉴스 분석이 필요하다면 별도의 영어 FinBERT 모델을 추가해야 합니다.

Alchemetric — AI 기반 퀀트 트레이딩 플랫폼
NLP Sentiment Analysis Pipeline (Phase 4)

transformers + KR-FinBert-SC → Lazy Singleton → Batch Inference → Feature Store