{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [ISR02] SNS ์ „์Ÿ ๊ฒŒ์‹œ๋ฌผ ์ค‘๋ณตยท๋ณ€์กฐยท์ถœ์ฒ˜ ์‹ ๋ขฐ๋„ ์ž๋™ ๋ถ„๋ฅ˜\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์ •๋ณดยท๊ฐ์‹œยท์ •์ฐฐ(ISR) / Level 2)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 1] ์ „์žฅ SNS(ํ…”๋ ˆ๊ทธ๋žจ, X ๋“ฑ) ํ…์ŠคํŠธ/์ด๋ฏธ์ง€ ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ง‘ ๋ฐ OSINT ์›์ฒœ ๋ฐ์ดํ„ฐ์…‹ ์ •์˜ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def validate(rec):\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 1] ์ „์žฅ SNS(ํ…”๋ ˆ๊ทธ๋žจ, X ๋“ฑ) ํ…์ŠคํŠธ/์ด๋ฏธ์ง€ ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ง‘ ๋ฐ OSINT ์›์ฒœ ๋ฐ์ดํ„ฐ์…‹ ์ •์˜ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 2] ๊ฒŒ์‹œ๋ฌผ ํ…์ŠคํŠธ ๋ถˆ์šฉ์–ด ์ •์ œ, ์ด๋ฏธ์ง€ ํ•ด์‹œ(pHash) ์ถ”์ถœ ๋ฐ ์ค‘๋ณต ์ œ๊ฑฐ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 2] ๊ฒŒ์‹œ๋ฌผ ํ…์ŠคํŠธ ๋ถˆ์šฉ์–ด ์ •์ œ, ์ด๋ฏธ์ง€ ํ•ด์‹œ(pHash) ์ถ”์ถœ ๋ฐ ์ค‘๋ณต ์ œ๊ฑฐ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 3] ๊ฒŒ์‹œ๋ฌผ ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ๋‹จ์ˆœ ๋ณต์ œ๋ณธ ํƒ์ง€ ๋ฐ 1์ฐจ ์ถœ์ฒ˜ ๋ถ„๋ฅ˜ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 3] ๊ฒŒ์‹œ๋ฌผ ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ๋‹จ์ˆœ ๋ณต์ œ๋ณธ ํƒ์ง€ ๋ฐ 1์ฐจ ์ถœ์ฒ˜ ๋ถ„๋ฅ˜ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 4] ์ƒ์„ฑํ˜• AI ์กฐ์ž‘ ์ด๋ฏธ์ง€/ํ…์ŠคํŠธ ํƒ์ง€ ๋ฐ ์ถœ์ฒ˜ ๊ณ„์ • ์‹ ๋ขฐ๋„ ํ‰๊ฐ€ ๋ชจ๋ธ ๊ณ ๋„ํ™” ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 4] ์ƒ์„ฑํ˜• AI ์กฐ์ž‘ ์ด๋ฏธ์ง€/ํ…์ŠคํŠธ ํƒ์ง€ ๋ฐ ์ถœ์ฒ˜ ๊ณ„์ • ์‹ ๋ขฐ๋„ ํ‰๊ฐ€ ๋ชจ๋ธ ๊ณ ๋„ํ™” ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 5] ๊ฐ€์งœ ์ •๋ณด(Disinformation) ์˜ค๋ถ„๋ฅ˜ ๋ฐฉ์ง€ ๋ฐ ๋ณตํ•ฉ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ์—”์ง„ ๊ฐœ๋ฐœ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 5] ๊ฐ€์งœ ์ •๋ณด(Disinformation) ์˜ค๋ถ„๋ฅ˜ ๋ฐฉ์ง€ ๋ฐ ๋ณตํ•ฉ ์‹ ๋ขฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ์—”์ง„ ๊ฐœ๋ฐœ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 6] SNS ์ „์Ÿ ์ •๋ณด ํƒ€์ž„๋ผ์ธ ๋ฐ ์ถœ์ฒ˜ ์‹ ๋ขฐ๋„ ๋„คํŠธ์›Œํฌ ์‹œ๊ฐํ™” ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 6] SNS ์ „์Ÿ ์ •๋ณด ํƒ€์ž„๋ผ์ธ ๋ฐ ์ถœ์ฒ˜ ์‹ ๋ขฐ๋„ ๋„คํŠธ์›Œํฌ ์‹œ๊ฐํ™” ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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": [ "# [TODO : ํ•™์ƒ ์‹ค์Šต 7] ์ „์ˆ ์  ํŒŒ๊ธ‰๋ ฅ์ด ํฐ ๊ฐ€์งœ ์ •๋ณด ์‹๋ณ„ ์‹œ ์ •๋ณด๊ด€ HITL ๊ธด๊ธ‰ ์Šน์ธ/์ฐจ๋‹จ ์›Œํฌํ”Œ๋กœ์šฐ ์„ค๊ณ„\n", "# ๋ฏธ์…˜ ์ง€์นจ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€์„ ํ™•์ธํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ ๋ฐ ํŠœ๋‹ํ•˜์„ธ์š”.\n", "\"\"\"๊ตฐ์‚ฌ์  ํŒŒ๊ธ‰๋ ฅ์ด ํฐ 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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 8] ์„ ์ „์ „ ํญ์ฆ ๋ชจ์˜ ๋Œ€๋Ÿ‰ SNS ์ŠคํŠธ๋ฆผ ๋ฐ์ดํ„ฐ ์œ ์ž… ๋ถ€ํ•˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 8] ์„ ์ „์ „ ํญ์ฆ ๋ชจ์˜ ๋Œ€๋Ÿ‰ SNS ์ŠคํŠธ๋ฆผ ๋ฐ์ดํ„ฐ ์œ ์ž… ๋ถ€ํ•˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 9] AAR ๊ธฐ๋ฐ˜ ์ถœ์ฒ˜ ์˜ค๋ถ„๋ฅ˜ ์‚ฌ๋ก€ ๋ถ„์„ ๋ฐ ๊ฐ€์งœ ์ •๋ณด ํƒ์ง€ ๋ฃฐ์…‹/๊ฐ€์ค‘์น˜ ์žฌ๋ณด์ • ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 9] AAR ๊ธฐ๋ฐ˜ ์ถœ์ฒ˜ ์˜ค๋ถ„๋ฅ˜ ์‚ฌ๋ก€ ๋ถ„์„ ๋ฐ ๊ฐ€์งœ ์ •๋ณด ํƒ์ง€ ๋ฃฐ์…‹/๊ฐ€์ค‘์น˜ ์žฌ๋ณด์ • ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 10] SNS ์ „์Ÿ ๊ฒŒ์‹œ๋ฌผ ์‹ ๋ขฐ๋„ ์ž๋™ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ๋ฐ ์ •๋ณด์ „ ๋Œ€์‘ ๋ธŒ๋ฆฌํ•‘ ํŒจํ‚ค์ง€ ์™„์„ฑ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def variant(img, rng):\n", " \"\"\"์žฌ๊ฒŒ์‹œ ๊ณผ์ •์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณ€ํ˜•: ์žฌ์••์ถ• ์žก์Œ, ๋ฐ๊ธฐ ๋ณ€ํ™”, ๊ฐ€์žฅ์ž๋ฆฌ ์ž˜๋ฆผ ์ค‘ ํ•˜๋‚˜.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 10] SNS ์ „์Ÿ ๊ฒŒ์‹œ๋ฌผ ์‹ ๋ขฐ๋„ ์ž๋™ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ๋ฐ ์ •๋ณด์ „ ๋Œ€์‘ ๋ธŒ๋ฆฌํ•‘ ํŒจํ‚ค์ง€ ์™„์„ฑ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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 }