1. 程式人生 > >異端審判器!一個泛用型文字聚類模型的實現(2)

異端審判器!一個泛用型文字聚類模型的實現(2)

上文連結:異端審判器!一個泛用型文字聚類模型的實現(1)

上回,我們提出了一種只要輸入一堆字串,就能根據字串的構造挑揀出“少數派”,以識別異常引數的構想。我們將它稱作“異端審判”。

前文中我們已經定義好了一些必要概念,並寫出了函式實現。我們的程式遞進地量化了字元之間的差異、字串之間的差異,最終得到了字串集合之間的差異。有了這項指標,我們就能完成分揀工作。

在生活中,我們常有幾排人一起合影的經歷。有時是前排蹲下後排站立,有時是矮個子站在前排高個子位居後排。不妨假想一下,如果你就是那位攝影師,正指揮大家列隊,你習慣於怎樣安排隊形呢?

通常情況下,你會直接要求站成大致均勻的兩排,再逐個調整細節,直到整個隊形看上去令人滿意。

這為我們識別“異端”提供了靈感。

想象一位“主教”威立於尖塔的陽臺,望著城樓下的人群,現在他要做的就是將人分成兩類,一類大致可信,一類有些可疑,再逐個把後者中的信眾移進前者,“異端”自然被剩下。

這篇文章中,我們就是要實現這樣一件事。

從一刀切開始分類

我們先將每個輸入都視作單獨的一類,以啟動整個流程。整個全集記作 C

# 初始化
# 輸入一個列表,如['a','b','c']
# 輸出一個把每個元素都封裝為列表的列表,如[['a'],['b'],['c']]
def init(sample_list):
    C = []
    for x in sample_list:
        C.append([x])
    return
C 複製程式碼

基於此前定義的字串集間距離(在文章中簡稱為類間距離),選擇最接近的兩類,合併它們。

這步操作聽上去很簡單,實際上確實也很簡單,但我們會遇到一些麻煩:我們一直使用列表來簡單表示集合這個數學概念,它們性質並不相同。集合的三個主要特性中,列表不滿足無序性與互異性,因此需要一些額外的處理。

例如,找到最接近的兩類,無論如何我們也需要計算出 n^2 個距離,這就不是一件輕鬆的事。我們將最小距離記作d——

def find_min(C):
    # 邏輯告訴我們無論怎樣做都必須計算兩兩之間的全部距離,這裡用一個二維列表來記錄
    # 數學告訴我們 a->b 與 b->a 的距離是一樣的,其實開銷可以減小一半
# 作者告訴大家由於我很懶,就不做這個優化了…… scale = len(C) d = [[0 for i in range(scale)] for i in range(scale)] min_d = classesDistanse(C[0], C[1]) where_min_d = [0, 1] for i in range(scale): for j in range(scale): d[i][j] = classesDistanse(C[i], C[j]) if i != j and d[i][j] < min_d: min_d = d[i][j] where_min_d = [i, j] return where_min_d 複製程式碼

找到了最小的 d 以後,就該合併它們了。在進行並運算時,我們就會遇到列表與集合的性質差異、邏輯與運算的表示差異等問題,我們重新定義運算函式來彌補這些偏差。

如果這部分讓你有點眩暈,不要為此擔心。你可以將它們都視作 dirty hack,記住我們只是在做一件簡單的事情:將剛才已經找到的類間距離最小的兩個集合,合併成一個。

# C:=C-Ci-Cj+CiUCj
# 輸入全集列表C及其選出的兩個子列表Ci、Cj,如C=[['a'],['b'],['c']],Ci=['a'], Cj=['b']
# 需要注意的是,邏輯上,集合Ci與集合Cj是集合C的【元素】,而交併差都是【集合】之間的運算
# 輸出合併Ci與Cj之後的全集列表,如[[['a'],['b']],['c']]
def merge(C, i, j):
    # 在數學上,集合[[1],[2]]與集合[[1,2]]的並集有三個元素,因為[1],[2],[1,2]都是完全不同的元素。但在這裡的邏輯上,需要結果為[[1,2]],所以另外定義了特殊的“交集”運算
    # 交集與差集的運算是針對集合的(如[[1]])而非元素(如[1]),所以需要手動裝進列表再傳參。(其實已經特殊處理的交集運算無必要這樣做,但為了邏輯一致遵守了統一的寫法)
    C_u = special_union([C[i]], [C[j]])
    C_d = difference(difference(C, [C[i]]), [C[j]])
    C_n = C_d
    C_n.append(C_u)
    return C_n
複製程式碼

我們將最接近的兩類合併成一類了,而目標是“一刀切”,即把整個全集劃分為大致均勻的兩類。所以我們不斷查詢最接近的兩類,將其合併,直到有某個集合的總量超過全集的一半。

# 查詢規模最大的一個子列表
# 輸入全集C,如[[['a'],['b']],['c']]
# 輸出規模最大即集合內元素最多的列表的下標,如 0
def find_largest(C):
    s = [0] * len(C)
    max_s = len(C[0])
    where_max_s = 0
    for x in range(len(C)):
        s[x] = len(C[x])
        if s[x] > max_s:
            max_s = s[x]
            where_max_s = x
    return where_max_s
複製程式碼

每個步驟都已經定義就緒,整個操作流程是這樣的:

def layerClassification(sample_list):
    C = init(sample_list)
    while True:
        where_min_d = find_min(C)
        i, j = where_min_d
        C = merge(C, i, j)
        where_max_s = find_largest(C)
        if count_elem(C[where_max_s]) > 0.5 * len(C):
            break
    CM = C[where_max_s]
    CN = difference(C, [CM])
    return flatten(CM), flatten(CN)
複製程式碼

這段程式碼中提到了兩個輔助函式,其中 count_elem() 用於遞迴遍歷每個集合中實際包含的字串個數(而非子元素個數),分類的最終結果可能出現複雜的多維列表,而我們只需要兩個簡單的一維列表用於表示兩個集合,定義 flatten() 來展開巢狀。

你!到那邊去!

經過了剛才的分類,現在我們有了兩個集合。其中的一個包含了原本聚類性比較明顯的元素,他們可能長相非常近似,剩下一半隻是單純被剩下了而已,風馬牛齊聚一堂,看上去亂糟糟的。

接下來就是“微調”時間啦,我們要從那個泥沙俱下的集合中,把“信眾”逐個移動到前面那個相對齊整的集合裡,從而將“異端”孤立。

這件事的關鍵是何時停止:移到哪一步時,那個混亂的集合恰好只剩“異端”,而又沒有“異端”錯誤地赦免呢?

好在我們的主教無需落子無悔,移錯了就倒回去嘛。他甚至可以命人把所有結果都羅列出來,由他來判斷哪一個方案是最好的。

那我們不妨先不考慮決策的事情,提供全部方案就好。

我們將分類方案記作 S,一個分類方案由兩個集合構成,即{C1, C2},同樣地,我們使用列表來表示。為了在不斷移動的過程中,儲存每一時刻的 C1 與 C2,而不作為引用跟隨變化,我們需要使用深拷貝。

def note_solution(S, C1, C2, N):
    _C1 = copy.deepcopy(C1)
    _C2 = copy.deepcopy(C2)
    S.append([_C1, _C2])
    N = N + 1
    return S
複製程式碼

基於此前定義的類間距離,我們能夠選到 C2 中最接近 C1 的樣本:

def select_min(C1, C2):
    min_x = C2[0]
    min_d = classesDistance(C1, min_x)
    for x in C2:
        temp = classesDistance(C1, x)
        if temp < min_d:
            min_d = temp
            min_x = x
    return min_x
複製程式碼

把這個樣本從 C2 中放進 C1:

def update(min_x, C1, C2):
    C1.append(min_x)
    C2.remove(min_x)
    return [C1, C2]
複製程式碼

我們不斷搬運元素,直到那個沒有聚類性的 C2 被搬空。記錄下這個過程中所有分類方案。除了全部分類方案 S 以外,我們同時維護另一個列表,記錄被移動的元素,以便於撤回。由於這個列表裡所有元素都是我們每一步選出的到 C1 距離最小元素,不妨就將這個列表稱作 M,整個過程如下:

def iterateClassification(C):
    N = 0
    S = []
    M = []
    C1 = C[0]
    C2 = C[1]
    while True:
        note_solution(S, C1, C2, N)
        min_x = select_min(C1, C2)
        M.append(min_x)
        update(min_x, C1, C2)
        if len(C2) == 0:
            break
    del(S[0])
    return S, M
複製程式碼

到這裡為止,我們反覆運用上篇文章中定義的類間距離,做了一次粗選,又列出了所有微調生成的方案。最佳方案必然就是其中之一,留給我們大主教的,只剩一個優化問題。

讓我們下回再見~


編者按:

本文未完待續,敬請期待後續推送。參考文獻及整理後的示例程式碼將在完整文章末給出。

文 / YvesX

反正你也猜不出我是做什麼的

編 / 熒聲

本文由創宇前端作者授權釋出,版權屬於作者,創宇前端出品。 歡迎註明出處轉載本文。文章連結:www.yvesx.com/archives/he…

想要訂閱更多來自知道創宇開發一線的分享,請搜尋關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回覆。

感謝您的閱讀。