腾讯抗黑灰产——自监督发现行话黑词识别一词多义
本文作者:lorenzwang ,騰訊 TEG 安全工程師
常見的中文 NLP 下游任務一般都是以分詞作為起點(以 transformer 為核心的算法除外),對每個詞取 embedding,作為模型的輸入。不過在黑灰產領域,這種處理方法有一個問題:大量的黑話/黑詞對于下游任務非常有效,但卻不在通用的詞典中,導致分詞器無法準確切分出對應的詞。比如,今年 315 晚會曝光的“714 ”,再比如“口子”。以及本人參加新人培訓時講師提的一些 00 后常用詞,“擴列”, “暖說說”。
筆者所在的防水墻團隊整合了多源異質數據,在黑產人群識別、威脅滲透、黑產對抗等場景具備行業領先的能力。作為黑灰產能力建立的基礎,行話/黑詞的識別顯得至關重要。本文將介紹一些我們在新詞發現及一詞多義上的解決方案。
1 新詞發現新詞發現方法較多,本文將介紹一種比較簡單且有效的新詞發現方案:自由度+凝固度。
我們首先來定義一個問題:
怎樣的字符可以組成一個中文語義下的“詞”1.1 具有比較豐富的上文、下文詞作為中文一個基本的語義單元,具備一個比較顯著的特征:可以比較靈活的應用到不同場景中。比如說“機器學習”這個詞,上下文均可以搭配很多動詞和名詞,“學習人工智能知識”, “從事人工智能行業”, “選修人工智能課程”,“基于人工智能 xx”,“人工智能賦予了 xx”, “人工智能識別了 xx”,“人工智能實現了 xx”。但是對于“人工智”這個詞來說,上文依然可以搭配很多詞語,可是下文基本上只能搭配“能”了。
再比如筆者的家鄉:臨沂。“住在臨沂”,“建設大美臨沂”, “臨沂煎餅”, “臨沂機場”都存在豐富的上文、下文,但是對于“沂”(友情提示:yi, 二聲),下文可以接“煎餅”, “機場”, “人”,但是上文則大概率只有“臨”,以及其他幾個很少的字。
寫到這里,各位讀者可以自己自己考慮下一些常用詞是否符合這一點。
上文和下文的豐富程度可以用信息熵來度量:熵是一種表示信息量的指標,熵越高意味著不確定越高,越難以預測
讓我們用一個實際的例子來感受下。假設我們有一個硬幣,每次拋起來之后正面向上的概率為 x, x range(0, 1, 0.1):x取值與熵的關系從圖中可以看出,當正面向上的概率 x=0 或 x=1 時,熵=0,因此此時隨便拋一次硬幣我們都可以準確的預測到正面是否朝上。熵的最大值發生在 x=0.5 處,x=0.5 時我們預測準下一次拋硬幣時正面向上的概率最小。
對應到詞的上文和下文,對于那些上文(下文)不豐富的詞來說,我們可以比較大概率的預測準詞的上文或下文,比如“珠穆朗”這個詞,它的下文大概率是“瑪”, 比如“沂”這個詞,它的上文大概率是“臨”,對于“珠穆朗”,我們稱它的右信息熵比較小(信息熵越小,則確定性越高),對于“沂”這個詞,我們稱它的左信息熵比較小(信息熵越小,確定性越高)。
詞的上文和下文越豐富,則其左信息熵(右信息熵)越大。一般來說,我們取左右信息熵中的最小值(考慮下這是為什么)。
1.2 詞的內部凝聚度要足夠高
上面提到說對于一個合格的詞語而言,需要具有豐富的上下文(不然就沒必要作為一個基本的不可劃分的語義單元了),但滿足了這一點就可以了嘛?讓我們來看一個例子。
小明在學校看了小紅的演唱會其中,“在學校”幾乎百搭,“的演唱會”也是,不信來看:
xx/在學校/xx:他在學校經常搗亂、美術展在學校美術館舉辦xx/的演唱會/xx:周杰倫的演唱會棒極了、成功的演唱會太難了
但是“在學校”和“的演唱會”很明顯不是我們直覺上的詞,為什么會出現這種情況?因為“在”和“的”在中文中出現的太頻繁了(“的太頻繁”是不是也符合擁有豐富的上文、下文這一點?)
很明顯,合格的詞不僅要在外部有豐富的上文、下文,其本身內部也要滿足一定的條件。上面我們講過,詞是一個基本的語義單元,意味著一般情況下不應該繼續細分了,這也就意味著詞內部要比較穩固或者內部凝固程度比較高。穩固意味著不可分,不可分如何衡量?
先說結論,我們用(點間)互信息衡量詞內部的凝聚程度:
公式最右邊是 , 假如 x 和 y 完全獨立,,上面公式=0。
假如有一篇介紹臨沂的文章,總共有 100 個字,其中,臨沂出現 1 次,臨出現一次,沂出現一次,那么 p(臨,沂)=1/100, p(臨)=1/100,p(沂)=1/100,我們可以看到“臨沂”這個詞內部還是比較穩固的,向心力比較強,離心力很小
1.3 總結
基于上述兩點,我們可以得出如下結論:詞之所以成詞,其外部需要有比較豐富的上文和下文,其內部要足夠穩固一般不可再分。
基于文本選取合適的 score,也可以分別取左熵,右熵,PMI 的閾值對詞進行篩選。
1.4 新詞發現流程
生成候選詞
這一步我們將文本按字分割后拼接為二元組,三元組,…,k 元組(一般 k <= 5),如“新詞發現及一詞多義的解決方案”對應的二元組有:[“新詞”, “詞發”, “發現”, “現及”, “及一”, “一詞”, “詞多”, “多義”, “義的”, “的解”, “解決”, “決方”, “方案”]。
在這一步如果采用現有的分詞工具進行分詞會導致很多詞可能在這一步就被拆分開了,對應的詞后續無法被識別到。候選詞得分計算
將新詞加入到分詞器的詞典中
將通過上述步驟得到的詞加入到分詞器的詞典中,如在 jieba 中采用如下方式:
jieba.add_word("德瑪西亞")后續采用這個分詞器進行分詞時,相應的新詞計算得到每個詞的 embedding vector
基于種子黑詞,計算新詞(或所有詞)與種子黑詞的相似度,篩選得到黑詞
比如種子詞選取毒品,最終發現“溜冰”這個原本看似人畜無害的詞與毒品相關的種子詞相似程度均很高,即可推測自己發現了一個該領域的新詞。
2 一詞多義首先讓我們來看一下例子:溜冰對于那些天真無邪的同學來說,溜冰就是在冰上溜來溜去。對于那些了解世事的同學來說,溜冰就是在冰上溜來溜去。只不過,「此冰非彼冰,此溜也非彼溜」 。
和 714 、擴列、暖說說這種需要新詞發現才能識別的新詞不同(嚴格來說現在擴列已經成為通用詞了),溜冰是現有的詞,只是在特定的場景下意義發生了變化。如下面兩句:
周末和小伙伴一起去溜冰周末和小伙伴在出租屋溜冰nlp 任務的輸入一般是詞的 emb vector, 上一步完成新詞發現保證黑詞/行話能夠被正常發現后我們可以對分詞后的文本進行嵌入。
word2vec 生成的靜態詞嵌入無法解決一詞多義問題,BERT 等雖然可以解決一詞多義,但是對于單純的新詞發現任務/黑詞擴散等任務來說顯得有點多此一舉了。因此我們在這個 bert 已經大殺四方的時候選擇了嘗試 ELMo.
2.1 什么是靜態詞向量
靜態詞向量的生成過程:訓練 language model,將 language model 中預測的 hidden state 作為 word 的表示。給定 N 個 tokens 的序列(),前向 language model 就是基于前 k-1 個輸入序列去預測第 k 個位置的 token,訓練目標即:像常見的 word2vec, glove 生成的都是靜態詞向量。但是比較反常識的是,word2vec,glove 這些對每個詞都會生成一個確定的 emb vector。
對于前面我們提到的“溜冰”,不同語境下很明顯其 vec 應該不同
2.2 ELMo 的解決方式
ELMo 不再得到詞的確定的 emb vec, 而是得到一個訓練好的 language model(以下用 LM 代指,切記不是 likelihood maximization)。這個 LM 會基于給定的上下文“動態的”生成每個詞的嵌入。
這里再啰嗦幾句。
從結果上看,我們最終生成的 emb vec 肯定是常量而不是隨機量。
這里的動態指的是每次輸入不同的 context,嵌入都會不同,從過程上看嵌入是動態的。ELMo 本身是個根據上下文對 emb 動態調整的思路。
所以,ELMo 采用了典型的兩階段過程,第一階段利用語言模型進行預訓練,第二階段是在做下游任務時,從預訓練語言模型中提取對應單詞的 emb 作為新特征補充道下游任務中。
上文之所以寫這些是因為,我們剛開始具體在用 ELMo 的時候,忽略了第一和第二階段,以為可以直接把第一階段訓練得到的語言模型中的單詞的 emb 拿出來作為 ELMo 的產出,實際不是的。第一階段訓練完畢后,雖然每個單詞也有一個 emb,但是這個 emb 只是一個中間產物,直接拿來用效果會差到難以想象(痛徹心扉!!)
本文不贅述 ELMo 的理論,下面兩部分將分別講述 ELMo 的預訓練和實際應用。
2.3 ELMo 第一階段 -- ELMo 預訓練先上源碼,步驟:
類比我們在本文第一部分的工作,先進行新詞發現,將發現的新詞加入到分詞的詞表,進行分詞操作。
1)詞向量部分
偽代碼:for?item?in?new_words:
??????jieba.add_word(item)
stopwords?=?...
df_mid?=?df.rdd.flatMap(lambda?x:?jieba.lcut(x)).toDF(['sentence'])
w2v?=?Word2Vec(vectorSize?=?,?inputCol?=?'sentence')
model?=?w2v.fit(df_mid)
word_df?=?model.getVectors()
上面操作可以得到詞的 w2v 詞向量。由于后續詞表需要按詞頻從高到低排列,并且詞表中的詞需要和詞向量中的嵌入向量一一對應,因此這里得到嵌入詞向量之后,需要把詞向量文件按照詞頻從大到小進行排序。
詞向量文件開頭第一行分別是詞數和詞向量維度,形式如下:圖中全部的分隔符都是空格,不是'\t'
2)語料。corpus 是分詞后的語料,每一行是一個 string, 用空格分隔:3)詞表。得到詞向量部分對應的詞,詞需要按照在語料中的詞頻從高到低排列。鑒于我們在 1)中已經把詞向量按照詞頻排序了,這里只需要把詞拿出來單純保存即可。
詞表開頭必須是<S\>, </S\>, <UNK\>,且大小寫敏感,形式如下:1)和 3)部分的代碼如下:with?open('trans_data/word_vectors','r',?encoding='utf-8')?as?f:
????with?open('trans_data/vectors.txt',?'w',?encoding='utf-8')?as?fout:
????????fout.write(str(word_count)?+?'?'?+?str(dim)?+?'\n')
????????with?open('trans_data/vocab.txt',?'w',?encoding='utf-8')?as?fvocab:
????????????fvocab.write('<S>')
????????????fvocab.write('\n')
????????????fvocab.write('</S>')
????????????fvocab.write('\n')
????????????fvocab.write('<UNK>')
????????????fvocab.write('\n')
????????????for?line?in?f:
????????????????x?=?line.split('\t')
????????????????tmp?=?x[1]
????????????????tmp?=?tmp.strip('')
????????????????tmp?=?tmp.lstrip('[')
????????????????tmp?=?tmp.rstrip(']\n')
????????????????tmp?=?tmp.replace(',',?'?')
????????????????vocab.append(x[0])
????????????????item?=?x[0]?+?'?'?+?tmp?+?'\n'
????????????????fout.write(item)
????????????????fvocab.write(x[0]?+?'\n')
2.3.2 修改代碼
├──?bilm??????????????????#?模型文件目錄│???├──?__init__.py
│???├──?data.py???????????#?數據準備入口
│???├──?elmo.py???????????#?加總elmo不同層得到輸出
│???├──?model.py??????????#?雙向語言模型結構文件
│???└──?training.py???????#?模型架構
├──?bin???????????????????#?訓練文件目錄
│???├──?dump_weights.py
│???├──?restart.py
│???├──?run_test.py
│???└──?train_elmo.py?????#?訓練入口
├──?test
│???├──?test_data.py
│???├──?test_elmo.py
│???├──?test_model.py
│???└──?test_training.py
└──?usage_token.py????????#?示例
最重要的幾個文件是 bilm/training.py, bin/train_elmo.py
1)修改訓練參數 bin/train_elmo.py實際應用主要修改以下參數:
1.batch_size2.epoch3.n_gpus?and?cuda_visible_devices4.n_train_tokens5.projection_dim,決定了elmo輸出向量的維度2)修改 bilm/training.py 中的 LanguageModel 類將上面生成的詞的 w2v 向量通過initializer = tf.constant_initializer(tmp_embed)傳進去,隨后通過 embedding_lookup 查表對應到批次內的詞
3)保存 embedding 用于第二階段4)輸出訓練 loss初始代碼中并未輸出每個 batch 對應的 loss,導致開始的時候無法判斷模型是否收斂,畢竟簡單的解決方案是周期性的在日志中打印 train_perplexity
5)打印更多信息
原始代碼并未輸出詳細的訓練信息,建議在一些關鍵步驟上打印相應信息,后續調優等操作可以有的放矢。2.3.3 訓練模型,得到 vocab_embedding.hdf5
nohup?python3?-u?bin/train_elmo.py?\--train_prefix='/data/home/xxxx/elmo_data/trans_data/corpus.txt' \--vocab_file /data/home/xxxx/elmo_data/trans_data/vocab.txt \--save_dir /data/home/xxxx/bilm-tf/output_dir > /data/home/xxxx/bilm-tf/output_dir/bilm_out.txt 2>&1 &其中,
nohup: 退出 shell 不退出進程train_elmo.py: 主程序入口train_prefix: 語料路徑vocab_file: 詞表路徑save_dir: 訓練日志、checkpoint、options.json 輸出路徑輸出文件如下:不過,訓練完之后我們還得到了一個最重要的輸出:vocab_embedding.hdfs(參見 2.3.2 中的第三部分)2.3.4 得到 weights.hdf5上面一步計算得到了 ckpt 文件,下面進一步得到 model weights
nohup python3 -u bin/dump_weights.py \--save_dir /data1/home/xxxx/bilm-tf/output_dir \--outfile /data1/home/xxxx/elmo_data/trans_out/weights.hdf5 > /data1/home/xxxx/elmo_data/trans_out/bilm_out_weights.txt 2>&1 &save_dir: 上面保存 ckpt 的路徑
outfile: model weights 輸出路徑
2.3.5 總結
vocab_embedding 是 vocab 的一個初始嵌入,不是 ELMo 的最終輸出!不是 ELMo 的最終輸出!不是 ELMo 的最終輸出!
weights.hdf5 是 language model 的系數
有了:
vocab_embedding.hdf5
weights.hdf5
options.json?
就可以把 ELMo 用起來了!
BTW:訓練過程中,總共有 93246334 條語料(文本比較短,且對分詞后的文本進行了過濾,平均文本長度大概在 10 個詞),峰值 cpu 占用 50g,4 張 Tesla K40m 跑 80 個 epoch 需要 10 個小時。
2.4 ELMo 第二階段 -- 得到語料的 ELMo embedding
2.4.1 訓練代碼及過程源碼中的 usage_token.py 是 ELMo 第二階段的示例,不過例子并不好,可以基于這個示例進行改寫。下面提供一個偽代碼:
import tensorflow as tfimport osimport numpy as npfrom bilm import TokenBatcher, BidirectionalLanguageModel, weight_layers, dump_token_embeddings# 根據實際情況進行修改vocab_file = '/data/home/xxxx/elmo_data/trans_data/vocab4.txt'options_file = '/data/home/xxxx/bilm-tf/output_dir/options.json'weight_file = '/data/home/xxxx/elmo_data/trans_out/weights_8.hdf5'token_embedding_file = '/data/home/xxxx/elmo_data/trans_out/vocab_embedding_8.hdf5'tokenized_context = [['吸毒', '溜冰', '販毒', '吸毒', '販毒', '吸毒', '毒品', '吸毒'], ['定期', '組織', '吸毒', '活動', '販毒', '制毒', '毒品', '情況', '溜冰', '吸毒'], ['星期天', '中午', '組隊', '體育場', '文化宮', '溜冰', '熱愛', '輪滑', '溜友', '踴躍報名', '參加']]# Create a TokenBatcher to map text to token ids.batcher = TokenBatcher(vocab_file)# Input placeholders to the biLM.context_token_ids = tf.placeholder('int32', shape=(None, None))# Build the biLM graph.bilm = BidirectionalLanguageModel( options_file, weight_file, use_character_inputs=False, embedding_weight_file=token_embedding_file)# Get ops to compute the LM embeddings.context_embeddings_op = bilm(context_token_ids)elmo_context_output = weight_layers('output', context_embeddings_op, l2_coef=0.0)with tf.Session() as sess:# It is necessary to initialize variables once before running inference. sess.run(tf.global_variables_initializer())# Create batches of data. context_ids = batcher.batch_sentences(tokenized_context)# Compute ELMo representations (here for the output). elmo_context_output_ = sess.run( elmo_context_output['weighted_op'], feed_dict={context_token_ids: context_ids} ) print('elmo_context_ouput_:') print(elmo_context_output_.shape) print(elmo_context_output_)# ------------------------elmo_context_output才是elmo真正的輸出------------------------------## sentences similarities d1, d2, d3 = elmo_context_output_.shape# d1 = 3, d2 = 11, d3 = 128, d2=所有sentences中最大長度# 維度128 = projection_dim * 2(因為elmo會把前向和后向語言模型concat起來,所以最終生成的維度是128) group_vector_output = np.array([]).reshape(0, 128)for i in range(d1): tmp_vec_out = np.sum(elmo_context_output_[i, :, :], axis=0) # 把每個句子中所有token的emb加總起來 sentence_vector_output = np.vstack([sentence_vector_output, tmp_vec_out]) print(str(i)+"th sentence_vector_output: ") print(sentence_vector_output) print('output result')# 接下來計算三個句子間的similaritiesfor i in range(d1): vec1 = sentence_vector_output[i, :]for j in range(i+1, d1): vec2 = sentence_vector_output[j, :] num = vec1.dot(vec2.T) denom = np.linalg.norm(vec1) * np.linalg.norm(vec2) cos = num / denom print(str(i)+ ' ' + str(j) + ' ' + str(cos))# 接下來計算三個句子中“溜冰”這個單詞的相似度# elmo_context_output_[0, 1, :]對應第一個句子中的第2個token,# elmo_context_output_[1, 8, :]對應第二個句子中的第9個token,# elmo_context_output_[2, 5, :]對應第三個句子中的第6個token,# 正好分別對應著各自句子中溜冰的位置 print('0 1') num = elmo_context_output_[0, 1, :].dot(elmo_context_output_[1, 8, :].T) denom = np.linalg.norm(elmo_context_output_[0, 1, :]) * np.linalg.norm(elmo_context_output_[1, 8, :])print (num / denom) print('1 2') num = elmo_context_output_[1, 8, :].dot(elmo_context_output_[2, 5, :].T) denom = np.linalg.norm(elmo_context_output_[1, 8, :]) * np.linalg.norm(elmo_context_output_[2, 5, :]) print(num / denom) print('0 2') num = elmo_context_output_[0, 1, :].dot(elmo_context_output_[2, 5, :].T) denom = np.linalg.norm(elmo_context_output_[0, 1, :]) * np.linalg.norm(elmo_context_output_[2, 5, :]) print(num / denom)輸出:上圖表示的是三個句子兩兩之間的相似度:上圖表示的是三個句子中溜冰之間的相似度,可以看出第一和第二個句子中的溜冰相似度最高,1 和 3, 2 和 3 中溜冰的相似度都會低一些,初步看符合我們的預期。
假如上述代碼為 sen2vec.py,這一步只需要運行
python3 sen2vec.py2.4.2 小結
實際應用中可以把候選文本都過一遍 elmo,將生成的 emb 存到 hadoop 表里面,隨時調用效率會比較高。
另外可搭配上述新詞發現使用,效果更佳。
Reference:
ELMo 論文
ELMo 原理解析及簡單上手使用
如何將 ELMo 用于中文
chinese new word detection using mutual information
新詞發現
新詞發現算法探討與優化
「防水墻」是由騰訊安全團隊打造的一款覆蓋金融、廣告、電商、新零售等行業的安全防護產品,在金融領域打造了覆蓋反欺詐、反洗錢、反催收及風險情報預警的全流程產品矩陣;在廣告領域,提供流量反作弊、Kingsman、內容監控及 KOL 甄選等服務;新零售領域,覆蓋生產、流通、銷售等核心環節風控,為商超、鞋服、日化等 KA 提供全流程安全服務,深度解決羊毛黨問題。防水墻可提供諸如注冊保護、登陸保護、驗證碼、活動防刷等服務,目前為內外部客戶提供日均 500 億+次的安全防護,更多詳情可見:https://007.qq.com
總結
以上是生活随笔為你收集整理的腾讯抗黑灰产——自监督发现行话黑词识别一词多义的全部內容,希望文章能夠幫你解決所遇到的問題。

- 上一篇: Bing搜索核心技术BitFunnel原
- 下一篇: 化繁为简 - 腾讯计费高一致TDXA的实