歡迎光臨
每天分享高質量文章

實戰分享之專業領域詞彙無監督挖掘

作者丨蘇劍林

單位丨廣州火焰資訊科技有限公司

研究方向丨NLP,神經網路

個人主頁丨kexue.fm

去年 Data Fountain 曾舉辦了一個“電力專業領域詞彙挖掘”的比賽,該比賽有意思的地方在於它是一個“無監督”的比賽,也就是說它考驗的是從大量的語料中無監督挖掘專業詞彙的能力。 

 

大賽主頁:

https://www.datafountain.cn/competitions/320/details

 

這個確實是工業界比較有價值的一個能力,又想著我之前也在無監督新詞發現中做過一定的研究,加之“無監督比賽”的新穎性,所以當時毫不猶豫地參加了,然而最終排名並不靠前。

 

不管怎樣,還是分享一下我自己的做法,這是一個真正意義上的無監督做法,也許會對部分讀者有些參考價值。

基準對比

 

首先,新詞發現部分,用到了我自己寫的庫 NLP Zero,基本思路是先分別對“比賽所給語料”、“自己爬的一部分百科百科語料”做新詞發現,然後兩者進行對比,就能找到一批“比賽所給語料”的特徵詞。 

 

NLP Zero:

https://kexue.fm/archives/5597

 

參考的原始碼是:

 

from nlp_zero import *
import re
import pandas as pd
import pymongo
import logging
logging.basicConfig(level = logging.INFO, format = '%(asctime)s - %(name)s - %(message)s')


class D: # 讀取比賽方所給語料
    def __iter__(self):
        with open('data.txt'as f:
            for l in f:
                l = l.strip().decode('utf-8')
                l = re.sub(u'[^\u4e00-\u9fa5]+'' ', l)
                yield l


class DO: # 讀取自己的語料(相當於平行語料)
    def __iter__(self):
        db = pymongo.MongoClient().baike.items
        for i in db.find().limit(300000):
            l = i['content']
            l = re.sub(u'[^\u4e00-\u9fa5]+'' ', l)
            yield l


# 在比賽方語料中做新詞發現
f = Word_Finder(min_proba=1e-6, min_pmi=0.5)
f.train(D()) # 統計互資訊
f.find(D()) # 構建詞庫

# 匯出詞表
words = pd.Series(f.words).sort_values(ascending=False)


# 在自己的語料中做新詞發現
fo = Word_Finder(min_proba=1e-6, min_pmi=0.5)
fo.train(DO()) # 統計互資訊
fo.find(DO()) # 構建詞庫

# 匯出詞表
other_words = pd.Series(fo.words).sort_values(ascending=False)
other_words = other_words / other_words.sum() * words.sum() # 總詞頻歸一化(這樣才便於對比)


"""對比兩份語料詞頻,得到特徵詞。
對比指標是(比賽方語料的詞頻 + alpha)/(自己語料的詞頻 + beta);
alpha和beta的計算參考自 http://www.matrix67.com/blog/archives/5044
"""

WORDS = words.copy()
OTHER_WORDS = other_words.copy()

total_zeros = (WORDS + OTHER_WORDS).fillna(0) * 0
words = WORDS + total_zeros
other_words = OTHER_WORDS + total_zeros
total = words + other_words

alpha = words.sum() / total.sum()

result = (words + total.mean() * alpha) / (total + total.mean())
result = result.sort_values(ascending=False)
idxs = [i for i in result.index if len(i) >= 2# 排除掉單字詞

# 匯出csv格式
pd.Series(idxs[:20000]).to_csv('result_1.csv', encoding='utf-8', essay-header=None, index=None)

語意篩選

 

註意到,按照上述方法匯出來的詞表,頂多算是“語料特徵詞”,但是還不完全是“電力專業領域詞彙”。如果著眼於電力詞彙,那麼需要對詞表進行語意上的篩選。

我的做法是:用匯出來的詞表對比賽語料進行分詞,然後訓練一個 Word2Vec 模型,根據 Word2Vec 得到的詞向量來對詞進行聚類。

首先是訓練 Word2Vec:

 

# nlp zero提供了良好的封裝,可以直到匯出一個分詞器,詞表是新詞發現得到的詞表。
tokenizer = f.export_tokenizer()

class DW:
    def __iter__(self):
        for l in D():
            yield tokenizer.tokenize(l, combine_Aa123=False)


from gensim.models import Word2Vec

word_size = 100
word2vec = Word2Vec(DW(), size=word_size, min_count=2, sg=1, negative=10)

然後是聚類,不過這不是嚴格意義上的聚類,而是根據我們自己跳出來的若干個種子詞,然後找到一批相似詞來。演演算法是用相似的傳遞性(有點類似基於連通性的聚類演演算法),即 A 和 B 相似,B 和 C也相似,那麼 A、B、C 就聚為一類(哪怕A、C從指標上看是不相似的)。

當然,這樣傳遞下去很可能把整個詞表都遍歷了,所以要逐步加強對相似的限制。比如 A 是種子詞,B、C 都不是種子詞,A、B 的相似度為 0.6 就定義它為相似,B、C 的相似度要大於 0.7 才能認為它們相似,不然這樣一級級地傳遞下去,後面的詞就會離種子詞的語意越來越遠。

聚類演演算法如下:

import numpy as np
from multiprocessing.dummy import Queue


def most_similar(word, center_vec=None, neg_vec=None):
    """根據給定詞、中心向量和負向量找最相近的詞
    """
    vec = word2vec[word] + center_vec - neg_vec
    return word2vec.similar_by_vector(vec, topn=200)


def find_words(start_words, center_words=None, neg_words=None, min_sim=0.6, max_sim=1., alpha=0.25):
    if center_words == None and neg_words == None:
        min_sim = max(min_sim, 0.6)
    center_vec, neg_vec = np.zeros([word_size]), np.zeros([word_size])
    if center_words: # 中心向量是所有種子詞向量的平均
        _ = 0
        for w in center_words:
            if w in word2vec.wv.vocab:
                center_vec += word2vec[w]
                _ += 1
        if _ > 0:
            center_vec /= _
    if neg_words: # 負向量是所有負種子詞向量的平均(本文沒有用到它)
        _ = 0
        for w in neg_words:
            if w in word2vec.wv.vocab:
                neg_vec += word2vec[w]
                _ += 1
        if _ > 0:
            neg_vec /= _
    queue_count = 1
    task_count = 0
    cluster = []
    queue = Queue() # 建立佇列
    for w in start_words:
        queue.put((0, w))
        if w not in cluster:
            cluster.append(w)
    while not queue.empty():
        idx, word = queue.get()
        queue_count -= 1
        task_count += 1
        sims = most_similar(word, center_vec, neg_vec)
        min_sim_ = min_sim + (max_sim-min_sim) * (1-np.exp(-alpha*idx))
        if task_count % 10 == 0:
            log = '%s in cluster, %s in queue, %s tasks done, %s min_sim'%(len(cluster), queue_count, task_count, min_sim_)
            print log
        for i,j in sims:
            if j >= min_sim_:
                if i not in cluster and is_good(i): # is_good是人工寫的過濾規則
                    queue.put((idx+1, i))
                    if i not in cluster and is_good(i):
                        cluster.append(i)
                    queue_count += 1
    return cluster


規則過濾

總的來說,無監督演演算法始終是難以做到完美的,在工程上,常見的方法就是人工觀察結果然後手寫一些規則來處理。在這個任務中,由於前面是純無監督的,哪怕進行了語意聚類,還是會出來一些非電力專業詞彙(比如“麥克斯韋方程”),甚至還保留一些“非詞”,所以我寫了一通規則來過濾(寫得有點醜):

 

def is_good(w):
    if re.findall(u'[\u4e00-\u9fa5]', w) \
        and len(i) >= 2\
        and not re.findall(u'[較很越增]|[多少大小長短高低好差]', w)\
        and not u'的' in w\
        and not u'了' in w\
        and not u'這' in w\
        and not u'那' in w\
        and not u'到' in w\
        and not w[-1in u'為一人給內中後省市局院上所在有與及廠稿下廳部商者從獎出'\
        and not w[0in u'每各該個被其從與及當為'\
        and not w[-2:] in [u'問題'u'市場'u'郵件'u'合約'u'假設'u'編號'u'預算'u'施加'u'戰略'u'狀況'u'工作'u'考核'u'評估'u'需求'u'溝通'u'階段'u'賬號'u'意識'u'價值'u'事故'u'競爭'u'交易'u'趨勢'u'主任'u'價格'u'門戶'u'治區'u'培養'u'職責'u'社會'u'主義'u'辦法'u'幹部'u'員會'u'商務'u'發展'u'原因'u'情況'u'國家'u'園區'u'夥伴'u'對手'u'標的'u'委員'u'人員'u'如下'u'況下'u'見圖'u'全國'u'創新'u'共享'u'資訊'u'隊伍'u'農村'u'貢獻'u'爭力'u'地區'u'客戶'u'領域'u'查詢'u'應用'u'可以'u'運營'u'成員'u'書記'u'附近'u'結果'u'經理'u'學位'u'經營'u'思想'u'監管'u'能力'u'責任'u'意見'u'精神'u'講話'u'營銷'u'業務'u'總裁'u'見表'u'電力'u'主編'u'作者'u'專輯'u'學報'u'建立'u'支援'u'資助'u'規劃'u'計劃'u'資金'u'代表'u'部門'u'版社'u'表明'u'證明'u'專家'u'教授'u'教師'u'基金'u'如圖'u'位於'u'從事'u'公司'u'企業'u'專業'u'思路'u'集團'u'建設'u'管理'u'水平'u'領導'u'體系'u'政務'u'單位'u'部分'u'董事'u'院士'u'經濟'u'意義'u'內部'u'專案'u'建設'u'服務'u'總部'u'管理'u'討論'u'改進'u'文獻']\
        and not w[:2in [u'考慮'u'圖中'u'每個'u'出席'u'一個'u'隨著'u'不會'u'本次'u'產生'u'查詢'u'是否'u'作者']\
        and not (u'博士' in w or u'碩士' in w or u'研究生' in w)\
        and not (len(set(w)) == 1 and len(w) > 1)\
        and not (w[0in u'一二三四五六七八九十' and len(w) == 2)\
        and re.findall(u'[^一七廠月二夕氣產蘭丫田洲戶尹屍甲乙日卜幾口工舊門目曰石悶匕勺]', w)\
        and not u'進一步' in w:
        return True
    else:
        return False

至此,我們就可以完整地執行這個演演算法了:

 

# 種子詞,在第一步得到的詞表中的前面部分挑一挑即可,不需要特別準
start_words = [u'電網'u'電壓'u'直流'u'電力系統'u'變壓器'u'電流'u'負荷'u'發電機'u'變電站'u'機組'u'母線'u'電容'u'放電'u'等效'u'節點'u'電機'u'故障'u'輸電線路'u'波形'u'電感'u'導線'u'繼電'u'輸電'u'引數'u'無功'u'線路'u'模擬'u'功率'u'短路'u'控制器'u'諧波'u'勵磁'u'電阻'u'模型'u'開關'u'繞組'u'電力'u'電廠'u'演演算法'u'供電'u'阻抗'u'排程'u'發電'u'場強'u'電源'u'負載'u'擾動'u'儲能'u'電弧'u'配電'u'繫數'u'雷電'u'輸出'u'並聯'u'迴路'u'濾波器'u'電纜'u'分散式'u'故障診斷'u'充電'u'絕緣'u'接地'u'感應'u'額定'u'高壓'u'相位'u'可靠性'u'數學模型'u'接線'u'穩態'u'誤差'u'電場強度'u'電容器'u'電場'u'線圈'u'非線性'u'接入'u'模態'u'神經網路'u'頻率'u'風速'u'小波'u'補償'u'電路'u'曲線'u'峰值'u'容量'u'有效性'u'取樣'u'訊號'u'電極'u'實測'u'變電'u'間隙'u'模組'u'試驗'u'濾波'u'量測'u'元件'u'最優'u'損耗'u'特性'u'諧振'u'帶電'u'瞬時'u'阻尼'u'轉速'u'最佳化'u'低壓'u'系統'u'停電'u'選取'u'感測器'u'耦合'u'振蕩'u'線性'u'資訊系統'u'矩陣'u'可控'u'脈衝'u'控制'u'套管'u'監控'u'汽輪機'u'擊穿'u'延時'u'聯絡線'u'向量'u'整流'u'傳輸'u'檢修'u'模擬'u'高頻'u'測量'u'樣本'u'高階工程師'u'變換'u'試樣'u'試驗研究'u'平均值'u'向量'u'特徵值'u'導體'u'電暈'u'磁通'u'千伏'u'切換'u'響應'u'效率']

cluster_words = find_words(start_words, min_sim=0.6, alpha=0.35)

result2 = result[cluster_words].sort_values(ascending=False)
idxs = [i for i in result2.index if is_good(i)]

pd.Series([i for i in idxs if len(i) > 2][:10000]).to_csv('result_1_2.csv', encoding='utf-8', essay-header=None, index=None)

 

最終結果(部分):

 

變壓器
發電機
變電站
過電壓
可靠性
控制器
斷路器
分散式
輸電線路
數學模型
濾波器
電容器
故障診斷
神經網路
直流電壓
等離子體
聯絡線
感測器
汽輪機
閘流體
電動機
約束條件
資料庫
可行性
持續時間
整流器
穩定性
調節器
電磁場

後記感想

本文的演演算法在榜上的成績大約是 0.22 左右,封榜時排在 100 名左右,榜首已經是 0.49 了,所以從成績來看其實沒什麼值得炫耀的。不過當時聽說不少人拿現成的專業詞典去做字標註,所以當時就沒做下去了。要是真的那樣子的話,我覺得就很沒意思了。

總之,本文算是提供了一個無監督抽取專業詞的實現模版,如果讀者覺得有可取之處,大方取之即可;如果覺得一無是處,敬請無視它。

已同步到看一看
贊(0)

分享創造快樂