众所周知,个性化推荐系统能够根据用户的兴趣、偏好等信息向用户推荐相关内容,使得用户更感兴趣,从而提升用户体验,提高用户粘度,之前我们曾经使用协同过滤算法构建过个性化推荐系统,但基于显式反馈的算法就会有一定的局限性,本次我们使用无监督的Lda文本聚类方式来构建文本的个性化推荐系统。
推荐算法:协同过滤/Lda聚类
我们知道,协同过滤算法是一种基于用户的历史行为来推荐物品的算法。协同过滤算法利用用户之间的相似性来推荐物品,如果两个用户对某些物品的评分相似,则协同过滤算法会将这两个用户视为相似的,并向其中一个用户推荐另一个用户喜欢的物品。
说白了,它基于用户的显式反馈,什么是显式反馈?举个例子,本如本篇文章,用户看了之后,可能会点赞,也可能会疯狂点踩,或者写一些关于文本的评论,当然评论内容可能是负面、正面或者中性,所有这些用户给出的行为,都是显式反馈,但如果用户没有反馈出这些行为,就只是看了看,协同过滤算法的效果就会变差。
(相关资料图)
LDA聚类是一种文本聚类算法,它通过对文本进行主题建模来聚类文本。LDA聚类算法在聚类文本时,不考虑用户的历史行为,而是根据文本的内容和主题来聚类。
说得通俗一点,协同过滤是一种主动推荐,系统根据用户历史行为来进行内容推荐,而LDA聚类则是一种被动推荐,在用户还没有产生用户行为时,就已经开始推荐动作。
LDA聚类的主要目的是将文本分为几类,使得每类文本的主题尽可能相似。
LDA聚类算法的工作流程大致如下:
1.对文本进行预处理,去除停用词等。
2.使用LDA模型对文本进行主题建模,得到文本的主题分布。
3.将文本按照主题分布相似性进行聚类。
4.将聚类结果作为类标签,对文本进行分类。
大体上,LDA聚类算法是一种自动将文本分类的算法,它通过对文本进行主题建模,将文本按照主题相似性进行聚类,最终实现文本的分类。
Python3.10实现
实际应用层面,我们需要做的是让主题模型能够识别在文本里的主题,并且挖掘文本信息中隐式信息,并且在主题聚合、从非结构化文本中提取信息。
首先安装分词以及聚类模型库:
pip3 install jiebapip3 install gensim
随后进行分词操作,这里以笔者的几篇文章为例子:
import jiebaimport pandas as pdimport numpy as nptitle1=\"乾坤大挪移,如何将同步阻塞(sync)三方库包转换为异步非阻塞(async)模式?Python3.10实现。\"title2=\"Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现\"title3=\"周而复始,往复循环,递归、尾递归算法与无限极层级结构的探究和使用(Golang1.18)\"title4=\"彩虹女神跃长空,Go语言进阶之Go语言高性能Web框架Iris项目实战-JWT和中间件(Middleware)的使用EP07\"content = [title1,title2, title3,title4]#分词content_S = []all_words = []for line in content: current_segment = [w for w in jieba.cut(line) if len(w)>1] for x in current_segment: all_words.append(x) if len(current_segment) >1 and current_segment != "\r ": content_S.append(current_segment)#分词结果转为DataFramedf_content = pd.DataFrame({"content_S":content_S})print(all_words)
可以看到,这里通过四篇文章标题构建分词列表,最后打印分词结果:
["乾坤", "挪移", "如何", "同步", "阻塞", "sync", "三方", "库包", "转换", "异步", "阻塞", "async", "模式", "Python3.10", "实现", "Generator", "生成器", "入门", "初基", "Coroutine", "原生", "协程", "登峰造极", "Python3.10", "并发", "异步", "编程", "async", "底层", "实现", "周而复始", "往复", "循环", "递归", "递归", "算法", "无限极", "层级", "结构", "探究", "使用", "Golang1.18", "彩虹", "女神", "长空", "Go", "语言", "进阶", "Go", "语言", "高性能", "Web", "框架", "Iris", "项目", "实战", "JWT", "中间件", "Middleware", "使用", "EP07"]
接着就可以针对这些词进行聚类操作,我们可以先让ChatGPT帮我们进行聚类看看结果:
可以看到,ChatGPT已经帮我们将分词结果进行聚类操作,分为两大类:Python和Golang。
严谨起见,我们可以针对分词结果进行过滤操作,过滤内容是停用词,停用词是在文本分析、自然语言处理等应用中,用来过滤掉不需要的词的。通常来说,停用词是指在英文中的介词、代词、连接词等常用词,在中文中的助词、介词、连词等常用词:
———》),)÷(1-”,)、=(:→℃ &*一一~~~~’. 『.一./-- 』=″【[*]}>[⑤]][①D]c]ng昉*//[][②e][②g]={},也 ‘A[①⑥][②B] [①a][④a][①③][③h]③]1. -- [②b]’‘ ××× [①⑧]0:2 =[[⑤b][②c] [④b][②③][③a][④c][①⑤][①⑦][①g]∈[ [①⑨][①④][①c][②f][②⑧][②①][①C][③c][③g][②⑤][②②]一.[①h].数[][①B]数/[①i][③e][①①][④d][④e][③b][⑤a][①A][②⑧][②⑦][①d][②j]〕〔][://′∈[②④[⑤e]12%b]......................…………………………………………………③ZXFITL[③F]」[①o]]∧′=[ ∪φ∈′|{-②c}[③①]R.L.[①E]Ψ-[*]-↑.日 [②d][②[②⑦][②②][③e][①i][①B][①h][①d][①g][①②][②a]f][⑩]a][①e][②h][②⑥][③d][②⑩]e]〉】元/吨[②⑩]2.3%5:0 [①]::[②][③][④][⑤][⑥][⑦][⑧][⑨] ……——?、。“”《》!,:;?.,."? ·———──? —<>()〔〕[]()-+~×//①②③④⑤⑥⑦⑧⑨⑩ⅢВ\";#@γμφφ.× Δ■▲subexp supsubLex #%&'++ξ++--β<<±<Δ<λ<φ<<===☆=->>λ_~±~+[⑤f][⑤d][②i]≈ [②G][①f]LI㈧ [-......〉[③⑩]第二一番一直一个一些许多种有的是也就是说末##末啊阿哎哎呀哎哟唉俺俺们按按照吧吧哒把罢了被本本着比比方比如鄙人彼彼此边别别的别说并并且不比不成不单不但不独不管不光不过不仅不拘不论不怕不然不如不特不惟不问不只朝朝着趁趁着乘冲除除此之外除非除了此此间此外从从而打待但但是当当着到得的的话等等等地第叮咚对对于多多少而而况而且而是而外而言而已尔后反过来反过来说反之非但非徒否则嘎嘎登该赶个各各个各位各种各自给根据跟故故此固然关于管归果然果真过哈哈哈呵和何何处何况何时嘿哼哼唷呼哧乎哗还是还有换句话说换言之或或是或者极了及及其及至即即便即或即令即若即使几几时己既既然既是继而加之假如假若假使鉴于将较较之叫接着结果借紧接着进而尽尽管经经过就就是就是说据具体地说具体说来开始开外靠咳可可见可是可以况且啦来来着离例如哩连连同两者了临另另外另一方面论嘛吗慢说漫说冒么每每当们莫若某某个某些拿哪哪边哪儿哪个哪里哪年哪怕哪天哪些哪样那那边那儿那个那会儿那里那么那么些那么样那时那些那样乃乃至呢能你你们您宁宁可宁肯宁愿哦呕啪达旁人呸凭凭借其其次其二其他其它其一其余其中起起见起见岂但恰恰相反前后前者且然而然后然则让人家任任何任凭如如此如果如何如其如若如上所述若若非若是啥上下尚且设若设使甚而甚么甚至省得时候什么什么样使得是是的首先谁谁知顺顺着似的虽虽然虽说虽则随随着所所以他他们他人它它们她她们倘倘或倘然倘若倘使腾替通过同同时哇万一往望为为何为了为什么为着喂嗡嗡我我们呜呜呼乌乎无论无宁毋宁嘻吓相对而言像向向着嘘呀焉沿沿着要要不要不然要不是要么要是也也罢也好一一般一旦一方面一来一切一样一则依依照矣以以便以及以免以至以至于以致抑或因因此因而因为哟用由由此可见由于有有的有关有些又于于是于是乎与与此同时与否与其越是云云哉再说再者在在下咱咱们则怎怎么怎么办怎么样怎样咋照照着者这这边这儿这个这会儿这就是说这里这么这么点儿这么些这么样这时这些这样正如吱之之类之所以之一只是只限只要只有至至于诸位着着呢自自从自个儿自各儿自己自家自身综上所述总的来看总的来说总的说来总而言之总之纵纵令纵然纵使遵照作为兮呃呗咚咦喏啐喔唷嗬嗯嗳
这里使用哈工大的停用词列表。
首先加载停用词列表,然后进行过滤操作:
#去除停用词def drop_stopwords(contents,stopwords): contents_clean = [] all_words = [] for line in contents: line_clean = [] for word in line: if word in stopwords: continue line_clean.append(word) all_words.append(word) contents_clean.append(line_clean) return contents_clean,all_words#停用词加载stopwords = pd.read_table("stop_words.txt",names = ["stopword"],quoting = 3)contents = df_content.content_S.values.tolist()contents_clean,all_words = drop_stopwords(contents,stopwords)
接着交给Gensim进行聚类操作:
from gensim import corpora,models,similaritiesimport gensimdictionary = corpora.Dictionary(contents_clean)corpus = [dictionary.doc2bow(sentence) for sentence in contents_clean]lda = gensim.models.ldamodel.LdaModel(corpus=corpus,id2word=dictionary,num_topics=2,random_state=3)#print(lda.print_topics(num_topics=2, num_words=4))for e, values in enumerate(lda.inference(corpus)[0]): print(content[e]) for ee, value in enumerate(values): print(" 分类%d推断值%.2f" % (ee, value))
这里使用LdaModel模型进行训练,分类设置(num_topics)为2种,随机种子(random_state)为3,在训练机器学习模型时,很多模型的训练过程都会涉及到随机数的生成,例如随机梯度下降法(SGD)就是一种随机梯度下降的优化算法。在训练过程中,如果不设置random_state参数,则每次训练结果可能都不同。而设置random_state参数后,每次训练结果都会相同,这就方便了我们在调参时对比模型的效果。如果想要让每次训练的结果都随机,可以将random_state参数设置为None。
程序返回:
[["乾坤", "挪移", "同步", "阻塞", "sync", "三方", "库包", "转换", "异步", "阻塞", "async", "模式", "Python3.10", "实现"], ["Generator", "生成器", "入门", "初基", "Coroutine", "原生", "协程", "登峰造极", "Python3.10", "并发", "异步", "编程", "async", "底层", "实现"], ["周而复始", "往复", "循环", "递归", "递归", "算法", "无限极", "层级", "结构", "探究", "使用", "Golang1.18"], ["彩虹", "女神", "长空", "Go", "语言", "进阶", "Go", "语言", "高性能", "Web", "框架", "Iris", "项目", "实战", "JWT", "中间件", "Middleware", "使用", "EP07"]]乾坤大挪移,如何将同步阻塞(sync)三方库包转换为异步非阻塞(async)模式?Python3.10实现。 分类0推断值0.57 分类1推断值14.43Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现 分类0推断值0.58 分类1推断值15.42周而复始,往复循环,递归、尾递归算法与无限极层级结构的探究和使用(Golang1.18) 分类0推断值12.38 分类1推断值0.62彩虹女神跃长空,Go语言进阶之Go语言高性能Web框架Iris项目实战-JWT和中间件(Middleware)的使用EP07 分类0推断值19.19 分类1推断值0.81
可以看到,结果和ChatGPT聚类结果一致,前两篇为一种分类,后两篇为另外一种分类。
随后可以将聚类结果保存为模型文件:
lda.save("mymodel.model")
以后有新的文章发布,直接对新的文章进行分类推测即可:
from gensim.models import ldamodelimport pandas as pdimport jiebafrom gensim import corporadoc0=\"巧如范金,精比琢玉,一分钟高效打造精美详实的Go语言技术简历(Golang1.18)\"# 加载模型lda = ldamodel.LdaModel.load("mymodel.model")content = [doc0]#分词content_S = []for line in content: current_segment = [w for w in jieba.cut(line) if len(w)>1] if len(current_segment) >1 and current_segment != "\r ": content_S.append(current_segment)#分词结果转为DataFramedf_content = pd.DataFrame({"content_S":content_S})#去除停用词def drop_stopwords(contents,stopwords): contents_clean = [] all_words = [] for line in contents: line_clean = [] for word in line: if word in stopwords: continue line_clean.append(word) all_words.append(word) contents_clean.append(line_clean) return contents_clean,all_words#停用词加载stopwords = pd.read_table("stop_words.txt",names = ["stopword"],quoting = 3)contents = df_content.content_S.values.tolist()contents_clean,all_words = drop_stopwords(contents,stopwords)dictionary = corpora.Dictionary(contents_clean)word = [w for w in jieba.cut(doc0)]bow = dictionary.doc2bow(word)print(lda.get_document_topics(bow))
程序返回:
➜ nlp_chinese /opt/homebrew/bin/python3.10 \"/Users/liuyue/wodfan/work/nlp_chinese/new_text.py\"Building prefix dict from the default dictionary ...Loading model from cache /var/folders/5x/gpftd0654bv7zvzyv39449rc0000gp/T/jieba.cacheLoading model cost 0.264 seconds.Prefix dict has been built successfully.[(0, 0.038379338), (1, 0.9616206)]
这里显示文章推断结果为分类2,也就是Golang类型的文章。
完整调用逻辑:
import jiebaimport pandas as pdimport numpy as npfrom gensim.models import ldamodelfrom gensim import corpora,models,similaritiesimport gensimclass LdaRec: def __init__(self,cotent:list) ->None: self.content = content self.contents_clean = [] self.lda = None def test_text(self,content:str): self.lda = ldamodel.LdaModel.load("mymodel.model") self.content = [content] #分词 content_S = [] for line in self.content: current_segment = [w for w in jieba.cut(line) if len(w)>1] if len(current_segment) >1 and current_segment != "\r ": content_S.append(current_segment) #分词结果转为DataFrame df_content = pd.DataFrame({"content_S":content_S}) contents = df_content.content_S.values.tolist() dictionary = corpora.Dictionary(contents) word = [w for w in jieba.cut(content)] bow = dictionary.doc2bow(word) print(self.lda.get_document_topics(bow)) # 训练 def train(self,num_topics=2,random_state=3): dictionary = corpora.Dictionary(self.contents_clean) corpus = [dictionary.doc2bow(sentence) for sentence in self.contents_clean] self.lda = gensim.models.ldamodel.LdaModel(corpus=corpus,id2word=dictionary,num_topics=num_topics,random_state=random_state) for e, values in enumerate(self.lda.inference(corpus)[0]): print(self.content[e]) for ee, value in enumerate(values): print(" 分类%d推断值%.2f" % (ee, value)) # 过滤停用词 def drop_stopwords(self,contents,stopwords): contents_clean = [] for line in contents: line_clean = [] for word in line: if word in stopwords: continue line_clean.append(word) contents_clean.append(line_clean) return contents_clean def cut_word(self) ->list: #分词 content_S = [] for line in self.content: current_segment = [w for w in jieba.cut(line) if len(w)>1] if len(current_segment) >1 and current_segment != "\r ": content_S.append(current_segment) #分词结果转为DataFrame df_content = pd.DataFrame({"content_S":content_S}) # 停用词列表 stopwords = pd.read_table("stop_words.txt",names = ["stopword"],quoting = 3) contents = df_content.content_S.values.tolist() stopwords = stopwords.stopword.values.tolist() self.contents_clean = self.drop_stopwords(contents,stopwords)if __name__ == "__main__": title1=\"乾坤大挪移,如何将同步阻塞(sync)三方库包转换为异步非阻塞(async)模式?Python3.10实现。\" title2=\"Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现\" title3=\"周而复始,往复循环,递归、尾递归算法与无限极层级结构的探究和使用(Golang1.18)\" title4=\"彩虹女神跃长空,Go语言进阶之Go语言高性能Web框架Iris项目实战-JWT和中间件(Middleware)的使用EP07\" content = [title1,title2, title3,title4] lr = LdaRec(content) lr.cut_word() lr.train() lr.lda.save("mymodel.model") lr.test_text(\"巧如范金,精比琢玉,一分钟高效打造精美详实的Go语言技术简历(Golang1.18)\")
至此,基于聚类的推荐系统构建完毕,每一篇文章只需要通过既有分类模型进行训练,推断分类之后,给用户推送同一分类下的文章即可,截止本文发布,该分类模型已经在本站进行落地实践:
结语
金无足赤,LDA聚类算法也不是万能的,LDA聚类算法有许多超参数,包括主题个数、学习率、迭代次数等,这些参数的设置对结果有很大影响,但是很难确定最优参数,同时聚类算法的时间复杂度是O(n^2)级别的,在处理大规模文本数据时,计算速度较慢,反之,在样本数据较少的情况下,模型的泛化能力较差。最后,奉上项目地址,与君共觞:https://github.com/zcxey2911/Lda-Gensim-Recommended-System-Python310