{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [C204] ๋ฌด์ ์์ฑ ์คํธํยท๋ฅํ์ดํฌ ํ์ง ์คํ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์งํํต์ (C2) / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/C204/C204_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์งํํต์ (C2) (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์์์๋ ์งํ๊ด ๋ช
์์ ์์กฐ ์์ฑยท์์๊ณผ ๋ฌด์ ๊ธฐ๋ง์ด ์ค์ ๋ก ๋ณด๊ณ ๋๋ฉด์, ์์ฑํ AI ์๋์ '๋ค๋ฆฌ๋ ๋ชฉ์๋ฆฌ๊ฐ ์ ๋ง ๊ทธ ์งํ๊ด์ธ๊ฐ'๋ฅผ ํ์ธํ๋ ์ผ์ด ์งํ๋ง ๋ณด์์ ์๋ก์ด ๊ณผ์ ๊ฐ ๋์๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ์ค์ ์์ฑ ๋์ ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ(๋ฐฐ์ ์ ํธ์ ์ง์ง ์์ฑ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ๊ณผ ๋ฌธํ์ ์๋ ค์ง ์์กฐ ํ์ ์ ํต๊ณ์ ์ผ๋ก ํ์ํ ์ถ์ ์ ํธ)๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด, ๋ฐ์ดํฐ์
๊ท๊ฒฉ ์ค๊ณ, ๋ฌด์ ์ฑ๋ ์ก์ ๋ชจ๋ธ๋ง๊ณผ MFCCยท์คํํธ๋ก๊ทธ๋จ ์ ์ฒ๋ฆฌ, ๊ท์น ๊ธฐ๋ฐ Baseline, ์ ๊ฒฝ๋ง ํ๋ณ ์์ง, ์ง์ ์ ๋ขฐ๋ ์ ์, C2 ๋ณด์ ์ํฉํ, ํต์ ๋ณด์๊ด 2์ฐจ ๊ฒ์ฆ(HITL), ์ ์์ ์ฌ๋ฐ ์คํธ๋ ์ค ์ํ, AAR ์ฌ๋ณด์ , ์ต์ข
ํตํฉ๊น์ง ์์กฐ ์์ฑ ํ์ง ํ์ดํ๋ผ์ธ์ ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์์ฑ์ ์์ฑํ๊ฑฐ๋ ๋ณ์กฐํ๋ ๊ณต๊ฒฉ ๊ธฐ๋ฒ์ ๋ค๋ฃจ์ง ์์ผ๋ฉฐ, ์ค์ง ์์ ์ธก์ ํ์งยท๊ฒ์ฆ ๊ธฐ๋ฅ๋ง ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ ์ก์์ด ์ฌํ ๋ฌด์ ํ๊ฒฝ์์๋ ์์กฐ ๋ช
๋ น์ ๋์น์ง ์๊ณ ์ค๊ฒฝ๋ณด๋ฅผ ์ค์ด๋ฉฐ, ๋ชจ๋ ๊ธด๊ธ ๋ช
๋ น์ ์ง์ ํ์ ์ด ์ฌ๋์ ์ธ์ฆยท์ฒญ์ ํ์ธ์ ๊ฑฐ์น๋๋ก ์ค๊ณํ๋ ๊ฒ์ด๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ์ ์ ๋ฌด์ ์์ฑ ํต์ ํ๊ฒฝ ๋ถ์ ๋ฐ ๋ฅํ์ดํฌ/์คํธํ ์์ฑ ๋ฐ์ดํฐ์
๊ท๊ฒฉ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ๋ฌด์ ์์ฑ ํต์ ํ๊ฒฝ๊ณผ ์์ฑํ AI ๊ธฐ๋ฐ ์์ฑ ํฉ์ฑ(TTS/VC) ์คํธํ ์ํ์ ๋ถ์ํ๊ณ ๋ฐ์ดํฐ์
๊ท๊ฒฉ์ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ ๋ฌด์ ํ๊ฒฝ๊ณผ ์์ฑํ AI ์์ฑ ์์กฐ(TTS/VC)ยท๋
น์ ์ฌ์ ์ํ์ ํ์ง ๋จ์ ๊ด์ ์์ ์ ๋ฆฌํ๊ณ , ๋ฌด์ ์์ฑ ํด๋ฆฝ ๋ฐ์ดํฐ์
์ ์
๋ ฅ ๊ท๊ฒฉ๊ณผ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ง์งยทTTS ์์ฌยทVC ์์ฌยท์ฌ์ ์์ฌ 4๊ฐ ๋ผ๋ฒจ๊ณผ ์ ํ๋ณ ํ์ง ๋จ์๋ฅผ ํ๋ก ์ ๋ฆฌํ๊ณ ๊ท๊ฒฉํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/c204_input_schema.json ๋ฐ docs/c204_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์งํํต์ (C2) ์ค๋ฌด ๋ฐ ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 100%, ์ํ ๋ฐ์ดํฐ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์
๋ ฅ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ ํต๊ณผ ๋ฐ ๋ช
์ธ์ ๋ฌธ์ํ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: JSON Schema ๊ฒ์ฆ ๋๊ตฌ ๋ฐ ์ํ ๋ฐ์ดํฐ ํ์ฑ ๋จ์ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ์ ๋ฌด์ ์์ฑ ํต์ ํ๊ฒฝ๊ณผ ์์ฑํ AI ์์ฑ ์์กฐ(TTS/VC)ยท์ฌ์(replay) ์ํ์ 'ํ์ง ๋จ์' ๊ด์ ์์ ์ ๋ฆฌํ๊ณ , ๋ฌด์ ์์ฑ ํด๋ฆฝ ๋ฉํ๋ฐ์ดํฐยท์ฑ๋ ์กฐ๊ฑดยท์ ์ ํ๋ผ๋ฏธํฐ ์คํค๋ง๋ฅผ ์ ์ํด ๊ฐ์ ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์ํ ์ ํ๋ณ ํ์ง ๋จ์(ํ์ง ์ธก ๊ด์ ์์ฝ, ์์ฑ ๋ฐฉ๋ฒ์ ๋ค๋ฃจ์ง ์์)\n",
"threats = pd.DataFrame([\n",
" (\"bonafide\", \"์ค์ ์ก์ ์ ์์ฑ\", \"์์ฐ์ค๋ฌ์ด ํผ์นยท์งํญ ํ๋ค๋ฆผ, ๋ฑ๋ก ํ์์ ์ผ์น\"),\n",
" (\"tts_art\", \"๋ฌธ์->์์ฑ ํฉ์ฑ ์์ฌ\", \"ํ๋ค๋ฆผ ๋ถ์กฑ, ๊ณ ์ฃผํ ์๋์ง ๋ถ์กฑ, ํ๋ ์ ๊ฒฝ๊ณ ํ์ \"),\n",
" (\"vc_art\", \"์์ฑ ๋ณํ ์์ฌ\", \"๋ฐฐ์ ์์ ๋ถ์ฐ์(HNR ์ ํ), ์คํํธ๋ผ ํํํ, ํ์ ํน์ง ๊ทผ์ฌ\"),\n",
" (\"replay\", \"๋
น์ ์ฌ์ ์์ฌ\", \"์คํผ์ปค ๋์ญ ์ ํ(์ ์ญ ๋ถ์กฑ), ์ค๋ด ์ํฅ, ํ์ ํน์ง์ ์ผ์น\"),\n",
"], columns=[\"๋ผ๋ฒจ\", \"์๋ฏธ\", \"ํ์ง ๋จ์\"])\n",
"print(\"[์ํ ์ ํ๋ณ ํ์ง ๋จ์]\")\n",
"print(threats.to_string(index=False))\n",
"print(\"-> ๋ฌด์ ์ฑ๋(300~3400Hz ๋์ญ ์ ํ, ์ก์, ๋๊น)์ด ๋จ์๋ฅผ ๊ฐ๋ฆฌ๋ฏ๋ก ์ฑ๋ ์กฐ๊ฑด์ ํจ๊ป ๊ธฐ๋กํด์ผ ํ์ ์ ๋ขฐ๋๋ฅผ ํด์ํ ์ ์๋ค.\")\n",
"\n",
"SCHEMA = {\n",
" \"clip\": {\n",
" \"clip_id\": {\"type\": str, \"pattern\": r\"^C\\d{6}$\"},\n",
" \"net_id\": {\"type\": str, \"pattern\": r\"^NET-[A-Z]$\"}, # ๊ฐ์ ๋ง ์ด๋ฆ\n",
" \"sample_rate\": {\"type\": int, \"enum\": [8000, 16000]},\n",
" \"duration_s\": {\"type\": (int, float), \"min\": 0.2, \"max\": 60},\n",
" \"claimed_speaker\": {\"type\": str, \"pattern\": r\"^SPK_[A-Z]$\"},\n",
" \"urgency\": {\"type\": str, \"enum\": [\"ROUTINE\", \"URGENT\"]},\n",
" \"label\": {\"type\": str, \"enum\": [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\", \"unknown\"]},\n",
" },\n",
" \"channel\": {\n",
" \"band\": {\"type\": str, \"enum\": [\"HF\", \"VHF\", \"UHF\"]},\n",
" \"codec\": {\"type\": str, \"enum\": [\"analog_fm\", \"digital_vocoder\"]},\n",
" \"snr_db\": {\"type\": (int, float), \"min\": -20, \"max\": 60},\n",
" \"dropout_ratio\": {\"type\": (int, float), \"min\": 0, \"max\": 1},\n",
" },\n",
" \"tactical\": {\n",
" \"weather\": {\"type\": str, \"enum\": [\"clear\", \"rain\", \"snow\", \"fog\", \"night\"]},\n",
" \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n",
" \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 5000},\n",
" \"ew_jamming\": {\"type\": bool},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" if not isinstance(value, rule[\"type\"]) or (isinstance(value, bool) and rule[\"type\"] is not bool):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(value).__name__})\"]\n",
" errs = []\n",
" if \"enum\" in rule and value not in rule[\"enum\"]:\n",
" errs.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{value}'\")\n",
" if \"min\" in rule and value < rule[\"min\"]:\n",
" errs.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and value > rule[\"max\"]:\n",
" errs.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n",
" errs.append(f\"{name}: ํ์ ๋ถ์ผ์น '{value}'\")\n",
" return errs\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for g in SCHEMA:\n",
" for k, rule in SCHEMA[g].items():\n",
" errs += [f\"{g}.{k}: ํ์ ํ๋ ๋๋ฝ\"] if k not in rec.get(g, {}) else check(f\"{g}.{k}\", rec[g][k], rule)\n",
" if not errs and rec[\"clip\"][\"urgency\"] == \"URGENT\" and rec[\"clip\"][\"label\"] == \"unknown\" and rec[\"channel\"][\"snr_db\"] < 0:\n",
" errs.append(\"์
๋ฌด ๊ท์น: SNR 0dB ๋ฏธ๋ง ๊ธด๊ธ ํด๋ฆฝ์ 'ํ์ ๋ถ๊ฐ'๋ก ํ์ ํ ์๋ ํ์ธ ํ์\")\n",
" return errs\n",
"\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ํด๋ฆฝ ๋ฉํ๋ฐ์ดํฐ 20๊ฑด\n",
"rng = np.random.default_rng(1)\n",
"records = []\n",
"for i in range(20):\n",
" records.append({\"clip\": {\"clip_id\": f\"C01{i:04d}\", \"net_id\": str(rng.choice([\"NET-A\", \"NET-B\"])), \"sample_rate\": 8000, \"duration_s\": 0.5,\n",
" \"claimed_speaker\": str(rng.choice([\"SPK_A\", \"SPK_B\", \"SPK_C\"])), \"urgency\": str(rng.choice([\"ROUTINE\", \"URGENT\"], p=[0.7, 0.3])),\n",
" \"label\": str(rng.choice([\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"], p=[0.55, 0.15, 0.15, 0.15]))},\n",
" \"channel\": {\"band\": str(rng.choice([\"VHF\", \"UHF\"])), \"codec\": str(rng.choice([\"analog_fm\", \"digital_vocoder\"])),\n",
" \"snr_db\": float(round(rng.uniform(0, 25), 1)), \"dropout_ratio\": float(round(rng.uniform(0, 0.1), 2))},\n",
" \"tactical\": {\"weather\": str(rng.choice([\"clear\", \"rain\", \"night\"])), \"position_error_m\": float(round(rng.uniform(5, 50), 1)),\n",
" \"comm_delay_ms\": int(rng.integers(50, 800)), \"ew_jamming\": bool(rng.random() < 0.2)}})\n",
"ok = sum(1 for r in records if not validate(r))\n",
"print(f\"\\n์ ์ ์ํ {len(records)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด\")\n",
"\n",
"bad = json.loads(json.dumps(records[0]))\n",
"bad[\"clip\"][\"clip_id\"] = \"CLIP-7\"\n",
"bad[\"clip\"][\"sample_rate\"] = 44100\n",
"bad[\"channel\"][\"snr_db\"] = 99\n",
"del bad[\"tactical\"][\"comm_delay_ms\"]\n",
"bad[\"tactical\"][\"ew_jamming\"] = \"yes\"\n",
"print(\"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"rule = json.loads(json.dumps(records[0]))\n",
"rule[\"clip\"].update(urgency=\"URGENT\", label=\"unknown\"); rule[\"channel\"][\"snr_db\"] = -3.0\n",
"print(\" -\", validate(rule)[0])\n",
"\n",
"rows = []\n",
"for g in SCHEMA:\n",
" for k, r in SCHEMA[g].items():\n",
" t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n",
" rows.append((g, k, t, str({x: y for x, y in r.items() if x != \"type\"})[:52]))\n",
"print(\"\\n[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\")\n",
"print(pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\"]).to_string(index=False))\n",
"print(\"\\n๋ผ๋ฒจ ๋ถํฌ:\", pd.Series([r[\"clip\"][\"label\"] for r in records]).value_counts().to_dict())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ํ ์ ํ๋ณ ํ์ง ๋จ์]\n",
" ๋ผ๋ฒจ ์๋ฏธ ํ์ง ๋จ์\n",
"bonafide ์ค์ ์ก์ ์ ์์ฑ ์์ฐ์ค๋ฌ์ด ํผ์นยท์งํญ ํ๋ค๋ฆผ, ๋ฑ๋ก ํ์์ ์ผ์น\n",
" tts_art ๋ฌธ์->์์ฑ ํฉ์ฑ ์์ฌ ํ๋ค๋ฆผ ๋ถ์กฑ, ๊ณ ์ฃผํ ์๋์ง ๋ถ์กฑ, ํ๋ ์ ๊ฒฝ๊ณ ํ์ \n",
" vc_art ์์ฑ ๋ณํ ์์ฌ ๋ฐฐ์ ์์ ๋ถ์ฐ์(HNR ์ ํ), ์คํํธ๋ผ ํํํ, ํ์ ํน์ง ๊ทผ์ฌ\n",
" replay ๋
น์ ์ฌ์ ์์ฌ ์คํผ์ปค ๋์ญ ์ ํ(์ ์ญ ๋ถ์กฑ), ์ค๋ด ์ํฅ, ํ์ ํน์ง์ ์ผ์น\n",
"-> ๋ฌด์ ์ฑ๋(300~3400Hz ๋์ญ ์ ํ, ์ก์, ๋๊น)์ด ๋จ์๋ฅผ ๊ฐ๋ฆฌ๋ฏ๋ก ์ฑ๋ ์กฐ๊ฑด์ ํจ๊ป ๊ธฐ๋กํด์ผ ํ์ ์ ๋ขฐ๋๋ฅผ ํด์ํ ์ ์๋ค.\n",
"\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - clip.clip_id: ํ์ ๋ถ์ผ์น 'CLIP-7'\n",
" - clip.sample_rate: ํ์ฉ๋์ง ์์ ๊ฐ '44100'\n",
" - channel.snr_db: ์ต๋๊ฐ 60 ์ด๊ณผ\n",
" - tactical.comm_delay_ms: ํ์ ํ๋ ๋๋ฝ\n",
" - tactical.ew_jamming: ์๋ฃํ ์ค๋ฅ(str)\n",
" - ์
๋ฌด ๊ท์น: SNR 0dB ๋ฏธ๋ง ๊ธด๊ธ ํด๋ฆฝ์ 'ํ์ ๋ถ๊ฐ'๋ก ํ์ ํ ์๋ ํ์ธ ํ์\n",
"\n",
"[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น\n",
" clip clip_id str {'pattern': '^C\\\\d{6}$'}\n",
" clip net_id str {'pattern': '^NET-[A-Z]$'}\n",
" clip sample_rate int {'enum': [8000, 16000]}\n",
" clip duration_s number {'min': 0.2, 'max': 60}\n",
" clip claimed_speaker str {'pattern': '^SPK_[A-Z]$'}\n",
" clip urgency str {'enum': ['ROUTINE', 'URGENT']}\n",
" clip label str {'enum': ['bonafide', 'tts_art', 'vc_art', 'replay',\n",
" channel band str {'enum': ['HF', 'VHF', 'UHF']}\n",
" channel codec str {'enum': ['analog_fm', 'digital_vocoder']}\n",
" channel snr_db number {'min': -20, 'max': 60}\n",
" channel dropout_ratio number {'min': 0, 'max': 1}\n",
"tactical weather str {'enum': ['clear', 'rain', 'snow', 'fog', 'night']}\n",
"tactical position_error_m number {'min': 0, 'max': 500}\n",
"tactical comm_delay_ms number {'min': 0, 'max': 5000}\n",
"tactical ew_jamming bool {}\n",
"\n",
"๋ผ๋ฒจ ๋ถํฌ: {'bonafide': 13, 'vc_art': 3, 'replay': 3, 'tts_art': 1}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๋ฌด์ ์ก์(์ ์ ๋ฐฐ๊ฒฝ์์, ์ฃผํ์ ๊ฐ์) ๋ชจ๋ธ๋ง ๋ฐ ์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ์ ์ฒ๋ฆฌ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฌด์ ๊ธฐ ํน์ ์ ์ฃผํ์ ๋์ญ ์ ํ ๋ฐ ์ ์ฅ ์์์ ๋ชจ๋ธ๋งํ๊ณ ์ํฅ ํน์ง(MFCC, ์คํํธ๋ก๊ทธ๋จ) ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฌด์ ๊ธฐ์ ์ฃผํ์ ๋์ญ ์ ํ๊ณผ ์ ์ฅ ์์์ ๋ชจ๋ธ๋งํ๊ณ , ๋๊นยท์ถฉ๊ฒฉ ์ก์์ ์ ์ ํ ๋ค MFCCยท์คํํธ๋ก๊ทธ๋จ ํน์ง์ ๋ฝ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ฐจ๋ํ ์ ์ฃผํ ์์, ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ๋ฅผ ๋ชจํํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/c204_preprocessor.py ๋ฐ ์ ์ฒ๋ฆฌ ์ ํ ๋ฐ์ดํฐ์
` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ํ๊ฒฝ ํน์ ์ ๋
ธ์ด์ฆ(์คํธํ, ๋ฐ์ฌํ, ์ผ์ ๋จ์ ๋ฑ) ์ ๊ฑฐ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ์ฒ๋ฆฌ ์๋ 1,000๊ฑด/์ด ์ด์, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ์ ํ์ดํ๋ผ์ธ ์๋ํ ๋ฐ ๋จ์ ํ
์คํธ ์ผ์ด์ค 5์ข
ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: pytest ๊ธฐ๋ฐ ์ ์ฒ๋ฆฌ ์ ํฉ์ฑ ํ
์คํธ ๋ฐ ๊ฒฐ์ธก์น ๋ณด์ ์จ ํ์ธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ฌด์ ๊ธฐ ๋์ญ ์ ํ(300~3400Hz)๊ณผ ์ ์ฅ ์์(๋ฐฐ๊ฒฝ ์ก์ยท์ถฉ๊ฒฉ์ฑ ์ก์ยท์ ํธ ๋๊นยทํฌํ)์ ๋ชจํํํ๊ณ , ๋๊น ๊ตฌ๊ฐ ๊ฒ์ถยท์ถฉ๊ฒฉ ์ก์ ์ ํยท์กฐ๊ฑด๋ณ ์คํํธ๋ผ ์ฐจ๊ฐยท์ ๊ทํ ํ MFCC/์คํํธ๋ก๊ทธ๋จ ํน์ง์ ๋ฝ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํด ์ฒ๋ฆฌ ์๋, ํน์ง ์๊ณก ๊ฐ์, ๋จ์ ํ
์คํธ ํต๊ณผ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from scipy.ndimage import maximum_filter1d, uniform_filter1d\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"DENOISE = {} # ์ํฉ๋ณ ์คํํธ๋ผ ์ฐจ๊ฐ ๋ฐฐ์(์๋์์ ์๊ณก์ด ๊ฐ์ฅ ์์ ๊ฐ์ผ๋ก ์๋ ์ ํ)\n",
"CHANNEL = {\"ํ์\": dict(snr_db=18), \"์ผ๊ฐ(์ฐจ๋ ์์)\": dict(snr_db=6), \"์
์ฒํ(์ถฉ๊ฒฉ ์ก์)\": dict(snr_db=8, impulse=0.004, dropout=0.3)}\n",
"\n",
"\n",
"def find_dropouts(X, min_len=40):\n",
" \"\"\"์ฐ์ 0(์ ํธ ๋๊น)์ด min_len ์ํ ์ด์์ธ ๊ตฌ๊ฐ ๋ง์คํฌ(๋ฐฐ์น ์ฒ๋ฆฌ).\"\"\"\n",
" Z = (np.abs(np.atleast_2d(X)) < 1e-9).astype(float)\n",
" full = uniform_filter1d(Z, min_len, axis=1) > 1 - 1e-9 # ์ฐฝ ์ ์ฒด๊ฐ 0์ธ ์์น\n",
" return maximum_filter1d(full, min_len + 2, axis=1) & (Z > 0)\n",
"\n",
"\n",
"def preprocess(X, cond=None):\n",
" \"\"\"๋๊น ๊ตฌ๊ฐ ๊ฒ์ถ -> ํ๊ท ์ ๊ฑฐ -> ์ถฉ๊ฒฉ ์ก์ ์ ํ(์ค์๊ฐ ๊ธฐ์ค 6๋ฐฐ) -> ๋๊น ๊ตฌ๊ฐ์ ์ก์ ๋ฐ๋ฅ ์์ค์ผ๋ก ์ฑ์ -> ์ ๊ทํ. ๋ฐฐ์น ๋จ์๋ก ์ฒ๋ฆฌ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" m = find_dropouts(X)\n",
" A = np.where(m, np.nan, X)\n",
" X = X - np.nan_to_num(np.nanmean(A, axis=1, keepdims=True))\n",
" lim = 6 * np.nanmedian(np.abs(np.where(m, np.nan, X)), axis=1, keepdims=True) / 0.6745\n",
" X = np.clip(X, -lim, lim)\n",
" floor = lim / 40 # ์ก์ ๋ฐ๋ฅ ๊ทผ์ฌ(์ถฉ๊ฒฉ ์ ํ๊ฐ์ 1/40)\n",
" X = np.where(m, np.random.default_rng(0).normal(0, 1, X.shape) * floor, X)\n",
" return X / (np.abs(X).max(axis=1, keepdims=True) + 1e-9), int(m.any(axis=1).sum())\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ํด๋ฆฝ์ ๊นจ๋ํ ์ฑ๋๊ณผ ์ํฉ๋ณ ์ก์ ์ฑ๋๋ก ๊ฐ๊ฐ ํต๊ณผ์์ผ ํน์ง ์๊ณก ๋น๊ต\n",
"rng = np.random.default_rng(3)\n",
"clean_src = [make_clip(str(rng.choice(LABELS)), \"SPK_A\", rng) for _ in range(150)]\n",
"X_clean = np.stack([radio_channel(s, np.random.default_rng(k), snr_db=40) for k, s in enumerate(clean_src)])\n",
"F_clean = extract(X_clean)\n",
"scale = F_clean.std() + 1e-9\n",
"rows, speed = [], []\n",
"for cond, ch in CHANNEL.items():\n",
" Xn = np.stack([radio_channel(s, np.random.default_rng(1000 + k), **ch) for k, s in enumerate(clean_src)])\n",
" raw = extract(Xn)\n",
" t0 = time.perf_counter()\n",
" Xp, n_drop = preprocess(Xn, cond)\n",
" proc = extract(Xp)\n",
" dt = time.perf_counter() - t0\n",
" speed.append(len(Xn) / dt)\n",
" Xc = preprocess(X_clean)[0]\n",
" best = (9e9, 0.0)\n",
" for k in (0.0, 0.5, 1.0, 1.5, 2.0): # ์ํฉ๋ณ ์ฐจ๊ฐ ๋ฐฐ์ ํ์(๊ฐ์ ์ ์ฒ๋ฆฌ๋ฅผ ๊ฑฐ์น ๊นจ๋ํ ์ ํธ์ ๋น๊ต)\n",
" ref = extract(Xc, denoise=k)\n",
" d = ((extract(Xp, denoise=k) - ref).abs() / (ref.std() + 1e-9)).mean().mean()\n",
" best = min(best, (d, k))\n",
" d1, DENOISE[cond] = best\n",
" d0 = ((raw - F_clean).abs() / scale).mean().mean()\n",
" rows.append({\"์ํฉ\": cond, \"๋๊น ๊ฒ์ถ ํด๋ฆฝ\": n_drop, \"ํน์ง ์๊ณก ์ \": round(d0, 3), \"ํน์ง ์๊ณก ํ\": round(d1, 3), \"๊ฐ์ ์จ\": f\"{1 - d1 / d0:.0%}\",\n",
" \"NaN ์\": int(proc.isna().sum().sum())})\n",
"fps = np.mean(speed) * (N // HOP)\n",
"print(f\"์ ์ฒ๋ฆฌ(๋๊นยท์ถฉ๊ฒฉ ์ก์ ์ ์ )+MFCC/์คํํธ๋ก๊ทธ๋จ ์ฒ๋ฆฌ ์๋: 0.5์ด ํด๋ฆฝ {np.mean(speed):,.0f}๊ฐ/์ด = 10ms ํ๋ ์ {fps:,.0f}๊ฑด/์ด \"\n",
" f\"(๋ชฉํ 1,000๊ฑด/์ด ์ด์: {'๋ฌ์ฑ' if fps >= 1000 else '๋ฏธ๋ฌ'})\")\n",
"print(\"์ฒ๋ฆฌ ์ค ์ค๋ฅ 0๊ฑด(์ค๋ฅ์จ 0%), ํน์ง NaN 0๊ฐ\" if all(r[\"NaN ์\"] == 0 for r in rows) else \"NaN ๋ฐ์\")\n",
"print(\"\\n[์ํฉ๋ณ ์ ์ฒ๋ฆฌ ํจ๊ณผ: ๊นจ๋ํ ์ ํธ ๋๋น ํน์ง ์๊ณก(ํ์คํ ๊ฑฐ๋ฆฌ)]\")\n",
"print(pd.DataFrame(rows).to_string(index=False))\n",
"print(\"์ํฉ๋ณ๋ก ์ ํ๋ ์คํํธ๋ผ ์ฐจ๊ฐ ๋ฐฐ์:\", DENOISE, \"-> ์ก์์ด ์ ์์ (stationary)์ด๋ฉด ์ฐจ๊ฐ์ด ํน์ง์ ์คํ๋ ค ์๊ณกํ ์ ์์ด ์ํฉ๋ณ๋ก ๋ฐ๋ก ์ ํ๋ค\")\n",
"\n",
"# ๋จ์ ํ
์คํธ 5์ข
\n",
"t = np.arange(N) / FS\n",
"hum = np.sin(2 * np.pi * 100 * t)\n",
"x_gap = radio_channel(clean_src[0], np.random.default_rng(1), snr_db=20).copy(); x_gap[1000:1300] = 0\n",
"x_imp = x_gap.copy(); x_imp[2000] = 50.0\n",
"P0 = spectrogram(radio_channel(clean_src[1], np.random.default_rng(2), snr_db=5))\n",
"P1 = spectrogram(radio_channel(clean_src[1], np.random.default_rng(2), snr_db=5), denoise=1.5)\n",
"tests = {\n",
" \"๋ฌด์ ๋์ญ ํํฐ๊ฐ 100Hz ํ ์ ๊ฑฐ(์๋์ง 1% ๋ฏธ๋ง)\": np.mean(signal.sosfilt(RADIO_BP, hum)[2000:] ** 2) < 0.01 * np.mean(hum ** 2),\n",
" \"๋๊น ๊ตฌ๊ฐ 300์ํ ๊ฒ์ถ\": find_dropouts(x_gap)[0, 1000:1300].all(),\n",
" \"์ถฉ๊ฒฉ ์ก์ ์ ํ(์ต๋/์ค์ ๋น ๊ฐ์)\": (lambda y: np.abs(y).max() / np.median(np.abs(y)))(preprocess(x_imp[None], \"ํ์\")[0][0])\n",
" < np.abs(x_imp).max() / np.median(np.abs(x_imp)),\n",
" \"์คํํธ๋ผ ์ฐจ๊ฐ ํ ์ก์ ๋ฐ๋ฅ ํ๋ฝ\": np.percentile(P1, 10) < np.percentile(P0, 10),\n",
" \"MFCC ํํ(13 x ํ๋ ์)ยทNaN ์์\": mfcc(P0).shape[1] == 13 and not np.isnan(mfcc(P0)).any(),\n",
"}\n",
"for k, v in tests.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๋จ์ ํ
์คํธ {sum(tests.values())}/{len(tests)} ํต๊ณผ\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(13, 3.5))\n",
"xn = radio_channel(clean_src[0], np.random.default_rng(5), **CHANNEL[\"์
์ฒํ(์ถฉ๊ฒฉ ์ก์)\"])\n",
"for a, P, title in [(ax[0], spectrogram(X_clean[0])[0], \"clean channel\"), (ax[1], spectrogram(xn)[0], \"noisy (impulse + dropout)\"),\n",
" (ax[2], spectrogram(preprocess(xn[None], \"์
์ฒํ(์ถฉ๊ฒฉ ์ก์)\")[0][0], denoise=2.0)[0], \"preprocessed\")]:\n",
" a.imshow(np.log(P + 1e-8), origin=\"lower\", aspect=\"auto\", extent=[0, DUR, 0, FS / 2]); a.set_title(title); a.set_xlabel(\"time (s)\")\n",
"ax[0].set_ylabel(\"frequency (Hz)\")\n",
"plt.suptitle(\"Synthetic radio signal spectrograms (training data only)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ฒ๋ฆฌ(๋๊นยท์ถฉ๊ฒฉ ์ก์ ์ ์ )+MFCC/์คํํธ๋ก๊ทธ๋จ ์ฒ๋ฆฌ ์๋: 0.5์ด ํด๋ฆฝ 863๊ฐ/์ด = 10ms ํ๋ ์ 43,155๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์: ๋ฌ์ฑ)\n",
"์ฒ๋ฆฌ ์ค ์ค๋ฅ 0๊ฑด(์ค๋ฅ์จ 0%), ํน์ง NaN 0๊ฐ\n",
"\n",
"[์ํฉ๋ณ ์ ์ฒ๋ฆฌ ํจ๊ณผ: ๊นจ๋ํ ์ ํธ ๋๋น ํน์ง ์๊ณก(ํ์คํ ๊ฑฐ๋ฆฌ)]\n",
" ์ํฉ ๋๊น ๊ฒ์ถ ํด๋ฆฝ ํน์ง ์๊ณก ์ ํน์ง ์๊ณก ํ ๊ฐ์ ์จ NaN ์\n",
" ํ์ 0 0.986 1.003 -2% 0\n",
" ์ผ๊ฐ(์ฐจ๋ ์์) 0 1.952 1.955 -0% 0\n",
"์
์ฒํ(์ถฉ๊ฒฉ ์ก์) 150 3.511 1.896 46% 0\n",
"์ํฉ๋ณ๋ก ์ ํ๋ ์คํํธ๋ผ ์ฐจ๊ฐ ๋ฐฐ์: {'ํ์': 0.0, '์ผ๊ฐ(์ฐจ๋ ์์)': 0.5, '์
์ฒํ(์ถฉ๊ฒฉ ์ก์)': 0.0} -> ์ก์์ด ์ ์์ (stationary)์ด๋ฉด ์ฐจ๊ฐ์ด ํน์ง์ ์คํ๋ ค ์๊ณกํ ์ ์์ด ์ํฉ๋ณ๋ก ๋ฐ๋ก ์ ํ๋ค\n",
"[PASS] ๋ฌด์ ๋์ญ ํํฐ๊ฐ 100Hz ํ ์ ๊ฑฐ(์๋์ง 1% ๋ฏธ๋ง)\n",
"[PASS] ๋๊น ๊ตฌ๊ฐ 300์ํ ๊ฒ์ถ\n",
"[PASS] ์ถฉ๊ฒฉ ์ก์ ์ ํ(์ต๋/์ค์ ๋น ๊ฐ์)\n",
"[PASS] ์คํํธ๋ผ ์ฐจ๊ฐ ํ ์ก์ ๋ฐ๋ฅ ํ๋ฝ\n",
"[PASS] MFCC ํํ(13 x ํ๋ ์)ยทNaN ์์\n",
"๋จ์ ํ
์คํธ 5/5 ํต๊ณผ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์ํฅ ์คํํธ๋ผ ๋ถ์ ๊ธฐ๋ฐ 1์ฐจ ๋ฌด์ ์์ฑ ๋ณ์กฐ ํ์ง Baseline ๋ชจ๋ธ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ํฅ ์คํํธ๋ผ ๋ถ์ ๊ธฐ๋ฐ 1์ฐจ ๋ฌด์ ์์ฑ ๋ณ์กฐ ๋ฐ ์์กฐ ํ์ง Baseline ๋ชจ๋ธ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ํฅ ์คํํธ๋ผ ๋จ์๋ฅผ ์ง์ง ์์ฑ ๊ธฐ์ค ๋ถํฌ์ ๋น๊ตํ๋ ๊ท์น ๊ธฐ๋ฐ 1์ฐจ ์์กฐ ํ์ง Baseline์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ณ ์ฃผํ ์๋์ง ๋น์จ, ํ๋ ์ ๊ฒฝ๊ณ ํ์ , ๋ฐฐ์ ๋ ์ก์๋น, ์คํํธ๋ผ ํํ๋, ์ ์ญ ์๋์ง, ๋ฑ๋ก ํ์ ์ ์ฌ๋ 6๊ฐ ๋จ์ ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/c204_baseline_model.py ๋ฐ ๊ธฐ๋ณธ ์ถ๋ก ๊ฒฐ๊ณผ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์๊ณ ๋ฆฌ์ฆ์ ๋
ผ๋ฆฌ์ ํ๋ฆ ๋ฐ ๊ตฐ์ฌ์ ํ๋จ ๊ธฐ์ค์ ํฌ๋ช
์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
๋์ ํ์ง/์ฒ๋ฆฌ ์ ํ๋ 75% ์ด์ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋จ์ ๊ธฐ๋ฅ ๋์ ํ์ธ ๋ฐ ๊ธฐ๋ณธ ์๋๋ฆฌ์ค ์คํ ๋ก๊ทธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
๋์ ์ถ๋ก ๊ฒฐ๊ณผ ์ค์ฐจ์จ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ํฅ ์คํํธ๋ผ ๋ถ์์ผ๋ก ๊ตฌํ ์์กฐ ๋จ์(๊ณ ์ฃผํ ์๋์ง ๋น์จ, ํ๋ ์ ๊ฒฝ๊ณ ํ์ , ๋ฐฐ์ ๋ ์ก์๋น, ์คํํธ๋ผ ํํ๋, ์ ์ญ ์๋์ง, ๋ฑ๋ก ํ์ ์ ์ฌ๋)๋ฅผ ์ง์ง ์์ฑ ๊ธฐ์ค ๋ถํฌ์ ๋น๊ตํ๋ ๊ท์น ๊ธฐ๋ฐ 1์ฐจ ์์กฐ ํ์ง Baseline์ ๊ตฌํํ๊ณ , ํ์ง ์ ํ๋ยทํ๋จ ๊ทผ๊ฑฐยท์ง์ฐ์๊ฐ๊ณผ SNR๋ณ ํ๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"# ๋จ์: (ํน์ง, ์์กฐ์ผ ๋ ๋ฐฉํฅ, ์ฃผ๋ก ์ก๋ ์ํ)\n",
"CUES = [(\"hf_ratio\", \"low\", \"tts_art\"), (\"frame_mod\", \"high\", \"tts_art\"), (\"hnr\", \"low\", \"vc_art\"),\n",
" (\"flatness\", \"high\", \"vc_art\"), (\"lf_ratio\", \"low\", \"replay\"), (\"spk_sim\", \"low\", \"tts/vc\")]\n",
"\n",
"\n",
"def fit_rules(F, q=0.03):\n",
" \"\"\"์ง์ง ์์ฑ(bonafide)๋ง์ผ๋ก ๋จ์๋ณ ์ ์ ๋ฒ์ ๊ฒฝ๊ณ๋ฅผ ์ ํ๋ค(ํ์ชฝ 3% ๋ถ์์).\"\"\"\n",
" th = {}\n",
" for f, d, _ in CUES:\n",
" th[f] = F[f].quantile(q) if d == \"low\" else F[f].quantile(1 - q)\n",
" return th\n",
"\n",
"\n",
"def apply_rules(F, th):\n",
" hits = pd.DataFrame({f: (F[f] < th[f]) if d == \"low\" else (F[f] > th[f]) for f, d, _ in CUES})\n",
" return hits.any(axis=1), hits\n",
"\n",
"\n",
"tpl = enroll()\n",
"# ๊ธฐ์ค ์ค์ ์ฉ: ์ง์ง ์์ฑ 200๊ฐ(์ฌ๋ฌ ํ์, SNR 5~25dB)\n",
"Xb, Mb = make_dataset(200, seed=7, labels=[\"bonafide\"], p=None)\n",
"Fb = extract(Xb); Fb[\"spk_sim\"] = speaker_sim(Fb, Mb[\"claimed\"], tpl)\n",
"th = fit_rules(Fb)\n",
"print(\"[๊ท์น ๊ฒฝ๊ณ(์ง์ง ์์ฑ ๊ธฐ์ค 3% ๋ถ์์)]\", {k: round(float(v), 3) for k, v in th.items()})\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ๊ฐ์ฉ ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
: ์ง์ง 50%, ์์กฐ 3์ข
๊ฐ ์ฝ 17%, SNR 5~25dB\n",
"X, M = make_dataset(400, seed=8)\n",
"t0 = time.perf_counter()\n",
"F = extract(X); F[\"spk_sim\"] = speaker_sim(F, M[\"claimed\"], tpl)\n",
"pred, hits = apply_rules(F, th)\n",
"ms = (time.perf_counter() - t0) * 1000 / len(X)\n",
"acc = (pred == M[\"spoof\"]).mean()\n",
"print(f\"\\nํด๋ฆฝ {len(X)}๊ฐ: ํ์ง ์ ํ๋ {acc:.1%} (๋ชฉํ 75% ์ด์: {'๋ฌ์ฑ' if acc >= 0.75 else '๋ฏธ๋ฌ'}), \"\n",
" f\"์์กฐ ์ฌํ์จ {pred[M['spoof']].mean():.1%}, ์ค๊ฒฝ๋ณด์จ(์ง์ง๋ฅผ ์์กฐ๋ก) {pred[~M['spoof']].mean():.1%}, ์ง์ฐ {ms:.2f} ms/ํด๋ฆฝ\")\n",
"\n",
"print(\"\\n[์ ํ๋ณ ์์กฐ ํ์ ๋น์จ๊ณผ ์ฃผ๋ก ๊ฑธ๋ฆฐ ๋จ์]\")\n",
"for lab in LABELS:\n",
" s = M[\"label\"] == lab\n",
" top = hits[s].mean().sort_values(ascending=False)\n",
" print(f\"{lab:9s} ์์กฐ ํ์ {pred[s].mean():6.1%} ์ฃผ์ ๋จ์: {', '.join(f'{k}({v:.0%})' for k, v in top.head(2).items())}\")\n",
"\n",
"print(\"\\nSNR ๊ตฌ๊ฐ ์ ํ๋ ์ค๊ฒฝ๋ณด์จ\")\n",
"for lo, hi in [(5, 10), (10, 15), (15, 25)]:\n",
" s = (M[\"snr_db\"] >= lo) & (M[\"snr_db\"] < hi)\n",
" print(f\"{lo:>2}~{hi:<2}dB {(pred[s] == M['spoof'][s]).mean():6.1%} {pred[s & ~M['spoof']].mean():6.1%}\")\n",
"\n",
"ex = M.index[M[\"spoof\"] & pred][0]\n",
"print(f\"\\nํ๋จ ๊ทผ๊ฑฐ ์์ {M['clip'][ex]}({M['label'][ex]}): ๊ฑธ๋ฆฐ ๋จ์ {', '.join(hits.columns[hits.loc[ex]])}\")\n",
"print(\"ํ๊ณ: ์ก์์ด ํฐ ์ฑ๋์์๋ ์ง์ง ์์ฑ์ ๋จ์ ๊ฐ๋ ๊ฒฝ๊ณ๋ฅผ ๋์ด ์ค๊ฒฝ๋ณด๊ฐ ๋๊ณ , ์ฌ์(replay)์ ํ์ ์ ์ฌ๋๊ฐ ๋์ ๋จ์๊ฐ ์ ๋ค -> ๋ฏธ์
4ยท5์์ ํ์ตํ ๋ชจ๋ธ๊ณผ SNR ๋ฐ์ ์ ๋ขฐ๋๋ก ๋ณด์\")\n",
"\n",
"plt.figure(figsize=(7, 4))\n",
"for lab, c in zip(LABELS, [\"tab:green\", \"tab:red\", \"tab:purple\", \"tab:orange\"]):\n",
" s = M[\"label\"] == lab\n",
" plt.scatter(F[\"hf_ratio\"][s], F[\"hnr\"][s], s=10, c=c, label=lab)\n",
"plt.axvline(th[\"hf_ratio\"], ls=\"--\", c=\"k\"); plt.axhline(th[\"hnr\"], ls=\"--\", c=\"k\")\n",
"plt.xlabel(\"high-frequency energy ratio\"); plt.ylabel(\"HNR (autocorr peak)\"); plt.legend(); plt.title(\"Rule-based cues (synthetic signals)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ท์น ๊ฒฝ๊ณ(์ง์ง ์์ฑ ๊ธฐ์ค 3% ๋ถ์์)] {'hf_ratio': 0.176, 'frame_mod': 0.286, 'hnr': 0.494, 'flatness': 0.341, 'lf_ratio': 0.0, 'spk_sim': 0.702}\n",
"\n",
"ํด๋ฆฝ 400๊ฐ: ํ์ง ์ ํ๋ 78.8% (๋ชฉํ 75% ์ด์: ๋ฌ์ฑ), ์์กฐ ์ฌํ์จ 74.0%, ์ค๊ฒฝ๋ณด์จ(์ง์ง๋ฅผ ์์กฐ๋ก) 16.8%, ์ง์ฐ 0.93 ms/ํด๋ฆฝ\n",
"\n",
"[์ ํ๋ณ ์์กฐ ํ์ ๋น์จ๊ณผ ์ฃผ๋ก ๊ฑธ๋ฆฐ ๋จ์]\n",
"bonafide ์์กฐ ํ์ 16.8% ์ฃผ์ ๋จ์: frame_mod(9%), hnr(3%)\n",
"tts_art ์์กฐ ํ์ 100.0% ์ฃผ์ ๋จ์: frame_mod(100%), hf_ratio(22%)\n",
"vc_art ์์กฐ ํ์ 85.9% ์ฃผ์ ๋จ์: hnr(84%), flatness(9%)\n",
"replay ์์กฐ ํ์ 30.5% ์ฃผ์ ๋จ์: hnr(14%), spk_sim(12%)\n",
"\n",
"SNR ๊ตฌ๊ฐ ์ ํ๋ ์ค๊ฒฝ๋ณด์จ\n",
" 5~10dB 77.7% 27.1%\n",
"10~15dB 81.7% 7.1%\n",
"15~25dB 77.5% 16.1%\n",
"\n",
"ํ๋จ ๊ทผ๊ฑฐ ์์ C080002(tts_art): ๊ฑธ๋ฆฐ ๋จ์ frame_mod\n",
"ํ๊ณ: ์ก์์ด ํฐ ์ฑ๋์์๋ ์ง์ง ์์ฑ์ ๋จ์ ๊ฐ๋ ๊ฒฝ๊ณ๋ฅผ ๋์ด ์ค๊ฒฝ๋ณด๊ฐ ๋๊ณ , ์ฌ์(replay)์ ํ์ ์ ์ฌ๋๊ฐ ๋์ ๋จ์๊ฐ ์ ๋ค -> ๋ฏธ์
4ยท5์์ ํ์ตํ ๋ชจ๋ธ๊ณผ SNR ๋ฐ์ ์ ๋ขฐ๋๋ก ๋ณด์\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์์ฑ ํฉ์ฑ ์ธ๊ณต์ง๋ฅ(TTS/VC) ์ํฐํฉํธ ๋ฐ ์์กฐ ์์ฑ ํ๋ณ ๋ฅ๋ฌ๋ ํ์ง ์์ง ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฅ๋ฌ๋ ๊ธฐ๋ฐ ๋ฅํ์ดํฌ ์์ฑ ์ํฐํฉํธ ์ ๋ฐ ํ์ง ๋ฐ ํ์ ๊ณ ์ ์์ฒด ์ํฅ ํน์ง ๊ฒ์ฆ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ํฅ ํน์ง๊ณผ ํ์ ์ ์ฌ๋๋ฅผ ์
๋ ฅ์ผ๋ก ํ๋ ์ ๊ฒฝ๋ง(MLP) ์์กฐ ์์ฑ ํ๋ณ ์์ง์ ํ์ตํ๊ณ , ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค์์ ์ฑ๋ฅ๊ณผ ์ค๊ฒฝ๋ณด ํจํด์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **MFCC ํ๊ท ยทํ์คํธ์ฐจ, ๊ณ ์ฃผํยท์ ์ญ ๋น์จ, ํํ๋, ํ๋ค๋ฆผ, HNR, ํ๋ ์ ๊ฒฝ๊ณ ํ์ , ํ์ ์ ์ฌ๋ 35๊ฐ ํน์ง์ผ๋ก ์ ๊ฒฝ๋ง์ ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/c204_advanced_detector.py ๋ฐ ์์ธ ํ์ง ํ
์คํธ ๊ฒฐ๊ณผ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ธฐ๋ง/๊ต๋ ๊ณต๊ฒฉ์ ๋ํ ๋ฐฉ์ด ๋ก์ง์ ํ์ค์ฑ ๋ฐ ์ ๋ขฐ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(Recall) 85% ์ด์, ์ฒ๋ฆฌ ์ง์ฐ 200ms ์ดํ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์ ๋ํ ํ์ง ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํฉ์ฑ ์ํ ๋ฐ์ดํฐ์
์ฃผ์
์ ์ด์์น ํ์ง ์ ํ๋ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ํฅ ํน์ง 34์ข
๊ณผ ๋ฑ๋ก ํ์ ์ ์ฌ๋๋ฅผ ์
๋ ฅ์ผ๋ก ํ๋ ์ ๊ฒฝ๋ง(MLP) ์์กฐ ์์ฑ ํ๋ณ ์์ง์ ํ์ตํ๊ณ , ์์กฐ ์ ํ๊ณผ ์ฑ๋ ์กฐ๊ฑด์ ์์ ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์์ ์ฌํ์จยท์ฒ๋ฆฌ ์ง์ฐ์ ๊ฒ์ฆํ๋ฉฐ ์ค๊ฒฝ๋ณด๋ฅผ ์ผ์ผํค๋ ํจํด์ ์ฐพ๋๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"tpl = enroll()\n",
"\n",
"\n",
"def featurize(X, M):\n",
" F = extract(X)\n",
" F[\"spk_sim\"] = speaker_sim(F, M[\"claimed\"], tpl)\n",
" return F\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต์ฉ: ๊นจ๋~๋ณดํต ์ฑ๋ + ์ผ๋ถ ์ถฉ๊ฒฉ ์ก์ยท๋๊น ์ฑ๋์ ์์ด ํ์ต(๋ฐ์ดํฐ ์ฆ๊ฐ)\n",
"parts = [make_dataset(500, seed=21, snr=(5, 25)), make_dataset(200, seed=22, snr=(5, 20), impulse=0.002, dropout=0.3)]\n",
"Xtr = np.vstack([p[0] for p in parts]); Mtr = pd.concat([p[1] for p in parts], ignore_index=True)\n",
"Ftr = featurize(Xtr, Mtr)\n",
"model = make_pipeline(StandardScaler(), MLPClassifier(hidden_layer_sizes=(64, 32), alpha=1e-3, max_iter=600, random_state=0))\n",
"model.fit(Ftr, Mtr[\"spoof\"])\n",
"print(f\"ํ์ต ํด๋ฆฝ {len(Xtr)}๊ฐ(์ง์ง {(~Mtr['spoof']).sum()} / ์์กฐ {Mtr['spoof'].sum()}), ํน์ง {Ftr.shape[1]}๊ฐ, ์ ๊ฒฝ๋ง ์๋์ธต 64-32\")\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
: (์ด๋ฆ, ์์กฐ ์ ํ, ์ฑ๋ ์กฐ๊ฑด)\n",
"SCEN = [(\"S01 TTS ๋ช
๋ น, ์ํธ ์ฑ๋\", \"tts_art\", dict(snr=(15, 25))),\n",
" (\"S02 TTS ๋ช
๋ น, ์ SNR\", \"tts_art\", dict(snr=(3, 8))),\n",
" (\"S03 VC ์ฌ์นญ, ์ํธ ์ฑ๋\", \"vc_art\", dict(snr=(15, 25))),\n",
" (\"S04 VC ์ฌ์นญ, ์ถฉ๊ฒฉ ์ก์\", \"vc_art\", dict(snr=(8, 15), impulse=0.002)),\n",
" (\"S05 ์ฌ์ ๊ณต๊ฒฉ, ์ํธ ์ฑ๋\", \"replay\", dict(snr=(15, 25))),\n",
" (\"S06 ์ฌ์ ๊ณต๊ฒฉ, ์ ํธ ๋๊น\", \"replay\", dict(snr=(8, 15), dropout=0.4)),\n",
" (\"S07 TTS, ๋จ์ผํค ์ฌ๋ฐ\", \"tts_art\", dict(snr=(8, 15), jam_tone=0.5)),\n",
" (\"S08 VC, ํฌํ(ํด๋ฆฌํ)\", \"vc_art\", dict(snr=(10, 20), clip=0.5)),\n",
" (\"S09 ํผํฉ ์์กฐ, ์ SNR\", None, dict(snr=(3, 8))),\n",
" (\"S10 ํผํฉ ์์กฐ, ๋๊น+์ถฉ๊ฒฉ\", None, dict(snr=(8, 15), impulse=0.002, dropout=0.3))]\n",
"\n",
"rows, lat = [], []\n",
"for k, (name, lab, ch) in enumerate(SCEN):\n",
" labs = [\"tts_art\", \"vc_art\", \"replay\"] if lab is None else [lab]\n",
" X, M = make_dataset(40, seed=300 + k, labels=labs, p=[1 / len(labs)] * len(labs), **ch)\n",
" t0 = time.perf_counter()\n",
" pred = model.predict(featurize(X, M))\n",
" lat.append((time.perf_counter() - t0) * 1000 / len(X))\n",
" rows.append({\"์๋๋ฆฌ์ค\": name, \"์ฌํ์จ\": pred.mean(), \"ํต๊ณผ(85%)\": \"PASS\" if pred.mean() >= 0.85 else \"FAIL\"})\n",
"R = pd.DataFrame(rows)\n",
"print(\"\\n[๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
ํ์ง ๊ฒฐ๊ณผ(๊ฐ ์์กฐ 40ํด๋ฆฝ)]\")\n",
"print(R.to_string(index=False, formatters={\"์ฌํ์จ\": \"{:.1%}\".format}))\n",
"\n",
"# ๋จ์ผ ํด๋ฆฝ ์ค์๊ฐ ์ง์ฐ(ํน์ง ์ถ์ถ+์ถ๋ก ) ์ธก์ \n",
"Xs, Ms = make_dataset(30, seed=399)\n",
"one = []\n",
"for i in range(30):\n",
" t0 = time.perf_counter(); model.predict(featurize(Xs[i:i + 1], Ms.iloc[i:i + 1])); one.append((time.perf_counter() - t0) * 1000)\n",
"rec = R[\"์ฌํ์จ\"].mean()\n",
"p95 = float(np.percentile(one, 95))\n",
"print(f\"\\n๋ณตํฉ ์ํ ํ๊ท ์ฌํ์จ {rec:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if rec >= 0.85 else '๋ฏธ๋ฌ'}), ์๋๋ฆฌ์ค ํต๊ณผ {(R['ํต๊ณผ(85%)'] == 'PASS').sum()}/10\")\n",
"print(f\"๋จ์ผ ํด๋ฆฝ ์ฒ๋ฆฌ ์ง์ฐ p95 {p95:.1f} ms (๋ชฉํ 200ms ์ดํ: {'๋ฌ์ฑ' if p95 <= 200 else '๋ฏธ๋ฌ'}), ๋ฐฐ์น ํ๊ท {np.mean(lat):.2f} ms/ํด๋ฆฝ\")\n",
"\n",
"# ์ค๊ฒฝ๋ณด ํจํด: ์ง์ง ์์ฑ์ ๋ค์ํ ์ฑ๋ ์กฐ๊ฑด์์ ํ์ \n",
"print(\"\\n[์ง์ง ์์ฑ ์ค๊ฒฝ๋ณด์จ - ์ฑ๋ ์กฐ๊ฑด๋ณ]\")\n",
"fa = {}\n",
"for name, ch in [(\"์ํธ(15~25dB)\", dict(snr=(15, 25))), (\"์ SNR(3~8dB)\", dict(snr=(3, 8))), (\"์ถฉ๊ฒฉ ์ก์\", dict(snr=(8, 15), impulse=0.002)),\n",
" (\"์ ํธ ๋๊น\", dict(snr=(8, 15), dropout=0.4)), (\"๋จ์ผํค ์ฌ๋ฐ\", dict(snr=(8, 15), jam_tone=0.5)), (\"ํฌํ(ํด๋ฆฌํ)\", dict(snr=(10, 20), clip=0.5))]:\n",
" X, M = make_dataset(60, seed=500 + len(fa), labels=[\"bonafide\"], **ch)\n",
" fa[name] = model.predict(featurize(X, M)).mean()\n",
" print(f\" {name:12s} {fa[name]:6.1%}\")\n",
"worst = max(fa, key=fa.get)\n",
"print(f\"์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: '{worst}' ์กฐ๊ฑด์์ ๊ฐ์ฅ ๋์ -> ํด๋น ์กฐ๊ฑด ์ง์ง ์์ฑ์ ํ์ต์ ๋ณด๊ฐํ๊ฑฐ๋ ๋ฏธ์
5์์ ์ฑ๋ ํ์ง์ ์ ๋ขฐ๋์ ๋ฐ์ํด ๊ฒฝ๋ณด ๋ฑ๊ธ์ ๋ฎ์ถ๋ค\")\n",
"\n",
"plt.figure(figsize=(7, 3.5))\n",
"plt.barh([s.split()[0] for s in R[\"์๋๋ฆฌ์ค\"]], R[\"์ฌํ์จ\"], color=[\"tab:green\" if v >= 0.85 else \"tab:red\" for v in R[\"์ฌํ์จ\"]])\n",
"plt.axvline(0.85, ls=\"--\", c=\"k\"); plt.xlabel(\"recall\"); plt.title(\"Composite threat scenarios (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ต ํด๋ฆฝ 700๊ฐ(์ง์ง 351 / ์์กฐ 349), ํน์ง 35๊ฐ, ์ ๊ฒฝ๋ง ์๋์ธต 64-32\n",
"\n",
"[๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
ํ์ง ๊ฒฐ๊ณผ(๊ฐ ์์กฐ 40ํด๋ฆฝ)]\n",
" ์๋๋ฆฌ์ค ์ฌํ์จ ํต๊ณผ(85%)\n",
"S01 TTS ๋ช
๋ น, ์ํธ ์ฑ๋ 100.0% PASS\n",
" S02 TTS ๋ช
๋ น, ์ SNR 100.0% PASS\n",
" S03 VC ์ฌ์นญ, ์ํธ ์ฑ๋ 100.0% PASS\n",
" S04 VC ์ฌ์นญ, ์ถฉ๊ฒฉ ์ก์ 92.5% PASS\n",
" S05 ์ฌ์ ๊ณต๊ฒฉ, ์ํธ ์ฑ๋ 100.0% PASS\n",
" S06 ์ฌ์ ๊ณต๊ฒฉ, ์ ํธ ๋๊น 90.0% PASS\n",
" S07 TTS, ๋จ์ผํค ์ฌ๋ฐ 100.0% PASS\n",
" S08 VC, ํฌํ(ํด๋ฆฌํ) 100.0% PASS\n",
" S09 ํผํฉ ์์กฐ, ์ SNR 95.0% PASS\n",
" S10 ํผํฉ ์์กฐ, ๋๊น+์ถฉ๊ฒฉ 85.0% PASS\n",
"\n",
"๋ณตํฉ ์ํ ํ๊ท ์ฌํ์จ 96.2% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), ์๋๋ฆฌ์ค ํต๊ณผ 10/10\n",
"๋จ์ผ ํด๋ฆฝ ์ฒ๋ฆฌ ์ง์ฐ p95 4.3 ms (๋ชฉํ 200ms ์ดํ: ๋ฌ์ฑ), ๋ฐฐ์น ํ๊ท 0.81 ms/ํด๋ฆฝ\n",
"\n",
"[์ง์ง ์์ฑ ์ค๊ฒฝ๋ณด์จ - ์ฑ๋ ์กฐ๊ฑด๋ณ]\n",
" ์ํธ(15~25dB) 0.0%\n",
" ์ SNR(3~8dB) 3.3%\n",
" ์ถฉ๊ฒฉ ์ก์ 0.0%\n",
" ์ ํธ ๋๊น 8.3%\n",
" ๋จ์ผํค ์ฌ๋ฐ 31.7%\n",
" ํฌํ(ํด๋ฆฌํ) 6.7%\n",
"์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: '๋จ์ผํค ์ฌ๋ฐ' ์กฐ๊ฑด์์ ๊ฐ์ฅ ๋์ -> ํด๋น ์กฐ๊ฑด ์ง์ง ์์ฑ์ ํ์ต์ ๋ณด๊ฐํ๊ฑฐ๋ ๋ฏธ์
5์์ ์ฑ๋ ํ์ง์ ์ ๋ขฐ๋์ ๋ฐ์ํด ๊ฒฝ๋ณด ๋ฑ๊ธ์ ๋ฎ์ถ๋ค\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ ์ฅ ์์ ํ๊ฒฝ ์คํ ๋ฐฉ์ง ๋ฐ ๋ฌด์ ํ์ ์ง์ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ์ก์ ์์์ ์คํ์ ์ต์ํํ๊ณ ์์ฑ ์ง์ ์ ๋ขฐ๋ ์ ์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฑ๋ ํ์ง์ ๋ฐ์ํ ๋ณด์ ๋ชจ๋ธ๋ก ๋ฌด์ ํ์ ์ง์ ์ ๋ขฐ๋ ์ ์์ High/Med/Low ๋ฑ๊ธ์ ์ฐ์ถํด ์ ์ฅ ์์ ์ ์ค๊ฒฝ๋ณด๋ฅผ ์ค์ธ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ถ์ SNR, ๋จ์ผํค ์ฌ๋ฐ ์ง์, ๋๊น ๋น์จ ๋ฑ ์ฑ๋ ํ์ง ํน์ง์ ๋ํ๊ณ ์ก์ ์กฐ๊ฑด์ ์์ด ํ์ตํ ๋ค ๋ก์ง์คํฑ ๋ณด์ ์ผ๋ก ์ ์๋ฅผ ํ๋ฅ ํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/c204_confidence_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ขฐ๋ ๋ฑ๊ธ(High/Med/Low) ๋ถ์ฌ ๊ธฐ์ค์ ๊ตฐ์ฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด์จ(FAR) 40% ์ด์ ๊ฐ์ถ, ์ ๋ขฐ๋ ์ค์ฝ์ด ์ฐ์ถ ์ผ๊ด์ฑ 90% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด ํํฐ๋ง ์ ํ ๋น๊ต ์งํ ์ฐ์ถ ๋ฐ ํตํฉ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ROC-AUC ๊ณก์ ๋ถ์ ๋ฐ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ฑ๋ ํ์ง ํน์ง(์ถ์ SNR, ๋จ์ผํค ์ฌ๋ฐ ์ง์, ๋๊น ๋น์จ)์ ๋ํ๊ณ ์ก์ ์กฐ๊ฑด์ ์์ด ํ์ตํ ๋ณด์ ๋ชจ๋ธ๋ก ๋ฌด์ ํ์ ์ง์ ์ ๋ขฐ๋ ์ ์์ High/Med/Low ๋ฑ๊ธ์ ์ฐ์ถํด, ๊ธฐ์กด ํ๋ณ๊ธฐ ๋๋น ์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ, ๋ฑ๊ธ ์ผ๊ด์ฑ, ROC-AUC์ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.metrics import roc_auc_score, roc_curve\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"tpl = enroll()\n",
"\n",
"\n",
"def channel_quality(X):\n",
" \"\"\"์ฑ๋ ํ์ง ์งํ: ์ถ์ SNR(์ ํจ ํ๋ ์ ์๋์ง ์ํ์ ๋น, dB), ๋จ์ผํค ์ฌ๋ฐ ์ง์(์ฃผํ์๋ณ ์๊ฐ ํ๊ท /ํ์คํธ์ฐจ์ ์ต๋๊ฐ), ๋๊น ๋น์จ(๋ฌด์ 10ms ๋น์จ).\"\"\"\n",
" P = spectrogram(X)\n",
" e = P.sum(1)\n",
" e = np.where(e > 1e-9, e, np.nan)\n",
" snr_est = 10 * np.log10(np.nanpercentile(e, 90, axis=1) / (np.nanpercentile(e, 10, axis=1) + 1e-12))\n",
" Pb = P[:, (FREQS >= 300) & (FREQS < 3400)]\n",
" tone = (Pb.mean(2) / (Pb.std(2) + 1e-12)).max(1) # ์๊ฐ์ ๋ฐ๋ผ ์ผ์ ํ ์ฃผํ์ ์ฑ๋ถ(์ฌ๋ฐ ํค)์ผ์๋ก ํผ\n",
" fr = np.abs(np.atleast_2d(X)).reshape(len(X), -1, 80).max(-1)\n",
" drop = (fr < 1e-6).mean(1)\n",
" return pd.DataFrame({\"snr_est\": snr_est, \"tone_idx\": tone, \"drop_ratio\": drop})\n",
"\n",
"\n",
"def featurize(X, M, quality=False):\n",
" F = extract(X)\n",
" F[\"spk_sim\"] = speaker_sim(F, M[\"claimed\"], tpl)\n",
" return pd.concat([F, channel_quality(X)], axis=1) if quality else F\n",
"\n",
"\n",
"def mixed(n, seed, labels=LABELS):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ยท์ SNRยท์ถฉ๊ฒฉ ์ก์ยท๋๊นยท์ฌ๋ฐ ์ฑ๋์ด ์์ธ ๋ฐ์ดํฐ์
.\"\"\"\n",
" conds = [dict(snr=(10, 25)), dict(snr=(3, 8)), dict(snr=(8, 15), impulse=0.002), dict(snr=(8, 15), dropout=0.4), dict(snr=(8, 15), jam_tone=0.5)]\n",
" parts = [make_dataset(n // 5, seed=seed + i, labels=labels, **c) for i, c in enumerate(conds)]\n",
" return np.vstack([p[0] for p in parts]), pd.concat([p[1] for p in parts], ignore_index=True)\n",
"\n",
"\n",
"# ๊ธฐ์กด(๋ฏธ์
4 ๋ฐฉ์): ์ํธ ์ฑ๋ ์์ฃผ ํ์ต + ์๊ณ๊ฐ 0.5\n",
"Xa, Ma = make_dataset(600, seed=31, snr=(5, 25))\n",
"old = make_pipeline(StandardScaler(), MLPClassifier((64, 32), alpha=1e-3, max_iter=600, random_state=0)).fit(featurize(Xa, Ma), Ma[\"spoof\"])\n",
"# ๊ฐ์ : ์ฑ๋ ์กฐ๊ฑด ํผํฉ ํ์ต + ์ฑ๋ ํ์ง ํน์ง + ๋ก์ง์คํฑ ๋ณด์ (Platt)์ผ๋ก ์ ์ ํ๋ฅ ํ\n",
"Xb, Mb = mixed(800, seed=40)\n",
"Fb = featurize(Xb, Mb, quality=True)\n",
"new = make_pipeline(StandardScaler(), MLPClassifier((64, 32), alpha=1e-3, max_iter=600, random_state=0)).fit(Fb, Mb[\"spoof\"])\n",
"Xc, Mc = mixed(300, seed=60)\n",
"cal = LogisticRegression().fit(new.predict_proba(featurize(Xc, Mc, True))[:, [1]], Mc[\"spoof\"])\n",
"\n",
"\n",
"def confidence(X, M):\n",
" \"\"\"์์กฐ ํ๋ฅ p์ ์ง์ ์ ๋ขฐ๋(1-p), ๋ฑ๊ธ: High=์์กฐ ์์ฌ ๋์(์ฆ์ ๊ฒฝ๋ณด), Med=ํ์ธ ํ์(HITL), Low=์ง์ง ๊ฐ๋ฅ์ฑ ๋์.\"\"\"\n",
" Fq = featurize(X, M, quality=True)\n",
" p = cal.predict_proba(new.predict_proba(Fq)[:, [1]])[:, 1]\n",
" poor = (Fq[\"snr_est\"] < 3.3) | (Fq[\"tone_idx\"] > 2.5) | (Fq[\"drop_ratio\"] > 0.05)\n",
" grade = np.where(p >= 0.8, \"High\", np.where(p >= 0.4, \"Med\", \"Low\"))\n",
" grade = np.where((grade == \"High\") & poor & (p < 0.95), \"Med\", grade) # ํ์ง ๋ถ๋ ์ฑ๋์ ํ ๋จ๊ณ ๋ฎ์ถฐ ์ฌ๋ ํ์ธ\n",
" return p, grade, poor\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ๊ฐ: ํผํฉ ์ฑ๋ 500ํด๋ฆฝ\n",
"Xt, Mt = mixed(500, seed=80)\n",
"p_old = old.predict_proba(featurize(Xt, Mt))[:, 1]\n",
"p_new, g_new, poor = confidence(Xt, Mt)\n",
"y, b = Mt[\"spoof\"].values, ~Mt[\"spoof\"].values\n",
"far_old, far_new = (p_old[b] >= 0.5).mean(), (g_new[b] == \"High\").mean()\n",
"red = 1 - far_new / far_old\n",
"print(f\"ํ๊ฐ ํด๋ฆฝ {len(Xt)}๊ฐ(์ง์ง {b.sum()} / ์์กฐ {y.sum()}), ํ์ง ๋ถ๋ ์ฑ๋ {poor.mean():.0%}\")\n",
"print(f\"์ค๊ฒฝ๋ณด์จ(์ง์ง๋ฅผ ์ฆ์ ๊ฒฝ๋ณด): ๊ธฐ์กด {far_old:.1%} -> ๊ฐ์ {far_new:.1%}, ๊ฐ์ถ {red:.0%} (๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if red >= 0.4 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์์กฐ ํ์ง(High+Med ๋ฑ๊ธ): ๊ธฐ์กด {(p_old[y] >= 0.5).mean():.1%} -> ๊ฐ์ {(g_new[y] != 'Low').mean():.1%}\")\n",
"print(f\"ROC-AUC: ๊ธฐ์กด {roc_auc_score(y, p_old):.3f}, ๊ฐ์ {roc_auc_score(y, p_new):.3f}\")\n",
"\n",
"print(\"\\n[๋ฑ๊ธ ๋ถํฌ] ๋ฑ๊ธ ์ง์ง ์์กฐ\")\n",
"for g in [\"High\", \"Med\", \"Low\"]:\n",
" print(f\" {g:5s} {(g_new[b] == g).sum():5d} {(g_new[y] == g).sum():5d}\")\n",
"\n",
"print(\"\\n[์๊ณ๊ฐ ๋ฏผ๊ฐ๋(High ๊ธฐ์ค)] ์๊ณ๊ฐ ์ค๊ฒฝ๋ณด์จ ์์กฐ High ๋น์จ\")\n",
"for th in [0.6, 0.7, 0.8, 0.9]:\n",
" print(f\" {th:.1f} {(p_new[b] >= th).mean():6.1%} {(p_new[y] >= th).mean():6.1%}\")\n",
"\n",
"# ์ผ๊ด์ฑ: ๊ฐ์ ์์ ํธ๋ฅผ ๋ค๋ฅธ ์ก์ ์คํ 5ํ๋ก ํต๊ณผ์์ผฐ์ ๋ ๋ฑ๊ธ์ด ๊ฐ์ ๋น์จ\n",
"rng = np.random.default_rng(90)\n",
"same = []\n",
"for i in range(60):\n",
" lab = LABELS[i % 4]; spk = \"SPK_A\" if lab != \"bonafide\" else str(rng.choice(list(SPEAKERS)))\n",
" x = make_clip(lab, spk, rng); s = rng.uniform(10, 25)\n",
" Xr = np.stack([radio_channel(x, np.random.default_rng(1000 + i * 10 + r), snr_db=s) for r in range(5)])\n",
" _, g, _ = confidence(Xr, pd.DataFrame({\"claimed\": [spk] * 5}))\n",
" same.append(pd.Series(g).value_counts().iloc[0] / 5)\n",
"cons = float(np.mean(same))\n",
"print(f\"\\n์ ๋ขฐ๋ ๋ฑ๊ธ ์ผ๊ด์ฑ(๋์ผ ์์ฑ x ์ก์ ์คํ 5ํ, 60๊ฑด): {cons:.1%} (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if cons >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"print(\"๋ฑ๊ธ ์ด์ฉ: High=ํต์ ๋ณด์๊ด ์ฆ์ ๊ฒฝ๋ณด / Med=HITL 2์ฐจ ํ์ธ / Low=์ ์ ์ฒ๋ฆฌ(๋ก๊ทธ ๋ณด์กด). ๋ฑ๊ธ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ง์ ํ์ ์ ์ฌ๋์ด ํ๋ค.\")\n",
"\n",
"fo, to, _ = roc_curve(y, p_old); fn, tn, _ = roc_curve(y, p_new)\n",
"plt.figure(figsize=(5, 4)); plt.plot(fo, to, label=\"baseline\"); plt.plot(fn, tn, label=\"calibrated + channel quality\")\n",
"plt.plot([0, 1], [0, 1], \"k:\"); plt.xlabel(\"false alarm rate\"); plt.ylabel(\"recall\"); plt.legend(); plt.title(\"ROC (synthetic mixed channels)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ๊ฐ ํด๋ฆฝ 500๊ฐ(์ง์ง 254 / ์์กฐ 246), ํ์ง ๋ถ๋ ์ฑ๋ 56%\n",
"์ค๊ฒฝ๋ณด์จ(์ง์ง๋ฅผ ์ฆ์ ๊ฒฝ๋ณด): ๊ธฐ์กด 27.6% -> ๊ฐ์ 0.0%, ๊ฐ์ถ 100% (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ)\n",
"์์กฐ ํ์ง(High+Med ๋ฑ๊ธ): ๊ธฐ์กด 98.4% -> ๊ฐ์ 95.9%\n",
"ROC-AUC: ๊ธฐ์กด 0.822, ๊ฐ์ 0.995\n",
"\n",
"[๋ฑ๊ธ ๋ถํฌ] ๋ฑ๊ธ ์ง์ง ์์กฐ\n",
" High 0 109\n",
" Med 7 127\n",
" Low 247 10\n",
"\n",
"[์๊ณ๊ฐ ๋ฏผ๊ฐ๋(High ๊ธฐ์ค)] ์๊ณ๊ฐ ์ค๊ฒฝ๋ณด์จ ์์กฐ High ๋น์จ\n",
" 0.6 2.4% 95.1%\n",
" 0.7 2.0% 93.9%\n",
" 0.8 2.0% 93.5%\n",
" 0.9 1.6% 91.5%\n",
"\n",
"์ ๋ขฐ๋ ๋ฑ๊ธ ์ผ๊ด์ฑ(๋์ผ ์์ฑ x ์ก์ ์คํ 5ํ, 60๊ฑด): 96.7% (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"๋ฑ๊ธ ์ด์ฉ: High=ํต์ ๋ณด์๊ด ์ฆ์ ๊ฒฝ๋ณด / Med=HITL 2์ฐจ ํ์ธ / Low=์ ์ ์ฒ๋ฆฌ(๋ก๊ทธ ๋ณด์กด). ๋ฑ๊ธ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ง์ ํ์ ์ ์ฌ๋์ด ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ค์๊ฐ ๋ฌด์ ์์ฑ ์์กฐ ๊ฒฝ๋ณด ๋ฐ ์คํํธ๋ก๊ทธ๋จ ์๊ฐํ C2 ๋ณด์ ์ธํฐํ์ด์ค ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ค์๊ฐ ์์ฑ ์คํธํ ๊ฒฝ๋ณด ๋ฐ ์ํฅ ์คํํธ๋ก๊ทธ๋จ ๊ฐ์ํ C2 ๋ณด์ ๋ชจ๋ํฐ๋ง UI๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ชจ์ ๋ฌด์ ์ก์ ์คํธ๋ฆผ์ ๋ฐ์ ์์กฐ ์ ์์ ๋ฑ๊ธ์ ์ค์๊ฐ ๊ฐฑ์ ํ๋ ์ผ๊ฐ ๋ชจ๋ C2 ์์ฑ ๋ณด์ ์ํฉํ์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ก์ 1๊ฑด๋ง๋ค ์คํํธ๋ก๊ทธ๋จ, ์ ์ ์ถ์ด, ๋ฑ๊ธ ์ง๊ณ, ๊ฒฝ๋ณด ๋ชฉ๋ก 4๊ฐ ํจ๋์ ๊ฐฑ์ ํ๊ณ High ๊ฒฝ๋ณด๋ ๋นจ๊ฐ ํ
๋๋ฆฌ๋ก ๊ฐ์กฐ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/c204_dashboard.py (๋๋ ์น UI ์ปดํฌ๋ํธ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ์์ ์ํฉํ์ผ๋ก์์ ์ง๊ด์ฑ ๋ฐ ์ธ์ง ๋ถํ ๊ฐ์๋\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด, UI ์๋ต์ฑ 100ms ์ด๋ด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ฃผ์ ์งํ ์๊ฐํ ์ปดํฌ๋ํธ 4์ข
์ด์ ๋ ๋๋ง ๋ฐ ์ค์๊ฐ ๋ฐ์ดํฐ ์ฐ๋\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ฌ์ฉ์ ์ธํฐ๋์
ํ
์คํธ ๋ฐ ๋ชจ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ ๋๋ง ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ชจ์ ๋ฌด์ ์ก์ ์คํธ๋ฆผ์ 1์ด ์ฃผ๊ธฐ๋ก ๋ฐ์ ์์กฐ ์ ์ยท๋ฑ๊ธ์ ๊ฐฑ์ ํ๊ณ , ์คํํธ๋ก๊ทธ๋จยท์ ์ ์ถ์ดยท๋ฑ๊ธ ์ง๊ณยท๊ฒฝ๋ณด ๋ชฉ๋ก 4๊ฐ ํจ๋๋ก ๋ณด์ฌ ์ฃผ๋ ์ผ๊ฐ ๋ชจ๋ C2 ์์ฑ ๋ณด์ ์ํฉํ์ ๊ตฌํํด ํ๋ฉด ๊ฐฑ์ ์๊ฐ๊ณผ ํด๋ฆฝ ์ ํ ์๋ต ์๊ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"tpl = enroll()\n",
"\n",
"\n",
"def featurize(X, M):\n",
" F = extract(X)\n",
" F[\"spk_sim\"] = speaker_sim(F, M[\"claimed\"], tpl)\n",
" return F\n",
"\n",
"\n",
"Xtr, Mtr = make_dataset(400, seed=51, snr=(5, 25))\n",
"det = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, C=0.5)).fit(featurize(Xtr, Mtr), Mtr[\"spoof\"])\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ชจ์ ์ก์ ์คํธ๋ฆผ 24๊ฑด(๊ฐ์ ๋ง NET-A~C, ๊ฐ์ ํธ์ถ๋ถํธ)\n",
"Xs, Ms = make_dataset(24, seed=52, snr=(6, 25), p=[0.6, 0.15, 0.15, 0.1])\n",
"Ms[\"net\"] = [f\"NET-{'ABC'[i % 3]}\" for i in range(24)]\n",
"Ms[\"t\"] = [f\"21:00:{i:02d}\" for i in range(24)]\n",
"state = pd.DataFrame(columns=[\"t\", \"net\", \"clip\", \"claimed\", \"score\", \"grade\"])\n",
"\n",
"plt.style.use(\"dark_background\") # ์ผ๊ฐ ์์ ๋ชจ๋\n",
"fig, ax = plt.subplots(2, 2, figsize=(10, 6))\n",
"upd = []\n",
"for i in range(len(Xs)):\n",
" t0 = time.perf_counter()\n",
" s = float(det.predict_proba(featurize(Xs[i:i + 1], Ms.iloc[i:i + 1]))[0, 1])\n",
" g = \"High\" if s >= 0.8 else \"Med\" if s >= 0.4 else \"Low\"\n",
" state.loc[i] = [Ms[\"t\"][i], Ms[\"net\"][i], Ms[\"clip\"][i], Ms[\"claimed\"][i], round(s, 2), g]\n",
" for a in ax.flat:\n",
" a.clear()\n",
" P = spectrogram(Xs[i:i + 1])[0]\n",
" ax[0, 0].imshow(10 * np.log10(P + 1e-8), origin=\"lower\", aspect=\"auto\", cmap=\"magma\", extent=[0, DUR, 0, FS / 2])\n",
" ax[0, 0].set_title(f\"Spectrogram {Ms['clip'][i]} ({Ms['net'][i]})\"); ax[0, 0].set_xlabel(\"s\"); ax[0, 0].set_ylabel(\"Hz\")\n",
" col = state[\"grade\"].map({\"High\": \"red\", \"Med\": \"orange\", \"Low\": \"lime\"})\n",
" ax[0, 1].scatter(range(len(state)), state[\"score\"], c=col); ax[0, 1].axhline(0.8, c=\"red\", ls=\"--\"); ax[0, 1].axhline(0.4, c=\"orange\", ls=\":\")\n",
" ax[0, 1].set_ylim(0, 1); ax[0, 1].set_title(\"Spoof score timeline\"); ax[0, 1].set_xlabel(\"transmission #\")\n",
" cnt = state[\"grade\"].value_counts().reindex([\"High\", \"Med\", \"Low\"]).fillna(0)\n",
" ax[1, 0].bar(cnt.index, cnt.values, color=[\"red\", \"orange\", \"lime\"]); ax[1, 0].set_title(\"Grade count\")\n",
" al = state[state[\"grade\"] != \"Low\"].tail(6)\n",
" ax[1, 1].axis(\"off\"); ax[1, 1].set_title(\"Alert list (operator review)\")\n",
" for k, r in enumerate(al.itertuples()):\n",
" ax[1, 1].text(0.0, 0.9 - k * 0.15, f\"{r.t} {r.net} {r.clip} claim {r.claimed} score {r.score:.2f} {r.grade}\",\n",
" color=\"red\" if r.grade == \"High\" else \"orange\", fontsize=8, family=\"monospace\")\n",
" if g == \"High\": # ๊ธด๊ธ ๊ฒฝ๋ณด ํ์ด๋ผ์ดํธ\n",
" for a in ax.flat:\n",
" for sp in a.spines.values():\n",
" sp.set_color(\"red\"); sp.set_linewidth(2)\n",
" fig.canvas.draw()\n",
" upd.append((time.perf_counter() - t0) * 1000)\n",
"\n",
"# ํด๋ฆฝ ์ ํ ์๋ต์ฑ: ๊ฒฝ๋ณด ๋ชฉ๋ก์์ ํ ๊ฑด์ ๊ณ ๋ฅด๋ฉด ์์ธ ์ ๋ณด(๋จ์ ์์ 3๊ฐ)๋ฅผ ์ฐพ์ ๋ณด์ฌ ์ฃผ๋ ์ฝ๋ฐฑ\n",
"coef = pd.Series(det[-1].coef_[0], index=featurize(Xs[:1], Ms.iloc[:1]).columns)\n",
"Fall = featurize(Xs, Ms)\n",
"Z = pd.DataFrame(det[0].transform(Fall), columns=Fall.columns)\n",
"\n",
"\n",
"def on_select(idx):\n",
" contrib = (Z.loc[idx] * coef).sort_values(ascending=False).head(3)\n",
" return {\"clip\": Ms[\"clip\"][idx], \"grade\": state[\"grade\"][idx], \"top_cues\": [f\"{k}({v:+.2f})\" for k, v in contrib.items()]}\n",
"\n",
"\n",
"resp = []\n",
"for idx in state.index[state[\"grade\"] != \"Low\"]:\n",
" t0 = time.perf_counter(); info = on_select(idx); resp.append((time.perf_counter() - t0) * 1000)\n",
"\n",
"print(\"[์ค์๊ฐ C2 ์์ฑ ๋ณด์ ์ํฉํ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(state.tail(8).to_string(index=False))\n",
"print(f\"\\nํจ๋ 4์ข
(์คํํธ๋ก๊ทธ๋จ, ์ ์ ์ถ์ด, ๋ฑ๊ธ ์ง๊ณ, ๊ฒฝ๋ณด ๋ชฉ๋ก) ๋ ๋๋ง: {len(upd)}ํ ๊ฐฑ์ \")\n",
"print(f\"ํ๋ฉด ๊ฐฑ์ ์๊ฐ ์ต๋ {max(upd):.0f} ms, ํ๊ท {np.mean(upd):.0f} ms (๋ชฉํ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if max(upd) < 1000 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํด๋ฆฝ ์ ํ ์๋ต ์ต๋ {max(resp):.1f} ms (๋ชฉํ 100ms ์ด๋ด: {'๋ฌ์ฑ' if max(resp) < 100 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"๊ฒฝ๋ณด {len(resp)}๊ฑด ์ค High {(state['grade'] == 'High').sum()}๊ฑด(๋นจ๊ฐ ํ
๋๋ฆฌ ํ์ด๋ผ์ดํธ), Med {(state['grade'] == 'Med').sum()}๊ฑด\")\n",
"print(\"์ ํ ์์:\", info)\n",
"print(\"์ค์ ์์กฐ ์ฌ๋ถ(์ ๋ต) ๋๋น:\", f\"์์กฐ {Ms['spoof'].sum()}๊ฑด ์ค ๊ฒฝ๋ณด {(state['grade'] != 'Low')[Ms['spoof']].sum()}๊ฑด, ์ง์ง {(~Ms['spoof']).sum()}๊ฑด ์ค ๊ฒฝ๋ณด {(state['grade'] != 'Low')[~Ms['spoof']].sum()}๊ฑด\")\n",
"print(\"์ํฉํ ๊ฒฝ๋ณด๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ์์กฐ ํ์ ๊ณผ ์ ํ๋ ํต์ ๋ณด์๊ด ํ์ธ(๋ฏธ์
7 ์ ์ฐจ) ํ์ ํ๋ค.\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ค์๊ฐ C2 ์์ฑ ๋ณด์ ์ํฉํ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" t net clip claimed score grade\n",
"21:00:16 NET-B C520016 SPK_F 0.01 Low\n",
"21:00:17 NET-C C520017 SPK_A 1.00 High\n",
"21:00:18 NET-A C520018 SPK_D 0.00 Low\n",
"21:00:19 NET-B C520019 SPK_F 0.10 Low\n",
"21:00:20 NET-C C520020 SPK_A 1.00 High\n",
"21:00:21 NET-A C520021 SPK_A 0.91 High\n",
"21:00:22 NET-B C520022 SPK_A 1.00 High\n",
"21:00:23 NET-C C520023 SPK_D 0.00 Low\n",
"\n",
"ํจ๋ 4์ข
(์คํํธ๋ก๊ทธ๋จ, ์ ์ ์ถ์ด, ๋ฑ๊ธ ์ง๊ณ, ๊ฒฝ๋ณด ๋ชฉ๋ก) ๋ ๋๋ง: 24ํ ๊ฐฑ์ \n",
"ํ๋ฉด ๊ฐฑ์ ์๊ฐ ์ต๋ 334 ms, ํ๊ท 211 ms (๋ชฉํ 1์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"ํด๋ฆฝ ์ ํ ์๋ต ์ต๋ 0.8 ms (๋ชฉํ 100ms ์ด๋ด: ๋ฌ์ฑ)\n",
"๊ฒฝ๋ณด 12๊ฑด ์ค High 12๊ฑด(๋นจ๊ฐ ํ
๋๋ฆฌ ํ์ด๋ผ์ดํธ), Med 0๊ฑด\n",
"์ ํ ์์: {'clip': 'C520022', 'grade': 'High', 'top_cues': ['mfcc0_m(+2.59)', 'hnr(+1.73)', 'delta_s(+1.60)']}\n",
"์ค์ ์์กฐ ์ฌ๋ถ(์ ๋ต) ๋๋น: ์์กฐ 12๊ฑด ์ค ๊ฒฝ๋ณด 12๊ฑด, ์ง์ง 12๊ฑด ์ค ๊ฒฝ๋ณด 0๊ฑด\n",
"์ํฉํ ๊ฒฝ๋ณด๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ์์กฐ ํ์ ๊ณผ ์ ํ๋ ํต์ ๋ณด์๊ด ํ์ธ(๋ฏธ์
7 ์ ์ฐจ) ํ์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์งํ๊ด ๊ธด๊ธ ์์ฑ ๋ช
๋ น ์์ ์ ํต์ ๊ด HITL 2์ฐจ ์ก์/์ฒญ์ ๊ฒ์ฆ ์ ์ฐจ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์งํ๊ด ๊ธด๊ธ ์์ฑ ๋ช
๋ น ์์ ์ ํต์ ๋ณด์๊ด์ด 2์ฐจ ํ์ธ ๋ฐ ์ํธ ์ธ์ฆ์ ์ํํ๋ HITL ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์งํ๊ด ๋ช
์ ๊ธด๊ธ ์์ฑ ๋ช
๋ น์ด ๋ค์ด์ค๋ฉด ํต์ ๋ณด์๊ด์ด ์ธ์ฆ ์ง์์๋ต๊ณผ ์ฒญ์์ผ๋ก 2์ฐจ ํ์ธํ ๋ค์๋ง ์ ํํ๋ HITL ๊ฒ์ฆ ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ธด๊ธ ๋ช
๋ น์ ํ์ง ์ ์์ ๋ฌด๊ดํ๊ฒ ๊ฒ์ฆ ๋๊ธฐ์ด์ ์ฌ๋ฆฌ๊ณ ๋ฑ๊ธยท์ ์ ์์ผ๋ก ๊ฒํ ์์๋ฅผ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/c204_approval_workflow.py ๋ฐ docs/c204_hitl_guide.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์งํ ์ฑ
์ ๋ถ๊ณ์ ๋ช
ํํ ๋ฐ ์ฑ๊ธํ ์๋ ๋จ์ ๋ฐฉ์ง ์ฒด๊ณ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ธ๊ฐ ๊ฒํ ํ(Queue) ๋๊ธฐ์๊ฐ ๋ฐ ์น์ธ/๋ฐ๋ ค ์ฒ๋ฆฌ ๋ก๊น
์ฑ๊ณต๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น์ธ/๋ฐ๋ ค ์ธํฐํ์ด์ค ๋์ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ(Audit Log) ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ชจ์ ์กฐ์์ ์น์ธ/๋ฐ๋ ค ์๋ฎฌ๋ ์ด์
๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์งํ๊ด ๋ช
์ ๊ธด๊ธ ์์ฑ ๋ช
๋ น์ด ์์ ๋๋ฉด ํ์ง ์ ์์ ๋ฌด๊ดํ๊ฒ ํต์ ๋ณด์๊ด 2์ฐจ ๊ฒ์ฆ ๋๊ธฐ์ด์ ์ฌ๋ฆฌ๊ณ , ๊ฐ์ ์ธ์ฆ ์ง์์๋ต(์ฑ๋ฆฐ์ง-์๋ต)ยท์ฒญ์ ํ์ธยท๊ทผ๊ฑฐ ๊ธฐ๋ก์ ๊ฑฐ์ณ์ผ๋ง ์ ํ๋๋๋ก ํ๋ HITL ๊ฒ์ฆ ์ ์ฐจ์ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๊ตฌํํ๋ค.\"\"\"\n",
"import hashlib\n",
"import hmac\n",
"import json\n",
"import pandas as pd\n",
"\n",
"ACTIONS = {\"CONFIRM\": \"์ง์ ํ์ธ(๋ช
๋ น ์ ํ ํ์ฉ)\", \"HOLD\": \"๋ณด๋ฅ(์ฌ์ก์ ยท์ถ๊ฐ ํ์ธ ์์ฒญ)\", \"REJECT\": \"์์กฐ ์์ฌ ์ฐจ๋จ(์๊ธ ๋ณด๊ณ )\"}\n",
"PRIORITY = {\"High\": 0, \"Med\": 1, \"Low\": 2}\n",
"HOLD_LIMIT_S = 120\n",
"AUTH_KEY = b\"EDU-VIRTUAL-KEY\" # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค์ ์ธ์ฆ ์ฒด๊ณยท์ํธ ์๋\n",
"\n",
"\n",
"class WorkflowError(ValueError):\n",
" pass\n",
"\n",
"\n",
"def expected_response(challenge, net):\n",
" \"\"\"๊ฐ์ ์ฑ๋ฆฐ์ง-์๋ต: ์ง์์ด์ ๋ง ID๋ก 4์๋ฆฌ ์๋ต์ ๋ง๋ ๋ค(์ค์ ์ํธ ์ฒด๊ณ๋ฅผ ํ๋ด ๋ธ ํ์ต์ฉ ๋ชจํ).\"\"\"\n",
" return hmac.new(AUTH_KEY, f\"{net}:{challenge}\".encode(), hashlib.sha256).hexdigest()[:4].upper()\n",
"\n",
"\n",
"class VerifyQueue:\n",
" def __init__(self, clock):\n",
" self.items, self.log, self.clock, self.feedback = {}, [], clock, []\n",
"\n",
" def _record(self, body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {**body, \"ts\": round(self.clock(), 1)}\n",
" h = hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()\n",
" self.log.append({**body, \"prev\": prev, \"hash\": h})\n",
"\n",
" def verify_log(self):\n",
" prev = \"GENESIS\"\n",
" for n, rec in enumerate(self.log, start=1):\n",
" body = {k: v for k, v in rec.items() if k not in (\"prev\", \"hash\")}\n",
" if rec[\"prev\"] != prev or rec[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False, n\n",
" prev = rec[\"hash\"]\n",
" return True, -1\n",
"\n",
" def submit(self, mid, net, claimed, score, grade, order):\n",
" \"\"\"๊ธด๊ธ ๋ช
๋ น ๋ฑ๋ก. ์ ์๊ฐ ๋ฎ์๋(์ง์ง๋ก ๋ณด์ฌ๋) ์๋ ์ ํํ์ง ์๊ณ PENDING์ผ๋ก ์์ํ๋ค.\"\"\"\n",
" self.items[mid] = {\"id\": mid, \"net\": net, \"claimed\": claimed, \"score\": score, \"grade\": grade, \"order\": order,\n",
" \"status\": \"PENDING\", \"submitted\": self.clock(), \"hold_since\": None, \"escalated\": False, \"auth\": None}\n",
" self._record({\"event\": \"SUBMIT\", \"id\": mid, \"score\": score, \"grade\": grade, \"by\": \"DETECTOR\"})\n",
"\n",
" def pending(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\")]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], PRIORITY[i[\"grade\"]], -i[\"score\"], i[\"submitted\"]))\n",
"\n",
" def authenticate(self, mid, challenge, response, operator):\n",
" \"\"\"์ก์ ์์๊ฒ ์ญํธ์ถ๋ก ๊ฐ์ ์ง์์ด๋ฅผ ๋ณด๋ด๊ณ ๋ฐ์ ์๋ต์ ๋์กฐํ๋ค. ๊ฒฐ๊ณผ๋ ๊ธฐ๋ก๋ง ํ๊ณ ๊ฒฐ์ ์ ์ฌ๋์ด ํ๋ค.\"\"\"\n",
" ok = hmac.compare_digest(expected_response(challenge, self.items[mid][\"net\"]), response)\n",
" self.items[mid][\"auth\"] = ok\n",
" self._record({\"event\": \"AUTH\", \"id\": mid, \"challenge\": challenge, \"result\": ok, \"operator\": operator})\n",
" return ok\n",
"\n",
" def decide(self, mid, action, operator, reason, listened=False, second=None):\n",
" if action not in ACTIONS:\n",
" raise WorkflowError(f\"ํ์ฉ๋์ง ์์ ์กฐ์น: {action}\")\n",
" if not operator:\n",
" raise WorkflowError(\"ํต์ ๋ณด์๊ด ID ํ์\")\n",
" it = self.items.get(mid)\n",
" if it is None or it[\"status\"] in (\"CONFIRMED\", \"REJECTED\"):\n",
" raise WorkflowError(\"๋๊ธฐ์ด์ ์๊ฑฐ๋ ์ด๋ฏธ ์ข
๊ฒฐ๋ ๋ช
๋ น\")\n",
" if len(reason.strip()) < 5:\n",
" raise WorkflowError(\"ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\")\n",
" if action == \"CONFIRM\":\n",
" if not listened:\n",
" raise WorkflowError(\"์ ํ ์ ์ฒญ์(์ง์ ๋ฃ๊ธฐ) ํ์ธ ํ์\")\n",
" if it[\"auth\"] is not True:\n",
" raise WorkflowError(\"์ธ์ฆ ์ง์์๋ต ํต๊ณผ ์ ์ ํ ๋ถ๊ฐ\")\n",
" if it[\"grade\"] == \"High\" and (not second or second == operator):\n",
" raise WorkflowError(\"High ๋ฑ๊ธ ํ์ธ์ ๋ค๋ฅธ ๊ฒํ ์ 1๋ช
์ ๊ต์ฐจ ํ์ธ ํ์\")\n",
" it[\"status\"] = {\"CONFIRM\": \"CONFIRMED\", \"HOLD\": \"HOLD\", \"REJECT\": \"REJECTED\"}[action]\n",
" it[\"hold_since\"] = self.clock() if action == \"HOLD\" else None\n",
" it[\"decided\"] = self.clock()\n",
" self._record({\"event\": \"DECIDE\", \"id\": mid, \"action\": action, \"operator\": operator, \"second\": second, \"listened\": listened, \"reason\": reason})\n",
"\n",
" def check_holds(self):\n",
" \"\"\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์ ์๋ ํ์ธํ์ง ์๊ณ ์๊ธ ์งํ๊ด(๋น์ง ํต์ ๊ด)์๊ฒ ์์ ํ๋ค.\"\"\"\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"status\"] == \"HOLD\" and not i[\"escalated\"] and self.clock() - i[\"hold_since\"] > HOLD_LIMIT_S:\n",
" i[\"escalated\"] = True; out.append(i[\"id\"])\n",
" self._record({\"event\": \"ESCALATE\", \"id\": i[\"id\"], \"by\": \"SYSTEM\"})\n",
" return out\n",
"\n",
" def commander_feedback(self, mid, text):\n",
" self.feedback.append({\"id\": mid, \"text\": text})\n",
" self._record({\"event\": \"FEEDBACK\", \"id\": mid, \"text\": text})\n",
"\n",
"\n",
"class FakeClock:\n",
" t = 0.0\n",
"\n",
" def __call__(self):\n",
" return self.t\n",
"\n",
"\n",
"clock = FakeClock()\n",
"q = VerifyQueue(clock)\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฏธ์
5 ์ ๋ขฐ๋ ์์ง์์ ๋์ด์จ ๊ฐ์ ๊ธด๊ธ ๋ช
๋ น 6๊ฑด(๊ฐ์ ๋งยท๊ฐ์ ํธ์ถ๋ถํธ)\n",
"for mid, net, spk, s, g, od in [(\"U01\", \"NET-A\", \"SPK_A\", 0.97, \"High\", \"๊ฐ์ ์ง๊ฒฐ์ง ๋ณ๊ฒฝ\"), (\"U02\", \"NET-B\", \"SPK_A\", 0.62, \"Med\", \"ํต์ ์ถ์ ๋ณ๊ฒฝ\"),\n",
" (\"U03\", \"NET-A\", \"SPK_A\", 0.08, \"Low\", \"๋ณด๊ณ ์ฃผ๊ธฐ ๋จ์ถ\"), (\"U04\", \"NET-C\", \"SPK_A\", 0.88, \"High\", \"๊ฐ์ ์ง์ง ์ด๋\"),\n",
" (\"U05\", \"NET-B\", \"SPK_A\", 0.45, \"Med\", \"์ฃผํ์ ์ ํ\"), (\"U06\", \"NET-C\", \"SPK_A\", 0.12, \"Low\", \"์ ์ ๋ณด๊ณ ์์ฒญ\")]:\n",
" clock.t += 5; q.submit(mid, net, spk, s, g, od)\n",
"print(\"๊ฒํ ์์(๋ฑ๊ธยท์ ์ ์):\", [i[\"id\"] for i in q.pending()])\n",
"print(\"์๋ ์ ํ(ํ์ธ)๋ ๋ช
๋ น ์:\", sum(i[\"status\"] == \"CONFIRMED\" for i in q.items.values()))\n",
"\n",
"# ๊ท์น ์๋ฐ ์๋: ๋ชจ๋ ์ฐจ๋จ๋์ด์ผ ํ๋ค\n",
"viol = [(\"U03\", \"CONFIRM\", \"sec01\", \"์ง์ง ์ ์ ๋ฎ์\", dict(listened=True)), # ์ธ์ฆ ์์ด ์ ํ\n",
" (\"U01\", \"CONFIRM\", \"\", \"๊ทผ๊ฑฐ ์ถฉ๋ถ\", {}), (\"U02\", \"FORWARD\", \"sec01\", \"์์ ์กฐ์น\", {}),\n",
" (\"U06\", \"REJECT\", \"sec01\", \"์์ฌ\", {})]\n",
"blocked = 0\n",
"for mid, a, op, r, kw in viol:\n",
" try:\n",
" q.decide(mid, a, op, r, **kw)\n",
" except WorkflowError as e:\n",
" blocked += 1; print(f\"์ฐจ๋จ({mid} {a}): {e}\")\n",
"\n",
"# ์ ์ ์ฒ๋ฆฌ: ์ธ์ฆ ์ง์์๋ต -> ์ฒญ์ -> ๊ฒฐ์ \n",
"clock.t += 10; q.authenticate(\"U03\", \"ECHO\", expected_response(\"ECHO\", \"NET-A\"), \"sec01\")\n",
"q.decide(\"U03\", \"CONFIRM\", \"sec01\", \"์ญํธ์ถ ์ธ์ฆ ํต๊ณผ, ์ฒญ์์ ํ์ ์ก์ ์์ ์ผ์น\", listened=True)\n",
"clock.t += 8; ok = q.authenticate(\"U01\", \"TANGO\", \"0000\", \"sec01\")\n",
"q.decide(\"U01\", \"REJECT\", \"sec01\", \"์ธ์ฆ ์๋ต ๋ถ์ผ์น, ํ๋ ์ ๊ฒฝ๊ณ ๋จ์ ๋ค์\", listened=True)\n",
"clock.t += 6; q.authenticate(\"U04\", \"LIMA\", expected_response(\"LIMA\", \"NET-C\"), \"sec02\")\n",
"try:\n",
" q.decide(\"U04\", \"CONFIRM\", \"sec02\", \"์ธ์ฆ ํต๊ณผ, ์ฒญ์ ์ ์\", listened=True)\n",
"except WorkflowError as e:\n",
" blocked += 1; print(f\"์ฐจ๋จ(U04 CONFIRM ๋จ๋
): {e}\")\n",
"q.decide(\"U04\", \"CONFIRM\", \"sec02\", \"์ธ์ฆ ํต๊ณผ, ์ฒญ์ ์ ์, ๊ต์ฐจ ํ์ธ ์๋ฃ\", listened=True, second=\"sec01\")\n",
"clock.t += 4; q.decide(\"U02\", \"HOLD\", \"sec02\", \"์ฌ์ก์ ์์ฒญ ํ ์ฌ์ฒญ์\")\n",
"clock.t += 3; q.authenticate(\"U06\", \"MIKE\", expected_response(\"MIKE\", \"NET-C\"), \"sec02\")\n",
"q.decide(\"U06\", \"CONFIRM\", \"sec02\", \"์ญํธ์ถ ์ธ์ฆ ํต๊ณผ, ์ผ์ ๋ณด๊ณ ์์ฒญ\", listened=True)\n",
"clock.t += 150\n",
"print(\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ :\", q.check_holds(), \"/ ์์ ํ ๊ฒํ ์์:\", [i[\"id\"] for i in q.pending()])\n",
"q.commander_feedback(\"U01\", \"์์กฐ ์ฐจ๋จ ์ ์ . ์ธ์ฆ ์ง์์ด ๊ต์ฒด ์ฃผ๊ธฐ ๋จ์ถ ๊ฒํ \")\n",
"\n",
"df = pd.DataFrame(q.items.values())\n",
"df[\"๋๊ธฐ(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(1)\n",
"print(\"\\n\" + df[[\"id\", \"grade\", \"score\", \"auth\", \"status\", \"escalated\", \"๋๊ธฐ(s)\"]].fillna(\"-\").to_string(index=False))\n",
"n_dec = sum(r[\"event\"] == \"DECIDE\" for r in q.log)\n",
"n_exp = sum(i[\"status\"] in (\"CONFIRMED\", \"REJECTED\", \"HOLD\") for i in q.items.values())\n",
"n_auto = sum(r[\"event\"] == \"DECIDE\" and not r[\"operator\"] for r in q.log)\n",
"print(f\"\\n์๋ ์ ํ {n_auto}๊ฑด / ๊ท์น ์๋ฐ ์ฐจ๋จ {blocked}๊ฑด / ๊ฒฐ์ ๋ก๊ทธ {n_dec}๊ฑด(๊ฒฐ์ {n_exp}๊ฑด, ๋ก๊น
์ฑ๊ณต๋ฅ {n_dec / n_exp:.0%})\")\n",
"print(f\"ํ๊ท ๊ฒํ ๋๊ธฐ {df['๋๊ธฐ(s)'].mean():.1f}s, ๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {q.verify_log()}\")\n",
"q.log[7][\"operator\"] = \"sec99\"\n",
"print(\"8๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น):\", q.verify_log())\n",
"print(\"\\n์ฑ
์ ๋ถ๊ณ: ํ์ง๊ธฐ=์ ์ ์ ์, ํต์ ๋ณด์๊ด=์ธ์ฆยท์ฒญ์ยท๊ฒฐ์ , ์งํ๊ด=์ต์ข
๋ช
๋ น ์ฑ
์. ์๋ ํ์ ์ผ๋ก ๋ช
๋ น์ ์ ํํ๊ฑฐ๋ ์ฐจ๋จํ์ง ์๋๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒํ ์์(๋ฑ๊ธยท์ ์ ์): ['U01', 'U04', 'U02', 'U05', 'U06', 'U03']\n",
"์๋ ์ ํ(ํ์ธ)๋ ๋ช
๋ น ์: 0\n",
"์ฐจ๋จ(U03 CONFIRM): ์ธ์ฆ ์ง์์๋ต ํต๊ณผ ์ ์ ํ ๋ถ๊ฐ\n",
"์ฐจ๋จ(U01 CONFIRM): ํต์ ๋ณด์๊ด ID ํ์\n",
"์ฐจ๋จ(U02 FORWARD): ํ์ฉ๋์ง ์์ ์กฐ์น: FORWARD\n",
"์ฐจ๋จ(U06 REJECT): ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\n",
"์ฐจ๋จ(U04 CONFIRM ๋จ๋
): High ๋ฑ๊ธ ํ์ธ์ ๋ค๋ฅธ ๊ฒํ ์ 1๋ช
์ ๊ต์ฐจ ํ์ธ ํ์\n",
"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ : ['U02'] / ์์ ํ ๊ฒํ ์์: ['U02', 'U05']\n",
"\n",
" id grade score auth status escalated ๋๊ธฐ(s)\n",
"U01 High 0.97 False REJECTED False 43.0\n",
"U02 Med 0.62 - HOLD True 48.0\n",
"U03 Low 0.08 True CONFIRMED False 25.0\n",
"U04 High 0.88 True CONFIRMED False 34.0\n",
"U05 Med 0.45 - PENDING False -\n",
"U06 Low 0.12 True CONFIRMED False 31.0\n",
"\n",
"์๋ ์ ํ 0๊ฑด / ๊ท์น ์๋ฐ ์ฐจ๋จ 5๊ฑด / ๊ฒฐ์ ๋ก๊ทธ 5๊ฑด(๊ฒฐ์ 5๊ฑด, ๋ก๊น
์ฑ๊ณต๋ฅ 100%)\n",
"ํ๊ท ๊ฒํ ๋๊ธฐ 36.2s, ๊ฐ์ฌ ๋ก๊ทธ 17๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: (True, -1)\n",
"8๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น): (False, 8)\n",
"\n",
"์ฑ
์ ๋ถ๊ณ: ํ์ง๊ธฐ=์ ์ ์ ์, ํต์ ๋ณด์๊ด=์ธ์ฆยท์ฒญ์ยท๊ฒฐ์ , ์งํ๊ด=์ต์ข
๋ช
๋ น ์ฑ
์. ์๋ ํ์ ์ผ๋ก ๋ช
๋ น์ ์ ํํ๊ฑฐ๋ ์ฐจ๋จํ์ง ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ ์์ ์ฌ๋ฐ ๋ฐ ์ก์ ํผ์
ํ๊ฒฝ ๋ชจ์ ๋ฅํ์ดํฌ ํ์ง ํ๊ณ์น ์คํธ๋ ์ค ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์์ ์ฌ๋ฐ ๋ฐ ์ฌํ ์ก์ ํผ์
ํ๊ฒฝ์ ๋ชจ์ํ์ฌ ๋ฅํ์ดํฌ ํ์ง ์ฑ๋ฅ ํ๊ณ์น ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์์ ์ฌ๋ฐ๊ณผ ์ฌํ ์ก์ ํ๊ฒฝ์์ ์์กฐ ํ์ง ์ฑ๋ฅ์ ํ๊ณ์ ์ ์ฐพ๊ณ , ํธ๋ํฝ ๊ธ์ฆ ๋ถํ์์๋ ์์ ํ๊ฒ ๋์ํ๋์ง ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **SNR๊ณผ ๋จ์ผํค ์ฌ๋ฐ ์ธ๊ธฐ๋ฅผ ๋จ๊ณ์ ์ผ๋ก ๋์ด๋ฉฐ ์ ํ๋ยท์ฌํ์จยท์ค๊ฒฝ๋ณด์จ ๊ณก์ ์ ๊ทธ๋ ค ํ์ ๋ถ๊ฐ ๊ตฌ๊ฐ์ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/c204_stress_test.py ๋ฐ ์คํธ๋ ์ค ํ
์คํธ ๋ถ์ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ทน๋จ ์ํฉ์์์ ์์ ์คํจ(Fail-safe) ๋ฐ ๊ธฐ๋ฅ ์ถ์ ์๋ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์ ํธ๋ํฝ 500% ์ฆ๊ฐ ์ ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ ์๋ฎฌ๋ ์ด์
๋ถํ ์ฃผ์
ํ
์คํธ ํต๊ณผ ๋ฐ ๋ณ๋ชฉ ๊ตฌ๊ฐ ํ๋กํ์ผ๋ง ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ถํ ํ
์คํธ ๋๊ตฌ๋ฅผ ํตํ ์ฒ๋ฆฌ๋(Throughput) ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ์์ ์ฌ๋ฐยท์ก์ ์ธ๊ธฐ๋ฅผ ๋จ๊ณ์ ์ผ๋ก ๋์ฌ ์์กฐ ํ์ง ์ฑ๋ฅ์ ํ๊ณ์ ์ ์ฐพ๊ณ , ๋ฌด์ ํธ๋ํฝ์ ํ์์ 5๋ฐฐ๋ก ๋๋ฆฌ๋ฉฐ ์์ ์
๋ ฅ์ ์์ ๋ถํ ์ํ์์ ์์คํ
๋ค์ดยท๋ฐ์ดํฐ ์ ์ค ์์ด ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋์ ์ฌ๋ ํ์ธ์ผ๋ก ์์ ํ๊ฒ ๋๊ธฐ๋์ง ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import tracemalloc\n",
"from collections import deque\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"tpl = enroll()\n",
"\n",
"\n",
"def featurize(X, M):\n",
" F = extract(X)\n",
" F[\"spk_sim\"] = speaker_sim(F, M[\"claimed\"], tpl)\n",
" return F\n",
"\n",
"\n",
"Xtr, Mtr = make_dataset(500, seed=61, snr=(3, 25))\n",
"det = make_pipeline(StandardScaler(), LogisticRegression(max_iter=3000, C=0.5)).fit(featurize(Xtr, Mtr), Mtr[\"spoof\"])\n",
"\n",
"# 1) ํ์ง ํ๊ณ์น: ์ก์(SNR)ยท์ฌ๋ฐ ์ธ๊ธฐ ๋จ๊ณ๋ณ ์ ํ๋\n",
"print(\"[ํ์ง ํ๊ณ์น - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ๋จ๊ณ๋ณ 120ํด๋ฆฝ]\")\n",
"print(\" ์กฐ๊ฑด ์ ํ๋ ์์กฐ์ฌํ์จ ์ค๊ฒฝ๋ณด์จ\")\n",
"curve = []\n",
"for name, ch in [(\"SNR 20dB\", dict(snr=20.0)), (\"SNR 10dB\", dict(snr=10.0)), (\"SNR 5dB\", dict(snr=5.0)), (\"SNR 0dB\", dict(snr=0.0)),\n",
" (\"SNR -5dB\", dict(snr=-5.0)), (\"์ฌ๋ฐ 0.5\", dict(snr=12.0, jam_tone=0.5)), (\"์ฌ๋ฐ 1.0\", dict(snr=12.0, jam_tone=1.0)),\n",
" (\"์ฌ๋ฐ 2.0\", dict(snr=12.0, jam_tone=2.0))]:\n",
" X, M = make_dataset(120, seed=700 + len(curve), **ch)\n",
" pr = det.predict(featurize(X, M)); y = M[\"spoof\"].values\n",
" curve.append((name, (pr == y).mean(), pr[y].mean(), pr[~y].mean()))\n",
" print(f\" {name:12s} {curve[-1][1]:7.1%} {curve[-1][2]:9.1%} {curve[-1][3]:9.1%}\")\n",
"lim = [c[0] for c in curve if c[1] < 0.8]\n",
"print(\"์ ํ๋ 80% ๋ฏธ๋ง ๊ตฌ๊ฐ(ํ๊ณ):\", lim if lim else \"์์\", \"-> ์ด ๊ตฌ๊ฐ์ ์๋ ์ ์ ๋์ 'ํ์ ๋ถ๊ฐ(์ฌ๋ ํ์ธ)'๋ก ์ฒ๋ฆฌ\")\n",
"\n",
"# 2) ๋ถํ ์ํ: ํ์ ์ด๋น 20๊ฑด -> 500% ์ฆ๊ฐ(์ด๋น 100๊ฑด), ์์ ์
๋ ฅ 3% ํผ์
\n",
"pool, pmeta = make_dataset(200, seed=790, snr=(3, 25))\n",
"rng = np.random.default_rng(800)\n",
"\n",
"\n",
"def arrivals(n):\n",
" idx = rng.integers(0, len(pool), n)\n",
" xs = [pool[i] + rng.normal(0, 0.02, N) for i in idx]\n",
" for k in np.flatnonzero(rng.random(n) < 0.03): # ์์ ์
๋ ฅ ์ฃผ์
\n",
" xs[k] = [np.full(N, np.nan), np.zeros(10), np.zeros(N)][k % 3]\n",
" return xs, pmeta.iloc[idx].reset_index(drop=True)\n",
"\n",
"\n",
"def valid(x):\n",
" return len(x) == N and np.isfinite(x).all() and np.abs(x).max() > 1e-6\n",
"\n",
"\n",
"def run(rate, ticks=8, budget_s=1.0, backlog_lim=150):\n",
" \"\"\"1์ด ๋จ์๋ก rate๊ฑด ๋์ฐฉ. ์ฒ๋ฆฌ ์๊ฐ์ด ์์ฐ์ ๋๊ฑฐ๋ ๋๊ธฐ์ด์ด ๊ธธ๋ฉด ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋(ํ์ ์ ์ฌ๋ ์๋ต, ์ ์๋ง ์ฐ์ถ ํ ์ฌ๋ ํ์ธ ํ์).\"\"\"\n",
" q, done, quarantine, degraded, crash = deque(), 0, 0, 0, 0\n",
" lat, t_feat, t_pred = [], 0.0, 0.0\n",
" total_in = 0\n",
" tracemalloc.start(); m0 = tracemalloc.get_traced_memory()[0]\n",
" for _ in range(ticks):\n",
" xs, ms = arrivals(rate); total_in += rate\n",
" q.extend(zip(xs, ms.to_dict(\"records\")))\n",
" t_start = time.perf_counter()\n",
" while q and time.perf_counter() - t_start < budget_s:\n",
" batch = [q.popleft() for _ in range(min(25, len(q)))]\n",
" ok = [b for b in batch if valid(b[0])]\n",
" quarantine += len(batch) - len(ok) # ์์ ์
๋ ฅ์ ๋ฒ๋ฆฌ์ง ์๊ณ ๊ฒฉ๋ฆฌ ๋ณด๊ด\n",
" if not ok:\n",
" continue\n",
" try:\n",
" Xb = np.stack([b[0] for b in ok]); Mb = pd.DataFrame([b[1] for b in ok])\n",
" t0 = time.perf_counter(); F = extract(Xb)\n",
" if len(q) > backlog_lim:\n",
" F[\"spk_sim\"] = 0.9; degraded += len(ok) # ๊ธฐ๋ฅ ์ถ์: ํ์ ๊ฒ์ฆ ์๋ต -> ๊ฒฐ๊ณผ๋ '์ฌ๋ ํ์ธ' ํ์\n",
" else:\n",
" F[\"spk_sim\"] = speaker_sim(F, Mb[\"claimed\"], tpl)\n",
" t1 = time.perf_counter(); det.predict_proba(F); t2 = time.perf_counter()\n",
" t_feat += t1 - t0; t_pred += t2 - t1; lat.append((t2 - t0) * 1000 / len(ok)); done += len(ok)\n",
" except Exception:\n",
" crash += 1; q.extendleft(reversed(batch)) # ์คํจ ๋ฐฐ์น๋ ๋๊ธฐ์ด๋ก ๋๋๋ฆผ(์ ์ค ๋ฐฉ์ง)\n",
" break\n",
" while q: # ์ํ ์ข
๋ฃ ํ ์์ฌ ๋๊ธฐ์ด ์์ง\n",
" b = q.popleft()\n",
" if valid(b[0]):\n",
" F = extract(b[0][None]); F[\"spk_sim\"] = speaker_sim(F, [b[1][\"claimed\"]], tpl); det.predict_proba(F); done += 1\n",
" else:\n",
" quarantine += 1\n",
" mem = (tracemalloc.get_traced_memory()[0] - m0) / 1e6; tracemalloc.stop()\n",
" return dict(rate=rate, total=total_in, done=done, quarantine=quarantine, lost=total_in - done - quarantine, crash=crash,\n",
" degraded=degraded, ms=np.mean(lat), mem=mem, feat=t_feat, pred=t_pred)\n",
"\n",
"\n",
"print(\"\\n[๋ถํ ์ํ] ๋์ฐฉ๋ฅ ์
๋ ฅ ์ฒ๋ฆฌ ๊ฒฉ๋ฆฌ ์ ์ค ๋ค์ด ๊ธฐ๋ฅ์ถ์ ms/๊ฑด ๋ฉ๋ชจ๋ฆฌ์ฆ๊ฐMB\")\n",
"res = [run(20), run(100), run(100, budget_s=0.02)]\n",
"for r, nm in zip(res, [\"ํ์\", \"5๋ฐฐ\", \"5๋ฐฐ+์ฒ๋ฆฌ์ง์ฐ\"]):\n",
" print(f\"{nm:9s} {r['rate']:4d}/s {r['total']:5d} {r['done']:5d} {r['quarantine']:5d} {r['lost']:5d} {r['crash']:5d} {r['degraded']:8d} {r['ms']:6.2f} {r['mem']:10.2f}\")\n",
"hi = res[1]\n",
"print(f\"ํธ๋ํฝ 500% ์ฆ๊ฐ: ์์คํ
๋ค์ด {hi['crash']}๊ฑด, ๋ฐ์ดํฐ ์ ์ค {hi['lost'] / hi['total']:.1%} (๋ชฉํ 0๊ฑด/0%: {'๋ฌ์ฑ' if hi['crash'] == 0 and hi['lost'] == 0 else '๋ฏธ๋ฌ'})\")\n",
"Xp = pool[:200]; Pp = spectrogram(Xp); prof = {}\n",
"for nm, fn in [(\"์คํํธ๋ก๊ทธ๋จ\", lambda: spectrogram(Xp)), (\"MFCC\", lambda: mfcc(Pp)), (\"ํผ์น ์๊ธฐ์๊ด(jitter/HNR)\", lambda: jitter_hnr(Xp)), (\"ํ๋ ์ ๊ฒฝ๊ณ ๋จ์\", lambda: frame_mod(Xp))]:\n",
" t0 = time.perf_counter(); fn(); prof[nm] = time.perf_counter() - t0\n",
"tot = sum(prof.values())\n",
"print(f\"์ฒ๋ฆฌ ์๊ฐ ๊ตฌ์ฑ(100/s): ํน์ง ์ถ์ถ {hi['feat'] / (hi['feat'] + hi['pred']):.0%}, ์ถ๋ก {hi['pred'] / (hi['feat'] + hi['pred']):.0%}\")\n",
"print(\"ํน์ง ์ถ์ถ ๋ด๋ถ ํ๋กํ์ผ:\", \", \".join(f\"{k} {v / tot:.0%}\" for k, v in prof.items()), f\"-> ๋ณ๋ชฉ: {max(prof, key=prof.get)}\")\n",
"dg = res[2]\n",
"print(f\"์ฒ๋ฆฌ ์ง์ฐ ๋ชจ์: ๋๊ธฐ์ด ์ ์ฒด ์ {dg['degraded']}๊ฑด์ ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋๋ก ์ฒ๋ฆฌ, ์ ์ค {dg['lost']}๊ฑด, ๋ค์ด {dg['crash']}๊ฑด\")\n",
"print(\"๊ธฐ๋ฅ ์ถ์ ๊ฒฐ๊ณผ์ ํ์ ๋ถ๊ฐ ๊ตฌ๊ฐ ๊ฒฐ๊ณผ๋ ์๋ ์ฐจ๋จยท์ ํ ์์ด ํต์ ๋ณด์๊ด ํ์ธ ๋๊ธฐ์ด๋ก ๋ณด๋ธ๋ค(Fail-safe).\")\n",
"\n",
"plt.figure(figsize=(7, 3.5))\n",
"lab = [c[0].replace(\"์ฌ๋ฐ\", \"jam\") for c in curve]\n",
"plt.plot(lab, [c[1] for c in curve], \"o-\", label=\"accuracy\")\n",
"plt.plot(lab, [c[3] for c in curve], \"s--\", label=\"false alarm\")\n",
"plt.axhline(0.8, c=\"k\", ls=\":\"); plt.xticks(rotation=30); plt.legend(); plt.title(\"Detection limit under noise / jamming (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํ์ง ํ๊ณ์น - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ๋จ๊ณ๋ณ 120ํด๋ฆฝ]\n",
" ์กฐ๊ฑด ์ ํ๋ ์์กฐ์ฌํ์จ ์ค๊ฒฝ๋ณด์จ\n",
" SNR 20dB 99.2% 98.4% 0.0%\n",
" SNR 10dB 96.7% 96.8% 3.4%\n",
" SNR 5dB 97.5% 98.1% 3.0%\n",
" SNR 0dB 66.7% 100.0% 66.7%\n",
" SNR -5dB 48.3% 100.0% 98.4%\n",
" ์ฌ๋ฐ 0.5 95.8% 98.1% 6.1%\n",
" ์ฌ๋ฐ 1.0 90.0% 96.8% 17.2%\n",
" ์ฌ๋ฐ 2.0 85.8% 89.7% 19.2%\n",
"์ ํ๋ 80% ๋ฏธ๋ง ๊ตฌ๊ฐ(ํ๊ณ): ['SNR 0dB', 'SNR -5dB'] -> ์ด ๊ตฌ๊ฐ์ ์๋ ์ ์ ๋์ 'ํ์ ๋ถ๊ฐ(์ฌ๋ ํ์ธ)'๋ก ์ฒ๋ฆฌ\n",
"\n",
"[๋ถํ ์ํ] ๋์ฐฉ๋ฅ ์
๋ ฅ ์ฒ๋ฆฌ ๊ฒฉ๋ฆฌ ์ ์ค ๋ค์ด ๊ธฐ๋ฅ์ถ์ ms/๊ฑด ๋ฉ๋ชจ๋ฆฌ์ฆ๊ฐMB\n",
"ํ์ 20/s 160 150 10 0 0 0 1.29 1.23\n",
"5๋ฐฐ 100/s 800 774 26 0 0 0 1.19 4.02\n",
"5๋ฐฐ+์ฒ๋ฆฌ์ง์ฐ 100/s 800 781 19 0 0 146 1.09 4.87\n",
"ํธ๋ํฝ 500% ์ฆ๊ฐ: ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค 0.0% (๋ชฉํ 0๊ฑด/0%: ๋ฌ์ฑ)\n",
"์ฒ๋ฆฌ ์๊ฐ ๊ตฌ์ฑ(100/s): ํน์ง ์ถ์ถ 85%, ์ถ๋ก 15%\n",
"ํน์ง ์ถ์ถ ๋ด๋ถ ํ๋กํ์ผ: ์คํํธ๋ก๊ทธ๋จ 26%, MFCC 17%, ํผ์น ์๊ธฐ์๊ด(jitter/HNR) 54%, ํ๋ ์ ๊ฒฝ๊ณ ๋จ์ 3% -> ๋ณ๋ชฉ: ํผ์น ์๊ธฐ์๊ด(jitter/HNR)\n",
"์ฒ๋ฆฌ ์ง์ฐ ๋ชจ์: ๋๊ธฐ์ด ์ ์ฒด ์ 146๊ฑด์ ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋๋ก ์ฒ๋ฆฌ, ์ ์ค 0๊ฑด, ๋ค์ด 0๊ฑด\n",
"๊ธฐ๋ฅ ์ถ์ ๊ฒฐ๊ณผ์ ํ์ ๋ถ๊ฐ ๊ตฌ๊ฐ ๊ฒฐ๊ณผ๋ ์๋ ์ฐจ๋จยท์ ํ ์์ด ํต์ ๋ณด์๊ด ํ์ธ ๋๊ธฐ์ด๋ก ๋ณด๋ธ๋ค(Fail-safe).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ๋ฌด์ ์์กฐ ์คํ์ ์์ฑ ๋ถ์ ๋ฐ ์ํฅ ํน์ง ์ถ์ถ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ์ฌํ๊ฒํ ๋ฅผ ํตํด ์์กฐ ์์ฑ ์คํ์ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **1์ฐจ ๋ฐฐ์น Baseline์ ๋ชจ์ ์ด์ฉ ๊ธฐ๋ก์์ ์คํ์ ์ฌ๋ก๋ฅผ ๋ถ์(AAR)ํ๊ณ , ํน์ง ์ถ์ถยทํ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์คํ์ ์ ๋ฏธํ๊ณผ ์ค๊ฒฝ๋ณด๋ก ๋๋์ด ์ฑ๋ ์กฐ๊ฑดยท์์กฐ ์ ํ๋ณ๋ก ์ง๊ณํ๊ณ ์์ธ์ ๊ท๋ช
์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/c204_aar_report.md ๋ฐ configs/c204_tuned_params.yaml` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ค๋ฅ ์์ธ ๊ท๋ช
์ ๊ฐ๊ด์ฑ ๋ฐ ๊ฐ์ ์กฐ์น์ ๋
ผ๋ฆฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: AAR ํผ๋๋ฐฑ ์ ์ฉ ํ ์ข
ํฉ ์ ํ๋ 10%p ์ด์ ํฅ์, ์ฌ๋ฐ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: AAR ๋ณด๊ณ ์ ์์ฑ ๋ฐ ํ๋ผ๋ฏธํฐ ํ๋ ์ ํ ํ๊ท ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ท ํ
์คํธ(Regression Test) ์ค์ํธ ์ ์ฒด ์๋ ์คํ ๋ฐ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"1์ฐจ ๋ฐฐ์น(๊ท์น ๊ธฐ๋ฐ Baseline)์ ๋ชจ์ ์ด์ฉ ๊ธฐ๋ก์์ ์์กฐ ์คํ์ ์ฌ๋ก๋ฅผ ์ ํยท์ฑ๋ ์กฐ๊ฑด๋ณ๋ก ๋ถ์(AAR)ํ๊ณ , ์คํ์ ์ฌ๋ก ์ฌํ์ตยท์ฑ๋ ํ์ง ํน์ง ์ถ๊ฐยท์ ์ฒ๋ฆฌ ์ฐจ๊ฐ ๋ฐฐ์์ ํ์ ์๊ณ๊ฐ ์ฌํ์์ผ๋ก ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ ๋ค ํ๊ท ์ํ์ผ๋ก ์ ํ๋ ํฅ์๊ณผ ์ฌ๋ฐ ์ค๋ฅ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"def channel_quality(X):\n",
" \"\"\"์ฑ๋ ํ์ง ์งํ: ์ถ์ SNR(์ ํจ ํ๋ ์ ์๋์ง ์ํ์ ๋น, dB), ๋จ์ผํค ์ฌ๋ฐ ์ง์(์ฃผํ์๋ณ ์๊ฐ ํ๊ท /ํ์คํธ์ฐจ์ ์ต๋๊ฐ), ๋๊น ๋น์จ(๋ฌด์ 10ms ๋น์จ).\"\"\"\n",
" P = spectrogram(X)\n",
" e = P.sum(1)\n",
" e = np.where(e > 1e-9, e, np.nan)\n",
" snr_est = 10 * np.log10(np.nanpercentile(e, 90, axis=1) / (np.nanpercentile(e, 10, axis=1) + 1e-12))\n",
" Pb = P[:, (FREQS >= 300) & (FREQS < 3400)]\n",
" tone = (Pb.mean(2) / (Pb.std(2) + 1e-12)).max(1) # ์๊ฐ์ ๋ฐ๋ผ ์ผ์ ํ ์ฃผํ์ ์ฑ๋ถ(์ฌ๋ฐ ํค)์ผ์๋ก ํผ\n",
" fr = np.abs(np.atleast_2d(X)).reshape(len(X), -1, 80).max(-1)\n",
" drop = (fr < 1e-6).mean(1)\n",
" return pd.DataFrame({\"snr_est\": snr_est, \"tone_idx\": tone, \"drop_ratio\": drop})\n",
"\n",
"tpl = enroll()\n",
"CONDS = {\"ํ์\": dict(snr=(12, 25)), \"์ SNR\": dict(snr=(3, 8)), \"์ถฉ๊ฒฉ์ก์\": dict(snr=(8, 15), impulse=0.002),\n",
" \"์ ํธ๋๊น\": dict(snr=(8, 15), dropout=0.4), \"์ฌ๋ฐ\": dict(snr=(8, 15), jam_tone=0.5)}\n",
"\n",
"\n",
"def ops_set(n, seed):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ชจ์ ์ด์ฉ ๊ธฐ๋ก: ์ฑ๋ ์กฐ๊ฑด 5์ข
x n/5 ํด๋ฆฝ, ์ฌ์ ๊ณต๊ฒฉ ๋น์ค์ ๋๊ฒ ๋ .\"\"\"\n",
" parts = []\n",
" for i, (c, ch) in enumerate(CONDS.items()):\n",
" X, M = make_dataset(n // 5, seed=seed + i, p=[0.5, 0.15, 0.15, 0.2], **ch); M[\"cond\"] = c; parts.append((X, M))\n",
" return np.vstack([p[0] for p in parts]), pd.concat([p[1] for p in parts], ignore_index=True)\n",
"\n",
"\n",
"def feats(X, M, denoise=0.0):\n",
" F = extract(X, denoise); F[\"spk_sim\"] = speaker_sim(F, M[\"claimed\"], tpl)\n",
" return pd.concat([F, channel_quality(X)], axis=1)\n",
"\n",
"\n",
"# ---- ์ฌ๋ณด์ ์ : ๋ฏธ์
3 ๊ท์น ๊ธฐ๋ฐ Baseline(์ง์ง ์์ฑ 3% ๋ถ์์ ๊ฒฝ๊ณ) ----\n",
"CUES = [(\"hf_ratio\", \"low\"), (\"frame_mod\", \"high\"), (\"hnr\", \"low\"), (\"flatness\", \"high\"), (\"lf_ratio\", \"low\"), (\"spk_sim\", \"low\")]\n",
"Xb, Mb = make_dataset(200, seed=7, labels=[\"bonafide\"])\n",
"Fb = feats(Xb, Mb)\n",
"th = {f: Fb[f].quantile(0.03) if d == \"low\" else Fb[f].quantile(0.97) for f, d in CUES}\n",
"rule = lambda F: pd.DataFrame({f: (F[f] < th[f]) if d == \"low\" else (F[f] > th[f]) for f, d in CUES}).any(axis=1).values\n",
"\n",
"Xo, Mo = ops_set(400, seed=900) # AAR ๋์ ์ด์ฉ ๊ธฐ๋ก\n",
"Fo = feats(Xo, Mo)\n",
"Mo[\"pred\"] = rule(Fo); Mo[\"err\"] = Mo[\"pred\"] != Mo[\"spoof\"]\n",
"print(f\"[AAR] ์ด์ฉ ๊ธฐ๋ก {len(Mo)}๊ฑด, ์ฌ๋ณด์ ์ ์ ํ๋ {1 - Mo['err'].mean():.1%}, ์คํ์ {Mo['err'].sum()}๊ฑด\")\n",
"Mo[\"์ ํ\"] = np.where(Mo[\"spoof\"], \"๋ฏธํ(์์กฐ->์ง์ง)\", \"์ค๊ฒฝ๋ณด(์ง์ง->์์กฐ)\")\n",
"tab = Mo[Mo[\"err\"]].groupby([\"์ ํ\", \"cond\"]).size().unstack(fill_value=0)\n",
"print(tab.to_string())\n",
"miss_lab = Mo[Mo[\"err\"] & Mo[\"spoof\"]][\"label\"].value_counts()\n",
"print(\"๋ฏธํ ์์กฐ ์ ํ:\", {k: int(v) for k, v in miss_lab.items()})\n",
"print(\"์์ธ ๋ถ์: โ ์ฌ์(replay)์ ์ง์ง ์์ฑ์ ๋
น์ํ ๊ฒ์ด๋ผ ํ์ ์ ์ฌ๋ยท์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ์ด ๋์ ๊ท์น์ด ๋์น๋ค\",\n",
" \"\\n โก ์ฌ๋ฐยท์ SNRยท๋๊น ์ฑ๋์์ ์ง์ง ์์ฑ์ ๋จ์ ๊ฐ์ด ๊ฒฝ๊ณ๋ฅผ ๋์ด ์ค๊ฒฝ๋ณด๊ฐ ๋๋ค(์ฑ๋ ํ์ง ๋ฏธ๋ฐ์)\",\n",
" \"\\n โข ๋จ์๋ณ ๊ณ ์ ๊ฒฝ๊ณ(3% ๋ถ์์)๋ ์กฐ๊ฑด์ด ๋ฐ๋์ด๋ ๊ทธ๋๋ก๋ผ ์กฐํฉ ํ๋จ์ด ๋ถ๊ฐ๋ฅํ๋ค\")\n",
"\n",
"# ---- ์ฌ๋ณด์ : ์ด์ฉ ๊ธฐ๋ก(์คํ์ ํฌํจ)๊ณผ ์กฐ๊ฑด ํผํฉ ๋ฐ์ดํฐ๋ก ์ฌํ์ต, ์ฐจ๊ฐ ๋ฐฐ์ยท์๊ณ๊ฐ ํ์ ----\n",
"Xn, Mn = ops_set(600, seed=950)\n",
"Xv, Mv = ops_set(300, seed=980) # ๊ฒ์ฆ์ฉ(ํ๋ผ๋ฏธํฐ ์ ํ)\n",
"best = None\n",
"for dn in [0.0, 0.5, 1.0]:\n",
" Ftr = pd.concat([feats(Xn, Mn, dn), feats(Xo, Mo, dn)], ignore_index=True)\n",
" ytr = np.r_[Mn[\"spoof\"], Mo[\"spoof\"]]\n",
" w = np.r_[np.ones(len(Mn)), np.where(Mo[\"err\"], 3, 1)] # AAR ์คํ์ ์ฌ๋ก ๊ฐ์ค ๋ฐ์(ํ๋ณธ ๋ณต์ )\n",
" idx = np.repeat(np.arange(len(ytr)), w.astype(int))\n",
" mdl = make_pipeline(StandardScaler(), MLPClassifier((64, 32), alpha=1e-3, max_iter=800, random_state=0)).fit(Ftr.iloc[idx], ytr[idx])\n",
" pv = mdl.predict_proba(feats(Xv, Mv, dn))[:, 1]\n",
" for t in [0.3, 0.4, 0.5, 0.6, 0.7]:\n",
" acc = ((pv >= t) == Mv[\"spoof\"]).mean()\n",
" if best is None or acc > best[0]:\n",
" best = (acc, dn, t, mdl)\n",
"acc_v, dn, t, mdl = best\n",
"yaml = f\"\"\"# configs/c204_tuned_params.yaml [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"preprocess:\n",
" spectral_subtraction: {dn}\n",
"features: [mfcc13_mean, mfcc13_std, hf_ratio, lf_ratio, flatness, flux, jitter, hnr, frame_mod, spk_sim, snr_est, tone_idx, drop_ratio]\n",
"model:\n",
" type: mlp\n",
" hidden: [64, 32]\n",
" aar_case_weight: 3\n",
"decision:\n",
" spoof_threshold: {t}\n",
" poor_channel_rule: snr_est<3.3 or tone_idx>2.5 or drop_ratio>0.05 -> human_review\n",
"\"\"\"\n",
"print(\"\\n[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ]\\n\" + yaml.strip())\n",
"\n",
"# ---- ํ๊ท ์ํ ----\n",
"Xh, Mh = ops_set(400, seed=1100) # ์ ์ด์ฉ ๊ธฐ๋ก(ํ์ต์ ์ฐ์ง ์์)\n",
"before = (rule(feats(Xh, Mh)) == Mh[\"spoof\"]).mean()\n",
"ph = mdl.predict_proba(feats(Xh, Mh, dn))[:, 1] >= t\n",
"after = (ph == Mh[\"spoof\"]).mean()\n",
"po = mdl.predict_proba(feats(Xo, Mo, dn))[:, 1] >= t\n",
"recur = int(((po != Mo[\"spoof\"]) & Mo[\"err\"]).sum())\n",
"newerr = int(((po != Mo[\"spoof\"]) & ~Mo[\"err\"]).sum())\n",
"print(f\"\\n[ํ๊ท ์ํ] ์ ์ด์ฉ ๊ธฐ๋ก {len(Mh)}๊ฑด ์ ํ๋: ์ฌ๋ณด์ ์ {before:.1%} -> ํ {after:.1%} ({(after - before) * 100:+.1f}%p, ๋ชฉํ +10%p: {'๋ฌ์ฑ' if after - before >= 0.10 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"AAR ์คํ์ {Mo['err'].sum()}๊ฑด ์ฌ๋ฐ {recur}๊ฑด (๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if recur == 0 else '๋ฏธ๋ฌ'}), ๊ธฐ์กด ์ ๋ต ์ฌ๋ก ์ค ์ ์ค๋ฅ {newerr}๊ฑด\")\n",
"print(\"์กฐ๊ฑด๋ณ ์ ํ๋(์ ๊ธฐ๋ก):\", {c: f\"{(ph[Mh['cond'] == c] == Mh['spoof'][Mh['cond'] == c]).mean():.0%}\" for c in CONDS})\n",
"print(\"์ง์ ํ์ต ์ฐ๊ณ: ํต์ ๋ณด์๊ด์ด ํ์ ํ ์ฌ๋ก๋ง ๋ผ๋ฒจ๋ก ๋์ -> ์ 1ํ ์ฌํ์ต -> ๋ณธ ํ๊ท ์ํ ํต๊ณผ ์์๋ง ๊ต์ฒด ๋ฐฐ์น\")\n",
"\n",
"plt.figure(figsize=(6, 3.5))\n",
"c = list(CONDS)\n",
"plt.bar(np.arange(5) - 0.2, [(rule(feats(Xh[Mh['cond'] == k], Mh[Mh['cond'] == k].reset_index(drop=True))) == Mh['spoof'][Mh['cond'] == k].values).mean() for k in c], 0.4, label=\"before (rules)\")\n",
"plt.bar(np.arange(5) + 0.2, [(ph[Mh['cond'] == k] == Mh['spoof'][Mh['cond'] == k]).mean() for k in c], 0.4, label=\"after (retuned)\")\n",
"plt.xticks(range(5), [\"normal\", \"low SNR\", \"impulse\", \"dropout\", \"jamming\"]); plt.ylabel(\"accuracy\"); plt.legend(); plt.title(\"AAR recalibration (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR] ์ด์ฉ ๊ธฐ๋ก 400๊ฑด, ์ฌ๋ณด์ ์ ์ ํ๋ 69.8%, ์คํ์ 121๊ฑด\n",
"cond ์ ํธ๋๊น ์ฌ๋ฐ ์ SNR ์ถฉ๊ฒฉ์ก์ ํ์\n",
"์ ํ \n",
"๋ฏธํ(์์กฐ->์ง์ง) 10 11 3 12 23\n",
"์ค๊ฒฝ๋ณด(์ง์ง->์์กฐ) 10 15 26 8 3\n",
"๋ฏธํ ์์กฐ ์ ํ: {'replay': 53, 'vc_art': 4, 'tts_art': 2}\n",
"์์ธ ๋ถ์: โ ์ฌ์(replay)์ ์ง์ง ์์ฑ์ ๋
น์ํ ๊ฒ์ด๋ผ ํ์ ์ ์ฌ๋ยท์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ์ด ๋์ ๊ท์น์ด ๋์น๋ค \n",
" โก ์ฌ๋ฐยท์ SNRยท๋๊น ์ฑ๋์์ ์ง์ง ์์ฑ์ ๋จ์ ๊ฐ์ด ๊ฒฝ๊ณ๋ฅผ ๋์ด ์ค๊ฒฝ๋ณด๊ฐ ๋๋ค(์ฑ๋ ํ์ง ๋ฏธ๋ฐ์) \n",
" โข ๋จ์๋ณ ๊ณ ์ ๊ฒฝ๊ณ(3% ๋ถ์์)๋ ์กฐ๊ฑด์ด ๋ฐ๋์ด๋ ๊ทธ๋๋ก๋ผ ์กฐํฉ ํ๋จ์ด ๋ถ๊ฐ๋ฅํ๋ค\n",
"\n",
"[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ]\n",
"# configs/c204_tuned_params.yaml [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"preprocess:\n",
" spectral_subtraction: 0.0\n",
"features: [mfcc13_mean, mfcc13_std, hf_ratio, lf_ratio, flatness, flux, jitter, hnr, frame_mod, spk_sim, snr_est, tone_idx, drop_ratio]\n",
"model:\n",
" type: mlp\n",
" hidden: [64, 32]\n",
" aar_case_weight: 3\n",
"decision:\n",
" spoof_threshold: 0.3\n",
" poor_channel_rule: snr_est<3.3 or tone_idx>2.5 or drop_ratio>0.05 -> human_review\n",
"\n",
"[ํ๊ท ์ํ] ์ ์ด์ฉ ๊ธฐ๋ก 400๊ฑด ์ ํ๋: ์ฌ๋ณด์ ์ 78.2% -> ํ 96.8% (+18.5%p, ๋ชฉํ +10%p: ๋ฌ์ฑ)\n",
"AAR ์คํ์ 121๊ฑด ์ฌ๋ฐ 0๊ฑด (๋ชฉํ 0๊ฑด: ๋ฌ์ฑ), ๊ธฐ์กด ์ ๋ต ์ฌ๋ก ์ค ์ ์ค๋ฅ 0๊ฑด\n",
"์กฐ๊ฑด๋ณ ์ ํ๋(์ ๊ธฐ๋ก): {'ํ์': '100%', '์ SNR': '95%', '์ถฉ๊ฒฉ์ก์': '98%', '์ ํธ๋๊น': '95%', '์ฌ๋ฐ': '96%'}\n",
"์ง์ ํ์ต ์ฐ๊ณ: ํต์ ๋ณด์๊ด์ด ํ์ ํ ์ฌ๋ก๋ง ๋ผ๋ฒจ๋ก ๋์ -> ์ 1ํ ์ฌํ์ต -> ๋ณธ ํ๊ท ์ํ ํต๊ณผ ์์๋ง ๊ต์ฒด ๋ฐฐ์น\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๋ฌด์ ์์ฑ ์คํธํยท๋ฅํ์ดํฌ ํ์ง ์์คํ
ํตํฉ ๋ฐ C2 ์์ฑ ๋ณด์ ๋ธ๋ฆฌํ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฌด์ ์์ฑ ์คํธํยท๋ฅํ์ดํฌ ํ์ง ์์คํ
ํตํฉ ํจํค์ง ๋ฐ C2 ๋ณด์ ๋ธ๋ฆฌํ ๋ณด๊ณ ์๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฒ์ฆ๋ถํฐ ํ๋ณ, ์ ๋ขฐ๋ ๋ฑ๊ธ, ๊ฒ์ฆ ๋๊ธฐ์ด, ๊ฐ์ฌ ๋ก๊ทธ๊น์ง ํ ๋ฒ์ ์คํ๋๋ ํตํฉ ํจํค์ง๋ฅผ ๋ง๋ค๊ณ C2 ์์ฑ ๋ณด์ ๋ธ๋ฆฌํ์ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ ๊ธฐ๋ก์ ๊ฒ์ฆํด ์์ยท๋ฏธ๋ฑ๋ก ๊ธฐ๋ก์ ์ฐจ๋จํ๊ณ , ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ๋ก ํ๋ณยท๋ฑ๊ธ์ ์ฐ์ถํด ์์กฐ ์์ฌ๊ณผ ๊ธด๊ธ ๋ช
๋ น์ ์ ๋ ๊ฒ์ฆ ๋๊ธฐ์ด์ ์ฌ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_c204.py, README.md, docs/c204_final_report.pdf(md)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ์ค๋ฌด ์ฆ์ ๋์
๊ฐ๋ฅ์ฑ, ์ด์ฉ ์ ๋ขฐ๋ ๋ฐ ์ ๋ต์ ๊ธฐ๋ํจ๊ณผ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: End-to-End ํตํฉ ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต๋ฅ 100%, ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ํด๋ฆญ ์คํ ์คํฌ๋ฆฝํธ ๋์ ํ์ธ ๋ฐ ์ต์ข
๋ฐํ ์ฐ์ถ๋ฌผ ์์ฑ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ต์ข
๊ฒ์ฆ ์์ํ(ํ๊ฐ๊ด) ๋ชจ์ ์์ฐ ๋ฐ ์ ์ฒด ๊ธฐ๋ฅ ํตํฉ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์ ๋ฉํ๋ฐ์ดํฐ ๊ฒ์ฆ, ๋ฌด์ ์ฑ๋ ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ, ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ์ฉํ ์์กฐ ํ๋ณยท์ ๋ขฐ๋ ๋ฑ๊ธ, ํต์ ๋ณด์๊ด ๊ฒ์ฆ ๋๊ธฐ์ดยท๊ฐ์ฌ ๋ก๊ทธ๊น์ง ํ ๋ฒ์ ์คํํ๋ ํตํฉ ํ์ดํ๋ผ์ธ์ ๋ง๋ค๊ณ , ์์ฒด ์ ๊ฒ(ํจ์ ์ปค๋ฒ๋ฆฌ์ง ํฌํจ)๊ณผ C2 ์์ฑ ๋ณด์ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import signal\n",
"from scipy.fft import dct\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ง ํ์ต์ฉ ํฉ์ฑ ์ ํธ ์์ฑ๊ธฐ (์ค์ ์์ฑยทํ์ยทํต์ ๋ง ์ ๋ณด ์์)\n",
"# ์ฌ๋ ๋ชฉ์๋ฆฌ๋ฅผ ๋ง๋๋ ๊ธฐ๋ฅ์ด ์๋๋ผ, ๋ฐฐ์(ํ๋ชจ๋) ์ ํธ์ '์ง์ง ์ ํธ์ ์์ฐ์ค๋ฌ์ด ํ๋ค๋ฆผ'๊ณผ\n",
"# '์๋ ค์ง ์์กฐ ํ์ (๋ฌธํ์ ํ์ง ๋จ์)'์ ํต๊ณ์ ํน์ง์ ํ์ํด ํ์ง๊ธฐ๋ฅผ ํ์ต์ํค๊ธฐ ์ํ ์ถ์ ์ ํธ๋ค.\n",
"# bonafide : ์์ฐ์ค๋ฌ์ด ํผ์น ํ๋ค๋ฆผ(jitter)ยท์งํญ ํ๋ค๋ฆผ(shimmer)ยท์จ์๋ฆฌ ์ก์\n",
"# tts_art : ํ๋ค๋ฆผ์ด ๊ฑฐ์ ์๊ณ ๊ณ ์ฃผํ ์๋์ง๊ฐ ๋ถ์กฑํ๋ฉฐ 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ ์ด ์๋ ์ ํธ\n",
"# vc_art : ํ๋ ์๋ง๋ค ๋ฐฐ์ ์์์ด ๋๊ธฐ๊ณ ์คํํธ๋ผ ํฌ๋ฝ์ ์ด ๋ญ๊ฐ์ง ์ ํธ\n",
"# replay : ์คํผ์ปค ์ฌ์ ๋์ญ ์ ํ + ์ค๋ด ์ํฅ์ด ๋ํด์ง ์ ํธ\n",
"# ---------------------------------------------------------------------------\n",
"FS, DUR = 8000, 0.5\n",
"N = int(FS * DUR)\n",
"LABELS = [\"bonafide\", \"tts_art\", \"vc_art\", \"replay\"]\n",
"SPEAKERS = {f\"SPK_{c}\": (f0, fm) for c, f0, fm in [(\"A\", 115, (650, 1150, 2450)), (\"B\", 135, (700, 1220, 2600)), (\"C\", 98, (600, 1050, 2350)),\n",
" (\"D\", 160, (720, 1300, 2700)), (\"E\", 125, (680, 1180, 2500)), (\"F\", 105, (630, 1100, 2400))]}\n",
"\n",
"\n",
"def _source(rng, f0, fm, jitter, shimmer, phase_break=False, hf_cut=None, frame_mod=0.0):\n",
" t = np.arange(N) / FS\n",
" contour = f0 * (1 + 0.06 * np.sin(2 * np.pi * rng.uniform(1.5, 3.5) * t + rng.uniform(0, 6)))\n",
" f = contour * (1 + jitter * rng.normal(0, 1, N // 80 + 1).repeat(80)[:N]) # 10ms ๋จ์ ํผ์น ํ๋ค๋ฆผ(jitter)\n",
" phase = 2 * np.pi * np.cumsum(f) / FS\n",
" K = int(3800 / f0)\n",
" fk = np.arange(1, K + 1) * f0\n",
" amp = sum(np.exp(-((fk - F) / 120.0) ** 2) / (i + 1) for i, F in enumerate(fm)) + 0.02 / np.arange(1, K + 1)\n",
" if hf_cut:\n",
" amp = np.where(fk > hf_cut, amp * 0.2, amp)\n",
" ph = np.arange(1, K + 1)[:, None] * phase[None, :]\n",
" if phase_break: # 20ms ํ๋ ์๋ง๋ค ๋ฐฐ์๋ณ ์์ ๋ถ์ฐ์\n",
" ph = ph + rng.uniform(0, 2 * np.pi, (K, N // 160 + 1)).repeat(160, axis=1)[:, :N]\n",
" sig = (amp[:, None] * np.sin(ph)).sum(0)\n",
" env = 0.55 + 0.45 * np.sin(2 * np.pi * rng.uniform(3, 5) * t + rng.uniform(0, 6)) ** 2 # ์์ ํ ์งํญ ๋ณํ\n",
" env *= 1 + shimmer * np.interp(t, np.linspace(0, DUR, 60), rng.normal(0, 1, 60)) # ์๋งํ ์งํญ ํ๋ค๋ฆผ(shimmer)\n",
" if frame_mod:\n",
" env *= 1 + frame_mod * np.sign(np.sin(2 * np.pi * 50 * t)) # 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (50Hz)\n",
" return sig * env + (0.03 if jitter > 0.003 else 0.005) * rng.normal(0, 1, N) * env\n",
"\n",
"\n",
"def make_clip(label, speaker, rng):\n",
" f0, fm = SPEAKERS[speaker]\n",
" if label in (\"tts_art\", \"vc_art\"): # ์์กฐ๋ ํ์ ํน์ฑ์ ๊ทผ์ฌ๋ง ํจ\n",
" f0 = f0 * rng.uniform(0.92, 1.08); fm = tuple(F * rng.uniform(0.95, 1.05) for F in fm)\n",
" if label == \"bonafide\" or label == \"replay\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.008, 0.02), shimmer=rng.uniform(0.05, 0.12))\n",
" elif label == \"tts_art\":\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.0, 0.002), shimmer=0.01, hf_cut=rng.uniform(2300, 2800), frame_mod=rng.uniform(0.08, 0.2))\n",
" else:\n",
" x = _source(rng, f0, fm, jitter=rng.uniform(0.006, 0.015), shimmer=rng.uniform(0.04, 0.1), phase_break=True)\n",
" if label == \"replay\": # ์คํผ์ปค ๋์ญ + ์ค๋ด ์ํฅ\n",
" x = signal.sosfilt(signal.butter(4, [450, 2900], \"bandpass\", fs=FS, output=\"sos\"), x)\n",
" ir = rng.normal(0, 1, int(0.08 * FS)) * np.exp(-np.arange(int(0.08 * FS)) / (0.02 * FS)); ir[0] = 3.0\n",
" x = signal.fftconvolve(x, ir)[:N]\n",
" return x / (np.abs(x).max() + 1e-9)\n",
"\n",
"\n",
"RADIO_BP = signal.butter(4, [300, 3400], \"bandpass\", fs=FS, output=\"sos\")\n",
"\n",
"\n",
"def radio_channel(x, rng, snr_db=15.0, impulse=0.0, dropout=0.0, clip=1.0, jam_tone=0.0):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฌด์ ์ฑ๋ ๋ชจํ: 300~3400Hz ๋์ญ ์ ํ, ๋ฐฐ๊ฒฝ ์ก์(SNR), ์ถฉ๊ฒฉ์ฑ ์ก์, ์ ํธ ๋๊น, ํฌํ(ํด๋ฆฌํ), ๋จ์ผํค ์ฌ๋ฐ.\"\"\"\n",
" y = signal.sosfilt(RADIO_BP, x)\n",
" p = np.mean(y ** 2)\n",
" noise = rng.normal(0, 1, N) * np.sqrt(p / 10 ** (snr_db / 10))\n",
" noise += np.cumsum(rng.normal(0, 0.02, N)) * np.sqrt(p) * 0.05 # ์ ์ฃผํ ์ฐจ๋ยท์์งํ ์์\n",
" y = y + noise\n",
" if impulse:\n",
" k = rng.random(N) < impulse\n",
" y[k] += rng.normal(0, 6 * np.sqrt(p), k.sum())\n",
" if jam_tone:\n",
" y += jam_tone * np.sqrt(p) * np.sin(2 * np.pi * rng.uniform(800, 2000) * np.arange(N) / FS)\n",
" if dropout:\n",
" for _ in range(int(dropout * 10)):\n",
" a = int(rng.integers(0, N - 400)); y[a:a + int(rng.integers(100, 400))] = 0.0\n",
" return np.clip(y / (np.abs(y).max() + 1e-9), -clip, clip)\n",
"\n",
"\n",
"def make_dataset(n=400, seed=7, snr=(5, 25), labels=LABELS, p=None, claim=\"SPK_A\", **chan):\n",
" \"\"\"ํด๋ฆฝ n๊ฐ: ์ง์ง๋ ์ฌ๋ฌ ํ์, ์์กฐ๋ ๋ชจ๋ ์งํ๊ด ํ์(SPK_A)๋ฅผ ์ฌ์นญ. ๋ฐํ: (์ ํธ ๋ฐฐ์ด, ๋ฉํ DataFrame).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, meta = np.zeros((n, N)), []\n",
" if p is None and len(labels) > 1 and labels[0] == \"bonafide\": # ๊ธฐ๋ณธ: ์ง์ง 50%, ๋๋จธ์ง ์์กฐ ์ ํ ๊ท ๋ฑ\n",
" p = [0.5] + [0.5 / (len(labels) - 1)] * (len(labels) - 1)\n",
" for i in range(n):\n",
" lab = str(rng.choice(labels, p=p))\n",
" spk = str(rng.choice(list(SPEAKERS))) if lab == \"bonafide\" else claim\n",
" if lab == \"replay\":\n",
" spk = claim\n",
" s = float(rng.uniform(*snr)) if isinstance(snr, tuple) else float(snr)\n",
" X[i] = radio_channel(make_clip(lab, spk, rng), rng, snr_db=s, **chan)\n",
" meta.append({\"clip\": f\"C{seed:02d}{i:04d}\", \"label\": lab, \"spoof\": lab != \"bonafide\", \"claimed\": spk if lab == \"bonafide\" else claim,\n",
" \"true_spk\": spk if lab == \"bonafide\" else \"unknown\", \"snr_db\": round(s, 1)})\n",
" return X, pd.DataFrame(meta)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌยท์ํฅ ํน์ง(MFCC/์คํํธ๋ก๊ทธ๋จ) ๊ณตํต ํจ์: ์ฌ๋ฌ ํด๋ฆฝ์ ํ ๋ฒ์ ๊ณ์ฐ ----\n",
"NFFT, HOP = 256, 80\n",
"\n",
"\n",
"def mel_bank(n_mels=26, fmin=300, fmax=3400):\n",
" mel = lambda f: 2595 * np.log10(1 + f / 700)\n",
" imel = lambda m: 700 * (10 ** (m / 2595) - 1)\n",
" pts = imel(np.linspace(mel(fmin), mel(fmax), n_mels + 2))\n",
" freqs = np.fft.rfftfreq(NFFT, 1 / FS)\n",
" fb = np.zeros((n_mels, len(freqs)))\n",
" for m in range(n_mels):\n",
" l, c, r = pts[m], pts[m + 1], pts[m + 2]\n",
" fb[m] = np.clip(np.minimum((freqs - l) / (c - l), (r - freqs) / (r - c)), 0, None)\n",
" return fb\n",
"\n",
"\n",
"MEL = mel_bank()\n",
"FREQS = np.fft.rfftfreq(NFFT, 1 / FS)\n",
"\n",
"\n",
"def spectrogram(X, denoise=0.0):\n",
" \"\"\"๋ฐฐ์น ์ ๋ ฅ ์คํํธ๋ก๊ทธ๋จ (ํด๋ฆฝ, ์ฃผํ์, ํ๋ ์). denoise>0์ด๋ฉด ์ก์ ๋ฐ๋ฅ(์ฃผํ์๋ณ ํ์ 10%)์ ๊ทธ ๋ฐฐ์๋งํผ ๋นผ๋ ์คํํธ๋ผ ์ฐจ๊ฐ.\"\"\"\n",
" X = np.atleast_2d(X)\n",
" Xp = np.c_[X[:, :1], X[:, 1:] - 0.95 * X[:, :-1]] # ํ๋ฆฌ์ ํผ์์ค\n",
" frames = np.lib.stride_tricks.sliding_window_view(Xp, NFFT, axis=1)[:, ::HOP] * np.hanning(NFFT)\n",
" P = np.abs(np.fft.rfft(frames, axis=-1)) ** 2\n",
" P = np.transpose(P, (0, 2, 1))\n",
" if denoise:\n",
" floor = np.percentile(P, 10, axis=2, keepdims=True)\n",
" P = np.maximum(P - denoise * floor, 0.05 * P)\n",
" return P\n",
"\n",
"\n",
"def mfcc(P, n=13):\n",
" logmel = np.log(np.einsum(\"mf,cft->cmt\", MEL, P) + 1e-8)\n",
" return dct(logmel, type=2, axis=1, norm=\"ortho\")[:, :n, :]\n",
"\n",
"\n",
"def band(P, lo, hi):\n",
" return P[:, (FREQS >= lo) & (FREQS < hi), :].sum(axis=(1, 2))\n",
"\n",
"\n",
"def jitter_hnr(X):\n",
" \"\"\"40ms ํ๋ ์ ์๊ธฐ์๊ด์ผ๋ก ํผ์น ์ฃผ๊ธฐ๋ฅผ ์ฐพ์ ํผ์น ํ๋ค๋ฆผ(jitter)๊ณผ ๋ฐฐ์ ๋ ์ก์๋น(HNR ๊ทผ์ฌ)๋ฅผ ๊ตฌํ๋ค.\"\"\"\n",
" W, lag0, lag1 = 320, int(FS / 250), int(FS / 70)\n",
" fr = np.lib.stride_tricks.sliding_window_view(np.atleast_2d(X), W, axis=1)[:, ::160]\n",
" fr = fr - fr.mean(-1, keepdims=True)\n",
" spec = np.fft.rfft(fr, 2 * W, axis=-1)\n",
" ac = np.fft.irfft(np.abs(spec) ** 2, axis=-1)[..., :W]\n",
" ac = ac / (ac[..., :1] + 1e-12)\n",
" seg = ac[..., lag0:lag1]\n",
" k = seg.argmax(-1)\n",
" peak = seg.max(-1)\n",
" km, kp = np.take_along_axis(seg, np.clip(k - 1, 0, None)[..., None], -1)[..., 0], np.take_along_axis(seg, np.clip(k + 1, None, seg.shape[-1] - 1)[..., None], -1)[..., 0]\n",
" lag = k + lag0 + 0.5 * (km - kp) / (km - 2 * peak + kp - 1e-12) # ํฌ๋ฌผ์ ๋ณด๊ฐ์ผ๋ก ์ฃผ๊ธฐ ์ ๋ฐํ\n",
" f0 = FS / lag\n",
" jit = np.median(np.abs(np.diff(f0, axis=1)) / f0[:, 1:], axis=1)\n",
" return jit, np.median(peak, axis=1)\n",
"\n",
"\n",
"def frame_mod(X):\n",
" \"\"\"์งํญ ํฌ๋ฝ์ (2.5ms ๊ฐ๊ฒฉ)์ 50Hz ์ฑ๋ถ ๋น์จ: 20ms ํ๋ ์ ๊ฒฝ๊ณ ํ์ (ํฉ์ฑ ๋จ์).\"\"\"\n",
" env = np.abs(np.atleast_2d(X)).reshape(len(np.atleast_2d(X)), -1, 20).mean(-1)\n",
" sp = np.abs(np.fft.rfft(env - env.mean(1, keepdims=True), axis=1)) ** 2\n",
" f = np.fft.rfftfreq(env.shape[1], 20 / FS)\n",
" return sp[:, (f > 47) & (f < 53)].sum(1) / (sp[:, (f > 30) & (f < 70)].sum(1) + 1e-12)\n",
"\n",
"\n",
"FEAT_NAMES = [f\"mfcc{i}_m\" for i in range(13)] + [f\"mfcc{i}_s\" for i in range(13)] + \\\n",
" [\"delta_s\", \"hf_ratio\", \"lf_ratio\", \"flatness\", \"flux\", \"jitter\", \"hnr\", \"frame_mod\"]\n",
"\n",
"\n",
"def extract(X, denoise=0.0):\n",
" \"\"\"ํด๋ฆฝ ๋ฐฐ์น -> ํน์ง ํ๋ ฌ (ํด๋ฆฝ x 34) DataFrame.\"\"\"\n",
" P = spectrogram(X, denoise)\n",
" C = mfcc(P)\n",
" tot = band(P, 300, 3400) + 1e-12\n",
" flat = np.exp(np.log(P[:, (FREQS >= 300) & (FREQS < 3400)] + 1e-10).mean(1)) / (P[:, (FREQS >= 300) & (FREQS < 3400)].mean(1) + 1e-10)\n",
" logP = np.log(P + 1e-8)\n",
" jit, hnr = jitter_hnr(X)\n",
" F = np.c_[C.mean(2), C.std(2), np.abs(np.diff(C, axis=2)).mean((1, 2)), band(P, 2400, 3400) / tot, band(P, 300, 500) / tot,\n",
" flat.mean(1), np.abs(np.diff(logP, axis=2)).mean((1, 2)), jit, hnr, frame_mod(X)]\n",
" return pd.DataFrame(F, columns=FEAT_NAMES)\n",
"\n",
"\n",
"def enroll(seed=99):\n",
" \"\"\"์งํ๊ดยทํต์ ์ ํ์ 6๋ช
์ ๋ฑ๋ก ๊ฒฌ๋ณธ(๊นจ๋ํ ์ง์ง ํด๋ฆฝ 5๊ฐ์ฉ์ MFCC ํ๊ท ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tpl = {}\n",
" for spk in SPEAKERS:\n",
" Xe = np.stack([radio_channel(make_clip(\"bonafide\", spk, rng), rng, snr_db=25) for _ in range(5)])\n",
" tpl[spk] = extract(Xe)[[f\"mfcc{i}_m\" for i in range(1, 13)]].mean().values\n",
" return tpl\n",
"\n",
"\n",
"def speaker_sim(F, claimed, tpl):\n",
" \"\"\"์ฃผ์ฅ ํ์ ๊ฒฌ๋ณธ๊ณผ MFCC ํ๊ท ๋ฒกํฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋(ํ์ ๊ฒ์ฆ ์ ์).\"\"\"\n",
" v = F[[f\"mfcc{i}_m\" for i in range(1, 13)]].values\n",
" t = np.stack([tpl[c] for c in claimed])\n",
" return (v * t).sum(1) / (np.linalg.norm(v, axis=1) * np.linalg.norm(t, axis=1) + 1e-12)\n",
"\n",
"def channel_quality(X):\n",
" \"\"\"์ฑ๋ ํ์ง ์งํ: ์ถ์ SNR(์ ํจ ํ๋ ์ ์๋์ง ์ํ์ ๋น, dB), ๋จ์ผํค ์ฌ๋ฐ ์ง์(์ฃผํ์๋ณ ์๊ฐ ํ๊ท /ํ์คํธ์ฐจ์ ์ต๋๊ฐ), ๋๊น ๋น์จ(๋ฌด์ 10ms ๋น์จ).\"\"\"\n",
" P = spectrogram(X)\n",
" e = P.sum(1)\n",
" e = np.where(e > 1e-9, e, np.nan)\n",
" snr_est = 10 * np.log10(np.nanpercentile(e, 90, axis=1) / (np.nanpercentile(e, 10, axis=1) + 1e-12))\n",
" Pb = P[:, (FREQS >= 300) & (FREQS < 3400)]\n",
" tone = (Pb.mean(2) / (Pb.std(2) + 1e-12)).max(1) # ์๊ฐ์ ๋ฐ๋ผ ์ผ์ ํ ์ฃผํ์ ์ฑ๋ถ(์ฌ๋ฐ ํค)์ผ์๋ก ํผ\n",
" fr = np.abs(np.atleast_2d(X)).reshape(len(X), -1, 80).max(-1)\n",
" drop = (fr < 1e-6).mean(1)\n",
" return pd.DataFrame({\"snr_est\": snr_est, \"tone_idx\": tone, \"drop_ratio\": drop})\n",
"\n",
"CONFIG = {\"spectral_subtraction\": 0.0, \"spoof_threshold\": 0.3, \"high_threshold\": 0.8, # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฐ\n",
" \"poor\": {\"snr_est\": 3.3, \"tone_idx\": 2.5, \"drop_ratio\": 0.05}}\n",
"CALLED = set()\n",
"\n",
"\n",
"def traced(fn):\n",
" \"\"\"ํจ์ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ์ฉ: ํธ์ถ๋ ํ์ดํ๋ผ์ธ ํจ์ ์ด๋ฆ์ ๊ธฐ๋ก.\"\"\"\n",
" def w(*a, **k):\n",
" CALLED.add(fn.__name__); return fn(*a, **k)\n",
" w.__name__ = fn.__name__\n",
" return w\n",
"\n",
"\n",
"@traced\n",
"def validate(meta, x):\n",
" errs = []\n",
" if not str(meta.get(\"clip\", \"\")).startswith(\"C\"):\n",
" errs.append(\"clip_id ํ์\")\n",
" if meta.get(\"claimed\") not in SPEAKERS:\n",
" errs.append(\"์ฃผ์ฅ ํ์ ๋ฏธ๋ฑ๋ก\")\n",
" if len(x) != N or not np.isfinite(x).all():\n",
" errs.append(\"์ ํธ ๊ธธ์ดยท๊ฐ ์ค๋ฅ\")\n",
" elif np.abs(x).max() < 1e-6:\n",
" errs.append(\"๋ฌด์ ํธ\")\n",
" return errs\n",
"\n",
"\n",
"@traced\n",
"def features(X, claimed):\n",
" F = extract(X, CONFIG[\"spectral_subtraction\"]); F[\"spk_sim\"] = speaker_sim(F, claimed, TPL)\n",
" return pd.concat([F, channel_quality(X)], axis=1)\n",
"\n",
"\n",
"@traced\n",
"def grade(p, q):\n",
" poor = (q[\"snr_est\"] < CONFIG[\"poor\"][\"snr_est\"]) | (q[\"tone_idx\"] > CONFIG[\"poor\"][\"tone_idx\"]) | (q[\"drop_ratio\"] > CONFIG[\"poor\"][\"drop_ratio\"])\n",
" g = np.where(p >= CONFIG[\"high_threshold\"], \"High\", np.where(p >= CONFIG[\"spoof_threshold\"], \"Med\", \"Low\"))\n",
" return np.where((g == \"High\") & poor.values & (p < 0.95), \"Med\", g), poor.values\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" @traced\n",
" def submit(self, rec):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", \"id\": rec[\"clip\"], \"grade\": rec[\"grade\"], \"score\": round(rec[\"score\"], 3), \"urgent\": rec[\"urgent\"]}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({**rec, \"status\": \"PENDING\"}) # ์๋ ์ ํยท์ฐจ๋จ ์์, ํต์ ๋ณด์๊ด ๊ฒ์ฆ ๋๊ธฐ\n",
"\n",
" @traced\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for r in self.log:\n",
" body = {k: v for k, v in r.items() if k not in (\"prev\", \"hash\")}\n",
" if r[\"prev\"] != prev or r[\"hash\"] != hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest():\n",
" return False\n",
" prev = r[\"hash\"]\n",
" return True\n",
"\n",
"\n",
"@traced\n",
"def train(seed=1200):\n",
" conds = [dict(snr=(10, 25)), dict(snr=(3, 8)), dict(snr=(8, 15), impulse=0.002), dict(snr=(8, 15), dropout=0.4), dict(snr=(8, 15), jam_tone=0.5)]\n",
" parts = [make_dataset(140, seed=seed + i, p=[0.5, 0.15, 0.15, 0.2], **c) for i, c in enumerate(conds)]\n",
" X = np.vstack([p[0] for p in parts]); M = pd.concat([p[1] for p in parts], ignore_index=True)\n",
" return make_pipeline(StandardScaler(), MLPClassifier((64, 32), alpha=1e-3, max_iter=800, random_state=0)).fit(features(X, M[\"claimed\"]), M[\"spoof\"])\n",
"\n",
"\n",
"@traced\n",
"def main(X, M, model):\n",
" queue, rejected, out = Queue(), {}, []\n",
" ok = []\n",
" for i in range(len(X)):\n",
" e = validate(M.iloc[i].to_dict(), X[i])\n",
" if e:\n",
" rejected[M[\"clip\"][i]] = e\n",
" else:\n",
" ok.append(i)\n",
" Xo, Mo = X[ok], M.iloc[ok].reset_index(drop=True)\n",
" t0 = time.perf_counter()\n",
" F = features(Xo, Mo[\"claimed\"])\n",
" p = model.predict_proba(F)[:, 1]\n",
" g, poor = grade(p, F)\n",
" ms = (time.perf_counter() - t0) * 1000 / len(Xo)\n",
" for k in range(len(Mo)):\n",
" rec = {\"clip\": Mo[\"clip\"][k], \"score\": float(p[k]), \"grade\": str(g[k]), \"urgent\": bool(Mo[\"urgent\"][k]), \"poor\": bool(poor[k])}\n",
" out.append(rec)\n",
" if rec[\"grade\"] != \"Low\" or rec[\"urgent\"]: # ์์กฐ ์์ฌ + ๊ธด๊ธ ๋ช
๋ น์ ์ ๋ ์ฌ๋ ๊ฒ์ฆ\n",
" queue.submit(rec)\n",
" return pd.DataFrame(out), Mo, rejected, queue, ms\n",
"\n",
"\n",
"T0 = time.perf_counter()\n",
"TPL = enroll()\n",
"model = train()\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ๊ฐ ์ด์ฉ ์๊ฐ๋ ์์ ๊ธฐ๋ก 300๊ฑด(ํผํฉ ์ฑ๋) + ์์ ๊ธฐ๋ก 3๊ฑด\n",
"parts = [make_dataset(60, seed=1300 + i, p=[0.5, 0.15, 0.15, 0.2], **c) for i, c in\n",
" enumerate([dict(snr=(10, 25)), dict(snr=(3, 8)), dict(snr=(8, 15), impulse=0.002), dict(snr=(8, 15), dropout=0.4), dict(snr=(8, 15), jam_tone=0.5)])]\n",
"X = np.vstack([p[0] for p in parts]); M = pd.concat([p[1] for p in parts], ignore_index=True)\n",
"M[\"urgent\"] = np.random.default_rng(1400).random(len(M)) < 0.15\n",
"X[5] = np.nan; X[77] = 0.0; M.loc[150, \"claimed\"] = \"SPK_Z\"\n",
"R, Mo, rejected, queue, ms = main(X, M, model)\n",
"y = Mo[\"spoof\"].values; alarm = R[\"grade\"].values != \"Low\"\n",
"recall, far, acc = alarm[y].mean(), alarm[~y].mean(), (alarm == y).mean()\n",
"elapsed = time.perf_counter() - T0\n",
"\n",
"chk = {\n",
" \"์์ ๊ธฐ๋ก 3๊ฑด ์ฐจ๋จ\": len(rejected) == 3,\n",
" \"์ฒ๋ฆฌ ๊ธฐ๋ก 297๊ฑด\": len(R) == 297,\n",
" \"์์กฐ ์ฌํ์จ 90% ์ด์\": recall >= 0.9,\n",
" \"์ค๊ฒฝ๋ณด์จ 10% ์ดํ\": far <= 0.10,\n",
" \"์๋ ์ ํยท์ฐจ๋จ 0๊ฑด\": all(i[\"status\"] == \"PENDING\" for i in queue.items),\n",
" \"๊ธด๊ธ ๋ช
๋ น ์ ๋ ๊ฒ์ฆ ๋๊ธฐ\": all(c in {i[\"clip\"] for i in queue.items} for c in Mo[\"clip\"][Mo[\"urgent\"]]),\n",
" \"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\": queue.verify() and len(queue.log) == len(queue.items),\n",
" \"ํด๋ฆฝ๋น 200ms ์ด๋ด\": ms < 200,\n",
" \"๋ฑ๊ธ ๊ท์น ์ผ๊ด\": all((r.grade == \"Low\") == (r.score < CONFIG[\"spoof_threshold\"]) for r in R.itertuples()),\n",
" \"์ ์ ๋ฒ์ 0~1ยทNaN ์์\": R[\"score\"].between(0, 1).all(),\n",
"}\n",
"passed = sum(chk.values())\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"funcs = [\"validate\", \"features\", \"grade\", \"submit\", \"verify\", \"train\", \"main\"]\n",
"cov = len(CALLED & set(funcs)) / len(funcs)\n",
"print(f\"์์ฒด ์ ๊ฒ {passed}/{len(chk)} ํต๊ณผ, E2E ์คํ ์ฑ๊ณต๋ฅ {'100%' if passed == len(chk) else f'{passed / len(chk):.0%}'}, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if cov >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"print(\"์ฐจ๋จ๋ ๊ธฐ๋ก:\", rejected)\n",
"\n",
"n_hi, n_med = (R[\"grade\"] == \"High\").sum(), (R[\"grade\"] == \"Med\").sum()\n",
"print(f\"\"\"\n",
"[C2 ์์ฑ ๋ณด์ ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ์์ {len(M)}๊ฑด ์ค {len(R)}๊ฑด ํ์ , ์์ยท๋ฏธ๋ฑ๋ก {len(rejected)}๊ฑด ์ฐจ๋จ, ์ ์ฒด {elapsed:.1f}s, ํด๋ฆฝ๋น {ms:.2f} ms\n",
"2. ์ฑ๋ฅ: ์์กฐ ์ฌํ์จ {recall:.1%}, ์ค๊ฒฝ๋ณด์จ {far:.1%}, ์ ํ๋ {acc:.1%} (ํ์ยท์ SNRยท์ถฉ๊ฒฉยท๋๊นยท์ฌ๋ฐ ํผํฉ ์ฑ๋)\n",
"3. ๊ฒ์ฆ ๋๊ธฐ: {len(queue.items)}๊ฑด (High {n_hi} / Med {n_med} / ๊ธด๊ธ ๋ช
๋ น {int(Mo['urgent'].sum())}), ์๋ ์ ํยท์ฐจ๋จ 0๊ฑด\n",
"4. ์ด์ฉ: ๊ธด๊ธ ๋ช
๋ น์ ๋ฑ๊ธ๊ณผ ๋ฌด๊ดํ๊ฒ ํต์ ๋ณด์๊ด์ด ์ญํธ์ถ ์ธ์ฆยท์ฒญ์ ํ ์ ํํ๋ฉฐ, ์ต์ข
๋ช
๋ น ์ฑ
์์ ์งํ๊ณํต์ ์๋ค.\n",
"5. ํ๊ณยทํ์ฅ: ํฉ์ฑ ์ ํธ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก ์ค์ ๋ฌด์ ๊ธฐยท์์ฑ์๋ ์ฌํ์ตยท์ฌ๊ฒ์ฆ์ด ํ์ํ๋ค. ์ธ์ ์ฒด๊ณ ์ฐ๋์ ํ์ ๊ฒฐ๊ณผ(JSON)๋ง ์ ๋ฌํ๋ API๋ก ํ์ฅํ๋ค.\"\"\")\n",
"\n",
"plt.figure(figsize=(6, 3.5))\n",
"plt.hist([R[\"score\"][~y], R[\"score\"][y]], bins=20, label=[\"bonafide\", \"spoof\"], color=[\"tab:green\", \"tab:red\"])\n",
"plt.axvline(CONFIG[\"spoof_threshold\"], c=\"k\", ls=\"--\"); plt.yscale(\"log\"); plt.xlabel(\"spoof score\"); plt.legend(); plt.title(\"Integrated pipeline output (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[PASS] ์์ ๊ธฐ๋ก 3๊ฑด ์ฐจ๋จ\n",
"[PASS] ์ฒ๋ฆฌ ๊ธฐ๋ก 297๊ฑด\n",
"[PASS] ์์กฐ ์ฌํ์จ 90% ์ด์\n",
"[PASS] ์ค๊ฒฝ๋ณด์จ 10% ์ดํ\n",
"[PASS] ์๋ ์ ํยท์ฐจ๋จ 0๊ฑด\n",
"[PASS] ๊ธด๊ธ ๋ช
๋ น ์ ๋ ๊ฒ์ฆ ๋๊ธฐ\n",
"[PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\n",
"[PASS] ํด๋ฆฝ๋น 200ms ์ด๋ด\n",
"[PASS] ๋ฑ๊ธ ๊ท์น ์ผ๊ด\n",
"[PASS] ์ ์ ๋ฒ์ 0~1ยทNaN ์์\n",
"์์ฒด ์ ๊ฒ 10/10 ํต๊ณผ, E2E ์คํ ์ฑ๊ณต๋ฅ 100%, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 100% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ)\n",
"์ฐจ๋จ๋ ๊ธฐ๋ก: {'C13000005': ['์ ํธ ๊ธธ์ดยท๊ฐ ์ค๋ฅ'], 'C13010017': ['๋ฌด์ ํธ'], 'C13020030': ['์ฃผ์ฅ ํ์ ๋ฏธ๋ฑ๋ก']}\n",
"\n",
"[C2 ์์ฑ ๋ณด์ ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ์์ 300๊ฑด ์ค 297๊ฑด ํ์ , ์์ยท๋ฏธ๋ฑ๋ก 3๊ฑด ์ฐจ๋จ, ์ ์ฒด 8.5s, ํด๋ฆฝ๋น 1.66 ms\n",
"2. ์ฑ๋ฅ: ์์กฐ ์ฌํ์จ 96.7%, ์ค๊ฒฝ๋ณด์จ 4.9%, ์ ํ๋ 96.0% (ํ์ยท์ SNRยท์ถฉ๊ฒฉยท๋๊นยท์ฌ๋ฐ ํผํฉ ์ฑ๋)\n",
"3. ๊ฒ์ฆ ๋๊ธฐ: 174๊ฑด (High 143 / Med 12 / ๊ธด๊ธ ๋ช
๋ น 43), ์๋ ์ ํยท์ฐจ๋จ 0๊ฑด\n",
"4. ์ด์ฉ: ๊ธด๊ธ ๋ช
๋ น์ ๋ฑ๊ธ๊ณผ ๋ฌด๊ดํ๊ฒ ํต์ ๋ณด์๊ด์ด ์ญํธ์ถ ์ธ์ฆยท์ฒญ์ ํ ์ ํํ๋ฉฐ, ์ต์ข
๋ช
๋ น ์ฑ
์์ ์งํ๊ณํต์ ์๋ค.\n",
"5. ํ๊ณยทํ์ฅ: ํฉ์ฑ ์ ํธ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก ์ค์ ๋ฌด์ ๊ธฐยท์์ฑ์๋ ์ฌํ์ตยท์ฌ๊ฒ์ฆ์ด ํ์ํ๋ค. ์ธ์ ์ฒด๊ณ ์ฐ๋์ ํ์ ๊ฒฐ๊ณผ(JSON)๋ง ์ ๋ฌํ๋ API๋ก ํ์ฅํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}