跳至主要內容

    t-SNE 與 UMAP:漂亮的群集圖能證明什麼?

    從鄰居機率、模糊圖到參數敏感度,讀懂降維散點圖的證據邊界

    sunny 桑尼· · 28 MIN READ · 240 次瀏覽

    t-SNE 與 UMAP:漂亮的群集圖能證明什麼?
    本文目錄

    把一百個欄位壓成兩個座標,畫出幾座分開的小島,很容易讓人說:「我們找到四種客戶了。」但這張圖首先證明的,是某種降維程序在某組設定下,把這批資料排成了這個樣子。四座島是否對應四種穩定、有用的客戶類型,還需要回到原始特徵與獨立證據。

    t-SNE與UMAP都能幫我們探索高維資料的鄰域關係。它們適合用來提出問題:哪些樣本值得一起檢查?一個已知標籤是否在局部混在一起?某批異常資料是否值得追查?把圖當成探索入口,就能保留它的價值,也比較不容易把排版效果誤認為資料的本質。

    同一批資料,換個設定就可能換一張故事

    圖1|從左至右為原始資料及perplexity 5、30、50、100的輸出。官方範例用150點、內圈半徑比例0.5、雜訊0.05;資料與t-SNE的random_state皆為0,嵌入用random初始化、300次迭代。紅綠是生成時的內外圈標籤,只用來著色。來源:The scikit-learn developers,t-SNE perplexity原例,BSD-3-Clause。節錄原圖第1列,手機將原來五格依序上下排;不是本站重跑結果。

    先看左邊的兩個圓,再看右邊。perplexity很小時,圓被拆成多段;其他設定下,有的比較接近圓,有的出現扭轉。點沒有增加,原始資料也沒有變成另一種客戶。改變的是嵌入程序看待鄰域與安排平面的方式。

    圖中兩圈的間隔與面積不能直接讀成原始空間的距離與密度。即使某張圖看起來最像原始圓,也不能推論那個參數對所有資料都最好。接著換一種資料,就能看見限制。

    圖2|另一批150筆三維S曲線樣本。第一格只畫原始X的第0與第2欄,不是完整三維幾何;t-SNE實際接收三維輸入。顏色沿生成位置連續變化,不是離散群標籤。嵌入使用random初始化、random_state=0、learning_rate='auto'及300次迭代。來源同圖1;節錄第2列,手機五格直排。

    同樣是perplexity 30,在S曲線上得到的形狀與同心圓完全不同。連續色帶被拉長、折轉或分段,讓我們看見局部關係與整體形狀並非同一件事。不要因為輸出有幾段空白,就替它們取上幾個「真實族群」的名字。

    圖3|原始資料是12×12共144點的規則網格;雖然程式前面寫n_samples=150,取整數平方根建網格後實際只有144點。顏色表示原始x座標。這列t-SNE使用random初始化、random_state=0及400次迭代,與前兩列的300次不同。來源同圖1;節錄第3列,手機五格直排。

    網格沒有預先定義的幾個離散群,輸出仍可能呈現裂開的塊狀。這個反例足以提醒我們:圖上有空隙,未必代表原始資料也有同樣的空隙。它沒有反過來證明所有群集都是假的;它要求我們補上圖外的檢查。

    t-SNE保留的是鄰居關係的機率描述

    PCA用線性方向盡量保留變異,t-SNE的問題則接近:「原空間裡,i多常把j看成鄰居?能否讓平面上的關係也接近這個描述?」以下先用標準數學形式建立直覺;實務套件可以使用近似計算。

    對每個點i,以高斯形式把距離換成條件機率,排除自己:

    p(j|i) = exp(-||x_i-x_j||² / (2 sigma_i²))
             / sum_{k != i} exp(-||x_i-x_k||² / (2 sigma_i²))
    
    p_ij = (p(j|i) + p(i|j)) / (2n),  i != j
    

    距離越近,通常權重越大。但sigma_i會依各點的鄰域調整,並非所有地方都套一個相同半徑。因此高維空間裡兩個不同密度區域,最後可能都被攤成看似差不多大的島。

    perplexity是鄰居機率分布的有效規模,並不是要求畫出幾群。 用自然對數寫,它等於熵的指數:

    H_i = -sum_j p(j|i) log p(j|i)
    perplexity_i = exp(H_i)
    

    若恰好有三個鄰居各占1/3,熵是log(3),perplexity就是3。若許多點都分到一些權重,它仍不是「只看最近三個,其餘一律忽略」的硬切割。套件調整sigma,使各點的熵接近指定目標;scikit-learn的perplexity必須小於樣本數。

    低維空間改用具有較厚尾部的Student t形式,再對所有非自身的有序點對正規化:

    q_ij = (1 + ||y_i-y_j||²)^(-1)
           / sum_{k != l} (1 + ||y_k-y_l||²)^(-1)
    
    loss = KL(P || Q) = sum_{i != j} p_ij log(p_ij / q_ij)
    

    厚尾讓平面上比較遠的點仍可分配到一些相似性,有助於緩解把高維鄰域塞進低維時的擁擠。KL的方向也有意義:原空間裡重要的鄰居,如果在圖上被分配到太低的相似性,代價會增加。

    這個目標不等於最小化所有原始距離與平面距離的差,也不要求兩座島之間的空白忠實保留。不要把圖上「A到B比A到C遠兩倍」讀成原始特徵的差異也有兩倍。演算法的最佳化還會受初始化、學習率、迭代與隨機性影響;固定種子能幫助重現一次運算,無法替結論提供跨設定的穩定性。

    UMAP先建鄰域圖,再安排低維座標

    UMAP也從鄰居出發,但先把局部關係組成帶權重的圖,再學一個低維排列。下面這段官方示意用同一組帶雜訊的正弦曲線,逐步呈現建圖概念。這些點本來就畫在二維平面上;前四組圖是在解釋鄰域如何建成,還沒有展示最終嵌入。

    圖4|左為原始點,右在原位置加上固定半徑的覆蓋。來源:Leland McInnes/UMAP官方How UMAP Works,BSD-3-Clause。兩張完整原圖左右排,手機上下排。顏色沿曲線位置變化;不是DBSCAN調參結果,也不是本站產生的資料。

    若只用一個固定半徑,密集處有很多重疊,稀疏處卻可能連不起來。對資料的採樣疏密不均勻時,「多遠才算附近」可能需要局部尺度。

    圖5|左圖把固定覆蓋形成的鄰接畫成線,可看到斷開的區段;右圖改用局部尺度,在稀疏處擴大覆蓋。兩張顯示的物件不同:一張是圖,一張是覆蓋,並非最佳化前後的座標。來源同圖4;完整原圖編排。

    實作上,UMAP以近鄰關係估計各點周圍的尺度。這能避免把密集處與稀疏處全都用同一把局部尺衡量,但不表示它自動知道資料的真正生成機制。你選擇的特徵、縮放方式與距離定義,仍決定了它先找到哪些鄰居。

    圖6|從有明確邊界的圈,轉成隨距離漸淡的模糊權重,再納入局部連通的構造。點的位置仍在原平面。這些色暈不是類別機率或高維密度的數值熱圖。來源同圖4;完整原圖編排。

    連結也不只剩「有」或「沒有」。用常見的簡化形式,可以把i對j的局部連結強度寫成:

    w(i -> j) = exp(-max(0, d(i,j) - rho_i) / sigma_i)
    

    rho_i與局部連通的距離尺度有關,sigma_i控制衰減;這是幫助理解的形式,實際估計會處理近鄰、重複點及套件細節。注意i與j使用自己的局部尺度,因此i對j的強度,未必等於j對i的強度。

    圖7|左圖保留兩端局部描述產生的平行邊,右圖合成單一帶權重的關係。仍是原始曲線座標;右圖不是UMAP最終低維布局。來源同圖4;完整原圖編排。

    預設模糊聯集可用a+b-ab理解。若兩端權重是0.6與0.5,合成後是0.8,既不是平均0.55,也不是直接相加1.1。這個值是建圖的連結強度,不能解讀成「兩筆資料有80%機率屬於同一群」。

    接著才輪到低維座標。UMAP用由低維距離決定的連結形式q,讓它接近原空間建出的權重w。概念上,忽略與低維座標無關的常數,可把目標寫為:

    q_ij = 1 / (1 + a ||y_i-y_j||^(2b))
    loss = -sum_{i<j} [w_ij log(q_ij) + (1-w_ij) log(1-q_ij)]
    

    a與b是控制曲線形狀的參數,不是前面示範聯集時的兩個權重。第一項鼓勵有強連結的點靠近;第二項提供排斥。實作使用取樣與近似最佳化,不是逐步精算每一對點後保證找到全域最佳解。模糊拓樸的理論提供了建模依據,也不等於有限、含雜訊的任意資料都會保留完整拓樸。

    真正的嵌入長怎樣?先換一批說得清楚的資料

    圖8|這次是另一批資料:官方用seed=42抽800筆四維均勻隨機數,四欄分別作R、G、B、alpha,也直接用來著色。這是UMAP真正的二維輸出,不是前面正弦曲線建圖的最後一步。來源:UMAP Basic UMAP Parameters原圖,BSD-3-Clause;參數章原作者致謝Philippe Rivière/visionscarto.net。保留完整原圖。來源沒有固定嵌入random_state,圖中結果使用其歷史範例設定,不冒稱本次重跑。

    相近顏色常出現在鄰近區域,這符合以四維顏色距離建鄰域的方向。但這批資料並沒有預先設定幾個離散族群。alpha也是輸入特徵,點看起來比較淡,不應直接當成那一帶的原始資料密度比較低。

    這個例子很適合看參數,因為原始資料的來源很單純:同一個四維單位立方體裡的均勻抽樣。某張圖比較成團,不代表生成資料突然多出幾種顏色類別。

    n_neighbors決定建圖時看多大的鄰域

    圖9|依序為n_neighbors=2、20、200;同800筆RGBA資料,來源函式其餘設定為min_dist=0.1、二維、Euclidean距離。來源未固定每次嵌入的種子,因此變化包含隨機因素,不能當成嚴格隔離單一因素的實驗。來源同圖8,三張完整原圖依序上下排,桌機與手機皆保留可讀的標題與各自原刻度。

    小鄰域強調很近的局部關係,圖中會出現許多短鏈與零碎片段。擴大鄰域後,能把較廣範圍的關係納入建圖,但可能犧牲細部局部差異。這種局部與較大範圍的取捨,不代表把n_neighbors開大,就能保證所有全域距離都正確。

    也不要在不同圖裡依座標的正負判斷某群「移到右邊了」。嵌入可以旋轉、翻轉或改變尺度;跨圖比較應關心鄰居與關係是否穩定,不能直接對齊原始座標數字。

    min_dist影響成團傾向,不是每對點的硬距離下限

    圖10|依序為min_dist=0、0.5、0.99;來源函式固定n_neighbors=15、二維及Euclidean距離,同800筆RGBA輸入,但嵌入種子未固定。來源同圖8,完整原圖編排。點的顏色與透明度未調整;不是本站的多種子穩定性檢驗。

    較小的min_dist容許布局形成較緊的小團,較大的值傾向比較鬆散。它透過低維連結曲線影響最佳化,並不是每更新一次座標,都強制檢查任兩點距離不得小於這個數字。原始教學文字把它簡稱為最小距離,閱讀時應補上這個實作差別。

    因此,想讓群看起來更分明,調參通常做得到;問題是分明是否有任務上的證據。若你要把嵌入接到分群,不必為了畫圖強迫只留二維,也要檢查其他維度、原始特徵、參數與樣本變動。這是需要驗證的管線選擇,不能只憑漂亮的二維圖採用。

    實際跑一次:檢查輸出與鄰域,先不要評比誰勝出

    下面的程式在Python 3.10.12、隔離的WSL環境實跑。它從scikit-learn Digits隨機取400張8×8影像,每張攤成64維。抽樣不使用數字標籤;先做20維PCA,再各跑兩次t-SNE與UMAP,檢查形狀、有限值、同環境重跑結果與trustworthiness。

    這是對這400筆資料的描述性探索,PCA也在同400筆上擬合。它沒有宣稱保留測試集、預測新數字或取得分類準確率;前面10組配圖來自官方來源,並非這個程式的輸出。

    在新建環境安裝以下固定版本,再執行後面的Python。這是本次已驗版本組合,不是對未來最新版的保證。

    numpy==1.26.3
    scipy==1.14.0
    scikit-learn==1.5.1
    umap-learn==0.5.6
    pynndescent==0.5.13
    numba==0.60.0
    llvmlite==0.43.0
    joblib==1.4.2
    threadpoolctl==3.5.0
    tqdm==4.66.4
    
    python3 -m venv .venv
    source .venv/bin/activate
    # 將上方版本清單另存為requirements.txt
    python -m pip install -r requirements.txt
    # 將下方程式另存為neighborhood_embeddings.py
    python neighborhood_embeddings.py
    
    import numpy as np
    import sklearn
    import numba
    import pynndescent
    import umap
    from sklearn.datasets import load_digits
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE, trustworthiness
    from threadpoolctl import threadpool_limits
    
    # Descriptive exploration of a fixed400-row sample, not a held-out prediction.
    # Digit labels are not used for sampling, PCA or either embedding.
    X, _ = load_digits(return_X_y=True)
    indices = np.sort(np.random.default_rng(42).choice(len(X), 400, replace=False))
    with threadpool_limits(limits=1):
        base = PCA(n_components=20, svd_solver="full").fit_transform(X[indices])
        tsne_options = dict(n_components=2, perplexity=30, max_iter=500,
                            init="pca", learning_rate="auto",
                            random_state=42, n_jobs=1)
        umap_options = dict(n_components=2, n_neighbors=15, min_dist=0.1,
                            metric="euclidean", random_state=42,
                            n_jobs=1, n_epochs=300)
        t1 = TSNE(**tsne_options).fit_transform(base)
        t2 = TSNE(**tsne_options).fit_transform(base)
        u1 = umap.UMAP(**umap_options).fit_transform(base)
        u2 = umap.UMAP(**umap_options).fit_transform(base)
        print(f"numpy={np.__version__} sklearn={sklearn.__version__} "
              f"umap={umap.__version__} pynndescent={pynndescent.__version__} "
              f"numba={numba.__version__}")
        print(f"exploration rows={len(base)} original_features=64 PCA_features=20")
        for name, embedding, repeated in [("TSNE", t1, t2), ("UMAP", u1, u2)]:
            print(f"{name} shape={embedding.shape} finite={np.isfinite(embedding).all()} "
                  f"same_environment_repeat_equal={np.array_equal(embedding, repeated)}")
            print(f"{name} trustworthiness_k10_in_PCA_space="
                  f"{trustworthiness(base, embedding, n_neighbors=10):.6f}")
    
    uniform = np.full(3, 1 / 3)
    entropy = -np.sum(uniform * np.log(uniform))
    print(f"three_uniform_neighbors perplexity={np.exp(entropy):.6f}")
    a, b = 0.6, 0.5
    print(f"fuzzy_union_0.6_0.5={a + b - a * b:.6f}")
    

    本次實際輸出:

    numpy=1.26.3 sklearn=1.5.1 umap=0.5.6 pynndescent=0.5.13 numba=0.60.0
    exploration rows=400 original_features=64 PCA_features=20
    TSNE shape=(400, 2) finite=True same_environment_repeat_equal=True
    TSNE trustworthiness_k10_in_PCA_space=0.985141
    UMAP shape=(400, 2) finite=True same_environment_repeat_equal=True
    UMAP trustworthiness_k10_in_PCA_space=0.980451
    three_uniform_neighbors perplexity=3.000000
    fuzzy_union_0.6_0.5=0.800000
    

    400×2與finite=True表示這次產生了尺寸正確、沒有NaN或無限值的座標。same_environment_repeat_equal=True表示在這個固定環境、種子及執行設定下,兩次結果逐元素相同;它沒有測試換種子、不同硬體或不同套件版本是否穩定。

    trustworthiness檢查的是:低維圖上出現的近鄰,有多少其實在參考空間裡排得很遠? 它對這些「新闖入的假鄰居」按參考空間的名次加罰,再正規化。分數越接近1,表示依這項準則造成的假鄰居代價越低。它不是「98.5%的鄰居完整保留」,也不完整衡量被拆散的原鄰居或所有全域距離。

    這裡的參考空間明確是PCA後的20維,k=10;不是原64維,也不是已知數字標籤。要檢查PCA本身造成的損失,得另用原64維作比較。單一抽樣、單一參數與這一個指標的差距,無法推出t-SNE全面勝過UMAP。兩者迭代數也不相同,程式沒有做時間或運算預算公平比較。

    做完圖,如何決定接下來相信什麼?

    先回到樣本本身。在同一座島隨機抽幾筆,在兩座島的邊緣各抽幾筆,查看原始影像、欄位或文件。若島與島只是資料收集批次、缺失值編碼或尺度差異,應優先處理資料問題。這也是為什麼降維之前的特徵選擇與縮放,比事後調漂亮顏色更重要。

    接著比較設定。保存前處理、樣本索引、距離、套件版本、初始化與種子;變動合理範圍的perplexity或n_neighbors,使用多個種子,看同一批樣本的近鄰是否穩定。不要只從十張結果裡挑最像預期的一張。前面的實跑只驗了固定種子的重現,這項多設定檢查仍是讀者要另外執行的分析。

    最後才看任務。若你要主張客戶分群有用,應在原始特徵描述各群,檢查重抽樣或新批次下的穩定性,再驗證與實際行為或決策有何關係。若你要預測,則先切好訓練與保留資料,讓縮放、PCA與可用的降維步驟在訓練範圍內擬合,對完整管線做驗證。全資料探索圖不能充當保留集成效。

    你看到的現象可以提出的問題還不足以宣稱什麼
    有幾座分離小島島內樣本有哪些共同特徵?原資料天然就有相同數目的群
    已知標籤常聚在一起局部特徵是否與標籤有關?新資料分類準確率很高
    換種子後仍有相近鄰域哪些局部關係比較穩定?全域距離或密度也被忠實保留
    trustworthiness較高指定參考空間與k下,假鄰居代價如何?所有鄰居保留率、真群數或商業價值

    還有一個會影響系統設計的差別:scikit-learn 1.5.1的TSNE沒有標準transform方法,不能直接把新樣本丟給它取得與既有點對齊的座標。UMAP有transform新資料的介面,但那也需要用訓練資料建好的模型與相同前處理;對分布改變的新資料仍要驗證。重新把所有資料一起fit,舊點座標可能跟著變,不能當成固定的地圖。

    t-SNE與UMAP讓我們有機會從高維資料中找到值得追問的局部關係。圖像最有用的時刻,是它帶你去檢查幾筆具體樣本、發現資料問題或提出可驗證的假設。當你能說清楚「它保留了哪種關係、這次驗到了哪裡」,漂亮的圖才開始成為可靠的分析工具。

    延伸閱讀與素材出處

    圖片授權聲明

    圖1–3採用The scikit-learn developers原圖,節錄三列並製作手機直排版。圖4–10採用UMAP官方文件既有圖片,保留完整原圖,只做左右/上下編排;封面取圖8等比縮放加白邊。沒有重畫資料點、改變原圖參數標題或替來源補造實驗結果。UMAP文件保留原2018年作者來源資訊;套件授權的著作權年份依下列原始LICENSE。兩套BSD聲明分別保留,不混用。

    以下是scikit-learn隨來源附帶的完整BSD-3-Clause聲明:

    BSD 3-Clause License
    
    Copyright (c) 2007-2026 The scikit-learn developers.
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    
    * Neither the name of the copyright holder nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    

    以下是UMAP隨來源附帶的完整BSD-3-Clause聲明:

    BSD 3-Clause License
    
    Copyright (c) 2017, Leland McInnes
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    
    * Neither the name of the copyright holder nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →