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

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

上回,咱們提出了一種只要輸入一堆字符串,就能根據字符串的構造挑揀出「少數派」,以識別異常參數的構想。咱們將它稱做「異端審判」。python

前文中咱們已經定義好了一些必要概念,並寫出了函數實現。咱們的程序遞進地量化了字符之間的差別、字符串之間的差別,最終獲得了字符串集合之間的差別。有了這項指標,咱們就能完成分揀工做。微信

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

一般狀況下,你會直接要求站成大體均勻的兩排,再逐個調整細節,直到整個隊形看上去使人滿意。函數

這爲咱們識別「異端」提供了靈感。post

想象一位「主教」威立於尖塔的陽臺,望着城樓下的人羣,如今他要作的就是將人分紅兩類,一類大體可信,一類有些可疑,再逐個把後者中的信衆移進前者,「異端」天然被剩下。優化

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

從一刀切開始分類

咱們先將每一個輸入都視做單獨的一類,以啓動整個流程。整個全集記做 Ccode

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

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

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

例如,找到最接近的兩類,不管如何咱們也須要計算出 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)。歡迎留言討論,咱們會盡量回復。

感謝您的閱讀。

相關文章
相關標籤/搜索