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

創宇前端發表於2019-02-27

給你的入侵檢測系統提供一個靈感。


如果給你一大堆使用者輸入,裡面有大量的中文地名,像是“北京”、“成都”、“東莞”,不幸的是,其中也混有一些羅馬地名,比如 “Singapore”、“New York”、“Tokyo”。你的任務是將它們分開,你會如何去做?

當然,有很多方法可以輕易做到。

如果是一堆 “good”、“fine”、“not bad”、“amazing”、"nice" 的簡短反饋裡混有 “Fallout 4 is the epitemy of everything wrong with modern gaming, it has a total of 2 compelling quests, its gameplay is worse then the rest, and to top it off they added microtransactions to it. it is the worst of the fallout series.” 這樣的長篇抱怨呢?

你可能會想,這不更簡單了嘛,檢測字串長度甚至標點符號數目就行呀。

如果是一堆 “12345678”、“5201314”、“password”裡混有“password' and (select count(*) from data)>0 and 'a'='a”、“>"'>” 呢?

或許你已經不耐煩了:這點安全素養還是有的!檢測關鍵字和特殊符號呀!

你已經不打算讓我再“如果”下去了:沒有什麼是一段正規表示式搞不定的,如果有,那就該再學一次。

好,但是現在,我們需要的是,用同一個模型實現上述所有場景——當字串有長有短,它要將長度異常的字串分開。當有常規字串和包含特殊符號的字串,它能把特殊的那些拎出來。當字串混有不同的語言,它能進行“淨化”。甚至,還有各種不在意料之中的情形。

這段程式碼就像是在宗教戰爭中審判異端,無論是中出了一個叛徒還是乾脆分裂成了兩類,它總是能根據字串的長相,把少數派給抓出來。

如果你恰好做過一些事,例如探索深度學習對網路安全的應用,相信你看著資料集,能很快想到這個“異端審判器”的實用價值。

讓我們默契地眨眨眼。在後文裡,我們會實現這樣一個玩具。

主教的自我修養:看臉

北京與成都之間相距再遠,也可以用歐式距離輕鬆度量。但 “Beijing” 與 “Chengdu” 之間的距離呢?

我們需要看臉,根據字串“外貌”的特徵,去定義和量化這樣一種差異。

不難發現,字串之間的距離至少應該包括如下組分:

  • 字串長度差異(如 catmiaomiaomiaomiaomiao
  • 字符集差異(如 123abc
  • 字元序列差異(如 上海自來水水來自海上

長度差異

這有什麼好說的……長度為5的字串顯然比長度為3的字串多出一個2……

在此略過。

def strLengthDiffer(str1, str2):
	return abs(len(str1) - len(str2))
複製程式碼

字符集差異

字符集的差異是為了刻畫不同字串在字元選擇上的差異,我們應該對差異較大的字串——特別是出現了不同類別的字元時——進行距離上的懲罰。

為了實現這個目標,首先要定義字元間的距離。這裡,我們把相同字元間距離定義為 0, 同類字元(如ab)間距離定義為 1,不同類字元間距離定義為 10。

字元分類可以為小寫字母、大寫字母、數字和其他,當然讀者也可以根據自己的實際用途進行分類,把系統需要敏感識別的差異分為不同的兩類。

有了字元間距離,我們定義字元 A(1) 與字符集 B 的距離為該 A(1) 到 B 中每一個字元的距離的最小值。

在上述基礎上,我們進一步定義字符集 A 到字符集 B 間的距離為:A 中每一個字元到 B 的距離的算術和。

顯然:

  • 字元距離(a, b) = 字元距離(b, a)
  • 字元到字符集距離(a, B) = 字符集到字元距離(B, a)
  • 字符集間距離(A, B) = 字符集間距離(B, A)

由此,我們對字符集間距離完成了符合認知的定義。

def charSetDiffer(s1, s2):
    # 由於筆者使用的程式碼版本在這裡有更復雜的邏輯,就不提供程式碼細節了
    # 已經講得這麼明確了,寫寫看吧
    return s
複製程式碼

字元序列差異

對於開發者而言,使用者輸入是 alert("test") 還是 aeelrstt""(),顯然有著完全不同的含義。後面這種意味不明的字串根本不會讓人多看一眼,而前者如果被使用者執行成功,那麼他後續多半會再搞些別的破壞,非常邪惡。

這個故事告訴我們,字元序列的差異不容忽視。

在這裡,我們使用 N-Gram 語言模型,藉助 N=2 時的 Gram 數目來度量兩個序列的差異。

如果你並不知道我在說什麼,那麼具體而言是像這樣的計算:

  1. 假設我們有字串 S1 與 S2。
  2. 將字串 S1 每兩個連續字元作為一個元素,構成集合 G1,同理也有 G2。
  3. 字串 S1 與 S2 之間的序列差異就是 G1 與 G2 中不同元素的數目。顯然,你可以通過他們的交集減去他們的並集取到該值。
def n_grams(a):
    z = (islice(a, i, None) for i in range(2))
    return list(zip(*z))
複製程式碼
def groupDiffer(s1, s2):
    len1 = len(list(set(s1).intersection(set(s2))))
    len2 = len(list(set(s1).union(set(s2))))
    return abs((len2 - len1))
複製程式碼

總算有了字串間距離

到現在為止,我們對兩個字串間三個形式維度的差異都有了量化,接下來做的就是通過精妙絕倫的加權求和,算出那個令人拍案叫絕的字串間距離。

在此,筆者使用的方法是——

def samplesDistance(str1, str2):
    a = strLengthDiffer(str1, str2)
    b = charSetDiffer(str1, str2)
    s1 = n_grams(str1)
    s2 = n_grams(str2)
    c = groupDiffer(s1, s2)
    d = a+b+c
    return d
複製程式碼

是的!簡單相加……

山不在高,有廟則有人送錦旗,演算法不在複雜,有用就行。

你當然可以根據自己的需要,去調節系統對於其中三個維度的不同敏感度,但筆者認為字符集差異的值天然就比另外兩種差異的值要大,已經符合我的需要,就不再調整啦。

你好像和他們不太一樣

有了字串間的距離,進一步,就有一個字串到另一堆字串的距離。我們定義如下:

字串樣本與字串集合的距離 = 該字串樣本到字串集合中每個字串樣本的距離的算術平均值

即:

def sampleClassDistance(sample, class1):
    list_0 = []
    length = len(class1)
    for item in class1:
        list_0.append(samplesDistance(sample, item))
    return sum(list_0)/length
複製程式碼

你們是兩類

由上一節的一個字串到一堆字串的距離出發,我們可以得到一堆字串到另一堆字串的距離。它的定義形式很相似:

字串集合間的距離 = 該字串集合中的每一樣本到字串集合的距離的算術平均值 = 該字串集合中每一樣本到另一字串中每一樣本的距離的算術平均值

即:

def classesDistance(class1, class2):
    list_0 = []
    class1 = flatten(class1)
    class2 = flatten(class2)
    m = len(class1)
    n = len(class2)
    for item1 in class1:
        for item2 in class2:
            list_0.append(sampleDistance(item1, item2))
    return sum(list_0)/(m*n)
複製程式碼

類內無派,千奇百怪

同理,也可以定義“類內距離”作為一堆字串內部的屬性。它在實際意義上可能有些接近於方差。我們規定:

類內距離 = 該字串集合到自己的距離

def innerClassesDistanse(class1):
    return classesDistance(class1, class1)
複製程式碼

讓我們停下來整理一下思路

到這裡你可能已經暈了,定義這麼多距離到底要幹嘛?

我們說過,要把兩類不確定的形式不同的字串分開,關鍵是定義差異,也就是去量化“長得顯然不同”到底有多不同。

於是我們發明了一些“距離”作為量化屬性,兩個字串之間,有長度不同、構成的字元不同、字元序列不同,那麼這兩個字串就有可量化的距離。

兩個字串有距離,那麼一個字串到另一類字串、一類字串到另一類字串、同一類字串內部也有距離。

當你混跡人群,最重要的事情是弄清誰是朋友、誰是敵人。而當你需要把人群分為兩類,最重要的事情就是知道兩類人有多不同,以及每類人內部有多一致

放在分類字串的情景,就是要能夠量化類間距離類內距離

嘿,這不,我們已經有了 classesDistance()innerClassesDistanse()

就到這裡,我們下次再會 :)


編者按:

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

作者認為清晰的描述能讓不會寫程式碼的人寫出程式碼,所以文中程式碼來自並不會寫 Python 的朋友,程式碼風格可能有些奇怪。

文 / YvesX

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

編 / 熒聲

本文已由作者授權釋出,版權屬於創宇前端。歡迎註明出處轉載本文。本文連結:knownsec-fed.com/2018-09-25-…

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

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

感謝您的閱讀。

相關文章