leewatson commited on
Commit
d445415
ยท
verified ยท
1 Parent(s): c977434

Upload 2 files

Browse files
Files changed (2) hide show
  1. utils/emotion_utils.py +82 -0
  2. utils/prediction_utils.py +83 -0
utils/emotion_utils.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/emotion_utils.py
2
+ import re
3
+ import math
4
+ import numpy as np
5
+ from collections import defaultdict
6
+
7
+ LABELS = ['๋ถˆํ‰/๋ถˆ๋งŒ', 'ํ™˜์˜/ํ˜ธ์˜', '๊ฐ๋™/๊ฐํƒ„', '์ง€๊ธ‹์ง€๊ธ‹', '๊ณ ๋งˆ์›€', '์Šฌํ””',
8
+ 'ํ™”๋‚จ/๋ถ„๋…ธ', '์กด๊ฒฝ', '๊ธฐ๋Œ€๊ฐ', '์šฐ์ญ๋Œ/๋ฌด์‹œํ•จ', '์•ˆํƒ€๊นŒ์›€/์‹ค๋ง', '๋น„์žฅํ•จ',
9
+ '์˜์‹ฌ/๋ถˆ์‹ ', '๋ฟŒ๋“ฏํ•จ', 'ํŽธ์•ˆ/์พŒ์ ', '์‹ ๊ธฐํ•จ/๊ด€์‹ฌ', '์•„๊ปด์ฃผ๋Š”', '๋ถ€๋„๋Ÿฌ์›€',
10
+ '๊ณตํฌ/๋ฌด์„œ์›€', '์ ˆ๋ง', 'ํ•œ์‹ฌํ•จ', '์—ญ๊ฒจ์›€/์ง•๊ทธ๋Ÿฌ์›€', '์งœ์ฆ', '์–ด์ด์—†์Œ',
11
+ '์—†์Œ', 'ํŒจ๋ฐฐ/์ž๊ธฐํ˜์˜ค', '๊ท€์ฐฎ์Œ', 'ํž˜๋“ฆ/์ง€์นจ', '์ฆ๊ฑฐ์›€/์‹ ๋‚จ', '๊นจ๋‹ฌ์Œ',
12
+ '์ฃ„์ฑ…๊ฐ', '์ฆ์˜ค/ํ˜์˜ค', 'ํ๋ญ‡ํ•จ(๊ท€์—ฌ์›€/์˜ˆ์จ)', '๋‹นํ™ฉ/๋‚œ์ฒ˜', '๊ฒฝ์•…',
13
+ '๋ถ€๋‹ด/์•ˆ_๋‚ดํ‚ด', '์„œ๋Ÿฌ์›€', '์žฌ๋ฏธ์—†์Œ', '๋ถˆ์Œํ•จ/์—ฐ๋ฏผ', '๋†€๋žŒ', 'ํ–‰๋ณต',
14
+ '๋ถˆ์•ˆ/๊ฑฑ์ •', '๊ธฐ์จ', '์•ˆ์‹ฌ/์‹ ๋ขฐ']
15
+
16
+ NEGATIVE_EMOTIONS = [
17
+ '๋ถˆํ‰/๋ถˆ๋งŒ', '์ง€๊ธ‹์ง€๊ธ‹', '์Šฌํ””', 'ํ™”๋‚จ/๋ถ„๋…ธ', '์˜์‹ฌ/๋ถˆ์‹ ', '๊ณตํฌ/๋ฌด์„œ์›€', '์ ˆ๋ง', 'ํ•œ์‹ฌํ•จ',
18
+ '์—ญ๊ฒจ์›€/์ง•๊ทธ๋Ÿฌ์›€', '์งœ์ฆ', '์–ด์ด์—†์Œ', 'ํŒจ๋ฐฐ/์ž๊ธฐํ˜์˜ค', '๊ท€์ฐฎ์Œ', 'ํž˜๋“ฆ/์ง€์นจ', '์ฃ„์ฑ…๊ฐ',
19
+ '์ฆ์˜ค/ํ˜์˜ค', '๋‹นํ™ฉ/๋‚œ์ฒ˜', '๋ถ€๋‹ด/์•ˆ_๋‚ดํ‚ด', '์„œ๋Ÿฌ์›€', '์žฌ๋ฏธ์—†์Œ'
20
+ ]
21
+
22
+ def parse_dialogue(text: str):
23
+ """
24
+ "ํ™”์ž:๋ฌธ์žฅ" ํ˜•์‹์˜ ์ค„๋‹จ์œ„ ๋Œ€ํ™”๋ฅผ ํŒŒ์‹ฑ
25
+ """
26
+ lines = [ln.strip() for ln in text.strip().split("\n") if ln.strip()]
27
+ pairs = []
28
+ for line in lines:
29
+ m = re.match(r"([^:]+):(.+)", line)
30
+ if m:
31
+ pairs.append((m.group(1).strip(), m.group(2).strip()))
32
+ return pairs
33
+
34
+ def adjusted_score(raw_prob: float, k: float = 5.0) -> float:
35
+ """
36
+ [0,1] ํ™•๋ฅ ์„ ์‚ฌ๋žŒ์ด ์ง๊ด€์ ์œผ๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก 0~100 ์ ์ˆ˜๋กœ ์Šค์ผ€์ผ๋ง
37
+ (logistic stretching)
38
+ """
39
+ return 100.0 / (1.0 + math.exp(-k * (raw_prob - 0.5)))
40
+
41
+ def apply_ema(series, alpha=0.4):
42
+ if not series:
43
+ return []
44
+ smoothed = [series[0]]
45
+ for s in series[1:]:
46
+ smoothed.append(alpha * s + (1 - alpha) * smoothed[-1])
47
+ return smoothed
48
+
49
+ def detect_emotion_spikes(emotion_series, z_thresh=1.8, min_len=5):
50
+ """
51
+ Z-score ๊ธฐ๋ฐ˜ ๊ธ‰๋ณ€ ๊ฐ์ • ํƒ์ง€
52
+ - ํ†ต๊ณ„์  ๋ฐฐ๊ฒฝ: ํ‘œ์ค€์ ์ˆ˜ z = (x - ฮผ) / ฯƒ
53
+ - ๊ถŒ์žฅ z ์ž„๊ณ„: 1.8~2.5 (๋ฐ์ดํ„ฐ ๋ณ€๋™์„ฑ์— ๋”ฐ๋ผ ์กฐ์ •)
54
+ """
55
+ if len(emotion_series) < min_len:
56
+ return []
57
+ mean = float(np.mean(emotion_series))
58
+ std = float(np.std(emotion_series)) + 1e-6
59
+ spikes = []
60
+ for i, v in enumerate(emotion_series):
61
+ z = (v - mean) / std
62
+ if z >= z_thresh:
63
+ spikes.append((i, v, round(z, 2)))
64
+ return spikes
65
+
66
+ def infer_conflict_initiator(dialogue, spikes_by_speaker):
67
+ """
68
+ ๊ฐ„๋‹จํ•œ ๊ทœ์น™๊ธฐ๋ฐ˜ ์œ ๋ฐœ์ž ์ถ”์ •:
69
+ - B์˜ ๊ฐ์ •์ด ๊ธ‰๋ณ€ํ•œ ์‹œ์  idx์˜ ์ง์ „ ๋ฐœํ™”์ž๊ฐ€ A๋ผ๋ฉด, A๊ฐ€ ์œ ๋ฐœ count +1
70
+ - ๊ฐ€์žฅ ๋งŽ์€ ๊ธ‰๋ณ€์„ ์œ ๋ฐœํ•œ ํ™”์ž๋ฅผ '์ถ”์ • ์œ ๋ฐœ์ž'๋กœ ๋ฐ˜ํ™˜
71
+ """
72
+ blame = {}
73
+ for speaker, spikes in spikes_by_speaker.items():
74
+ for (idx, v, z) in spikes:
75
+ if idx == 0:
76
+ continue
77
+ prev_speaker = dialogue[idx - 1][0]
78
+ if prev_speaker != speaker:
79
+ blame[prev_speaker] = blame.get(prev_speaker, 0) + 1
80
+ if not blame:
81
+ return None
82
+ return sorted(blame.items(), key=lambda x: -x[1])[0][0]
utils/prediction_utils.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/prediction_utils.py
2
+ import warnings
3
+ warnings.filterwarnings("ignore")
4
+
5
+ import numpy as np
6
+
7
+ # Prophet ์‹œ๋„, ์‹คํŒจ ์‹œ Holt(์ง€์ˆ˜ ํ‰ํ™œ) ํด๋ฐฑ
8
+ try:
9
+ from prophet import Prophet
10
+ _HAS_PROPHET = True
11
+ except Exception:
12
+ _HAS_PROPHET = False
13
+
14
+ try:
15
+ from statsmodels.tsa.holtwinters import ExponentialSmoothing
16
+ _HAS_HOLT = True
17
+ except Exception:
18
+ _HAS_HOLT = False
19
+
20
+ import pandas as pd
21
+
22
+ def prophet_emotion_forecast(scores, steps=3):
23
+ """
24
+ Prophet ๊ธฐ๋ฐ˜ ์˜ˆ์ธก. ์ž…๋ ฅ: ์ ์ˆ˜ ๋ฆฌ์ŠคํŠธ(0~100)
25
+ ๋ฐ˜ํ™˜: ์˜ˆ์ธก ์ ์ˆ˜ ๋ฆฌ์ŠคํŠธ ๊ธธ์ด=steps (float)
26
+ ์ฐธ๊ณ : Taylor & Letham (2018) 'Forecasting at Scale'
27
+ """
28
+ if not _HAS_PROPHET or len(scores) < 5:
29
+ return None
30
+
31
+ df = pd.DataFrame({
32
+ 'ds': pd.date_range(start='2023-01-01', periods=len(scores), freq='D'),
33
+ 'y': scores
34
+ })
35
+ model = Prophet(
36
+ daily_seasonality=False, weekly_seasonality=False, yearly_seasonality=False,
37
+ changepoint_prior_scale=0.2 # ๋ณ€ํ™”์  ๋ฏผ๊ฐ๋„
38
+ )
39
+ model.fit(df)
40
+ future = model.make_future_dataframe(periods=steps)
41
+ forecast = model.predict(future)
42
+ tail = forecast[['yhat']].tail(steps)['yhat'].to_list()
43
+ # ์•ˆ์ „ ํด๋ฆฌํ•‘
44
+ return [float(np.clip(x, 0, 100)) for x in tail]
45
+
46
+ def holt_fallback_forecast(scores, steps=3):
47
+ """
48
+ Holt ์ง€์ˆ˜ํ‰ํ™œ(์ถ”์„ธํ˜•) ํด๋ฐฑ ์˜ˆ์ธก.
49
+ Prophet ์‚ฌ์šฉ ๋ถˆ๊ฐ€ ํ™˜๊ฒฝ์—์„œ ๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ •์ .
50
+ """
51
+ if not _HAS_HOLT or len(scores) < 4:
52
+ return None
53
+ try:
54
+ model = ExponentialSmoothing(
55
+ np.array(scores, dtype=float),
56
+ trend='add', seasonal=None
57
+ ).fit()
58
+ fcast = model.forecast(steps)
59
+ return [float(np.clip(x, 0, 100)) for x in fcast.tolist()]
60
+ except Exception:
61
+ return None
62
+
63
+ def forecast_scores(scores, steps=3):
64
+ """
65
+ ํ†ตํ•ฉ ์˜ˆ์ธก ์—”์ง„:
66
+ 1) Prophet ์„ฑ๊ณต ์‹œ Prophet ์‚ฌ์šฉ
67
+ 2) ์•„๋‹ˆ๋ฉด Holt ํด๋ฐฑ
68
+ 3) ๊ทธ๋ž˜๋„ ์•ˆ ๋˜๋ฉด, ๋งˆ์ง€๋ง‰ ๊ธฐ์šธ๊ธฐ ์„ ํ˜• ์™ธ์‚ฝ(๊ทน๋‹จ ํด๋ฐฑ)
69
+ """
70
+ if len(scores) < 2:
71
+ return [scores[-1]] * steps if scores else [0.0]*steps
72
+
73
+ yhat = prophet_emotion_forecast(scores, steps=steps)
74
+ if yhat is not None:
75
+ return yhat
76
+
77
+ yhat = holt_fallback_forecast(scores, steps=steps)
78
+ if yhat is not None:
79
+ return yhat
80
+
81
+ # ๋งˆ์ง€๋ง‰ ๋‘ ์ ์˜ ๊ธฐ์šธ๊ธฐ๋กœ ์™ธ์‚ฝ
82
+ slope = scores[-1] - scores[-2]
83
+ return [float(np.clip(scores[-1] + slope*(i+1), 0, 100)) for i in range(steps)]