物以类聚人以群分,通过GensimLda文本聚类算法构建人工智能个性化推荐系统(Python3.10)

by Liu Yue/2023-01-09

    众所周知,个性化推荐系统能够根据用户的兴趣、偏好等信息向用户推荐相关内容,使得用户更感兴趣,从而提升用户体验,提高用户粘度,之前我们曾经使用协同过滤算法构建过个性化推荐系统,但基于显式反馈的算法就会有一定的局限性,本次我们使用无监督的Lda文本聚类方式来构建文本的个性化推荐系统。

    推荐算法:协同过滤/Lda聚类

    我们知道,协同过滤算法是一种基于用户的历史行为来推荐物品的算法。协同过滤算法利用用户之间的相似性来推荐物品,如果两个用户对某些物品的评分相似,则协同过滤算法会将这两个用户视为相似的,并向其中一个用户推荐另一个用户喜欢的物品。

    说白了,它基于用户的显式反馈,什么是显式反馈?举个例子,本如本篇文章,用户看了之后,可能会点赞,也可能会疯狂点踩,或者写一些关于文本的评论,当然评论内容可能是负面、正面或者中性,所有这些用户给出的行为,都是显式反馈,但如果用户没有反馈出这些行为,就只是看了看,协同过滤算法的效果就会变差。

    LDA聚类是一种文本聚类算法,它通过对文本进行主题建模来聚类文本。LDA聚类算法在聚类文本时,不考虑用户的历史行为,而是根据文本的内容和主题来聚类。

    说得通俗一点,协同过滤是一种主动推荐,系统根据用户历史行为来进行内容推荐,而LDA聚类则是一种被动推荐,在用户还没有产生用户行为时,就已经开始推荐动作。

    LDA聚类的主要目的是将文本分为几类,使得每类文本的主题尽可能相似。

    LDA聚类算法的工作流程大致如下:

    1.对文本进行预处理,去除停用词等。

    2.使用LDA模型对文本进行主题建模,得到文本的主题分布。

    3.将文本按照主题分布相似性进行聚类。

    4.将聚类结果作为类标签,对文本进行分类。


    大体上,LDA聚类算法是一种自动将文本分类的算法,它通过对文本进行主题建模,将文本按照主题相似性进行聚类,最终实现文本的分类。

    Python3.10实现

    实际应用层面,我们需要做的是让主题模型能够识别在文本里的主题,并且挖掘文本信息中隐式信息,并且在主题聚合、从非结构化文本中提取信息。

    首先安装分词以及聚类模型库:

pip3 install jieba
pip3 install gensim

    随后进行分词操作,这里以笔者的几篇文章为例子:

import jieba
import pandas as pd
import numpy as np
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]


#分词
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\t':
content_S.append(current_segment)
#分词结果转为DataFrame
df_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]
={
}
,也


[①⑥]
[②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
[①]
::
[②]
[③]
[④]
[⑤]
[⑥]
[⑦]
[⑧]
[⑨]
……
——
?












,

'
?
·
———
──
?

<
>




[
]
(
)
-
+

×

/











В
"
;
#
@
γ
μ
φ
φ.
×
Δ


sub
exp
sup
sub
Lex





+ξ
++

-β

<±
<Δ
<λ
<φ
<<
=

=☆
=-

>λ
_
~±
~+
[⑤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,similarities
import gensim

dictionary = 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('\t分类%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.43
Generator(生成器),入门初基,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  ldamodel
import pandas as pd
import jieba
from gensim import corpora

doc0="巧如范金,精比琢玉,一分钟高效打造精美详实的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\t':
content_S.append(current_segment)
#分词结果转为DataFrame
df_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.cache
Loading model cost 0.264 seconds.
Prefix dict has been built successfully.
[(0, 0.038379338), (1, 0.9616206)]

    这里显示文章推断结果为分类2,也就是Golang类型的文章。

    完整调用逻辑:

import jieba
import pandas as pd
import numpy as np
from gensim.models import ldamodel
from gensim import corpora,models,similarities
import gensim


class 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\t':
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('\t分类%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\t':
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