TF-IDF的文章分类系统

技术学习 / 2021-08-17

前言

接上,一个聚合类的阅读软件当然少不了文章分类系统,模型之类的根本来不及看,来不及学,就采用了 TF-IDF 算法来写,自己构建词库,自己算,准确率还算比较高的,对于我们这个小项目来说够用了反正

原理

  • 将已分类的文章做分词,保留 TF-IDF 算法前五的名词
  • 将每篇文章的五个名词去重合成一个大的集合作为词库
  • 根据字典生成每篇文章的单独向量
  • 将所有相同类别的文章向量相加求平均得到文章类别的平均向量
  • 将未分类的文章做分词,保留前 20 的名词做向量
  • 与所有文章类别的平均向量做比对,保留匹配度最高的三个

文章分类系统

参考文章:

【python】爬虫篇:通过文章内容使用TF-IDF算法对文章进行分类(五)

【python】爬虫篇:最后一篇之TF-IDF分类代码篇(六)

项目地址:

本项目已在 github 上开源:github地址

源代码:

  • Dictionnary_Builder.py

    # encoding = utf-8
    import sys
    import pymysql
    import jieba
    import jieba.analyse
    import numpy as np
    
    np.set_printoptions(threshold = sys.maxsize)
    np.set_printoptions(suppress = True) # 不使用科学计数法
    mysql = pymysql.connect(host="bj-cynosdbmysql-grp-2w3ca8rc.sql.tencentcdb.com", port=25197, user="tmp", passwd="Aa1@0000", db="lsilencej_test_post", charset = "utf8") # 连接数据库
    cur = mysql.cursor() # 生成游标
    word_num = 0
    
    
    def Dictionary_Builder(posts):
    	merged_words = [] # 用来存放所有词的 list
    	post_words = [] # post_words 是把每一条数据的 id 和生成的分词情况保存下来的list
    	for index in range(len(posts)): # 遍历每一篇文章
    		words = jieba.analyse.extract_tags(posts[index][1], withWeight = True, allowPOS = 'n', topK = 5) # 为每一篇文章分词(allowPOS = 'n'代表分为名词词性),为每个词打上权重,取权重最大的前5个词语
    		words_weight = {tag: weight for tag, weight in words} # 分开词和权重并存入 words_weight 中
    		merged_words = set(words_weight.keys()) | set(merged_words) # 将词放入 set 中进行去重并整合成一个大的 list
    		post_words.append((posts[index][0], words_weight)) # 把该条数据的 id 和所分的词加入到 post_words 中
    	dictionary = ','.join(merged_words) # 通过逗号分隔所有词,构成词库
    	word_num = len(merged_words) # 记录词的总数, 后面作为维数使用
    	sql = "insert into article_dictionary values(null, '{dictionary}')".format(dictionary = dictionary) # 把词库插入数据库表 article_dictionary
    	cur.execute(sql)
    	mysql.commit()
    	print('词库写入完毕')
    	print('正在生成已分类文章的向量')
    	for post_word in post_words: # post_word 是 (id, (tag, weight)) 的形式
    		vector = [] # 清空 vector 集合
    		for merged_word in merged_words: # 在词库中遍历每个词
    			if merged_word in post_word[1].keys(): # 如果文章中存在词库中的遍历到的这个词
    				vector.append(post_word[1][merged_word]) # 把权重放入对应词的位置
    			else: 
    				vector.append(0) # 否则为0
    		sql = "update article_category_data set weight = '{vector}' where id = {post_id}".format(vector = vector, post_id = post_word[0]) # 把每条数据的权重值集合插入表中
    		cur.execute(sql)
    		mysql.commit()
    		# print('-----------------提交成功------------------------')
    
    def Ave_Vector_Builder(index, category): # 计算每一类文章的平均向量
    	sql = "select id, weight from article_category_data where category='{category}'".format(category = category)
    	cur.execute(sql)
    	cated_posts = cur.fetchall()  # 所有的这一类的文章的 id, 权重 集合
    	null_vector = np.zeros(word_num) # 创造一个一维,长度为 word_num 的零向量
    	print('一维零向量生成完毕')
    	for i in range(len(cated_posts)): # 遍历每一条数据
    		dat = cated_posts[i][1][1:-1].split(',') # 取出对应的数据
    		dat_vector = list(map(float, dat)) # 把取出来的字符串转为 float 类型并放入集合中
    		temp_vector = np.array(dat_vector) # 生成向量
    		null_vector += temp_vector # 与零向量相加
    	print('遍历完毕')
    	ave_vector = (null_vector / len(cated_posts)).tolist() # 获得平均向量
    	result = '['+','.join([str('{:.10f}'.format(item) if item != 0.0 else item) for item in ave_vector])+']' # 固定生成的字符串格式,如果不为零保留十位小数,为零不改变
    	print('字符串格式固定完毕')
    	sql = "insert into article_category_weight values({index}, '{category}', '{result}')".format(index = index, category = category, result = result) # 把 id, 分类, 该分类的平均向量插入 article_category_weight 中
    	cur.execute(sql)
    	mysql.commit()
    	print('提交完毕')
    
    if __name__ == '__main__':
    	sql = 'select id, category, content from article_category_data where content is not null' # 在数据库表 article_category_data 中查询内容不为空的文章数据
    	cur.execute(sql) # 执行sql
    	datas = cur.fetchall()   # 每一行的数据的集合
    	posts = [] # 存储文章的列表
    	print('拉取数据')
    	for index in range(len(datas)):
    		posts.append((datas[index][0], datas[index][2])) # 把 id 和 content 作为一个值构建一个 posts 集合
    	print('数据拉取并且遍历完毕')
    	Dictionary_Builder(posts) # 运行
    	print(' Dictionary_Builder 运行结束, 词库已生成')
    	cate =['文化', '娱乐', '体育', '财经', '科技', '游戏'] # 类别
    	for index in range(len(cate)): # 对每一类文章进行枚举
    		Ave_Vector_Builder(index + 1, cate[index]) # id 从 1 开始
    	mysql.close()
    
  • Post_Classify.py

    # encoding = utf-8
    import pymysql
    import jieba
    import jieba.analyse
    
    mysql = pymysql.connect(host="bj-cynosdbmysql-grp-2w3ca8rc.sql.tencentcdb.com", port=25197, user="tmp", passwd="Aa1@0000", db="lsilencej_test_post", charset = "utf8") # 连接数据库
    cur = mysql.cursor() # 生成游标
    
    def Vector_Reader(): # 读取所有文章类型的平均向量
    	sql = "select category, category_weight from article_category_weight" # 在 article_category_weight 中读取文章种类和该种类的平均向量
    	cur.execute(sql)
    	categories_weight = cur.fetchall()  # 每一行的数据
    	vectors = []
    	for index in range(len(categories_weight)):
    		vectors.append((categories_weight[index][0], categories_weight[index][1]))
    	return vectors
    	
    def cosine_similarity(vector1, vector2): # 求两向量之间的夹角
        molecule = 0.0 # 初始化
        denominatorA = 0.0
        denominatorB = 0.0
        for a, b in zip(vector1, vector2):
            if(a != 0 or b != 0):
                molecule += a * b # 计算分子
                denominatorA += a ** 2 # 计算分母
                denominatorB += b ** 2
        if denominatorB == 0.0 or denominatorB == 0.0: # 分母不为0
            return 0
        else: # 计算两个向量的夹角
            return round(molecule / ((denominatorA ** 0.5) * (denominatorB ** 0.5)) * 100, 2) # 乘以 100 防止数据过小
     
    
    def Cos_Comparer(result, vectors, dictionary): # 通过比较两个向量的cos值来判断相似度,传入的参数分别是未分类的文章,多个类型的平均向量,字典
        post_id = result[0] # 获取当前数据的id
        post = result[1] # 获取当前数据的文本内容
        words = jieba.analyse.extract_tags(post, withWeight = True, allowPOS = 'n', topK = 20)
        words_weight = {tag: weight for tag, weight in words}
        # print('words_weight : ', words_weight)
        # print("============")
        dictionary_list = ','.join(dictionary).split(',') # 字符串变成列表
        vector = [] # 未分类的文章的文章向量
        for index_word in dictionary_list: # 枚举词库中的每个词
            if index_word in words_weight: # 如果新文章中存在这个词
                vector.append(words_weight[index_word]) # 把权重加入 vector 列表中
            else:
                vector.append(0) # 否则为0
        # print(vector) # 新输入的文章向量
        topK = [] # 将该文章的向量和所有文章类别的平均向量比较,获取相关度为前三的类别
        cos = ()
        for v in vectors:
            temp_v = v[1][1:-1].split(',') # 取每类文章的平均向量
            temp_f_v = list(map(float, temp_v)) # 字符串转数的集合
            num_cos = cosine_similarity(vector, temp_f_v) # 计算未分类文章和每类文章的平均向量的相关度
            cos = (v[0], num_cos) # 格式 ('科技', 9.8)
            if (len(topK) < 3): # 只保留前三个数据, 此时集合中少于三个元素
                topK.append(cos) # 加入 topK 当中
                topK.sort(reverse = True) # 从大到小排序
            else: # 集合中多于三个元素
                if (cos[1] > topK[0][1]): # 如果现在这个余弦夹角比 topK 中的最大值还大,即更加相关,则插入到头部
                    tmp = topK[1]
                    topK[1] = topK[0]
                    topK[0] = cos
                    topK[2] = tmp
     
        print('post', post_id, ':', topK)
        sql = "update article set category = '{top}' where id = {post_id}".format(top = topK[0][0], post_id = post_id)
        # print(sql)
        cur.execute(sql)
        mysql.commit()
    
    
    vectors = Vector_Reader() # 获得所有类型的平均向量
    print('已获得向量')
    sql = "select dictionary from article_dictionary" # 在 article_dictionary 中读取词库
    cur.execute(sql)
    dictionary = cur.fetchone() # 只有一行数据
    print('已查询到字典')
    sql = "select id, content from article where category is null" # 在 article 中读取未分类的文章
    cur.execute(sql)
    results = cur.fetchall()
    print('拉取到数据')
    for result in results:
    	Cos_Comparer(result, vectors, dictionary)
    mysql.close()
    

文件

Dictionnary_Builder.py

构建词库和各类文章的平均向量

Post_Classify.py

将未分类的文章和各类文章的平均向量进行比对

数据库表

article

未分类的文章,字段最低需求:

  • id int 非空 键

  • category varchar 可空

  • content longtext 非空

article_category_data

已分类的文章,字段最低需求:

  • id int 非空 键

  • category varchar 非空

  • content longtext 非空

  • weight longtext 可空

article_category_weight

各类文章的平均向量,字段最低需求:

  • id int 非空 键

  • category varchar 非空

  • category_weight longtext 非空

article_dictionary

词库,字段最低需求:

  • id int 非空 键

  • dictionary longtext 非空

Tips

  • 由于大部分代码和 id 绑定在一起,如果原数据库表有数据可能会有bug存在
  • 代码中的分类可调,相应的已分类的文章需要提供
NLP