python | sklearn ,做一個調包俠來解決新聞文本分類問題

https://zhuanlan.zhihu.com/p/30455047?utm_medium=social&utm_source=qq

本文介紹一下使用樸素貝葉斯算法來做文本分類任務。

數據集是搜狗新聞數據集“corpus_6_4000”,它包含六大類新聞,每類新聞4000篇,每篇新聞長度在幾百到幾千字不等。六類新聞分別是'Auto', 'Culture', 'Economy', 'Medicine', 'Military', 'Sports'。今天的任務就是使用監督學習算法(樸素貝葉斯)來實現文本自動分類問題。話不多說,讓我們開幹吧!

1、數據探索分析和預處理

首先看看數據集是什麼樣的

file_dir = './corpus_6_4000'

file_list = os.listdir(file_dir)
file_list[:5]

"""
['Auto_0.txt', 'Auto_1.txt', 'Auto_10.txt', 'Auto_100.txt', 'Auto_1000.txt']
"""

看一下文件名稱,從名稱可以看出文件名稱就是新聞的類別(標籤),因此我們需要把這些標籤提取出來,讓他們和文本組成一一對應的列表,如[[doc1,label1],[doc2,label2],...]形式。

stop_word = []
with open('stopwords_cn.txt','r') as f:
    for word in f.readlines():
        stop_word.append(word.strip())

data_set = []
for file in file_list:
    doc_label = []
    file_path = file_dir + '/' + file
    with open(file_path) as f:
        data = f.read()
    data = re.sub('[a-zA-Z0-9]+','',data.strip())
    data = jieba.cut(data)
    datas = [word for word in data if word not in stop_word and len(word)>1]
    doc_label.append(datas)
    label = file.split('_')[0]
    doc_label.append(label)

    data_set.append(doc_label)

stop_word爲加載的停用詞列表,data_set存儲新聞文檔,doc_label存儲標籤。在這裏我們將文檔中的數字字母都去掉了,而且分詞之後,把單個的字也去掉了;因爲我覺得這些字對文檔的特徵表達意義不大。

接下來將數據集的順序隨機打亂,並且保存下來,供之後使用。

random.shuffle(data_set)
pickle.dump(data_set,open('./data/data_set.pkl','wb'))

df = pd.DataFrame(data_set,columns=['data','label'])
df['label'] = df['label'].map(map_list)

data = df['data'].tolist()
label = df['label'].tolist()

train_data = data[:16800]
test_data = data[16800:]

train_label = label[:16800]
test_label = label[16800:]

pickle.dump(train_data,open('./data/train_data.pkl','wb'))
pickle.dump(test_data,open('./data/test_data.pkl','wb'))
pickle.dump(train_label,open('./data/train_label.pkl','wb'))
pickle.dump(test_label,open('./data/test_label.pkl','wb'))

我將24000篇新聞的80%用於訓練模型,20%用於測試。

2、特徵工程

老話說的好,機器學習中,特徵的好壞決定了你的模型性能的上限,而算法的好壞決定你能否逼近這個上限。因此,特徵工程非常重要。那麼怎麼對文本進行構造特徵?由於計算機是不能對字符串進行計算的,因此我們需要將文本進行數字化,也就向量化,我們把每一篇新聞用一個向量表示,那麼怎樣用向量表示呢?

第一個思路是使用關鍵詞詞頻,也就是哪些詞出現的次數多,我就把他們作爲關鍵詞,然後構造向量空間模型。經過對訓練數據集的統計發現:

[('中國', 43812),
('一個', 29356),
('市場', 23137),
('汽車', 22275),
('沒有', 20719),
('已經', 17960),
('發展', 17408),
('進行', 16574),
('目前', 15792),
('公司', 15480),
('問題', 15072),
('表示', 14901),
('記者', 14688),
('文化', 14311),
('可能', 13462),
('工作', 13163),
('國家', 13156),
('北京', 12984),
('網易', 12710),
('認爲', 11984),
('.%', 11888),
('美國', 11732),
('比賽', 11615),
('經濟', 11422),
('成爲', 11016),
('企業', 10861),
('方面', 10685),
('車型', 10465),
('現在', 10403),
('醫院', 10308)]

我們看到有相當多高詞頻的詞根本是對分類沒有任何意義的,比如”中國“,”一個“,”目前“等,如果把詞頻最高的一些詞作爲特徵去構造特徵,這樣數據集中將有很大的噪聲,並且無法分類,我選取了詞頻最高的5000個詞,10000個詞,15000個詞分別作了實驗,準確率都只有16%左右。因此很明顯,選用詞頻構造特徵完全不行。

於是我們把目光轉向TF-IDF,TF-IDF指詞頻和逆文檔頻率。詞頻計算公式如下:

公式裏除以文章總詞數是爲了消除不同文章的長短不同所帶來的影響。詞頻也可以如下計算:

逆文檔頻率計算公式如下:

IF-IDF就是將兩者相乘

TF-IDF的具體意思,以及它爲什麼能做表示文章特徵的原因,參看這篇文章

計算TF-IDF可以使用sklearn庫裏面的函數進行計算,但是今天,我自己動手實現了一下,也讓大家能更好的理解TF-IDF。

def make_idf_vocab(train_data):
    if os.path.exists('./data/idf.pkl'):
        idf = pickle.load(open('./data/idf.pkl','rb'))
        vocab = pickle.load(open('./data/vocab.pkl','rb'))
    else:
        word_to_doc = {}
        idf = {}
        total_doc_num = float(len(train_data))

        for doc in train_data:
            for word in set(doc):
                if word not in word_to_doc.keys():
                    word_to_doc[word] = 1
                else:
                    word_to_doc[word] += 1

        for word in word_to_doc.keys():
            if word_to_doc[word] > 10:
                idf[word] = np.log(total_doc_num/(word_to_doc[word]+1))

        sort_idf = sorted(idf.items(),key=lambda x:x[1])
        vocab = [x[0] for x in sort_idf]
        pickle.dump(idf,open('./data/idf.pkl','wb'))
        pickle.dump(vocab,open('./data/vocab.pkl','wb'))
    return idf,vocab

word_to_doc字典存儲每一個詞在多少篇文章出現,vocab存儲經過idf值從大到小排序後的詞的列表。本文中我將那些只在10篇及以下的新聞中出現的那些詞排除掉了。

接下來實現計算文檔詞頻的函數。

def cal_term_freq(doc):
    term_freq = {}
    for word in doc:
        if word not in term_freq.keys():
            term_freq[word] = 1
        else:
            term_freq[word] += 1
    for word in term_freq.keys():
        term_freq[word] = term_freq[word]/float(len(doc))
    return term_freq

計算單個文檔的詞頻,完全按照以上公式來的。

接下來實現構造文檔特徵的函數。

def make_doc_feature(vocab,idf,doc,topN):
    doc_feature = [0.]*topN
    vocab = vocab[:topN]
    tf = cal_term_freq(doc)
    for word in doc:
        if word in vocab:
            index = vocab.index(word)
            doc_feature[index] = tf[word]*idf[word]
    return doc_feature

topN確定構造多少維的特徵向量,維數越高,包含的信息也越多,但是噪聲也會越多,而且會增加計算難度。

將訓練數據集矩陣轉換成tfidf權重矩陣。

def make_tfidf(train_data,vocab,idf,topN):
    tfidf_data = []
    for doc in train_data:
        doc_feature = make_doc_feature(vocab,idf,doc,topN)
        tfidf_data.append(doc_feature)
    return tfidf_data

到這裏,特徵工程完成了!

3、訓練模型

我先後使用了多項式樸素貝葉斯算法和K近鄰算法來進行分類,發現樸素貝爺效果更好;這裏使用多項式貝葉斯的原因是,我們的tfidf特徵值是0-1之間的實數,是連續的,而伯努利貝葉斯適合離散型的特徵。

train_data = pickle.load(open('./data/train_data.pkl','rb'))
train_label = pickle.load(open('./data/train_label.pkl','rb'))

idf,vocab = make_idf_vocab(train_data)
tfidf_data = make_tfidf(train_data,vocab,idf,6000)
train_x = np.array(tfidf_data[:13500])
train_y = np.array(train_label[:13500])
val_x = np.array(tfidf_data[13500:])
val_y = np.array(train_label[13500:])

我使用13500個文本作爲訓練集,剩下的3300個文本作爲驗證集。另外,我topN取6000,也就是我選6000個idf值排前的特徵詞來構文本造特徵向量。

在驗證集上準確率達到94%,非常不錯,所以我就沒有調參,打算直接用這個模型去測試集上試試。

另外使用KNN的結果如下,只有59%的準確率,差很多。

樸素貝葉斯算法在測試集上的結果如下:

和驗證集上的結果差不多。

所以可以看到做文本分類時,採用TF-IDF作爲文本的特徵效果是非常不錯的。另外我們也可以採用互信息作爲特徵。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章