Raise the clustering to 100 groups

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 22:38:31 +08:00
co-authored by Claude Opus 5
parent 8cdeb3db2e
commit 15f1843cde
6 changed files with 55 additions and 14 deletions
+18
View File
@@ -462,3 +462,21 @@
平台輸出過濾非隨機地排除了語料中最核心的一批歌,此 平台輸出過濾非隨機地排除了語料中最核心的一批歌,此
現象本身列入論文限制討論。標註步驟已入庫的兩次執行 現象本身列入論文限制討論。標註步驟已入庫的兩次執行
重製量約 43%,處置待議。 重製量約 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 命名對
大而異質的組必然失準,該碼在結果中呈現為零使用即是
儀器的誠實紀錄;改名等於在看過結果後調整儀器。
+18 -6
View File
@@ -37,12 +37,24 @@ Run、Song,一列一筆出現,依三欄排序),供收斂軌跡分析;
- **嵌入**`sentence-transformers/all-mpnet-base-v2` - **嵌入**`sentence-transformers/all-mpnet-base-v2`
(釘定 revision),關鍵字的連字號先還原為空格再編碼, (釘定 revision),關鍵字的連字號先還原為空格再編碼,
輸出 768 維向量並 L2 正規化。 輸出 768 維向量並 L2 正規化。
- **分群**:階層式聚合分群(Ward linkage),k=50。 - **分群**:階層式聚合分群(Ward linkage),k=100。
向量既已正規化,歐氏距離與餘弦相似度單調對應Ward 向量既已正規化,歐氏距離與餘弦相似度單調對應;三種
在保持語意距離的同時給出大小平衡的分割。 linkage 實測比較,Ward 於各個 k 的組內一致性均最高
average 與 complete 皆產生吞噬半數語料的巨大異質
組)。
- **組數的取捨**:k 太小則壓縮比過高,樹上層被迫併入
不相干的詞,組雖大而無主題(k=30 最大組 491 詞、
組內一致性 0.41,成員橫跨籃球、海灘、外星人綁架);
k 太大則人工難以通覽。定於 100,理由是實測顯示雜物櫃
組於此始裂解為有主題的組,且編碼實測未見碼數過多的
副作用(詞彙表外的碼歸零、僅一個碼零使用)。
- **組名**:取 medoid——與該組中心(成員向量均值後 - **組名**:取 medoid——與該組中心(成員向量均值後
正規化)餘弦相似度最高的成員詞。組名因此必為模型 正規化)餘弦相似度最高的成員詞。組名因此必為模型
自己產出過的關鍵字,非任何人事後撰寫。 自己產出過的關鍵字,非任何人事後撰寫。已知限制:
組越大越異質時,medoid 只是折衷詞,可能代表不了組內
內容(實測 `mutual-individuality` 組內一致 0.70 而
編碼從未使用);此類碼於結果中呈現為零使用,據實
報告,不事後改名。
- **取捨紀錄**:曾以 LLM 單發收斂(mergecap 兩步) - **取捨紀錄**:曾以 LLM 單發收斂(mergecap 兩步)
實作本步,四種模型六次執行全部無法維持完整分割, 實作本步,四種模型六次執行全部無法維持完整分割,
已棄用(詳見 `decision-log.md` 2026-08-05;棄用的 已棄用(詳見 `decision-log.md` 2026-08-05;棄用的
@@ -62,8 +74,8 @@ Run、Song,一列一筆出現,依三欄排序),供收斂軌跡分析;
### women-power 的注入 ### women-power 的注入
定案詞彙表為 50 個分群組名再加上 `women-power` 一詞, 定案詞彙表為 100 個分群組名再加上 `women-power` 一詞,
51 個碼。`women-power` 是研究者任意決定的先驗主題(即本 共 101 個碼。`women-power` 是研究者任意決定的先驗主題(即本
論文的主題本身),不由資料產生,屬揭露的儀器介入。該詞 論文的主題本身),不由資料產生,屬揭露的儀器介入。該詞
於執行時以 `--extra-keyword` 明示加入,不寫死在程式裏 於執行時以 `--extra-keyword` 明示加入,不寫死在程式裏
——研究者的介入因此每次都出現在重現命令上,而非無聲 ——研究者的介入因此每次都出現在重現命令上,而非無聲
+5 -4
View File
@@ -97,8 +97,9 @@
thematic keywords:前導研究三粒度比較(keywords 過碎、 thematic keywords:前導研究三粒度比較(keywords 過碎、
themes 過早抽象)之繼承,於執行前鎖定,防止事後擇優。 themes 過早抽象)之繼承,於執行前鎖定,防止事後擇優。
2. **詞彙表建構(步驟 2**:兩次執行的關鍵字取聯集去重 2. **詞彙表建構(步驟 2**:兩次執行的關鍵字取聯集去重
後,以句向量模型嵌入、階層式聚合分群,k=50,組名取 後,以句向量模型嵌入、階層式聚合分群,k=100,組名取
medoid;進池與分群為同一個確定性子命令。完整分割由演算法 medoid;進池與分群為同一個確定性子命令。完整分割由
演算法
結構保證。頻次不入收斂:頻率的分析角色由步驟 3 編碼 結構保證。頻次不入收斂:頻率的分析角色由步驟 3 編碼
承擔;池中頻次含跨執行噪音。 承擔;池中頻次含跨執行噪音。
3. **編碼(步驟 3**:以定稿詞彙表對全部歌曲 2+1 編碼 3. **編碼(步驟 3**:以定稿詞彙表對全部歌曲 2+1 編碼
@@ -110,7 +111,7 @@
扭曲,不作主結果。 扭曲,不作主結果。
**先驗主題詞 `women-power` 併入詞彙表**:定案詞彙表為 **先驗主題詞 `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/307/31 | | 0 | 基礎建設:git init、目錄結構、.gitignore、決策日誌、runner script(含 Batch API)、codebook v0 骨架 | script + 討論 | 7/307/31 |
| 1 | 資料準備:`run_llm` 改走統一設定 → `build-db`(解析榜單成 songs/chart_entries/artists/song_artists)→ `import-lyrics`pilot 20182025)→ `fetch-lyrics`201617 與缺漏,Lyrics.ovh / LRCLIB)→ `fetch-artists`Wikidata 快照)→ `export-llm-input` | 子命令 | 7/318/3 | | 1 | 資料準備:`run_llm` 改走統一設定 → `build-db`(解析榜單成 songs/chart_entries/artists/song_artists)→ `import-lyrics`pilot 20182025)→ `fetch-lyrics`201617 與缺漏,Lyrics.ovh / LRCLIB)→ `fetch-artists`Wikidata 快照)→ `export-llm-input` | 子命令 | 7/318/3 |
| 2 | 自然編碼管線:tag ×2 進池 → 詞向量分群 k=50 → 併入 women-power → 詞彙表定稿 → code 2+1(全 883 首,附引述) | API + script | 8/48/7 | | 2 | 自然編碼管線:tag ×2 進池 → 詞向量分群 k=100 → 併入 women-power → 詞彙表定稿 → code 2+1(全 883 首,附引述) | API + script | 8/48/7 |
| 3 | 黃金標準:依 codebook 人工逐首判定 genuine/peripheral/fake,附引用歌詞證據表(LLM 只做摘錄,不給判定建議);先以 10–15 首校準樣本試編並修訂 codebook 後凍結;同批校準樣本實測 Sonnet 4.6 vs Opus 5 一致率 | 人工 + script 輔助 | 8/58/9 | | 3 | 黃金標準:依 codebook 人工逐首判定 genuine/peripheral/fake,附引用歌詞證據表(LLM 只做摘錄,不給判定建議);先以 10–15 首校準樣本試編並修訂 codebook 後凍結;同批校準樣本實測 Sonnet 4.6 vs Opus 5 一致率 | 人工 + script 輔助 | 8/58/9 |
| 4 | 受控比較(盲點實驗):條件 A(詞彙層提示)vs 條件 B(框架感知提示),各 2+1,對照黃金標準計算假陽/假陰率 | API | 8/88/11 | | 4 | 受控比較(盲點實驗):條件 A(詞彙層提示)vs 條件 B(框架感知提示),各 2+1,對照黃金標準計算假陽/假陰率 | API | 8/88/11 |
| 4' | 映射分析:自然編碼結果(第 4 步)與黃金標準交叉表;軌跡對映 vs 直接編碼的扭曲診斷(分析方法先寫入 methodology.md 再看結果) | script | 與 4 並行 | | 4' | 映射分析:自然編碼結果(第 4 步)與黃金標準交叉表;軌跡對映 vs 直接編碼的扭曲診斷(分析方法先寫入 methodology.md 再看結果) | script | 與 4 並行 |
+2 -1
View File
@@ -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 | 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 | 引述減量實驗(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 | 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
@@ -40,7 +40,6 @@ from typing import Any
from ..utils import format_duration from ..utils import format_duration
MODEL: str = "sentence-transformers/all-mpnet-base-v2" MODEL: str = "sentence-transformers/all-mpnet-base-v2"
DEFAULT_CLUSTERS: int = 50
CLUSTER_EXTRA_MESSAGE: str = ( CLUSTER_EXTRA_MESSAGE: str = (
"cluster-keywords requires the optional \"cluster\"" "cluster-keywords requires the optional \"cluster\""
" dependency group; install it with" " dependency group; install it with"
@@ -101,8 +100,9 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
"--revision", default=None, "--revision", default=None,
help="the model revision to pin (default: unpinned)") help="the model revision to pin (default: unpinned)")
parser.add_argument( parser.add_argument(
"--clusters", type=int, default=DEFAULT_CLUSTERS, "--clusters", type=int, required=True,
help=f"the number of clusters (default {DEFAULT_CLUSTERS})") help="the number of clusters; required, so that the\n"
"group count is stated on every invocation")
parser.add_argument( parser.add_argument(
"--extra-keyword", dest="extra_keywords", action="append", "--extra-keyword", dest="extra_keywords", action="append",
default=None, default=None,
+9
View File
@@ -478,6 +478,15 @@ class TestClusterKeywords(unittest.TestCase):
["a-center", "aaa-extra", "b-middle", "zzz-extra"]) ["a-center", "aaa-extra", "b-middle", "zzz-extra"])
self.assertEqual(keywords, sorted(keywords)) 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: def test_duplicate_extra_keyword_rejected(self) -> None:
"""Test that repeating the same ``--extra-keyword`` value """Test that repeating the same ``--extra-keyword`` value
fails the run without writing any output file.""" fails the run without writing any output file."""