From 15f1843cde3652410434d35944f1f8f035860e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Thu, 6 Aug 2026 05:55:11 +0800 Subject: [PATCH] Raise the clustering to 100 groups Co-Authored-By: Claude Opus 5 (1M context) --- docs/decision-log.md | 18 ++++++++++++++ docs/methodology.md | 24 ++++++++++++++----- docs/research-plan.md | 9 +++---- docs/run-costs.md | 3 ++- .../commands/cluster_keywords.py | 6 ++--- tools/tests/test_cluster_keywords.py | 9 +++++++ 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/docs/decision-log.md b/docs/decision-log.md index 180fc54..39d5aeb 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -462,3 +462,21 @@ 平台輸出過濾非隨機地排除了語料中最核心的一批歌,此 現象本身列入論文限制討論。標註步驟已入庫的兩次執行 重製量約 43%,處置待議。 +- **分群組數自 50 改為 100**:k=50 下 + 出現數個「雜物櫃組」——組內語意發散、medoid 只是折衷 + 詞,編碼實測幾乎不用(`mutual-individuality` 158 詞、 + 僅用於 2% 的歌;`fantasy-and-imagination` 232 詞含 + 籃球、海灘、神性意象)。k=30 更劣(最大組 491 詞、 + 組內一致 0.41)。改 linkage 無用:三種 linkage 於各個 + k 實測,Ward 的組內一致性均最高,average 與 complete + 反而產生吞噬半數語料的巨組。診斷為壓縮比過高——語料 + 的主題多樣性超過 50 種,非演算法選擇問題。裁定:k + 取 100,雜物櫃組於此始裂解為有主題的組。實測 148 首 + 探測:101 碼下詞彙表外的 + 碼由 3 降為 0、未使用碼由多個降為 1、每首碼數中位數 + 12→16、`women-power` 命中率 13%→9%(碼變細後邊緣 + 歌不再被歸入)。代價:每次編碼執行成本由 $3.96 升至 + 約 $7.0。 +- **零使用的碼據實報告,不事後改名**:medoid 命名對 + 大而異質的組必然失準,該碼在結果中呈現為零使用即是 + 儀器的誠實紀錄;改名等於在看過結果後調整儀器。 diff --git a/docs/methodology.md b/docs/methodology.md index f393f87..9800ba4 100644 --- a/docs/methodology.md +++ b/docs/methodology.md @@ -37,12 +37,24 @@ Run、Song,一列一筆出現,依三欄排序),供收斂軌跡分析; - **嵌入**:`sentence-transformers/all-mpnet-base-v2` (釘定 revision),關鍵字的連字號先還原為空格再編碼, 輸出 768 維向量並 L2 正規化。 -- **分群**:階層式聚合分群(Ward linkage),k=50。 - 向量既已正規化,歐氏距離與餘弦相似度單調對應,Ward - 在保持語意距離的同時給出大小平衡的分割。 +- **分群**:階層式聚合分群(Ward linkage),k=100。 + 向量既已正規化,歐氏距離與餘弦相似度單調對應;三種 + linkage 實測比較,Ward 於各個 k 的組內一致性均最高 + (average 與 complete 皆產生吞噬半數語料的巨大異質 + 組)。 +- **組數的取捨**:k 太小則壓縮比過高,樹上層被迫併入 + 不相干的詞,組雖大而無主題(k=30 最大組 491 詞、 + 組內一致性 0.41,成員橫跨籃球、海灘、外星人綁架); + k 太大則人工難以通覽。定於 100,理由是實測顯示雜物櫃 + 組於此始裂解為有主題的組,且編碼實測未見碼數過多的 + 副作用(詞彙表外的碼歸零、僅一個碼零使用)。 - **組名**:取 medoid——與該組中心(成員向量均值後 正規化)餘弦相似度最高的成員詞。組名因此必為模型 - 自己產出過的關鍵字,非任何人事後撰寫。 + 自己產出過的關鍵字,非任何人事後撰寫。已知限制: + 組越大越異質時,medoid 只是折衷詞,可能代表不了組內 + 內容(實測 `mutual-individuality` 組內一致 0.70 而 + 編碼從未使用);此類碼於結果中呈現為零使用,據實 + 報告,不事後改名。 - **取捨紀錄**:曾以 LLM 單發收斂(merge/cap 兩步) 實作本步,四種模型六次執行全部無法維持完整分割, 已棄用(詳見 `decision-log.md` 2026-08-05;棄用的 @@ -62,8 +74,8 @@ Run、Song,一列一筆出現,依三欄排序),供收斂軌跡分析; ### women-power 的注入 -定案詞彙表為 50 個分群組名再加上 `women-power` 一詞,共 -51 個碼。`women-power` 是研究者任意決定的先驗主題(即本 +定案詞彙表為 100 個分群組名再加上 `women-power` 一詞, +共 101 個碼。`women-power` 是研究者任意決定的先驗主題(即本 論文的主題本身),不由資料產生,屬揭露的儀器介入。該詞 於執行時以 `--extra-keyword` 明示加入,不寫死在程式裏 ——研究者的介入因此每次都出現在重現命令上,而非無聲 diff --git a/docs/research-plan.md b/docs/research-plan.md index 6e60445..4c62f57 100644 --- a/docs/research-plan.md +++ b/docs/research-plan.md @@ -97,8 +97,9 @@ thematic keywords:前導研究三粒度比較(keywords 過碎、 themes 過早抽象)之繼承,於執行前鎖定,防止事後擇優。 2. **詞彙表建構(步驟 2)**:兩次執行的關鍵字取聯集去重 - 後,以句向量模型嵌入、階層式聚合分群,k=50,組名取 - medoid;進池與分群為同一個確定性子命令。完整分割由演算法 + 後,以句向量模型嵌入、階層式聚合分群,k=100,組名取 + medoid;進池與分群為同一個確定性子命令。完整分割由 + 演算法 結構保證。頻次不入收斂:頻率的分析角色由步驟 3 編碼 承擔;池中頻次含跨執行噪音。 3. **編碼(步驟 3)**:以定稿詞彙表對全部歌曲 2+1 編碼 @@ -110,7 +111,7 @@ 扭曲,不作主結果。 **先驗主題詞 `women-power` 併入詞彙表**:定案詞彙表為 -50 個分群組名再加上 `women-power`,共 51 個碼。該詞是 +100 個分群組名再加上 `women-power`,共 101 個碼。該詞是 研究者任意決定的先驗主題,即本研究的論文主題本身,不由 資料產生,屬據實揭露的儀器介入。不另設單目標篩選軌,理由是 讓研究者的主題詞與模型自己收斂出的類別在同一份提示詞、 @@ -136,7 +137,7 @@ |---|---|---|---| | 0 | 基礎建設:git init、目錄結構、.gitignore、決策日誌、runner script(含 Batch API)、codebook v0 骨架 | script + 討論 | 7/30–7/31 | | 1 | 資料準備:`run_llm` 改走統一設定 → `build-db`(解析榜單成 songs/chart_entries/artists/song_artists)→ `import-lyrics`(pilot 2018–2025)→ `fetch-lyrics`(2016–17 與缺漏,Lyrics.ovh / LRCLIB)→ `fetch-artists`(Wikidata 快照)→ `export-llm-input` | 子命令 | 7/31–8/3 | -| 2 | 自然編碼管線:tag ×2 進池 → 詞向量分群 k=50 → 併入 women-power → 詞彙表定稿 → code 2+1(全 883 首,附引述) | API + script | 8/4–8/7 | +| 2 | 自然編碼管線:tag ×2 進池 → 詞向量分群 k=100 → 併入 women-power → 詞彙表定稿 → code 2+1(全 883 首,附引述) | API + script | 8/4–8/7 | | 3 | 黃金標準:依 codebook 人工逐首判定 genuine/peripheral/fake,附引用歌詞證據表(LLM 只做摘錄,不給判定建議);先以 10–15 首校準樣本試編並修訂 codebook 後凍結;同批校準樣本實測 Sonnet 4.6 vs Opus 5 一致率 | 人工 + script 輔助 | 8/5–8/9 | | 4 | 受控比較(盲點實驗):條件 A(詞彙層提示)vs 條件 B(框架感知提示),各 2+1,對照黃金標準計算假陽/假陰率 | API | 8/8–8/11 | | 4' | 映射分析:自然編碼結果(第 4 步)與黃金標準交叉表;軌跡對映 vs 直接編碼的扭曲診斷(分析方法先寫入 methodology.md 再看結果) | script | 與 4 並行 | diff --git a/docs/run-costs.md b/docs/run-costs.md index b7e19c3..fc4cf98 100644 --- a/docs/run-costs.md +++ b/docs/run-costs.md @@ -31,5 +31,6 @@ $3/$15、opus-4-6 $5/$25、opus-5 與 fable-5 $10/$50) | 2026-08-05 | 03-01-code | run1 | claude-sonnet-4-6 | msgbatch_01L7VepTxSNz5vruvzzjuL2c | 6 分 36 秒 | 1,247,264 | 492,730 | $5.57 | 36 首輸出遭內容過濾攔阻,待修訂重跑 | | 2026-08-05 | 03-01-code | 引述減量實驗(36 首)| claude-sonnet-4-6 | msgbatch_01Hskdhi2DkudYmgZhhgFts7 | 3 分 51 秒 | 56,458 | 11,074 | $0.17 | 實驗:每碼一行引述,36 首全數通過過濾;歸檔不入 repo | | 2026-08-05 | 03-01-code | run1 | claude-sonnet-4-6 | msgbatch_01GG5Ez9KT1pPwtQaY4sW7tv | 4 分 35 秒 | 1,293,724 | 269,166 | $3.96 | 過濾零攔阻;song-168、song-590 因餘額用盡失敗,song-775 拒答 | +| 2026-08-05 | 03-01-code | 101 碼樹狀探測(148 首)| claude-sonnet-4-6 | msgbatch_017jo3E5gWVks9b39iWMTqz3 | 2 小時 11 分 | 385,270 | 79,690 | $0.87 | 實驗:k=100 葉碼+women-power;零違規碼; 歸檔不入 repo | -累計支出:$43.40。 +累計支出:$44.27。 diff --git a/tools/src/pop_fem_audit_tools/commands/cluster_keywords.py b/tools/src/pop_fem_audit_tools/commands/cluster_keywords.py index 8255fb6..5005ae9 100644 --- a/tools/src/pop_fem_audit_tools/commands/cluster_keywords.py +++ b/tools/src/pop_fem_audit_tools/commands/cluster_keywords.py @@ -40,7 +40,6 @@ from typing import Any from ..utils import format_duration MODEL: str = "sentence-transformers/all-mpnet-base-v2" -DEFAULT_CLUSTERS: int = 50 CLUSTER_EXTRA_MESSAGE: str = ( "cluster-keywords requires the optional \"cluster\"" " dependency group; install it with" @@ -101,8 +100,9 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace: "--revision", default=None, help="the model revision to pin (default: unpinned)") parser.add_argument( - "--clusters", type=int, default=DEFAULT_CLUSTERS, - help=f"the number of clusters (default {DEFAULT_CLUSTERS})") + "--clusters", type=int, required=True, + help="the number of clusters; required, so that the\n" + "group count is stated on every invocation") parser.add_argument( "--extra-keyword", dest="extra_keywords", action="append", default=None, diff --git a/tools/tests/test_cluster_keywords.py b/tools/tests/test_cluster_keywords.py index defa919..15ee7e0 100644 --- a/tools/tests/test_cluster_keywords.py +++ b/tools/tests/test_cluster_keywords.py @@ -478,6 +478,15 @@ class TestClusterKeywords(unittest.TestCase): ["a-center", "aaa-extra", "b-middle", "zzz-extra"]) self.assertEqual(keywords, sorted(keywords)) + def test_missing_clusters_option_rejected(self) -> None: + """Test that omitting --clusters fails the run.""" + with self.assertRaises(SystemExit) as caught, \ + redirect_stderr(io.StringIO()): + cluster_keywords.parse_args( + [str(self.__run1), str(self.__run2), + str(self.__output_dir)]) + self.assertNotEqual(caught.exception.code, 0) + def test_duplicate_extra_keyword_rejected(self) -> None: """Test that repeating the same ``--extra-keyword`` value fails the run without writing any output file."""