眾所周知,個性化推薦系統能夠根據使用者的興趣、偏好等資訊向使用者推薦相關內容,使得使用者更感興趣,從而提升使用者體驗,提高使用者粘度,之前我們曾經使用協同過濾演算法構建過個性化推薦系統,但基於顯式反饋的演算法就會有一定的侷限性,本次我們使用無監督的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]
={
}
,也
‘
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
[①]
::
[②]
[③]
[④]
[⑤]
[⑥]
[⑦]
[⑧]
[⑨]
……
——
?
、
。
“
”
《
》
!
,
:
;
?
.
,
.
'
?
·
———
──
?
—
<
>
(
)
〔
〕
[
]
(
)
-
+
~
×
/
/
①
②
③
④
⑤
⑥
⑦
⑧
⑨
⑩
Ⅲ
В
"
;
#
@
γ
μ
φ
φ.
×
Δ
■
▲
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