2018年9月5日 星期三

自製直覺的文章分類程式 - 使用Python

  擔任研究助理的工作當中,主要任務之一是對資料庫資料進行撈取與分類,撈取的過程就是單純地以關鍵字進行搜尋,然而分類的過程就不容易了,必須自行設定合理的分類架構與分類說明,並實際將這些已撈取的資料分類,若該筆資料可同時歸類在不同類別,亦可重複分類。

  資料分類的過程極其耗時費力又無意義,但是似乎又難以將這樣重複單調的任務標準化、程式化,於是只好先硬著頭皮以人工進行資料分類,觀察以人工分類時可以歸納出怎樣的分類邏輯。這些資料為中文的文章,包含年分、標題、關鍵詞、摘要等眾多欄位,起初分類時,會依自己的經驗,閱讀所有欄位後判斷資料性質再進行分類,但是這樣的做法耗時費力,分類準則變得更加主觀,又容易隨著自己「心中的那把尺」改變而影響分類結果,因此在分類的過程中自己也掙扎了許久。

  其實若要做到非主觀的分類,最後還是得回到「關鍵字」,例如專題主題為「雲端運算」,分類架構包含「財稅」、「警政」、「教育」、「交通」等類別,我們在閱讀資料文章的時候,只要看到「車載」這個關鍵字,就幾乎可以不加思索地將該筆資料歸類在「交通」類別,而只要看到「治安」這個關鍵字,也幾乎可以直接將該筆資料歸類在「警政」類別,因此我們可以發現,資料分類的任務是有跡可循的,我們能夠將之自動化。

  一般來說,文章分類程式會以機器學習、支援向量機(Support Vector Machine, SVM)等方式來製作,因為諸如「車載」、「治安」這類關鍵字不勝枚舉,難以全盤掌握,直接讓程式以機率統計的方式來處理比較好。

  筆者自製文章分類程式時,起初僅以正規表示式,使用人工分類時累積的經驗,將「車載」、「治安」這類特色關鍵字逐一列出,讓程式自動分類新的文章;由於筆者尚未接觸機器學習領域,在工作上又必須承接他人的資料分類專題,因此在沒有機器學習技術,又無人工分類經驗之下,只好以「直覺」的方式,利用中文斷詞工具,透過已分類文章協助產出分類時所需之特色關鍵字,再將這些特色關鍵字傳回正規表示式,讓程式自動分類新的文章。

文章分類程式的運作邏輯圖,以下將製作三個步驟的程式原型功能。

  以下將整理結巴中文斷詞套件的功能,並將文章分類程式分為三個步驟來製作原型,以便未來實作文章分類程式時,能夠依照實際情形調整程式碼,再將之整併起來使用,若需要在其它沒有安裝Python的電腦使用文章分類程式時,可以先使用pyinstaller.py檔打包成.exe檔,往後在其它沒有安裝Python的電腦上亦可使用這樣的文章分類程式了。

pyinstaller

pyinstaller
l   若需將.py檔打包成.exe檔,在命令提示字元輸入pip install pyinstaller,下載並安裝pyinstaller
l   # 在命令提示字元輸入以查看參數
pyinstaller -h
#
在命令提示字元輸入以打包.py
pyinstaller -F .\test.py

jieba

jieba - 結巴中文斷詞台灣繁體版本
l   下載與安裝:
英文可以使用空格執行斷詞,並透過ngram的概念進行切詞分析,然而中文為方塊字,斷詞不易執行,因此建議透過第三方套件協助執行;可在命令提示字元輸入pip install jieba,下載並安裝jieba,然而該套件為中國大陸開發,因此詞庫及HMM機率表皆為適用對岸的版本,加上必須有簡體中文轉換繁體中文的額外步驟,因此推薦使用交大實驗室改良的jieba分支版本,在其GitHub下載後,將jieba資料夾複製到以下路徑即可使用:
Python
安裝路徑\Lib\site-packages
l   三種斷詞模式:
import jieba
content = open("article.txt", "r").read()

#
精確模式:將句子最精確地切開,為預設模式
words = jieba.lcut(content, cut_all = False)

#
全模式:把句子所有詞語都掃描出來
# words = jieba.lcut(content, cut_all = True)

#
搜索引擎模式:在精確模式基礎上對長詞再次切分
# words = jieba.lcut_for_search(content)

print(words)
l   三種斷詞模式輸出內容比較:
原始內容:
艾倫.狄波頓一九六九年生於瑞士蘇黎士,從八歲起在英國接受教育,曾求學於頂尖的哈羅學院與劍橋大學。

精確模式輸出內容:
['
艾倫', '', '', '波頓', '一九六九年', '生於', '瑞士', '蘇黎士', '', '', '', '', '', '', '英國', '接受', '教育', '', '', '求學', '', '頂尖', '', '哈羅', '學院', '', '劍橋', '大學', '']

全模式輸出內容:
['
艾倫', '', '', '', '波頓', '一九六九年', '六九年', '九年', '生於', '瑞士', '蘇黎', '蘇黎士', '', '', '', '', '', '', '', '英國', '接受', '受教', '教育', '', '', '', '求學', '', '頂尖', '', '', '', '學院', '', '劍橋', '大學', '', '']

搜索引擎模式輸出內容:
['
艾倫', '', '', '波頓', '九年', '六九年', '一九六九年', '生於', '瑞士', '蘇黎', '蘇黎士', '', '', '', '', '', '', '英國', '接受', '教育', '', '', '求學', '', '頂尖', '', '哈羅', '學院', '', '劍橋', '大學', '']

詞典
l   自定義詞典的格式和預設詞典dict.txt的格式相同,一個詞佔一行,每一行包含用空格隔開的詞語、詞頻(可省略)、詞性(可省略),存檔時編碼必須為UTF-8
l   修改預設詞典:
import jieba
# https://raw.githubusercontent.com/fxsjy/jieba/master/extra_dict/dict.txt.big
jieba.set_dictionary("dict.txt")
content = open("article.txt", "r").read()
words = jieba.lcut(content, cut_all = False)
print(words)
l   修改預設詞典後精確模式輸出內容:
['
艾倫', '', '', '波頓', '一九六九年', '生於', '瑞士', '蘇黎士', '', '', '八歲', '', '', '英國', '接受', '教育', '', '', '求學', '', '頂尖', '', '哈羅', '學院', '', '劍橋大學', '']
l   加入自訂詞典:
import jieba
# userdict.txt
內含"狄波頓""哈羅學院""劍橋大學"三個詞語
jieba.load_userdict("userdict.txt")
content = open("article.txt", "r").read()
words = jieba.lcut(content, cut_all = False)
print(words)
l   加入自訂詞典後精確模式輸出內容:
['
艾倫', '', '狄波頓', '一九六九年', '生於', '瑞士', '蘇黎士', '', '', '', '', '', '', '英國', '接受', '教育', '', '', '求學', '', '頂尖', '', '哈羅學院', '', '劍橋大學', '']
l   加入停用詞典:
import jieba
# stopdict.txt
內含音界號()、逗號()、句號()三個符號
stopContent = open("stopdict.txt", "r").read()
stopWords = stopContent.split()
content = open("article.txt", "r").read()
words = jieba.lcut(content, cut_all = False)
selectedWords = [word for word in words if word not in stopWords]
print(selectedWords)
l   加入停用詞典後精確模式輸出內容:
['
艾倫', '', '波頓', '一九六九年', '生於', '瑞士', '蘇黎士', '', '', '', '', '', '英國', '接受', '教育', '', '求學', '', '頂尖', '', '哈羅', '學院', '', '劍橋', '大學']

關鍵字
l   使用TF-IDF演算法抽取關鍵字:
import jieba.analyse
content = open("article.txt", "r").read()
words = jieba.analyse.extract_tags(content)
print(words)
l   使用TF-IDF演算法抽取關鍵字後輸出內容:
['
艾倫', '波頓', '生於', '蘇黎士', '英國', '求學', '頂尖', '哈羅', '學院', '劍橋', '大學', '一九六九年', '瑞士', '教育', '接受']
l   使用TF-IDF演算法並加入停用詞典以抽取關鍵字:
import jieba.analyse
# stopdict.txt
內含"接受"一個詞語
jieba.analyse.set_stop_words("stopdict.txt")
content = open("article.txt", "r").read()
words = jieba.analyse.extract_tags(content)
print(words)
l   使用TF-IDF演算法並加入停用詞典以抽取關鍵字後輸出內容:
['
艾倫', '波頓', '生於', '蘇黎士', '英國', '求學', '頂尖', '哈羅', '學院', '劍橋', '大學', '一九六九年', '瑞士', '教育']

文章分類程式原型功能-步驟一

# 步驟一-解析相同類別文章的關鍵字
import os, jieba
from collections import Counter

# 如果有已解析的關鍵字存檔,載入以累加記錄關鍵字
oldWords = []
if os.path.exists("wordsSave.txt"):
    savedContent = open("wordsSave.txt", "r")
    savedWords = savedContent.read().split()
    for i in range(len(savedWords)):
        # 僅載入關鍵字
        if i % 2 == 0:
            # 依照計次載入關鍵字
            for j in range(int(savedWords[i + 1])):
                oldWords.append(savedWords[i])
    savedContent.close()

# 加入自訂詞典
jieba.load_userdict("userdict.txt")
# 載入未解析的相同類別文章以解析關鍵字
content = open("article.txt", "r")
# 刪除換行字元並進行斷詞
words = jieba.lcut(content.read().replace("\n", ""), cut_all = False)
# 刪除單個字元的關鍵字
words = [word for word in words if len(word) > 1]
content.close()

# 累加記錄關鍵字
for oldword in oldWords:
    words.append(oldword)
# 計次並排序
sortedList = sorted(Counter(words).items(), key = lambda a : a[1], reverse = True)

# 存檔已解析的關鍵字
wordsSave = open("wordsSave.txt", "w")
for item in sortedList:
    for subItem in item:
        #
一行為一關鍵字與其計次
        wordsSave.write(str(subItem) + " ")
    wordsSave.write("\n")
wordsSave.close()

文章分類程式原型功能-步驟二

# 步驟二-比較不同類別文章的關鍵字,找出各類別的特色關鍵字
import os

# 不同類別文章的關鍵字存檔結尾皆為wordsSave.txt,此串列儲存檔名開頭
filenameHead = []
# 此串列儲存不同類別文章的關鍵字集合
allTypeWords = []

# 載入不同類別文章的關鍵字存檔
for filename in os.listdir(os.path.abspath(".")):
    if filename.endswith("wordsSave.txt"):
        # 儲存檔名開頭
        filenameHead.append(filename.rstrip("wordsSave.txt"))
        # 儲存關鍵字集合
        oldWords = []
        savedContent = open(filename, "r")
        savedWords = savedContent.read().split()
        for i in range(len(savedWords)):
            # 僅載入關鍵字
            if i % 2 == 0:
                # 計次若少於指定數值則不載入該關鍵字
                if int(savedWords[i + 1]) < 3:
                    continue
                oldWords.append(savedWords[i])
        savedContent.close()
        allTypeWords.append(set(oldWords))

# 解析並存檔不同類別文章的特色關鍵字
for filenameNum in range(len(filenameHead)):
    # 將目標類別以外的關鍵字做聯集
    unitedWords = set()
    for allTypeNum in range(len(allTypeWords)):
        if filenameNum != allTypeNum:
            unitedWords = unitedWords.union(allTypeWords[allTypeNum])
    # 將目標類別的關鍵字與其它類別的關鍵字做差集
    traitsWords = allTypeWords[filenameNum].difference(unitedWords)
   
    # 存檔已解析的特色關鍵字
    traitsSave = open(filenameHead[filenameNum] + "traitsSave.txt", "w")
    for traitsWord in traitsWords:
        # 一行為一關鍵字
        traitsSave.write(traitsWord + "\n")
    traitsSave.close()

文章分類程式原型功能-步驟三

# 步驟三-特色關鍵字搭配正規表示式,嘗試以程式分類新的文章
import re

# 載入特色關鍵字存檔並以管道字元組裝
traitsContent = open("traitsSave.txt", "r")
traitsWords = "|".join(traitsContent.read().split())
traitsContent.close()

# 載入新的文章
newContent = open("article.txt", "r")
newArticle = newContent.read().replace("\n", "")
newContent.close()

# 以特色關鍵字搜尋新的文章
aRegex = re.compile(r"("+traitsWords+")")
aMatch = aRegex.findall(r"("+newArticle+")")

# 以搜尋結果判斷是否將該文章歸類該類別
if aMatch != []:
    print("The Article Would Be Classified")
    print(aMatch)
else:
    print("The Article Would Not Be Classified")

資料分類實驗的結果

  本實驗以筆者承接前人的資料分類專題做為試驗,前人留下共688筆資料,以人工歸類為19項,其後筆者繼續以人工分類381筆新資料。以688筆舊資料當作擷取19項分類的特色關鍵字標準,並以程式分類381筆新資料,對照人工分類與程式分類的差異如下表:

表一、以.xlsx檔不同欄位做為分類標準之結果差異
l   做為分類標準之欄位:資料名稱
l   人工分類與程式分類結果相同的儲存格數:6745
人工分類與程式分類結果不同的儲存格數:494
儲存格分類相同比率:93.18%
l   人工分類與程式分類結果相同的資料筆數:93
人工分類與程式分類結果不同的資料筆數:288
資料分類相同比率:24.41%
l   做為分類標準之欄位:資料關鍵字
l   人工分類與程式分類結果相同的儲存格數:6729
人工分類與程式分類結果不同的儲存格數:510
儲存格分類相同比率:92.95%
l   人工分類與程式分類結果相同的資料筆數:41
人工分類與程式分類結果不同的資料筆數:340
資料分類相同比率:10.76%
l   做為分類標準之欄位:資料摘要
l   人工分類與程式分類結果相同的儲存格數:5262
人工分類與程式分類結果不同的儲存格數:1977
儲存格分類相同比率:72.69%
l   人工分類與程式分類結果相同的資料筆數:9
人工分類與程式分類結果不同的資料筆數:372
資料分類相同比率:2.36%
l   做為分類標準之欄位:資料名稱 + 資料關鍵字
l   人工分類與程式分類結果相同的儲存格數:6638
人工分類與程式分類結果不同的儲存格數:601
儲存格分類相同比率:91.7%
l   人工分類與程式分類結果相同的資料筆數:71
人工分類與程式分類結果不同的資料筆數:310
資料分類相同比率:18.64%
l   做為分類標準之欄位:資料關鍵字 + 資料摘要
l   人工分類與程式分類結果相同的儲存格數:5194
人工分類與程式分類結果不同的儲存格數:2045
儲存格分類相同比率:71.75%
l   人工分類與程式分類結果相同的資料筆數:10
人工分類與程式分類結果不同的資料筆數:371
資料分類相同比率:2.62%
l   做為分類標準之欄位:資料摘要 + 資料名稱
l   人工分類與程式分類結果相同的儲存格數:5188
人工分類與程式分類結果不同的儲存格數:2051
儲存格分類相同比率:71.67%
l   人工分類與程式分類結果相同的資料筆數:4
人工分類與程式分類結果不同的資料筆數:377
資料分類相同比率:1.05%
l   做為分類標準之欄位:資料名稱 + 資料關鍵字 + 資料摘要
l   人工分類與程式分類結果相同的儲存格數:5143
人工分類與程式分類結果不同的儲存格數:2096
儲存格分類相同比率:71.05%
l   人工分類與程式分類結果相同的資料筆數:4
人工分類與程式分類結果不同的資料筆數:377
資料分類相同比率:1.05%
備註:共有19項分類,因此1筆資料含有19格儲存格,儲存格總數為資料總數的19

  分類結果令人驚艷,因為這是未使用自訂詞典與停用詞典,以及未檢視特色關鍵字詞典良窳之下進行的分類;筆者在程式中只排除換行字元、單個字元,以及資料摘要中總出現頻率低於3次的詞彙,因此實際上,本分類實驗仍有大量雜訊詞彙在干擾分類結果,即便如此,資料分類相同比率最高也有24.41%,請記得本實驗中1筆資料內含19格儲存格,人工分類的19格儲存格必須與程式分類的19格儲存格完全相同,才會將該筆資料算入資料分類相同比率,這是相當嚴格的判斷標準。

  從另一個角度發想,人工分類一定擁有比較好的分類品質嗎?這方面我是懷疑的,因為本實驗是筆者承接前人的資料分類專題來進行的,因此前人的分類準則與筆者的分類準則很可能從一開始就有差異:就筆者觀察本實驗中特色關鍵字分類新資料時的紀錄,已可以發現前人分類資料時「心中的那把尺」,似乎與筆者分類資料時有些微的差異。

  文章分類是一件無趣,亦無重要附加價值的工作,這也導致了資料分類專題負責人頻頻更換、缺乏一致性等後果,這些現象對整個專題而言,長遠之下將帶來致命的後果,因為人工分類的標準不一,即便再明確地闡述分類準則,分類的最終決定權仍在分類者當下的判斷,因此筆者才會提倡程式分類,除了可以降低資料分類者的負擔,更可以協助維持整個專題的長遠運作,儘管這可能不是一系列好用的分類程式,但絕對勝過純粹以人工執行這樣的工作任務。

沒有留言:

張貼留言