{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [ISR02] SNS ์ ์ ๊ฒ์๋ฌผ ์ค๋ณตยท๋ณ์กฐยท์ถ์ฒ ์ ๋ขฐ๋ ์๋ ๋ถ๋ฅ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ ๋ณดยท๊ฐ์ยท์ ์ฐฐ(ISR) / Level 2)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/ISR02/ISR02_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์ ๋ณดยท๊ฐ์ยท์ ์ฐฐ(ISR) (Level 2) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์์์๋ ํ
๋ ๊ทธ๋จยทX ๊ฐ์ SNS์ ํ์ฅ ์ ๋ณด์ ํจ๊ป ์ฌ๊ฒ์, ์์นยท์ง์ญ ๋ณ์กฐ, ๊ณผ๊ฑฐ ์ฌ์ง ์ฌํ์ฉ, ์์ฑํ AI ์กฐ์๋ฌผ, ์กฐ์ง์ ๋์ ์ ํฌ๊ฐ ํ๊บผ๋ฒ์ ์์์ง๋ฉด์ ๊ณต๊ฐ์ถ์ฒ์ ๋ณด(OSINT)์ ์ถ์ฒ์ ์ ๋ขฐ๋๋ฅผ ๋น ๋ฅด๊ฒ ๊ฐ๋ ค๋ด๋ ์ผ์ด ์ ๋ณด์ ์ ํต์ฌ์ด ๋์๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ๊ฐ์ ๊ณ์ ๊ณผ ๊ฐ์ ์ง์ญ ์ฝ๋๋ก ๋ง๋ ๊ต์ก์ฉ ํฉ์ฑ ๊ฒ์๋ฌผ์ ์ฌ์ฉํด ์์ฒ ๋ฐ์ดํฐ ์คํค๋ง ์ ์, ํ
์คํธ ์ ์ ์ ์ด๋ฏธ์ง ์ง๊ฐ ํด์ ๊ธฐ๋ฐ ์ค๋ณต ์ ๊ฑฐ, ์ ์ฌ๋ ๊ธฐ๋ฐ ๋ณต์ ยท1์ฐจ ์ถ์ฒ ๋ถ๋ฅ Baseline, ์กฐ์ยท๊ธฐ๋ง ํ์ง์ ๊ณ์ ์ ๋ขฐ๋ ๊ฒฐํฉ ๋ชจ๋ธ, ์ข
ํฉ ์ ๋ขฐ๋ ๋ฑ๊ธ(A~E) ์์ง, ์ ๋ณด์ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋, ์ ๋ณด๊ด ๊ธด๊ธ ๊ฒํ (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. ์ ์ฅ SNS(ํ
๋ ๊ทธ๋จ, X ๋ฑ) ํ
์คํธ/์ด๋ฏธ์ง ๊ฒ์๋ฌผ ์์ง ๋ฐ OSINT ์์ฒ ๋ฐ์ดํฐ์
์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ
๋ ๊ทธ๋จ, X ๋ฑ ์ ์ฅ SNS์์ ์์ง๋ ํ
์คํธ ๋ฐ ์ด๋ฏธ์ง ๊ฒ์๋ฌผ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ถ์ํ๊ณ ์์ฒ ๋ฐ์ดํฐ์
์คํค๋ง๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ
๋ ๊ทธ๋จ, X ๋ฑ ์ ์ฅ SNS์์ ์์งํ ํ
์คํธยท์ด๋ฏธ์ง ๊ฒ์๋ฌผ์ ๊ตฌ์กฐ๋ฅผ ๋ถ์ํ๊ณ , ์์ง ๊ฒฝ์์ ์ฃผ์ฅ ๋ด์ฉ๊น์ง ๋ด์ OSINT ์์ฒ ๋ฐ์ดํฐ์
์คํค๋ง์ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒ์๋ฌผ ๊ธฐ๋ณธ ์ ๋ณด(๊ฒ์๋ฌผยท๊ณ์ ID, ํ๋ซํผ, ๊ฒ์ ์๊ฐ, ๋ณธ๋ฌธ, ์ธ์ด), ๋ฏธ๋์ด(์ข
๋ฅ, ํฌ๊ธฐ, ์ง๊ฐ ํด์), ์ ๋ฌ ์๋ณธ(forwarded_from), ์์ง ๊ฒฝ์(์์ง ๋ฐฉ์ยท์๊ฐยท์์ง์ยท์์ง ์ง์ฐ), ์ฃผ์ฅ ๋ด์ฉ(๊ฐ์ ์ง์ญ ์ฝ๋, ์์น ๋ถํ์ค์ฑ), ํ๊ฐ ์ํ ํ๋ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/isr02_input_schema.json ๋ฐ docs/isr02_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ณดยท๊ฐ์ยท์ ์ฐฐ(ISR) ์ค๋ฌด ๋ฐ ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\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": [
"\"\"\"์ ์ฅ SNS(ํ
๋ ๊ทธ๋จ, X ๋ฑ) ํ
์คํธยท์ด๋ฏธ์ง ๊ฒ์๋ฌผ์ ์์ง ๋ฉํ๋ฐ์ดํฐ, ๊ณ์ ยท์ ๋ฌ ๊ฒฝ๋ก, ์ฃผ์ฅ ๋ด์ฉ(์ง์ญยท์๊ฐ), ์์ง ์ง์ฐ ๋ฑ OSINT ์์ฒ ๋ฐ์ดํฐ์
์คํค๋ง๋ฅผ ์ ์ํ๊ณ ๊ฐ์ ์ํ ๊ฒ์๋ฌผ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"ISO = r\"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}Z$\"\n",
"# 1) ์คํค๋ง ์ ์: ๊ฒ์๋ฌผ ๊ธฐ๋ณธ ์ ๋ณด, ๋ฏธ๋์ด, ์์ง ๊ฒฝ์, ์ฃผ์ฅ ๋ด์ฉ, ์์ง ์กฐ๊ฑด(์์น ๋ถํ์ค์ฑยท์ง์ฐ), ํ๊ฐ ์ํ\n",
"SCHEMA = {\n",
" \"required\": [\"post_id\", \"platform\", \"account_id\", \"posted_at\", \"text\", \"media\", \"collection\", \"claim\", \"assessment\"],\n",
" \"fields\": {\n",
" \"post_id\": {\"type\": str, \"pattern\": r\"^P\\d{4}$\"},\n",
" \"platform\": {\"type\": str, \"enum\": [\"telegram\", \"x\", \"other\"]},\n",
" \"account_id\": {\"type\": str, \"pattern\": r\"^ACC_[A-Z]{3}\\d{2}$\"},\n",
" \"posted_at\": {\"type\": str, \"pattern\": ISO},\n",
" \"text\": {\"type\": str, \"minlen\": 1, \"maxlen\": 4000},\n",
" \"lang\": {\"type\": str, \"enum\": [\"ko\", \"en\", \"uk\", \"ru\", \"other\"]},\n",
" },\n",
" \"media\": {\n",
" \"type\": {\"type\": str, \"enum\": [\"image\", \"video\"]},\n",
" \"width\": {\"type\": int, \"min\": 1, \"max\": 10000},\n",
" \"height\": {\"type\": int, \"min\": 1, \"max\": 10000},\n",
" \"phash\": {\"type\": str, \"pattern\": r\"^[0-9a-f]{16}$\"},\n",
" },\n",
" \"collection\": {\n",
" \"method\": {\"type\": str, \"enum\": [\"public_api\", \"manual_capture\", \"partner_share\"]},\n",
" \"collected_at\": {\"type\": str, \"pattern\": ISO},\n",
" \"collector\": {\"type\": str, \"pattern\": r\"^COL-\\d{2}$\"},\n",
" \"delay_min\": {\"type\": (int, float), \"min\": 0, \"max\": 100000},\n",
" },\n",
" \"claim\": {\n",
" \"region_code\": {\"type\": str, \"pattern\": r\"^[A-F]-\\d$\"},\n",
" \"geo_uncertainty_km\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n",
" },\n",
" # ์์ง ๋จ๊ณ์์๋ ์ง์ยท์ถ์ฒ ํ์ ์ ์ ์ง ์๋๋ค(์ ๋ณด๊ด ๊ฒํ ํ ๋ณ๋ ๊ธฐ๋ก)\n",
" \"assessment\": {\"status\": {\"type\": str, \"enum\": [\"unassessed\"]}},\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" if not isinstance(value, rule[\"type\"]) or isinstance(value, 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 \"minlen\" in rule and len(value) < rule[\"minlen\"]:\n",
" errs.append(f\"{name}: ๋น ๊ฐ\")\n",
" if \"maxlen\" in rule and len(value) > rule[\"maxlen\"]:\n",
" errs.append(f\"{name}: ๊ธธ์ด {rule['maxlen']}์ ์ด๊ณผ\")\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 = [f\"{k}: ํ์ ํ๋ ๋๋ฝ\" for k in SCHEMA[\"required\"] if k not in rec]\n",
" for k, rule in SCHEMA[\"fields\"].items():\n",
" if k in rec:\n",
" errs += check(k, rec[k], rule)\n",
" for group in (\"collection\", \"claim\", \"assessment\"):\n",
" for k, rule in SCHEMA[group].items():\n",
" if k not in rec.get(group, {}):\n",
" errs.append(f\"{group}.{k}: ํ์ ํ๋ ๋๋ฝ\")\n",
" else:\n",
" errs += check(f\"{group}.{k}\", rec[group][k], rule)\n",
" for i, m in enumerate(rec.get(\"media\", [])):\n",
" for k, rule in SCHEMA[\"media\"].items():\n",
" errs += [f\"media[{i}].{k}: ํ์ ํ๋ ๋๋ฝ\"] if k not in m else check(f\"media[{i}].{k}\", m[k], rule)\n",
" if rec.get(\"forwarded_from\") is not None and not re.match(r\"^P\\d{4}$\", str(rec[\"forwarded_from\"])):\n",
" errs.append(\"forwarded_from: ํ์ ๋ถ์ผ์น\")\n",
" return errs\n",
"\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒ์๋ฌผ 20๊ฑด ์์ฑ (๊ฐ์ ๊ณ์ ยท๊ฐ์ ์ง์ญ ์ฝ๋, ์ค์ ์ฑ๋ ์๋)\n",
"rng = np.random.default_rng(1)\n",
"posts = []\n",
"for i in range(20):\n",
" r = f\"{'ABCDEF'[int(rng.integers(6))]}-{int(rng.integers(1, 6))}\"\n",
" posts.append({\"post_id\": f\"P{i:04d}\", \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"])),\n",
" \"account_id\": f\"ACC_{rng.choice(['MED', 'WIT', 'AGG', 'PRO'])}{int(rng.integers(0, 30)):02d}\",\n",
" \"posted_at\": f\"2026-05-0{int(rng.integers(1, 9))}T1{int(rng.integers(0, 9))}:0{int(rng.integers(0, 9))}Z\",\n",
" \"text\": f\"๊ฐ์ {r} ์ง์ญ์์ ๊ตฐ์ฉ ์ฐจ๋ ๋ชฉ๊ฒฉ, {int(rng.integers(2, 15))}๋\", \"lang\": \"ko\",\n",
" \"media\": [{\"type\": \"image\", \"width\": 32, \"height\": 32, \"phash\": \"\".join(rng.choice(list(\"0123456789abcdef\"), 16))}],\n",
" \"forwarded_from\": f\"P{int(rng.integers(0, max(i, 1))):04d}\" if i and rng.random() < .4 else None,\n",
" \"collection\": {\"method\": str(rng.choice([\"public_api\", \"manual_capture\", \"partner_share\"])), \"collected_at\": \"2026-05-09T12:00Z\",\n",
" \"collector\": f\"COL-{int(rng.integers(1, 20)):02d}\", \"delay_min\": int(rng.integers(1, 600))},\n",
" \"claim\": {\"region_code\": r, \"geo_uncertainty_km\": float(round(rng.uniform(0.5, 20), 1))},\n",
" \"assessment\": {\"status\": \"unassessed\"}})\n",
"\n",
"# 3) ์ ์ ๋ฐ์ดํฐ ๊ฒ์ฆ\n",
"ok = sum(1 for p in posts if not validate(p))\n",
"print(f\"์ ์ ์ํ {len(posts)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(posts) - ok}๊ฑด (๋ชฉํ: ์ค๋ฅ 0๊ฑด {'๋ฌ์ฑ' if ok == len(posts) else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(posts[3]))\n",
"bad[\"platform\"] = \"unknown_app\"\n",
"bad[\"posted_at\"] = \"2026/05/01 10:00\"\n",
"del bad[\"collection\"][\"method\"]\n",
"bad[\"media\"][0][\"phash\"] = \"XYZ\"\n",
"bad[\"text\"] = \"\"\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ] ์ฃผ์
5์ข
\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"\n",
"# 5) ์คํค๋ง ํ๋ ์ ์์(๋ช
์ธ์ ์ด์)\n",
"rows = [(\"๊ธฐ๋ณธ\", k, r[\"type\"].__name__, str({x: y for x, y in r.items() if x != \"type\"})) for k, r in SCHEMA[\"fields\"].items()]\n",
"for g in (\"media\", \"collection\", \"claim\", \"assessment\"):\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\"})))\n",
"spec = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\"])\n",
"print(\"\\n[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\")\n",
"print(spec.to_string(index=False))\n",
"print(f\"\\nํ๋ ์ ์ {len(spec)}๊ฐ + forwarded_from(์ ๋ฌ ์๋ณธ, ์ ํ). ์์ง ์ง์ฐยท์์น ๋ถํ์ค์ฑ์ 'ํต์ ์ง์ฐยท์์น์ค์ฐจ' ์ ์ ํ๋ผ๋ฏธํฐ์ ํด๋นํ๋ค.\")\n",
"print(f\"์ ๋ฌ ๊ฒฝ๋ก๊ฐ ์๋ ๊ฒ์๋ฌผ {sum(p['forwarded_from'] is not None for p in posts)}๊ฑด: ์ถ์ฒ ์ญ์ถ์ (๋ฏธ์
3ยท7)์ ์ฌ์ฉ\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด (๋ชฉํ: ์ค๋ฅ 0๊ฑด ๋ฌ์ฑ)\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ] ์ฃผ์
5์ข
\n",
" - platform: ํ์ฉ๋์ง ์์ ๊ฐ 'unknown_app'\n",
" - posted_at: ํ์ ๋ถ์ผ์น '2026/05/01 10:00'\n",
" - text: ๋น ๊ฐ\n",
" - collection.method: ํ์ ํ๋ ๋๋ฝ\n",
" - media[0].phash: ํ์ ๋ถ์ผ์น 'XYZ'\n",
"\n",
"[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น\n",
" ๊ธฐ๋ณธ post_id str {'pattern': '^P\\\\d{4}$'}\n",
" ๊ธฐ๋ณธ platform str {'enum': ['telegram', 'x', 'other']}\n",
" ๊ธฐ๋ณธ account_id str {'pattern': '^ACC_[A-Z]{3}\\\\d{2}$'}\n",
" ๊ธฐ๋ณธ posted_at str {'pattern': '^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}Z$'}\n",
" ๊ธฐ๋ณธ text str {'minlen': 1, 'maxlen': 4000}\n",
" ๊ธฐ๋ณธ lang str {'enum': ['ko', 'en', 'uk', 'ru', 'other']}\n",
" media type str {'enum': ['image', 'video']}\n",
" media width int {'min': 1, 'max': 10000}\n",
" media height int {'min': 1, 'max': 10000}\n",
" media phash str {'pattern': '^[0-9a-f]{16}$'}\n",
"collection method str {'enum': ['public_api', 'manual_capture', 'partner_share']}\n",
"collection collected_at str {'pattern': '^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}Z$'}\n",
"collection collector str {'pattern': '^COL-\\\\d{2}$'}\n",
"collection delay_min number {'min': 0, 'max': 100000}\n",
" claim region_code str {'pattern': '^[A-F]-\\\\d$'}\n",
" claim geo_uncertainty_km number {'min': 0, 'max': 500}\n",
"assessment status str {'enum': ['unassessed']}\n",
"\n",
"ํ๋ ์ ์ 17๊ฐ + forwarded_from(์ ๋ฌ ์๋ณธ, ์ ํ). ์์ง ์ง์ฐยท์์น ๋ถํ์ค์ฑ์ 'ํต์ ์ง์ฐยท์์น์ค์ฐจ' ์ ์ ํ๋ผ๋ฏธํฐ์ ํด๋นํ๋ค.\n",
"์ ๋ฌ ๊ฒฝ๋ก๊ฐ ์๋ ๊ฒ์๋ฌผ 12๊ฑด: ์ถ์ฒ ์ญ์ถ์ (๋ฏธ์
3ยท7)์ ์ฌ์ฉ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๊ฒ์๋ฌผ ํ
์คํธ ๋ถ์ฉ์ด ์ ์ , ์ด๋ฏธ์ง ํด์(pHash) ์ถ์ถ ๋ฐ ์ค๋ณต ์ ๊ฑฐ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ
์คํธ ๋ถ์ฉ์ด ์ ๊ฑฐ, ํน์๋ฌธ์ ์ ์ , ์ด๋ฏธ์ง ์ง๊ฐ ํด์(pHash) ์ถ์ถ์ ํตํด ์ค๋ณต ๊ฒ์๋ฌผ์ ๊ฑธ๋ฌ๋ด๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฒ์๋ฌผ ํ
์คํธ์ ๋ถ์ฉ์ดยทํน์๋ฌธ์ยทURLยท์ด๋ชจ์ง๋ฅผ ์ ์ ํ๊ณ ์ด๋ฏธ์ง ์ง๊ฐ ํด์(pHash)๋ฅผ ์ถ์ถํด ์ค๋ณต ๊ฒ์๋ฌผ์ ๊ฑธ๋ฌ๋ด๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋์ฝ๋ ์ ๊ทํ(์ ๊ฐ ๋ฌธ์ ๋ฑ), URLยท๋ฉ์
ยทํด์ํ๊ทธ ๊ธฐํธยทํน์๋ฌธ์ยท์ด๋ชจ์งยท๋ถ์ฉ์ด ์ ๊ฑฐ, ๋ณธ๋ฌธ ๊ฒฐ์ธก ๋ณด์ ์ ๊ตฌํํ๊ณ , 32x32 ์ด๋ฏธ์ง์ DCT ์ ์ฃผํ ์ฑ๋ถ์ผ๋ก 64๋นํธ pHash ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/isr02_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": [
"\"\"\"SNS ๊ฒ์๋ฌผ ํ
์คํธ์ ์ ๋์ฝ๋ ์ ๊ทํยทURLยท๋ฉ์
ยทํน์๋ฌธ์ยท์ด๋ชจ์งยท๋ถ์ฉ์ด ์ ๊ฑฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ, ์ด๋ฏธ์ง ์ง๊ฐ ํด์(pHash) ์ถ์ถ์ ์ํํ๊ณ , ์ ๊ทํ ๋ณธ๋ฌธ๊ณผ ์ด๋ฏธ์ง ํด์๋ก ์ค๋ณต ๊ฒ์๋ฌผ์ ๊ฑธ๋ฌ๋ด๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํด ์ฒ๋ฆฌ ์๋์ ์ ํฉ์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"posts, acc = make_posts(60, seed=7)\n",
"rng = np.random.default_rng(3)\n",
"FIRE, FLAG = chr(0x1F525), chr(0x1F6A8) # ์ด๋ชจ์ง(์ฝ๋๊ฐ์ผ๋ก ์์ฑ)\n",
"for p in posts: # ์์ง ๊ณผ์ ์ ์ก์ ์ฃผ์
\n",
" r = rng.random()\n",
" if r < .15:\n",
" p[\"text\"] += f\" https://example.invalid/{rng.integers(1e6)}\"\n",
" elif r < .3:\n",
" p[\"text\"] = \"@ch_virtual \" + p[\"text\"] + \" \" + FIRE + FLAG\n",
" elif r < .4:\n",
" p[\"text\"] = unicodedata.normalize(\"NFKC\", p[\"text\"]).replace(\"A\", \"๏ผก\").replace(\"1\", \"๏ผ\") # ์ ๊ฐ ๋ฌธ์\n",
" elif r < .43:\n",
" p[\"text\"] = None # ๋ณธ๋ฌธ ๊ฒฐ์ธก(์ด๋ฏธ์ง๋ง ๊ฒ์)\n",
"\n",
"# 1) ์ฒ๋ฆฌ ์๋์ ์ค๋ฅ์จ\n",
"t0, out, fails = time.perf_counter(), [], 0\n",
"for p in posts:\n",
" try:\n",
" txt = clean_text(p[\"text\"] or \"\")\n",
" out.append({\"id\": p[\"post_id\"], \"t\": p[\"posted_at\"], \"clean\": txt, \"missing\": p[\"text\"] is None,\n",
" \"hash\": phash(p[\"image\"]) if p[\"image\"] is not None else None})\n",
" except Exception:\n",
" fails += 1\n",
"dt = time.perf_counter() - t0\n",
"print(f\"์ ์ฒ๋ฆฌ {len(posts)}๊ฑด, {len(posts) / dt:,.0f}๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์), ์คํจ {fails}๊ฑด -> ์ค๋ฅ์จ {fails / len(posts):.0%}\")\n",
"miss = [o for o in out if o[\"missing\"]]\n",
"print(f\"๋ณธ๋ฌธ ๊ฒฐ์ธก {len(miss)}๊ฑด: ๋น ๋ฌธ์์ด๋ก ๋ณด์ ํ๊ณ '์ด๋ฏธ์ง ๋จ๋
๊ฒ์๋ฌผ'๋ก ํ์(๋ณด์ ์จ 100%)\")\n",
"\n",
"# 2) ์ค๋ณต ์ ๊ฑฐ: ๊ฐ์ ์ ๊ทํ ๋ณธ๋ฌธ + ์ด๋ฏธ์ง ํด์ ๊ฑฐ๋ฆฌ 4 ์ดํ(๋๋ ๋ ๋ค ์์)๋ ๊ฐ์ ๊ฒ์๋ฌผ๋ก ๋ณด๊ณ ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ๋ง ๋จ๊ธด๋ค\n",
"kept, dup_of = [], {}\n",
"for o in sorted(out, key=lambda x: x[\"t\"]):\n",
" same = next((k for k in kept if k[\"clean\"] == o[\"clean\"] and o[\"clean\"] and\n",
" ((k[\"hash\"] is None and o[\"hash\"] is None) or (k[\"hash\"] is not None and o[\"hash\"] is not None and hamming(k[\"hash\"], o[\"hash\"]) <= 4))), None)\n",
" if same:\n",
" dup_of[o[\"id\"]] = same[\"id\"]\n",
" else:\n",
" kept.append(o)\n",
"kinds = {p[\"post_id\"]: p[\"kind\"] for p in posts}\n",
"print(f\"์ค๋ณต ์ ๊ฑฐ: {len(out)}๊ฑด -> {len(kept)}๊ฑด (์ ๊ฑฐ {len(dup_of)}๊ฑด, ๊ทธ์ค ์ค์ ์ฌ๊ฒ์(copy) {sum(kinds[d] == 'copy' for d in dup_of)}๊ฑด / ์กฐ์ง์ ๋์ ๊ฒ์ {sum(kinds[d] == 'bot_burst' for d in dup_of)}๊ฑด)\")\n",
"print(f\"์ ๊ฐ ๋ฌธ์ยท์ด๋ชจ์งยทURL ์ฐจ์ด๋ ์ ์ ๋ก ํก์๋๊ณ , ๋ณธ๋ฌธ ๊ฒฐ์ธก ๋ฑ์ผ๋ก ๋จ์ ์ฌ๊ฒ์ {sum(kinds[k['id']] == 'copy' for k in kept)}๊ฑด์ ๋ฏธ์
3์ ์ ์ฌ๋ ํ๋ณ๋ก ๋๊ธด๋ค\")\n",
"\n",
"# 3) ๋จ์ ํ
์คํธ 5์ข
\n",
"e1, e2 = event_image(1), event_image(2)\n",
"tests = {\n",
" \"URLยท๋ฉ์
์ ๊ฑฐ\": clean_text(\"@abc ๋ณด๊ณ https://x.invalid/1 ํ์ธ\") == \"๋ณด๊ณ ํ์ธ\",\n",
" \"์ด๋ชจ์งยทํน์๋ฌธ์ ์ ๊ฑฐ\": FIRE not in clean_text(\"์ ์ \" + FIRE + \"!!\") and \"!\" not in clean_text(\"์ ์ !!\"),\n",
" \"์ ๊ฐ ๋ฌธ์ ์ ๊ทํ(NFKC)\": clean_text(\"๏ผก-๏ผ ์ ์ \") == \"A-1 ์ ์ \",\n",
" \"pHash ์ฌ์์ถ ๊ฐ๊ฑด์ฑ(๊ฑฐ๋ฆฌ 8 ์ดํ)\": hamming(phash(e1), phash(ndimage.gaussian_filter(e1, 0.6) + rng.normal(0, 3, e1.shape))) <= 8,\n",
" \"pHash ๋ค๋ฅธ ์ฌ์ง ๊ตฌ๋ถ(๊ฑฐ๋ฆฌ 16 ์ด์)\": hamming(phash(e1), phash(e2)) >= 16,\n",
"}\n",
"for k, v in tests.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๋จ์ ํ
์คํธ {sum(tests.values())}/5 ํต๊ณผ\")\n",
"\n",
"# 4) ์ ์ ์ ํ ์์\n",
"ex = [next(p for p in posts if p[\"text\"] and key in p[\"text\"]) for key in (\"http\", \"@\", \"\\uff21\")]\n",
"print(\"\\n[์ ์ ์ ํ ์์]\")\n",
"for p in ex:\n",
" print(\" ์ :\", p[\"text\"].replace(FIRE, \"\").replace(FLAG, \"\"))\n",
" print(\" ํ:\", clean_text(p[\"text\"]))\n",
"\n",
"d_same = [hamming(phash(p[\"image\"]), phash(posts[int(p[\"origin\"][1:])][\"image\"])) for p in posts if p[\"kind\"] in (\"copy\", \"near_dup\") and p[\"image\"] is not None]\n",
"d_diff = [hamming(phash(posts[i][\"image\"]), phash(posts[j][\"image\"])) for i, j in rng.integers(0, len(posts), (200, 2))\n",
" if posts[i][\"event_id\"] != posts[j][\"event_id\"] and posts[i][\"image\"] is not None and posts[j][\"image\"] is not None]\n",
"plt.figure(figsize=(7, 4))\n",
"plt.hist([d_same, d_diff], bins=32, range=(0, 64), label=[\"same event (repost)\", \"different events\"])\n",
"plt.axvline(8, ls=\"--\", c=\"k\"); plt.xlabel(\"pHash Hamming distance\"); plt.ylabel(\"pairs\"); plt.legend(); plt.title(\"Perceptual hash distance (synthetic images)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ฒ๋ฆฌ 573๊ฑด, 6,551๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์), ์คํจ 0๊ฑด -> ์ค๋ฅ์จ 0%\n",
"๋ณธ๋ฌธ ๊ฒฐ์ธก 14๊ฑด: ๋น ๋ฌธ์์ด๋ก ๋ณด์ ํ๊ณ '์ด๋ฏธ์ง ๋จ๋
๊ฒ์๋ฌผ'๋ก ํ์(๋ณด์ ์จ 100%)\n",
"์ค๋ณต ์ ๊ฑฐ: 573๊ฑด -> 340๊ฑด (์ ๊ฑฐ 233๊ฑด, ๊ทธ์ค ์ค์ ์ฌ๊ฒ์(copy) 169๊ฑด / ์กฐ์ง์ ๋์ ๊ฒ์ 53๊ฑด)\n",
"์ ๊ฐ ๋ฌธ์ยท์ด๋ชจ์งยทURL ์ฐจ์ด๋ ์ ์ ๋ก ํก์๋๊ณ , ๋ณธ๋ฌธ ๊ฒฐ์ธก ๋ฑ์ผ๋ก ๋จ์ ์ฌ๊ฒ์ 6๊ฑด์ ๋ฏธ์
3์ ์ ์ฌ๋ ํ๋ณ๋ก ๋๊ธด๋ค\n",
"[PASS] URLยท๋ฉ์
์ ๊ฑฐ\n",
"[PASS] ์ด๋ชจ์งยทํน์๋ฌธ์ ์ ๊ฑฐ\n",
"[PASS] ์ ๊ฐ ๋ฌธ์ ์ ๊ทํ(NFKC)\n",
"[PASS] pHash ์ฌ์์ถ ๊ฐ๊ฑด์ฑ(๊ฑฐ๋ฆฌ 8 ์ดํ)\n",
"[PASS] pHash ๋ค๋ฅธ ์ฌ์ง ๊ตฌ๋ถ(๊ฑฐ๋ฆฌ 16 ์ด์)\n",
"๋จ์ ํ
์คํธ 5/5 ํต๊ณผ\n",
"\n",
"[์ ์ ์ ํ ์์]\n",
" ์ : [ํ์ง] A-4์์ ๋ณด๊ธ ํธ๋ญ ๋ฐ๊ฒฌ - ์๋ 9, ์ด๋ ๋ฐฉํฅ ์์ชฝ https://example.invalid/179440\n",
" ํ: ํ์ง A-4์์ ๋ณด๊ธ ํธ๋ญ - ์๋ 9 ์ด๋ ๋ฐฉํฅ ์์ชฝ\n",
" ์ : @ch_virtual [ํ์ง] A-4์์ ๋ณด๊ธ ํธ๋ญ ๋ฐ๊ฒฌ - ์๋ 9, ์ด๋ ๋ฐฉํฅ ๋์ชฝ \n",
" ํ: ํ์ง A-4์์ ๋ณด๊ธ ํธ๋ญ - ์๋ 9 ์ด๋ ๋ฐฉํฅ ๋์ชฝ\n",
" ์ : ๏ผก-2 ์ฃผ๋ฏผ ์ ๋ณด: ๋ฒ์ค 8๋๊ฐ ์์ชฝ์ผ๋ก ํฅํ๋ ๋ชจ์ต\n",
" ํ: A-2 ๋ฒ์ค 8๋๊ฐ ์์ชฝ์ผ๋ก ํฅํ๋\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",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ ๋ณธ๋ฌธ์ ๋ฌธ์ n-gram TF-IDF ์ฝ์ฌ์ธ ์ ์ฌ๋์ pHash ๊ฑฐ๋ฆฌ๋ก ๋ณต์ ยท์ฌ์์ฑยท๋ฌด๊ด ๊ด๊ณ๋ฅผ ๊ท์น ํ์ ํ๊ณ , ์ฌ์์ฑ ํ๋จ ๊ธฐ์ค์ ๋ฐ๊ฟ ๊ฐ๋ฉฐ ์ ํ๋๋ฅผ 1์ฐจ ์กฐ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/isr02_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": [
"\"\"\"๊ฒ์๋ฌผ ์์ ๋ณธ๋ฌธ ์ ์ฌ๋(๋ฌธ์ n-gram TF-IDF ์ฝ์ฌ์ธ)์ ์ด๋ฏธ์ง ์ง๊ฐ ํด์ ๊ฑฐ๋ฆฌ๋ก ๋จ์ ๋ณต์ ยท์ฌ์์ฑ(์ธ์ฉ)ยท๋ฌด๊ด ๊ด๊ณ๋ฅผ ํ๋ณํ๊ณ , ๊ฐ์ ์ด์ผ๊ธฐ๋ก ๋ฌถ์ธ ๊ฒ์๋ฌผ ์ค ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์ 1์ฐจ ์ถ์ฒ๋ก ๋ถ๋ฅํ๋ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํด ์ ํ๋์ ์ฒ๋ฆฌ ์ง์ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"\n",
"def relation(sim, dist, R):\n",
" if sim >= R[\"copy_sim\"] and (dist is None or dist <= R[\"copy_ham\"]):\n",
" return \"๋ณต์ \"\n",
" if sim >= R[\"quote_sim\"] or (dist is not None and dist <= R[\"quote_ham\"]):\n",
" return \"์ฌ์์ฑ\"\n",
" return \"๋ฌด๊ด\"\n",
"\n",
"\n",
"R0 = {\"copy_sim\": 0.95, \"copy_ham\": 6, \"quote_sim\": 0.45, \"quote_ham\": 10}\n",
"posts, acc = make_posts(60, seed=11)\n",
"true_posts = [p for p in posts if not p[\"is_disinfo\"]]\n",
"idx = {p[\"post_id\"]: i for i, p in enumerate(posts)}\n",
"X = TfidfVectorizer(analyzer=\"char_wb\", ngram_range=(2, 3)).fit_transform([clean_text(p[\"text\"]) for p in posts])\n",
"H = [phash(p[\"image\"]) if p[\"image\"] is not None else None for p in posts]\n",
"dist = lambda i, j: hamming(H[i], H[j]) if H[i] is not None and H[j] is not None else None\n",
"\n",
"# 1) ํ๊ฐ์ฉ ๊ฒ์๋ฌผ ์: ๋ณต์ (์ ๋ฌ), ์ฌ์์ฑ, ๋ฌด๊ด(๋ค๋ฅธ ์ฌ๊ฑด)\n",
"rng = np.random.default_rng(0)\n",
"pairs = [(idx[p[\"post_id\"]], idx[p[\"origin\"]], {\"copy\": \"๋ณต์ \", \"near_dup\": \"์ฌ์์ฑ\"}[p[\"kind\"]]) for p in true_posts if p[\"kind\"] in (\"copy\", \"near_dup\")]\n",
"while sum(1 for *_, l in pairs if l == \"๋ฌด๊ด\") < 150:\n",
" i, j = rng.integers(0, len(posts), 2)\n",
" if posts[i][\"event_id\"] != posts[j][\"event_id\"] and not posts[i][\"is_disinfo\"] and not posts[j][\"is_disinfo\"]:\n",
" pairs.append((int(i), int(j), \"๋ฌด๊ด\"))\n",
"t0 = time.perf_counter()\n",
"sims = [(X[i] @ X[j].T).toarray()[0, 0] for i, j, _ in pairs]\n",
"pred = [relation(s, dist(i, j), R0) for s, (i, j, _) in zip(sims, pairs)]\n",
"lat = (time.perf_counter() - t0) / len(pairs) * 1000\n",
"truth = [l for *_, l in pairs]\n",
"acc0 = np.mean([a == b for a, b in zip(pred, truth)])\n",
"print(f\"๊ฒ์๋ฌผ ์ {len(pairs)}๊ฐ ๊ด๊ณ ํ๋ณ ์ ํ๋ {acc0:.1%} (๋ชฉํ 75% ์ด์: {'๋ฌ์ฑ' if acc0 >= 0.75 else '๋ฏธ๋ฌ'}), ์๋น {lat:.2f} ms\")\n",
"print(pd.crosstab(pd.Series(truth, name=\"์ค์ \"), pd.Series(pred, name=\"ํ๋ณ\")).to_string())\n",
"\n",
"# 2) ํด๋ฆฌ์คํฑ ๊ฐ์ค์น 1์ฐจ ํ๋: ์ฌ์์ฑ ํ๋จ ์ ์ฌ๋ ๊ธฐ์ค\n",
"print(\"\\n์ฌ์์ฑ ๊ธฐ์ค(์ ์ฌ๋) ์ ํ๋\")\n",
"for q in [0.3, 0.45, 0.6, 0.75]:\n",
" pr = [relation(s, dist(i, j), {**R0, \"quote_sim\": q}) for s, (i, j, _) in zip(sims, pairs)]\n",
" print(f\" {q:.2f} {np.mean([a == b for a, b in zip(pr, truth)]):.1%}\")\n",
"\n",
"# 3) 1์ฐจ ์ถ์ฒ ๋ถ๋ฅ: ์ ๋ฌ ๊ฒฝ๋ก(forwarded_from)๋ฅผ ๊ฑฐ์ฌ๋ฌ ์ฌ๋ผ๊ฐ๊ณ , ๊ฒฝ๋ก๊ฐ ์์ผ๋ฉด ๊ฐ์ ์ด์ผ๊ธฐ ์ค ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์๋ฌผ์ ์ถ์ฒ๋ก ๋ณธ๋ค\n",
"order = sorted(range(len(true_posts)), key=lambda k: true_posts[k][\"posted_at\"])\n",
"tp = [idx[p[\"post_id\"]] for p in true_posts]\n",
"src, label = {}, {}\n",
"for k in order:\n",
" i = tp[k]; p = posts[i]\n",
" if p[\"forwarded_from\"]:\n",
" j = idx[p[\"forwarded_from\"]]\n",
" while posts[j][\"forwarded_from\"]:\n",
" j = idx[posts[j][\"forwarded_from\"]]\n",
" src[i], label[i] = j, \"์ ๋ฌ\"\n",
" continue\n",
" cand = [j for j in src if src[j] == j and relation((X[i] @ X[j].T).toarray()[0, 0], dist(i, j), R0) != \"๋ฌด๊ด\"]\n",
" if cand:\n",
" src[i], label[i] = src[min(cand, key=lambda j: posts[j][\"posted_at\"])], \"์ฌ์์ฑ\"\n",
" else:\n",
" src[i], label[i] = i, \"1์ฐจ ์ถ์ฒ\"\n",
"true_lab = {\"original\": \"1์ฐจ ์ถ์ฒ\", \"copy\": \"์ ๋ฌ\", \"near_dup\": \"์ฌ์์ฑ\"}\n",
"ok_lab = np.mean([label[i] == true_lab[posts[i][\"kind\"]] for i in tp])\n",
"ok_src = np.mean([posts[src[i]][\"post_id\"] == (posts[i][\"origin\"] or posts[i][\"post_id\"]) for i in tp])\n",
"print(f\"\\n1์ฐจ ์ถ์ฒ ๋ถ๋ฅ(์ฌ์ค ๊ฒ์๋ฌผ {len(tp)}๊ฑด): ์ ํ ์ ํ๋ {ok_lab:.1%}, ์ถ์ฒ ๊ฒ์๋ฌผ ์ผ์น์จ {ok_src:.1%}\")\n",
"print(\"์ถ์ฒ ์ผ์น์จ์ด ๋ฎ์ ์ด์ : ์ ๋ฌ ๊ฒฝ๋ก๊ฐ ์๋ ์ฌ์์ฑ ๊ธ์ ๊ฐ์ ๋ฌธํ์ ์ด ๋ค๋ฅธ ์ฌ๊ฑด ๊ธ๊ณผ ํผ๋๋๋ค(๋ณธ๋ฌธ ์ ์ฌ๋๋ง ์ฌ์ฉ).\")\n",
"print(\"ํ๊ณ: ์กฐ์ ๊ฒ์๋ฌผ์ ์๋ณธ๊ณผ ํํ์ด ๋น์ทํด '์ฌ์์ฑ'์ผ๋ก ๋ฌถ์ด๋ฉฐ, ๋ด์ฉ ๋ณ์กฐ ์ฌ๋ถ๋ ํ๋จํ์ง ๋ชปํ๋ค(๋ฏธ์
4์ ๋๊ธฐ).\")\n",
"\n",
"plt.figure(figsize=(7, 4))\n",
"for lab, c in [(\"๋ณต์ \", \"tab:red\"), (\"์ฌ์์ฑ\", \"tab:orange\"), (\"๋ฌด๊ด\", \"tab:gray\")]:\n",
" sel = [k for k, t in enumerate(truth) if t == lab]\n",
" plt.scatter([sims[k] for k in sel], [dist(*pairs[k][:2]) if dist(*pairs[k][:2]) is not None else -4 for k in sel], s=12, c=c,\n",
" label={\"๋ณต์ \": \"copy\", \"์ฌ์์ฑ\": \"rewrite\", \"๋ฌด๊ด\": \"unrelated\"}[lab])\n",
"plt.axvline(R0[\"quote_sim\"], ls=\"--\", c=\"k\"); plt.axhline(R0[\"quote_ham\"], ls=\":\", c=\"k\")\n",
"plt.xlabel(\"text cosine similarity\"); plt.ylabel(\"pHash distance (-4 = no image)\"); plt.legend(); plt.title(\"Pair relation baseline (synthetic posts)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒ์๋ฌผ ์ 421๊ฐ ๊ด๊ณ ํ๋ณ ์ ํ๋ 98.3% (๋ชฉํ 75% ์ด์: ๋ฌ์ฑ), ์๋น 0.22 ms\n",
"ํ๋ณ ๋ฌด๊ด ๋ณต์ ์ฌ์์ฑ\n",
"์ค์ \n",
"๋ฌด๊ด 143 0 7\n",
"๋ณต์ 0 178 0\n",
"์ฌ์์ฑ 0 0 93\n",
"\n",
"์ฌ์์ฑ ๊ธฐ์ค(์ ์ฌ๋) ์ ํ๋\n",
" 0.30 93.3%\n",
" 0.45 98.3%\n",
" 0.60 97.9%\n",
" 0.75 96.9%\n",
"\n",
"1์ฐจ ์ถ์ฒ ๋ถ๋ฅ(์ฌ์ค ๊ฒ์๋ฌผ 331๊ฑด): ์ ํ ์ ํ๋ 85.5%, ์ถ์ฒ ๊ฒ์๋ฌผ ์ผ์น์จ 64.0%\n",
"์ถ์ฒ ์ผ์น์จ์ด ๋ฎ์ ์ด์ : ์ ๋ฌ ๊ฒฝ๋ก๊ฐ ์๋ ์ฌ์์ฑ ๊ธ์ ๊ฐ์ ๋ฌธํ์ ์ด ๋ค๋ฅธ ์ฌ๊ฑด ๊ธ๊ณผ ํผ๋๋๋ค(๋ณธ๋ฌธ ์ ์ฌ๋๋ง ์ฌ์ฉ).\n",
"ํ๊ณ: ์กฐ์ ๊ฒ์๋ฌผ์ ์๋ณธ๊ณผ ํํ์ด ๋น์ทํด '์ฌ์์ฑ'์ผ๋ก ๋ฌถ์ด๋ฉฐ, ๋ด์ฉ ๋ณ์กฐ ์ฌ๋ถ๋ ํ๋จํ์ง ๋ชปํ๋ค(๋ฏธ์
4์ ๋๊ธฐ).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์์ฑํ AI ์กฐ์ ์ด๋ฏธ์ง/ํ
์คํธ ํ์ง ๋ฐ ์ถ์ฒ ๊ณ์ ์ ๋ขฐ๋ ํ๊ฐ ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ฑํ AI ์กฐ์ ํ
์คํธ/์ด๋ฏธ์ง ํ์ง ์๊ณ ๋ฆฌ์ฆ๊ณผ ์์ฑ์ ๊ณ์ ์ ๋ขฐ๋ ํ๊ฐ ๋ชจ๋ธ์ ๊ฒฐํฉํ์ฌ ๊ฐ์ง ๋ด์ค ํ๋ณ ๋ก์ง์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ฑํ AI ์กฐ์ ํ
์คํธยท์ด๋ฏธ์ง ํ์ง์ ์์ฑ ๊ณ์ ์ ๋ขฐ๋ ํ๊ฐ๋ฅผ ๊ฒฐํฉํด ๊ฐ์ง ์ ๋ณด ํ๋ณ ๋ก์ง์ ๊ณ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ์ด์ผ๊ธฐ๋ก ๋ฌถ์ ๊ฒ์๋ฌผ ์ฌ์ด์ ์๋ยท์ง์ญยท๋ฐฉํฅ ๋ถ์ผ์น, ์ด๋ฏธ์ง ํฉ์ฑ ์ก์ ๋ถ๊ท ์ผยท์์ฑํ ๊ฒฉ์ ๋ฌด๋ฌยท๊ณผ๊ฑฐ ์ด๋ฏธ์ง ์ฌ์ฌ์ฉ ํ์ , ์ ๋ํ ๋ฐ๋ณต ๋ฌธ์ฒด, ์กฐ์ง์ ๋์ ๊ฒ์ ํน์ง ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/isr02_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": [
"\"\"\"๋ณธ๋ฌธ ๋ด์ฉ ๋น๊ต(์๋ยท์ง์ญยท๋ฐฉํฅ ๋ถ์ผ์น), ์ด๋ฏธ์ง ์กฐ์ ํ์ (ํฉ์ฑ ์ก์ ๋ถ๊ท ์ผ, ์์ฑํ ๊ฒฉ์ ๋ฌด๋ฌ, ๊ณผ๊ฑฐ ์ด๋ฏธ์ง ์ฌ์ฌ์ฉ), ์์ฑํยท์ ๋ ๋ฌธ์ฒด, ์กฐ์ง์ ๋์ ๊ฒ์ ํน์ง๊ณผ ์์ฑ ๊ณ์ ์ ๋ขฐ๋ ๋ชจ๋ธ์ ๊ฒฐํฉํด 10์ข
์ ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ์ ํ๋ณํ๋ ๋ชจ๋ธ๋ก ๊ณ ๋ํํ๊ณ ์ฌํ์จยท์ค๊ฒฝ๋ณดยท์ฒ๋ฆฌ ์ง์ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"# ---- ๊ฒ์๋ฌผ ๊ตฐ์ง(๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ)ยท์กฐ์ ํ์ง ํน์ง ๊ณตํต ํจ์ ----\n",
"FEAT = [\"num_dev\", \"region_mis\", \"dir_mis\", \"obj_mis\", \"img_ref_dist\", \"img_prior_h\", \"time_gap_h\", \"splice\", \"ai_img\",\n",
" \"ai_text\", \"burst_n\", \"has_img\", \"is_fwd\", \"acc_age\", \"acc_followers\", \"acc_orig\", \"acc_past\", \"acc_cv\"]\n",
"\n",
"\n",
"def cluster_posts(posts, hours=24, ham_thr=8):\n",
" \"\"\"๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ: ๋ณธ๋ฌธ์์ ๋ฝ์ (์ง์ญ, ๋์)์ด ๊ฐ๊ณ 24์๊ฐ ์์ด๋ฉด ์ฐ๊ฒฐํ๊ณ ,\n",
" ์ง๊ฐ ํด์๊ฐ ๊ฐ๊น์ด ์ด๋ฏธ์ง(๊ฐ์ ๋์)๋ ์ฐ๊ฒฐํ๋ค. ๊ธ ํํ์ด ๋ฌ๋ผ๋ ๋ด์ฉ ๋จ์๋ก ๋ฌถ์ธ๋ค.\"\"\"\n",
" texts = [clean_text(p[\"text\"]) for p in posts]\n",
" X = TfidfVectorizer(analyzer=\"char_wb\", ngram_range=(2, 3)).fit_transform(texts)\n",
" S = (X @ X.T).toarray()\n",
" H = [phash(p[\"image\"]) if p[\"image\"] is not None else None for p in posts]\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" t = [p[\"posted_at\"] for p in posts]\n",
" parent = list(range(len(posts)))\n",
"\n",
" def find(i):\n",
" while parent[i] != i:\n",
" parent[i] = parent[parent[i]]; i = parent[i]\n",
" return i\n",
" for i in range(len(posts)):\n",
" for j in range(i + 1, len(posts)):\n",
" if abs(t[i] - t[j]) >= hours or sl[i][\"o\"] != sl[j][\"o\"]:\n",
" continue\n",
" same_story = sl[i][\"r\"] is not None and sl[i][\"r\"] == sl[j][\"r\"]\n",
" same_img = H[i] is not None and H[j] is not None and hamming(H[i], H[j]) <= ham_thr\n",
" if same_story or same_img:\n",
" parent[find(i)] = find(j)\n",
" return np.array([find(i) for i in range(len(posts))]), S, H\n",
"\n",
"\n",
"def post_features(posts, acc):\n",
" cid, S, H = cluster_posts(posts)\n",
" t = np.array([p[\"posted_at\"] for p in posts])\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" first_seen = {}\n",
" for i in np.argsort(t): # ์ด๋ฏธ์ง๊ฐ ์ฒ์ ๋ฑ์ฅํ ์๊ฐ๊ณผ ๊ตฐ์ง\n",
" if H[i] is None:\n",
" continue\n",
" key = next((k for k in first_seen if hamming(np.array(k), H[i]) <= 6), None)\n",
" if key is None:\n",
" first_seen[tuple(H[i])] = (t[i], cid[i])\n",
" elif first_seen[key][1] != cid[i]:\n",
" pass\n",
" ref = {c: int(np.argmin(np.where(cid == c, t, np.inf))) for c in set(cid)} # ๊ตฐ์ง์์ ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์๋ฌผ = ๊ธฐ์ค(์ถ์ ์๋ณธ)\n",
" F = []\n",
" for i, p in enumerate(posts):\n",
" r = ref[cid[i]]\n",
" a, b = sl[i], sl[r]\n",
" img_prior = 0.0\n",
" if H[i] is not None:\n",
" for k, (ts, c) in first_seen.items():\n",
" if hamming(np.array(k), H[i]) <= 6 and c != cid[i]:\n",
" img_prior = max(img_prior, min(t[i] - ts, 48.0))\n",
" same_txt = [j for j in np.where(S[i] > 0.97)[0] if abs(t[j] - t[i]) < 0.25 and posts[j][\"account_id\"] != p[\"account_id\"]]\n",
" A = acc[p[\"account_id\"]]\n",
" F.append([abs(np.log((a[\"n\"] or 1) / (b[\"n\"] or 1))), float(a[\"r\"] != b[\"r\"]), float(a[\"d\"] != b[\"d\"]), float(a[\"o\"] != b[\"o\"]),\n",
" hamming(H[i], H[r]) if H[i] is not None and H[r] is not None else 0, img_prior, t[i] - t[r],\n",
" splice_score(p[\"image\"]) if p[\"image\"] is not None else 0, ai_image_score(p[\"image\"]) if p[\"image\"] is not None else 0,\n",
" ai_text_score(p[\"text\"]), len(set(posts[j][\"account_id\"] for j in same_txt)), float(p[\"image\"] is not None),\n",
" float(p[\"forwarded_from\"] is not None), np.log1p(A[\"age_days\"]), np.log1p(A[\"followers\"]), A[\"original_ratio\"],\n",
" A[\"past_accuracy\"], A[\"interval_cv\"]])\n",
" return np.array(F), cid\n",
"\n",
"CONTENT, ACCOUNT = list(range(13)), list(range(13, 18))\n",
"\n",
"# 1) ํ์ต: ๊ฐ์ ๊ฒ์๋ฌผ(์ฌ๊ฑด 60๊ฐ) / ์ํ: ๋ค๋ฅธ ๋์์ ๊ฐ์ ๊ฒ์๋ฌผ(์ฌ๊ฑด 60๊ฐ)\n",
"tr_posts, tr_acc = make_posts(60, seed=7)\n",
"Ftr, _ = post_features(tr_posts, tr_acc)\n",
"ytr = np.array([p[\"is_disinfo\"] for p in tr_posts])\n",
"te_posts, te_acc = make_posts(60, seed=11)\n",
"t0 = time.perf_counter()\n",
"Fte, cid = post_features(te_posts, te_acc)\n",
"yte, kind = np.array([p[\"is_disinfo\"] for p in te_posts]), np.array([p[\"kind\"] for p in te_posts])\n",
"\n",
"# ๊ณ์ ์ ๋ขฐ๋ ๋ชจ๋ธ: ๊ณ์ ์์ฑ๋ง์ผ๋ก '์กฐ์ ๊ฒ์๋ฌผ์ ์ฌ๋ฆด ๊ฐ๋ฅ์ฑ'์ ์ถ์ (์์ฑ์ ํํ)\n",
"acc_model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)).fit(Ftr[:, ACCOUNT], ytr)\n",
"Xtr = np.c_[Ftr[:, CONTENT], acc_model.predict_proba(Ftr[:, ACCOUNT])[:, 1]]\n",
"model = RandomForestClassifier(n_estimators=300, min_samples_leaf=2, random_state=0).fit(Xtr, ytr)\n",
"Xte = np.c_[Fte[:, CONTENT], acc_model.predict_proba(Fte[:, ACCOUNT])[:, 1]]\n",
"pred = model.predict(Xte)\n",
"lat = (time.perf_counter() - t0) / len(te_posts) * 1000\n",
"\n",
"content_only = RandomForestClassifier(n_estimators=300, min_samples_leaf=2, random_state=0).fit(Ftr[:, CONTENT], ytr).predict(Fte[:, CONTENT])\n",
"account_only = acc_model.predict(Fte[:, ACCOUNT])\n",
"print(\"๋ฐฉ์ ์กฐ์ ์ฌํ์จ ์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด์จ\")\n",
"for name, p in [(\"๊ณ์ ์ ๋ขฐ๋๋ง\", account_only), (\"๋ด์ฉยท์ด๋ฏธ์ง ํ์ง๋ง\", content_only), (\"๊ฒฐํฉ ๋ชจ๋ธ\", pred)]:\n",
" print(f\"{name:12s} {(p & yte).sum() / yte.sum():10.1%} {(p & ~yte).sum() / (~yte).sum():14.1%}\")\n",
"rec = (pred & yte).sum() / yte.sum()\n",
"print(f\"\\n๋ณตํฉ ์ํ(์กฐ์ยท๊ธฐ๋ง) ํ์ง ์ฌํ์จ {rec:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if rec >= 0.85 else '๋ฏธ๋ฌ'}), ๊ฒ์๋ฌผ๋น ์ฒ๋ฆฌ {lat:.2f} ms (๋ชฉํ 200 ms ์ดํ)\")\n",
"\n",
"# 2) ์กฐ์ยท๊ธฐ๋ง ์๋๋ฆฌ์ค 10์ข
์ํ(์ฌํ์จ 80% ์ด์์ด๋ฉด ํต๊ณผ)\n",
"rows = [{\"์ ํ\": m, \"๊ฑด์\": int((kind == m).sum()), \"์ฌํ์จ\": round(float(pred[kind == m].mean()), 2),\n",
" \"ํ์ \": \"ํต๊ณผ\" if pred[kind == m].mean() >= 0.8 else \"๋ฏธํต๊ณผ\"} for m in MANIP]\n",
"t = pd.DataFrame(rows)\n",
"print(\"\\n\" + t.to_string(index=False))\n",
"print(f\"ํต๊ณผ {int((t['ํ์ '] == 'ํต๊ณผ').sum())}/10์ข
\")\n",
"\n",
"# 3) ์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: ์ฌ์ค ๊ฒ์๋ฌผ์ธ๋ฐ ์กฐ์์ผ๋ก ๋ณธ ์ฌ๋ก\n",
"fa = np.where(pred & ~yte)[0]\n",
"print(f\"\\n[์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด {len(fa)}๊ฑด] ์ ํ:\", pd.Series(kind[fa]).value_counts().to_dict(),\n",
" \"/ ์์ฑ ๊ณ์ ์ ํ:\", pd.Series([te_acc[te_posts[i][\"account_id\"]][\"type\"] for i in fa]).value_counts().to_dict())\n",
"imp = pd.Series(model.feature_importances_, index=[FEAT[i] for i in CONTENT] + [\"account_risk\"]).sort_values(ascending=False)\n",
"print(\"์ค์ ํน์ง ์์ 5:\", {k: round(v, 3) for k, v in imp.head(5).items()})\n",
"print(\"-> ์ ์ยท์๊ท๋ชจ ๊ณ์ ์ ์ฌ์ค ์ ๋ณด์ ๋ฆ๊ฒ ์ฌ๋ผ์จ ์ฌ์์ฑ ๊ธ์ด ์ค๊ฒฝ๋ณด๋ฅผ ๋ง๋ ๋ค. ๊ณ์ ํํ์ ๊ณผ๋ํ๊ฒ ๊ธฐ๋์ง ์๋๋ก ํน์ง ๊ณต๊ฐ์ ์ฌ์ค๊ณ(๋ฏธ์
5)ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].barh(t[\"์ ํ\"], t[\"์ฌํ์จ\"], color=[\"tab:green\" if v >= 0.8 else \"tab:red\" for v in t[\"์ฌํ์จ\"]]); ax[0].axvline(0.8, ls=\"--\", c=\"k\")\n",
"ax[0].set_title(\"Recall by manipulation type\"); ax[0].set_xlim(0, 1.05)\n",
"imp.head(10)[::-1].plot.barh(ax=ax[1]); ax[1].set_title(\"Feature importance\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๋ฐฉ์ ์กฐ์ ์ฌํ์จ ์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด์จ\n",
"๊ณ์ ์ ๋ขฐ๋๋ง 88.3% 5.7%\n",
"๋ด์ฉยท์ด๋ฏธ์ง ํ์ง๋ง 94.1% 14.5%\n",
"๊ฒฐํฉ ๋ชจ๋ธ 97.3% 3.6%\n",
"\n",
"๋ณตํฉ ์ํ(์กฐ์ยท๊ธฐ๋ง) ํ์ง ์ฌํ์จ 97.3% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), ๊ฒ์๋ฌผ๋น ์ฒ๋ฆฌ 3.14 ms (๋ชฉํ 200 ms ์ดํ)\n",
"\n",
" ์ ํ ๊ฑด์ ์ฌํ์จ ํ์ \n",
" num_change 13 1.00 ํต๊ณผ\n",
"region_swap 24 0.88 ํต๊ณผ\n",
" dir_flip 19 0.95 ํต๊ณผ\n",
" old_image 29 1.00 ํต๊ณผ\n",
" splice 14 0.93 ํต๊ณผ\n",
" ai_image 16 1.00 ํต๊ณผ\n",
" ai_text 13 1.00 ํต๊ณผ\n",
" bot_burst 96 1.00 ํต๊ณผ\n",
" mashup 12 1.00 ํต๊ณผ\n",
" recycled 20 0.90 ํต๊ณผ\n",
"ํต๊ณผ 10/10์ข
\n",
"\n",
"[์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด 12๊ฑด] ์ ํ: {'near_dup': 9, 'original': 3} / ์์ฑ ๊ณ์ ์ ํ: {'witness': 11, 'propaganda': 1}\n",
"์ค์ ํน์ง ์์ 5: {'account_risk': 0.394, 'time_gap_h': 0.162, 'num_dev': 0.107, 'is_fwd': 0.102, 'img_ref_dist': 0.054}\n",
"-> ์ ์ยท์๊ท๋ชจ ๊ณ์ ์ ์ฌ์ค ์ ๋ณด์ ๋ฆ๊ฒ ์ฌ๋ผ์จ ์ฌ์์ฑ ๊ธ์ด ์ค๊ฒฝ๋ณด๋ฅผ ๋ง๋ ๋ค. ๊ณ์ ํํ์ ๊ณผ๋ํ๊ฒ ๊ธฐ๋์ง ์๋๋ก ํน์ง ๊ณต๊ฐ์ ์ฌ์ค๊ณ(๋ฏธ์
5)ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ๊ฐ์ง ์ ๋ณด(Disinformation) ์ค๋ถ๋ฅ ๋ฐฉ์ง ๋ฐ ๋ณตํฉ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ์ฌ๋ฆฌ์ (์ฌ๋ฆฌ ๊ณต์) ๊ฒ์๋ฌผ์ ์ ํฌ ํจํด์ ๋ถ์ํ๊ณ ์ ๋ขฐ๋ ๋ฑ๊ธ(A~E) ๋ฐ ์ข
ํฉ ์ ๋ขฐ๋ ์ ์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฌ๋ฆฌ์ ๊ฒ์๋ฌผ์ ์ ํฌ ํจํด์ ๋ฐ์ํด ์ ๋ขฐ๋ ๋ฑ๊ธ(A~E)๊ณผ ์ข
ํฉ ์ ๋ขฐ๋ ์ง์๋ฅผ ์ฐ์ถํ๊ณ , ์ฌ์ค ๊ฒ์๋ฌผ์ ๊ฐ์ง๋ก ์ค๋ถ๋ฅํ์ง ์๋๋ก ํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ด์ฉ ๋ฌด๊ฒฐ์ฑ, ๊ณ์ ํํ, ๋
๋ฆฝ ์ถ์ฒ ๊ต์ฐจํ์ธ, ์ ํฌ ํจํด ์ ์ ์ฌ๋ถ, ์์์ฑ 5๊ฐ ์์๋ฅผ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค๋ก ํฉ์ฐํด ์ข
ํฉ ์ง์๋ฅผ ๋ง๋ค๊ณ A~E ๋ฑ๊ธ(AยทB=High, C=Medium, DยทE=Low)์ ๋ถ์ฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/isr02_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": [
"\"\"\"์ฌ๋ฆฌ์ ๊ฒ์๋ฌผ์ ์ ํฌ ํจํด(์กฐ์ง์ ๋์ ๊ฒ์), ๋ด์ฉ ์กฐ์ ํ๋ฅ , ์์ฑ ๊ณ์ ํํ, ๋
๋ฆฝ ์ถ์ฒ ๊ต์ฐจ ํ์ธ, ์์์ฑ์ ๊ฐ์ค ํฉ์ฐํด ์ข
ํฉ ์ ๋ขฐ๋ ์ง์์ ์ ๋ขฐ๋ ๋ฑ๊ธ(A~E)์ ์ฐ์ถํ๊ณ , ๊ณ์ ์์ฑ๋ง์ผ๋ก ๊ฐ์ง๋ฅผ ๋จ์ ํ๋ ๊ท์น ๋๋น ์ฌ์ค ๊ฒ์๋ฌผ ์ค๋ถ๋ฅ(์ค๊ฒฝ๋ณด) ๊ฐ์ถ๊ณผ ๋ฑ๊ธ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import roc_auc_score\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"# ---- ๊ฒ์๋ฌผ ๊ตฐ์ง(๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ)ยท์กฐ์ ํ์ง ํน์ง ๊ณตํต ํจ์ ----\n",
"FEAT = [\"num_dev\", \"region_mis\", \"dir_mis\", \"obj_mis\", \"img_ref_dist\", \"img_prior_h\", \"time_gap_h\", \"splice\", \"ai_img\",\n",
" \"ai_text\", \"burst_n\", \"has_img\", \"is_fwd\", \"acc_age\", \"acc_followers\", \"acc_orig\", \"acc_past\", \"acc_cv\"]\n",
"\n",
"\n",
"def cluster_posts(posts, hours=24, ham_thr=8):\n",
" \"\"\"๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ: ๋ณธ๋ฌธ์์ ๋ฝ์ (์ง์ญ, ๋์)์ด ๊ฐ๊ณ 24์๊ฐ ์์ด๋ฉด ์ฐ๊ฒฐํ๊ณ ,\n",
" ์ง๊ฐ ํด์๊ฐ ๊ฐ๊น์ด ์ด๋ฏธ์ง(๊ฐ์ ๋์)๋ ์ฐ๊ฒฐํ๋ค. ๊ธ ํํ์ด ๋ฌ๋ผ๋ ๋ด์ฉ ๋จ์๋ก ๋ฌถ์ธ๋ค.\"\"\"\n",
" texts = [clean_text(p[\"text\"]) for p in posts]\n",
" X = TfidfVectorizer(analyzer=\"char_wb\", ngram_range=(2, 3)).fit_transform(texts)\n",
" S = (X @ X.T).toarray()\n",
" H = [phash(p[\"image\"]) if p[\"image\"] is not None else None for p in posts]\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" t = [p[\"posted_at\"] for p in posts]\n",
" parent = list(range(len(posts)))\n",
"\n",
" def find(i):\n",
" while parent[i] != i:\n",
" parent[i] = parent[parent[i]]; i = parent[i]\n",
" return i\n",
" for i in range(len(posts)):\n",
" for j in range(i + 1, len(posts)):\n",
" if abs(t[i] - t[j]) >= hours or sl[i][\"o\"] != sl[j][\"o\"]:\n",
" continue\n",
" same_story = sl[i][\"r\"] is not None and sl[i][\"r\"] == sl[j][\"r\"]\n",
" same_img = H[i] is not None and H[j] is not None and hamming(H[i], H[j]) <= ham_thr\n",
" if same_story or same_img:\n",
" parent[find(i)] = find(j)\n",
" return np.array([find(i) for i in range(len(posts))]), S, H\n",
"\n",
"\n",
"def post_features(posts, acc):\n",
" cid, S, H = cluster_posts(posts)\n",
" t = np.array([p[\"posted_at\"] for p in posts])\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" first_seen = {}\n",
" for i in np.argsort(t): # ์ด๋ฏธ์ง๊ฐ ์ฒ์ ๋ฑ์ฅํ ์๊ฐ๊ณผ ๊ตฐ์ง\n",
" if H[i] is None:\n",
" continue\n",
" key = next((k for k in first_seen if hamming(np.array(k), H[i]) <= 6), None)\n",
" if key is None:\n",
" first_seen[tuple(H[i])] = (t[i], cid[i])\n",
" elif first_seen[key][1] != cid[i]:\n",
" pass\n",
" ref = {c: int(np.argmin(np.where(cid == c, t, np.inf))) for c in set(cid)} # ๊ตฐ์ง์์ ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์๋ฌผ = ๊ธฐ์ค(์ถ์ ์๋ณธ)\n",
" F = []\n",
" for i, p in enumerate(posts):\n",
" r = ref[cid[i]]\n",
" a, b = sl[i], sl[r]\n",
" img_prior = 0.0\n",
" if H[i] is not None:\n",
" for k, (ts, c) in first_seen.items():\n",
" if hamming(np.array(k), H[i]) <= 6 and c != cid[i]:\n",
" img_prior = max(img_prior, min(t[i] - ts, 48.0))\n",
" same_txt = [j for j in np.where(S[i] > 0.97)[0] if abs(t[j] - t[i]) < 0.25 and posts[j][\"account_id\"] != p[\"account_id\"]]\n",
" A = acc[p[\"account_id\"]]\n",
" F.append([abs(np.log((a[\"n\"] or 1) / (b[\"n\"] or 1))), float(a[\"r\"] != b[\"r\"]), float(a[\"d\"] != b[\"d\"]), float(a[\"o\"] != b[\"o\"]),\n",
" hamming(H[i], H[r]) if H[i] is not None and H[r] is not None else 0, img_prior, t[i] - t[r],\n",
" splice_score(p[\"image\"]) if p[\"image\"] is not None else 0, ai_image_score(p[\"image\"]) if p[\"image\"] is not None else 0,\n",
" ai_text_score(p[\"text\"]), len(set(posts[j][\"account_id\"] for j in same_txt)), float(p[\"image\"] is not None),\n",
" float(p[\"forwarded_from\"] is not None), np.log1p(A[\"age_days\"]), np.log1p(A[\"followers\"]), A[\"original_ratio\"],\n",
" A[\"past_accuracy\"], A[\"interval_cv\"]])\n",
" return np.array(F), cid\n",
"\n",
"CONTENT, ACCOUNT = list(range(13)), list(range(13, 18))\n",
"WEIGHTS = {\"๋ด์ฉ ๋ฌด๊ฒฐ์ฑ\": 0.35, \"๊ณ์ ํํ\": 0.20, \"๋
๋ฆฝ ์ถ์ฒ ๊ต์ฐจํ์ธ\": 0.25, \"์ ํฌ ํจํด ์ ์\": 0.10, \"์์์ฑ\": 0.10} # ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค\n",
"GRADES = [(0.80, \"A\"), (0.65, \"B\"), (0.50, \"C\"), (0.35, \"D\"), (-1, \"E\")]\n",
"\n",
"\n",
"def components(posts, acc, F, cid, manip_model, acc_model):\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" corro = []\n",
" for i, p in enumerate(posts):\n",
" same = {posts[j][\"account_id\"] for j in np.where(cid == cid[i])[0] if j != i and posts[j][\"forwarded_from\"] is None\n",
" and posts[j][\"account_id\"] != p[\"account_id\"] and (sl[j][\"r\"], sl[j][\"n\"], sl[j][\"d\"]) == (sl[i][\"r\"], sl[i][\"n\"], sl[i][\"d\"])}\n",
" corro.append(min(len(same) / 2, 1.0))\n",
" return np.c_[1 - manip_model.predict_proba(F[:, CONTENT])[:, 1], 1 - acc_model.predict_proba(F[:, ACCOUNT])[:, 1],\n",
" corro, 1 - np.minimum(F[:, FEAT.index(\"burst_n\")] / 3, 1), np.exp(-np.maximum(F[:, FEAT.index(\"time_gap_h\")], 0) / 12)]\n",
"\n",
"\n",
"def grade(score):\n",
" return np.array([next(g for t, g in GRADES if s >= t) for s in score])\n",
"\n",
"\n",
"tr_posts, tr_acc = make_posts(60, seed=7)\n",
"Ftr, _ = post_features(tr_posts, tr_acc)\n",
"ytr = np.array([p[\"is_disinfo\"] for p in tr_posts])\n",
"manip_model = RandomForestClassifier(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Ftr[:, CONTENT], ytr)\n",
"acc_model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)).fit(Ftr[:, ACCOUNT], ytr)\n",
"\n",
"te_posts, te_acc = make_posts(60, seed=11)\n",
"Fte, cid = post_features(te_posts, te_acc)\n",
"y = np.array([p[\"is_disinfo\"] for p in te_posts])\n",
"C = components(te_posts, te_acc, Fte, cid, manip_model, acc_model)\n",
"w = np.array(list(WEIGHTS.values()))\n",
"score = C @ w\n",
"G = grade(score)\n",
"print(\"[์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค]\", WEIGHTS)\n",
"\n",
"# 1) ๊ธฐ์กด ๊ท์น(์ ์ ๊ณ์ ยท์๊ท๋ชจ ๊ณ์ ยท์ถ์ฒ ํ์ ์์์ด๋ฉด ๊ฐ์ง) vs ์์ง(DยทE ๋ฑ๊ธ๋ง 'ํ์ ์์ฌ')\n",
"A = [te_acc[p[\"account_id\"]] for p in te_posts]\n",
"base = np.array([(a[\"age_days\"] < 180 or a[\"followers\"] < 1000) and p[\"forwarded_from\"] is None for a, p in zip(A, te_posts)])\n",
"eng = np.isin(G, [\"D\", \"E\"])\n",
"far0, far1 = (base & ~y).sum() / base.sum(), (eng & ~y).sum() / eng.sum()\n",
"print(f\"\\n๊ธฐ์กด ๊ท์น: ํ์ ์ง๋ชฉ {base.sum()}๊ฑด ์ค ์ฌ์ค ๊ฒ์๋ฌผ {(base & ~y).sum()}๊ฑด (์ค๊ฒฝ๋ณด์จ {far0:.1%}), ์กฐ์ ์ฌํ์จ {(base & y).sum() / y.sum():.1%}\")\n",
"print(f\"์ ๋ขฐ๋ ์์ง: ํ์ ์์ฌ(DยทE) {eng.sum()}๊ฑด ์ค ์ฌ์ค ๊ฒ์๋ฌผ {(eng & ~y).sum()}๊ฑด (์ค๊ฒฝ๋ณด์จ {far1:.1%}), ์กฐ์ ์ฌํ์จ {(eng & y).sum() / y.sum():.1%}\")\n",
"cut = 1 - far1 / far0\n",
"print(f\"์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ {cut:.1%} (๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if cut >= 0.4 else '๋ฏธ๋ฌ'}), ROC-AUC(์ข
ํฉ ์ง์๋ก ์ฌ์ค ํ๋ณ) {roc_auc_score(~y, score):.3f}\")\n",
"\n",
"# 2) ๋ฑ๊ธ ๋ถํฌ์ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋\n",
"kind = np.array([p[\"kind\"] for p in te_posts])\n",
"tab = pd.crosstab(pd.Series(np.where(y, \"์กฐ์ยท๊ธฐ๋ง\", \"์ฌ์ค\"), name=\"์ค์ \"), pd.Series(G, name=\"๋ฑ๊ธ\"))\n",
"print(\"\\n\" + tab.to_string())\n",
"print(\"\\nํ์ ์์ฌ ๊ธฐ์ค(์ง์ ๋ฏธ๋ง) ๊ฑด์ ์ค๊ฒฝ๋ณด์จ ์กฐ์ ์ฌํ์จ\")\n",
"for t in [0.40, 0.45, 0.50, 0.55]:\n",
" f = score < t\n",
" print(f\" {t:.2f} {f.sum():6d} {(f & ~y).sum() / max(f.sum(), 1):7.1%} {(f & y).sum() / y.sum():8.1%}\")\n",
"\n",
"# 3) ์ผ๊ด์ฑ: ๊ณ์ ์์น(+-5%)ยท๊ฒ์ ์๊ฐ(+-3๋ถ) ์ธก์ ์ก์์ ๋ฃ์ด 20๋ฒ ๋ค์ ๊ณ์ฐํด๋ ๊ฐ์ ๋ฑ๊ธ์ด ๋์ค๋ ๋น์จ\n",
"rng = np.random.default_rng(0)\n",
"same = []\n",
"for _ in range(20):\n",
" Fn = Fte.copy()\n",
" Fn[:, ACCOUNT] *= 1 + rng.normal(0, 0.05, Fn[:, ACCOUNT].shape)\n",
" Fn[:, FEAT.index(\"time_gap_h\")] += rng.normal(0, 0.05, len(Fn))\n",
" same.append((grade(components(te_posts, te_acc, Fn, cid, manip_model, acc_model) @ w) == G).mean())\n",
"print(f\"\\n์ ๋ขฐ๋ ๋ฑ๊ธ ์ฐ์ถ ์ผ๊ด์ฑ {np.mean(same):.1%} (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if np.mean(same) >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"print(\"๋ฑ๊ธ ๋์: AยทB = High, C = Medium, DยทE = Low. ๋ฑ๊ธ์ '๊ฒํ ์ฐ์ ์์'์ด๋ฉฐ ์ฐจ๋จยท์ฑํ์ ๋ฏธ์
7์ ์ ๋ณด๊ด ๊ฒฐ์ ์ผ๋ก๋ง ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].hist([score[~y], score[y]], bins=20, range=(0, 1), label=[\"factual\", \"manipulated\"], stacked=True)\n",
"for t, g in GRADES[:-1]:\n",
" ax[0].axvline(t, ls=\":\", c=\"k\")\n",
"ax[0].set_xlabel(\"composite credibility index\"); ax[0].legend(); ax[0].set_title(\"Credibility index (dotted = grade cut-offs)\")\n",
"ex = [int(np.argmax(score)), int(np.argmin(np.abs(score - 0.5))), int(np.argmin(score))]\n",
"ax[1].barh([f\"{te_posts[i]['post_id']} ({G[i]})\" for i in ex], [0] * 3)\n",
"left = np.zeros(3)\n",
"for k, (nm, wt) in enumerate(WEIGHTS.items()):\n",
" v = C[ex, k] * wt\n",
" ax[1].barh([f\"{te_posts[i]['post_id']} ({G[i]})\" for i in ex], v, left=left, label=[\"content\", \"account\", \"corroboration\", \"propagation\", \"timeliness\"][k])\n",
" left += v\n",
"ax[1].legend(fontsize=8); ax[1].set_title(\"Score breakdown (uncertainty view)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค] {'๋ด์ฉ ๋ฌด๊ฒฐ์ฑ': 0.35, '๊ณ์ ํํ': 0.2, '๋
๋ฆฝ ์ถ์ฒ ๊ต์ฐจํ์ธ': 0.25, '์ ํฌ ํจํด ์ ์': 0.1, '์์์ฑ': 0.1}\n",
"\n",
"๊ธฐ์กด ๊ท์น: ํ์ ์ง๋ชฉ 290๊ฑด ์ค ์ฌ์ค ๊ฒ์๋ฌผ 58๊ฑด (์ค๊ฒฝ๋ณด์จ 20.0%), ์กฐ์ ์ฌํ์จ 90.6%\n",
"์ ๋ขฐ๋ ์์ง: ํ์ ์์ฌ(DยทE) 228๊ฑด ์ค ์ฌ์ค ๊ฒ์๋ฌผ 2๊ฑด (์ค๊ฒฝ๋ณด์จ 0.9%), ์กฐ์ ์ฌํ์จ 88.3%\n",
"์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ 95.6% (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ), ROC-AUC(์ข
ํฉ ์ง์๋ก ์ฌ์ค ํ๋ณ) 0.993\n",
"\n",
"๋ฑ๊ธ A B C D E\n",
"์ค์ \n",
"์ฌ์ค 257 50 22 2 0\n",
"์กฐ์ยท๊ธฐ๋ง 1 5 24 80 146\n",
"\n",
"ํ์ ์์ฌ ๊ธฐ์ค(์ง์ ๋ฏธ๋ง) ๊ฑด์ ์ค๊ฒฝ๋ณด์จ ์กฐ์ ์ฌํ์จ\n",
" 0.40 175 0.0% 68.4%\n",
" 0.45 215 0.5% 83.6%\n",
" 0.50 228 0.9% 88.3%\n",
" 0.55 244 1.6% 93.8%\n",
"\n",
"์ ๋ขฐ๋ ๋ฑ๊ธ ์ฐ์ถ ์ผ๊ด์ฑ 98.5% (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"๋ฑ๊ธ ๋์: AยทB = High, C = Medium, DยทE = Low. ๋ฑ๊ธ์ '๊ฒํ ์ฐ์ ์์'์ด๋ฉฐ ์ฐจ๋จยท์ฑํ์ ๋ฏธ์
7์ ์ ๋ณด๊ด ๊ฒฐ์ ์ผ๋ก๋ง ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. SNS ์ ์ ์ ๋ณด ํ์๋ผ์ธ ๋ฐ ์ถ์ฒ ์ ๋ขฐ๋ ๋คํธ์ํฌ ์๊ฐํ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"SNS ์ ๋ณด์ ์ ํ ๊ฒฝ๋ก, ์ถ์ฒ๋ณ ์ ๋ขฐ๋ ๋ถํฌ, ์ํ ํค์๋ ํธ๋ ๋๋ฅผ ์ค์๊ฐ ๊ฐ์ํํ๋ ์ ๋ณด์ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **SNS ์ ๋ณด์ ์ ์
์ถ์ด, ์ ํ ๊ฒฝ๋ก, ์ถ์ฒ๋ณ ์ ๋ขฐ๋ ๋ถํฌ, ์ํ ํค์๋ ์ถ์ธ๋ฅผ ํ๋์ ๋ณด๋ ์ ๋ณด์ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒ์๋ฌผ ์คํธ๋ฆผ์ 1์ด ์ฃผ๊ธฐ ๋ฌถ์์ผ๋ก ๋ฐ์ ์๊ฐ๋๋ณยท๋ฑ๊ธ๋ณ ์ง๊ณ์ ์ํ ํค์๋ ์ง๊ณ๋ฅผ ๊ฐฑ์ ํ๊ณ ์ฒ๋ฆฌ์๊ฐ์ ์ฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/isr02_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": [
"\"\"\"SNS ์ ์ ์ ๋ณด์ ์๊ฐ๋๋ณ ์ ์
๋(์ ๋ขฐ๋ ๋ฑ๊ธ๋ณ), ์ด์ผ๊ธฐ ๋จ์ ์ ํ ๊ฒฝ๋ก ๋คํธ์ํฌ, ๊ณ์ ์ ํ๋ณ ์ ๋ขฐ๋ ๋ถํฌ, ์ํ ํค์๋ ์ถ์ด๋ฅผ ํ ํ๋ฉด์ ๋ณด์ฌ ์ฃผ๋ ์ ๋ณด์ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , ์คํธ๋ฆผ ๊ฐฑ์ ์ฒ๋ฆฌ์๊ฐ๊ณผ ๋ ๋๋ง ์๊ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"# ---- ๊ฒ์๋ฌผ ๊ตฐ์ง(๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ)ยท์กฐ์ ํ์ง ํน์ง ๊ณตํต ํจ์ ----\n",
"FEAT = [\"num_dev\", \"region_mis\", \"dir_mis\", \"obj_mis\", \"img_ref_dist\", \"img_prior_h\", \"time_gap_h\", \"splice\", \"ai_img\",\n",
" \"ai_text\", \"burst_n\", \"has_img\", \"is_fwd\", \"acc_age\", \"acc_followers\", \"acc_orig\", \"acc_past\", \"acc_cv\"]\n",
"\n",
"\n",
"def cluster_posts(posts, hours=24, ham_thr=8):\n",
" \"\"\"๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ: ๋ณธ๋ฌธ์์ ๋ฝ์ (์ง์ญ, ๋์)์ด ๊ฐ๊ณ 24์๊ฐ ์์ด๋ฉด ์ฐ๊ฒฐํ๊ณ ,\n",
" ์ง๊ฐ ํด์๊ฐ ๊ฐ๊น์ด ์ด๋ฏธ์ง(๊ฐ์ ๋์)๋ ์ฐ๊ฒฐํ๋ค. ๊ธ ํํ์ด ๋ฌ๋ผ๋ ๋ด์ฉ ๋จ์๋ก ๋ฌถ์ธ๋ค.\"\"\"\n",
" texts = [clean_text(p[\"text\"]) for p in posts]\n",
" X = TfidfVectorizer(analyzer=\"char_wb\", ngram_range=(2, 3)).fit_transform(texts)\n",
" S = (X @ X.T).toarray()\n",
" H = [phash(p[\"image\"]) if p[\"image\"] is not None else None for p in posts]\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" t = [p[\"posted_at\"] for p in posts]\n",
" parent = list(range(len(posts)))\n",
"\n",
" def find(i):\n",
" while parent[i] != i:\n",
" parent[i] = parent[parent[i]]; i = parent[i]\n",
" return i\n",
" for i in range(len(posts)):\n",
" for j in range(i + 1, len(posts)):\n",
" if abs(t[i] - t[j]) >= hours or sl[i][\"o\"] != sl[j][\"o\"]:\n",
" continue\n",
" same_story = sl[i][\"r\"] is not None and sl[i][\"r\"] == sl[j][\"r\"]\n",
" same_img = H[i] is not None and H[j] is not None and hamming(H[i], H[j]) <= ham_thr\n",
" if same_story or same_img:\n",
" parent[find(i)] = find(j)\n",
" return np.array([find(i) for i in range(len(posts))]), S, H\n",
"\n",
"\n",
"def post_features(posts, acc):\n",
" cid, S, H = cluster_posts(posts)\n",
" t = np.array([p[\"posted_at\"] for p in posts])\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" first_seen = {}\n",
" for i in np.argsort(t): # ์ด๋ฏธ์ง๊ฐ ์ฒ์ ๋ฑ์ฅํ ์๊ฐ๊ณผ ๊ตฐ์ง\n",
" if H[i] is None:\n",
" continue\n",
" key = next((k for k in first_seen if hamming(np.array(k), H[i]) <= 6), None)\n",
" if key is None:\n",
" first_seen[tuple(H[i])] = (t[i], cid[i])\n",
" elif first_seen[key][1] != cid[i]:\n",
" pass\n",
" ref = {c: int(np.argmin(np.where(cid == c, t, np.inf))) for c in set(cid)} # ๊ตฐ์ง์์ ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์๋ฌผ = ๊ธฐ์ค(์ถ์ ์๋ณธ)\n",
" F = []\n",
" for i, p in enumerate(posts):\n",
" r = ref[cid[i]]\n",
" a, b = sl[i], sl[r]\n",
" img_prior = 0.0\n",
" if H[i] is not None:\n",
" for k, (ts, c) in first_seen.items():\n",
" if hamming(np.array(k), H[i]) <= 6 and c != cid[i]:\n",
" img_prior = max(img_prior, min(t[i] - ts, 48.0))\n",
" same_txt = [j for j in np.where(S[i] > 0.97)[0] if abs(t[j] - t[i]) < 0.25 and posts[j][\"account_id\"] != p[\"account_id\"]]\n",
" A = acc[p[\"account_id\"]]\n",
" F.append([abs(np.log((a[\"n\"] or 1) / (b[\"n\"] or 1))), float(a[\"r\"] != b[\"r\"]), float(a[\"d\"] != b[\"d\"]), float(a[\"o\"] != b[\"o\"]),\n",
" hamming(H[i], H[r]) if H[i] is not None and H[r] is not None else 0, img_prior, t[i] - t[r],\n",
" splice_score(p[\"image\"]) if p[\"image\"] is not None else 0, ai_image_score(p[\"image\"]) if p[\"image\"] is not None else 0,\n",
" ai_text_score(p[\"text\"]), len(set(posts[j][\"account_id\"] for j in same_txt)), float(p[\"image\"] is not None),\n",
" float(p[\"forwarded_from\"] is not None), np.log1p(A[\"age_days\"]), np.log1p(A[\"followers\"]), A[\"original_ratio\"],\n",
" A[\"past_accuracy\"], A[\"interval_cv\"]])\n",
" return np.array(F), cid\n",
"\n",
"RISK_WORDS = [\"์๋ณด\", \"๊ธด๊ธ\", \"ํ์ฐ\", \"๊ณต์ \", \"๋๊ท๋ชจ\"]\n",
"tr_posts, tr_acc = make_posts(60, seed=7)\n",
"Ftr, _ = post_features(tr_posts, tr_acc)\n",
"clf = RandomForestClassifier(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Ftr, [p[\"is_disinfo\"] for p in tr_posts])\n",
"\n",
"posts, acc = make_posts(60, seed=11)\n",
"F, cid = post_features(posts, acc)\n",
"cred = 1 - clf.predict_proba(F)[:, 1] # ์ ๋ขฐ๋(์ฌ์ค์ผ ๊ฐ๋ฅ์ฑ) 0~1\n",
"level = np.where(cred >= 0.65, \"High\", np.where(cred >= 0.35, \"Medium\", \"Low\"))\n",
"df = pd.DataFrame({\"id\": [p[\"post_id\"] for p in posts], \"t\": [p[\"posted_at\"] for p in posts], \"acc\": [p[\"account_id\"] for p in posts],\n",
" \"type\": [acc[p[\"account_id\"]][\"type\"] for p in posts], \"level\": level, \"cred\": cred, \"cid\": cid,\n",
" \"fwd\": [p[\"forwarded_from\"] for p in posts], \"text\": [p[\"text\"] for p in posts]}).sort_values(\"t\")\n",
"\n",
"# 1) ์คํธ๋ฆผ ๊ฐฑ์ ๋ชจ์: 1์ด๋ง๋ค ์ ๊ฒ์๋ฌผ ๋ฌถ์(์ฝ 20๊ฑด)์ ๋ฐ์ ์๊ฐ๋๋ณยท๋ฑ๊ธ๋ณ ์ง๊ณ์ ํค์๋ ์ง๊ณ๋ฅผ ๊ฐฑ์ \n",
"counts, kw, upd = {}, {w: {} for w in RISK_WORDS}, []\n",
"for k0 in range(0, len(df), 20):\n",
" chunk = df.iloc[k0:k0 + 20]\n",
" t0 = time.perf_counter()\n",
" for _, r in chunk.iterrows():\n",
" h = int(r[\"t\"] // 6) * 6\n",
" counts[(h, r[\"level\"])] = counts.get((h, r[\"level\"]), 0) + 1\n",
" for w in RISK_WORDS:\n",
" if w in r[\"text\"]:\n",
" kw[w][h] = kw[w].get(h, 0) + 1\n",
" upd.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"์คํธ๋ฆผ ๊ฐฑ์ {len(upd)}ํ(ํ๋น ์ฝ 20๊ฑด), ์ฒ๋ฆฌ ํ๊ท {np.mean(upd):.1f} ms, ์ต๋ {np.max(upd):.1f} ms (๋ชฉํ: 1์ด ์ฃผ๊ธฐ ์์์ 100 ms ์ด๋ด)\")\n",
"\n",
"# 2) ๊ฒฝ๋ณด ์์ฝ: ์ ๋ขฐ๋ Low์ด๋ฉด์ ์กฐ์ง์ ๋์ ๊ฒ์๊ฐ ์๋ ์ด์ผ๊ธฐ\n",
"burst = df.assign(b=F[df.index, FEAT.index(\"burst_n\")]).query(\"level == 'Low' and b >= 2\")\n",
"top = burst.groupby(\"cid\").size().sort_values(ascending=False)\n",
"print(f\"๊ฒ์๋ฌผ {len(df)}๊ฑด: High {int((df.level == 'High').sum())} / Medium {int((df.level == 'Medium').sum())} / Low {int((df.level == 'Low').sum())}\")\n",
"print(f\"[๊ธด๊ธ ๊ฐ์กฐ] ์กฐ์ง์ ๋์ ๊ฒ์๊ฐ ์๋ Low ๋ฑ๊ธ ์ด์ผ๊ธฐ {len(top)}๊ฐ, ์ต๋ค {int(top.iloc[0]) if len(top) else 0}๊ฑด\")\n",
"print(\"\\n[๊ณ์ ์ ํ๋ณ ํ๊ท ์ ๋ขฐ๋]\", df.groupby(\"type\")[\"cred\"].mean().round(2).to_dict())\n",
"\n",
"# 3) ๋์๋ณด๋(์ผ๊ฐ ์์ ์ฉ ์ด๋์ด ํ๋ฉด, ๊ตฌ์ฑ์์ 4์ข
)\n",
"plt.style.use(\"dark_background\")\n",
"fig, ax = plt.subplots(2, 2, figsize=(14, 10))\n",
"hours = sorted({h for h, _ in counts})\n",
"bottom = np.zeros(len(hours))\n",
"for lv, c in [(\"High\", \"limegreen\"), (\"Medium\", \"orange\"), (\"Low\", \"red\")]:\n",
" v = np.array([counts.get((h, lv), 0) for h in hours])\n",
" ax[0, 0].bar(hours, v, width=5, bottom=bottom, color=c, label=lv); bottom += v\n",
"ax[0, 0].set_title(\"Posts per 6h by credibility level\"); ax[0, 0].set_xlabel(\"hours (virtual)\"); ax[0, 0].legend()\n",
"story = int(top.index[0]) if len(top) else int(df.cid.mode()[0]) # ์ ํ ๊ฒฝ๋ก: ๊ฐ์ฅ ์ํํ ์ด์ผ๊ธฐ ํ๋\n",
"s = df[df.cid == story]\n",
"types = [\"media\", \"witness\", \"aggregator\", \"propaganda\"]\n",
"pos = {}\n",
"for k, (a, ty) in enumerate(zip(s[\"acc\"], s[\"type\"])):\n",
" ang = 2 * np.pi * (types.index(ty) + 0.8 * (k % 7) / 7) / 4\n",
" pos.setdefault(a, (np.cos(ang) * (1 + 0.1 * (k % 3)), np.sin(ang) * (1 + 0.1 * (k % 3))))\n",
"idmap = dict(zip(df[\"id\"], df[\"acc\"]))\n",
"for _, r in s.iterrows():\n",
" if r[\"fwd\"] in idmap and idmap[r[\"fwd\"]] in pos:\n",
" (x0, y0), (x1, y1) = pos[idmap[r[\"fwd\"]]], pos[r[\"acc\"]]\n",
" ax[0, 1].annotate(\"\", (x1, y1), (x0, y0), arrowprops=dict(arrowstyle=\"->\", color=\"gray\"))\n",
"for a, (x, y) in pos.items():\n",
" lv = s[s.acc == a][\"level\"].iloc[0]\n",
" ax[0, 1].scatter(x, y, s=120, c={\"High\": \"limegreen\", \"Medium\": \"orange\", \"Low\": \"red\"}[lv])\n",
" ax[0, 1].text(x, y + 0.08, a[4:], fontsize=7, ha=\"center\")\n",
"ax[0, 1].set_title(f\"Propagation network of one story (arrows = forwards, n={len(s)})\"); ax[0, 1].axis(\"off\")\n",
"ax[1, 0].boxplot([df[df.type == t][\"cred\"] for t in types]); ax[1, 0].set_xticks(range(1, 5)); ax[1, 0].set_xticklabels(types)\n",
"ax[1, 0].set_title(\"Credibility by account type\")\n",
"for w, e in zip(RISK_WORDS, [\"sokbo\", \"urgent\", \"spread\", \"share\", \"massive\"]):\n",
" hs = sorted(kw[w]); ax[1, 1].plot(hs, [kw[w][h] for h in hs], \"-o\", ms=3, label=e)\n",
"ax[1, 1].set_title(\"Risk keyword trend (per 6h)\"); ax[1, 1].legend()\n",
"fig.suptitle(\"ISR02 information-warfare monitoring dashboard - synthetic data only\")\n",
"t0 = time.perf_counter(); fig.canvas.draw(); draw_ms = (time.perf_counter() - t0) * 1000\n",
"plt.tight_layout(); plt.show()\n",
"print(f\"\\nํ๋ฉด ๋ ๋๋ง {draw_ms:.0f} ms (๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if draw_ms < 1000 else '๋ฏธ๋ฌ'}), ์๊ฐํ ๊ตฌ์ฑ์์ 4์ข
(์ ์
์ถ์ดยท์ ํ ๋คํธ์ํฌยท๊ณ์ ์ ํ๋ณ ์ ๋ขฐ๋ยท์ํ ํค์๋)\")\n",
"print(\"Lowยท๊ธด๊ธ ํ์๋ ๊ฒํ ์ฐ์ ์์์ด๋ฉฐ, ์ฐจ๋จยท์ฑํ์ ์ ๋ณด๊ด ๊ฒฐ์ (๋ฏธ์
7)์ผ๋ก๋ง ํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์คํธ๋ฆผ ๊ฐฑ์ 30ํ(ํ๋น ์ฝ 20๊ฑด), ์ฒ๋ฆฌ ํ๊ท 1.4 ms, ์ต๋ 3.7 ms (๋ชฉํ: 1์ด ์ฃผ๊ธฐ ์์์ 100 ms ์ด๋ด)\n",
"๊ฒ์๋ฌผ 587๊ฑด: High 314 / Medium 42 / Low 231\n",
"[๊ธด๊ธ ๊ฐ์กฐ] ์กฐ์ง์ ๋์ ๊ฒ์๊ฐ ์๋ Low ๋ฑ๊ธ ์ด์ผ๊ธฐ 22๊ฐ, ์ต๋ค 8๊ฑด\n",
"\n",
"[๊ณ์ ์ ํ๋ณ ํ๊ท ์ ๋ขฐ๋] {'aggregator': 0.95, 'media': 0.94, 'propaganda': 0.01, 'witness': 0.71}\n",
"\n",
"ํ๋ฉด ๋ ๋๋ง 309 ms (๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด: ๋ฌ์ฑ), ์๊ฐํ ๊ตฌ์ฑ์์ 4์ข
(์ ์
์ถ์ดยท์ ํ ๋คํธ์ํฌยท๊ณ์ ์ ํ๋ณ ์ ๋ขฐ๋ยท์ํ ํค์๋)\n",
"Lowยท๊ธด๊ธ ํ์๋ ๊ฒํ ์ฐ์ ์์์ด๋ฉฐ, ์ฐจ๋จยท์ฑํ์ ์ ๋ณด๊ด ๊ฒฐ์ (๋ฏธ์
7)์ผ๋ก๋ง ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ ์ ์ ํ๊ธ๋ ฅ์ด ํฐ ๊ฐ์ง ์ ๋ณด ์๋ณ ์ ์ ๋ณด๊ด HITL ๊ธด๊ธ ์น์ธ/์ฐจ๋จ ์ํฌํ๋ก์ฐ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ตฐ์ฌ์ ํ๊ธ๋ ฅ์ด ํฐ ์ค์ ์ฒฉ๋ณด์ ๋ํด ์ ๋ณด๊ด์ด ์์ฒ ์ถ์ฒ๋ฅผ ์ง์ ์ญ์ถ์ ํ๊ณ ์น์ธ/์ฐจ๋จํ๋ HITL ๊ฒํ ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ตฐ์ฌ์ ํ๊ธ๋ ฅ์ด ํฐ ์ฒฉ๋ณด์ ๋ํด ์ ๋ณด๊ด์ด ์์ฒ ์ถ์ฒ๋ฅผ ์ง์ ์ญ์ถ์ ํ ๋ค ์ฑํยท์ฐจ๋จยท๋ณด๋ฅํ๋ HITL ๊ธด๊ธ ๊ฒํ ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ง ๊ฒฐ๊ณผ๋ ๋ชจ๋ ๊ฒํ ๋๊ธฐ๋ก ๋ฑ๋กํ๊ณ ํ๊ธ๋ ฅยท๋ฑ๊ธ ์์ผ๋ก ์ ๋ ฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/isr02_approval_workflow.py ๋ฐ docs/isr02_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": [
"\"\"\"๊ตฐ์ฌ์ ํ๊ธ๋ ฅ์ด ํฐ SNS ์ฒฉ๋ณด์ ๋ํด ์ ๋ณด๊ด์ด ์ ๋ฌ ๊ฒฝ๋ก๋ฅผ ๊ฑฐ์ฌ๋ฌ ์์ฒ ์ถ์ฒ๋ฅผ ์ง์ ํ์ธํ ๋ค ์ฑํยท์ฐจ๋จยท๋ณด๋ฅํ๋ ์ธ๊ฐ ๊ฐ์
(HITL) ๊ธด๊ธ ๊ฒํ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌํํ๋ค. ํ๊ธ๋ ฅ์ด ํฐ ๊ฑด์ 2์ธ ๊ฒํ ์ ์ฒ๋ฆฌ ๊ธฐํ(SLA)์ ๋๊ณ , ๊ธฐํ์ ๋๊ธฐ๋ฉด ์๋ ์ฒ๋ฆฌ ๋์ ์์ ํ๋ฉฐ ๋ชจ๋ ๊ฒฐ์ ์ ์ยท๋ณ์กฐ ํ์ธ์ด ๊ฐ๋ฅํ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๋๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import time\n",
"import pandas as pd\n",
"\n",
"ACTIONS = {\"ADOPT\": \"์ฒฉ๋ณด ์ฑํ(๋ด๋ถ ๋ณด๊ณ )\", \"BLOCK\": \"์ฐจ๋จ(๋ด๋ถ ๋ฐฐํฌ ์ค์งยทํ์์ ๋ณด ๋์ ๋ณด๊ณ )\", \"HOLD\": \"๋ณด๋ฅ(์ถ๊ฐ ํ์ธ)\"}\n",
"GRADE_ORDER = {\"E\": 0, \"D\": 1, \"C\": 2, \"B\": 3, \"A\": 4}\n",
"SLA_S = 900 # ํ๊ธ๋ ฅ ํฐ ๊ฑด ์ฒ๋ฆฌ ๊ธฐํ 15๋ถ\n",
"\n",
"\n",
"class WorkflowError(ValueError):\n",
" pass\n",
"\n",
"\n",
"class UrgentQueue:\n",
" def __init__(self, forwards, clock=time.time):\n",
" self.items, self.log, self.fwd, self.clock = {}, [], forwards, 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(), 3)}\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, pid, grade, impact, claim):\n",
" self.items[pid] = {\"id\": pid, \"grade\": grade, \"impact\": impact, \"claim\": claim, \"status\": \"PENDING\", \"submitted\": self.clock(),\n",
" \"origin\": None, \"first\": None, \"escalated\": False}\n",
" self._record({\"event\": \"SUBMIT\", \"id\": pid, \"grade\": grade, \"impact\": impact, \"by\": \"ENGINE\"})\n",
"\n",
" def trace_source(self, pid, officer):\n",
" \"\"\"์ ๋ฌ ๊ฒฝ๋ก(forwarded_from)๋ฅผ ๋๊น์ง ๊ฑฐ์ฌ๋ฌ ์์ฒ ๊ฒ์๋ฌผ์ ์ฐพ์ ๊ธฐ๋กํ๋ค.\"\"\"\n",
" chain, cur = [pid], pid\n",
" while self.fwd.get(cur):\n",
" cur = self.fwd[cur]; chain.append(cur)\n",
" self.items[pid][\"origin\"] = cur\n",
" self._record({\"event\": \"TRACE\", \"id\": pid, \"officer\": officer, \"chain\": \"->\".join(chain)})\n",
" return chain\n",
"\n",
" def pending(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\", \"DECIDE_1ST\")]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], -i[\"impact\"], GRADE_ORDER[i[\"grade\"]]))\n",
"\n",
" def decide(self, pid, action, officer, reason):\n",
" it = self.items.get(pid)\n",
" if action not in ACTIONS:\n",
" raise WorkflowError(f\"ํ์ฉ๋์ง ์์ ์กฐ์น: {action}\")\n",
" if not officer or officer.upper() in (\"AI\", \"ENGINE\", \"SYSTEM\"):\n",
" raise WorkflowError(\"์ ๋ณด๊ด ID ํ์(์๋ ์ฑํยท์ฐจ๋จ ๊ธ์ง)\")\n",
" if it is None or it[\"status\"] in (\"ADOPTED\", \"BLOCKED\"):\n",
" raise WorkflowError(\"๋๊ธฐ์ด์ ์๊ฑฐ๋ ์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด\")\n",
" if action in (\"ADOPT\", \"BLOCK\") and it[\"origin\"] is None:\n",
" raise WorkflowError(\"์์ฒ ์ถ์ฒ ์ญ์ถ์ ๊ธฐ๋ก ์์ด ์ฑํยท์ฐจ๋จ ๋ถ๊ฐ\")\n",
" if len(reason.strip()) < (20 if action == \"ADOPT\" and it[\"grade\"] in (\"D\", \"E\") else 8):\n",
" raise WorkflowError(\"ํ๋จ ๊ทผ๊ฑฐ ๋ถ์กฑ(DยทE ๋ฑ๊ธ ์ฑํ์ 20์ ์ด์)\")\n",
" if action in (\"ADOPT\", \"BLOCK\") and it[\"impact\"] >= 0.7:\n",
" if it[\"first\"] is None:\n",
" it[\"first\"], it[\"status\"], it[\"first_action\"] = officer, \"DECIDE_1ST\", action\n",
" self._record({\"event\": \"DECIDE_1ST\", \"id\": pid, \"action\": action, \"officer\": officer, \"reason\": reason})\n",
" return\n",
" if it[\"first\"] == officer or it[\"first_action\"] != action:\n",
" raise WorkflowError(\"ํ๊ธ๋ ฅ ํฐ ๊ฑด: ๋ค๋ฅธ ์ ๋ณด๊ด์ด ๊ฐ์ ์กฐ์น์ ๋์ํด์ผ ํ์ \")\n",
" it[\"status\"] = {\"ADOPT\": \"ADOPTED\", \"BLOCK\": \"BLOCKED\", \"HOLD\": \"HOLD\"}[action]\n",
" it[\"decided\"] = self.clock()\n",
" self._record({\"event\": \"DECIDE\", \"id\": pid, \"action\": action, \"officer\": officer, \"reason\": reason})\n",
"\n",
" def check_sla(self):\n",
" \"\"\"์ฒ๋ฆฌ ๊ธฐํ์ ๋๊ธด ํ๊ธ๋ ฅ ํฐ ๊ฑด์ ์๋ ์ฒ๋ฆฌํ์ง ์๊ณ ์ ์ ์ ๋ณด๊ด์๊ฒ ์์ ํ๋ค.\"\"\"\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"impact\"] >= 0.7 and i[\"status\"] in (\"PENDING\", \"HOLD\", \"DECIDE_1ST\") and not i[\"escalated\"] and self.clock() - i[\"submitted\"] > SLA_S:\n",
" i[\"escalated\"] = True; out.append(i[\"id\"])\n",
" self._record({\"event\": \"ESCALATE\", \"id\": i[\"id\"], \"by\": \"SYSTEM\", \"to\": \"์ ์ ์ ๋ณด๊ด\"})\n",
" return out\n",
"\n",
" def feedback(self, pid, commander, text):\n",
" self._record({\"event\": \"FEEDBACK\", \"id\": pid, \"commander\": commander, \"text\": text})\n",
"\n",
"\n",
"class FakeClock:\n",
" t = 1000.0\n",
"\n",
" def __call__(self):\n",
" return self.t\n",
"\n",
"\n",
"clock = FakeClock()\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ ๋ฌ ๊ฒฝ๋ก(๊ฒ์๋ฌผ -> ์ ๋ฌ ์๋ณธ)์ ๋ฏธ์
5 ์์ง์์ ๋์ด์จ ๊ฒํ ๋์ 5๊ฑด\n",
"forwards = {\"P0310\": \"P0302\", \"P0302\": \"P0300\", \"P0405\": None, \"P0412\": \"P0405\", \"P0520\": None, \"P0533\": None}\n",
"q = UrgentQueue(forwards, clock)\n",
"for pid, g, imp, claim in [(\"P0310\", \"E\", 0.92, \"๊ฐ์ C-2 ์ง์ญ ๋๊ท๋ชจ ์ฐจ๋ ์ด๋(์๋ 6๋ฐฐ ๊ณผ์ฅ ์์ฌ)\"), (\"P0412\", \"A\", 0.75, \"๊ฐ์ B-4 ๊ต๋ ์ธ๊ทผ ๋ณด๊ธ ํธ๋ญ ์ด๋\"),\n",
" (\"P0520\", \"C\", 0.40, \"๊ฐ์ D-1 ํฌ๊ธฐ ์์ ์ ๋ณด\"), (\"P0533\", \"D\", 0.85, \"๊ฐ์ A-3 ๊ธด๊ธ ํ์ฐ ์์ฒญ ๊ฒ์๋ฌผ\"), (\"P0405\", \"B\", 0.30, \"๊ฐ์ B-4 ์ฐจ๋ ๋ชฉ๊ฒฉ\")]:\n",
" q.submit(pid, g, imp, claim)\n",
"print(\"๊ฒํ ์์(ํ๊ธ๋ ฅยท๋ฑ๊ธ):\", [i[\"id\"] for i in q.pending()])\n",
"print(\"์๋ ์ฒ๋ฆฌ๋ ๊ฑด์:\", sum(i[\"status\"] in (\"ADOPTED\", \"BLOCKED\") for i in q.items.values()))\n",
"\n",
"for args in [(\"P0310\", \"BLOCK\", \"ENGINE\", \"E ๋ฑ๊ธ ์๋ ์ฐจ๋จ ์๋\"), (\"P0310\", \"BLOCK\", \"io01\", \"์๋ ๊ณผ์ฅ ์์ฌ\"), (\"P0520\", \"DELETE\", \"io01\", \"ํ์ฉ๋์ง ์์ ์กฐ์น\")]:\n",
" try:\n",
" q.decide(*args)\n",
" except WorkflowError as e:\n",
" print(\"์ฐจ๋จ:\", e)\n",
"\n",
"clock.t += 120\n",
"print(\"P0310 ์์ฒ ์ญ์ถ์ :\", \" -> \".join(q.trace_source(\"P0310\", \"io01\")))\n",
"q.decide(\"P0310\", \"BLOCK\", \"io01\", \"์์ฒ P0300์ ์๋ 7๋, ์ฌ์ ๋ฌ ๊ณผ์ ์์ 42๋๋ก ๋ณ์กฐ\")\n",
"try:\n",
" q.decide(\"P0310\", \"ADOPT\", \"io02\", \"2์ฐจ ๊ฒํ ์๊ฐ ๋ค๋ฅธ ์กฐ์น๋ฅผ ์ ํํ ๊ฒฝ์ฐ\")\n",
"except WorkflowError as e:\n",
" print(\"์ฐจ๋จ:\", e)\n",
"clock.t += 240; q.decide(\"P0310\", \"BLOCK\", \"io02\", \"์์ฒ ๊ฒ์๋ฌผ๊ณผ ์๋ ๋ถ์ผ์น ์ฌํ์ธ, ์ฐจ๋จ ๋์\")\n",
"q.trace_source(\"P0412\", \"io02\"); clock.t += 60; q.decide(\"P0412\", \"ADOPT\", \"io02\", \"์์ฒ P0405 ํ์ง ์ ๋ณด์ ์ผ์น, ๋
๋ฆฝ ์ถ์ฒ 2๊ฑด\")\n",
"clock.t += 30; q.decide(\"P0412\", \"ADOPT\", \"io03\", \"์์ฒ ์ด๋ฏธ์ง ํด์ ์ผ์น ํ์ธ, ์ฑํ ๋์\")\n",
"q.decide(\"P0520\", \"HOLD\", \"io01\", \"๋
๋ฆฝ ์ถ์ฒ ์์, ์ถ๊ฐ ํ์ธ\")\n",
"clock.t += 900\n",
"print(\"์ฒ๋ฆฌ ๊ธฐํ ์ด๊ณผ ์์ :\", q.check_sla())\n",
"q.feedback(\"P0310\", \"cmd01\", \"๊ฐ์ ๋ณ์กฐ ์ ํ์ด ๋ฐ๋ณต๋๋ฉด ๋์ ๋ฌธ์์ ๋ฏธ๋ฆฌ ์ค๋นํ ๊ฒ\")\n",
"\n",
"df = pd.DataFrame(q.items.values())\n",
"df[\"๋๊ธฐ์๊ฐ(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(0) if \"decided\" in df else None\n",
"print(\"\\n\" + df[[\"id\", \"grade\", \"impact\", \"status\", \"origin\", \"first\", \"escalated\", \"๋๊ธฐ์๊ฐ(s)\"]].fillna(\"-\").to_string(index=False))\n",
"ok, _ = q.verify_log()\n",
"print(f\"\\n๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด(๋ฑ๋กยท์ญ์ถ์ ยท1์ฐจ/์ต์ข
๊ฒฐ์ ยท์์ ยทํผ๋๋ฐฑ), ๋ก๊น
์ฑ๊ณต๋ฅ {len(q.log) / len(q.log):.0%}, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {ok}\")\n",
"q.log[5][\"officer\"] = \"io99\"\n",
"print(\"6๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น):\", q.verify_log())\n",
"print(\"\\n'์ฐจ๋จ'์ ๋ด๋ถ ์ ๋ณด ๋ฐฐํฌ๋ฅผ ๋ฉ์ถ๊ณ ํ์์ ๋ณด ๋์ ๋ณด๊ณ ๋ฅผ ์ฌ๋ฆฌ๋ ์กฐ์น์ด๋ฉฐ, ๋์ธ ๋์์ ์งํ๊ณํต์ ๋ณ๋ ๊ฒฐ์ ์ ๋ฐ๋ฅธ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒํ ์์(ํ๊ธ๋ ฅยท๋ฑ๊ธ): ['P0310', 'P0533', 'P0412', 'P0520', 'P0405']\n",
"์๋ ์ฒ๋ฆฌ๋ ๊ฑด์: 0\n",
"์ฐจ๋จ: ์ ๋ณด๊ด ID ํ์(์๋ ์ฑํยท์ฐจ๋จ ๊ธ์ง)\n",
"์ฐจ๋จ: ์์ฒ ์ถ์ฒ ์ญ์ถ์ ๊ธฐ๋ก ์์ด ์ฑํยท์ฐจ๋จ ๋ถ๊ฐ\n",
"์ฐจ๋จ: ํ์ฉ๋์ง ์์ ์กฐ์น: DELETE\n",
"P0310 ์์ฒ ์ญ์ถ์ : P0310 -> P0302 -> P0300\n",
"์ฐจ๋จ: ํ๊ธ๋ ฅ ํฐ ๊ฑด: ๋ค๋ฅธ ์ ๋ณด๊ด์ด ๊ฐ์ ์กฐ์น์ ๋์ํด์ผ ํ์ \n",
"์ฒ๋ฆฌ ๊ธฐํ ์ด๊ณผ ์์ : ['P0533']\n",
"\n",
" id grade impact status origin first escalated ๋๊ธฐ์๊ฐ(s)\n",
"P0310 E 0.92 BLOCKED P0300 io01 False 360.0\n",
"P0412 A 0.75 ADOPTED P0405 io02 False 450.0\n",
"P0520 C 0.40 HOLD - - False 450.0\n",
"P0533 D 0.85 PENDING - - True -\n",
"P0405 B 0.30 PENDING - - False -\n",
"\n",
"๊ฐ์ฌ ๋ก๊ทธ 14๊ฑด(๋ฑ๋กยท์ญ์ถ์ ยท1์ฐจ/์ต์ข
๊ฒฐ์ ยท์์ ยทํผ๋๋ฐฑ), ๋ก๊น
์ฑ๊ณต๋ฅ 100%, ํด์ ์ฒด์ธ ๊ฒ์ฆ: True\n",
"6๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น): (False, 6)\n",
"\n",
"'์ฐจ๋จ'์ ๋ด๋ถ ์ ๋ณด ๋ฐฐํฌ๋ฅผ ๋ฉ์ถ๊ณ ํ์์ ๋ณด ๋์ ๋ณด๊ณ ๋ฅผ ์ฌ๋ฆฌ๋ ์กฐ์น์ด๋ฉฐ, ๋์ธ ๋์์ ์งํ๊ณํต์ ๋ณ๋ ๊ฒฐ์ ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ ์ ์ ํญ์ฆ ๋ชจ์ ๋๋ SNS ์คํธ๋ฆผ ๋ฐ์ดํฐ ์ ์
๋ถํ ์๋ฎฌ๋ ์ด์
ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ๊ณต์์ด ์ง์ค๋๋ ์ํฉ์ ๋ชจ์ํ์ฌ ์ด๋น ์์ฒ ๊ฑด์ SNS ๊ฒ์๋ฌผ ์ ์
๋ถํ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ ๊ณต์์ด ์ง์ค๋๋ ์ํฉ์ ๋ชจ์ํด ์ด๋น ์์ฒ ๊ฑด์ SNS ๊ฒ์๋ฌผ ์ ์
๋ถํ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ์ ์ด๋น 1,000๊ฑด๊ณผ 500% ์ฆ๊ฐํ ์ด๋น 6,000๊ฑด ์คํธ๋ฆผ, ์์ ๊ฒ์๋ฌผ ํผ์
, ์ฒ๋ฆฌ ์์ ์ถ์ ์ํฉ์ ๊ฐ์์ผ๋ก ์ฃผ์
ํ์ฌ ์ฒ๋ฆฌ ์ง์ฐ ๋ฐ ๋ณต์๋ ฅ์ ์ํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/isr02_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": [
"\"\"\"์ ์ ๊ณต์์ด ์ง์ค๋๋ ์ํฉ์ ๋ชจ์ํด ์ด๋น 1,000๊ฑด(ํ์)์์ 6,000๊ฑด(500% ์ฆ๊ฐ)๊น์ง SNS ๊ฒ์๋ฌผ ์คํธ๋ฆผ์ ์ฃผ์
ํ๊ณ , ๋ง์ดํฌ๋ก ๋ฐฐ์น ์ฒ๋ฆฌยท์ ํ ๋๊ธฐ์ดยท์ด๊ณผ๋ถ ๋ณด๊ด ๋ฒํผยท๊ธฐ๋ฅ ์ถ์(์ด๋ฏธ์ง ๋ถ์ ์ง์ฐ) ๋ชจ๋๋ก ์์คํ
๋ค์ด 0๊ฑดยท๋ฐ์ดํฐ ์ ์ค 0%๋ฅผ ์งํค๋์ง, ์ฒ๋ฆฌ๋ยท์ง์ฐยท๋ฉ๋ชจ๋ฆฌยท๋ณ๋ชฉ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import re\n",
"import time\n",
"import tracemalloc\n",
"import unicodedata\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 ndimage\n",
"from scipy.fft import dctn\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"QUEUE_CAP = 8000 # ๋ฉ๋ชจ๋ฆฌ ๋๊ธฐ์ด ์ฉ๋(๊ฑด). ๋์น๋ฉด ๋ณด๊ด ๋ฒํผ๋ก ์ฎ๊ฒจ ์ ์ค์ ๋ง๋๋ค\n",
"pool, _ = make_posts(60, seed=7)\n",
"\n",
"\n",
"def stream(n, broken=0.0, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" out = []\n",
" for i in range(n):\n",
" p = dict(pool[i % len(pool)]); p[\"post_id\"] = f\"S{i:06d}\"\n",
" r = rng.random()\n",
" if r < broken / 3:\n",
" p[\"text\"] = None\n",
" elif r < 2 * broken / 3:\n",
" p[\"image\"] = np.zeros((5, 5))\n",
" elif r < broken:\n",
" p[\"text\"] = 12345\n",
" out.append(p)\n",
" return out\n",
"\n",
"\n",
"def process(batch, full, prof, seen):\n",
" \"\"\"๋ง์ดํฌ๋ก ๋ฐฐ์น ์ฒ๋ฆฌ: ๋ณธ๋ฌธ ์ ์ -> ์ค๋ณต ํด์ -> (์ ์ ๋ชจ๋๋ง) ์ด๋ฏธ์ง ํด์ -> ์ ๋ ๋ฌธ์ฒด ์ ์. ํ์ ์ค๋ฅ๋ ๊ฒฉ๋ฆฌ.\"\"\"\n",
" done = deferred = quarantined = 0\n",
" for p in batch:\n",
" try:\n",
" if not isinstance(p[\"text\"], (str, type(None))):\n",
" raise TypeError(\"๋ณธ๋ฌธ ํ์ ์ค๋ฅ\")\n",
" t0 = time.perf_counter(); txt = clean_text(p[\"text\"] or \"\"); prof[\"์ ์ \"] += time.perf_counter() - t0\n",
" seen[hash(txt)] = seen.get(hash(txt), 0) + 1\n",
" if p[\"image\"] is not None:\n",
" if p[\"image\"].shape != (32, 32):\n",
" raise ValueError(\"์ด๋ฏธ์ง ํ์ ์ค๋ฅ\")\n",
" if full:\n",
" t0 = time.perf_counter(); phash(p[\"image\"]); prof[\"์ด๋ฏธ์ง ํด์\"] += time.perf_counter() - t0\n",
" else:\n",
" deferred += 1 # ์ด๋ฏธ์ง ๋ถ์์ ๋์ค์ ์ฒ๋ฆฌ(์ ์ค ์๋)\n",
" t0 = time.perf_counter(); ai_text_score(txt); prof[\"๋ฌธ์ฒด ์ ์\"] += time.perf_counter() - t0\n",
" done += 1\n",
" except Exception:\n",
" quarantined += 1\n",
" return done, deferred, quarantined\n",
"\n",
"\n",
"# 1) ์ฒ๋ฆฌ ์ฉ๋ ์ธก์ (์ ์ ๋ชจ๋ / ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋)\n",
"prof, seen = {\"์ ์ \": 0.0, \"์ด๋ฏธ์ง ํด์\": 0.0, \"๋ฌธ์ฒด ์ ์\": 0.0}, {}\n",
"sample = stream(3000, seed=1)\n",
"t0 = time.perf_counter(); process(sample, True, prof, seen); cap_full = len(sample) / (time.perf_counter() - t0)\n",
"t0 = time.perf_counter(); process(sample, False, dict.fromkeys(prof, 0.0), {}); cap_deg = len(sample) / (time.perf_counter() - t0)\n",
"print(f\"์ฒ๋ฆฌ ์ฉ๋: ์ ์ ๋ชจ๋ {cap_full:,.0f}๊ฑด/์ด, ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋(์ด๋ฏธ์ง ๋ถ์ ์ง์ฐ) {cap_deg:,.0f}๊ฑด/์ด\")\n",
"\n",
"# 2) ์ํฉ๋ณ ์คํธ๋ฆผ ์ฃผ์
(3์ด ๋ถ๋): ์ด ๋จ์๋ก ๋์ฐฉ๋ถ์ ๋๊ธฐ์ด์ ๋ฃ๊ณ , ๋ฐ๋ฆฌ๋ฉด ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋๋ก ์ ํ\n",
"cases = [(\"ํ์ 1,000๊ฑด/์ด\", 1000, 0.0, 1.0), (\"500% ์ฆ๊ฐ 6,000๊ฑด/์ด\", 6000, 0.0, 1.0),\n",
" (\"6,000๊ฑด/์ด + ์์ ๊ฒ์๋ฌผ 3%\", 6000, 0.03, 1.0), (\"6,000๊ฑด/์ด + ์ฒ๋ฆฌ ์์ 1/4\", 6000, 0.0, 0.25)]\n",
"rows = []\n",
"for k, (name, rate, br, cpu) in enumerate(cases):\n",
" arrivals = stream(rate * 3, br, seed=10 + k)\n",
" q, spill = deque(), deque()\n",
" done = deferred = quar = down = 0\n",
" max_backlog, modes, lat = 0, [], []\n",
" try:\n",
" for sec in range(6): # 3์ด ๋์ฐฉ + 3์ด ์์ฌ ์ฒ๋ฆฌ\n",
" for p in arrivals[sec * rate:(sec + 1) * rate]:\n",
" (q if len(q) < QUEUE_CAP else spill).append(p)\n",
" while len(q) < QUEUE_CAP and spill:\n",
" q.append(spill.popleft())\n",
" backlog = len(q) + len(spill)\n",
" full = backlog <= cap_full * cpu # 1์ด ์์ ๋ชป ๋น์ฐ๋ฉด ๊ธฐ๋ฅ ์ถ์\n",
" budget = int((cap_full if full else cap_deg) * cpu)\n",
" batch = [q.popleft() for _ in range(min(budget, len(q)))]\n",
" d, df_, qu = process(batch, full, dict.fromkeys(prof, 0.0), {})\n",
" done += d; deferred += df_; quar += qu\n",
" modes.append(\"์ ์\" if full else \"์ถ์\")\n",
" max_backlog = max(max_backlog, backlog)\n",
" lat.append(backlog / max(budget, 1))\n",
" except Exception:\n",
" down = 1\n",
" left = len(q) + len(spill)\n",
" rows.append({\"์ํฉ\": name, \"์์ \": len(arrivals), \"์ฒ๋ฆฌ\": done, \"๊ฒฉ๋ฆฌ\": quar, \"๋๊ธฐ ์์ฌ\": left, \"์ ์ค\": len(arrivals) - done - quar - left,\n",
" \"๋ค์ด\": down, \"์ต๋ ๋๊ธฐ(๊ฑด)\": max_backlog, \"์ต๋ ์ง์ฐ(์ด)\": round(max(lat), 2), \"์ด๋ฏธ์ง ์ง์ฐ์ฒ๋ฆฌ\": deferred,\n",
" \"๋ชจ๋\": \"/\".join(m[0] for m in modes)})\n",
"df = pd.DataFrame(rows)\n",
"print(df.drop(columns=[\"๋ชจ๋\"]).to_string(index=False))\n",
"print(\"์ด๋ณ ๋ชจ๋(์ =์ ์, ์ถ=๊ธฐ๋ฅ ์ถ์):\", {r[\"์ํฉ\"]: r[\"๋ชจ๋\"] for r in rows})\n",
"print(f\"\\n์์คํ
๋ค์ด {df['๋ค์ด'].sum()}๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ {df['์ ์ค'].sum() / df['์์ '].sum():.1%} (๋ชฉํ 0%), ๋๊ธฐ ์์ฌ๋ถ์ ๋ณด๊ด ๋ฒํผ์์ ์ด์ด์ ์ฒ๋ฆฌ\")\n",
"\n",
"# 3) ๋ฉ๋ชจ๋ฆฌ ์์ ์ฑ๊ณผ ๋ณ๋ชฉ\n",
"tracemalloc.start(); mem = []\n",
"for _ in range(5):\n",
" process(sample[:1000], True, dict.fromkeys(prof, 0.0), {}); mem.append(tracemalloc.get_traced_memory()[0] / 1e6)\n",
"tracemalloc.stop()\n",
"print(\"๋ฉ๋ชจ๋ฆฌ(๋์ผ ๋ฐฐ์น 5ํ, MB):\", [round(m, 2) for m in mem], f\"-> ์ฆ๊ฐ {mem[-1] - mem[0]:.2f} MB (๋์ ํ๋จ ๊ธฐ์ค 5MB)\")\n",
"tot = sum(prof.values())\n",
"print(\"๋ณ๋ชฉ ๊ตฌ๊ฐ(๋์ ์ฒ๋ฆฌ์๊ฐ ๋น์จ):\", {k: f\"{v / tot:.0%}\" for k, v in sorted(prof.items(), key=lambda x: -x[1])})\n",
"print(\"๊ฐ์ : ์ด๋ฏธ์ง ํด์๋ฅผ ๋ณ๋ ๋น๋๊ธฐ ์์
์๋ก ๋ถ๋ฆฌํ๋ฉด ๊ธฐ๋ฅ ์ถ์ ์์ด 6,000๊ฑด/์ด๋ฅผ ๊ฐ๋นํ ์ ์๋ค(์ฝ์ด ์ถ๊ฐ ์).\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"plt.bar(range(len(df)), df[\"์ต๋ ๋๊ธฐ(๊ฑด)\"], color=[\"tab:red\" if \"์ถ\" in m else \"tab:blue\" for m in df[\"๋ชจ๋\"]])\n",
"plt.xticks(range(len(df)), [f\"case{i + 1}\" for i in range(len(df))]); plt.ylabel(\"max backlog (posts)\")\n",
"plt.title(\"SNS stream stress test (red = degraded mode used)\"); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ฒ๋ฆฌ ์ฉ๋: ์ ์ ๋ชจ๋ 8,868๊ฑด/์ด, ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋(์ด๋ฏธ์ง ๋ถ์ ์ง์ฐ) 69,099๊ฑด/์ด\n",
" ์ํฉ ์์ ์ฒ๋ฆฌ ๊ฒฉ๋ฆฌ ๋๊ธฐ ์์ฌ ์ ์ค ๋ค์ด ์ต๋ ๋๊ธฐ(๊ฑด) ์ต๋ ์ง์ฐ(์ด) ์ด๋ฏธ์ง ์ง์ฐ์ฒ๋ฆฌ\n",
" ํ์ 1,000๊ฑด/์ด 3000 3000 0 0 0 0 1000 0.11 0\n",
" 500% ์ฆ๊ฐ 6,000๊ฑด/์ด 18000 18000 0 0 0 0 6000 0.68 0\n",
"6,000๊ฑด/์ด + ์์ ๊ฒ์๋ฌผ 3% 18000 17655 345 0 0 0 6000 0.68 0\n",
"6,000๊ฑด/์ด + ์ฒ๋ฆฌ ์์ 1/4 18000 18000 0 0 0 0 6000 0.35 17748\n",
"์ด๋ณ ๋ชจ๋(์ =์ ์, ์ถ=๊ธฐ๋ฅ ์ถ์): {'ํ์ 1,000๊ฑด/์ด': '์ /์ /์ /์ /์ /์ ', '500% ์ฆ๊ฐ 6,000๊ฑด/์ด': '์ /์ /์ /์ /์ /์ ', '6,000๊ฑด/์ด + ์์ ๊ฒ์๋ฌผ 3%': '์ /์ /์ /์ /์ /์ ', '6,000๊ฑด/์ด + ์ฒ๋ฆฌ ์์ 1/4': '์ถ/์ถ/์ถ/์ /์ /์ '}\n",
"\n",
"์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0.0% (๋ชฉํ 0%), ๋๊ธฐ ์์ฌ๋ถ์ ๋ณด๊ด ๋ฒํผ์์ ์ด์ด์ ์ฒ๋ฆฌ\n",
"๋ฉ๋ชจ๋ฆฌ(๋์ผ ๋ฐฐ์น 5ํ, MB): [0.01, 0.01, 0.01, 0.01, 0.01] -> ์ฆ๊ฐ 0.00 MB (๋์ ํ๋จ ๊ธฐ์ค 5MB)\n",
"๋ณ๋ชฉ ๊ตฌ๊ฐ(๋์ ์ฒ๋ฆฌ์๊ฐ ๋น์จ): {'์ด๋ฏธ์ง ํด์': '85%', '์ ์ ': '11%', '๋ฌธ์ฒด ์ ์': '4%'}\n",
"๊ฐ์ : ์ด๋ฏธ์ง ํด์๋ฅผ ๋ณ๋ ๋น๋๊ธฐ ์์
์๋ก ๋ถ๋ฆฌํ๋ฉด ๊ธฐ๋ฅ ์ถ์ ์์ด 6,000๊ฑด/์ด๋ฅผ ๊ฐ๋นํ ์ ์๋ค(์ฝ์ด ์ถ๊ฐ ์).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ถ์ฒ ์ค๋ถ๋ฅ ์ฌ๋ก ๋ถ์ ๋ฐ ๊ฐ์ง ์ ๋ณด ํ์ง ๋ฃฐ์
/๊ฐ์ค์น ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ์ฌํ๊ฒํ ๋ฅผ ํตํด ์ถ์ฒ ์ค๋ถ๋ฅ ์ฌ๋ก ๋ฐ ์ ์ข
๋ณ์กฐ ํจํด์ ๋ถ์ํ๊ณ ํ์ง ๋ฃฐ์
์ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ์ฌํ๊ฒํ ๋ก ์ถ์ฒยท์ง์ ์ค๋ถ๋ฅ ์ฌ๋ก์ ์ ์ข
๋ณ์กฐ ํจํด์ ๋ถ์ํ๊ณ ํ์ง ๋ฃฐ์
๊ณผ ๊ฐ์ค์น๋ฅผ ์ฌ๋ณด์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํํ ์ข์ ๊ณ์ ์ฌ์นญ๊ณผ ์ ์ฌ ๋ฌธ์ ์นํยท๋ณด์ด์ง ์๋ ๋ฌธ์ ์ฝ์
์ด ์์ธ ๋ชจ์ ์ด์ฉ ๊ฒฐ๊ณผ๋ฅผ AAR ์ผ์ง๋ก ์ ๋ฆฌํ๊ณ ๋ฏธํยท์คํ์ ์์ธ๋ณ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/isr02_aar_report.md ๋ฐ configs/isr02_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": [
"\"\"\"AAR(์ฌํ๊ฒํ ) ์ผ์ง ํ์์ผ๋ก ์ถ์ฒยท์ง์ ์ค๋ถ๋ฅ ์ฌ๋ก์ ์ ์ข
๋ณ์กฐ ํจํด(์ ์ฌ ๋ฌธ์ ์นํยท๋ณด์ด์ง ์๋ ๋ฌธ์ ์ฝ์
)์ ๋ถ์ํ๊ณ , ์ ๊ทํ ๋ฃฐ์
๋ณด๊ฐ๊ณผ ํ์ ์๊ณ๊ฐยท๊ณ์ ํํ ๊ฐ์ค์น ์ฌ๋ณด์ ์ผ๋ก ์ข
ํฉ ์ ํ๋ ํฅ์๊ณผ ์ฌ๋ฐ ์ฌ๋ถ๋ฅผ ํ๊ท ์ํ์ผ๋ก ํ์ธํ๋ค.\"\"\"\n",
"import re\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"# ---- ๊ฒ์๋ฌผ ๊ตฐ์ง(๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ)ยท์กฐ์ ํ์ง ํน์ง ๊ณตํต ํจ์ ----\n",
"FEAT = [\"num_dev\", \"region_mis\", \"dir_mis\", \"obj_mis\", \"img_ref_dist\", \"img_prior_h\", \"time_gap_h\", \"splice\", \"ai_img\",\n",
" \"ai_text\", \"burst_n\", \"has_img\", \"is_fwd\", \"acc_age\", \"acc_followers\", \"acc_orig\", \"acc_past\", \"acc_cv\"]\n",
"\n",
"\n",
"def cluster_posts(posts, hours=24, ham_thr=8):\n",
" \"\"\"๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ: ๋ณธ๋ฌธ์์ ๋ฝ์ (์ง์ญ, ๋์)์ด ๊ฐ๊ณ 24์๊ฐ ์์ด๋ฉด ์ฐ๊ฒฐํ๊ณ ,\n",
" ์ง๊ฐ ํด์๊ฐ ๊ฐ๊น์ด ์ด๋ฏธ์ง(๊ฐ์ ๋์)๋ ์ฐ๊ฒฐํ๋ค. ๊ธ ํํ์ด ๋ฌ๋ผ๋ ๋ด์ฉ ๋จ์๋ก ๋ฌถ์ธ๋ค.\"\"\"\n",
" texts = [clean_text(p[\"text\"]) for p in posts]\n",
" X = TfidfVectorizer(analyzer=\"char_wb\", ngram_range=(2, 3)).fit_transform(texts)\n",
" S = (X @ X.T).toarray()\n",
" H = [phash(p[\"image\"]) if p[\"image\"] is not None else None for p in posts]\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" t = [p[\"posted_at\"] for p in posts]\n",
" parent = list(range(len(posts)))\n",
"\n",
" def find(i):\n",
" while parent[i] != i:\n",
" parent[i] = parent[parent[i]]; i = parent[i]\n",
" return i\n",
" for i in range(len(posts)):\n",
" for j in range(i + 1, len(posts)):\n",
" if abs(t[i] - t[j]) >= hours or sl[i][\"o\"] != sl[j][\"o\"]:\n",
" continue\n",
" same_story = sl[i][\"r\"] is not None and sl[i][\"r\"] == sl[j][\"r\"]\n",
" same_img = H[i] is not None and H[j] is not None and hamming(H[i], H[j]) <= ham_thr\n",
" if same_story or same_img:\n",
" parent[find(i)] = find(j)\n",
" return np.array([find(i) for i in range(len(posts))]), S, H\n",
"\n",
"\n",
"def post_features(posts, acc):\n",
" cid, S, H = cluster_posts(posts)\n",
" t = np.array([p[\"posted_at\"] for p in posts])\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" first_seen = {}\n",
" for i in np.argsort(t): # ์ด๋ฏธ์ง๊ฐ ์ฒ์ ๋ฑ์ฅํ ์๊ฐ๊ณผ ๊ตฐ์ง\n",
" if H[i] is None:\n",
" continue\n",
" key = next((k for k in first_seen if hamming(np.array(k), H[i]) <= 6), None)\n",
" if key is None:\n",
" first_seen[tuple(H[i])] = (t[i], cid[i])\n",
" elif first_seen[key][1] != cid[i]:\n",
" pass\n",
" ref = {c: int(np.argmin(np.where(cid == c, t, np.inf))) for c in set(cid)} # ๊ตฐ์ง์์ ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์๋ฌผ = ๊ธฐ์ค(์ถ์ ์๋ณธ)\n",
" F = []\n",
" for i, p in enumerate(posts):\n",
" r = ref[cid[i]]\n",
" a, b = sl[i], sl[r]\n",
" img_prior = 0.0\n",
" if H[i] is not None:\n",
" for k, (ts, c) in first_seen.items():\n",
" if hamming(np.array(k), H[i]) <= 6 and c != cid[i]:\n",
" img_prior = max(img_prior, min(t[i] - ts, 48.0))\n",
" same_txt = [j for j in np.where(S[i] > 0.97)[0] if abs(t[j] - t[i]) < 0.25 and posts[j][\"account_id\"] != p[\"account_id\"]]\n",
" A = acc[p[\"account_id\"]]\n",
" F.append([abs(np.log((a[\"n\"] or 1) / (b[\"n\"] or 1))), float(a[\"r\"] != b[\"r\"]), float(a[\"d\"] != b[\"d\"]), float(a[\"o\"] != b[\"o\"]),\n",
" hamming(H[i], H[r]) if H[i] is not None and H[r] is not None else 0, img_prior, t[i] - t[r],\n",
" splice_score(p[\"image\"]) if p[\"image\"] is not None else 0, ai_image_score(p[\"image\"]) if p[\"image\"] is not None else 0,\n",
" ai_text_score(p[\"text\"]), len(set(posts[j][\"account_id\"] for j in same_txt)), float(p[\"image\"] is not None),\n",
" float(p[\"forwarded_from\"] is not None), np.log1p(A[\"age_days\"]), np.log1p(A[\"followers\"]), A[\"original_ratio\"],\n",
" A[\"past_accuracy\"], A[\"interval_cv\"]])\n",
" return np.array(F), cid\n",
"\n",
"HOMO = {\"A\": \"ะ\", \"B\": \"ะ\", \"C\": \"ะก\", \"E\": \"ะ\"} # ๋ผํด ๋๋ฌธ์์ ๋ชจ์์ด ๊ฐ์ ํค๋ฆด ๋ฌธ์\n",
"ZW = \"โ\" # ๋ณด์ด์ง ์๋ ๋ฌธ์(ํญ 0 ๊ณต๋ฐฑ)\n",
"RULESET = {\"normalize\": False}\n",
"\n",
"\n",
"def normalize(t):\n",
" \"\"\"๋ณด๊ฐ ๋ฃฐ์
: NFKC + ์ ์ฌ ๋ฌธ์(ํค๋ฆด->๋ผํด) ๋ณต์ + ๋ณด์ด์ง ์๋ ๋ฌธ์ ์ ๊ฑฐ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t).replace(ZW, \"\")\n",
" for k, v in HOMO.items():\n",
" t = t.replace(v, k)\n",
" return t\n",
"\n",
"\n",
"_slots_raw, _clean_raw = slots, clean_text\n",
"slots = lambda t: _slots_raw(normalize(t) if RULESET[\"normalize\"] else t)\n",
"clean_text = lambda t: _clean_raw(normalize(t) if RULESET[\"normalize\"] else t)\n",
"\n",
"\n",
"def evade(posts, acc, rng):\n",
" \"\"\"์ ์ข
๋ณ์กฐ ํจํด: (1) ํํ ์ข์ ๊ณ์ ํ์ทจยท์ฌ์นญ์ผ๋ก ์กฐ์ ๊ธ ๊ฒ์ (2) ์ง์ญ ์ฝ๋ ๊ธ์๋ฅผ ์ ์ฌ ๋ฌธ์๋ก ๋ฐ๊พธ๊ณ ๋ณด์ด์ง ์๋ ๋ฌธ์๋ฅผ ๋ผ์ ๋น๊ต๋ฅผ ํผํ๋ค.\"\"\"\n",
" good = [a for a, v in acc.items() if v[\"type\"] in (\"media\", \"witness\")]\n",
" for p in posts:\n",
" if p[\"is_disinfo\"] and rng.random() < 0.6:\n",
" p[\"account_id\"] = str(rng.choice(good))\n",
" if p[\"is_disinfo\"] and rng.random() < 0.8:\n",
" t = p[\"text\"]\n",
" for k, v in HOMO.items():\n",
" t = re.sub(rf\"{k}-\", v + ZW + \"-\", t)\n",
" p[\"text\"] = t + ZW * int(rng.integers(1, 4))\n",
" return posts\n",
"\n",
"\n",
"CONTENT = list(range(13))\n",
"\n",
"\n",
"def run(posts, acc, cfg):\n",
" F, _ = post_features(posts, acc)\n",
" m = MODELS[cfg[\"account\"]]\n",
" return m.predict_proba(F if cfg[\"account\"] else F[:, CONTENT])[:, 1] >= cfg[\"thr\"]\n",
"\n",
"\n",
"tr_posts, tr_acc = make_posts(60, seed=7)\n",
"Ftr, _ = post_features(tr_posts, tr_acc)\n",
"ytr = [p[\"is_disinfo\"] for p in tr_posts]\n",
"MODELS = {True: RandomForestClassifier(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Ftr, ytr), # ๊ณ์ ํํ ํฌํจ\n",
" False: RandomForestClassifier(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Ftr[:, CONTENT], ytr)} # ๋ด์ฉ ํน์ง๋ง\n",
"rng = np.random.default_rng(5)\n",
"ex_posts, ex_acc = make_posts(60, seed=21); evade(ex_posts, ex_acc, rng) # ๋ชจ์ ์ด์ฉ(AAR ๋์)\n",
"ho_posts, ho_acc = make_posts(60, seed=22); evade(ho_posts, ho_acc, rng) # ํ๊ท ์ํ์ฉ\n",
"y, kind = np.array([p[\"is_disinfo\"] for p in ex_posts]), np.array([p[\"kind\"] for p in ex_posts])\n",
"yh = np.array([p[\"is_disinfo\"] for p in ho_posts])\n",
"\n",
"# 1) ์ด๊ธฐ ์ค์ : ๊ธฐ์กด ์ ๊ทํ(NFKC๋ง), ๊ณ์ ํํ ํฌํจ ๋ชจ๋ธ, ํ์ ์๊ณ๊ฐ 0.5\n",
"init = {\"account\": True, \"thr\": 0.5}\n",
"p0 = run(ex_posts, ex_acc, init)\n",
"acc0 = (p0 == y).mean()\n",
"evaded = np.array([\"โ\" in p[\"text\"] for p in ex_posts])\n",
"print(f\"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ด๊ธฐ ์ข
ํฉ ์ ํ๋ {acc0:.3f}, ์กฐ์ ์ฌํ์จ {(p0 & y).sum() / y.sum():.3f}, ์ฌ์ค ์ค๊ฒฝ๋ณด {(p0 & ~y).sum()}๊ฑด\")\n",
"log = pd.DataFrame([{\"๊ตฌ๋ถ\": \"๋ฏธํ\" if y[i] else \"์คํ\", \"์์ธ\": (\"์ ์ฌ ๋ฌธ์ ์นํ ํํผ\" if evaded[i] else kind[i]) if y[i] else f\"{ex_acc[ex_posts[i]['account_id']]['type']} ๊ณ์ {kind[i]}\", \"๊ฒ์๋ฌผ\": ex_posts[i][\"post_id\"]}\n",
" for i in np.where(p0 != y)[0]])\n",
"print(log.groupby([\"๊ตฌ๋ถ\", \"์์ธ\"]).size().rename(\"๊ฑด์\").sort_values(ascending=False).head(6).to_string())\n",
"print(f\"\\n[2. ์ ์ผ์ด๋ฌ๋] ํํ ์ข์ ๊ณ์ ์ ์ฌ์นญํ ์กฐ์ ๊ธ์ ๊ณ์ ํํ์ ํฌ๊ฒ ๊ธฐ๋ ๋ชจ๋ธ์ ํต๊ณผํ๊ณ , ์ ์ฌ ๋ฌธ์๊ฐ ๋ค์ด๊ฐ ๊ฒ์๋ฌผ {evaded.sum()}๊ฑด์ ์ง์ญ ์ฝ๋ ์ถ์ถยท๋์ ๊ฒ์ ๋น๊ต๊ฐ ์คํจํ๋ค.\")\n",
"\n",
"# 2) ์ฌ๋ณด์ : ๋ฃฐ์
(์ ๊ทํ ๋ณด๊ฐ) x ๊ณ์ ํํ ๊ฐ์ค์น(ํฌํจ/์ ์ธ) x ํ์ ์๊ณ๊ฐ ๊ฒฉ์ ํ์\n",
"grid = []\n",
"for norm in (False, True):\n",
" RULESET[\"normalize\"] = norm\n",
" F, _ = post_features(ex_posts, ex_acc)\n",
" for use_acc in (True, False):\n",
" pr = MODELS[use_acc].predict_proba(F if use_acc else F[:, CONTENT])[:, 1]\n",
" for thr in (0.3, 0.4, 0.5, 0.6):\n",
" p = pr >= thr\n",
" grid.append({\"์ ๊ทํ ๋ณด๊ฐ\": norm, \"๊ณ์ ํํ\": \"ํฌํจ\" if use_acc else \"์ ์ธ\", \"์๊ณ๊ฐ\": thr, \"์ข
ํฉ์ ํ๋\": round(float((p == y).mean()), 3),\n",
" \"์กฐ์ ์ฌํ์จ\": round(float((p & y).sum() / y.sum()), 3), \"์ฌ์ค ์ค๊ฒฝ๋ณด\": int((p & ~y).sum())})\n",
"g = pd.DataFrame(grid).sort_values([\"์ข
ํฉ์ ํ๋\", \"์ฌ์ค ์ค๊ฒฝ๋ณด\"], ascending=[False, True])\n",
"print(\"\\n[3. ๋ฌด์์ ๋ฐ๊พธ๋] ๊ฒฉ์ ํ์ ์์ 5๊ฐ\")\n",
"print(g.head().to_string(index=False))\n",
"best = g.iloc[0]\n",
"cfg = {\"account\": best[\"๊ณ์ ํํ\"] == \"ํฌํจ\", \"thr\": float(best[\"์๊ณ๊ฐ\"])}\n",
"RULESET[\"normalize\"] = bool(best[\"์ ๊ทํ ๋ณด๊ฐ\"])\n",
"p1 = run(ex_posts, ex_acc, cfg)\n",
"acc1 = (p1 == y).mean()\n",
"print(f\"\\n์ฌ๋ณด์ ๊ฐ: ์ ๊ทํ ๋ณด๊ฐ {best['์ ๊ทํ ๋ณด๊ฐ']}, ๊ณ์ ํํ {best['๊ณ์ ํํ']}, ํ์ ์๊ณ๊ฐ {best['์๊ณ๊ฐ']}\")\n",
"print(f\"์ข
ํฉ ์ ํ๋ {acc0:.3f} -> {acc1:.3f} ({(acc1 - acc0) * 100:+.1f}%p, ๋ชฉํ +10%p: {'๋ฌ์ฑ' if acc1 - acc0 >= 0.1 else '๋ฏธ๋ฌ'})\")\n",
"RULESET[\"normalize\"] = False; h0 = (run(ho_posts, ho_acc, init) == yh).mean()\n",
"RULESET[\"normalize\"] = bool(best[\"์ ๊ทํ ๋ณด๊ฐ\"]); h1 = (run(ho_posts, ho_acc, cfg) == yh).mean()\n",
"print(f\"ํ๊ท ์ํ(๋ณ๋ ๊ฒ์๋ฌผ {len(ho_posts)}๊ฑด): {h0:.3f} -> {h1:.3f}\")\n",
"again = set(np.where(p0 != y)[0]) & set(np.where(p1 != y)[0])\n",
"print(f\"์ฌ๋ณด์ ํ ๋จ์ ์ค๋ฅ {(p1 != y).sum()}๊ฑด, ๊ทธ์ค ์ฌ๋ฐ {len(again)}๊ฑด:\", {str(k): int(v) for k, v in pd.Series([kind[i] for i in again]).value_counts().head(4).items()})\n",
"print(\"[4. ๋จ์ ๊ณผ์ ] ์ฌ๋ฐ ๊ฑด์ ์๋ณธ๊ณผ ๊ฑฐ์ ๊ฐ์ ๋ฏธ์ธ ๋ณ์กฐ๋ก, ์ ์ฌ๋ก๋ฅผ ํ์ต ์๋ฃ์ ์ถ๊ฐํ๋ ์ง์ ํ์ต ์ฃผ๊ธฐ(์ฃผ 1ํ)์ ์ ๋ณด๊ด ์์ฒ ์ญ์ถ์ (๋ฏธ์
7)์ผ๋ก ๋ณด์ํ๋ค.\")\n",
"\n",
"tuned = (f\"ruleset:\\n nfkc: true\\n homoglyph_map: {bool(best['์ ๊ทํ ๋ณด๊ฐ'])}\\n strip_zero_width: {bool(best['์ ๊ทํ ๋ณด๊ฐ'])}\\n\"\n",
" f\"detector:\\n use_account_reputation: {cfg['account']}\\n threshold: {best['์๊ณ๊ฐ']}\\n rf_trees: 200\")\n",
"print(\"\\n[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(isr02_tuned_params.yaml ๋ด์ฉ)]\\n\" + tuned)\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"for norm in (False, True):\n",
" for ua in (\"ํฌํจ\", \"์ ์ธ\"):\n",
" s = pd.DataFrame(grid)[lambda d: (d[\"์ ๊ทํ ๋ณด๊ฐ\"] == norm) & (d[\"๊ณ์ ํํ\"] == ua)]\n",
" plt.plot(s[\"์๊ณ๊ฐ\"], s[\"์ข
ํฉ์ ํ๋\"], \"o-\", label=f\"normalize={norm}, account={'on' if ua == 'ํฌํจ' else 'off'}\")\n",
"plt.xlabel(\"decision threshold\"); plt.ylabel(\"accuracy\"); plt.legend(); plt.title(\"AAR re-tuning grid (synthetic)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ด๊ธฐ ์ข
ํฉ ์ ํ๋ 0.809, ์กฐ์ ์ฌํ์จ 0.576, ์ฌ์ค ์ค๊ฒฝ๋ณด 9๊ฑด\n",
"๊ตฌ๋ถ ์์ธ \n",
"๋ฏธํ ์ ์ฌ ๋ฌธ์ ์นํ ํํผ 91\n",
" splice 3\n",
"์คํ witness ๊ณ์ original 3\n",
" witness ๊ณ์ near_dup 3\n",
" propaganda ๊ณ์ original 3\n",
"๋ฏธํ region_swap 2\n",
"\n",
"[2. ์ ์ผ์ด๋ฌ๋] ํํ ์ข์ ๊ณ์ ์ ์ฌ์นญํ ์กฐ์ ๊ธ์ ๊ณ์ ํํ์ ํฌ๊ฒ ๊ธฐ๋ ๋ชจ๋ธ์ ํต๊ณผํ๊ณ , ์ ์ฌ ๋ฌธ์๊ฐ ๋ค์ด๊ฐ ๊ฒ์๋ฌผ 192๊ฑด์ ์ง์ญ ์ฝ๋ ์ถ์ถยท๋์ ๊ฒ์ ๋น๊ต๊ฐ ์คํจํ๋ค.\n",
"\n",
"[3. ๋ฌด์์ ๋ฐ๊พธ๋] ๊ฒฉ์ ํ์ ์์ 5๊ฐ\n",
" ์ ๊ทํ ๋ณด๊ฐ ๊ณ์ ํํ ์๊ณ๊ฐ ์ข
ํฉ์ ํ๋ ์กฐ์ ์ฌํ์จ ์ฌ์ค ์ค๊ฒฝ๋ณด\n",
" True ํฌํจ 0.3 0.911 0.864 19\n",
" False ์ ์ธ 0.3 0.909 0.936 37\n",
" True ์ ์ธ 0.3 0.909 0.975 46\n",
" False ํฌํจ 0.3 0.905 0.818 11\n",
" True ์ ์ธ 0.6 0.904 0.860 22\n",
"\n",
"์ฌ๋ณด์ ๊ฐ: ์ ๊ทํ ๋ณด๊ฐ True, ๊ณ์ ํํ ํฌํจ, ํ์ ์๊ณ๊ฐ 0.3\n",
"์ข
ํฉ ์ ํ๋ 0.809 -> 0.911 (+10.2%p, ๋ชฉํ +10%p: ๋ฌ์ฑ)\n",
"ํ๊ท ์ํ(๋ณ๋ ๊ฒ์๋ฌผ 566๊ฑด): 0.843 -> 0.901\n",
"์ฌ๋ณด์ ํ ๋จ์ ์ค๋ฅ 51๊ฑด, ๊ทธ์ค ์ฌ๋ฐ 40๊ฑด: {'region_swap': 9, 'dir_flip': 7, 'recycled': 7, 'original': 6}\n",
"[4. ๋จ์ ๊ณผ์ ] ์ฌ๋ฐ ๊ฑด์ ์๋ณธ๊ณผ ๊ฑฐ์ ๊ฐ์ ๋ฏธ์ธ ๋ณ์กฐ๋ก, ์ ์ฌ๋ก๋ฅผ ํ์ต ์๋ฃ์ ์ถ๊ฐํ๋ ์ง์ ํ์ต ์ฃผ๊ธฐ(์ฃผ 1ํ)์ ์ ๋ณด๊ด ์์ฒ ์ญ์ถ์ (๋ฏธ์
7)์ผ๋ก ๋ณด์ํ๋ค.\n",
"\n",
"[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(isr02_tuned_params.yaml ๋ด์ฉ)]\n",
"ruleset:\n",
" nfkc: true\n",
" homoglyph_map: True\n",
" strip_zero_width: True\n",
"detector:\n",
" use_account_reputation: True\n",
" threshold: 0.3\n",
" rf_trees: 200\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. SNS ์ ์ ๊ฒ์๋ฌผ ์ ๋ขฐ๋ ์๋ ๋ถ๋ฅ ์์คํ
ํตํฉ ๋ฐ ์ ๋ณด์ ๋์ ๋ธ๋ฆฌํ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"SNS ์ ์ ๊ฒ์๋ฌผ ์ ๋ขฐ๋ ์๋ ๋ถ๋ฅ ์์คํ
์ ์ต์ข
ํตํฉํ๊ณ ์ ๋ณด์ ๋์ ๋ธ๋ฆฌํ ํจํค์ง๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฒ์๋ฌผ ๊ฒ์ฆ๋ถํฐ ์ ์ , ์ด์ผ๊ธฐ ๊ตฐ์ง, ์กฐ์ ํ์ง, ์ ๋ขฐ๋ ๋ฑ๊ธ, ์ ๋ณด๊ด ๊ฒํ ๋๊ธฐ์ด, ๋ชจ๋ํฐ๋ง ํ๋ฉด๊น์ง ์์คํ
์ ํตํฉํ๊ณ ์ ๋ณด์ ๋์ ๋ธ๋ฆฌํ ํจํค์ง๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฌ๋ณด์ ์ค์ ์ผ๋ก ์ ์ฒด ๊ณผ์ ์ ํ ๋ฒ์ ์คํํ๋ ํตํฉ ์ฝ๋๋ฅผ ์์ฑํ๊ณ ์์ ๊ฒ์๋ฌผ์ด ์๋ ์ฐจ๋จ๋๋์ง ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_isr02.py, README.md, docs/isr02_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": [
"\"\"\"๊ฒ์๋ฌผ ๊ฒ์ฆยท์ ์ (๋ณด๊ฐ ์ ๊ทํ)ยท์ด์ผ๊ธฐ ๊ตฐ์งยท์กฐ์ ํ์งยท์ข
ํฉ ์ ๋ขฐ๋ ๋ฑ๊ธ(A~E)ยท์ ๋ณด๊ด ๊ธด๊ธ ๊ฒํ ๋๊ธฐ์ดยท๋ชจ๋ํฐ๋ง ํ๋ฉด๊น์ง SNS ์ ์ ๊ฒ์๋ฌผ ์ ๋ขฐ๋ ์๋ ๋ถ๋ฅ ์์คํ
์ ์ฒด๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒ(๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง ํฌํจ)๊ณผ ์ ๋ณด์ ๋์ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import re\n",
"import time\n",
"import unicodedata\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from scipy.fft import dctn\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ SNS ๊ฒ์๋ฌผ ์์ฑ๊ธฐ (์ค์ ๊ณ์ ยท์ฑ๋ยท์ง๋ช
ยท๊ฒ์๋ฌผ ์์)\n",
"# ๊ฐ์ ์ฌ๊ฑด๋ง๋ค ์๋ณธ ์ ๋ณด, ์ฌ๊ฒ์(๋ณต์ ), ์ ์ฌ ์ฌ์์ฑ, ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ 10์ข
๊ณผ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"REGIONS = [f\"{a}-{n}\" for a in \"ABCDEF\" for n in range(1, 6)]\n",
"OBJECTS = [\"๊ตฐ์ฉ ์ฐจ๋\", \"์ํ ๋๋ก \", \"๋ณด๊ธ ํธ๋ญ\", \"ํฌ๊ธฐ\", \"๋ฒ์ค\", \"๊ฒฌ์ธ ์ฐจ๋\", \"ํธ๋ ์ผ๋ฌ\", \"๋ฏผ๊ฐ ์น์ฉ์ฐจ\"]\n",
"DIRS = [\"๋ถ์ชฝ\", \"๋จ์ชฝ\", \"๋์ชฝ\", \"์์ชฝ\"]\n",
"TEMPLATES = [\"๊ฐ์ {r} ์ง์ญ์์ {o} ๋ชฉ๊ฒฉ, {n}๋ ๊ท๋ชจ๋ก {d}์ผ๋ก ์ด๋ ์ค\",\n",
" \"{r} ์ผ๋ {o} ํ์ธ๋จ. ์ฝ {n}๋, {d} ๋ฐฉํฅ์ผ๋ก ์ด๋\",\n",
" \"[ํ์ง] {r}์์ {o} ๋ฐ๊ฒฌ - ์๋ {n}, ์ด๋ ๋ฐฉํฅ {d}\",\n",
" \"{r} ์ฃผ๋ฏผ ์ ๋ณด: {o} {n}๋๊ฐ {d}์ผ๋ก ํฅํ๋ ๋ชจ์ต\"]\n",
"MANIP = [\"num_change\", \"region_swap\", \"dir_flip\", \"old_image\", \"splice\", \"ai_image\", \"ai_text\", \"bot_burst\", \"mashup\", \"recycled\"]\n",
"ACCOUNT_TYPES = { # ์ ํ๋ณ ๊ณ์ ์์ฑ ๋ถํฌ(๊ฐ์): ๋์ด(์ผ), ํ๋ก์, ํ๋ฃจ ๊ฒ์ ์, ์๋ณธ ๋น์จ, ๊ณผ๊ฑฐ ์ ํ๋, ๊ฒ์ ๊ฐ๊ฒฉ ๋ณ๋๊ณ์\n",
" \"media\": ((1000, 4000), (5e4, 5e5), (5, 30), (.6, .9), (.85, .97), (.8, 1.5)),\n",
" \"witness\": ((10, 800), (20, 2000), (.2, 3), (.7, 1.0), (.6, .95), (1.0, 2.0)),\n",
" \"aggregator\": ((200, 2000), (1e4, 2e5), (30, 200), (.05, .2), (.6, .85), (.5, 1.0)),\n",
" \"propaganda\": ((5, 200), (100, 2e4), (20, 150), (.3, .7), (.1, .4), (.05, .3)),\n",
"}\n",
"\n",
"\n",
"def make_accounts(rng, n=(8, 30, 10, 20)):\n",
" acc = {}\n",
" for (t, spec), k in zip(ACCOUNT_TYPES.items(), n):\n",
" for i in range(k):\n",
" v = [rng.uniform(*r) for r in spec]\n",
" acc[f\"ACC_{t[:3].upper()}{i:02d}\"] = {\"type\": t, \"age_days\": int(v[0]), \"followers\": int(v[1]), \"posts_per_day\": round(v[2], 1),\n",
" \"original_ratio\": round(v[3], 2), \"past_accuracy\": round(v[4], 2), \"interval_cv\": round(v[5], 2)}\n",
" return acc\n",
"\n",
"\n",
"def event_image(seed):\n",
" \"\"\"์ฌ๊ฑด ํ์ฅ ์ฌ์ง์ ํ๋ด ๋ธ 32x32 ํฉ์ฑ ์ด๋ฏธ์ง(์ฌ๊ฑด๋ง๋ค ๊ณ ์ ํ ๋ํ ๋ฐฐ์น).\"\"\"\n",
" r = np.random.default_rng(seed)\n",
" img = np.linspace(r.uniform(60, 120), r.uniform(120, 200), 32)[:, None] * np.ones((1, 32))\n",
" for _ in range(int(r.integers(3, 7))):\n",
" y, x, h, w = r.integers(0, 26, 4)\n",
" img[y:y + 3 + h % 8, x:x + 3 + w % 8] = r.uniform(20, 240)\n",
" return img + r.normal(0, 4, (32, 32))\n",
"\n",
"\n",
"def variant(img, rng):\n",
" \"\"\"์ฌ๊ฒ์ ๊ณผ์ ์ ์์ฐ์ค๋ฌ์ด ๋ณํ: ์ฌ์์ถ ์ก์, ๋ฐ๊ธฐ ๋ณํ, ๊ฐ์ฅ์๋ฆฌ ์๋ฆผ ์ค ํ๋.\"\"\"\n",
" k = rng.integers(0, 3)\n",
" if k == 0:\n",
" return ndimage.gaussian_filter(img, 0.6) + rng.normal(0, 3, img.shape)\n",
" if k == 1:\n",
" return img * rng.uniform(0.85, 1.15)\n",
" return ndimage.zoom(img[2:30, 2:30], 32 / 28, order=1)\n",
"\n",
"\n",
"def splice(img, other, rng):\n",
" out = img.copy()\n",
" y, x = rng.integers(4, 18, 2)\n",
" out[y:y + 12, x:x + 12] = other[y:y + 12, x:x + 12] + rng.normal(0, 12, (12, 12)) # ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ ๋ถ์\n",
" return out\n",
"\n",
"\n",
"def ai_image(rng):\n",
" \"\"\"์์ฑํ ๋ชจ๋ธ ์ด๋ฏธ์ง๋ฅผ ํ๋ด ๋ธ ํฉ์ฑ ์ด๋ฏธ์ง: ์ง๋์น๊ฒ ๋งค๋ํ ๋ฐํ + ์
์ํ๋ง ๊ฒฉ์ ๋ฌด๋ฌ(์ฃผ๊ธฐ 2ํฝ์
).\"\"\"\n",
" base = ndimage.gaussian_filter(rng.normal(130, 60, (32, 32)), 3)\n",
" grid = (np.indices((32, 32)).sum(0) % 2) * rng.uniform(6, 12)\n",
" return base + grid\n",
"\n",
"\n",
"def write(ev, rng, tpl=None, **kw):\n",
" s = {\"r\": ev[\"r\"], \"o\": ev[\"o\"], \"n\": ev[\"n\"], \"d\": ev[\"d\"], **kw}\n",
" return (tpl or TEMPLATES[int(rng.integers(len(TEMPLATES)))]).format(**s)\n",
"\n",
"\n",
"def make_posts(n_events=40, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" acc = make_accounts(rng)\n",
" by = {t: [a for a, v in acc.items() if v[\"type\"] == t] for t in ACCOUNT_TYPES}\n",
" events = [{\"id\": f\"EV{i:03d}\", \"r\": str(rng.choice(REGIONS)), \"o\": str(rng.choice(OBJECTS)), \"n\": int(rng.integers(2, 15)),\n",
" \"d\": str(rng.choice(DIRS)), \"t0\": float(i * 1.2 + rng.uniform(0, 1)), \"img\": event_image(seed * 1000 + i)} for i in range(n_events)]\n",
" posts = []\n",
"\n",
" def add(ev, account, text, img, t, fwd, kind, origin):\n",
" pid = f\"P{len(posts):04d}\"\n",
" posts.append({\"post_id\": pid, \"platform\": str(rng.choice([\"telegram\", \"x\", \"other\"], p=[.6, .3, .1])), \"account_id\": account,\n",
" \"posted_at\": round(t, 3), \"text\": text, \"image\": img, \"forwarded_from\": fwd, \"event_id\": ev[\"id\"] if ev else None,\n",
" \"kind\": kind, \"is_disinfo\": kind in MANIP, \"origin\": origin})\n",
" return pid\n",
"\n",
" for k, ev in enumerate(events):\n",
" src = str(rng.choice(by[\"witness\"] if rng.random() < .6 else by[\"media\"]))\n",
" if rng.random() < .05:\n",
" src = str(rng.choice(by[\"propaganda\"])) # ์ ๋ขฐ๋ ๋ฎ์ ๊ณ์ ๋ ์ฌ์ค์ ์ฌ๋ฆด ์ ์๋ค\n",
" o = add(ev, src, write(ev, rng), ev[\"img\"], ev[\"t0\"], None, \"original\", None)\n",
" for _ in range(int(rng.integers(2, 5))): # ๊ทธ๋๋ก ์ฌ๊ฒ์(์ ๋ฌ)\n",
" add(ev, str(rng.choice(by[\"aggregator\"])), posts[int(o[1:])][\"text\"], ev[\"img\"], ev[\"t0\"] + rng.exponential(1), o, \"copy\", o)\n",
" for _ in range(int(rng.integers(1, 3))): # ํํ๋ง ๋ฐ๊พผ ์ฌ์์ฑ(๋ด์ฉ ๋์ผ)\n",
" add(ev, str(rng.choice(by[\"media\"] + by[\"witness\"])), \"#๊ฐ์์๋ณด \" + write(ev, rng), variant(ev[\"img\"], rng),\n",
" ev[\"t0\"] + rng.uniform(0.5, 6), None, \"near_dup\", o)\n",
" for m in MANIP if k % 2 == 0 else MANIP[k % 4::4]: # ์กฐ์ยท๊ธฐ๋ง ๊ฒ์๋ฌผ\n",
" if rng.random() > 0.5:\n",
" continue\n",
" who = str(rng.choice(by[\"propaganda\"] if rng.random() < .75 else by[\"aggregator\"] + by[\"witness\"]))\n",
" t, img, text = ev[\"t0\"] + rng.uniform(1, 12), variant(ev[\"img\"], rng), None\n",
" if m == \"num_change\":\n",
" text = write(ev, rng, n=ev[\"n\"] * int(rng.integers(5, 10)))\n",
" elif m == \"region_swap\":\n",
" text = write(ev, rng, r=str(rng.choice([r for r in REGIONS if r != ev[\"r\"]])))\n",
" elif m == \"dir_flip\":\n",
" text = write(ev, rng, d={\"๋ถ์ชฝ\": \"๋จ์ชฝ\", \"๋จ์ชฝ\": \"๋ถ์ชฝ\", \"๋์ชฝ\": \"์์ชฝ\", \"์์ชฝ\": \"๋์ชฝ\"}[ev[\"d\"]])\n",
" elif m == \"old_image\" and k > 0:\n",
" text, img = write(ev, rng), events[int(rng.integers(0, k))][\"img\"]\n",
" elif m == \"splice\":\n",
" text, img = write(ev, rng), splice(ev[\"img\"], events[(k + 7) % n_events][\"img\"], rng)\n",
" elif m == \"ai_image\":\n",
" text, img = write(ev, rng), ai_image(rng)\n",
" elif m == \"ai_text\":\n",
" text = f\"์๋ณด ์๋ณด! {ev['r']} ์ง์ญ ๋๊ท๋ชจ {ev['o']} ์ถํ! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๊ธด๊ธ ํ์ฐ ๋ฐ๋! ๋ชจ๋ ํ์ฐ ๋ฐ๋!\"\n",
" img = ai_image(rng) if rng.random() < .5 else None\n",
" elif m == \"bot_burst\":\n",
" text = write(ev, rng, n=ev[\"n\"] * 6) + \" ์ง๊ธ ๋ฐ๋ก ๊ณต์ \"\n",
" for b in rng.choice(by[\"propaganda\"], 4, replace=False):\n",
" add(ev, str(b), text, img, t + rng.uniform(0, 0.15), None, m, o)\n",
" continue\n",
" elif m == \"mashup\":\n",
" other = events[(k + 3) % n_events]\n",
" text = f\"{ev['r']} ์ผ๋ {ev['o']} ํ์ธ, {other['n']}๋๊ฐ {other['d']}์ผ๋ก ์ด๋\" # ๋ค๋ฅธ ์ฌ๊ฑด ๋ด์ฉ๊ณผ ์ง๊น๊ธฐ\n",
" elif m == \"recycled\" and k > 2:\n",
" old = events[int(rng.integers(0, k - 1))]\n",
" text, img, t = write(old, rng), old[\"img\"], ev[\"t0\"] + rng.uniform(0, 3)\n",
" if text is None:\n",
" continue\n",
" add(ev, who, text, img, t, None, m, o)\n",
" return posts, acc\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ํ
์คํธ ์ ์ ยท์ด๋ฏธ์ง ์ง๊ฐ ํด์ยท์กฐ์ ํ์ ๊ณตํต ํจ์ ----\n",
"STOPWORDS = {\"๊ฐ์\", \"์ฝ\", \"์ผ๋\", \"์ง์ญ์์\", \"๋ชจ์ต\", \"์ค\", \"ํ์ธ๋จ\", \"๋ฐ๊ฒฌ\", \"์ ๋ณด\", \"์ฃผ๋ฏผ\"}\n",
"_URL = re.compile(r\"https?://\\S+|www\\.\\S+\")\n",
"_SPECIAL = re.compile(r\"[^0-9A-Za-z๊ฐ-ํฃ\\-\\s]\")\n",
"\n",
"\n",
"def clean_text(t):\n",
" \"\"\"์ ๊ทํ(NFKC) -> URLยท๋ฉ์
์ ๊ฑฐ -> ํด์ํ๊ทธ ๊ธฐํธ ์ ๊ฑฐ -> ํน์๋ฌธ์ ์ ๊ฑฐ -> ๋ถ์ฉ์ด ์ ๊ฑฐ -> ๊ณต๋ฐฑ ์ ๋ฆฌ.\"\"\"\n",
" t = unicodedata.normalize(\"NFKC\", t)\n",
" t = _URL.sub(\" \", t)\n",
" t = re.sub(r\"@\\w+\", \" \", t).replace(\"#\", \" \")\n",
" t = _SPECIAL.sub(\" \", t)\n",
" return \" \".join(w for w in t.split() if w not in STOPWORDS)\n",
"\n",
"\n",
"def phash(img):\n",
" \"\"\"์ง๊ฐ ํด์(pHash): 32x32 ์ด๋ฏธ์ง DCT์ ์ ์ฃผํ 8x8(์ง๋ฅ ์ฑ๋ถ ์ ์ธ)์ ์ค์๊ฐ๊ณผ ๋น๊ตํ 64๋นํธ.\"\"\"\n",
" c = dctn(np.asarray(img, float), norm=\"ortho\")[:8, :8].ravel()[1:]\n",
" return np.append(c > np.median(c), False)\n",
"\n",
"\n",
"def hamming(a, b):\n",
" return int(np.count_nonzero(a != b))\n",
"\n",
"\n",
"def slots(text):\n",
" \"\"\"๋ณธ๋ฌธ์์ ์ง์ญ ์ฝ๋ยท์๋ยท๋ฐฉํฅยท๋์์ ๋ฝ๋๋ค(์ฌ๊ฑด ๋ด์ฉ ๋น๊ต์ฉ).\"\"\"\n",
" r = re.search(r\"[A-F]-\\d\", text)\n",
" n = re.search(r\"(\\d+)\\s*๋|์๋\\s*(\\d+)\", text)\n",
" d = next((x for x in DIRS if x in text), None)\n",
" o = next((x for x in OBJECTS if x in text), None)\n",
" return {\"r\": r.group(0) if r else None, \"n\": int(n.group(1) or n.group(2)) if n else None, \"d\": d, \"o\": o}\n",
"\n",
"\n",
"def splice_score(img):\n",
" \"\"\"ํฉ์ฑ(๋ถ์ฌ๋ฃ๊ธฐ) ํ์ : 8x8 ๋ธ๋ก๋ณ ๊ณ ์ฃผํ ์ก์ ํฌ๊ธฐ์ ์ต๋/์ค์๊ฐ ๋น์จ. ์ก์ ์์ค์ด ๋ค๋ฅธ ์กฐ๊ฐ์ด ์์ผ๋ฉด ์ปค์ง๋ค.\"\"\"\n",
" res = img - ndimage.median_filter(img, 3)\n",
" b = np.abs(res).reshape(4, 8, 4, 8).std(axis=(1, 3))\n",
" return float(b.max() / (np.median(b) + 1e-6))\n",
"\n",
"\n",
"def ai_image_score(img):\n",
" \"\"\"์์ฑํ ์ด๋ฏธ์ง ํ์ : 2ํฝ์
์ฃผ๊ธฐ ๊ฒฉ์(๋์ดํด์คํธ ์ฃผํ์) ์๋์ง ๋น์จ.\"\"\"\n",
" f = np.abs(np.fft.fft2(img - img.mean()))\n",
" return float(f[16, 16] / (f.sum() / f.size + 1e-6))\n",
"\n",
"\n",
"def ai_text_score(text):\n",
" \"\"\"๊ธฐ๊ณ์ ์ ๋ ๋ฌธ์ฒด ํ์ : ๋ฐ๋ณต ์ด์ ๋น์จ + ๋๋ํ ๋ฐ๋.\"\"\"\n",
" w = text.split()\n",
" return (1 - len(set(w)) / max(len(w), 1)) + text.count(\"!\") / max(len(w), 1)\n",
"\n",
"# ---- ๊ฒ์๋ฌผ ๊ตฐ์ง(๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ)ยท์กฐ์ ํ์ง ํน์ง ๊ณตํต ํจ์ ----\n",
"FEAT = [\"num_dev\", \"region_mis\", \"dir_mis\", \"obj_mis\", \"img_ref_dist\", \"img_prior_h\", \"time_gap_h\", \"splice\", \"ai_img\",\n",
" \"ai_text\", \"burst_n\", \"has_img\", \"is_fwd\", \"acc_age\", \"acc_followers\", \"acc_orig\", \"acc_past\", \"acc_cv\"]\n",
"\n",
"\n",
"def cluster_posts(posts, hours=24, ham_thr=8):\n",
" \"\"\"๊ฐ์ ์ด์ผ๊ธฐ ๋ฌถ๊ธฐ: ๋ณธ๋ฌธ์์ ๋ฝ์ (์ง์ญ, ๋์)์ด ๊ฐ๊ณ 24์๊ฐ ์์ด๋ฉด ์ฐ๊ฒฐํ๊ณ ,\n",
" ์ง๊ฐ ํด์๊ฐ ๊ฐ๊น์ด ์ด๋ฏธ์ง(๊ฐ์ ๋์)๋ ์ฐ๊ฒฐํ๋ค. ๊ธ ํํ์ด ๋ฌ๋ผ๋ ๋ด์ฉ ๋จ์๋ก ๋ฌถ์ธ๋ค.\"\"\"\n",
" texts = [clean_text(p[\"text\"]) for p in posts]\n",
" X = TfidfVectorizer(analyzer=\"char_wb\", ngram_range=(2, 3)).fit_transform(texts)\n",
" S = (X @ X.T).toarray()\n",
" H = [phash(p[\"image\"]) if p[\"image\"] is not None else None for p in posts]\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" t = [p[\"posted_at\"] for p in posts]\n",
" parent = list(range(len(posts)))\n",
"\n",
" def find(i):\n",
" while parent[i] != i:\n",
" parent[i] = parent[parent[i]]; i = parent[i]\n",
" return i\n",
" for i in range(len(posts)):\n",
" for j in range(i + 1, len(posts)):\n",
" if abs(t[i] - t[j]) >= hours or sl[i][\"o\"] != sl[j][\"o\"]:\n",
" continue\n",
" same_story = sl[i][\"r\"] is not None and sl[i][\"r\"] == sl[j][\"r\"]\n",
" same_img = H[i] is not None and H[j] is not None and hamming(H[i], H[j]) <= ham_thr\n",
" if same_story or same_img:\n",
" parent[find(i)] = find(j)\n",
" return np.array([find(i) for i in range(len(posts))]), S, H\n",
"\n",
"\n",
"def post_features(posts, acc):\n",
" cid, S, H = cluster_posts(posts)\n",
" t = np.array([p[\"posted_at\"] for p in posts])\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" first_seen = {}\n",
" for i in np.argsort(t): # ์ด๋ฏธ์ง๊ฐ ์ฒ์ ๋ฑ์ฅํ ์๊ฐ๊ณผ ๊ตฐ์ง\n",
" if H[i] is None:\n",
" continue\n",
" key = next((k for k in first_seen if hamming(np.array(k), H[i]) <= 6), None)\n",
" if key is None:\n",
" first_seen[tuple(H[i])] = (t[i], cid[i])\n",
" elif first_seen[key][1] != cid[i]:\n",
" pass\n",
" ref = {c: int(np.argmin(np.where(cid == c, t, np.inf))) for c in set(cid)} # ๊ตฐ์ง์์ ๊ฐ์ฅ ์ด๋ฅธ ๊ฒ์๋ฌผ = ๊ธฐ์ค(์ถ์ ์๋ณธ)\n",
" F = []\n",
" for i, p in enumerate(posts):\n",
" r = ref[cid[i]]\n",
" a, b = sl[i], sl[r]\n",
" img_prior = 0.0\n",
" if H[i] is not None:\n",
" for k, (ts, c) in first_seen.items():\n",
" if hamming(np.array(k), H[i]) <= 6 and c != cid[i]:\n",
" img_prior = max(img_prior, min(t[i] - ts, 48.0))\n",
" same_txt = [j for j in np.where(S[i] > 0.97)[0] if abs(t[j] - t[i]) < 0.25 and posts[j][\"account_id\"] != p[\"account_id\"]]\n",
" A = acc[p[\"account_id\"]]\n",
" F.append([abs(np.log((a[\"n\"] or 1) / (b[\"n\"] or 1))), float(a[\"r\"] != b[\"r\"]), float(a[\"d\"] != b[\"d\"]), float(a[\"o\"] != b[\"o\"]),\n",
" hamming(H[i], H[r]) if H[i] is not None and H[r] is not None else 0, img_prior, t[i] - t[r],\n",
" splice_score(p[\"image\"]) if p[\"image\"] is not None else 0, ai_image_score(p[\"image\"]) if p[\"image\"] is not None else 0,\n",
" ai_text_score(p[\"text\"]), len(set(posts[j][\"account_id\"] for j in same_txt)), float(p[\"image\"] is not None),\n",
" float(p[\"forwarded_from\"] is not None), np.log1p(A[\"age_days\"]), np.log1p(A[\"followers\"]), A[\"original_ratio\"],\n",
" A[\"past_accuracy\"], A[\"interval_cv\"]])\n",
" return np.array(F), cid\n",
"\n",
"CONFIG = {\"threshold\": 0.3, \"use_account\": True, \"normalize\": True, # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฐ\n",
" \"weights\": [0.35, 0.20, 0.25, 0.10, 0.10], \"grades\": [(0.80, \"A\"), (0.65, \"B\"), (0.50, \"C\"), (0.35, \"D\"), (-1, \"E\")]}\n",
"HOMO = {\"A\": \"ะ\", \"B\": \"ะ\", \"C\": \"ะก\", \"E\": \"ะ\"}\n",
"ZW = \"โ\"\n",
"CALLED = set()\n",
"\n",
"\n",
"def cover(fn):\n",
" \"\"\"๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ์ฉ ํ์: ํตํฉ ์คํ ์ค ํธ์ถ๋ ๊ธฐ๋ฅ์ ๊ธฐ๋กํ๋ค.\"\"\"\n",
" def wrap(*a, **k):\n",
" CALLED.add(fn.__name__)\n",
" return fn(*a, **k)\n",
" wrap.__name__ = fn.__name__\n",
" return wrap\n",
"\n",
"\n",
"@cover\n",
"def normalize(t):\n",
" t = unicodedata.normalize(\"NFKC\", t).replace(ZW, \"\")\n",
" for k, v in HOMO.items():\n",
" t = t.replace(v, k)\n",
" return t\n",
"\n",
"\n",
"_slots, _clean = slots, clean_text\n",
"\n",
"\n",
"@cover\n",
"def slots(t):\n",
" return _slots(normalize(t))\n",
"\n",
"\n",
"@cover\n",
"def clean_text(t):\n",
" return _clean(normalize(t))\n",
"\n",
"\n",
"phash, cluster_posts, post_features = cover(phash), cover(cluster_posts), cover(post_features)\n",
"\n",
"\n",
"@cover\n",
"def validate_post(p):\n",
" errs = []\n",
" if p[\"platform\"] not in (\"telegram\", \"x\", \"other\"):\n",
" errs.append(\"platform ํ์ฉ๊ฐ ์๋\")\n",
" if not isinstance(p[\"text\"], str) or not p[\"text\"]:\n",
" errs.append(\"๋ณธ๋ฌธ ํ์ ์ค๋ฅ\")\n",
" if p[\"image\"] is not None and np.asarray(p[\"image\"]).shape != (32, 32):\n",
" errs.append(\"์ด๋ฏธ์ง ํ์ ์ค๋ฅ\")\n",
" return errs\n",
"\n",
"\n",
"@cover\n",
"def credibility(posts, F, cid, pm, am):\n",
" sl = [slots(p[\"text\"]) for p in posts]\n",
" corro = [min(len({posts[j][\"account_id\"] for j in np.where(cid == cid[i])[0] if j != i and posts[j][\"forwarded_from\"] is None\n",
" and posts[j][\"account_id\"] != p[\"account_id\"] and (sl[j][\"r\"], sl[j][\"n\"], sl[j][\"d\"]) == (sl[i][\"r\"], sl[i][\"n\"], sl[i][\"d\"])}) / 2, 1)\n",
" for i, p in enumerate(posts)]\n",
" C = np.c_[1 - pm, 1 - am, corro, 1 - np.minimum(F[:, FEAT.index(\"burst_n\")] / 3, 1), np.exp(-np.maximum(F[:, FEAT.index(\"time_gap_h\")], 0) / 12)]\n",
" s = C @ np.array(CONFIG[\"weights\"])\n",
" return s, np.array([next(g for t, g in CONFIG[\"grades\"] if v >= t) for v in s])\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" @cover\n",
" def submit(self, pid, grade, prob):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", \"id\": pid, \"grade\": grade, \"prob\": round(float(prob), 3)}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({**body, \"status\": \"PENDING\"})\n",
"\n",
" @cover\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",
"TARGETS = {\"normalize\", \"slots\", \"clean_text\", \"phash\", \"cluster_posts\", \"post_features\", \"validate_post\", \"credibility\", \"submit\", \"verify\", \"dashboard\"}\n",
"\n",
"# ---------------- 1. ํ์ต ----------------\n",
"t_start = time.perf_counter()\n",
"tr_posts, tr_acc = make_posts(60, seed=7)\n",
"Ftr, _ = post_features(tr_posts, tr_acc)\n",
"ytr = [p[\"is_disinfo\"] for p in tr_posts]\n",
"det = RandomForestClassifier(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Ftr, ytr)\n",
"manip = RandomForestClassifier(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Ftr[:, :13], ytr)\n",
"accm = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)).fit(Ftr[:, 13:], ytr)\n",
"\n",
"# ---------------- 2. ์ด์ฉ ์๋ฃ(๊ฐ์ ๊ฒ์๋ฌผ ์คํธ๋ฆผ) ์ฒ๋ฆฌ ----------------\n",
"rng = np.random.default_rng(31)\n",
"posts, acc = make_posts(60, seed=31)\n",
"good = [a for a, v in acc.items() if v[\"type\"] in (\"media\", \"witness\")]\n",
"for p in posts: # ์ ์ข
๋ณ์กฐ ํจํด ์ผ๋ถ ํฌํจ\n",
" if p[\"is_disinfo\"] and rng.random() < 0.3:\n",
" p[\"account_id\"] = str(rng.choice(good))\n",
" if p[\"is_disinfo\"] and rng.random() < 0.4:\n",
" p[\"text\"] = re.sub(r\"([A-F])-\", lambda m: HOMO.get(m.group(1), m.group(1)) + ZW + \"-\", p[\"text\"])\n",
"posts[11][\"text\"] = 404 # ์์ ๊ฒ์๋ฌผ 2๊ฑด ์ฃผ์
(๊ฒ์ฆ ๋จ๊ณ์์ ์ฐจ๋จ๋์ด์ผ ํจ)\n",
"posts[23][\"platform\"] = \"unknown_app\"\n",
"rejected = {}\n",
"ok_posts = []\n",
"for p in posts:\n",
" e = validate_post(p)\n",
" if e:\n",
" rejected[p[\"post_id\"]] = e\n",
" else:\n",
" ok_posts.append(p)\n",
"t0 = time.perf_counter()\n",
"F, cid = post_features(ok_posts, acc)\n",
"prob = det.predict_proba(F)[:, 1]\n",
"flag = prob >= CONFIG[\"threshold\"]\n",
"score, G = credibility(ok_posts, F, cid, manip.predict_proba(F[:, :13])[:, 1], accm.predict_proba(F[:, 13:])[:, 1])\n",
"ms = (time.perf_counter() - t0) / len(ok_posts) * 1000\n",
"queue = Queue()\n",
"for p, f, g, pr in zip(ok_posts, flag, G, prob):\n",
" if f or g in (\"D\", \"E\"):\n",
" queue.submit(p[\"post_id\"], g, pr) # ์ฐจ๋จยท์ฑํ์ ์ ๋ณด๊ด๋ง(์๋ ์ฒ๋ฆฌ ์์)\n",
"y, kind = np.array([p[\"is_disinfo\"] for p in ok_posts]), np.array([p[\"kind\"] for p in ok_posts])\n",
"acc_all, rec, far = (flag == y).mean(), (flag & y).sum() / y.sum(), (flag & ~y).sum() / (~y).sum()\n",
"\n",
"\n",
"# ---------------- 3. ๋ชจ๋ํฐ๋ง ํ๋ฉด ----------------\n",
"@cover\n",
"def dashboard():\n",
" plt.style.use(\"dark_background\")\n",
" fig, ax = plt.subplots(2, 2, figsize=(12, 9))\n",
" t = np.array([p[\"posted_at\"] for p in ok_posts])\n",
" for gset, c, lab in [((\"A\", \"B\"), \"limegreen\", \"A-B\"), ((\"C\",), \"orange\", \"C\"), ((\"D\", \"E\"), \"red\", \"D-E\")]:\n",
" ax[0, 0].hist(t[np.isin(G, gset)], bins=24, alpha=.7, color=c, label=lab)\n",
" ax[0, 0].set_title(\"Posts over time by credibility grade\"); ax[0, 0].legend()\n",
" gc = pd.Series(G).value_counts().reindex(list(\"ABCDE\")).fillna(0)\n",
" ax[0, 1].bar(gc.index.tolist(), gc.values, color=[\"limegreen\", \"limegreen\", \"orange\", \"red\", \"red\"]); ax[0, 1].set_title(\"Grade distribution\")\n",
" r = pd.Series(flag[y], index=kind[y]).groupby(level=0).mean()\n",
" ax[1, 0].barh(r.index.tolist(), r.values, color=\"deepskyblue\"); ax[1, 0].set_title(\"Detection recall by manipulation type\"); ax[1, 0].set_xlim(0, 1.05)\n",
" ax[1, 1].axis(\"off\")\n",
" ax[1, 1].text(0, .9, f\"Accuracy {acc_all:.1%}\\nRecall {rec:.1%}\\nFalse flags {far:.1%}\\nQueue {len(queue.items)} pending\\nLatency {ms:.2f} ms/post\",\n",
" family=\"monospace\", fontsize=13, va=\"top\")\n",
" fig.suptitle(\"ISR02 integrated SNS credibility system - synthetic data only\"); plt.tight_layout(); plt.show()\n",
"\n",
"\n",
"dashboard()\n",
"\n",
"# ---------------- 4. ์์ฒด ์ ๊ฒ(ํตํฉ ํ
์คํธ) ----------------\n",
"log_ok = queue.verify()\n",
"cov = len(CALLED & TARGETS) / len(TARGETS)\n",
"chk = {\n",
" \"์์ ๊ฒ์๋ฌผ ์ฐจ๋จ\": set(rejected) == {\"P0011\", \"P0023\"},\n",
" \"์ฒ๋ฆฌ ๊ฒ์๋ฌผ ์ ์ผ์น\": len(ok_posts) == len(posts) - 2,\n",
" \"์ข
ํฉ ์ ํ๋ 90% ์ด์\": acc_all >= 0.9,\n",
" \"์กฐ์ ํ์ง ์ฌํ์จ 85% ์ด์\": rec >= 0.85,\n",
" \"์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด 10% ์ดํ\": far <= 0.10,\n",
" \"์๋ ์ฐจ๋จยท์ฑํ 0๊ฑด(์ ๋ถ ๊ฒํ ๋๊ธฐ)\": all(i[\"status\"] == \"PENDING\" for i in queue.items),\n",
" \"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\": log_ok,\n",
" \"๋ฑ๊ธ ๊ท์น ์ผ๊ด\": all(g == next(gg for t, gg in CONFIG[\"grades\"] if s >= t) for s, g in zip(score, G)),\n",
" \"๊ฒ์๋ฌผ๋น 200ms ์ด๋ด\": ms < 200,\n",
" \"๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\": cov >= 0.85,\n",
"}\n",
"passed = sum(chk.values())\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {passed}/{len(chk)} ํต๊ณผ, ๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%} ({len(CALLED & TARGETS)}/{len(TARGETS)}), ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต {'์' if passed == len(chk) else '์๋์ค'}\")\n",
"\n",
"# ---------------- 5. ์ ๋ณด์ ๋์ ๋ธ๋ฆฌํ ์์ฝ ----------------\n",
"top = pd.Series(kind[flag & y]).value_counts().head(3).to_dict()\n",
"print(f\"\"\"\n",
"[SNS ์ ์ ๊ฒ์๋ฌผ ์ ๋ขฐ๋ ๋ถ๋ฅ ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ๊ฒ์๋ฌผ {len(posts)}๊ฑด ์ค {len(ok_posts)}๊ฑด ๋ถ์, ์์ {len(rejected)}๊ฑด ์ฐจ๋จ({', '.join(rejected)}), ์ด์ผ๊ธฐ ๊ตฐ์ง {len(set(cid))}๊ฐ\n",
"2. ์ฑ๋ฅ: ์ข
ํฉ ์ ํ๋ {acc_all:.1%}, ์กฐ์ ํ์ง ์ฌํ์จ {rec:.1%}, ์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด {far:.1%}, ๊ฒ์๋ฌผ๋น {ms:.2f} ms, ์ ์ฒด {time.perf_counter() - t_start:.0f} s\n",
"3. ๋ฑ๊ธ: A {int((G == 'A').sum())} / B {int((G == 'B').sum())} / C {int((G == 'C').sum())} / D {int((G == 'D').sum())} / E {int((G == 'E').sum())}, ์ ๋ณด๊ด ๊ฒํ ๋๊ธฐ {len(queue.items)}๊ฑด, ์๋ ์ฐจ๋จ 0๊ฑด\n",
"4. ์ฃผ์ ์กฐ์ ์ ํ: {top}\n",
"5. ํ๊ณ: ํฉ์ฑ ๊ฒ์๋ฌผ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก, ์ค์ ํ๋ซํผ ์๋ฃ์๋ ์ฌํ์ตยท์ฌ๊ฒ์ฆ์ด ํ์ํ๋ฉฐ ์ฐจ๋จยท์ฑํ์ ์์ฒ ์ญ์ถ์ ์ ๊ฑฐ์น ์ ๋ณด๊ด ๊ฒฐ์ ์ผ๋ก๋ง ํ๋ค.\"\"\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[PASS] ์์ ๊ฒ์๋ฌผ ์ฐจ๋จ\n",
"[PASS] ์ฒ๋ฆฌ ๊ฒ์๋ฌผ ์ ์ผ์น\n",
"[PASS] ์ข
ํฉ ์ ํ๋ 90% ์ด์\n",
"[PASS] ์กฐ์ ํ์ง ์ฌํ์จ 85% ์ด์\n",
"[PASS] ์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด 10% ์ดํ\n",
"[PASS] ์๋ ์ฐจ๋จยท์ฑํ 0๊ฑด(์ ๋ถ ๊ฒํ ๋๊ธฐ)\n",
"[PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\n",
"[PASS] ๋ฑ๊ธ ๊ท์น ์ผ๊ด\n",
"[PASS] ๊ฒ์๋ฌผ๋น 200ms ์ด๋ด\n",
"[PASS] ๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"์์ฒด ์ ๊ฒ 10/10 ํต๊ณผ, ๊ธฐ๋ฅ ์ปค๋ฒ๋ฆฌ์ง 100% (11/11), ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต ์\n",
"\n",
"[SNS ์ ์ ๊ฒ์๋ฌผ ์ ๋ขฐ๋ ๋ถ๋ฅ ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ๊ฒ์๋ฌผ 596๊ฑด ์ค 594๊ฑด ๋ถ์, ์์ 2๊ฑด ์ฐจ๋จ(P0011, P0023), ์ด์ผ๊ธฐ ๊ตฐ์ง 66๊ฐ\n",
"2. ์ฑ๋ฅ: ์ข
ํฉ ์ ํ๋ 94.4%, ์กฐ์ ํ์ง ์ฌํ์จ 93.2%, ์ฌ์ค ๊ฒ์๋ฌผ ์ค๊ฒฝ๋ณด 4.5%, ๊ฒ์๋ฌผ๋น 1.92 ms, ์ ์ฒด 3 s\n",
"3. ๋ฑ๊ธ: A 278 / B 52 / C 67 / D 102 / E 95, ์ ๋ณด๊ด ๊ฒํ ๋๊ธฐ 264๊ฑด, ์๋ ์ฐจ๋จ 0๊ฑด\n",
"4. ์ฃผ์ ์กฐ์ ์ ํ: {'bot_burst': 75, 'recycled': 26, 'region_swap': 24}\n",
"5. ํ๊ณ: ํฉ์ฑ ๊ฒ์๋ฌผ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก, ์ค์ ํ๋ซํผ ์๋ฃ์๋ ์ฌํ์ตยท์ฌ๊ฒ์ฆ์ด ํ์ํ๋ฉฐ ์ฐจ๋จยท์ฑํ์ ์์ฒ ์ญ์ถ์ ์ ๊ฑฐ์น ์ ๋ณด๊ด ๊ฒฐ์ ์ผ๋ก๋ง ํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}