文章推荐系统(思路优化版)

前言

上次的文章推荐太过于简陋,而且限制太大,这次思路优化了一版,思路仍然是基于文章内容的推荐

基于文章内容的推荐

原理

如图所示,对于用户 lsilencej 来说,假设他只看过 article1 和 article2,通过对比发现,article3 和 article1 的内容相似度很高,article4 和 article2 的相似度很高,于是,article3 和 article4 也被推荐给 lsilencej

定义内容相似

对于一篇新闻或者长文章来说,可以从文本特征几个方面去提取它的特征信息,进而将不同的新闻间的信息进行比较

常见的特征信息有:文本的长度、文本的所属类型(科技、游戏、社会等)、文本的来源(知乎、CSDN、人民日报等)、文本关键词

文本的关键词可以大致概括文本主要内容

提取文本关键词

TF-IDF :TF-IDF 是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

当然,提到分词当然要祭出 jieba 库了,在 jieba 库中有 jieba.analyse.extract_tags,同样是基于 TF-IDF 算法实现

usage:keywords = jieba.analyse.extract_tags(article, topK=5, withWeight=True, allowPOS=(’n’))

  • article 代表要分词的文本
  • topK = 5 代表返回 5 个权重最大的关键词
  • withWeight = True 代表同时返回关键词的 TF-IDF 值
  • allowPOS = (“n”) 代表返回的均为名词

默认使用的是 jieba 库中自带的语料库,当然也可以自定义语料库,具体参照 jieba 分词

用户偏好构建

用户喜好的关键词如何获取?

这里采用了一个简单的方法:从用户历史浏览记录中挖掘

用户喜好关键词列表构建

  • 在数据库中为每个用户维护一个关键词列表

  • 每当用户浏览完某篇文章后,提取出该文章的关键词和 TF-IDF 值,并存入用户的关键词列表中

    如:lsilencej 的关键词列表:{ 技术:20,代码:18,手机:12,游戏:10……}

  • 如果关键词列表中已经存在相应关键词对应的 TF-IDF 值,则直接叠加或基于别的自定义权重加权进行叠加,以此来加强用户对该关键词的感兴趣程度

  • 为了防止用户的推荐结果收敛到几个关键词上,可以选择为关键词列表设置一个衰减系数 λ ,定期对用户的关键词列表的 TF-IDF 值进行更新,减少关键词的收敛倾向

用户关键词与文本内容的拟合度

通过用户的关键词列表和文本的关键词列表,做两个 map 的键匹配即可

如果键相同,则值相乘,如果没有相同的键,则值为 0 ,多个相同键的值乘积相加,最后的结果即作为拟合度

对于新文章计算拟合度,将拟合度最高的几个文章推荐给用户

代码实现

(文章关键词提取)article_keywords:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# coding = utf-8
import sys
import pymysql
import json
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-2r0nnbpu.sql.tencentcdb.com", port=22241, user="tmp", passwd="Aa1@0000", db="lsilencej_test_post", charset = "utf8") # 连接数据库
cur = mysql.cursor() # 生成游标

def text_keywords(article_id, article):
	keywords = jieba.analyse.extract_tags(article, topK=5, withWeight=True, allowPOS=('n'))
	words_weight = {keyword: weight * 100 for keyword, weight in keywords}
	str_keywords = json.dumps(words_weight, ensure_ascii = False)
	sql = "update article set keywords = '{str_keywords}' where id = {article_id}".format(str_keywords = str_keywords, article_id = article_id)
	cur.execute(sql)
	mysql.commit()
	# print("Complete")

if __name__ == '__main__':
	sql = "select id, content from article where keywords is null"
	cur.execute(sql)
	results = cur.fetchall()
	for result in results:
		text_keywords(result[0], result[1])

(用户关键词列表生成)user_article:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# coding = utf-8
import sys
import pymysql
import json
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-2r0nnbpu.sql.tencentcdb.com", port=22241, user="tmp", passwd="Aa1@0000", db="lsilencej_test_post", charset = "utf8") # 连接数据库
cur = mysql.cursor() # 生成游标

def get_category(article_id):
	sql = "select category from article where id = {}".format(article_id)
	cur.execute(sql)
	result = cur.fetchone()
	return result[0]

def user_dictionary_generator(user_result):
	histories = tuple(user_result[1].split(','))
	if (user_result[2] == None):
		dictionary = {}
	else:
		dictionary = json.loads(user_result[2])
	for history in histories:
		sql = "select keywords from article where id = {}".format(history)
		cur.execute(sql)
		result = cur.fetchone()
		dic_result = json.loads(result[0])
		print(type(dic_result))
		for key, value in dic_result.items():
			if key in dictionary.keys():
				dictionary[key] += value
			else:
				dictionary[key] = value
	print(dictionary)
	sql = "update user set user_dictionary = '{dictionary}' where id = {user_id}".format(dictionary = json.dumps(dictionary, ensure_ascii = False), user_id = user_result[0])
	cur.execute(sql)
	mysql.commit()
	# print("commit")


if __name__ == '__main__':
	sql = "select id, user_history, user_dictionary from user where updated = 'true'"
	cur.execute(sql)
	results = cur.fetchall()
	for result in results:
		user_dictionary_generator(result)

(文章用户兴趣拟合度比较)article_comparator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# coding = utf-8
import sys
import pymysql
import json
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-2r0nnbpu.sql.tencentcdb.com", port=22241, user="tmp", passwd="Aa1@0000", db="lsilencej_test_post", charset = "utf8") # 连接数据库
cur = mysql.cursor() # 生成游标

def user_recommend(user):
	dictionary = json.loads(user[2])
	histories = tuple(user[1].split(','))
	sql = "select id, keywords, content from article where id not in {}".format(tuple(histories))
	cur.execute(sql)
	results = cur.fetchall()
	topK = []
	rel = ()
	for result in results:
		keywords = json.loads(result[1])
		article_sum = 0
		for key, value in keywords.items():
			if key in dictionary.keys():
				article_sum += value * dictionary[key]
		rel = (article_sum, result[0], result[2])
		topK.append(rel)
	topK.sort(reverse = True) # 从大到小排序, article_sum为文章与用户字典的相关度, result[0]为推荐的文章id, result[2]为推荐的文章内容
	recommend = []
	# print(user[0], ':') # 这段打印的已经测试过了, 转字符串的程序还没测试
	for index in range(15):
		# print(topK[index])
		recommend.append(topK[index][1])
	str_recommend = ','.join([str(r) for r in recommend])
	sql = "update user set user_recommend = '{str_recommend}'".format(str_recommend = str_recommend)
	cur.execute(sql)
	mysql.commit()

if __name__ == '__main__':
	sql = "select id, user_history, user_dictionary from user where recommend is null"
	cur.execute(sql)
	results = cur.fetchall()
	for result in results:
		user_recommend(result)

最后的最后

本文旨在提供思路,如有需要可以参考源代码:Github 源码