์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽ™๏ธ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ํ”„๋กœ๊ทธ๋žจ ์†Œ์Šค์ฝ”๋“œ

๐ŸŽ™๏ธ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ํ”„๋กœ๊ทธ๋žจ ์†Œ์Šค์ฝ”๋“œ

๋ชฉ์†Œ๋ฆฌ ์†์— ์ˆจ๊ฒจ์ง„ ๊ฐ์ •์„ AI๋กœ ์ฝ์–ด๋‚ด๋Š” ์™„์ „ ๊ฐ€์ด๋“œ
Python ๋”ฅ๋Ÿฌ๋‹ ์Œ์„ฑ์ฒ˜๋ฆฌ ๊ฐ์ •์ธ์‹

์•ผ, ์†”์งํžˆ ๋งํ•ด๋ด. ๐Ÿ˜„ ์นด์นด์˜คํ†ก ์Œ์„ฑ๋ฉ”์‹œ์ง€ ๋“ค์„ ๋•Œ "์ด ์‚ฌ๋žŒ ์ง€๊ธˆ ํ™”๋‚ฌ๋‚˜?" ์‹ถ์—ˆ๋˜ ์  ์—†์–ด?
์•„๋‹ˆ๋ฉด ๊ณ ๊ฐ์„ผํ„ฐ ํ†ตํ™” ๋…น์Œ ๋“ฃ๋‹ค๊ฐ€ "์ด ๊ณ ๊ฐ ์—„์ฒญ ๋ถˆ๋งŒ์กฑ์Šค๋Ÿฌ์›Œ ๋ณด์ด๋Š”๋ฐ..." ๋А๋‚€ ์ ?

๊ทธ๊ฒŒ ๋ฐ”๋กœ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„(Speech Emotion Recognition, SER)์ด ํ•˜๋Š” ์ผ์ด์•ผ.
์‚ฌ๋žŒ์˜ ๋ชฉ์†Œ๋ฆฌ์—๋Š” ๋‹จ์ˆœํ•œ ๋‹จ์–ด ์ด์ƒ์˜ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ ์žˆ๊ฑฐ๋“ . ํ”ผ์น˜(์Œ๋†’์ด), ์—๋„ˆ์ง€, ๋ฆฌ๋“ฌ, ๋–จ๋ฆผ...
์ด ๋ชจ๋“  ๊ฑธ ์ˆ˜์น˜๋กœ ๋ฝ‘์•„๋‚ด์„œ "์ด ์‚ฌ๋žŒ ์ง€๊ธˆ ์Šฌํ”„๋‹ค" "์ด ์‚ฌ๋žŒ ์‹ ๋‚ฌ๋‹ค" ๋ฅผ AI๊ฐ€ ํŒ๋‹จํ•˜๋Š” ๊ฑฐ์ง€.

์˜ค๋Š˜์€ ์ด๊ฑธ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ์†Œ์Šค์ฝ”๋“œ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๊ฐ™์ด ๋งŒ๋“ค์–ด๋ณผ ๊ฑฐ์•ผ. ๐Ÿš€
์ด๋ก ๋งŒ ๋Š˜์–ด๋†“๋Š” ์ง€๋ฃจํ•œ ๊ธ€ ์•„๋‹ˆ๊ณ , ์ง„์งœ ๋Œ์•„๊ฐ€๋Š” ์ฝ”๋“œ ์œ„์ฃผ๋กœ ์„ค๋ช…ํ• ๊ฒŒ!

์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ ๐ŸŽ™๏ธ ์Œ์„ฑ ์ž…๋ ฅ WAV / MP3 โš™๏ธ ์ „์ฒ˜๋ฆฌ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ / ์ •๊ทœํ™” ๐Ÿ“Š ํŠน์ง• ์ถ”์ถœ MFCC / Mel-Spec ๐Ÿค– AI ๋ชจ๋ธ CNN / LSTM / Transformer ๐Ÿ˜Š ๊ฐ์ • ์ถœ๋ ฅ ๊ธฐ์จ/์Šฌํ””/๋ถ„๋…ธ... ์Œ์„ฑ ํŒŒํ˜• โ†’ ๊ฐ์ • ๋ ˆ์ด๋ธ” ๋ณ€ํ™˜ ๊ณผ์ • ์›๋ณธ ํŒŒํ˜• โ†“ MFCC ํŠน์ง• ์ถ”์ถœ ๊ธฐ์จ 82% ์Šฌํ”” 8% ๋ถ„๋…ธ 5% ๋†€๋žŒ 3% ์ค‘๋ฆฝ 2% โ†’ ๐Ÿ˜Š ๊ธฐ์จ! ์‹ ๋ขฐ๋„: 82%

๐Ÿง  1. ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์ด๋ž€? ํ•ต์‹ฌ ๊ฐœ๋… ์ •๋ฆฌ

๋จผ์ € ๊ฐœ๋…๋ถ€ํ„ฐ ๋น ๋ฅด๊ฒŒ ์žก๊ณ  ๊ฐ€์ž. ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„(SER: Speech Emotion Recognition)์€
์‚ฌ๋žŒ์˜ ๋ชฉ์†Œ๋ฆฌ์—์„œ ๊ฐ์ • ์ƒํƒœ๋ฅผ ์ž๋™์œผ๋กœ ์ธ์‹ํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ.

๋‹จ์ˆœํžˆ "๋ญ๋ผ๊ณ  ๋งํ–ˆ๋ƒ"๋ฅผ ๋ถ„์„ํ•˜๋Š” ์Œ์„ฑ ์ธ์‹(STT)๊ณผ๋Š” ๋‹ฌ๋ฆฌ,
"์–ด๋–ค ๊ฐ์ •์œผ๋กœ ๋งํ–ˆ๋ƒ"๋ฅผ ๋ถ„์„ํ•˜๋Š” ๊ฑฐ์ง€. ์™„์ „ ๋‹ค๋ฅธ ์˜์—ญ์ด์•ผ!

๐Ÿ“Œ SER์ด ํ™œ์šฉ๋˜๋Š” ์‹ค์ œ ๋ถ„์•ผ

๐ŸŽง ์ฝœ์„ผํ„ฐ ํ’ˆ์งˆ ๊ด€๋ฆฌ โ€” ๊ณ ๊ฐ ๋ถˆ๋งŒ ๊ฐ์ง€, ์ƒ๋‹ด์‚ฌ ๊ฐ์ • ๋ชจ๋‹ˆํ„ฐ๋ง
๐ŸŽฎ ๊ฒŒ์ž„/๋ฉ”ํƒ€๋ฒ„์Šค โ€” ํ”Œ๋ ˆ์ด์–ด ๊ฐ์ •์— ๋ฐ˜์‘ํ•˜๋Š” NPC
๐Ÿฅ ์ •์‹ ๊ฑด๊ฐ• ๋ชจ๋‹ˆํ„ฐ๋ง โ€” ์šฐ์šธ์ฆ, ์ŠคํŠธ๋ ˆ์Šค ์กฐ๊ธฐ ๊ฐ์ง€
๐Ÿš— ์ž๋™์ฐจ ์•ˆ์ „ โ€” ์šด์ „์ž ์กธ์Œ/์ŠคํŠธ๋ ˆ์Šค ๊ฐ์ง€
๐Ÿ“ฑ ์Šค๋งˆํŠธ ์Šคํ”ผ์ปค โ€” ์‚ฌ์šฉ์ž ๊ฐ์ •์— ๋งž๋Š” ์‘๋‹ต ์ƒ์„ฑ
๐ŸŽ“ ๊ต์œก ํ”Œ๋žซํผ โ€” ํ•™์Šต์ž ์ง‘์ค‘๋„/ํฅ๋ฏธ ๋ถ„์„

๊ฐ์ •์˜ ์ข…๋ฅ˜๋Š” ์—ฐ๊ตฌ๋งˆ๋‹ค ๋‹ค๋ฅด์ง€๋งŒ, ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ๊ฑด Ekman์˜ 6๊ฐ€์ง€ ๊ธฐ๋ณธ ๊ฐ์ •์ด์•ผ:
๊ธฐ์จ(Happy), ์Šฌํ””(Sad), ๋ถ„๋…ธ(Angry), ๊ณตํฌ(Fear), ํ˜์˜ค(Disgust), ๋†€๋žŒ(Surprise)
์—ฌ๊ธฐ์— ์ค‘๋ฆฝ(Neutral)์„ ์ถ”๊ฐ€ํ•ด์„œ 7๊ฐœ๋กœ ๋ถ„๋ฅ˜ํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด์•ผ.

๐Ÿ’ก ์žฌ๋ฏธ์žˆ๋Š” ์‚ฌ์‹ค! ์‚ฌ๋žŒ์ด ๊ฐ์ •์„ ์ „๋‹ฌํ•  ๋•Œ ์–ธ์–ด ์ž์ฒด๋ณด๋‹ค ๋ชฉ์†Œ๋ฆฌ ํ†ค(38%)๊ณผ ๋น„์–ธ์–ด์  ์š”์†Œ(55%)๊ฐ€ ํ›จ์”ฌ ๋” ํฐ ์˜ํ–ฅ์„ ๋ฏธ์นœ๋‹ค๋Š” ์—ฐ๊ตฌ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์–ด. (Mehrabian์˜ 7-38-55 ๋ฒ•์น™) ๊ทธ๋ž˜์„œ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์ด ํ…์ŠคํŠธ ๊ฐ์ • ๋ถ„์„๋ณด๋‹ค ๋” ํ’๋ถ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์ง€!

๐Ÿ› ๏ธ 2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ… โ€” ๋ญ๊ฐ€ ํ•„์š”ํ•ด?

์ž, ์ด์ œ ์ง„์งœ ์ฝ”๋“œ ์–˜๊ธฐ ์‹œ์ž‘! ๐Ÿ”ฅ
์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ์ฃผ์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด์•ผ. Python ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐˆ ๊ฑฐ์•ผ.

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ญํ•  ๋ฒ„์ „
librosa ์Œ์„ฑ ํŒŒ์ผ ๋กœ๋“œ & ํŠน์ง• ์ถ”์ถœ 0.10.x
numpy ์ˆ˜์น˜ ์—ฐ์‚ฐ 1.24.x
tensorflow / keras ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ ๊ตฌ์ถ• 2.13.x
scikit-learn ๋ฐ์ดํ„ฐ ๋ถ„ํ• , ํ‰๊ฐ€ ์ง€ํ‘œ 1.3.x
soundfile ์˜ค๋””์˜ค ํŒŒ์ผ I/O 0.12.x
matplotlib ์‹œ๊ฐํ™” 3.7.x
pandas ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ 2.0.x

์„ค์น˜๋Š” ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ผ:

pip install librosa numpy tensorflow scikit-learn soundfile matplotlib pandas
๐Ÿ’ก ๊ฐ€์ƒํ™˜๊ฒฝ ๊ผญ ์“ฐ์ž! conda๋‚˜ venv๋กœ ๊ฐ€์ƒํ™˜๊ฒฝ ๋งŒ๋“ค๊ณ  ์‹œ์ž‘ํ•˜๋Š” ๊ฑฐ ๊ฐ•๋ ฅ ์ถ”์ฒœ. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ์ถฉ๋Œ์ด ์ง„์งœ ๊ณจ์น˜ ์•„ํ”„๊ฑฐ๋“ . ํŠนํžˆ TensorFlow๋Š” ๋ฒ„์ „์— ๋ฏผ๊ฐํ•ด!

์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ์…‹์€ RAVDESS (Ryerson Audio-Visual Database of Emotional Speech and Song)์•ผ.
๊ณต๊ฐœ ๋ฐ์ดํ„ฐ์…‹ ์ค‘์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ์˜์–ด ๊ฐ์ • ์Œ์„ฑ ๋ฐ์ดํ„ฐ์…‹์ด๊ณ ,
24๋ช…์˜ ๋ฐฐ์šฐ๊ฐ€ 8๊ฐ€์ง€ ๊ฐ์ •์œผ๋กœ ๋…น์Œํ•œ 1,440๊ฐœ์˜ ์˜ค๋””์˜ค ํŒŒ์ผ์ด ๋“ค์–ด์žˆ์–ด.

ํ•œ๊ตญ์–ด ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ๋Š” KESDy (Korean Emotional Speech Dataset)๋‚˜
ETRI ๊ฐ์ • ์Œ์„ฑ ๋ฐ์ดํ„ฐ์…‹์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ํ•œ๊ตญ์–ด ํ”„๋กœ์ ํŠธ๋ผ๋ฉด ์ด์ชฝ์„ ์จ๋ด!


๐Ÿ“‚ 3. ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ & ์ „์ฒ˜๋ฆฌ ์†Œ์Šค์ฝ”๋“œ

RAVDESS ๋ฐ์ดํ„ฐ์…‹์˜ ํŒŒ์ผ๋ช…์—๋Š” ๊ฐ์ • ๋ ˆ์ด๋ธ”์ด ์ธ์ฝ”๋”ฉ๋˜์–ด ์žˆ์–ด.
์˜ˆ๋ฅผ ๋“ค์–ด 03-01-05-01-01-01-01.wav ์—์„œ ์„ธ ๋ฒˆ์งธ ์ˆซ์ž(05)๊ฐ€ ๊ฐ์ • ์ฝ”๋“œ์•ผ.
์ด๊ฑธ ํŒŒ์‹ฑํ•ด์„œ ๋ ˆ์ด๋ธ”์„ ๋งŒ๋“œ๋Š” ์ฝ”๋“œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์ž!

import os
import glob
import numpy as np
import pandas as pd
import librosa
import soundfile as sf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

# =============================================
# 1. RAVDESS ๊ฐ์ • ๋ ˆ์ด๋ธ” ๋งคํ•‘
# =============================================
EMOTION_MAP = {
'01': 'neutral',    # ์ค‘๋ฆฝ
'02': 'calm',       # ์ฐจ๋ถ„
'03': 'happy',      # ๊ธฐ์จ
'04': 'sad',        # ์Šฌํ””
'05': 'angry',      # ๋ถ„๋…ธ
'06': 'fearful',    # ๊ณตํฌ
'07': 'disgust',    # ํ˜์˜ค
'08': 'surprised'   # ๋†€๋žŒ
}

# =============================================
# 2. ๋ฐ์ดํ„ฐ์…‹ ๊ฒฝ๋กœ์—์„œ ํŒŒ์ผ ๋ชฉ๋ก & ๋ ˆ์ด๋ธ” ์ถ”์ถœ
# =============================================
def load_ravdess_data(data_path):
"""
RAVDESS ๋ฐ์ดํ„ฐ์…‹ ๋กœ๋“œ ํ•จ์ˆ˜
data_path: RAVDESS ํด๋” ๊ฒฝ๋กœ (Actor_01 ~ Actor_24 ํฌํ•จ)
"""
file_paths = []
labels = []

# ๋ชจ๋“  wav ํŒŒ์ผ ํƒ์ƒ‰
for wav_file in glob.glob(os.path.join(data_path, '**', '*.wav'), recursive=True):
filename = os.path.basename(wav_file)
parts = filename.replace('.wav', '').split('-')

# ํŒŒ์ผ๋ช… ํ˜•์‹ ๊ฒ€์ฆ
if len(parts) >= 3:
emotion_code = parts[2]  # ์„ธ ๋ฒˆ์งธ ์š”์†Œ๊ฐ€ ๊ฐ์ • ์ฝ”๋“œ
if emotion_code in EMOTION_MAP:
file_paths.append(wav_file)
labels.append(EMOTION_MAP[emotion_code])

print(f"์ด {len(file_paths)}๊ฐœ ํŒŒ์ผ ๋กœ๋“œ ์™„๋ฃŒ")

# ๊ฐ์ •๋ณ„ ๋ถ„ํฌ ํ™•์ธ
label_series = pd.Series(labels)
print("\n๊ฐ์ •๋ณ„ ๋ฐ์ดํ„ฐ ๋ถ„ํฌ:")
print(label_series.value_counts())

return file_paths, labels

# ์‚ฌ์šฉ ์˜ˆ์‹œ
# file_paths, labels = load_ravdess_data('./RAVDESS')

์ด์ œ ํ•ต์‹ฌ์ธ ํŠน์ง• ์ถ”์ถœ(Feature Extraction) ์ฝ”๋“œ์•ผ.
์Œ์„ฑ์—์„œ ์–ด๋–ค ํŠน์ง•์„ ๋ฝ‘๋А๋ƒ๊ฐ€ ๋ชจ๋ธ ์„ฑ๋Šฅ์„ ์ขŒ์šฐํ•˜๋Š” ํ•ต์‹ฌ์ด๊ฑฐ๋“ ! ๐ŸŽฏ

# =============================================
# 3. ์Œ์„ฑ ํŠน์ง• ์ถ”์ถœ ํ•จ์ˆ˜ (ํ•ต์‹ฌ!)
# =============================================
def extract_features(file_path, sr=22050, duration=3.0):
"""
์Œ์„ฑ ํŒŒ์ผ์—์„œ ๋‹ค์–‘ํ•œ ํŠน์ง•์„ ์ถ”์ถœํ•˜๋Š” ํ•จ์ˆ˜

์ถ”์ถœ ํŠน์ง•:
- MFCC (Mel-Frequency Cepstral Coefficients): 40๊ฐœ
- Mel Spectrogram: 128๊ฐœ
- Chroma Features: 12๊ฐœ
- Spectral Contrast: 7๊ฐœ
- Zero Crossing Rate: 1๊ฐœ
- RMS Energy: 1๊ฐœ
"""
try:
# ์˜ค๋””์˜ค ๋กœ๋“œ (๊ณ ์ • ๊ธธ์ด๋กœ ํŒจ๋”ฉ/์ž๋ฅด๊ธฐ)
audio, sample_rate = librosa.load(
file_path, 
sr=sr, 
duration=duration,
mono=True
)

# ๊ณ ์ • ๊ธธ์ด ๋งž์ถ”๊ธฐ (ํŒจ๋”ฉ ๋˜๋Š” ์ž๋ฅด๊ธฐ)
target_length = int(sr * duration)
if len(audio) < target_length:
# ์งง์œผ๋ฉด 0์œผ๋กœ ํŒจ๋”ฉ
audio = np.pad(audio, (0, target_length - len(audio)))
else:
# ๊ธธ๋ฉด ์ž๋ฅด๊ธฐ
audio = audio[:target_length]

# ---- MFCC ์ถ”์ถœ ----
# ์Œ์„ฑ์˜ ์ŠคํŽ™ํŠธ๋Ÿผ ํฌ๋ฝ์„ ์„ ํ‘œํ˜„, ๊ฐ์ • ์ธ์‹์— ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํŠน์ง•
mfcc = librosa.feature.mfcc(y=audio, sr=sample_rate, n_mfcc=40)
mfcc_mean = np.mean(mfcc, axis=1)      # ์‹œ๊ฐ„์ถ• ํ‰๊ท 
mfcc_std = np.std(mfcc, axis=1)         # ์‹œ๊ฐ„์ถ• ํ‘œ์ค€ํŽธ์ฐจ
mfcc_delta = np.mean(librosa.feature.delta(mfcc), axis=1)  # 1์ฐจ ๋ฏธ๋ถ„

# ---- Mel Spectrogram ----
mel_spec = librosa.feature.melspectrogram(
y=audio, sr=sample_rate, n_mels=128
)
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
mel_mean = np.mean(mel_spec_db, axis=1)

# ---- Chroma Features (์Œ์ • ๊ด€๋ จ) ----
chroma = librosa.feature.chroma_stft(y=audio, sr=sample_rate)
chroma_mean = np.mean(chroma, axis=1)

# ---- Spectral Contrast ----
contrast = librosa.feature.spectral_contrast(y=audio, sr=sample_rate)
contrast_mean = np.mean(contrast, axis=1)

# ---- Zero Crossing Rate (๋ชฉ์†Œ๋ฆฌ ๊ฑฐ์น ๊ธฐ) ----
zcr = librosa.feature.zero_crossing_rate(audio)
zcr_mean = np.mean(zcr)

# ---- RMS Energy (์Œ๋Ÿ‰/์—๋„ˆ์ง€) ----
rms = librosa.feature.rms(y=audio)
rms_mean = np.mean(rms)

# ---- Pitch (F0) ์ถ”์ถœ ----
pitches, magnitudes = librosa.piptrack(y=audio, sr=sample_rate)
pitch_mean = np.mean(pitches[pitches > 0]) if np.any(pitches > 0) else 0
pitch_std = np.std(pitches[pitches > 0]) if np.any(pitches > 0) else 0

# ๋ชจ๋“  ํŠน์ง• ๊ฒฐํ•ฉ
features = np.concatenate([
mfcc_mean,          # 40
mfcc_std,           # 40
mfcc_delta,         # 40
mel_mean,           # 128
chroma_mean,        # 12
contrast_mean,      # 7
[zcr_mean],         # 1
[rms_mean],         # 1
[pitch_mean],       # 1
[pitch_std]         # 1
])
# ์ด ํŠน์ง• ๋ฒกํ„ฐ ํฌ๊ธฐ: 271

return features

except Exception as e:
print(f"ํŠน์ง• ์ถ”์ถœ ์˜ค๋ฅ˜ ({file_path}): {e}")
return None


# =============================================
# 4. ์ „์ฒด ๋ฐ์ดํ„ฐ์…‹ ํŠน์ง• ์ถ”์ถœ
# =============================================
def prepare_dataset(file_paths, labels):
"""์ „์ฒด ๋ฐ์ดํ„ฐ์…‹์˜ ํŠน์ง•์„ ์ถ”์ถœํ•˜๊ณ  ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜"""
X = []
y = []

print("ํŠน์ง• ์ถ”์ถœ ์ค‘...")
for i, (fp, label) in enumerate(zip(file_paths, labels)):
if i % 100 == 0:
print(f"  ์ง„ํ–‰: {i}/{len(file_paths)}")

features = extract_features(fp)
if features is not None:
X.append(features)
y.append(label)

X = np.array(X)
y = np.array(y)

print(f"\nํŠน์ง• ์ถ”์ถœ ์™„๋ฃŒ!")
print(f"X shape: {X.shape}")
print(f"y shape: {y.shape}")

return X, y
๐Ÿ’ก MFCC๊ฐ€ ๋ญ”์ง€ ๊ถ๊ธˆํ•˜์ง€? Mel-Frequency Cepstral Coefficients์˜ ์•ฝ์ž์•ผ. ์‚ฌ๋žŒ์˜ ์ฒญ๊ฐ ํŠน์„ฑ์„ ๋ชจ๋ฐฉํ•œ ์ฃผํŒŒ์ˆ˜ ์Šค์ผ€์ผ(Mel Scale)์—์„œ ์Œ์„ฑ์˜ ์ŠคํŽ™ํŠธ๋Ÿผ ํฌ๋ฝ์„ ์„ ํ‘œํ˜„ํ•˜๋Š” ๊ณ„์ˆ˜๋“ค์ด์•ผ. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด "์ด ๋ชฉ์†Œ๋ฆฌ์˜ ์Œ์ƒ‰ ์ง€๋ฌธ"์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ. ๊ฐ์ • ์ธ์‹์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํŠน์ง•์œผ๋กœ ๊ผฝํ˜€!
MFCC ํŠน์ง• ์‹œ๊ฐํ™” ์˜ˆ์‹œ โ€” ๊ฐ์ •๋ณ„ ํŒจํ„ด ๋น„๊ต ๐Ÿ˜Š ๊ธฐ์จ ๐Ÿ˜ข ์Šฌํ”” ๐Ÿ˜  ๋ถ„๋…ธ ๐Ÿ˜ ์ค‘๋ฆฝ โ†‘ ๊ฐ ํ–‰์€ MFCC ๊ณ„์ˆ˜ 1~8๋ฒˆ, ๋ง‰๋Œ€ ๊ธธ์ด๋Š” ํ•ด๋‹น ๊ณ„์ˆ˜์˜ ํฌ๊ธฐ๋ฅผ ๋‚˜ํƒ€๋ƒ„ ๊ธฐ์จ: ๋†’์€ ์—๋„ˆ์ง€ ๋น ๋ฅธ ํ…œํฌ, ๋†’์€ ํ”ผ์น˜ ํฐ MFCC ๊ฐ’ ๋ถ„ํฌ ์Šฌํ””: ๋‚ฎ์€ ์—๋„ˆ์ง€ ๋А๋ฆฐ ํ…œํฌ, ๋‚ฎ์€ ํ”ผ์น˜ ์ž‘์€ MFCC ๊ฐ’ ๋ถ„ํฌ ๋ถ„๋…ธ: ์ตœ๊ณ  ์—๋„ˆ์ง€ ๋ถˆ๊ทœ์น™ ํŒจํ„ด, ๋†’์€ ZCR ์ตœ๋Œ€ MFCC ๊ฐ’ ๋ถ„ํฌ ์ค‘๋ฆฝ: ๊ท ์ผํ•œ ์—๋„ˆ์ง€ ์ผ์ •ํ•œ ํ…œํฌ/ํ”ผ์น˜ ์ค‘๊ฐ„ MFCC ๊ฐ’ ๋ถ„ํฌ ๊ฐ์ •๋งˆ๋‹ค ์Œ์„ฑ ํŠน์ง• ํŒจํ„ด์ด ๋šœ๋ ทํ•˜๊ฒŒ ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒŒ ํ•ต์‹ฌ! ์ด ํŒจํ„ด ์ฐจ์ด๋ฅผ AI๊ฐ€ ํ•™์Šตํ•ด์„œ ์ƒˆ๋กœ์šด ์Œ์„ฑ์˜ ๊ฐ์ •์„ ์˜ˆ์ธกํ•จ

๐Ÿค– 4. ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ ๊ตฌ์ถ• โ€” CNN + LSTM ํ•˜์ด๋ธŒ๋ฆฌ๋“œ

ํŠน์ง• ์ถ”์ถœ์ด ๋๋‚ฌ์œผ๋ฉด ์ด์ œ ๋ชจ๋ธ์„ ๋งŒ๋“ค ์ฐจ๋ก€์•ผ! ๐ŸŽ‰
์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์—๋Š” ์—ฌ๋Ÿฌ ๋ชจ๋ธ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์“ฐ์ด๋Š”๋ฐ,
์˜ค๋Š˜์€ CNN + LSTM ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด๋ณผ ๊ฑฐ์•ผ.

์™œ ์ด ์กฐํ•ฉ์ด๋ƒ๊ณ ? CNN์€ ๊ณต๊ฐ„์  ํŒจํ„ด(์ŠคํŽ™ํŠธ๋Ÿผ ํ˜•ํƒœ)์„ ์ž˜ ์žก๊ณ ,
LSTM์€ ์‹œ๊ฐ„์  ํŒจํ„ด(์Œ์„ฑ์˜ ํ๋ฆ„)์„ ์ž˜ ์žก๊ฑฐ๋“ . ๋‘˜์˜ ์žฅ์ ์„ ํ•ฉ์นœ ๊ฑฐ์ง€!

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Model
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import classification_report, confusion_matrix
import numpy as np

# =============================================
# 5. ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ (์Šค์ผ€์ผ๋ง & ์ธ์ฝ”๋”ฉ)
# =============================================
def preprocess_data(X, y, test_size=0.2, val_size=0.1):
"""
๋ฐ์ดํ„ฐ ์Šค์ผ€์ผ๋ง, ๋ ˆ์ด๋ธ” ์ธ์ฝ”๋”ฉ, ๋ฐ์ดํ„ฐ ๋ถ„ํ• 
"""
# ๋ ˆ์ด๋ธ” ์ธ์ฝ”๋”ฉ (๋ฌธ์ž์—ด โ†’ ์ˆซ์ž)
le = LabelEncoder()
y_encoded = le.fit_transform(y)
y_onehot = keras.utils.to_categorical(y_encoded)

print(f"๊ฐ์ • ํด๋ž˜์Šค: {le.classes_}")
print(f"ํด๋ž˜์Šค ์ˆ˜: {len(le.classes_)}")

# ํŠน์ง• ์ •๊ทœํ™” (StandardScaler)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# ํ•™์Šต/๊ฒ€์ฆ/ํ…Œ์ŠคํŠธ ๋ถ„ํ• 
X_temp, X_test, y_temp, y_test = train_test_split(
X_scaled, y_onehot, test_size=test_size, 
random_state=42, stratify=y_encoded
)

val_ratio = val_size / (1 - test_size)
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=val_ratio,
random_state=42
)

print(f"\n๋ฐ์ดํ„ฐ ๋ถ„ํ•  ๊ฒฐ๊ณผ:")
print(f"  ํ•™์Šต: {X_train.shape[0]}๊ฐœ")
print(f"  ๊ฒ€์ฆ: {X_val.shape[0]}๊ฐœ")
print(f"  ํ…Œ์ŠคํŠธ: {X_test.shape[0]}๊ฐœ")

return X_train, X_val, X_test, y_train, y_val, y_test, le, scaler


# =============================================
# 6. CNN + LSTM ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ชจ๋ธ
# =============================================
def build_cnn_lstm_model(input_dim, num_classes):
"""
CNN + LSTM ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฐ์ • ๋ถ„๋ฅ˜ ๋ชจ๋ธ

๊ตฌ์กฐ:
Input โ†’ Reshape โ†’ Conv1D ร— 3 โ†’ LSTM ร— 2 โ†’ Dense โ†’ Output
"""
inputs = keras.Input(shape=(input_dim,), name='audio_features')

# 1D ๋ฐฐ์—ด์„ ์‹œํ€€์Šค ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ (CNN/LSTM ์ž…๋ ฅ์šฉ)
x = layers.Reshape((input_dim, 1))(inputs)

# ---- CNN ๋ธ”๋ก 1 ----
x = layers.Conv1D(64, kernel_size=5, padding='same', activation='relu')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling1D(pool_size=2)(x)
x = layers.Dropout(0.3)(x)

# ---- CNN ๋ธ”๋ก 2 ----
x = layers.Conv1D(128, kernel_size=5, padding='same', activation='relu')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling1D(pool_size=2)(x)
x = layers.Dropout(0.3)(x)

# ---- CNN ๋ธ”๋ก 3 ----
x = layers.Conv1D(256, kernel_size=3, padding='same', activation='relu')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling1D(pool_size=2)(x)
x = layers.Dropout(0.3)(x)

# ---- LSTM ๋ธ”๋ก 1 ----
x = layers.LSTM(128, return_sequences=True, dropout=0.3)(x)
x = layers.BatchNormalization()(x)

# ---- LSTM ๋ธ”๋ก 2 ----
x = layers.LSTM(64, return_sequences=False, dropout=0.3)(x)
x = layers.BatchNormalization()(x)

# ---- Fully Connected ๋ ˆ์ด์–ด ----
x = layers.Dense(256, activation='relu')(x)
x = layers.Dropout(0.4)(x)
x = layers.Dense(128, activation='relu')(x)
x = layers.Dropout(0.3)(x)

# ---- ์ถœ๋ ฅ ๋ ˆ์ด์–ด ----
outputs = layers.Dense(num_classes, activation='softmax', name='emotion_output')(x)

model = Model(inputs=inputs, outputs=outputs, name='SER_CNN_LSTM')

return model


# =============================================
# 7. ๋ชจ๋ธ ์ปดํŒŒ์ผ & ํ•™์Šต
# =============================================
def train_model(X_train, X_val, y_train, y_val, num_classes, epochs=100):
"""๋ชจ๋ธ ํ•™์Šต ํ•จ์ˆ˜"""

input_dim = X_train.shape[1]
model = build_cnn_lstm_model(input_dim, num_classes)

# ๋ชจ๋ธ ๊ตฌ์กฐ ์ถœ๋ ฅ
model.summary()

# ์ปดํŒŒ์ผ
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss='categorical_crossentropy',
metrics=['accuracy']
)

# ์ฝœ๋ฐฑ ์„ค์ •
callbacks = [
# ๊ฒ€์ฆ ์†์‹ค์ด ๊ฐœ์„ ๋˜์ง€ ์•Š์œผ๋ฉด ํ•™์Šต๋ฅ  ๊ฐ์†Œ
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=10,
min_lr=1e-6,
verbose=1
),
# ๊ณผ์ ํ•ฉ ๋ฐฉ์ง€: ๊ฒ€์ฆ ์†์‹ค ๊ธฐ์ค€ ์กฐ๊ธฐ ์ข…๋ฃŒ
keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=20,
restore_best_weights=True,
verbose=1
),
# ์ตœ๊ณ  ์„ฑ๋Šฅ ๋ชจ๋ธ ์ €์žฅ
keras.callbacks.ModelCheckpoint(
'best_ser_model.h5',
monitor='val_accuracy',
save_best_only=True,
verbose=1
)
]

# ํ•™์Šต ์‹œ์ž‘!
history = model.fit(
X_train, y_train,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=32,
callbacks=callbacks,
verbose=1
)

return model, history
โš ๏ธ ์ฃผ์˜! RAVDESS ๋ฐ์ดํ„ฐ์…‹์€ ์•ฝ 1,440๊ฐœ๋กœ ๋”ฅ๋Ÿฌ๋‹ ๊ธฐ์ค€์œผ๋กœ๋Š” ์ž‘์€ ํŽธ์ด์•ผ. ๊ณผ์ ํ•ฉ(Overfitting)์ด ์‰ฝ๊ฒŒ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. Dropout, BatchNormalization, EarlyStopping์„ ๊ผญ ์‚ฌ์šฉํ•˜๊ณ , ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•(Data Augmentation)๋„ ๊ณ ๋ คํ•ด๋ด!

๐Ÿ”Š 5. ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• โ€” ์ž‘์€ ๋ฐ์ดํ„ฐ์…‹ ๊ทน๋ณตํ•˜๊ธฐ

์Œ์„ฑ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•์€ ํ…์ŠคํŠธ๋‚˜ ์ด๋ฏธ์ง€๋ณด๋‹ค ํ›จ์”ฌ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด.
์‹ค์ œ๋กœ ์ด ๊ธฐ๋ฒ•๋“ค์„ ์ ์šฉํ•˜๋ฉด ๋ชจ๋ธ ์„ฑ๋Šฅ์ด 5~15% ํ–ฅ์ƒ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„! ๐Ÿš€

# =============================================
# 8. ์Œ์„ฑ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• ๊ธฐ๋ฒ•๋“ค
# =============================================
def augment_audio(audio, sr=22050, augment_type='random'):
"""
์Œ์„ฑ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• ํ•จ์ˆ˜

์ฆ๊ฐ• ๊ธฐ๋ฒ•:
1. ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€ (Noise Addition)
2. ์‹œ๊ฐ„ ์ด๋™ (Time Shifting)
3. ํ”ผ์น˜ ๋ณ€ํ™˜ (Pitch Shifting)
4. ์†๋„ ๋ณ€ํ™˜ (Time Stretching)
5. ๋ณผ๋ฅจ ์กฐ์ ˆ (Volume Scaling)
"""
import random

if augment_type == 'random':
augment_type = random.choice([
'noise', 'shift', 'pitch', 'stretch', 'volume'
])

augmented = audio.copy()

if augment_type == 'noise':
# ๊ฐ€์šฐ์‹œ์•ˆ ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€ (์‹ค์ œ ํ™˜๊ฒฝ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
noise_factor = 0.005
noise = np.random.randn(len(audio))
augmented = audio + noise_factor * noise
augmented = augmented.astype(np.float32)

elif augment_type == 'shift':
# ์‹œ๊ฐ„ ์ด๋™ (์•ž๋’ค๋กœ ์ตœ๋Œ€ 1์ดˆ ์ด๋™)
shift_max = int(sr * 0.5)  # 0.5์ดˆ
shift = np.random.randint(-shift_max, shift_max)
augmented = np.roll(audio, shift)
# ์ด๋™๋œ ๋ถ€๋ถ„ 0์œผ๋กœ ์ฑ„์šฐ๊ธฐ
if shift > 0:
augmented[:shift] = 0
elif shift < 0:
augmented[shift:] = 0

elif augment_type == 'pitch':
# ํ”ผ์น˜ ๋ณ€ํ™˜ (-3 ~ +3 ๋ฐ˜์Œ)
n_steps = np.random.uniform(-3, 3)
augmented = librosa.effects.pitch_shift(
audio, sr=sr, n_steps=n_steps
)

elif augment_type == 'stretch':
# ์†๋„ ๋ณ€ํ™˜ (0.8x ~ 1.2x)
rate = np.random.uniform(0.8, 1.2)
augmented = librosa.effects.time_stretch(audio, rate=rate)
# ๊ธธ์ด ๋งž์ถ”๊ธฐ
target_len = len(audio)
if len(augmented) > target_len:
augmented = augmented[:target_len]
else:
augmented = np.pad(augmented, (0, target_len - len(augmented)))

elif augment_type == 'volume':
# ๋ณผ๋ฅจ ์กฐ์ ˆ (0.5x ~ 1.5x)
factor = np.random.uniform(0.5, 1.5)
augmented = audio * factor

return augmented


# =============================================
# 9. ์ฆ๊ฐ• ํฌํ•จ ํŠน์ง• ์ถ”์ถœ (๋ฐ์ดํ„ฐ์…‹ ํ™•์žฅ)
# =============================================
def extract_features_with_augmentation(file_path, sr=22050, 
duration=3.0, n_augments=2):
"""
์›๋ณธ + ์ฆ๊ฐ• ๋ฐ์ดํ„ฐ์˜ ํŠน์ง•์„ ํ•จ๊ป˜ ์ถ”์ถœ
n_augments: ์ฆ๊ฐ• ํšŸ์ˆ˜ (์›๋ณธ 1๊ฐœ + ์ฆ๊ฐ• n๊ฐœ)
"""
features_list = []

try:
audio, sample_rate = librosa.load(file_path, sr=sr, 
duration=duration, mono=True)
target_length = int(sr * duration)
if len(audio) < target_length:
audio = np.pad(audio, (0, target_length - len(audio)))
else:
audio = audio[:target_length]

# ์›๋ณธ ํŠน์ง• ์ถ”์ถœ
# (extract_features ํ•จ์ˆ˜๋ฅผ ์˜ค๋””์˜ค ๋ฐฐ์—ด ๋ฒ„์ „์œผ๋กœ ์ˆ˜์ • ํ•„์š”)
features_list.append(extract_features_from_array(audio, sr))

# ์ฆ๊ฐ• ๋ฐ์ดํ„ฐ ํŠน์ง• ์ถ”์ถœ
for _ in range(n_augments):
aug_audio = augment_audio(audio, sr)
features_list.append(extract_features_from_array(aug_audio, sr))

except Exception as e:
print(f"์˜ค๋ฅ˜: {e}")

return features_list


def extract_features_from_array(audio, sr=22050):
"""์˜ค๋””์˜ค ๋ฐฐ์—ด์—์„œ ์ง์ ‘ ํŠน์ง• ์ถ”์ถœ (ํŒŒ์ผ ๊ฒฝ๋กœ ๋Œ€์‹  ๋ฐฐ์—ด ์ž…๋ ฅ)"""
mfcc = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=40)
mfcc_mean = np.mean(mfcc, axis=1)
mfcc_std = np.std(mfcc, axis=1)
mfcc_delta = np.mean(librosa.feature.delta(mfcc), axis=1)

mel_spec = librosa.feature.melspectrogram(y=audio, sr=sr, n_mels=128)
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
mel_mean = np.mean(mel_spec_db, axis=1)

chroma = librosa.feature.chroma_stft(y=audio, sr=sr)
chroma_mean = np.mean(chroma, axis=1)

contrast = librosa.feature.spectral_contrast(y=audio, sr=sr)
contrast_mean = np.mean(contrast, axis=1)

zcr_mean = np.mean(librosa.feature.zero_crossing_rate(audio))
rms_mean = np.mean(librosa.feature.rms(y=audio))

pitches, _ = librosa.piptrack(y=audio, sr=sr)
pitch_mean = np.mean(pitches[pitches > 0]) if np.any(pitches > 0) else 0
pitch_std = np.std(pitches[pitches > 0]) if np.any(pitches > 0) else 0

return np.concatenate([
mfcc_mean, mfcc_std, mfcc_delta, mel_mean,
chroma_mean, contrast_mean,
[zcr_mean], [rms_mean], [pitch_mean], [pitch_std]
])

๐Ÿ“ˆ 6. ๋ชจ๋ธ ํ‰๊ฐ€ & ์‹œ๊ฐํ™”

๋ชจ๋ธ์„ ํ•™์Šตํ–ˆ์œผ๋ฉด ์–ผ๋งˆ๋‚˜ ์ž˜ ๋๋Š”์ง€ ํ™•์ธํ•ด์•ผ์ง€! ๐Ÿ“Š
๋‹จ์ˆœ ์ •ํ™•๋„(Accuracy)๋งŒ ๋ณด๋ฉด ์•ˆ ๋˜๊ณ , ๊ฐ ๊ฐ์ •๋ณ„ ์„ฑ๋Šฅ์„ ๊ผผ๊ผผํžˆ ๋ด์•ผ ํ•ด.

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import classification_report, confusion_matrix

# =============================================
# 10. ๋ชจ๋ธ ํ‰๊ฐ€ ํ•จ์ˆ˜
# =============================================
def evaluate_model(model, X_test, y_test, le):
"""
๋ชจ๋ธ ์„ฑ๋Šฅ ํ‰๊ฐ€ ๋ฐ ์‹œ๊ฐํ™”
"""
# ์˜ˆ์ธก
y_pred_prob = model.predict(X_test)
y_pred = np.argmax(y_pred_prob, axis=1)
y_true = np.argmax(y_test, axis=1)

# ์ „์ฒด ์ •ํ™•๋„
accuracy = np.mean(y_pred == y_true)
print(f"\n=== ๋ชจ๋ธ ํ‰๊ฐ€ ๊ฒฐ๊ณผ ===")
print(f"ํ…Œ์ŠคํŠธ ์ •ํ™•๋„: {accuracy:.4f} ({accuracy*100:.2f}%)")

# ๊ฐ์ •๋ณ„ ์ƒ์„ธ ์„ฑ๋Šฅ (Precision, Recall, F1-Score)
print("\n=== ๊ฐ์ •๋ณ„ ์„ฑ๋Šฅ ===")
print(classification_report(
y_true, y_pred, 
target_names=le.classes_
))

# ํ˜ผ๋™ ํ–‰๋ ฌ ์‹œ๊ฐํ™”
cm = confusion_matrix(y_true, y_pred)

plt.figure(figsize=(10, 8))
sns.heatmap(
cm, 
annot=True, 
fmt='d',
cmap='Blues',
xticklabels=le.classes_,
yticklabels=le.classes_
)
plt.title('๊ฐ์ • ๋ถ„๋ฅ˜ ํ˜ผ๋™ ํ–‰๋ ฌ (Confusion Matrix)', fontsize=14, pad=15)
plt.ylabel('์‹ค์ œ ๊ฐ์ •', fontsize=12)
plt.xlabel('์˜ˆ์ธก ๊ฐ์ •', fontsize=12)
plt.tight_layout()
plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')
plt.show()

return accuracy, y_pred, y_pred_prob


# =============================================
# 11. ํ•™์Šต ๊ณก์„  ์‹œ๊ฐํ™”
# =============================================
def plot_training_history(history):
"""ํ•™์Šต/๊ฒ€์ฆ ์†์‹ค ๋ฐ ์ •ํ™•๋„ ์‹œ๊ฐํ™”"""

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ์ •ํ™•๋„ ๊ทธ๋ž˜ํ”„
axes[0].plot(history.history['accuracy'], 
label='ํ•™์Šต ์ •ํ™•๋„', color='#667eea', linewidth=2)
axes[0].plot(history.history['val_accuracy'], 
label='๊ฒ€์ฆ ์ •ํ™•๋„', color='#764ba2', linewidth=2, linestyle='--')
axes[0].set_title('๋ชจ๋ธ ์ •ํ™•๋„', fontsize=13, fontweight='bold')
axes[0].set_xlabel('์—ํญ')
axes[0].set_ylabel('์ •ํ™•๋„')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# ์†์‹ค ๊ทธ๋ž˜ํ”„
axes[1].plot(history.history['loss'], 
label='ํ•™์Šต ์†์‹ค', color='#e17055', linewidth=2)
axes[1].plot(history.history['val_loss'], 
label='๊ฒ€์ฆ ์†์‹ค', color='#fdcb6e', linewidth=2, linestyle='--')
axes[1].set_title('๋ชจ๋ธ ์†์‹ค', fontsize=13, fontweight='bold')
axes[1].set_xlabel('์—ํญ')
axes[1].set_ylabel('์†์‹ค')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('training_history.png', dpi=150, bbox_inches='tight')
plt.show()

๐Ÿ“Š ์ผ๋ฐ˜์ ์ธ ์„ฑ๋Šฅ ๊ธฐ์ค€

RAVDESS ๋ฐ์ดํ„ฐ์…‹ ๊ธฐ์ค€์œผ๋กœ ํ˜„์žฌ ์—ฐ๊ตฌ๋“ค์˜ ์„ฑ๋Šฅ ๋ฒ”์œ„์•ผ:

๊ธฐ๋ณธ MLP ์•ฝ 55~65% ์ •ํ™•๋„
CNN ์•ฝ 65~75% ์ •ํ™•๋„
CNN+LSTM ์•ฝ 70~80% ์ •ํ™•๋„
Transformer ๊ธฐ๋ฐ˜ ์•ฝ 80~90% ์ •ํ™•๋„

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  CNN+LSTM ๋ชจ๋ธ๋กœ 70~78% ์ •๋„๋ฅผ ๋ชฉํ‘œ๋กœ ํ•˜๋ฉด ๋ผ.
๋ฐ์ดํ„ฐ ์ฆ๊ฐ•๊ณผ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹์œผ๋กœ ๋” ์˜ฌ๋ฆด ์ˆ˜ ์žˆ์–ด!


๐ŸŽค 7. ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ โ€” ๋งˆ์ดํฌ ์ž…๋ ฅ ์ฒ˜๋ฆฌ

ํ•™์Šต๋œ ๋ชจ๋ธ๋กœ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋งˆ์ดํฌ ์ž…๋ ฅ์„ ๋ถ„์„ํ•˜๋Š” ์ฝ”๋“œ์•ผ.
์ด๊ฒŒ ์ง„์งœ ์‹ค์šฉ์ ์ธ ๋ถ€๋ถ„์ด์ง€! ๐ŸŽ™๏ธ

import pyaudio
import wave
import threading
import queue
import time

# =============================================
# 12. ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„๊ธฐ
# =============================================
class RealTimeSER:
"""
์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ํด๋ž˜์Šค
๋งˆ์ดํฌ ์ž…๋ ฅ์„ ๋ฐ›์•„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ •์„ ๋ถ„๋ฅ˜
"""

# ๊ฐ์ • ์ด๋ชจ์ง€ ๋งคํ•‘
EMOTION_EMOJI = {
'neutral':   '๐Ÿ˜',
'calm':      '๐Ÿ˜Œ',
'happy':     '๐Ÿ˜Š',
'sad':       '๐Ÿ˜ข',
'angry':     '๐Ÿ˜ ',
'fearful':   '๐Ÿ˜จ',
'disgust':   '๐Ÿคข',
'surprised': '๐Ÿ˜ฒ'
}

def __init__(self, model, scaler, label_encoder, 
sr=22050, duration=3.0, chunk_size=1024):
self.model = model
self.scaler = scaler
self.le = label_encoder
self.sr = sr
self.duration = duration
self.chunk_size = chunk_size
self.is_running = False
self.audio_queue = queue.Queue()

def record_audio(self):
"""๋งˆ์ดํฌ์—์„œ ์˜ค๋””์˜ค ๋…น์Œ (๋ณ„๋„ ์Šค๋ ˆ๋“œ)"""
p = pyaudio.PyAudio()

stream = p.open(
format=pyaudio.paFloat32,
channels=1,
rate=self.sr,
input=True,
frames_per_buffer=self.chunk_size
)

print("๐ŸŽ™๏ธ ๋…น์Œ ์‹œ์ž‘! (Ctrl+C๋กœ ์ข…๋ฃŒ)")

while self.is_running:
frames = []
n_frames = int(self.sr * self.duration / self.chunk_size)

for _ in range(n_frames):
if not self.is_running:
break
data = stream.read(self.chunk_size, exception_on_overflow=False)
frames.append(np.frombuffer(data, dtype=np.float32))

if frames:
audio_data = np.concatenate(frames)
self.audio_queue.put(audio_data)

stream.stop_stream()
stream.close()
p.terminate()

def analyze_emotion(self):
"""ํ์—์„œ ์˜ค๋””์˜ค๋ฅผ ๊บผ๋‚ด ๊ฐ์ • ๋ถ„์„ (๋ณ„๋„ ์Šค๋ ˆ๋“œ)"""

while self.is_running:
try:
audio_data = self.audio_queue.get(timeout=1.0)

# ํŠน์ง• ์ถ”์ถœ
features = extract_features_from_array(audio_data, self.sr)
features_scaled = self.scaler.transform(features.reshape(1, -1))

# ๊ฐ์ • ์˜ˆ์ธก
pred_prob = self.model.predict(features_scaled, verbose=0)[0]
pred_idx = np.argmax(pred_prob)
pred_emotion = self.le.classes_[pred_idx]
confidence = pred_prob[pred_idx]

# ๊ฒฐ๊ณผ ์ถœ๋ ฅ
emoji = self.EMOTION_EMOJI.get(pred_emotion, 'โ“')
print(f"\r{emoji} ๊ฐ์ •: {pred_emotion:10s} | "
f"์‹ ๋ขฐ๋„: {confidence:.2%} | "
f"{'โ–ˆ' * int(confidence * 20)}", end='', flush=True)

# ์ƒ์œ„ 3๊ฐœ ๊ฐ์ • ์ถœ๋ ฅ
top3_idx = np.argsort(pred_prob)[-3:][::-1]
print(f"\n  ์ƒ์œ„ 3๊ฐœ: ", end='')
for idx in top3_idx:
em = self.le.classes_[idx]
prob = pred_prob[idx]
print(f"{self.EMOTION_EMOJI.get(em,'')} {em}({prob:.1%}) ", end='')
print()

except queue.Empty:
continue
except Exception as e:
print(f"\n๋ถ„์„ ์˜ค๋ฅ˜: {e}")

def start(self):
"""์‹ค์‹œ๊ฐ„ ๋ถ„์„ ์‹œ์ž‘"""
self.is_running = True

# ๋…น์Œ ์Šค๋ ˆ๋“œ
record_thread = threading.Thread(target=self.record_audio)
record_thread.daemon = True
record_thread.start()

# ๋ถ„์„ ์Šค๋ ˆ๋“œ
analyze_thread = threading.Thread(target=self.analyze_emotion)
analyze_thread.daemon = True
analyze_thread.start()

try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
print("\n\n๋ถ„์„ ์ข…๋ฃŒ!")
self.is_running = False

def analyze_file(self, file_path):
"""๋‹จ์ผ ํŒŒ์ผ ๊ฐ์ • ๋ถ„์„"""
features = extract_features(file_path)
if features is None:
return None

features_scaled = self.scaler.transform(features.reshape(1, -1))
pred_prob = self.model.predict(features_scaled, verbose=0)[0]
pred_idx = np.argmax(pred_prob)
pred_emotion = self.le.classes_[pred_idx]
confidence = pred_prob[pred_idx]

result = {
'emotion': pred_emotion,
'confidence': float(confidence),
'emoji': self.EMOTION_EMOJI.get(pred_emotion, 'โ“'),
'all_probabilities': {
self.le.classes_[i]: float(pred_prob[i]) 
for i in range(len(self.le.classes_))
}
}

return result


# =============================================
# ์‚ฌ์šฉ ์˜ˆ์‹œ
# =============================================
# ๋ชจ๋ธ ๋กœ๋“œ ํ›„ ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ์‹œ์ž‘
# model = keras.models.load_model('best_ser_model.h5')
# ser = RealTimeSER(model, scaler, le)
# 
# ํŒŒ์ผ ๋ถ„์„
# result = ser.analyze_file('test_audio.wav')
# print(f"๊ฐ์ •: {result['emoji']} {result['emotion']} ({result['confidence']:.1%})")
#
# ์‹ค์‹œ๊ฐ„ ๋ถ„์„
# ser.start()
๐Ÿ’ก pyaudio ์„ค์น˜ ํŒ! Windows์—์„œ๋Š” pip install pyaudio๊ฐ€ ์ž˜ ์•ˆ ๋  ์ˆ˜ ์žˆ์–ด. ์ด๋Ÿด ๋•Œ๋Š” pip install pipwin ํ›„ pipwin install pyaudio๋ฅผ ์จ๋ด. Mac์—์„œ๋Š” brew install portaudio ๋จผ์ € ํ•˜๊ณ  pip ์„ค์น˜ํ•˜๋ฉด ๋ผ!

๐Ÿš€ 8. ๊ณ ๊ธ‰ ๋ชจ๋ธ โ€” Wav2Vec2 ๊ธฐ๋ฐ˜ Transfer Learning

์ด์ œ ํ•œ ๋‹จ๊ณ„ ๋” ๋‚˜์•„๊ฐ€๋ณผ๊นŒ? ๐Ÿ˜Ž
์š”์ฆ˜ ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์˜ ์ตœ๊ฐ•์ž๋Š” Wav2Vec2๋‚˜ HuBERT ๊ฐ™์€
์‚ฌ์ „ํ•™์Šต๋œ ์Œ์„ฑ ๋ชจ๋ธ์„ ํŒŒ์ธํŠœ๋‹ํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ.
Hugging Face ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด!

# pip install transformers datasets torch torchaudio

from transformers import (
Wav2Vec2Processor, 
Wav2Vec2ForSequenceClassification,
TrainingArguments, 
Trainer
)
import torch
import torchaudio
from datasets import Dataset, DatasetDict
import numpy as np

# =============================================
# 13. Wav2Vec2 ๊ธฐ๋ฐ˜ ๊ฐ์ • ๋ถ„๋ฅ˜ ๋ชจ๋ธ
# =============================================

# ๊ฐ์ • ๋ ˆ์ด๋ธ” ์ •์˜
EMOTION_LABELS = ['neutral', 'calm', 'happy', 'sad', 
'angry', 'fearful', 'disgust', 'surprised']
label2id = {label: i for i, label in enumerate(EMOTION_LABELS)}
id2label = {i: label for i, label in enumerate(EMOTION_LABELS)}

def load_wav2vec2_model(num_labels=8):
"""
์‚ฌ์ „ํ•™์Šต๋œ Wav2Vec2 ๋ชจ๋ธ ๋กœ๋“œ ๋ฐ ๋ถ„๋ฅ˜ ํ—ค๋“œ ์ถ”๊ฐ€
"""
model_name = "facebook/wav2vec2-base"  # ๋˜๋Š” "facebook/wav2vec2-large"

# ํ”„๋กœ์„ธ์„œ ๋กœ๋“œ (์˜ค๋””์˜ค ์ „์ฒ˜๋ฆฌ ๋‹ด๋‹น)
processor = Wav2Vec2Processor.from_pretrained(model_name)

# ๋ถ„๋ฅ˜ ๋ชจ๋ธ ๋กœ๋“œ
model = Wav2Vec2ForSequenceClassification.from_pretrained(
model_name,
num_labels=num_labels,
label2id=label2id,
id2label=id2label,
ignore_mismatched_sizes=True
)

# ํŠน์ง• ์ถ”์ถœ๊ธฐ ๋ ˆ์ด์–ด ๋™๊ฒฐ (ํŒŒ์ธํŠœ๋‹ ์‹œ ์„ ํƒ์ )
# model.freeze_feature_encoder()

return model, processor


def preprocess_audio_for_wav2vec2(file_path, processor, sr=16000, max_duration=5.0):
"""
Wav2Vec2 ์ž…๋ ฅ ํ˜•์‹์œผ๋กœ ์˜ค๋””์˜ค ์ „์ฒ˜๋ฆฌ
"""
# ์˜ค๋””์˜ค ๋กœ๋“œ (16kHz ํ•„์ˆ˜!)
audio, orig_sr = torchaudio.load(file_path)

# ๋ชจ๋…ธ ๋ณ€ํ™˜
if audio.shape[0] > 1:
audio = torch.mean(audio, dim=0, keepdim=True)

# ๋ฆฌ์ƒ˜ํ”Œ๋ง (Wav2Vec2๋Š” 16kHz ์ž…๋ ฅ ํ•„์š”)
if orig_sr != sr:
resampler = torchaudio.transforms.Resample(orig_sr, sr)
audio = resampler(audio)

audio = audio.squeeze().numpy()

# ๊ธธ์ด ์ œํ•œ
max_length = int(sr * max_duration)
if len(audio) > max_length:
audio = audio[:max_length]

# ํ”„๋กœ์„ธ์„œ๋กœ ์ •๊ทœํ™”
inputs = processor(
audio,
sampling_rate=sr,
return_tensors="pt",
padding=True,
max_length=max_length,
truncation=True
)

return inputs


# =============================================
# 14. Trainer API๋ฅผ ํ™œ์šฉํ•œ ํŒŒ์ธํŠœ๋‹
# =============================================
def finetune_wav2vec2(train_dataset, eval_dataset, model, processor):
"""
Wav2Vec2 ํŒŒ์ธํŠœ๋‹ ์„ค์ • ๋ฐ ํ•™์Šต
"""

training_args = TrainingArguments(
output_dir="./wav2vec2-emotion",
num_train_epochs=10,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
gradient_accumulation_steps=2,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="accuracy",
learning_rate=3e-5,
warmup_ratio=0.1,
logging_dir="./logs",
logging_steps=10,
fp16=True,  # GPU ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ (CUDA ํ•„์š”)
dataloader_num_workers=4,
report_to="none"
)

def compute_metrics(eval_pred):
"""ํ‰๊ฐ€ ์ง€ํ‘œ ๊ณ„์‚ฐ"""
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
accuracy = np.mean(predictions == labels)
return {"accuracy": accuracy}

trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_metrics,
)

trainer.train()

# ์ตœ์ข… ํ‰๊ฐ€
results = trainer.evaluate()
print(f"\nWav2Vec2 ํŒŒ์ธํŠœ๋‹ ๊ฒฐ๊ณผ: {results}")

return trainer

๐Ÿ† ๋ชจ๋ธ ์•„ํ‚คํ…์ฒ˜ ๋น„๊ต

CNN+LSTM (์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ๊ฒƒ)
โ†’ ํ•™์Šต ๋น ๋ฆ„, ๊ฐ€๋ฒผ์›€, ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์‰ฌ์›€, ์ •ํ™•๋„ 70~78%

Wav2Vec2 ํŒŒ์ธํŠœ๋‹
โ†’ ๋†’์€ ์ •ํ™•๋„(85~92%), GPU ํ•„์š”, ํ•™์Šต ๋А๋ฆผ, ๋Œ€์šฉ๋Ÿ‰ ๋ชจ๋ธ

HuBERT ํŒŒ์ธํŠœ๋‹
โ†’ ์ตœ๊ณ  ์ˆ˜์ค€ ์ •ํ™•๋„(88~93%), ๊ฐ€์žฅ ๋ฌด๊ฑฐ์›€, ์—ฐ๊ตฌ์šฉ

์‹ค์šฉ์  ์„ ํƒ ๊ธฐ์ค€:
๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ž… โ†’ CNN+LSTM
ํ”„๋กœ๋•์…˜ ์„œ๋น„์Šค โ†’ Wav2Vec2
์ตœ๊ณ  ์„ฑ๋Šฅ ์—ฐ๊ตฌ โ†’ HuBERT


๐ŸŒ 9. Flask API ์„œ๋ฒ„๋กœ ๋ฐฐํฌํ•˜๊ธฐ

๋ชจ๋ธ์„ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์‹ค์ œ ์„œ๋น„์Šค๋กœ ๋ฐฐํฌํ•ด์•ผ์ง€! ๐ŸŒ
Flask๋กœ REST API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด์„œ ์–ด๋””์„œ๋“  ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋ณด์ž.

from flask import Flask, request, jsonify
import tensorflow as tf
from tensorflow import keras
import numpy as np
import librosa
import pickle
import io
import os
import tempfile

# =============================================
# 15. Flask REST API ์„œ๋ฒ„
# =============================================
app = Flask(__name__)

# ์ „์—ญ ๋ณ€์ˆ˜๋กœ ๋ชจ๋ธ ๋กœ๋“œ (์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ 1ํšŒ๋งŒ)
MODEL = None
SCALER = None
LABEL_ENCODER = None

def load_model_and_preprocessors():
"""์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ๋ชจ๋ธ ๋ฐ ์ „์ฒ˜๋ฆฌ๊ธฐ ๋กœ๋“œ"""
global MODEL, SCALER, LABEL_ENCODER

MODEL = keras.models.load_model('best_ser_model.h5')

with open('scaler.pkl', 'rb') as f:
SCALER = pickle.load(f)

with open('label_encoder.pkl', 'rb') as f:
LABEL_ENCODER = pickle.load(f)

print("โœ… ๋ชจ๋ธ ๋กœ๋“œ ์™„๋ฃŒ!")


@app.route('/health', methods=['GET'])
def health_check():
"""์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ"""
return jsonify({
'status': 'healthy',
'model_loaded': MODEL is not None,
'version': '1.0.0'
})


@app.route('/analyze', methods=['POST'])
def analyze_emotion():
"""
์Œ์„ฑ ํŒŒ์ผ ๊ฐ์ • ๋ถ„์„ API

Request: multipart/form-data
- file: ์˜ค๋””์˜ค ํŒŒ์ผ (WAV, MP3 ๋“ฑ)

Response: JSON
- emotion: ์˜ˆ์ธก๋œ ๊ฐ์ •
- confidence: ์‹ ๋ขฐ๋„
- all_emotions: ์ „์ฒด ๊ฐ์ •๋ณ„ ํ™•๋ฅ 
"""
if 'file' not in request.files:
return jsonify({'error': 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค'}), 400

audio_file = request.files['file']

if audio_file.filename == '':
return jsonify({'error': 'ํŒŒ์ผ๋ช…์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค'}), 400

# ํ—ˆ์šฉ๋œ ํŒŒ์ผ ํ˜•์‹ ํ™•์ธ
allowed_extensions = {'.wav', '.mp3', '.flac', '.ogg', '.m4a'}
file_ext = os.path.splitext(audio_file.filename)[1].lower()

if file_ext not in allowed_extensions:
return jsonify({
'error': f'์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: {file_ext}'
}), 400

try:
# ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
with tempfile.NamedTemporaryFile(
suffix=file_ext, delete=False
) as tmp_file:
audio_file.save(tmp_file.name)
tmp_path = tmp_file.name

# ํŠน์ง• ์ถ”์ถœ
features = extract_features(tmp_path)

# ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
os.unlink(tmp_path)

if features is None:
return jsonify({'error': 'ํŠน์ง• ์ถ”์ถœ ์‹คํŒจ'}), 500

# ์Šค์ผ€์ผ๋ง & ์˜ˆ์ธก
features_scaled = SCALER.transform(features.reshape(1, -1))
pred_prob = MODEL.predict(features_scaled, verbose=0)[0]
pred_idx = np.argmax(pred_prob)
pred_emotion = LABEL_ENCODER.classes_[pred_idx]
confidence = float(pred_prob[pred_idx])

# ์ „์ฒด ๊ฐ์ • ํ™•๋ฅ  ๋”•์…”๋„ˆ๋ฆฌ
all_emotions = {
LABEL_ENCODER.classes_[i]: float(pred_prob[i])
for i in range(len(LABEL_ENCODER.classes_))
}

# ์ƒ์œ„ 3๊ฐœ ๊ฐ์ •
top3_idx = np.argsort(pred_prob)[-3:][::-1]
top3 = [
{
'emotion': LABEL_ENCODER.classes_[i],
'probability': float(pred_prob[i])
}
for i in top3_idx
]

return jsonify({
'success': True,
'emotion': pred_emotion,
'confidence': confidence,
'top3_emotions': top3,
'all_emotions': all_emotions
})

except Exception as e:
return jsonify({'error': str(e)}), 500


@app.route('/batch_analyze', methods=['POST'])
def batch_analyze():
"""์—ฌ๋Ÿฌ ํŒŒ์ผ ์ผ๊ด„ ๋ถ„์„"""
files = request.files.getlist('files')

if not files:
return jsonify({'error': 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค'}), 400

results = []
for audio_file in files:
# ๊ฐ ํŒŒ์ผ ์ฒ˜๋ฆฌ (analyze_emotion ๋กœ์ง ์žฌ์‚ฌ์šฉ)
# ... (์ƒ๋žต)
pass

return jsonify({'results': results})


if __name__ == '__main__':
load_model_and_preprocessors()
app.run(host='0.0.0.0', port=5000, debug=False)

API ํ…Œ์ŠคํŠธ๋Š” ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ผ:

# curl๋กœ ํ…Œ์ŠคํŠธ
# curl -X POST http://localhost:5000/analyze \
#      -F "file=@test_audio.wav"

# Python requests๋กœ ํ…Œ์ŠคํŠธ
import requests

url = "http://localhost:5000/analyze"
with open("test_audio.wav", "rb") as f:
response = requests.post(url, files={"file": f})

result = response.json()
print(f"๊ฐ์ •: {result['emotion']}")
print(f"์‹ ๋ขฐ๋„: {result['confidence']:.2%}")
print(f"์ƒ์œ„ 3๊ฐœ: {result['top3_emotions']}")

๐Ÿ’ก 10. ์„ฑ๋Šฅ ํ–ฅ์ƒ ๊ฟ€ํŒ & ์‹ค์ „ ๋…ธํ•˜์šฐ

์ฝ”๋“œ๋งŒ ์žˆ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ! ์‹ค์ œ๋กœ ์ข‹์€ ์„ฑ๋Šฅ์„ ๋‚ด๋ ค๋ฉด
์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ์‹ ๊ฒฝ ์จ์•ผ ํ•ด. ๋‚ด๊ฐ€ ์ง์ ‘ ๊ฒช์–ด๋ณธ ๊ฒƒ๋“ค ์œ„์ฃผ๋กœ ์ •๋ฆฌํ–ˆ์–ด. ๐Ÿ˜ค

1
ํŠน์ง• ์„ ํƒ (Feature Selection)
๋ชจ๋“  ํŠน์ง•์ด ๋‹ค ์œ ์šฉํ•œ ๊ฑด ์•„๋‹ˆ์•ผ. MFCC 40๊ฐœ + delta + delta-delta ์กฐํ•ฉ์ด ๊ฐ€์žฅ ์•ˆ์ •์ ์ด๊ณ ,
Mel Spectrogram์„ 2D ์ด๋ฏธ์ง€๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹๋„ CNN์—์„œ ํšจ๊ณผ์ ์ด์•ผ.
2
ํด๋ž˜์Šค ๋ถˆ๊ท ํ˜• ์ฒ˜๋ฆฌ
๊ฐ์ • ๋ฐ์ดํ„ฐ๋Š” ์ค‘๋ฆฝ์ด ๋งŽ๊ณ  ํ˜์˜ค/๊ณตํฌ๊ฐ€ ์ ์€ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„.
class_weight ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ์˜ค๋ฒ„์ƒ˜ํ”Œ๋ง(SMOTE)์„ ํ™œ์šฉํ•ด๋ด.
3
๊ต์ฐจ ๊ฒ€์ฆ (Cross Validation)
ํ™”์ž ๋…๋ฆฝ์ (Speaker-Independent) ํ‰๊ฐ€๋ฅผ ์œ„ํ•ด
Leave-One-Speaker-Out(LOSO) ๊ต์ฐจ ๊ฒ€์ฆ์„ ์จ์•ผ ์ง„์งœ ์„ฑ๋Šฅ์„ ์•Œ ์ˆ˜ ์žˆ์–ด.
4
์•™์ƒ๋ธ” (Ensemble)
CNN+LSTM ๋ชจ๋ธ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ํ•™์Šต์‹œ์ผœ์„œ ์˜ˆ์ธก์„ ํ‰๊ท ๋‚ด๋ฉด ๋‹จ์ผ ๋ชจ๋ธ๋ณด๋‹ค 2~5% ์„ฑ๋Šฅ์ด ์˜ฌ๋ผ๊ฐ€.
5
Attention Mechanism ์ถ”๊ฐ€
LSTM ์œ„์— Self-Attention ๋ ˆ์ด์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์‹œ๊ฐ„์ ์œผ๋กœ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์–ด.
Transformer ์•„ํ‚คํ…์ฒ˜์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด์•ผ.
๐Ÿ’ก ์žฌ๋Šฅ๋„ท ํ™œ์šฉ ํŒ! ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ํ”„๋กœ์ ํŠธ๋ฅผ ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๋จธ์‹ ๋Ÿฌ๋‹/๋”ฅ๋Ÿฌ๋‹ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์–ด. ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ, ๋ชจ๋ธ ํŠœ๋‹, ๋ฐฐํฌ๊นŒ์ง€ ๊ฐ ๋‹จ๊ณ„๋ณ„๋กœ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ํŠนํžˆ ํ•œ๊ตญ์–ด ์Œ์„ฑ ๋ฐ์ดํ„ฐ์…‹ ๊ตฌ์ถ•์ด๋‚˜ ์ปค์Šคํ…€ ๋ชจ๋ธ ๊ฐœ๋ฐœ ๊ฐ™์€ ์ „๋ฌธ์ ์ธ ์ž‘์—…์—์„œ ํฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ!

๐Ÿ”ง 11. ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ ํ†ตํ•ฉ ์ฝ”๋“œ

์ง€๊ธˆ๊นŒ์ง€ ๋งŒ๋“  ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์ธ ์Šคํฌ๋ฆฝํŠธ์•ผ.
์ด๊ฑธ main.py๋กœ ์ €์žฅํ•˜๊ณ  ์‹คํ–‰ํ•˜๋ฉด ๋ผ!

# =============================================
# main.py โ€” ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ
# =============================================
import os
import pickle
import numpy as np
import warnings
warnings.filterwarnings('ignore')

def main():
print("=" * 60)
print("๐ŸŽ™๏ธ  ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ์‹œ์Šคํ…œ (SER)")
print("=" * 60)

# ---- ์„ค์ • ----
DATA_PATH = './RAVDESS'          # ๋ฐ์ดํ„ฐ์…‹ ๊ฒฝ๋กœ
MODEL_PATH = 'best_ser_model.h5' # ๋ชจ๋ธ ์ €์žฅ ๊ฒฝ๋กœ
SCALER_PATH = 'scaler.pkl'
LE_PATH = 'label_encoder.pkl'

# ---- 1๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ๋กœ๋“œ ----
print("\n[1/5] ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘...")
file_paths, labels = load_ravdess_data(DATA_PATH)

# ---- 2๋‹จ๊ณ„: ํŠน์ง• ์ถ”์ถœ ----
print("\n[2/5] ํŠน์ง• ์ถ”์ถœ ์ค‘...")
X, y = prepare_dataset(file_paths, labels)

# ---- 3๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ----
print("\n[3/5] ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ์ค‘...")
X_train, X_val, X_test, y_train, y_val, y_test, le, scaler = \
preprocess_data(X, y)

# ์ „์ฒ˜๋ฆฌ๊ธฐ ์ €์žฅ
with open(SCALER_PATH, 'wb') as f:
pickle.dump(scaler, f)
with open(LE_PATH, 'wb') as f:
pickle.dump(le, f)
print(f"์ „์ฒ˜๋ฆฌ๊ธฐ ์ €์žฅ ์™„๋ฃŒ: {SCALER_PATH}, {LE_PATH}")

# ---- 4๋‹จ๊ณ„: ๋ชจ๋ธ ํ•™์Šต ----
print("\n[4/5] ๋ชจ๋ธ ํ•™์Šต ์ค‘...")
num_classes = len(le.classes_)
model, history = train_model(
X_train, X_val, y_train, y_val, 
num_classes=num_classes, 
epochs=100
)

# ํ•™์Šต ๊ณก์„  ์‹œ๊ฐํ™”
plot_training_history(history)

# ---- 5๋‹จ๊ณ„: ๋ชจ๋ธ ํ‰๊ฐ€ ----
print("\n[5/5] ๋ชจ๋ธ ํ‰๊ฐ€ ์ค‘...")
accuracy, y_pred, y_pred_prob = evaluate_model(model, X_test, y_test, le)

print(f"\n{'='*60}")
print(f"โœ… ํ•™์Šต ์™„๋ฃŒ!")
print(f"   ์ตœ์ข… ํ…Œ์ŠคํŠธ ์ •ํ™•๋„: {accuracy:.4f} ({accuracy*100:.2f}%)")
print(f"   ๋ชจ๋ธ ์ €์žฅ ์œ„์น˜: {MODEL_PATH}")
print(f"{'='*60}")

# ---- ๋‹จ์ผ ํŒŒ์ผ ํ…Œ์ŠคํŠธ ----
print("\n[ํ…Œ์ŠคํŠธ] ๋‹จ์ผ ํŒŒ์ผ ๊ฐ์ • ๋ถ„์„:")
test_file = file_paths[0]  # ์ฒซ ๋ฒˆ์งธ ํŒŒ์ผ๋กœ ํ…Œ์ŠคํŠธ

ser = RealTimeSER(model, scaler, le)
result = ser.analyze_file(test_file)

if result:
print(f"  ํŒŒ์ผ: {os.path.basename(test_file)}")
print(f"  ์˜ˆ์ธก ๊ฐ์ •: {result['emoji']} {result['emotion']}")
print(f"  ์‹ ๋ขฐ๋„: {result['confidence']:.2%}")
print(f"  ์ „์ฒด ํ™•๋ฅ :")
for emotion, prob in sorted(
result['all_probabilities'].items(), 
key=lambda x: x[1], reverse=True
):
bar = 'โ–ˆ' * int(prob * 30)
print(f"    {emotion:12s}: {bar} {prob:.2%}")

return model, scaler, le


if __name__ == '__main__':
model, scaler, le = main()

๐Ÿ“ฑ 12. ํ•œ๊ตญ์–ด ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ โ€” ํŠน๋ณ„ ๊ณ ๋ ค์‚ฌํ•ญ

์˜์–ด ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ํ•™์Šตํ•œ ๋ชจ๋ธ์„ ํ•œ๊ตญ์–ด์— ๋ฐ”๋กœ ์“ฐ๋ฉด ์„ฑ๋Šฅ์ด ๋š ๋–จ์–ด์ ธ. ๐Ÿ˜…
ํ•œ๊ตญ์–ด ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์„ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด!

๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด SER ํŠน์ด์‚ฌํ•ญ

1. ํ•œ๊ตญ์–ด ์ „์šฉ ๋ฐ์ดํ„ฐ์…‹ ํ™œ์šฉ
โ†’ ETRI ๊ฐ์ • ์Œ์„ฑ ๋ฐ์ดํ„ฐ์…‹, KESDy, ํ•œ๊ตญ์–ด ๊ฐ์ • ์Œ์„ฑ ์ฝ”ํผ์Šค ๋“ฑ
โ†’ AI Hub(aihub.or.kr)์—์„œ ํ•œ๊ตญ์–ด ๊ฐ์ • ์Œ์„ฑ ๋ฐ์ดํ„ฐ ๋ฌด๋ฃŒ ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅ

2. ํ•œ๊ตญ์–ด ์Œ์„ฑ ํŠน์„ฑ ๋ฐ˜์˜
โ†’ ํ•œ๊ตญ์–ด๋Š” ์„ฑ์กฐ ์–ธ์–ด๊ฐ€ ์•„๋‹ˆ์ง€๋งŒ, ์–ต์–‘ ํŒจํ„ด์ด ์˜์–ด์™€ ๋‹ฌ๋ผ
โ†’ ๊ฒฝ์Œํ™”, ์—ฐ์Œ ๋“ฑ ํ•œ๊ตญ์–ด ํŠน์œ ์˜ ์Œ์šด ๋ณ€๋™ ๊ณ ๋ ค ํ•„์š”

3. ํ™”์ž ๋‹ค์–‘์„ฑ
โ†’ ์ง€์—ญ ๋ฐฉ์–ธ(๊ฒฝ์ƒ๋„, ์ „๋ผ๋„ ๋“ฑ)์— ๋”ฐ๋ฅธ ์–ต์–‘ ์ฐจ์ด ์กด์žฌ
โ†’ ์—ฐ๋ น๋Œ€๋ณ„ ์Œ์„ฑ ํŠน์„ฑ ์ฐจ์ด๋„ ๊ณ ๋ คํ•ด์•ผ ํ•ด

4. ํ•œ๊ตญ์–ด ์‚ฌ์ „ํ•™์Šต ๋ชจ๋ธ
โ†’ kresnik/wav2vec2-large-xlsr-korean (Hugging Face)
โ†’ NAVER์˜ ํ•œ๊ตญ์–ด ์Œ์„ฑ ๋ชจ๋ธ ํ™œ์šฉ ๊ฐ€๋Šฅ

# ํ•œ๊ตญ์–ด Wav2Vec2 ๋ชจ๋ธ ํ™œ์šฉ ์˜ˆ์‹œ
from transformers import Wav2Vec2Processor, Wav2Vec2ForSequenceClassification

# ํ•œ๊ตญ์–ด ์‚ฌ์ „ํ•™์Šต ๋ชจ๋ธ ๋กœ๋“œ
KO_MODEL_NAME = "kresnik/wav2vec2-large-xlsr-korean"

processor = Wav2Vec2Processor.from_pretrained(KO_MODEL_NAME)
model = Wav2Vec2ForSequenceClassification.from_pretrained(
KO_MODEL_NAME,
num_labels=7,  # ํ•œ๊ตญ์–ด ๊ฐ์ • ํด๋ž˜์Šค ์ˆ˜
label2id={
'๊ธฐ์จ': 0, '์Šฌํ””': 1, '๋ถ„๋…ธ': 2, '๊ณตํฌ': 3,
'ํ˜์˜ค': 4, '๋†€๋žŒ': 5, '์ค‘๋ฆฝ': 6
},
id2label={
0: '๊ธฐ์จ', 1: '์Šฌํ””', 2: '๋ถ„๋…ธ', 3: '๊ณตํฌ',
4: 'ํ˜์˜ค', 5: '๋†€๋žŒ', 6: '์ค‘๋ฆฝ'
},
ignore_mismatched_sizes=True
)

# ์ดํ›„ ํŒŒ์ธํŠœ๋‹ ๊ณผ์ •์€ ์˜์–ด ๋ชจ๋ธ๊ณผ ๋™์ผ!
๐Ÿ’ก AI Hub ๋ฐ์ดํ„ฐ์…‹ ํŒ! AI Hub(aihub.or.kr)์—์„œ "๊ฐ์ • ์Œ์„ฑ" ๊ฒ€์ƒ‰ํ•˜๋ฉด ํ•œ๊ตญ์–ด ๊ฐ์ • ์Œ์„ฑ ๋ฐ์ดํ„ฐ์…‹์„ ๋ฌด๋ฃŒ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ "๊ฐ์ • ๋ถ„๋ฅ˜๋ฅผ ์œ„ํ•œ ๋Œ€ํ™” ์Œ์„ฑ ๋ฐ์ดํ„ฐ์…‹"์€ ์•ฝ 40๋งŒ ๊ฐœ์˜ ํ•œ๊ตญ์–ด ๊ฐ์ • ์Œ์„ฑ ์ƒ˜ํ”Œ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด์„œ ํ•œ๊ตญ์–ด SER ํ”„๋กœ์ ํŠธ์— ์ตœ์ ์ด์•ผ!

๐ŸŽฏ 13. ํ”„๋กœ์ ํŠธ ํ™•์žฅ ์•„์ด๋””์–ด

๊ธฐ๋ณธ SER ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์—ˆ์œผ๋ฉด, ์ด๊ฑธ ์–ด๋–ป๊ฒŒ ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ์„๊นŒ? ๐Ÿค”
์‹ค์ œ๋กœ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์•„์ด๋””์–ด๋“ค์ด์•ผ!

๐Ÿ’ผ ์‹ค์šฉ์ ์ธ ํ™•์žฅ ํ”„๋กœ์ ํŠธ

๐ŸŽง ์ฝœ์„ผํ„ฐ ๊ฐ์ • ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ
โ†’ ์‹ค์‹œ๊ฐ„ ํ†ตํ™” ๊ฐ์ • ๋ถ„์„ + ์‹œ๊ฐํ™” ๋Œ€์‹œ๋ณด๋“œ
โ†’ ๊ณ ๊ฐ ๋ถˆ๋งŒ ์ž๋™ ๊ฐ์ง€ & ์•Œ๋ฆผ ์‹œ์Šคํ…œ

๐ŸŽฎ ๊ฐ์ • ๋ฐ˜์‘ํ˜• ์Œ์•… ํ”Œ๋ ˆ์ด์–ด
โ†’ ๋ชฉ์†Œ๋ฆฌ ๊ฐ์ •์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์Œ์•… ์žฅ๋ฅด ๋ณ€๊ฒฝ
โ†’ ๊ธฐ์  ๋•Œ ์‹ ๋‚˜๋Š” ์Œ์•…, ์Šฌํ”Œ ๋•Œ ์œ„๋กœ ์Œ์•…

๐Ÿ“š ์–ธ์–ด ํ•™์Šต ์•ฑ ๋ฐœ์Œ ์ฝ”์น˜
โ†’ ํ•™์Šต์ž์˜ ๋ฐœ์Œ ๊ฐ์ • ํ†ค ๋ถ„์„
โ†’ ์ž์—ฐ์Šค๋Ÿฌ์šด ์–ต์–‘ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต

๐Ÿฅ ์ •์‹ ๊ฑด๊ฐ• ๋ชจ๋‹ˆํ„ฐ๋ง ์•ฑ
โ†’ ์ผ์ƒ ๋Œ€ํ™”์—์„œ ์šฐ์šธ/์ŠคํŠธ๋ ˆ์Šค ํŒจํ„ด ๊ฐ์ง€
โ†’ ์ „๋ฌธ๊ฐ€ ์—ฐ๊ฒฐ ์ถ”์ฒœ ์‹œ์Šคํ…œ

๐Ÿค ํ™”์ƒํšŒ์˜ ๊ฐ์ • ๋ถ„์„ ํ”Œ๋Ÿฌ๊ทธ์ธ
โ†’ Zoom/Teams ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ํšŒ์˜ ์ฐธ์—ฌ์ž ๊ฐ์ • ๋ถ„์„
โ†’ ํŒ€ ๋ถ„์œ„๊ธฐ ๋ฆฌํฌํŠธ ์ž๋™ ์ƒ์„ฑ

์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋“ค์„ ํ˜ผ์ž ๋‹ค ๋งŒ๋“ค๊ธฐ ์–ด๋ ต๋‹ค๋ฉด,
์žฌ๋Šฅ๋„ท์—์„œ ํ”„๋ก ํŠธ์—”๋“œ, ๋ฐฑ์—”๋“œ, ๋ฐ์ดํ„ฐ ์‚ฌ์ด์–ธ์Šค ์ „๋ฌธ๊ฐ€๋“ค๊ณผ ํ˜‘์—…ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ.
๊ฐ์ž์˜ ์ „๋ฌธ ์˜์—ญ์„ ๋‚˜๋ˆ ์„œ ๋” ๋น ๋ฅด๊ณ  ์™„์„ฑ๋„ ๋†’์€ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿš€


๐Ÿ“‹ 14. ์ „์ฒด ์ฝ”๋“œ ๊ตฌ์กฐ ์š”์•ฝ

ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ๊ตฌ์กฐ & ๋ชจ๋“ˆ ๊ด€๊ณ„๋„ ๐Ÿ“‚ data_loader.py ๋ฐ์ดํ„ฐ ๋กœ๋“œ & ๋ ˆ์ด๋ธ” ํŒŒ์‹ฑ load_ravdess_data() ๐Ÿ”ฌ feature_extractor.py MFCC, Mel-Spec ์ถ”์ถœ extract_features() ๐Ÿ”„ augmentation.py ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• ๊ธฐ๋ฒ• augment_audio() ๐Ÿค– model.py CNN+LSTM ๋ชจ๋ธ ์ •์˜ build_cnn_lstm_model() โš™๏ธ train.py โ€” ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ํ†ตํ•ฉ preprocess_data() โ†’ train_model() โ†’ evaluate_model() ๐Ÿ’พ best_ser_model.h5 ํ•™์Šต๋œ ๋ชจ๋ธ ํŒŒ์ผ ๐Ÿ“Š scaler.pkl ํŠน์ง• ์Šค์ผ€์ผ๋Ÿฌ ๐Ÿท๏ธ label_encoder.pkl ๋ ˆ์ด๋ธ” ์ธ์ฝ”๋” ๐ŸŒ app.py (Flask) REST API ์„œ๋ฒ„ ๋ชจ๋“  ๋ชจ๋“ˆ์ด main.py์—์„œ ํ†ตํ•ฉ ์‹คํ–‰๋จ ํ•™์Šต ์™„๋ฃŒ ํ›„ Flask ์„œ๋ฒ„๋กœ ๋ฐฐํฌํ•˜์—ฌ REST API๋กœ ์„œ๋น„์Šค ์ œ๊ณต ์‹ค์‹œ๊ฐ„ ๋ถ„์„์€ RealTimeSER ํด๋ž˜์Šค๋กœ ๋งˆ์ดํฌ ์ž…๋ ฅ ์ฒ˜๋ฆฌ
ํŒŒ์ผ๋ช… ์—ญํ•  ์ฃผ์š” ํ•จ์ˆ˜
data_loader.py ๋ฐ์ดํ„ฐ์…‹ ๋กœ๋“œ & ๋ ˆ์ด๋ธ” ํŒŒ์‹ฑ load_ravdess_data()
feature_extractor.py MFCC, Mel-Spec ๋“ฑ ํŠน์ง• ์ถ”์ถœ extract_features()
augmentation.py ์Œ์„ฑ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• augment_audio()
model.py CNN+LSTM ๋ชจ๋ธ ์ •์˜ build_cnn_lstm_model()
train.py ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ํ†ตํ•ฉ train_model(), evaluate_model()
realtime_ser.py ์‹ค์‹œ๊ฐ„ ๋งˆ์ดํฌ ๋ถ„์„ RealTimeSER ํด๋ž˜์Šค
app.py Flask REST API ์„œ๋ฒ„ analyze_emotion() ์—”๋“œํฌ์ธํŠธ
main.py ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ main()

๐Ÿ” 15. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜ & ํ•ด๊ฒฐ๋ฒ•

์‹ค์ œ๋กœ ์ฝ”๋“œ ์งœ๋‹ค ๋ณด๋ฉด ์ด๋Ÿฐ ์˜ค๋ฅ˜๋“ค์„ ์ž์ฃผ ๋งŒ๋‚˜๊ฒŒ ๋ผ. ๋ฏธ๋ฆฌ ์•Œ์•„๋‘์ž! ๐Ÿ› ๏ธ

โš ๏ธ ์˜ค๋ฅ˜ 1: librosa.load() ์‹œ ์˜ค๋””์˜ค ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ
NoBackendError: No suitable audio I/O backend found

โ†’ ํ•ด๊ฒฐ: pip install soundfile audioread ์„ค์น˜. MP3 ํŒŒ์ผ์€ ffmpeg ํ•„์š”: conda install -c conda-forge ffmpeg
โš ๏ธ ์˜ค๋ฅ˜ 2: CUDA out of memory
โ†’ ํ•ด๊ฒฐ: batch_size๋ฅผ 32โ†’16โ†’8๋กœ ์ค„์ด๊ฑฐ๋‚˜,
tf.config.experimental.set_memory_growth(gpu, True)
์ถ”๊ฐ€
โš ๏ธ ์˜ค๋ฅ˜ 3: ๋ชจ๋ธ ์ •ํ™•๋„๊ฐ€ ๋„ˆ๋ฌด ๋‚ฎ์Œ (40% ์ดํ•˜)
โ†’ ํ•ด๊ฒฐ: ํŠน์ง• ์ •๊ทœํ™” ํ™•์ธ (StandardScaler ์ ์šฉ ์—ฌ๋ถ€), ํ•™์Šต๋ฅ  ์กฐ์ • (0.001โ†’0.0001), ์—ํญ ์ˆ˜ ์ฆ๊ฐ€, ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• ์ ์šฉ
โš ๏ธ ์˜ค๋ฅ˜ 4: ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ์‹œ ์ง€์—ฐ(Latency) ๋ฌธ์ œ
โ†’ ํ•ด๊ฒฐ: ๋ถ„์„ ์œˆ๋„์šฐ ํฌ๊ธฐ ์ค„์ด๊ธฐ (3์ดˆโ†’1.5์ดˆ), ๋ชจ๋ธ ๊ฒฝ๋Ÿ‰ํ™” (Quantization), TensorFlow Lite ๋ณ€ํ™˜ ๊ณ ๋ ค
๐Ÿ’ก ๋””๋ฒ„๊น… ํŒ! ํŠน์ง• ์ถ”์ถœ ํ›„ np.isnan(features).any()๋กœ NaN ๊ฐ’ ์ฒดํฌ ๊ผญ ํ•ด๋ด. ํ”ผ์น˜ ์ถ”์ถœ ์‹œ ๋ฌด์Œ ๊ตฌ๊ฐ„์—์„œ NaN์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์„œ ๋ชจ๋ธ ํ•™์Šต์„ ๋ง์น˜๋Š” ์ฃผ๋ฒ”์ด ๋˜๊ฑฐ๋“ !

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌ โ€” ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์˜ ๋ฏธ๋ž˜

์ž, ์˜ค๋Š˜ ์ •๋ง ๋งŽ์€ ๊ฑธ ๋‹ค๋ค˜์–ด! ๐ŸŽ‰
๋ฐ์ดํ„ฐ ๋กœ๋”ฉ๋ถ€ํ„ฐ ํŠน์ง• ์ถ”์ถœ, CNN+LSTM ๋ชจ๋ธ ๊ตฌ์ถ•, ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•, Flask API ๋ฐฐํฌ๊นŒ์ง€
์Œ์„ฑ ๊ฐ์ • ๋ถ„์„์˜ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์„ ์™„์„ฑํ–ˆ์–ด.

์Œ์„ฑ ๊ฐ์ • ๋ถ„์„ ๊ธฐ์ˆ ์€ ์ง€๊ธˆ๋„ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด.
GPT-4o๊ฐ€ ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๊ฐ์ • ์ธ์‹์„ ์„ ๋ณด์ด๋ฉด์„œ ์ด ๋ถ„์•ผ๊ฐ€ ๋” ์ฃผ๋ชฉ๋ฐ›๊ณ  ์žˆ๊ณ ,
๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๊ฐ์ • ์ธ์‹(์Œ์„ฑ + ํ‘œ์ • + ํ…์ŠคํŠธ ๋™์‹œ ๋ถ„์„)์ด ์ฐจ์„ธ๋Œ€ ํŠธ๋ Œ๋“œ์•ผ.

์˜ค๋Š˜ ๋งŒ๋“  ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ„์† ๋ฐœ์ „์‹œ์ผœ๋ด. ์ฒ˜์Œ์—” 70% ์ •ํ™•๋„์—์„œ ์‹œ์ž‘ํ•ด๋„,
๋ฐ์ดํ„ฐ๋ฅผ ๋Š˜๋ฆฌ๊ณ  ๋ชจ๋ธ์„ ๊ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ 90%์— ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿ’ช

๐Ÿ“š ๋” ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด

๋…ผ๋ฌธ: "Speech Emotion Recognition Using Deep Learning" (IEEE ๊ฒ€์ƒ‰)
๋ฐ์ดํ„ฐ์…‹: RAVDESS, IEMOCAP, CREMA-D, MSP-IMPROV, AI Hub ํ•œ๊ตญ์–ด ๊ฐ์ • ์Œ์„ฑ
๋ชจ๋ธ: Wav2Vec2, HuBERT, Whisper (OpenAI), SpeechBrain ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
๋ฒค์น˜๋งˆํฌ: SUPERB (Speech processing Universal PERformance Benchmark)
์ปค๋ฎค๋‹ˆํ‹ฐ: Papers With Code - Speech Emotion Recognition ์„น์…˜

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.