Spaces:
Sleeping
Sleeping
Add app.py and analyzer
Browse files- app.py +61 -0
- news_analyzer.py +464 -0
- requirements.txt +9 -0
- 제목주의지수.ipynb +543 -0
app.py
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""Untitled0.ipynb
|
3 |
+
|
4 |
+
Automatically generated by Colab.
|
5 |
+
|
6 |
+
Original file is located at
|
7 |
+
https://colab.research.google.com/drive/1wnyeCNxzRVxoae3tCcMuf3s9Adx503U7
|
8 |
+
"""
|
9 |
+
|
10 |
+
# app.py — Hugging Face Spaces entry
|
11 |
+
import gradio as gr
|
12 |
+
import torch
|
13 |
+
|
14 |
+
# 우리 분석 로직 불러오기
|
15 |
+
from news_analyzer import run_once, title_attention_index, load_models
|
16 |
+
|
17 |
+
# (선로드) 모델을 미리 로드해두면 첫 호출 지연이 줄어듭니다.
|
18 |
+
try:
|
19 |
+
load_models()
|
20 |
+
except Exception as e:
|
21 |
+
print("[WARN] warmup load_models failed:", e)
|
22 |
+
|
23 |
+
def predict(title, body):
|
24 |
+
"""
|
25 |
+
Gradio UI에서 호출되는 함수. news_analyzer.run_once()를 그대로 사용.
|
26 |
+
"""
|
27 |
+
r = run_once(title, body)
|
28 |
+
|
29 |
+
final_score = r["최종 기사 점수"]
|
30 |
+
grade = title_attention_index(final_score)
|
31 |
+
|
32 |
+
# UI에 보여줄 값들만 반환
|
33 |
+
return (
|
34 |
+
r["요약유사도"],
|
35 |
+
r["본문 일치도(Top5 평균)"],
|
36 |
+
r["과장점수"],
|
37 |
+
final_score,
|
38 |
+
grade,
|
39 |
+
)
|
40 |
+
|
41 |
+
demo = gr.Interface(
|
42 |
+
fn=predict,
|
43 |
+
inputs=[
|
44 |
+
gr.Textbox(label="제목", lines=2, placeholder="기사 제목을 입력"),
|
45 |
+
gr.Textbox(label="본문", lines=18, placeholder="기사 본문을 붙여넣으세요"),
|
46 |
+
],
|
47 |
+
outputs=[
|
48 |
+
gr.Number(label="요약유사도"),
|
49 |
+
gr.Number(label="본문 일치도(Top5 평균)"),
|
50 |
+
gr.Number(label="과장점수"),
|
51 |
+
gr.Number(label="최종 기사 점수"),
|
52 |
+
gr.Textbox(label="제목 주의 지수", interactive=False),
|
53 |
+
],
|
54 |
+
title="제목 주의 지수",
|
55 |
+
description="제목-본문 유사도와 과장 점수로 '제목 주의 지수'를 계산합니다.",
|
56 |
+
)
|
57 |
+
|
58 |
+
# Spaces에서는 share/server_name 지정 불필요
|
59 |
+
if __name__ == "__main__":
|
60 |
+
# queue=True: 모델 로딩/추론 대기열 관리
|
61 |
+
demo.queue().launch()
|
news_analyzer.py
ADDED
@@ -0,0 +1,464 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""제목주의지수 (4).ipynb
|
3 |
+
|
4 |
+
Automatically generated by Colab.
|
5 |
+
|
6 |
+
Original file is located at
|
7 |
+
https://colab.research.google.com/drive/1v2tMK6_NdEthlQJAU-Hipwkprq70y2jt
|
8 |
+
"""
|
9 |
+
|
10 |
+
import os
|
11 |
+
import re
|
12 |
+
import sys
|
13 |
+
import numpy as np
|
14 |
+
import pandas as pd
|
15 |
+
import torch
|
16 |
+
from tqdm import tqdm
|
17 |
+
from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration
|
18 |
+
from sentence_transformers import SentenceTransformer, util
|
19 |
+
|
20 |
+
import re, math, json, numpy as np, pandas as pd, torch
|
21 |
+
from typing import List, Dict, Tuple, Any
|
22 |
+
from collections import Counter
|
23 |
+
import argparse
|
24 |
+
|
25 |
+
DEVICE = ("cuda" if torch.cuda.is_available()
|
26 |
+
else "mps" if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available()
|
27 |
+
else "cpu")
|
28 |
+
SIM_DEVICE = "cpu" if DEVICE == "mps" else DEVICE
|
29 |
+
print(f"[INFO] Gen Device: {DEVICE} | Sim Device: {SIM_DEVICE}")
|
30 |
+
|
31 |
+
exag = {'가득': 2, '가세': 2, '가속': 2, '강력': 1, '강하다': 1, '거품': 3, '격돌': 1, '격앙': 1, '격차': 1, '경악': 1, '고비': 2, '고삐': 1, '고조': 2, '고지': 3, '고통': 3, '공세': 1, '공포': 1, '과장': 1, '광폭': 2, '광풍': 3, '괴물': 2, '구원투수': 3, '굴욕': 3, '극적': 2, '극찬': 2, '글쎄': 2, '급감': 2, '급등': 2, '급발진': 2, '급속': 2, '기승': 1, '기적': 2, '깜짝': 1, '껑충': 2, '꼴찌': 3, '꼼수': 1, '꽁꽁': 2, '꽂히다': 1, '꿀꺽': 2, '꿈틀': 1, '끔찍': 1, '난리': 2, '난항': 1, '날다': 1, '날벼락': 3, '냉각': 2, '넘치다': 1, '논란': 1, '놀라다': 1, '눈덩이': 2, '눈물': 2, '당장': 2, '대규모': 2, '대란': 3, '대박': 3, '대반전': 2, '대폭': 2, '대환영': 2, '덕분': 1, '돌파구': 2, '돌풍': 4, '뒷걸음질': 2, '뒷북': 3, '든든한': 2, '들썩': 1, '떡락': 3, '떡상': 3, '뚝딱': 2, '뚝뚝': 2, '뜨겁다': 2, '러브콜': 3, '레전드': 4, '막차': 3, '만능': 1, '매우': 2, '맵다': 2, '멘붕': 2, '몸살': 3, '무더기': 2, '급물살': 1, '뭇매': 2, '뭉칫돈': 2, '밉다': 3, '바람': 2, '박살': 3, '반전': 1, '반짝': 2, '발칵': 1, '방긋': 2, '방점': 2, '배신': 3, '벌써': 1, '벼랑': 3, '봇물': 2, '부담': 1, '분노': 3, '분수령': 2, '불가피': 1, '불과': 2, '불금': 2, '불기둥': 2, '불꽃': 2, '불똥': 2, '불씨': 1, '불안하다': 2, '불투명': 1, '불확실': 1, '붕괴': 1, '비명': 3, '뻥튀기': 2, '사상': 2, '상급': 3, '상승': 1, '선방': 1, '설상가상': 3, '성큼': 2, '소름': 1, '속출': 2, '손절': 2, '솔솔': 1, '쇼크': 3, '수백': 2, '수상한': 2, '수혈': 2, '순항': 1, '승기': 3, '시름': 2, '신기록': 2, '실망': 1, '심각': 1, '싹쓸이': 3, '쏟아지다': 1, '쓰리다': 2, '아비규환': 2, '악몽': 3, '악재': 1, '안간힘': 2, '안갯속': 2, '안도': 1, '알짜': 1, '압도적': 3, '압승': 3, '야심작': 3, '얼어붙다': 2, '역대': 2, '역대 최고': 2, '역대 최다': 2, '역대 최소': 2, '역대 최저 ': 2, '역대최고': 2, '역대최다': 2, '역대최소': 2, '역대최저 ': 2, '열풍': 3, '영광': 2, '영웅': 3, '오락가락': 2, '온기': 2, '와르르': 3, '와우': 3, '완패': 3, '외면': 2, '외환위기 이후': 2, '외환 위기 이후': 2, '요동치다': 2, '우뚝': 2, '우려': 1, '울다': 2, '위기급': 4, '위기': 3, '위축': 1, '위태': 2, '위협': 1, '유력': 2, '육박': 2, '의혹': 1, '잔치': 3, '잘나가다': 2, '재난급': 4, '저격': 3, '전격': 1, '전설': 3, '절대': 2, '절벽': 4, '족쇄': 2, '주의보': 2, '줄줄이': 2, '중증': 3, '증발': 2, '직격탄': 2, '진통': 2, '질타': 3, '쪽박': 2, '참담': 2, '척척': 2, '초대형': 2, '초비상': 2, '초유': 2, '초토화': 2, '촉각': 2, '최대': 2, '최상': 2, '최선': 2, '최악': 2, '최애': 2, '최저': 2, '최적': 1, '최초': 2, '최후': 2, '추락': 4, '출혈': 2, '충격': 1, '코앞': 3, '털썩': 2, '톡톡': 2, '투톱': 3, '특급': 4, '파격': 1, '편법': 1, '폭락': 3, '폭발': 2, '폭주': 2, '폭증': 2, '폭탄': 2, '폭풍': 2, '하락': 1, '한숨': 2, '함박': 3, '함정': 2, '허리띠': 1, '헌정 사상': 2, '헌정사상': 2, '혁명': 2, '호소': 1, '호평일색': 2, '호평 일색': 2, '호황': 3, '혼돈': 2, '홈런': 2, '확대': 1, '활기': 2, '활발': 1, '활짝': 2, '활활': 2, '후끈': 2, '훨훨': 2, '휩쓸다': 2, '흔들다': 2, 'imf 이후': 2, '역대급': 4, '무궁무진': 2, '1보': 1, '2보': 1, '3보': 1, '단독': 1, '속보': 1, '패닉': 3, '불패': 3, '제동': 2, '조짐': 1, '초긴장': 2, '급제동': 2, '뚝': 2, '복병': 2, '아우성': 3, '좌불안석': 3, '빈손': 2, '대세': 3, '생트집': 3, '주춤': 2, '끄덕': 2, '맞불': 2, '장벽': 2, '썰렁': 2, '먹구름': 3, '부메랑': 2, '롤러코스터': 2, '발목': 2, '반토막': 2, '휘청': 2, '곤두박질': 3, '울상': 2, '위풍당당': 3, '싸늘': 2, '주저': 1, '우수수': 2, '골머리': 2, '공화국': 3, '고공행진': 4}
|
32 |
+
econ_list = ['(?<![가-힣])정부(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])한국은행(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])기준금리(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])인플레이션(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디스인플레이션(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])환율(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])재정적자(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])국채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])세제개편(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지[\\s-]?지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지[\\s-]?지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지[\\s-]?지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지\\-지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])긴축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])확장재정(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])통화정책(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])금통위(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기[\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기[\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기[\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기\\-둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기침체(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])잠재성장률(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])생산성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])배터리(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])데이터센터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])AI(?![A-Za-z])', '(?<![A-Za-z])AI(?![A-Za-z])', '(?<![가-힣])인공지능(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])로봇(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])플랫폼(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])빅테크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])스타트업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])구조조정(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])M\\&A(?![A-Za-z])', '(?<![A-Za-z])IPO(?![A-Za-z])', '(?<![A-Za-z])IPO(?![A-Za-z])', '(?<![가-힣])상장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])리콜(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])리쇼어링\\(reshoring\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공급망\\(supply\\ chain\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])중국리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])최저임금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고용지표(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])비정규직(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노동시간(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노동[\\s-]?시간(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노동[\\s-]?시간(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주거비(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계부채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\s-]?부채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\s-]?부채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주담대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연체율(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])파산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자영업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년[\\s-]?실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년[\\s-]?실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])여성고용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])여성[\\s-]?고용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])여성[\\s-]?고용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근로시간제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])증시(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])코스피(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])코스닥(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])채권(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])은행(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예대금리차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예대[\\s-]?금리차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예대[\\s-]?금리차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])유동성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])PF(?![A-Za-z])', '(?<![A-Za-z])PF(?![A-Za-z])', '(?<![가-힣])프로젝트파이낸싱(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])증권사(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자본확충(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자본[\\s-]?확충(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자본[\\s-]?확충(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공매도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])ETF(?![A-Za-z])', '(?<![가-힣])디지털자산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디지털[\\s-]?자산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디지털[\\s-]?자산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])암호화폐(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])스테이블코인(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])규제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])부동산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택공급(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택[\\s-]?공급(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택[\\s-]?공급(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])분양가상한제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])분양가[\\s-]?상한제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])분양가[\\s-]?상한제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])재건축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])재개발(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])용도지역(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신도시(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])역세권(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공임대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\s-]?임대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\s-]?임대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])토지거래허가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣A-Za-z])건설사\\s*PF(?![가-힣A-Za-z])', '(?<![가-힣A-Za-z])건설사\\s*PF(?![가-힣A-Za-z])', '(?<![가-힣A-Za-z])건설사\\s*PF(?![가-힣A-Za-z])', '(?<![A-Za-z])SOC(?![A-Za-z])', '(?<![가-힣])교통망(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])월세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])외식물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])생활물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])국제유가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])곡물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기[\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기[\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가스요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가스[\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가스[\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전력시장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전력[\\s-]?시장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전력[\\s-]?시장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])SMP(?![A-Za-z])', '(?<![A-Za-z])SMP(?![A-Za-z])', '(?<![가-힣])전력도매가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원전(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])태양광(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])풍력(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수소(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])탄소배출권\\(ETS\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])RE100(?![A-Za-z])', '(?<![가-힣])수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])무역수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])무역[\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])무역[\\s-]?수지(?:은|는|��|가|을|를)?(?![가-힣])', '(?<![가-힣])경상수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])달러인덱스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])달러[\\s-]?인덱스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])달러[\\s-]?인덱스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\s-]?약세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\s-]?약세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\s-]?약세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\s-]?강세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\s-]?강세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\s-]?강세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])통상마찰(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])관세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대미\\(IRA\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대EU\\(CBAM\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대중수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대중[\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대중[\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체[\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체[\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])프렌드쇼어링\\(friend\\-shoring\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])오픈뱅킹(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])오픈[\\s-]?뱅킹(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])오픈[\\s-]?뱅킹(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])핀테크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])마이데이터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])마이[\\s-]?데이터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])마이[\\s-]?데이터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디지털세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])규제샌드박스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])클라우드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])SaaS(?![A-Za-z])', '(?<![가-힣])데이터경제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])개인정보(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])양극화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자산격차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소득분배(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년부담(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노인빈곤(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])세대갈등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])지역균형(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])지방소멸(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주거불안(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])시스템리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])시스템[\\s-]?리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])시스템[\\s-]?리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])그림자금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])그림자[\\s-]?금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])그림자[\\s-]?금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])역전세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연쇄부도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연쇄[\\s-]?부도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연쇄[\\s-]?부도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디폴트(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용스프레드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용[\\s-]?스프레드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용[\\s-]?스프레드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])CDS프리미엄(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용경색(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고금리[\\s-]?장기화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고금리[\\s-]?장기화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고금리[\\s-]?장기화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계부채\\ 관리\\ 강화(?![가-힣])', '(?<![가-힣])부동산\\ PF\\ 부실(?![가-힣])', '(?<![가-힣])공공요금[\\s-]?인상(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공요금[\\s-]?인상(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공요금[\\s-]?인상(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\s-]?반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\s-]?반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\s-]?반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])정책[\\s-]?불확실성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])정책[\\s-]?불확실성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])정책[\\s-]?불확실성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])관치금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])밸류업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금\\-물가[\\s-]?악순환(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금\\-물가[\\s-]?악순환(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금\\-물가[\\s-]?악순환(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])투자[\\s-]?위축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])투자[\\s-]?위축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])투자[\\s-]?위축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])인바운드\\ 관광\\ 회복(?![가-힣])', '(?<![가-힣])기후리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])CPI(?![A-Za-z])', '(?<![A-Za-z])CPI(?![A-Za-z])', '(?<![가-힣])소비자[\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소비자[\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소비자[\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근원물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근원[\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근원[\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])PPI(?![A-Za-z])', '(?<![A-Za-z])PMI(?![A-Za-z])', '(?<![A-Za-z])GDP(?![A-Za-z])','(?<![A-Za-z])IPI(?![A-Za-z])', '(?<![A-Za-z])IPI(?![A-Za-z])', '(?<![가-힣])광공업[\\s-]?생산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])광공업[\\s-]?생산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])광공업[\\s-]?생산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소비자심리지수(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])기대인플레(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고용동향(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계신용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\s-]?신용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\s-]?신용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출입[\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출입[\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출입[\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택가격지수(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전세가격지수(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양[\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양[\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])FOMC(?![A-Za-z])', '(?<![A-Za-z])ECB(?![A-Za-z])', '(?<![A-Za-z])BOJ(?![A-Za-z])', '(?<![가-힣])금통위(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])OPEC\\+[\\s-]?회의(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])OPEC\\+[\\s-]?회의(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])OPEC\\+[\\s-]?회의(?:은|는|이|가|을|를)?(?![가-힣])']
|
33 |
+
|
34 |
+
RE_BULLETS = re.compile(r"[■◆◇]")
|
35 |
+
RE_GUIDE = re.compile(r"^\*.*$|^※.*$", flags=re.MULTILINE)
|
36 |
+
RE_ROLES = re.compile(r"^(진행|앵커|출연)\s*:\s*.*$", flags=re.MULTILINE)
|
37 |
+
RE_EMAIL = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
|
38 |
+
RE_EXTRA = re.compile(r"대담 발췌\s*:\s*.*$", flags=re.MULTILINE)
|
39 |
+
RE_MULTINL = re.compile(r"\n+")
|
40 |
+
RE_LSTRIP = re.compile(r"^\s+", flags=re.MULTILINE)
|
41 |
+
|
42 |
+
def preprocess_text(text: str) -> str:
|
43 |
+
if not isinstance(text, str):
|
44 |
+
return ""
|
45 |
+
text = RE_BULLETS.sub("", text)
|
46 |
+
text = RE_GUIDE.sub("", text)
|
47 |
+
text = RE_ROLES.sub("", text)
|
48 |
+
text = RE_EMAIL.sub("", text)
|
49 |
+
text = RE_EXTRA.sub("", text)
|
50 |
+
text = RE_MULTINL.sub("\n", text).strip()
|
51 |
+
text = RE_LSTRIP.sub("", text)
|
52 |
+
return text
|
53 |
+
|
54 |
+
def sentence_split(text: str):
|
55 |
+
if not isinstance(text, str):
|
56 |
+
text = "" if text is None else str(text)
|
57 |
+
text = text.replace("\n", ".")
|
58 |
+
text = re.sub(r"\.{2,}", ".", text)
|
59 |
+
return [s.strip() for s in text.split("다.") if s.strip()]
|
60 |
+
|
61 |
+
def top5_title_body_sim(title: str, body_text: str, sbert) -> float:
|
62 |
+
sents = sentence_split(body_text)
|
63 |
+
if not sents:
|
64 |
+
return float("nan")
|
65 |
+
title_emb = sbert.encode(title, convert_to_tensor=True, normalize_embeddings=True)
|
66 |
+
sent_embs = sbert.encode(sents, convert_to_tensor=True, normalize_embeddings=True)
|
67 |
+
sims = util.pytorch_cos_sim(title_emb, sent_embs)[0].detach().cpu().numpy().tolist()
|
68 |
+
sims.sort(reverse=True)
|
69 |
+
return float(np.mean(sims[:5])) if sims else float("nan")
|
70 |
+
|
71 |
+
# 필요한 모델 불러오기
|
72 |
+
_tok = _bart = _sbert = None
|
73 |
+
def load_models():
|
74 |
+
global _tok, _bart, _sbert
|
75 |
+
if _tok is None or _bart is None:
|
76 |
+
_tok = PreTrainedTokenizerFast.from_pretrained("digit82/kobart-summarization")
|
77 |
+
if _tok.pad_token is None:
|
78 |
+
_tok.pad_token = _tok.eos_token
|
79 |
+
_tok.model_max_length = 1024
|
80 |
+
_bart = BartForConditionalGeneration.from_pretrained("digit82/kobart-summarization")
|
81 |
+
_bart.eval().to(DEVICE)
|
82 |
+
if DEVICE == "cuda":
|
83 |
+
try:
|
84 |
+
_bart.half()
|
85 |
+
except Exception:
|
86 |
+
pass
|
87 |
+
if _sbert is None:
|
88 |
+
_sbert = SentenceTransformer("snunlp/KR-SBERT-V40K-klueNLI-augSTS", device=SIM_DEVICE)
|
89 |
+
return _tok, _bart, _sbert
|
90 |
+
|
91 |
+
@torch.inference_mode()
|
92 |
+
def summarize(tok, model, text: str, max_new_tokens: int = 160) -> str:
|
93 |
+
if not text:
|
94 |
+
return ""
|
95 |
+
enc = tok(text, return_tensors="pt", truncation=True, max_length=1024, padding=False)
|
96 |
+
out = model.generate(
|
97 |
+
input_ids=enc["input_ids"].to(DEVICE),
|
98 |
+
attention_mask=enc["attention_mask"].to(DEVICE),
|
99 |
+
max_new_tokens=max_new_tokens,
|
100 |
+
num_beams=4,
|
101 |
+
no_repeat_ngram_size=3,
|
102 |
+
length_penalty=1.0,
|
103 |
+
early_stopping=True,
|
104 |
+
use_cache=True
|
105 |
+
)
|
106 |
+
return tok.decode(out[0], skip_special_tokens=True)
|
107 |
+
|
108 |
+
# 과장 표현 정규화 예외 설정
|
109 |
+
NORM_RULES = [
|
110 |
+
(r'외환\s*위기\s*이후', '외환위기이후'),
|
111 |
+
(r'IMF\s*이후', 'IMF이후'),
|
112 |
+
(r'imf\s*이후', 'imf이후'),
|
113 |
+
(r'IMF\s*급', 'IMF급'),
|
114 |
+
(r'imf\s*급', 'imf급'),
|
115 |
+
(r'호평\s*일색', '호평일색'),
|
116 |
+
(r'헌정\s*사상', '헌정사상'),
|
117 |
+
(r'역대\s*최고', '역대최고'),
|
118 |
+
(r'역대\s*최다', '역대최다'),
|
119 |
+
(r'역대\s*최소', '역대최소'),
|
120 |
+
(r'역대\s*최저', '역대최저'),
|
121 |
+
]
|
122 |
+
USER_TERMS = [
|
123 |
+
'대반전',
|
124 |
+
'외환위기이후',
|
125 |
+
'위기급', '재난급', '급물살',
|
126 |
+
'IMF이후', 'imf이후',
|
127 |
+
'IMF급', 'imf급',
|
128 |
+
'역대최고', '역대최다', '역대최소', '역대최저',
|
129 |
+
'역대급',
|
130 |
+
'떡상', '떡락',
|
131 |
+
'호평일색',
|
132 |
+
'헌정사상',
|
133 |
+
]
|
134 |
+
def normalize_expressions(text: str) -> str:
|
135 |
+
t = text if isinstance(text, str) else ""
|
136 |
+
for pat, rep in NORM_RULES:
|
137 |
+
t = re.sub(pat, rep, t)
|
138 |
+
return t
|
139 |
+
|
140 |
+
_score_map: Dict[str, int] = None
|
141 |
+
_unique_expr: List[str] = None
|
142 |
+
_lex_pats: List[re.Pattern] = None
|
143 |
+
_kiwi = None
|
144 |
+
|
145 |
+
def _load_label_score_map_from_dict(exag_dict: Dict[str, int]) -> Tuple[Dict[str,int], List[str]]:
|
146 |
+
""" exag 딕셔너리에서 점수 맵/표현 리스트 생성 """
|
147 |
+
score_map: Dict[str, int] = {}
|
148 |
+
for k, v in (exag_dict or {}).items():
|
149 |
+
key = re.sub(r"\s+", "", str(k)).strip()
|
150 |
+
try:
|
151 |
+
val = int(v)
|
152 |
+
except Exception:
|
153 |
+
val = 0
|
154 |
+
if key:
|
155 |
+
if key in score_map:
|
156 |
+
score_map[key] = max(score_map[key], val)
|
157 |
+
else:
|
158 |
+
score_map[key] = val
|
159 |
+
unique_expr = sorted(score_map.keys())
|
160 |
+
return score_map, unique_expr
|
161 |
+
|
162 |
+
def _compile_patterns_from_list(regex_list: List[str]) -> List[re.Pattern]:
|
163 |
+
""" econ_list 문자열 배열에서 정규식 패턴 컴파일 """
|
164 |
+
pats: List[re.Pattern] = []
|
165 |
+
for p in (regex_list or []):
|
166 |
+
if not isinstance(p, str):
|
167 |
+
continue
|
168 |
+
pat = p.strip()
|
169 |
+
if not pat:
|
170 |
+
continue
|
171 |
+
try:
|
172 |
+
pats.append(re.compile(pat, re.I))
|
173 |
+
except re.error:
|
174 |
+
# 잘못된 패턴은 무시
|
175 |
+
pass
|
176 |
+
return pats
|
177 |
+
|
178 |
+
def _build_kiwi(unique_expr: List[str]):
|
179 |
+
""" Kiwi > Okt > regex 순으로 형태소/토큰 추출기 준비 """
|
180 |
+
# 1) kiwipiepy
|
181 |
+
try:
|
182 |
+
from kiwipiepy import Kiwi
|
183 |
+
kiwi = Kiwi()
|
184 |
+
for w in USER_TERMS:
|
185 |
+
kiwi.add_user_word(w, 'NNG', 10)
|
186 |
+
for w in unique_expr:
|
187 |
+
if isinstance(w, str) and len(w) >= 2:
|
188 |
+
kiwi.add_user_word(w, 'NNG', 9)
|
189 |
+
return kiwi, "kiwi"
|
190 |
+
except Exception:
|
191 |
+
pass
|
192 |
+
# 2) konlpy Okt
|
193 |
+
try:
|
194 |
+
from konlpy.tag import Okt
|
195 |
+
_okt = Okt()
|
196 |
+
def _okt_extract(text: str):
|
197 |
+
norm = normalize_expressions(text)
|
198 |
+
# 명사/동사만
|
199 |
+
return [w for w, t in _okt.pos(norm, norm=True, stem=True) if t in ("Noun","Verb")]
|
200 |
+
return _okt_extract, "okt"
|
201 |
+
except Exception:
|
202 |
+
# 3) 정규식 토큰 나누기
|
203 |
+
def _regex_extract(text: str):
|
204 |
+
norm = normalize_expressions(text)
|
205 |
+
return re.findall(r"[가-힣A-Za-z0-9]+", norm)
|
206 |
+
return _regex_extract, "regex"
|
207 |
+
|
208 |
+
def _ensure_resources():
|
209 |
+
global _score_map, _unique_expr, _lex_pats, _kiwi
|
210 |
+
try:
|
211 |
+
exag_dict = exag
|
212 |
+
except NameError:
|
213 |
+
raise RuntimeError("exag 딕셔너리가 정의되어 있지 않습니다. exag = {'표현': 점수, ...} 형태로 먼저 정의하세요.")
|
214 |
+
if _score_map is None or _unique_expr is None:
|
215 |
+
_score_map, _unique_expr = _load_label_score_map_from_dict(exag_dict)
|
216 |
+
if _lex_pats is None:
|
217 |
+
try:
|
218 |
+
econ = econ_list
|
219 |
+
except NameError:
|
220 |
+
econ = []
|
221 |
+
_lex_pats = _compile_patterns_from_list(econ)
|
222 |
+
if _kiwi is None:
|
223 |
+
_kiwi, _ = _build_kiwi(_unique_expr)
|
224 |
+
|
225 |
+
# 형태소 추출(명사+동사)
|
226 |
+
def extract_noun_verb_kiwi(text: str) -> List[str]:
|
227 |
+
_ensure_resources()
|
228 |
+
norm = normalize_expressions(text)
|
229 |
+
try:
|
230 |
+
from kiwipiepy import Kiwi
|
231 |
+
if isinstance(_kiwi, Kiwi):
|
232 |
+
toks = []
|
233 |
+
for tok in _kiwi.tokenize(norm):
|
234 |
+
tag = tok.tag
|
235 |
+
if tag.startswith("NN"):
|
236 |
+
toks.append(tok.form)
|
237 |
+
elif tag == "VV":
|
238 |
+
toks.append(tok.lemma if tok.lemma else tok.form)
|
239 |
+
return toks
|
240 |
+
except Exception:
|
241 |
+
pass
|
242 |
+
return _kiwi(norm)
|
243 |
+
|
244 |
+
# 과장 라벨 점수 계산 및 가중치 산출
|
245 |
+
def _calc_raw_and_count(tokens: List[str]) -> Tuple[int, int]:
|
246 |
+
_ensure_resources()
|
247 |
+
if not isinstance(tokens, (list, tuple)):
|
248 |
+
return 0, 0
|
249 |
+
toks = [str(t).strip() for t in tokens if (t is not None) and str(t).strip() != ""]
|
250 |
+
joined = "".join(toks)
|
251 |
+
total_count, total_score = 0, 0
|
252 |
+
for expr, sc in _score_map.items():
|
253 |
+
c = joined.count(expr) # non-overlapping
|
254 |
+
if c:
|
255 |
+
total_count += c
|
256 |
+
total_score += c * int(sc)
|
257 |
+
return int(total_score), int(total_count)
|
258 |
+
|
259 |
+
def _bin_label(total_raw: int) -> int:
|
260 |
+
# (-inf,0] -> 0, [1,2] -> 1, [3,4] -> 2, [5, inf) -> 3
|
261 |
+
if total_raw <= 0: return 0
|
262 |
+
if 1 <= total_raw <= 2: return 1
|
263 |
+
if 3 <= total_raw <= 4: return 2
|
264 |
+
return 3
|
265 |
+
|
266 |
+
def _weight_by_count(n: int) -> float:
|
267 |
+
if n == 1: return 1.0
|
268 |
+
if n == 2: return 1.3
|
269 |
+
if n == 3: return 1.5
|
270 |
+
if n >= 4: return 1.7
|
271 |
+
return 0.0
|
272 |
+
|
273 |
+
def _has_keyword_and_matches(text: str) -> Tuple[bool, List[str]]:
|
274 |
+
_ensure_resources()
|
275 |
+
t = text or ""
|
276 |
+
seen, out = set(), []
|
277 |
+
has_any = False
|
278 |
+
for pat in _lex_pats:
|
279 |
+
m = pat.search(t)
|
280 |
+
if m:
|
281 |
+
has_any = True
|
282 |
+
s = m.group(0)
|
283 |
+
if s not in seen:
|
284 |
+
seen.add(s)
|
285 |
+
out.append(s)
|
286 |
+
return has_any, out
|
287 |
+
|
288 |
+
import math
|
289 |
+
def title_attention_index(score: float) -> str:
|
290 |
+
if score is None or (isinstance(score, float) and math.isnan(score)):
|
291 |
+
return "점수 없음"
|
292 |
+
# 구간: [0,0.95) 양호, [0.95,2.25) 관심, [2.25,3.70) 주의, [3.70,5) 매우 주의
|
293 |
+
if score < 0.95:
|
294 |
+
return "양호"
|
295 |
+
if score < 2.25:
|
296 |
+
return "관심"
|
297 |
+
if score < 3.70:
|
298 |
+
return "주의"
|
299 |
+
return "매우 주의"
|
300 |
+
|
301 |
+
# 메인 파이프라인
|
302 |
+
def run_once(title: str, body: str,
|
303 |
+
short_pass_len: int = 50, max_new_tokens: int = 160):
|
304 |
+
_ensure_resources()
|
305 |
+
|
306 |
+
# 모델
|
307 |
+
tok, bart, sbert = load_models()
|
308 |
+
|
309 |
+
# 본문 전처리
|
310 |
+
body_clean = preprocess_text(body)
|
311 |
+
|
312 |
+
# 요약
|
313 |
+
if len(body_clean) < short_pass_len:
|
314 |
+
summ = body_clean
|
315 |
+
else:
|
316 |
+
try:
|
317 |
+
@torch.inference_mode()
|
318 |
+
def _summarize(tok, model, text, max_new_tokens=160):
|
319 |
+
enc = tok(text, return_tensors="pt", truncation=True, max_length=1024, padding=False)
|
320 |
+
out = model.generate(
|
321 |
+
input_ids=enc["input_ids"].to(DEVICE),
|
322 |
+
attention_mask=enc["attention_mask"].to(DEVICE),
|
323 |
+
max_new_tokens=max_new_tokens,
|
324 |
+
num_beams=4,
|
325 |
+
no_repeat_ngram_size=3,
|
326 |
+
length_penalty=1.0,
|
327 |
+
early_stopping=True,
|
328 |
+
use_cache=True
|
329 |
+
)
|
330 |
+
return tok.decode(out[0], skip_special_tokens=True)
|
331 |
+
summ = _summarize(tok, bart, body_clean, max_new_tokens=max_new_tokens)
|
332 |
+
except Exception as e:
|
333 |
+
print(f"[WARN] summarization failed: {e}")
|
334 |
+
summ = ""
|
335 |
+
|
336 |
+
# 유사도
|
337 |
+
try:
|
338 |
+
if summ:
|
339 |
+
tvec = sbert.encode(title, convert_to_tensor=True, normalize_embeddings=True)
|
340 |
+
svec = sbert.encode(summ, convert_to_tensor=True, normalize_embeddings=True)
|
341 |
+
sim_sy = float(util.pytorch_cos_sim(tvec, svec).item())
|
342 |
+
else:
|
343 |
+
sim_sy = float("nan")
|
344 |
+
except Exception as e:
|
345 |
+
print(f"[WARN] title-summary sim failed: {e}")
|
346 |
+
sim_sy = float("nan")
|
347 |
+
|
348 |
+
try:
|
349 |
+
sim_b5 = top5_title_body_sim(title, body_clean, sbert)
|
350 |
+
except Exception as e:
|
351 |
+
print(f"[WARN] title-body top5 sim failed: {e}")
|
352 |
+
sim_b5 = float("nan")
|
353 |
+
|
354 |
+
# 제목 형태소(명사/동사)
|
355 |
+
try:
|
356 |
+
title_nv = extract_noun_verb_kiwi(title)
|
357 |
+
except Exception as e:
|
358 |
+
print(f"[WARN] kiwi extract failed: {e}")
|
359 |
+
title_nv = re.findall(r"[가-힣A-Za-z0-9]+", normalize_expressions(title or ""))
|
360 |
+
|
361 |
+
# 라벨 원점수/등장횟수 → 라벨점수 → 가중치 최종점수
|
362 |
+
raw_score, cnt = _calc_raw_and_count(title_nv)
|
363 |
+
label_score = _bin_label(raw_score) # 0/1/2/3
|
364 |
+
weight = _weight_by_count(cnt) # 1.0/1.3/1.5/1.7
|
365 |
+
label_final = float(label_score) * float(weight) # df['최종점수']
|
366 |
+
|
367 |
+
# 본문 키워드 여부/매칭 → 1.15배
|
368 |
+
has_kw, matches = _has_keyword_and_matches(body_clean)
|
369 |
+
exag_score = label_final * (1.15 if has_kw else 1.0) # df["과장점수"]
|
370 |
+
|
371 |
+
# 불일치도 & log10
|
372 |
+
summary_mismatch = (1 - sim_sy) if not np.isnan(sim_sy) else np.nan
|
373 |
+
body_mismatch = (1 - sim_b5) if not np.isnan(sim_b5) else np.nan
|
374 |
+
exag_log10 = float(np.log10(exag_score + 1.0))
|
375 |
+
|
376 |
+
# 최종 기사 점수
|
377 |
+
if not (np.isnan(summary_mismatch) or np.isnan(body_mismatch)):
|
378 |
+
final_article_score = round((exag_log10*0.5 + summary_mismatch*0.25 + body_mismatch*0.25) * 5, 2)
|
379 |
+
else:
|
380 |
+
final_article_score = np.nan
|
381 |
+
|
382 |
+
return {
|
383 |
+
"요약": summ,
|
384 |
+
"요약유사도": sim_sy,
|
385 |
+
"본문 일치도(Top5 평균)": sim_b5,
|
386 |
+
"title_nv": title_nv, #형태소 분석된 제목 리스트
|
387 |
+
"원점수": raw_score, #과장 표현 원점수
|
388 |
+
"등장횟수": cnt,
|
389 |
+
"라벨점수": int(label_score),
|
390 |
+
"가중치": float(weight),
|
391 |
+
"라벨최종점수": float(label_final),
|
392 |
+
"has_keyword": bool(has_kw), #가중 키워드 본문 포함 여부
|
393 |
+
"matches": matches, # 가중 키워드로 선정된 키워드 리스트
|
394 |
+
"과장점수": float(exag_score),
|
395 |
+
"과장점수_log10": exag_log10, #과장 최종 점수
|
396 |
+
"요약 불일치도": summary_mismatch,
|
397 |
+
"본문 불일치도": body_mismatch,
|
398 |
+
"최종 기사 점수": final_article_score
|
399 |
+
}
|
400 |
+
|
401 |
+
|
402 |
+
def run_cli():
|
403 |
+
print("제목을 입력하세요:")
|
404 |
+
title = input().strip()
|
405 |
+
print("본문을 입력하세요:")
|
406 |
+
body = input().strip()
|
407 |
+
r = run_once(title, body)
|
408 |
+
print("\n===== 결과 =====")
|
409 |
+
# print("본문 요약:\n", r["요약"])
|
410 |
+
print("제목과 본문 요약 유사도:", round(r["요약유사도"], 4))
|
411 |
+
print("제목과 본문 일치도(Top5 평균):", round(r["본문 일치도(Top5 평균)"], 4))
|
412 |
+
print("과장점수(log화):", round(r["과장점수_log10"], 4))
|
413 |
+
print("\n최종 제목 주의 점수는", r["최종 기사 점수"], "입니다")
|
414 |
+
|
415 |
+
def run_ui():
|
416 |
+
import gradio as gr
|
417 |
+
# ... (기존 내용 동일)
|
418 |
+
|
419 |
+
def predict(title, body):
|
420 |
+
r = run_once(title, body)
|
421 |
+
final_score = r["최종 기사 점수"]
|
422 |
+
grade = title_attention_index(final_score)
|
423 |
+
return (
|
424 |
+
r["요약유사도"],
|
425 |
+
r["본문 일치도(Top5 평균)"],
|
426 |
+
r["과장점수"],
|
427 |
+
final_score,
|
428 |
+
grade,
|
429 |
+
)
|
430 |
+
|
431 |
+
demo = gr.Interface(
|
432 |
+
fn=predict,
|
433 |
+
inputs=[
|
434 |
+
gr.Textbox(label="제목", lines=2),
|
435 |
+
gr.Textbox(label="본문", lines=18, placeholder="여기에 기사 본문을 붙여넣으세요"),
|
436 |
+
],
|
437 |
+
outputs=[
|
438 |
+
# gr.Textbox(label="요약", lines=10),
|
439 |
+
gr.Number(label="요약유사도"),
|
440 |
+
gr.Number(label="본문 일치도(Top5 평균)"),
|
441 |
+
gr.Number(label="과장점수"),
|
442 |
+
gr.Number(label="최종 기사 점수"),
|
443 |
+
gr.Textbox(label="제목 주의 지수", interactive=False),
|
444 |
+
],
|
445 |
+
title="제목 주의 지수",
|
446 |
+
description="제목/본문을 입력하면 제목-본문 유사도, 과장 점수를 바탕으로 '제목 주의 지수'를 계산합니다.",
|
447 |
+
# 필요시 allow_flagging="never"
|
448 |
+
)
|
449 |
+
|
450 |
+
demo.launch(server_name="0.0.0.0", server_port=7861, share=True)
|
451 |
+
|
452 |
+
|
453 |
+
|
454 |
+
if __name__ == "__main__":
|
455 |
+
parser = argparse.ArgumentParser()
|
456 |
+
parser.add_argument("--ui", action="store_true", help="Gradio UI 실행")
|
457 |
+
args, _ = parser.parse_known_args()
|
458 |
+
if args.ui:
|
459 |
+
run_ui()
|
460 |
+
else:
|
461 |
+
run_cli()
|
462 |
+
|
463 |
+
|
464 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
torch
|
2 |
+
transformers
|
3 |
+
sentence-transformers
|
4 |
+
kiwipiepy
|
5 |
+
gradio
|
6 |
+
pandas
|
7 |
+
numpy
|
8 |
+
tqdm
|
9 |
+
scipy
|
제목주의지수.ipynb
ADDED
@@ -0,0 +1,543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"id": "17ecb9c0-ec8e-4df9-a694-1b0dce05650b",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [
|
9 |
+
{
|
10 |
+
"name": "stdout",
|
11 |
+
"output_type": "stream",
|
12 |
+
"text": [
|
13 |
+
"WARNING:tensorflow:From C:\\Users\\15ZB995-GP5ALF\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\site-packages\\tf_keras\\src\\losses.py:2976: The name tf.losses.sparse_softmax_cross_entropy is deprecated. Please use tf.compat.v1.losses.sparse_softmax_cross_entropy instead.\n",
|
14 |
+
"\n"
|
15 |
+
]
|
16 |
+
}
|
17 |
+
],
|
18 |
+
"source": [
|
19 |
+
"import os\n",
|
20 |
+
"import re\n",
|
21 |
+
"import sys\n",
|
22 |
+
"import numpy as np\n",
|
23 |
+
"import pandas as pd\n",
|
24 |
+
"import torch\n",
|
25 |
+
"from tqdm import tqdm\n",
|
26 |
+
"from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration\n",
|
27 |
+
"from sentence_transformers import SentenceTransformer, util\n",
|
28 |
+
"\n",
|
29 |
+
"import re, math, json, numpy as np, pandas as pd, torch\n",
|
30 |
+
"from typing import List, Dict, Tuple, Any\n",
|
31 |
+
"from collections import Counter\n",
|
32 |
+
"import argparse"
|
33 |
+
]
|
34 |
+
},
|
35 |
+
{
|
36 |
+
"cell_type": "code",
|
37 |
+
"execution_count": 7,
|
38 |
+
"id": "d14540a3-cdfa-4455-a4e0-7caff0e5d473",
|
39 |
+
"metadata": {},
|
40 |
+
"outputs": [
|
41 |
+
{
|
42 |
+
"name": "stdout",
|
43 |
+
"output_type": "stream",
|
44 |
+
"text": [
|
45 |
+
"[INFO] Gen Device: cpu | Sim Device: cpu\n",
|
46 |
+
"제목을 입력하세요:\n"
|
47 |
+
]
|
48 |
+
},
|
49 |
+
{
|
50 |
+
"name": "stdin",
|
51 |
+
"output_type": "stream",
|
52 |
+
"text": [
|
53 |
+
" 전 세계적으로 갈라진 Z세대, 그리고 외면하는 정치 :한국 민주주의의 새로운 균열\n"
|
54 |
+
]
|
55 |
+
},
|
56 |
+
{
|
57 |
+
"name": "stdout",
|
58 |
+
"output_type": "stream",
|
59 |
+
"text": [
|
60 |
+
"본문을 입력하세요:\n"
|
61 |
+
]
|
62 |
+
},
|
63 |
+
{
|
64 |
+
"name": "stdin",
|
65 |
+
"output_type": "stream",
|
66 |
+
"text": [
|
67 |
+
" Z세대, '코호트 이론'의 예외인가 정치학의 오랜 이론 중 하나는 ‘코호트 효과(Cohort effect)’다. 같은 시대를 겪은 세대는 비슷한 정치적 가치를 공유한다는 것이다. 하지만 Z세대는 이 오래된 공식에 균열을 내고 있다. 하나의 스펙트럼으로 설명하기 어려운 이들의 복합성은, 특히 성별을 기준으로 나뉠 때 더욱 선명해진다. 세계에서 확인된 ‘성별 격차’ Z세대의 성별 분화는 이제 전 지구적 현상으로 자리 잡고 있다. 실제로 파이낸셜타임스(FT)의 데이터 칼럼은 미국·영국·독일 등 주요 민주주의 국가에서 공통적으로 나타나는 현상을 포착했다. 18~29세 청년층에서 남성의 보수화와 여성의 진보화가 마치 거울을 보듯 정반대로 진행되는 '거울상(鏡像) 효과'가 확인된 것이다. 추가로, Z세대에서 성평등·성 역할에 대한 남녀 인식 격차가 다른 어느 세대보다 컸으며, 한국은 '남녀 갈등 인식'이 30개국 중 가장 높게 나타났다. *18~29세 청년층의 정치적 이념 성향 (진보주의 - 보수주의 비율) 출처: 파이낸셜타임스(FT), 2022.03.07. 한국의 최근 선거에서 갈라진 선택 한국의 20대 청년층에서는 성별에 따른 정치적 선택이 뚜렷하게 드러났다. 2022년 제20대 대선 출구조사에서 20대 남성은 윤석열 58.7%, 20대 여성은 이재명 58.0%로, 같은 세대 안에서 선택이 정반대로 갈라졌다. 올해 2025년 제21대 대선에서도 양상은 비슷했다. 출구조사 기준 20대 남성은 이재명 24.0%, 20대 여성은 58.1%를 선택해 성별 간 분화가 재확인되었다. 다른 연령대에서도 차이는 존재했지만, 20대만큼 극단적인 분화는 드물었다는 점은 언론과 학계 분석에서 공통적으로 지적한다. 20대 대선 출구조사 21대 대선 출구조사 *방송 3사(KBS, MBC, SBS) 출구조사 결과, 신뢰수준 95%, 표본오차 ±0.8%p 학술 연구는 이 현상의 핵심을 짚어낸다. 김한나(서울대 한국정치연구소)의 로지스틱 회귀 분석에 따르면, 이념·정당 일체감·지역·후보 호감도 등을 통제한 뒤에도 ‘성별’만이 20대 유권자 선택에 유의미한 변수로 남았다. 이는 과거 한국 정치의 주요 균열 축이던 ‘지역’을, 최근에는 ‘성별’이 대체되고 있음을 보여준다. 언론의 관심 역시 높아졌다. 같은 기간 동안 정기간행물 전체기사 수 대비 젠더 관련 보도의 비중이 두드러지게 많았다는 점이 확인된 것이다. *대선 전후 3개월 간 보도된 기사량 비교 정치는 이 변화를 어떻게 반영했나 그렇다면 대통령 후보들은 이 분화를 어떻게 대응했을까? 법정 TV토론은 일방적 발표가 아니라 상호 공격과 대응이 오가는 전략적 장(場)이다. 법정 3차례의 제한된 시간 안에서 후보들은 ‘득표 효율’이 높은 쟁점을 던지며 상대의 실수를 부각한다. 그 결과 의제의 본질보다 공방 자체가 더 크�� 부상하기 쉽다. 반면 정책공약집(이하 ‘공약집’)은 사전 기획과 당내 조율을 거친 문서로, 위험을 관리·회피하기 용이하다는 점에서 TV토론과 성격이 다르다. 이 때문에 갈등을 촉발하거나 정치적 부담이 큰 의제는 의도적으로 축소되는 경향이 있다. 공약집 분석 결과, 제18대 대선 이후 젠더 관련 항목의 주목도와 언급 빈도는 지속적으로 감소했고, 청년과 젠더 교차 의제와 핵심 키워드 언급은 약 75% 감소했다. 줄어드는 청년표와 다음 과제 주목할 점은, 고령화와 함께 청년(19~34세) 인구가 빠르게 줄고 있다는 사실이다. 통계청에 따르면 2025년 1,025만 명이던 청년 인구는 2040년 722만 명으로, 약 30% 줄어들 전망이다. 특히 20대의 비율은 2025년 11.87%에서 2040년 8.82%까지 하락해 80대 이상 인구보다도 비중이 작아질 것으로 예측된다. 이처럼 청년층의 유권자 비중이 낮아지고 표심마저 성별에 따라 분화된 상황에서, 과거처럼 특정 쟁점이 이들의 표심을 결집하는 '캐스팅보트' 역할을 하기 어려워졌다. 결국 정치권은 '득표 효율성'을 앞세워 젠더 갈등과 같은 민감한 문제를 정면으로 다루기보다 회피하려는 경향을 보이게 된 것이다. * 통계청 장래인구 추계 그러나 여기서 멈춰서는 안 된다. 현재의 갈등은 단발성 선거 이슈가 아니라 구조적 변화의 신호일 것이다. 형성기 경험은 세대의 정치 태도를 장기간 고정시키고, 결국 한국 민주주의의 궤적을 바꿀 수 있다. 정치가 이 변화를 제대로 읽지 못하면 젠더 균열은 사회적 갈등으로 고착될 것이고, 반대로 현명하게 대응한다면 다양한 의견이 제도 안으로 수렴되는 계기가 될 것이다. Z세대의 성별 격차는 그저 ‘불편한 현실’이 아니라 미래를 좌우할 핵심 변수이다. 지금 필요한 것은 득표 효율이라는 계산이 아니라, 이 불편한 의제를 정면으로 마주하는 용기이며, 그 용기가 바로 한국 민주주의를 지키는 열쇠가 될 것이다. 읽어주셔서 감사합니다! 혹시나 대통령 토론회 분석 결과도 궁금하다면? © 2025 by 이해찬\n"
|
68 |
+
]
|
69 |
+
},
|
70 |
+
{
|
71 |
+
"name": "stderr",
|
72 |
+
"output_type": "stream",
|
73 |
+
"text": [
|
74 |
+
"You passed along `num_labels=3` with an incompatible id to label map: {'0': 'NEGATIVE', '1': 'POSITIVE'}. The number of labels will be overwritten to 2.\n"
|
75 |
+
]
|
76 |
+
},
|
77 |
+
{
|
78 |
+
"name": "stdout",
|
79 |
+
"output_type": "stream",
|
80 |
+
"text": [
|
81 |
+
"\n",
|
82 |
+
"===== 결과 =====\n",
|
83 |
+
"본문 요약:\n",
|
84 |
+
" 18~29세 청년층에서 남성의 보수화와 여성의 진보화가 마치 거울을 보듯 정반대로 진행되는 '거울상(鏡像) 효과'가 확인되었다.\n",
|
85 |
+
"제목과 본문 요약 유사도: 0.3056\n",
|
86 |
+
"제목과 본문 일치도(Top5 평균): 0.5496\n",
|
87 |
+
"과장점수(log화): 0.301\n",
|
88 |
+
"\n",
|
89 |
+
"최종 제목 주의 점수는 2.18 입니다\n"
|
90 |
+
]
|
91 |
+
}
|
92 |
+
],
|
93 |
+
"source": [
|
94 |
+
"DEVICE = (\"cuda\" if torch.cuda.is_available()\n",
|
95 |
+
" else \"mps\" if getattr(torch.backends, \"mps\", None) and torch.backends.mps.is_available()\n",
|
96 |
+
" else \"cpu\")\n",
|
97 |
+
"SIM_DEVICE = \"cpu\" if DEVICE == \"mps\" else DEVICE\n",
|
98 |
+
"print(f\"[INFO] Gen Device: {DEVICE} | Sim Device: {SIM_DEVICE}\")\n",
|
99 |
+
"\n",
|
100 |
+
"exag = {'가득': 2, '가세': 2, '가속': 2, '강력': 1, '강하다': 1, '거품': 3, '격돌': 1, '격앙': 1, '격차': 1, '경악': 1, '고비': 2, '고삐': 1, '고조': 2, '고지': 3, '고통': 3, '공세': 1, '공포': 1, '과장': 1, '광폭': 2, '광풍': 3, '괴물': 2, '구원투수': 3, '굴욕': 3, '극적': 2, '극찬': 2, '글쎄': 2, '급감': 2, '급등': 2, '급발진': 2, '급속': 2, '기승': 1, '기적': 2, '깜짝': 1, '껑충': 2, '꼴찌': 3, '꼼수': 1, '꽁꽁': 2, '꽂히다': 1, '꿀꺽': 2, '꿈틀': 1, '끔찍': 1, '난리': 2, '난항': 1, '날다': 1, '날벼락': 3, '냉각': 2, '넘치다': 1, '논란': 1, '놀라다': 1, '눈덩이': 2, '눈물': 2, '당장': 2, '대규모': 2, '대란': 3, '대박': 3, '대반전': 2, '대폭': 2, '대환영': 2, '덕분': 1, '돌파구': 2, '돌풍': 4, '뒷걸음질': 2, '뒷북': 3, '든든한': 2, '들썩': 1, '떡락': 3, '떡상': 3, '뚝딱': 2, '뚝뚝': 2, '뜨겁다': 2, '러브콜': 3, '레전드': 4, '막차': 3, '만능': 1, '매우': 2, '맵다': 2, '멘붕': 2, '몸살': 3, '무더기': 2, '급물살': 1, '뭇매': 2, '뭉칫돈': 2, '밉다': 3, '바람': 2, '박살': 3, '반전': 1, '반짝': 2, '발칵': 1, '방긋': 2, '방점': 2, '배신': 3, '벌써': 1, '벼랑': 3, '봇물': 2, '부담': 1, '분노': 3, '분수령': 2, '불가피': 1, '불과': 2, '불금': 2, '불기둥': 2, '불꽃': 2, '불똥': 2, '불씨': 1, '불안하다': 2, '불투명': 1, '불확실': 1, '붕괴': 1, '비명': 3, '뻥튀기': 2, '사상': 2, '상급': 3, '상승': 1, '선방': 1, '설상가상': 3, '성큼': 2, '소름': 1, '속출': 2, '손절': 2, '솔솔': 1, '쇼크': 3, '수백': 2, '수상한': 2, '수혈': 2, '순항': 1, '승기': 3, '시름': 2, '신기록': 2, '실망': 1, '심각': 1, '싹쓸이': 3, '쏟아지다': 1, '쓰리다': 2, '아비규환': 2, '악몽': 3, '악재': 1, '안간힘': 2, '안갯속': 2, '안도': 1, '알짜': 1, '압도적': 3, '압승': 3, '야심작': 3, '얼어붙다': 2, '역대': 2, '역대 최고': 2, '역대 최다': 2, '역대 최소': 2, '역대 최저 ': 2, '역대최고': 2, '역대최다': 2, '역대최소': 2, '역대최저 ': 2, '열풍': 3, '영광': 2, '영웅': 3, '오락가락': 2, '온기': 2, '와르르': 3, '와우': 3, '완패': 3, '외면': 2, '외환위기 이후': 2, '외환 위기 이후': 2, '요동치다': 2, '우뚝': 2, '우려': 1, '울다': 2, '위기급': 4, '위기': 3, '위축': 1, '위태': 2, '위협': 1, '유력': 2, '육박': 2, '의혹': 1, '잔치': 3, '잘나가다': 2, '재난급': 4, '저격': 3, '전격': 1, '전설': 3, '절대': 2, '절벽': 4, '족쇄': 2, '주의보': 2, '줄줄이': 2, '중증': 3, '증발': 2, '직격탄': 2, '진통': 2, '질타': 3, '쪽박': 2, '참담': 2, '척척': 2, '초대형': 2, '초비상': 2, '초유': 2, '초토화': 2, '촉각': 2, '최대': 2, '최상': 2, '최선': 2, '최악': 2, '최애': 2, '최저': 2, '최적': 1, '최초': 2, '최후': 2, '추락': 4, '출혈': 2, '충격': 1, '코앞': 3, '털썩': 2, '톡톡': 2, '투톱': 3, '특급': 4, '파격': 1, '편법': 1, '폭락': 3, '폭발': 2, '폭주': 2, '폭증': 2, '폭탄': 2, '폭풍': 2, '하락': 1, '한숨': 2, '함박': 3, '함정': 2, '허리띠': 1, '헌정 사상': 2, '헌정사상': 2, '혁명': 2, '호소': 1, '호평일색': 2, '호평 일색': 2, '호황': 3, '혼돈': 2, '홈런': 2, '확대': 1, '활기': 2, '활발': 1, '활짝': 2, '활활': 2, '후끈': 2, '훨훨': 2, '휩쓸다': 2, '흔들다': 2, 'imf 이후': 2, '역대급': 4, '무궁무진': 2, '1보': 1, '2보': 1, '3보': 1, '단독': 1, '속보': 1, '패닉': 3, '불패': 3, '제동': 2, '조짐': 1, '초긴장': 2, '급제동': 2, '뚝': 2, '복병': 2, '아우성': 3, '좌불안석': 3, '빈손': 2, '대세': 3, '생트집': 3, '주춤': 2, '끄덕': 2, '맞불': 2, '장벽': 2, '썰렁': 2, '먹구름': 3, '부메랑': 2, '롤러코스터': 2, '발목': 2, '반토막': 2, '휘청': 2, '곤두박질': 3, '울상': 2, '위풍당당': 3, '싸늘': 2, '주저': 1, '우수수': 2, '골머리': 2, '공화국': 3, '고공행진': 4}\n",
|
101 |
+
"econ_list = ['(?<![가-힣])정부(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])한국은행(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])기준금리(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])인플레이션(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디스인플레이션(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])환율(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])재정적자(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])국채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])세제개편(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지[\\\\s-]?지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지[\\\\s-]?지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지[\\\\s-]?지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])복지\\\\-지출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])긴축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])확장재정(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])통화정책(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])금통위(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기[\\\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기[\\\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기[\\\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기\\\\-둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기침체(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경기반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])잠재성장률(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])생산성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])배터리(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])데이터센터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])AI(?![A-Za-z])', '(?<![A-Za-z])AI(?![A-Za-z])', '(?<![가-힣])인공지능(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])로봇(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])플랫폼(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])빅테크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])스타트업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])구조조정(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])M\\\\&A(?![A-Za-z])', '(?<![A-Za-z])IPO(?![A-Za-z])', '(?<![A-Za-z])IPO(?![A-Za-z])', '(?<![가-힣])상장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])리콜(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])리쇼어링\\\\(reshoring\\\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공급망\\\\(supply\\\\ chain\\\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])중국리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])최저임금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고용지표(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])비정규직(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노동시간(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노동[\\\\s-]?시간(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노동[\\\\s-]?시간(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주거비(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계부채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\\\s-]?부채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\\\s-]?부채(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주담대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연체율(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])파산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자영업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년[\\\\s-]?실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년[\\\\s-]?실업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])여성고용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])여성[\\\\s-]?고용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])여성[\\\\s-]?고용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근로시간제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])증시(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])코스피(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])코스닥(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])채권(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])은행(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예대금리차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예대[\\\\s-]?금리차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예대[\\\\s-]?금리차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])예금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])유동성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])PF(?![A-Za-z])', '(?<![A-Za-z])PF(?![A-Za-z])', '(?<![가-힣])프로젝트파이낸싱(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])증권사(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자본확충(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자본[\\\\s-]?확충(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자본[\\\\s-]?확충(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공매도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])ETF(?![A-Za-z])', '(?<![가-힣])디지털자산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디지털[\\\\s-]?자산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디지털[\\\\s-]?자산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])암호화폐(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])스테이블코인(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])규제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])부동산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택공급(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택[\\\\s-]?공급(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택[\\\\s-]?공급(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])분양가상한제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])분양가[\\\\s-]?상한제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])분양가[\\\\s-]?상한제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])재건축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])재개발(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])용도지역(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신도시(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])역세권(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공임대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\\\s-]?임대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\\\s-]?임대(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])토지거래허가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣A-Za-z])건설사\\\\s*PF(?![가-힣A-Za-z])', '(?<![가-힣A-Za-z])건설사\\\\s*PF(?![가-힣A-Za-z])', '(?<![가-힣A-Za-z])건설사\\\\s*PF(?![가-힣A-Za-z])', '(?<![A-Za-z])SOC(?![A-Za-z])', '(?<![가-힣])교통망(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])월세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])외식물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])생활물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])국제유가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])곡물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기[\\\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전기[\\\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가스요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가스[\\\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가스[\\\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공[\\\\s-]?요금(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전력시장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전력[\\\\s-]?시장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전력[\\\\s-]?시장(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])SMP(?![A-Za-z])', '(?<![A-Za-z])SMP(?![A-Za-z])', '(?<![가-힣])전력도매가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원전(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])태양광(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])풍력(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수소(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])탄소배출권\\\\(ETS\\\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])RE100(?![A-Za-z])', '(?<![가-힣])수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])무역수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])무역[\\\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])무역[\\\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])달러인덱스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])달러[\\\\s-]?인덱스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])달러[\\\\s-]?인덱스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\\\s-]?약세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\\\s-]?약세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\\\s-]?약세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\\\s-]?강세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\\\s-]?강세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])원화[\\\\s-]?강세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])통상마찰(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])관세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대미\\\\(IRA\\\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대EU\\\\(CBAM\\\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대중수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대중[\\\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])대중[\\\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체[\\\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])반도체[\\\\s-]?수출(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])프렌드쇼어링\\\\(friend\\\\-shoring\\\\)(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])오픈뱅킹(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])오픈[\\\\s-]?뱅킹(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])오픈[\\\\s-]?뱅킹(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])핀테크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])마이데이터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])마이[\\\\s-]?데이터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])마이[\\\\s-]?데이터(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디지털세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])규제샌드박스(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])클라우드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])SaaS(?![A-Za-z])', '(?<![가-힣])데이터경제(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])개인정보(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])양극화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])자산격차(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소득분배(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])청년부담(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])노인빈곤(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])세대갈등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])지역균형(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])지방소멸(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주거불안(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])시스템리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])시스템[\\\\s-]?리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])시스템[\\\\s-]?리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])그림자금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])그림자[\\\\s-]?금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])그림자[\\\\s-]?금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])역전세(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연쇄부도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연쇄[\\\\s-]?부도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])연쇄[\\\\s-]?부도(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])디폴트(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용스프레드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용[\\\\s-]?스프레드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용[\\\\s-]?스프레드(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])CDS프리미엄(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])신용경색(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고금리[\\\\s-]?장기화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고금리[\\\\s-]?장기화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고금리[\\\\s-]?장기화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계부채\\\\ 관리\\\\ 강화(?![가-힣])', '(?<![가-힣])부동산\\\\ PF\\\\ 부실(?![가-힣])', '(?<![가-힣])공공요금[\\\\s-]?인상(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공요금[\\\\s-]?인상(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])공공요금[\\\\s-]?인상(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\\\s-]?반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\\\s-]?반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\\\s-]?반등(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출[\\\\s-]?둔화(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])정책[\\\\s-]?불확실성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])정책[\\\\s-]?불확실성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])정책[\\\\s-]?불확실성(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])관치금융(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])밸류업(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금\\\\-물가[\\\\s-]?악순환(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금\\\\-물가[\\\\s-]?악순환(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])임금\\\\-물가[\\\\s-]?악순환(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])투자[\\\\s-]?위축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])투자[\\\\s-]?위축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])투자[\\\\s-]?위축(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])인바운드\\\\ 관광\\\\ 회복(?![가-힣])', '(?<![가-힣])기후리스크(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])CPI(?![A-Za-z])', '(?<![A-Za-z])CPI(?![A-Za-z])', '(?<![가-힣])소비자[\\\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소비자[\\\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소비자[\\\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근원물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근원[\\\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])근원[\\\\s-]?물가(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])PPI(?![A-Za-z])', '(?<![A-Za-z])PMI(?![A-Za-z])', '(?<![A-Za-z])GDP(?![A-Za-z])','(?<![A-Za-z])IPI(?![A-Za-z])', '(?<![A-Za-z])IPI(?![A-Za-z])', '(?<![가-힣])광공업[\\\\s-]?생산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])광공업[\\\\s-]?생산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])광공업[\\\\s-]?생산(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])소비자심리지수(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])기대인플레(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])고용동향(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계신용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\\\s-]?신용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])가계[\\\\s-]?신용(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])경상[\\\\s-]?수지(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출입[\\\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출입[\\\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])수출입[\\\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])주택가격지수(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])전세가격지수(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양[\\\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])미분양[\\\\s-]?통계(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![A-Za-z])FOMC(?![A-Za-z])', '(?<![A-Za-z])ECB(?![A-Za-z])', '(?<![A-Za-z])BOJ(?![A-Za-z])', '(?<![가-힣])금통위(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])OPEC\\\\+[\\\\s-]?회의(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])OPEC\\\\+[\\\\s-]?회의(?:은|는|이|가|을|를)?(?![가-힣])', '(?<![가-힣])OPEC\\\\+[\\\\s-]?회의(?:은|는|이|가|을|를)?(?![가-힣])']\n",
|
102 |
+
"\n",
|
103 |
+
"RE_BULLETS = re.compile(r\"[■◆◇]\")\n",
|
104 |
+
"RE_GUIDE = re.compile(r\"^\\*.*$|^※.*$\", flags=re.MULTILINE)\n",
|
105 |
+
"RE_ROLES = re.compile(r\"^(진행|앵커|출연)\\s*:\\s*.*$\", flags=re.MULTILINE)\n",
|
106 |
+
"RE_EMAIL = re.compile(r\"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\")\n",
|
107 |
+
"RE_EXTRA = re.compile(r\"대담 발췌\\s*:\\s*.*$\", flags=re.MULTILINE)\n",
|
108 |
+
"RE_MULTINL = re.compile(r\"\\n+\")\n",
|
109 |
+
"RE_LSTRIP = re.compile(r\"^\\s+\", flags=re.MULTILINE)\n",
|
110 |
+
"\n",
|
111 |
+
"def preprocess_text(text: str) -> str:\n",
|
112 |
+
" if not isinstance(text, str): \n",
|
113 |
+
" return \"\"\n",
|
114 |
+
" text = RE_BULLETS.sub(\"\", text)\n",
|
115 |
+
" text = RE_GUIDE.sub(\"\", text)\n",
|
116 |
+
" text = RE_ROLES.sub(\"\", text)\n",
|
117 |
+
" text = RE_EMAIL.sub(\"\", text)\n",
|
118 |
+
" text = RE_EXTRA.sub(\"\", text)\n",
|
119 |
+
" text = RE_MULTINL.sub(\"\\n\", text).strip()\n",
|
120 |
+
" text = RE_LSTRIP.sub(\"\", text)\n",
|
121 |
+
" return text\n",
|
122 |
+
"\n",
|
123 |
+
"def sentence_split(text: str):\n",
|
124 |
+
" if not isinstance(text, str):\n",
|
125 |
+
" text = \"\" if text is None else str(text)\n",
|
126 |
+
" text = text.replace(\"\\n\", \".\")\n",
|
127 |
+
" text = re.sub(r\"\\.{2,}\", \".\", text)\n",
|
128 |
+
" return [s.strip() for s in text.split(\"다.\") if s.strip()]\n",
|
129 |
+
"\n",
|
130 |
+
"def top5_title_body_sim(title: str, body_text: str, sbert) -> float:\n",
|
131 |
+
" sents = sentence_split(body_text)\n",
|
132 |
+
" if not sents:\n",
|
133 |
+
" return float(\"nan\")\n",
|
134 |
+
" title_emb = sbert.encode(title, convert_to_tensor=True, normalize_embeddings=True)\n",
|
135 |
+
" sent_embs = sbert.encode(sents, convert_to_tensor=True, normalize_embeddings=True)\n",
|
136 |
+
" sims = util.pytorch_cos_sim(title_emb, sent_embs)[0].detach().cpu().numpy().tolist()\n",
|
137 |
+
" sims.sort(reverse=True)\n",
|
138 |
+
" return float(np.mean(sims[:5])) if sims else float(\"nan\")\n",
|
139 |
+
"\n",
|
140 |
+
"# 필요한 모델 불러오기\n",
|
141 |
+
"_tok = _bart = _sbert = None\n",
|
142 |
+
"def load_models():\n",
|
143 |
+
" global _tok, _bart, _sbert\n",
|
144 |
+
" if _tok is None or _bart is None:\n",
|
145 |
+
" _tok = PreTrainedTokenizerFast.from_pretrained(\"digit82/kobart-summarization\")\n",
|
146 |
+
" if _tok.pad_token is None: \n",
|
147 |
+
" _tok.pad_token = _tok.eos_token\n",
|
148 |
+
" _tok.model_max_length = 1024\n",
|
149 |
+
" _bart = BartForConditionalGeneration.from_pretrained(\"digit82/kobart-summarization\")\n",
|
150 |
+
" _bart.eval().to(DEVICE)\n",
|
151 |
+
" if DEVICE == \"cuda\":\n",
|
152 |
+
" try: \n",
|
153 |
+
" _bart.half()\n",
|
154 |
+
" except Exception:\n",
|
155 |
+
" pass\n",
|
156 |
+
" if _sbert is None:\n",
|
157 |
+
" _sbert = SentenceTransformer(\"snunlp/KR-SBERT-V40K-klueNLI-augSTS\", device=SIM_DEVICE)\n",
|
158 |
+
" return _tok, _bart, _sbert\n",
|
159 |
+
"\n",
|
160 |
+
"@torch.inference_mode()\n",
|
161 |
+
"def summarize(tok, model, text: str, max_new_tokens: int = 160) -> str:\n",
|
162 |
+
" if not text: \n",
|
163 |
+
" return \"\"\n",
|
164 |
+
" enc = tok(text, return_tensors=\"pt\", truncation=True, max_length=1024, padding=False)\n",
|
165 |
+
" out = model.generate(\n",
|
166 |
+
" input_ids=enc[\"input_ids\"].to(DEVICE),\n",
|
167 |
+
" attention_mask=enc[\"attention_mask\"].to(DEVICE),\n",
|
168 |
+
" max_new_tokens=max_new_tokens,\n",
|
169 |
+
" num_beams=4,\n",
|
170 |
+
" no_repeat_ngram_size=3,\n",
|
171 |
+
" length_penalty=1.0,\n",
|
172 |
+
" early_stopping=True,\n",
|
173 |
+
" use_cache=True\n",
|
174 |
+
" )\n",
|
175 |
+
" return tok.decode(out[0], skip_special_tokens=True)\n",
|
176 |
+
"\n",
|
177 |
+
"# 과장 표현 정규화 예외 설정\n",
|
178 |
+
"NORM_RULES = [\n",
|
179 |
+
" (r'외환\\s*위기\\s*이후', '외환위기이후'),\n",
|
180 |
+
" (r'IMF\\s*이후', 'IMF이후'),\n",
|
181 |
+
" (r'imf\\s*이후', 'imf이후'),\n",
|
182 |
+
" (r'IMF\\s*급', 'IMF급'),\n",
|
183 |
+
" (r'imf\\s*급', 'imf급'),\n",
|
184 |
+
" (r'호평\\s*일색', '호평일색'),\n",
|
185 |
+
" (r'헌정\\s*사상', '헌정사상'),\n",
|
186 |
+
" (r'역대\\s*최고', '역대최고'),\n",
|
187 |
+
" (r'역대\\s*최다', '역대최다'),\n",
|
188 |
+
" (r'역대\\s*최소', '역대최소'),\n",
|
189 |
+
" (r'역대\\s*최저', '역대최저'),\n",
|
190 |
+
"]\n",
|
191 |
+
"USER_TERMS = [\n",
|
192 |
+
" '대반전',\n",
|
193 |
+
" '외환위기이후',\n",
|
194 |
+
" '위기급', '재난급', '급물살',\n",
|
195 |
+
" 'IMF이후', 'imf이후',\n",
|
196 |
+
" 'IMF급', 'imf급',\n",
|
197 |
+
" '역대최고', '역대최다', '역대최소', '역대최저',\n",
|
198 |
+
" '역대급',\n",
|
199 |
+
" '떡상', '떡락',\n",
|
200 |
+
" '호평일색',\n",
|
201 |
+
" '헌정사상',\n",
|
202 |
+
"]\n",
|
203 |
+
"def normalize_expressions(text: str) -> str:\n",
|
204 |
+
" t = text if isinstance(text, str) else \"\"\n",
|
205 |
+
" for pat, rep in NORM_RULES:\n",
|
206 |
+
" t = re.sub(pat, rep, t)\n",
|
207 |
+
" return t\n",
|
208 |
+
"\n",
|
209 |
+
"_score_map: Dict[str, int] = None\n",
|
210 |
+
"_unique_expr: List[str] = None\n",
|
211 |
+
"_lex_pats: List[re.Pattern] = None\n",
|
212 |
+
"_kiwi = None\n",
|
213 |
+
"\n",
|
214 |
+
"def _load_label_score_map_from_dict(exag_dict: Dict[str, int]) -> Tuple[Dict[str,int], List[str]]:\n",
|
215 |
+
" \"\"\" exag 딕셔너리에서 점수 맵/표현 리스트 생성 \"\"\"\n",
|
216 |
+
" score_map: Dict[str, int] = {}\n",
|
217 |
+
" for k, v in (exag_dict or {}).items():\n",
|
218 |
+
" key = re.sub(r\"\\s+\", \"\", str(k)).strip()\n",
|
219 |
+
" try:\n",
|
220 |
+
" val = int(v)\n",
|
221 |
+
" except Exception:\n",
|
222 |
+
" val = 0\n",
|
223 |
+
" if key:\n",
|
224 |
+
" if key in score_map:\n",
|
225 |
+
" score_map[key] = max(score_map[key], val)\n",
|
226 |
+
" else:\n",
|
227 |
+
" score_map[key] = val\n",
|
228 |
+
" unique_expr = sorted(score_map.keys())\n",
|
229 |
+
" return score_map, unique_expr\n",
|
230 |
+
"\n",
|
231 |
+
"def _compile_patterns_from_list(regex_list: List[str]) -> List[re.Pattern]:\n",
|
232 |
+
" \"\"\" econ_list 문자열 배열에서 정규식 패턴 컴파일 \"\"\"\n",
|
233 |
+
" pats: List[re.Pattern] = []\n",
|
234 |
+
" for p in (regex_list or []):\n",
|
235 |
+
" if not isinstance(p, str):\n",
|
236 |
+
" continue\n",
|
237 |
+
" pat = p.strip()\n",
|
238 |
+
" if not pat:\n",
|
239 |
+
" continue\n",
|
240 |
+
" try:\n",
|
241 |
+
" pats.append(re.compile(pat, re.I))\n",
|
242 |
+
" except re.error:\n",
|
243 |
+
" # 잘못된 패턴은 무시\n",
|
244 |
+
" pass\n",
|
245 |
+
" return pats\n",
|
246 |
+
"\n",
|
247 |
+
"def _build_kiwi(unique_expr: List[str]):\n",
|
248 |
+
" \"\"\" Kiwi > Okt > regex 순으로 형태소/토큰 추출기 준비 \"\"\"\n",
|
249 |
+
" # 1) kiwipiepy\n",
|
250 |
+
" try:\n",
|
251 |
+
" from kiwipiepy import Kiwi\n",
|
252 |
+
" kiwi = Kiwi()\n",
|
253 |
+
" for w in USER_TERMS:\n",
|
254 |
+
" kiwi.add_user_word(w, 'NNG', 10)\n",
|
255 |
+
" for w in unique_expr:\n",
|
256 |
+
" if isinstance(w, str) and len(w) >= 2:\n",
|
257 |
+
" kiwi.add_user_word(w, 'NNG', 9)\n",
|
258 |
+
" return kiwi, \"kiwi\"\n",
|
259 |
+
" except Exception:\n",
|
260 |
+
" pass\n",
|
261 |
+
" # 2) konlpy Okt\n",
|
262 |
+
" try:\n",
|
263 |
+
" from konlpy.tag import Okt\n",
|
264 |
+
" _okt = Okt()\n",
|
265 |
+
" def _okt_extract(text: str):\n",
|
266 |
+
" norm = normalize_expressions(text)\n",
|
267 |
+
" # 명사/동사만\n",
|
268 |
+
" return [w for w, t in _okt.pos(norm, norm=True, stem=True) if t in (\"Noun\",\"Verb\")]\n",
|
269 |
+
" return _okt_extract, \"okt\"\n",
|
270 |
+
" except Exception:\n",
|
271 |
+
" # 3) 정규식 토큰 나누기\n",
|
272 |
+
" def _regex_extract(text: str):\n",
|
273 |
+
" norm = normalize_expressions(text)\n",
|
274 |
+
" return re.findall(r\"[가-힣A-Za-z0-9]+\", norm)\n",
|
275 |
+
" return _regex_extract, \"regex\"\n",
|
276 |
+
"\n",
|
277 |
+
"def _ensure_resources():\n",
|
278 |
+
" global _score_map, _unique_expr, _lex_pats, _kiwi\n",
|
279 |
+
" try:\n",
|
280 |
+
" exag_dict = exag \n",
|
281 |
+
" except NameError:\n",
|
282 |
+
" raise RuntimeError(\"exag 딕셔너리가 정의되어 있지 않습니다. exag = {'표현': 점수, ...} 형태로 먼저 정의하세요.\")\n",
|
283 |
+
" if _score_map is None or _unique_expr is None:\n",
|
284 |
+
" _score_map, _unique_expr = _load_label_score_map_from_dict(exag_dict)\n",
|
285 |
+
" if _lex_pats is None:\n",
|
286 |
+
" try:\n",
|
287 |
+
" econ = econ_list \n",
|
288 |
+
" except NameError:\n",
|
289 |
+
" econ = []\n",
|
290 |
+
" _lex_pats = _compile_patterns_from_list(econ)\n",
|
291 |
+
" if _kiwi is None:\n",
|
292 |
+
" _kiwi, _ = _build_kiwi(_unique_expr)\n",
|
293 |
+
"\n",
|
294 |
+
"# 형태소 추출(명사+동사)\n",
|
295 |
+
"def extract_noun_verb_kiwi(text: str) -> List[str]:\n",
|
296 |
+
" _ensure_resources()\n",
|
297 |
+
" norm = normalize_expressions(text)\n",
|
298 |
+
" try:\n",
|
299 |
+
" from kiwipiepy import Kiwi \n",
|
300 |
+
" if isinstance(_kiwi, Kiwi):\n",
|
301 |
+
" toks = []\n",
|
302 |
+
" for tok in _kiwi.tokenize(norm):\n",
|
303 |
+
" tag = tok.tag\n",
|
304 |
+
" if tag.startswith(\"NN\"):\n",
|
305 |
+
" toks.append(tok.form)\n",
|
306 |
+
" elif tag == \"VV\":\n",
|
307 |
+
" toks.append(tok.lemma if tok.lemma else tok.form)\n",
|
308 |
+
" return toks\n",
|
309 |
+
" except Exception:\n",
|
310 |
+
" pass\n",
|
311 |
+
" return _kiwi(norm)\n",
|
312 |
+
"\n",
|
313 |
+
"# 과장 라벨 점수 계산 및 가중치 산출\n",
|
314 |
+
"def _calc_raw_and_count(tokens: List[str]) -> Tuple[int, int]:\n",
|
315 |
+
" _ensure_resources()\n",
|
316 |
+
" if not isinstance(tokens, (list, tuple)):\n",
|
317 |
+
" return 0, 0\n",
|
318 |
+
" toks = [str(t).strip() for t in tokens if (t is not None) and str(t).strip() != \"\"]\n",
|
319 |
+
" joined = \"\".join(toks)\n",
|
320 |
+
" total_count, total_score = 0, 0\n",
|
321 |
+
" for expr, sc in _score_map.items():\n",
|
322 |
+
" c = joined.count(expr) # non-overlapping\n",
|
323 |
+
" if c:\n",
|
324 |
+
" total_count += c\n",
|
325 |
+
" total_score += c * int(sc)\n",
|
326 |
+
" return int(total_score), int(total_count)\n",
|
327 |
+
"\n",
|
328 |
+
"def _bin_label(total_raw: int) -> int:\n",
|
329 |
+
" # (-inf,0] -> 0, [1,2] -> 1, [3,4] -> 2, [5, inf) -> 3\n",
|
330 |
+
" if total_raw <= 0: return 0\n",
|
331 |
+
" if 1 <= total_raw <= 2: return 1\n",
|
332 |
+
" if 3 <= total_raw <= 4: return 2\n",
|
333 |
+
" return 3\n",
|
334 |
+
"\n",
|
335 |
+
"def _weight_by_count(n: int) -> float:\n",
|
336 |
+
" if n == 1: return 1.0\n",
|
337 |
+
" if n == 2: return 1.3\n",
|
338 |
+
" if n == 3: return 1.5\n",
|
339 |
+
" if n >= 4: return 1.7\n",
|
340 |
+
" return 0.0\n",
|
341 |
+
"\n",
|
342 |
+
"def _has_keyword_and_matches(text: str) -> Tuple[bool, List[str]]:\n",
|
343 |
+
" _ensure_resources()\n",
|
344 |
+
" t = text or \"\"\n",
|
345 |
+
" seen, out = set(), []\n",
|
346 |
+
" has_any = False\n",
|
347 |
+
" for pat in _lex_pats:\n",
|
348 |
+
" m = pat.search(t)\n",
|
349 |
+
" if m:\n",
|
350 |
+
" has_any = True\n",
|
351 |
+
" s = m.group(0)\n",
|
352 |
+
" if s not in seen:\n",
|
353 |
+
" seen.add(s)\n",
|
354 |
+
" out.append(s)\n",
|
355 |
+
" return has_any, out\n",
|
356 |
+
"\n",
|
357 |
+
"# 메인 파이프라인\n",
|
358 |
+
"def run_once(title: str, body: str,\n",
|
359 |
+
" short_pass_len: int = 50, max_new_tokens: int = 160):\n",
|
360 |
+
" _ensure_resources()\n",
|
361 |
+
"\n",
|
362 |
+
" # 모델\n",
|
363 |
+
" tok, bart, sbert = load_models()\n",
|
364 |
+
"\n",
|
365 |
+
" # 본문 전처리\n",
|
366 |
+
" body_clean = preprocess_text(body)\n",
|
367 |
+
"\n",
|
368 |
+
" # 요약\n",
|
369 |
+
" if len(body_clean) < short_pass_len:\n",
|
370 |
+
" summ = body_clean\n",
|
371 |
+
" else:\n",
|
372 |
+
" try:\n",
|
373 |
+
" @torch.inference_mode()\n",
|
374 |
+
" def _summarize(tok, model, text, max_new_tokens=160):\n",
|
375 |
+
" enc = tok(text, return_tensors=\"pt\", truncation=True, max_length=1024, padding=False)\n",
|
376 |
+
" out = model.generate(\n",
|
377 |
+
" input_ids=enc[\"input_ids\"].to(DEVICE),\n",
|
378 |
+
" attention_mask=enc[\"attention_mask\"].to(DEVICE),\n",
|
379 |
+
" max_new_tokens=max_new_tokens,\n",
|
380 |
+
" num_beams=4,\n",
|
381 |
+
" no_repeat_ngram_size=3,\n",
|
382 |
+
" length_penalty=1.0,\n",
|
383 |
+
" early_stopping=True,\n",
|
384 |
+
" use_cache=True\n",
|
385 |
+
" )\n",
|
386 |
+
" return tok.decode(out[0], skip_special_tokens=True)\n",
|
387 |
+
" summ = _summarize(tok, bart, body_clean, max_new_tokens=max_new_tokens)\n",
|
388 |
+
" except Exception as e:\n",
|
389 |
+
" print(f\"[WARN] summarization failed: {e}\")\n",
|
390 |
+
" summ = \"\"\n",
|
391 |
+
"\n",
|
392 |
+
" # 유사도\n",
|
393 |
+
" try:\n",
|
394 |
+
" if summ:\n",
|
395 |
+
" tvec = sbert.encode(title, convert_to_tensor=True, normalize_embeddings=True)\n",
|
396 |
+
" svec = sbert.encode(summ, convert_to_tensor=True, normalize_embeddings=True)\n",
|
397 |
+
" sim_sy = float(util.pytorch_cos_sim(tvec, svec).item())\n",
|
398 |
+
" else:\n",
|
399 |
+
" sim_sy = float(\"nan\")\n",
|
400 |
+
" except Exception as e:\n",
|
401 |
+
" print(f\"[WARN] title-summary sim failed: {e}\")\n",
|
402 |
+
" sim_sy = float(\"nan\")\n",
|
403 |
+
"\n",
|
404 |
+
" try:\n",
|
405 |
+
" sim_b5 = top5_title_body_sim(title, body_clean, sbert)\n",
|
406 |
+
" except Exception as e:\n",
|
407 |
+
" print(f\"[WARN] title-body top5 sim failed: {e}\")\n",
|
408 |
+
" sim_b5 = float(\"nan\")\n",
|
409 |
+
"\n",
|
410 |
+
" # 제목 형태소(명사/동사)\n",
|
411 |
+
" try:\n",
|
412 |
+
" title_nv = extract_noun_verb_kiwi(title)\n",
|
413 |
+
" except Exception as e:\n",
|
414 |
+
" print(f\"[WARN] kiwi extract failed: {e}\")\n",
|
415 |
+
" title_nv = re.findall(r\"[가-힣A-Za-z0-9]+\", normalize_expressions(title or \"\"))\n",
|
416 |
+
"\n",
|
417 |
+
" # 라벨 원점수/등장횟수 → 라벨점수 → 가중치 최종점수\n",
|
418 |
+
" raw_score, cnt = _calc_raw_and_count(title_nv)\n",
|
419 |
+
" label_score = _bin_label(raw_score) # 0/1/2/3\n",
|
420 |
+
" weight = _weight_by_count(cnt) # 1.0/1.3/1.5/1.7\n",
|
421 |
+
" label_final = float(label_score) * float(weight) # df['최종점수']\n",
|
422 |
+
"\n",
|
423 |
+
" # 본문 키워드 여부/매칭 → 1.15배\n",
|
424 |
+
" has_kw, matches = _has_keyword_and_matches(body_clean)\n",
|
425 |
+
" exag_score = label_final * (1.15 if has_kw else 1.0) # df[\"과장점수\"]\n",
|
426 |
+
"\n",
|
427 |
+
" # 불일치도 & log10\n",
|
428 |
+
" summary_mismatch = (1 - sim_sy) if not np.isnan(sim_sy) else np.nan\n",
|
429 |
+
" body_mismatch = (1 - sim_b5) if not np.isnan(sim_b5) else np.nan\n",
|
430 |
+
" exag_log10 = float(np.log10(exag_score + 1.0))\n",
|
431 |
+
"\n",
|
432 |
+
" # 최종 기사 점수\n",
|
433 |
+
" if not (np.isnan(summary_mismatch) or np.isnan(body_mismatch)):\n",
|
434 |
+
" final_article_score = round((exag_log10*0.5 + summary_mismatch*0.25 + body_mismatch*0.25) * 5, 2)\n",
|
435 |
+
" else:\n",
|
436 |
+
" final_article_score = np.nan\n",
|
437 |
+
"\n",
|
438 |
+
" return {\n",
|
439 |
+
" \"요약\": summ,\n",
|
440 |
+
" \"요약유사도\": sim_sy,\n",
|
441 |
+
" \"본문 일치도(Top5 평균)\": sim_b5,\n",
|
442 |
+
" \"title_nv\": title_nv, #형태소 분석된 제목 리스트 \n",
|
443 |
+
" \"원점수\": raw_score, #과장 표현 원점수\n",
|
444 |
+
" \"등장횟수\": cnt,\n",
|
445 |
+
" \"라벨점수\": int(label_score),\n",
|
446 |
+
" \"가중치\": float(weight),\n",
|
447 |
+
" \"라벨최종점수\": float(label_final), \n",
|
448 |
+
" \"has_keyword\": bool(has_kw), #가중 키워드 본문 포함 여부\n",
|
449 |
+
" \"matches\": matches, # 가중 키워드로 선정된 키워드 리스트\n",
|
450 |
+
" \"과장점수\": float(exag_score),\n",
|
451 |
+
" \"과장점수_log10\": exag_log10, #과장 최종 점수\n",
|
452 |
+
" \"요약 불일치도\": summary_mismatch,\n",
|
453 |
+
" \"본문 불일치도\": body_mismatch,\n",
|
454 |
+
" \"최종 기사 점수\": final_article_score\n",
|
455 |
+
" }\n",
|
456 |
+
"\n",
|
457 |
+
"def run_cli():\n",
|
458 |
+
" print(\"제목을 입력하세요:\")\n",
|
459 |
+
" title = input().strip()\n",
|
460 |
+
" print(\"본문을 입력하세요:\")\n",
|
461 |
+
" body = input().strip()\n",
|
462 |
+
" r = run_once(title, body)\n",
|
463 |
+
" print(\"\\n===== 결과 =====\")\n",
|
464 |
+
" print(\"본문 요약:\\n\", r[\"요약\"])\n",
|
465 |
+
" print(\"제목과 본문 요약 유사도:\", round(r[\"요약유사도\"], 4))\n",
|
466 |
+
" print(\"제목과 본문 일치도(Top5 평균):\", round(r[\"본문 일치도(Top5 평균)\"], 4))\n",
|
467 |
+
" print(\"과장점수(log화):\", round(r[\"과장점수_log10\"], 4))\n",
|
468 |
+
" print(\"\\n최종 제목 주의 점수는\", r[\"최종 기사 점수\"], \"입니다\")\n",
|
469 |
+
"\n",
|
470 |
+
"def run_ui():\n",
|
471 |
+
" import gradio as gr\n",
|
472 |
+
" def predict(title, body):\n",
|
473 |
+
" r = run_once(title, body)\n",
|
474 |
+
" return (\n",
|
475 |
+
" r[\"요약\"],\n",
|
476 |
+
" r[\"요약유사도\"],\n",
|
477 |
+
" r[\"본문 일치도(Top5 평균)\"],\n",
|
478 |
+
" r[\"과장점수\"],\n",
|
479 |
+
" r[\"최종 기사 점수\"],\n",
|
480 |
+
" )\n",
|
481 |
+
" demo = gr.Interface(\n",
|
482 |
+
" fn=predict,\n",
|
483 |
+
" inputs=[gr.Textbox(label=\"제목\", lines=2),\n",
|
484 |
+
" gr.Textbox(label=\"본문\", lines=18, placeholder=\"여기에 기사 본문을 붙여넣으세요\")],\n",
|
485 |
+
" outputs=[gr.Textbox(label=\"요약\", lines=10),\n",
|
486 |
+
" gr.Number(label=\"요약유사도\"),\n",
|
487 |
+
" gr.Number(label=\"본문 일치도(Top5 평균)\"),\n",
|
488 |
+
" gr.Number(label=\"과장점수\"),\n",
|
489 |
+
" gr.Number(label=\"최종 기사 점수\")],\n",
|
490 |
+
" title=\"뉴스 요약·유사도·과장점수·최종점수\",\n",
|
491 |
+
" description=\"제목/본문을 입력하면 요약, ���사도, 과장 점수 및 최종 기사 점수를 계산합니다.\"\n",
|
492 |
+
" )\n",
|
493 |
+
" demo.launch()\n",
|
494 |
+
"\n",
|
495 |
+
"if __name__ == \"__main__\":\n",
|
496 |
+
" parser = argparse.ArgumentParser()\n",
|
497 |
+
" parser.add_argument(\"--ui\", action=\"store_true\", help=\"Gradio UI 실행\")\n",
|
498 |
+
" args, _ = parser.parse_known_args() # unknown args(-f ...) 무시\n",
|
499 |
+
" if args.ui:\n",
|
500 |
+
" run_ui()\n",
|
501 |
+
" else:\n",
|
502 |
+
" run_cli()"
|
503 |
+
]
|
504 |
+
},
|
505 |
+
{
|
506 |
+
"cell_type": "code",
|
507 |
+
"execution_count": null,
|
508 |
+
"id": "a8992005-42e4-4651-a038-cc2f8a31af9e",
|
509 |
+
"metadata": {},
|
510 |
+
"outputs": [],
|
511 |
+
"source": []
|
512 |
+
},
|
513 |
+
{
|
514 |
+
"cell_type": "code",
|
515 |
+
"execution_count": null,
|
516 |
+
"id": "ff8e45fd-920d-4250-a1e9-56ee7f67c041",
|
517 |
+
"metadata": {},
|
518 |
+
"outputs": [],
|
519 |
+
"source": []
|
520 |
+
}
|
521 |
+
],
|
522 |
+
"metadata": {
|
523 |
+
"kernelspec": {
|
524 |
+
"display_name": "Python 3 (ipykernel)",
|
525 |
+
"language": "python",
|
526 |
+
"name": "python3"
|
527 |
+
},
|
528 |
+
"language_info": {
|
529 |
+
"codemirror_mode": {
|
530 |
+
"name": "ipython",
|
531 |
+
"version": 3
|
532 |
+
},
|
533 |
+
"file_extension": ".py",
|
534 |
+
"mimetype": "text/x-python",
|
535 |
+
"name": "python",
|
536 |
+
"nbconvert_exporter": "python",
|
537 |
+
"pygments_lexer": "ipython3",
|
538 |
+
"version": "3.11.9"
|
539 |
+
}
|
540 |
+
},
|
541 |
+
"nbformat": 4,
|
542 |
+
"nbformat_minor": 5
|
543 |
+
}
|