<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Similarity on Tarragon</title><link>https://tarrragon.github.io/blog/tags/similarity/</link><description>Recent content in Similarity on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/similarity/index.xml" rel="self" type="application/rss+xml"/><item><title>相同 ISBN 的兩本書、相似度只有 0.67 — 加權平均稀釋 identity 訊號</title><link>https://tarrragon.github.io/blog/work-log/flutter_weighted_average_dilutes_identity_signal/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/flutter_weighted_average_dilutes_identity_signal/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：Flutter 書籍管理 App 的書目比對——判斷 API 查回來的書跟館藏裡的書是不是同一本。測試給了兩本 &lt;strong>ISBN 完全相同&lt;/strong>的書，相似度算出 0.67、低於 0.8 的匹配閾值：系統認為「不確定是同一本」
&lt;strong>疑問來源&lt;/strong>：ISBN 相同幾乎就是同一本書的定義，演算法哪裡把這個確定性弄丟了？
&lt;strong>整理目的&lt;/strong>：記下加權平均對異質訊號的稀釋機制、訊號分級的修法、以及 identity 比對前的正規化前置
&lt;strong>本文邊界&lt;/strong>：素材是該專案 v0.6.1 的重構記錄；「identity 訊號 vs fuzzy 訊號」的分級思路通用於任何比對 / 評分系統&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="稀釋機制平均假設訊號同質但訊號不同質">稀釋機制：平均假設訊號同質、但訊號不同質&lt;/h2>
&lt;p>原始的相似度是一條加權和：ISBN、標題、作者、出版社各占一個權重、各算一個相似度、加權平均出總分。這個結構對&lt;strong>同質訊號&lt;/strong>成立——四個都是「有點像」的模糊證據時，加權平均是合理的融合。&lt;/p>
&lt;p>但 ISBN 不是模糊證據。它是 &lt;strong>identity 級訊號&lt;/strong>：兩本書 ISBN 相同、是同一本書的先驗機率接近 1——這是這個編號存在的目的。把它丟進平均池的後果：ISBN 完全匹配貢獻它權重上限的分數、然後被「標題寫法不同」「出版社欄位缺失」這些 fuzzy 訊號的低分&lt;strong>往下拉&lt;/strong>——確定性被稀釋成 0.67。這個數字的含義很難堪：系統對「ISBN 相同的兩本書」的信心、只比「標題有點像的兩本書」高一點。&lt;/p>
&lt;p>調閾值救不了它：降到 0.65 會讓真匹配過關、也讓一批「標題很像但不是同一本」的假陽性一起過關——問題不在線畫哪裡、在&lt;strong>兩種確定性被壓進了同一個刻度&lt;/strong>。&lt;/p>
&lt;h2 id="修法identity-訊號短路fuzzy-訊號才進池">修法：identity 訊號短路、fuzzy 訊號才進池&lt;/h2>
&lt;p>重構把計分拆成兩層、兩個函式各司其職：&lt;/p>
&lt;ul>
&lt;li>&lt;code>_calculateIsbnPriorityScore&lt;/code>——&lt;strong>identity 層&lt;/strong>：ISBN 完全匹配（含等價形式）時直接給 0.9 以上的基礎分、不進加權池。確定的事實用確定的分數表達&lt;/li>
&lt;li>&lt;code>_calculateWeightedSimilarity&lt;/code>——&lt;strong>fuzzy 層&lt;/strong>：ISBN 不可比（缺失、不匹配）時才落到這裡，標題、作者、出版社的加權平均在同質訊號之間做它擅長的事&lt;/li>
&lt;/ul>
&lt;p>結構化之後、分數重新有了語意：0.9 以上讀作「identity 證據確認」、中間段讀作「fuzzy 證據的綜合強度」——下游要對兩種情況做不同處置（自動合併 vs 請使用者確認）時，分數本身就攜帶了依據。這個「強訊號短路、弱訊號加權」的形狀在別處反覆出現：規則引擎的 hard rule 先於 scoring、搜尋的 exact match 先於 ranking——凡是訊號有等級的地方，先分級再融合。&lt;/p>
&lt;h2 id="前置identity-訊號自己要先正規化">前置：identity 訊號自己要先正規化&lt;/h2>
&lt;p>第二個修正堵住 identity 層自己的漏接：同一本書的 ISBN 有兩種合法表示——ISBN-10 跟 ISBN-13（978 前綴加重算校驗碼）。等價檢查沒實作時（重構前它是一條 TODO 註解），一邊是 10 碼一邊是 13 碼的同一本書、在 identity 層比對失敗、跌進 fuzzy 層被當成「標題很像的兩本書」。&lt;/p>
&lt;p>原則跟 &lt;a href="https://tarrragon.github.io/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 與 VO 的相等性&lt;/a>相通：&lt;strong>同一身分的多種表示、比對前先化到正規形式&lt;/strong>。identity 訊號的價值建立在「相等判斷可靠」上，表示層的多樣性沒被吸收掉、identity 層就會系統性漏接——而且漏得很安靜，每一筆都降級成 fuzzy 比對、只是分數低一點。&lt;/p>
&lt;p>附帶的第三個修正也值得一行：原本自製的「簡化版 Jaro-Winkler」字串相似度被記錄為「過於簡化、無法準確計算」——fuzzy 層的訊號品質同樣要顧，自製知名演算法的簡化版、通常是拿演算法的名字背書一個不是它的東西。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>「明明是同一個東西、比對分數卻不高」——查有沒有 identity 級欄位被丟進加權平均&lt;/li>
&lt;li>評分公式裡確定性訊號（唯一編號、hash、精確鍵）跟模糊訊號（字串相似、日期接近）共用權重——先分級、讓確定訊號短路&lt;/li>
&lt;li>identity 欄位有多種合法表示（含連字號 / 不含、10 碼 / 13 碼、大小寫）而比對是裸字串相等——正規化缺席、identity 層在漏接&lt;/li>
&lt;li>調閾值的討論反覆出現——通常是訊號結構的問題假扮成閾值問題&lt;/li>
&lt;/ul>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>正規形式的型別層做法：&lt;a href="https://tarrragon.github.io/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 三段遷移&lt;/a>——同一身分的表示多樣性在 value object 層吸收&lt;/li>
&lt;li>identity 的建模：&lt;a href="https://tarrragon.github.io/blog/work-log/dart_pos_item_four_lifecycle_models/" data-link-title="同一個品項、四個 model — value object 什麼時候該升級成 entity" data-link-desc="同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例，含 snapshot 與 live reference 的凍結時機。">同一個品項、四個 model&lt;/a>——「什麼算同一個」是 domain 定義、比對演算法只是它的執行者&lt;/li>
&lt;li>概念地基：&lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a>——「相等性定義承載業務規則」段&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：Flutter 書籍管理 App 的書目比對——判斷 API 查回來的書跟館藏裡的書是不是同一本。測試給了兩本 <strong>ISBN 完全相同</strong>的書，相似度算出 0.67、低於 0.8 的匹配閾值：系統認為「不確定是同一本」
<strong>疑問來源</strong>：ISBN 相同幾乎就是同一本書的定義，演算法哪裡把這個確定性弄丟了？
<strong>整理目的</strong>：記下加權平均對異質訊號的稀釋機制、訊號分級的修法、以及 identity 比對前的正規化前置
<strong>本文邊界</strong>：素材是該專案 v0.6.1 的重構記錄；「identity 訊號 vs fuzzy 訊號」的分級思路通用於任何比對 / 評分系統</p></blockquote>
<hr>
<h2 id="稀釋機制平均假設訊號同質但訊號不同質">稀釋機制：平均假設訊號同質、但訊號不同質</h2>
<p>原始的相似度是一條加權和：ISBN、標題、作者、出版社各占一個權重、各算一個相似度、加權平均出總分。這個結構對<strong>同質訊號</strong>成立——四個都是「有點像」的模糊證據時，加權平均是合理的融合。</p>
<p>但 ISBN 不是模糊證據。它是 <strong>identity 級訊號</strong>：兩本書 ISBN 相同、是同一本書的先驗機率接近 1——這是這個編號存在的目的。把它丟進平均池的後果：ISBN 完全匹配貢獻它權重上限的分數、然後被「標題寫法不同」「出版社欄位缺失」這些 fuzzy 訊號的低分<strong>往下拉</strong>——確定性被稀釋成 0.67。這個數字的含義很難堪：系統對「ISBN 相同的兩本書」的信心、只比「標題有點像的兩本書」高一點。</p>
<p>調閾值救不了它：降到 0.65 會讓真匹配過關、也讓一批「標題很像但不是同一本」的假陽性一起過關——問題不在線畫哪裡、在<strong>兩種確定性被壓進了同一個刻度</strong>。</p>
<h2 id="修法identity-訊號短路fuzzy-訊號才進池">修法：identity 訊號短路、fuzzy 訊號才進池</h2>
<p>重構把計分拆成兩層、兩個函式各司其職：</p>
<ul>
<li><code>_calculateIsbnPriorityScore</code>——<strong>identity 層</strong>：ISBN 完全匹配（含等價形式）時直接給 0.9 以上的基礎分、不進加權池。確定的事實用確定的分數表達</li>
<li><code>_calculateWeightedSimilarity</code>——<strong>fuzzy 層</strong>：ISBN 不可比（缺失、不匹配）時才落到這裡，標題、作者、出版社的加權平均在同質訊號之間做它擅長的事</li>
</ul>
<p>結構化之後、分數重新有了語意：0.9 以上讀作「identity 證據確認」、中間段讀作「fuzzy 證據的綜合強度」——下游要對兩種情況做不同處置（自動合併 vs 請使用者確認）時，分數本身就攜帶了依據。這個「強訊號短路、弱訊號加權」的形狀在別處反覆出現：規則引擎的 hard rule 先於 scoring、搜尋的 exact match 先於 ranking——凡是訊號有等級的地方，先分級再融合。</p>
<h2 id="前置identity-訊號自己要先正規化">前置：identity 訊號自己要先正規化</h2>
<p>第二個修正堵住 identity 層自己的漏接：同一本書的 ISBN 有兩種合法表示——ISBN-10 跟 ISBN-13（978 前綴加重算校驗碼）。等價檢查沒實作時（重構前它是一條 TODO 註解），一邊是 10 碼一邊是 13 碼的同一本書、在 identity 層比對失敗、跌進 fuzzy 層被當成「標題很像的兩本書」。</p>
<p>原則跟 <a href="/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 與 VO 的相等性</a>相通：<strong>同一身分的多種表示、比對前先化到正規形式</strong>。identity 訊號的價值建立在「相等判斷可靠」上，表示層的多樣性沒被吸收掉、identity 層就會系統性漏接——而且漏得很安靜，每一筆都降級成 fuzzy 比對、只是分數低一點。</p>
<p>附帶的第三個修正也值得一行：原本自製的「簡化版 Jaro-Winkler」字串相似度被記錄為「過於簡化、無法準確計算」——fuzzy 層的訊號品質同樣要顧，自製知名演算法的簡化版、通常是拿演算法的名字背書一個不是它的東西。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>「明明是同一個東西、比對分數卻不高」——查有沒有 identity 級欄位被丟進加權平均</li>
<li>評分公式裡確定性訊號（唯一編號、hash、精確鍵）跟模糊訊號（字串相似、日期接近）共用權重——先分級、讓確定訊號短路</li>
<li>identity 欄位有多種合法表示（含連字號 / 不含、10 碼 / 13 碼、大小寫）而比對是裸字串相等——正規化缺席、identity 層在漏接</li>
<li>調閾值的討論反覆出現——通常是訊號結構的問題假扮成閾值問題</li>
</ul>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>正規形式的型別層做法：<a href="/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 三段遷移</a>——同一身分的表示多樣性在 value object 層吸收</li>
<li>identity 的建模：<a href="/blog/work-log/dart_pos_item_four_lifecycle_models/" data-link-title="同一個品項、四個 model — value object 什麼時候該升級成 entity" data-link-desc="同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例，含 snapshot 與 live reference 的凍結時機。">同一個品項、四個 model</a>——「什麼算同一個」是 domain 定義、比對演算法只是它的執行者</li>
<li>概念地基：<a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a>——「相等性定義承載業務規則」段</li>
</ul>
]]></content:encoded></item></channel></rss>