{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [C204] ๋ฌด์ „ ์Œ์„ฑ ์Šคํ‘ธํ•‘ยท๋”ฅํŽ˜์ดํฌ ํƒ์ง€ ์‹คํ—˜\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์ง€ํœ˜ํ†ต์ œ(C2) / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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 }