1. 程式人生 > >從零開始的文字TF-IDF向量構造和基於餘弦相似度的文字分類

從零開始的文字TF-IDF向量構造和基於餘弦相似度的文字分類

一、任務需求

       1、給定資料庫裡面的N行資料每行代表一篇文章,屬性分別是[id, title, summuary,content] ,從mysql資料庫獲取資料並生成DataFrame格式的資料,有兩列,分別是id 和content。id對應資料表裡的id,content根據 content > summuary > title 重要程度排序,對應資料表相關列的資料。

      2、對DataFrame裡面的content進行分詞、去停用詞,構造每篇文章的TF-IDF向量。

     3、 對於新的文章,構造TF-IDF向量後和已有文章進行相似度比較,給出最相似的五篇文章的id。

給出文字TFIDF向量的定義:文字的one-hot編碼的升級版,每個編碼由對應詞語的tfidf值代替,本質上依然是一個稀疏向量。

二、任務流程

1、 資料庫內容的獲取、DataFrame資料的構造

import pymysql
import pandas as pd
df = pd.DataFrame()
train = pd.DataFrame()
db = pymysql.connect('填寫相關配置')
cursor = db.cursor()
# SQL 查詢語句
sql = "SELECT * FROM item WHERE item_id < %s" % (1811151105292680200222)

#執行SQL語句
cursor.execute(sql)
 # 獲取所有記錄列表
results = cursor.fetchall()
for row in results:
    item_id = row[0]
    title = row[2]
    summuary = row[4]
    content = row[8]
    s ={
        'item_id': [item_id],
        'title': [title],
        'summuary': [summuary],
        'content': [content]
    }
    s = pd.DataFrame(s)
    df = df.append(s,ignore_index=True)
for index, row in df.iterrows():
    if row['content'] is not None and len(row['content'])>10:
        s1 = {
            'item_id': [row['item_id']],
            'content': [row['content']]
        }
        train = train.append(pd.DataFrame(s1),ignore_index=True)
    elif row['summuary'] is not None and len(row['summuary'])>5:
        s2 = {
            'item_id': [row['item_id']],
            'content': [row['summuary']]
        }

        train = train.append(pd.DataFrame(s2), ignore_index=True)
    else:
        s3 = {
            'item_id': [row['item_id']],
            'content': [row['title']]
        }
        train = train.append(pd.DataFrame(s3), ignore_index=True)
train = train[:4698]
train.to_csv('corpora.csv',encoding='utf-8')

此處遇到的問題:1、定義空的DataFrame的時候,不需要自己定義列名。字典的鍵會作為DataFrame的列名。2、append()方法要寫ignore_index = True。3、在構造字典的時候,字典的值需要用以列表的形式存在,這樣在執行train.append(pd.DataFrame(s), ignore_index=True) 的時候才不會報錯。4、DataFrame獲取前n行的操作和列表一樣。

2、TF-IDF生成

這裡,最開始使用了gensim的 Tfidfmodel,但是發現它生成的TF-IDF並不是包含了所有單詞的稀疏矩陣。因為任務要求是得到每個文章TFIDF向量的稀疏矩陣(原因,gensim生成的稀疏向量是BOW向量,不是one-hot向量)。sklearn裡面的 tfidf模組,使用過程發生一個問題,(感覺sklearn對中文文字的處理並不好)。至於jieba 自帶的tfidf,暫時只知道用來求前n個關鍵詞,並不知道如何獲得給定文字的TFIDF向量。故只能手寫。

參照這篇文章 :https://mlln.cn/2018/08/18/%E6%96%87%E6%9C%AC%E5%90%91%E9%87%8F%E7%B3%BB%E5%88%97-%E5%A6%82%E4%BD%95%E5%9F%BA%E4%BA%8E%E8%AF%8D%E9%A2%91%E7%9F%A9%E9%98%B5%E5%92%8CTF-IDF%E6%9D%83%E9%87%8D%E6%9E%84%E5%BB%BA%E8%AF%8D%E5%90%91%E9%87%8F/

1、首先生成詞典和詞頻數矩陣

def word_matrix(documents):
    '''計算詞頻矩陣'''
    docs = [d for d in documents]
    docs = [word for word in docs]
    # 獲取所有詞
    words = list(set(chain(*docs)))
    # 詞到ID的對映, 使得每個詞有一個ID
    dictionary = dict(zip(words, range(len(words))))
    # 建立一個空的矩陣, 行數等於詞數, 列數等於文件數
    matrix = np.zeros((len(words), len(docs)),dtype='float32')
    # 逐個文件統計詞頻
    for col, d in enumerate(docs):
        # 統計詞頻
        count = Counter(d)
        for word in count:
            # 用word的id表示word在矩陣中的行數
            id = dictionary[word]
            # 把詞頻賦值給矩陣
            matrix[id, col] = count[word]
         

    return matrix, dictionary

 

這是整個任務最關鍵的一步,生成了詞典和詞頻矩陣。詞典的作用:使得詞語和其id一一對應。同時,對於新的分詞之後的文字,需要去掉詞典裡不存在的詞。詞頻矩陣的行代表每個單詞在每篇文章裡出現的次數,詞頻矩陣的列代表每篇文章裡包含的詞語及該詞語出現的次數。

2、通過詞頻矩陣計算每個詞語的tf-idf值

#計算tf
def tf(matrix):
    # 計算每個文件的總詞數
    sm = np.sum(matrix, axis=0)
    print(sm)
    # 每個詞的詞頻除以每個文件的詞頻
    try:
        a = matrix/sm
    except:
        print('error')
    print('tf運算結束')
    return matrix / sm


#計算idf |D|: 語料庫中的文件總數。 分子: 包含詞ti的文件數
def idf(matrix):
    '''計算IDF'''
    # 文件總數
    D = matrix.shape[1]
    # 包含每個詞的文件數
    j = np.sum(matrix>0, axis=1)
    print(j)
    return np.log(D / j)


#計算每個單詞的tf_idf值
def tf_idf(matrix):
    return tf(matrix) * idf(matrix).reshape(matrix.shape[0], 1)

涉及到了array的四則運算。值得注意的一點就是:二維array除以一維array,就是二維陣列的每一列裡面的所有數分別除一維陣列的對應列的值。 雖然研究生一直做複雜網路相關的東西,也是一直使用矩陣。在這裡第一次感受到矩陣運算的方便。

最後返回的就是需要的文字的TFIDF向量,以陣列的形式存在。

 3、儲存生成的模型

#儲存tfidf_matrix
tfidf_matrix = tf_idf(matrix)
np.save('./doc/numpy.npy',tfidf_matrix)
#儲存dictionary
list3_file = open('./doc/dictionary.pickle', 'wb')
pickle.dump(dictionary, list3_file)
list3_file.close()

使用了npy 和pickle兩種資料儲存格式。對資料的存取方面的問題,未完待續。

遇到的問題:最開始使用了10000篇文章,把matrix存為了txt格式,發現有30G,計算tf的時候電腦直接卡死。後來改成了5000篇文章。

4、下載模型

#載入dictionary
word_dict = open('./doc/dictionary.pickle', 'rb')
word_dict = pickle.load(word_dict)

#載入idf字典
with open('./doc/newidf.txt', 'r', encoding='utf-8')as f:
    dic = []
    for line in f.readlines():
        b = line.strip().split(' ')
        dic.append(b)
dic = dict(dic)
#載入matrix
matrix = np.load('./doc/numpy.npy')

5、生成新文章的TF-IDF向量

#生成一個一維向量,長度為字典的長度。然後對詞語所在位置進行賦值。使用了自定義的idf模型。
newdoc = np.zeros(len(word_dict)) 
data = readfile('./doc/re0.txt')
doc = chinese_word_cut(data)
k = len(doc)  # 計算單詞總數
result = {}
for word in doc:
    result[word] = doc.count(word)
    tf = float(result[word] / k)
    if word in list(dic.keys()):
        idf = float(dic[word])
    else:
        idf = 0.0
    tfidf = tf * idf
    if word in word_dict.keys():
        newdoc[word_dict[word]] = tfidf
print(np.sum(newdoc))

自定義idf模型的生成,之前部落格有寫,怪不得最開始實習的時候,讓我自己寫個idf模型。

6、計算文章之間的餘弦相似度

# 計算文章和舊文章的餘弦相似度。
def sim(array1,array2):
    num = float(np.matmul(array1,array2))
    s = np.linalg.norm(array1)*np.linalg.norm(array2)
    return num/s

matrix = np.load('./doc/numpy.npy')
print(matrix.shape[1])
doc2id = []
for i in range(matrix.shape[1]):
    doc2id.append(sim(newdoc,matrix[:,i]))
# 返回列表裡面最大的三個數值的索引。
max_num_index_list = map(doc2id.index, heapq.nlargest(6, doc2id))
print(list(max_num_index_list))

餘弦相似度:https://www.jianshu.com/p/ec834ec0d51f