基於 Python 的簡單自然語言處理實踐

王下邀月熊_Chevalier發表於2017-04-08

基於 Python 的簡單自然語言處理實踐 從屬於筆者的 程式猿的資料科學與機器學習實戰手冊

基於 Python 的簡單自然語言處理

本文是對於基於 Python 進行簡單自然語言處理任務的介紹,本文的所有程式碼放置在這裡。建議前置閱讀 Python 語法速覽與機器學習開發環境搭建,更多機器學習資料參考機器學習、深度學習與自然語言處理領域推薦的書籍列表以及面向程式猿的資料科學與機器學習知識體系及資料合集

Twenty News Group 語料集處理

20 Newsgroup 資料集包含了約 20000 篇來自於不同的新聞組的文件,最早由 Ken Lang 蒐集整理。本部分包含了對於資料集的抓取、特徵提取、簡單分類器訓練、主題模型訓練等。本部分程式碼包括主要的處理程式碼封裝庫基於 Notebook 的互動示範。我們首先需要進行資料抓取:

    def fetch_data(self, subset=`train`, categories=None):
        """return data
        執行資料抓取操作
        Arguments:
        subset -> string -- 抓取的目標集合 train / test / all
        """
        rand = np.random.mtrand.RandomState(8675309)
        data = fetch_20newsgroups(subset=subset,
                                  categories=categories,
                                  shuffle=True,
                                  random_state=rand)

        self.data[subset] = data

然後在 Notebook 中互動檢視資料格式:

# 例項化物件
twp = TwentyNewsGroup()
# 抓取資料
twp.fetch_data()
twenty_train = twp.data[`train`]
print("資料集結構", "->", twenty_train.keys())
print("文件數目", "->", len(twenty_train.data))
print("目標分類", "->",[ twenty_train.target_names[t] for t in twenty_train.target[:10]])

資料集結構 -> dict_keys([`data`, `filenames`, `target_names`, `target`, `DESCR`, `description`])
文件數目 -> 11314
目標分類 -> [`sci.space`, `comp.sys.mac.hardware`, `sci.electronics`, `comp.sys.mac.hardware`, `sci.space`, `rec.sport.hockey`, `talk.religion.misc`, `sci.med`, `talk.religion.misc`, `talk.politics.guns`]

接下來我們可以對語料集中的特徵進行提取:

# 進行特徵提取

# 構建文件-詞矩陣(Document-Term Matrix)

from sklearn.feature_extraction.text import CountVectorizer

count_vect = CountVectorizer()

X_train_counts = count_vect.fit_transform(twenty_train.data)

print("DTM 結構","->",X_train_counts.shape)

# 檢視某個詞在詞表中的下標
print("詞對應下標","->", count_vect.vocabulary_.get(u`algorithm`))

DTM 結構 -> (11314, 130107)
詞對應下標 -> 27366

為了將文件用於進行分類任務,還需要使用 TF-IDF 等常見方法將其轉化為特徵向量:

# 構建文件的 TF 特徵向量
from sklearn.feature_extraction.text import TfidfTransformer

tf_transformer = TfidfTransformer(use_idf=False).fit(X_train_counts)
X_train_tf = tf_transformer.transform(X_train_counts)

print("某文件 TF 特徵向量","->",X_train_tf)

# 構建文件的 TF-IDF 特徵向量
from sklearn.feature_extraction.text import TfidfTransformer

tf_transformer = TfidfTransformer().fit(X_train_counts)
X_train_tfidf = tf_transformer.transform(X_train_counts)

print("某文件 TF-IDF 特徵向量","->",X_train_tfidf)

某文件 TF 特徵向量 ->   (0, 6447)    0.0380693493813
  (0, 37842)    0.0380693493813

我們可以將特徵提取、分類器訓練與預測封裝為單獨函式:

    def extract_feature(self):
        """
        從語料集中抽取文件特徵
        """

        # 獲取訓練資料的文件-詞矩陣
        self.train_dtm = self.count_vect.fit_transform(self.data[`train`].data)

        # 獲取文件的 TF 特徵

        tf_transformer = TfidfTransformer(use_idf=False)

        self.train_tf = tf_transformer.transform(self.train_dtm)

        # 獲取文件的 TF-IDF 特徵

        tfidf_transformer = TfidfTransformer().fit(self.train_dtm)

        self.train_tfidf = tf_transformer.transform(self.train_dtm)

    def train_classifier(self):
        """
        從訓練集中訓練出分類器
        """

        self.extract_feature();

        self.clf = MultinomialNB().fit(
            self.train_tfidf, self.data[`train`].target)

    def predict(self, docs):
        """
        從訓練集中訓練出分類器
        """

        X_new_counts = self.count_vect.transform(docs)

        tfidf_transformer = TfidfTransformer().fit(X_new_counts)
        
        X_new_tfidf = tfidf_transformer.transform(X_new_counts)

        return self.clf.predict(X_new_tfidf)

然後執行訓練並且進行預測與評價:

# 訓練分類器
twp.train_classifier()

# 執行預測
docs_new = [`God is love`, `OpenGL on the GPU is fast`]
predicted = twp.predict(docs_new)

for doc, category in zip(docs_new, predicted):
    print(`%r => %s` % (doc, twenty_train.target_names[category]))
    
# 執行模型評測
twp.fetch_data(subset=`test`)

predicted = twp.predict(twp.data[`test`].data)

import numpy as np

# 誤差計算

# 簡單誤差均值
np.mean(predicted == twp.data[`test`].target)   

# Metrics

from sklearn import metrics

print(metrics.classification_report(
    twp.data[`test`].target, predicted,
    target_names=twp.data[`test`].target_names))

# Confusion Matrix
metrics.confusion_matrix(twp.data[`test`].target, predicted)

`God is love` => soc.religion.christian
`OpenGL on the GPU is fast` => rec.autos
                          precision    recall  f1-score   support

             alt.atheism       0.79      0.50      0.61       319
           ...
      talk.religion.misc       1.00      0.08      0.15       251

             avg / total       0.82      0.79      0.77      7532

Out[16]:
array([[158,   0,   1,   1,   0,   1,   0,   3,   7,   1,   2,   6,   1,
          8,   3, 114,   6,   7,   0,   0],
       ...
       [ 35,   3,   1,   0,   0,   0,   1,   4,   1,   1,   6,   3,   0,
          6,   5, 127,  30,   5,   2,  21]])

我們也可以對文件集進行主題提取:

# 進行主題提取

twp.topics_by_lda()

Topic 0 : stream s1 astronaut zoo laurentian maynard s2 gtoal pem fpu
Topic 1 : 145 cx 0d bh sl 75u 6um m6 sy gld
Topic 2 : apartment wpi mars nazis monash palestine ottoman sas winner gerard
Topic 3 : livesey contest satellite tamu mathew orbital wpd marriage solntze pope
Topic 4 : x11 contest lib font string contrib visual xterm ahl brake
Topic 5 : ax g9v b8f a86 1d9 pl 0t wm 34u giz
Topic 6 : printf null char manes behanna senate handgun civilians homicides magpie
Topic 7 : buf jpeg chi tor bos det que uwo pit blah
Topic 8 : oracle di t4 risc nist instruction msg postscript dma convex
Topic 9 : candida cray yeast viking dog venus bloom symptoms observatory roby
Topic 10 : cx ck hz lk mv cramer adl optilink k8 uw
Topic 11 : ripem rsa sandvik w0 bosnia psuvm hudson utk defensive veal
Topic 12 : db espn sabbath br widgets liar davidian urartu sdpa cooling
Topic 13 : ripem dyer ucsu carleton adaptec tires chem alchemy lockheed rsa
Topic 14 : ingr sv alomar jupiter borland het intergraph factory paradox captain
Topic 15 : militia palestinian cpr pts handheld sharks igc apc jake lehigh
Topic 16 : alaska duke col russia uoknor aurora princeton nsmca gene stereo
Topic 17 : uuencode msg helmet eos satan dseg homosexual ics gear pyron
Topic 18 : entries myers x11r4 radar remark cipher maine hamburg senior bontchev
Topic 19 : cubs ufl vitamin temple gsfc mccall astro bellcore uranium wesleyan

常見自然語言處理工具封裝

經過上面對於 20NewsGroup 語料集處理的介紹我們可以發現常見自然語言處理任務包括,資料獲取、資料預處理、資料特徵提取、分類模型訓練、主題模型或者詞向量等高階特徵提取等等。筆者還習慣用 python-fire 將類快速封裝為可通過命令列呼叫的工具,同時也支援外部模組呼叫使用。本部分我們主要以中文語料集為例,譬如我們需要對中文維基百科資料進行分析,可以使用 gensim 中的維基百科處理類

class Wiki(object):
    """
    維基百科語料集處理
    """
    
    def wiki2texts(self, wiki_data_path, wiki_texts_path=`./wiki_texts.txt`):
        """
        將維基百科資料轉化為文字資料
        Arguments:
        wiki_data_path -- 維基壓縮檔案地址
        """
        if not wiki_data_path:
            print("請輸入 Wiki 壓縮檔案路徑或者前往 https://dumps.wikimedia.org/zhwiki/ 下載")
            exit()

        # 構建維基語料集
        wiki_corpus = WikiCorpus(wiki_data_path, dictionary={})
        texts_num = 0

        with open(wiki_text_path, `w`, encoding=`utf-8`) as output:
            for text in wiki_corpus.get_texts():
                output.write(b` `.join(text).decode(`utf-8`) + `
`)
                texts_num += 1
                if texts_num % 10000 == 0:
                    logging.info("已處理 %d 篇文章" % texts_num)

        print("處理完畢,請使用 OpenCC 轉化為簡體字")

抓取完畢後,我們還需要用 OpenCC 轉化為簡體字。抓取完畢後我們可以使用結巴分詞對生成的文字檔案進行分詞,程式碼參考這裡,我們直接使用 python chinese_text_processor.py tokenize_file /output.txt 直接執行該任務並且生成輸出檔案。獲取分詞之後的檔案,我們可以將其轉化為簡單的詞袋錶示或者文件-詞向量,詳細程式碼參考這裡

class CorpusProcessor:
    """
    語料集處理
    """

    def corpus2bow(self, tokenized_corpus=default_documents):
        """returns (vocab,corpus_in_bow)
        將語料集轉化為 BOW 形式
        Arguments:
        tokenized_corpus -- 經過分詞的文件列表
        Return:
        vocab -- {`human`: 0, ... `minors`: 11}
        corpus_in_bow -- [[(0, 1), (1, 1), (2, 1)]...]
        """
        dictionary = corpora.Dictionary(tokenized_corpus)

        # 獲取詞表
        vocab = dictionary.token2id

        # 獲取文件的詞袋錶示
        corpus_in_bow = [dictionary.doc2bow(text) for text in tokenized_corpus]

        return (vocab, corpus_in_bow)

    def corpus2dtm(self, tokenized_corpus=default_documents, min_df=10, max_df=100):
        """returns (vocab, DTM)
        將語料集轉化為文件-詞矩陣
        - dtm -> matrix: 文件-詞矩陣
                I    like    hate    databases
        D1    1      1          0            1
        D2    1      0          1            1
        """

        if type(tokenized_corpus[0]) is list:
            documents = [" ".join(document) for document in tokenized_corpus]
        else:
            documents = tokenized_corpus

        if max_df == -1:
            max_df = round(len(documents) / 2)

        # 構建語料集統計向量
        vec = CountVectorizer(min_df=min_df,
                              max_df=max_df,
                              analyzer="word",
                              token_pattern="[S]+",
                              tokenizer=None,
                              preprocessor=None,
                              stop_words=None
                              )

        # 對於資料進行分析
        DTM = vec.fit_transform(documents)

        # 獲取詞表
        vocab = vec.get_feature_names()

        return (vocab, DTM)

我們也可以對分詞之後的文件進行主題模型或者詞向量提取,這裡使用分詞之後的檔案就可以忽略中英文的差異:

    def topics_by_lda(self, tokenized_corpus_path, num_topics=20, num_words=10, max_lines=10000, split="s+", max_df=100):
        """
        讀入經過分詞的檔案並且對其進行 LDA 訓練
        Arguments:
        tokenized_corpus_path -> string -- 經過分詞的語料集地址
        num_topics -> integer -- 主題數目
        num_words -> integer -- 主題詞數目
        max_lines -> integer -- 每次讀入的最大行數
        split -> string -- 文件的詞之間的分隔符
        max_df -> integer -- 避免常用詞,過濾超過該閾值的詞
        """

        # 存放所有語料集資訊
        corpus = []

        with open(tokenized_corpus_path, `r`, encoding=`utf-8`) as tokenized_corpus:

            flag = 0

            for document in tokenized_corpus:

                # 判斷是否讀取了足夠的行數
                if(flag > max_lines):
                    break

                # 將讀取到的內容新增到語料集中
                corpus.append(re.split(split, document))

                flag = flag + 1

        # 構建語料集的 BOW 表示
        (vocab, DTM) = self.corpus2dtm(corpus, max_df=max_df)

        # 訓練 LDA 模型

        lda = LdaMulticore(
            matutils.Sparse2Corpus(DTM, documents_columns=False),
            num_topics=num_topics,
            id2word=dict([(i, s) for i, s in enumerate(vocab)]),
            workers=4
        )

        # 列印並且返回主題資料
        topics = lda.show_topics(
            num_topics=num_topics,
            num_words=num_words,
            formatted=False,
            log=False)

        for ti, topic in enumerate(topics):
            print("Topic", ti, ":", " ".join(word[0] for word in topic[1]))

該函式同樣可以使用命令列直接呼叫,傳入分詞之後的檔案。我們也可以對其語料集建立詞向量,程式碼參考這裡;如果對於詞向量基本使用尚不熟悉的同學可以參考基於 Gensim 的 Word2Vec 實踐

    def wv_train(self, tokenized_text_path, output_model_path=`./wv_model.bin`):
        """
        對於文字進行詞向量訓練,並將輸出的詞向量儲存
        """

        sentences = word2vec.Text8Corpus(tokenized_text_path)

        # 進行模型訓練
        model = word2vec.Word2Vec(sentences, size=250)

        # 儲存模型
        model.save(output_model_path)

    def wv_visualize(self, model_path, word=["中國", "航空"]):
        """
        根據輸入的詞搜尋鄰近詞然後視覺化展示
        引數:
            model_path: Word2Vec 模型地址
        """

        # 載入模型
        model = word2vec.Word2Vec.load(model_path)

        # 尋找出最相似的多個詞
        words = [wp[0] for wp in model.most_similar(word, topn=20)]

        # 提取出詞對應的詞向量
        wordsInVector = [model[word] for word in words]

        # 進行 PCA 降維
        pca = PCA(n_components=2)
        pca.fit(wordsInVector)
        X = pca.transform(wordsInVector)

        # 繪製圖形
        xs = X[:, 0]
        ys = X[:, 1]

        plt.figure(figsize=(12, 8))
        plt.scatter(xs, ys, marker=`o`)

        # 遍歷所有的詞新增點註釋
        for i, w in enumerate(words):
            plt.annotate(
                w,
                xy=(xs[i], ys[i]), xytext=(6, 6),
                textcoords=`offset points`, ha=`left`, va=`top`,
                **dict(fontsize=10)
            )
        plt.show()

相關文章