天天看點

機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考

LDA主題模型

  • 1前言
    • 1.1 資料介紹
    • 1.2 我們為什麼要引入上面的外部資料源?
    • 1.3 那我們該怎麼去做?
  • 2 讀入資料
  • 3分詞處理
    • 3.1 先原始分詞
    • 3.2 引入常見停用詞
    • 3.3 自定義詞典
    • 3.4 批量對這批資料進行分詞處理
  • 4 提取特征
    • 4.1 先從資料中提取出高的标簽
      • 4.1.1 單個實驗
      • 4.1.2 封裝函數
    • 4.2 提取使用者年齡
    • 4.3 提取小孩年齡
    • 4.4 提取性别
  • 5 思路1:使用兩種關鍵詞提取方法
    • 5.1 TF-IDF
      • 5.1.1 不帶得分
      • 5.1.2 帶得分
      • 5.1.3 批量求解
    • 5.2 TEXT-RANK
  • 6 思路2:使用LDA主題模型
    • 6.1 什麼是LDA模型?
    • 6.2 數學推導
    • 6.3 LDA 中主題數目如何确定?
      • 6.3.1 業務上:手動調參
      • 6.3.2 學術上
    • 6.4 LDA模型的具體實作
      • 6.4.1 定義提取特征關鍵詞的數量
      • 6.4.2 關鍵詞提取和向量轉換
      • 6.4.3 開始進行LDA拟合資料 提取主題
      • 6.4.4 定義函數顯示每個主題裡面前若幹個關鍵詞
      • 6.4.5 可視化展示
      • 6.4.6 如何反向定位到每一個人屬于哪一個主題
      • 6.4.7 結果展示
      • 6.4.8 資料探索
    • 6.5 LDA模型的封裝
  • 7 參考

1前言

1.1 資料介紹

最近公司在推進一個項目,剛好自己在負責資料探索的部分,現在項目結束,将資料脫敏,總結一下當時的處理過程~

  • 現在外部資料源提供了如下的資料:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考
    - 資料解釋:每一條記錄代表了一個使用者的标簽,從該使用者手機app安裝的記錄及使用情況對使用者進行了一個畫像。

1.2 我們為什麼要引入上面的外部資料源?

因為部分低活的使用者标簽不完善,無法起到精準推薦的作用,希望能夠引入外部資料源,補充使用者的标簽,具體能達到兩個方面的目的:

  • 一方面補充使用者的标簽次元
  • 一方面進行關聯的推送相應内容

1.3 那我們該怎麼去做?

兩種思路:

  • 一種思路是對一個使用者對應文本描述中提取出關鍵詞,根據關鍵詞去補充使用者标簽,但這樣可能無法直覺看到類似的人群
  • 另一種思路則是将每一個使用者的描述看成一篇文檔,所有的使用者組成所有文檔,運用LDA主題模型去探究隐藏在背後的主題,然後根據得出的結果去和業務人員溝通,看幾類主題的可解釋性如何,如果ok,再去反定位到每一篇文檔,即每一個使用者對應的主題的機率,運用軟分類即可确定每一位使用者的主題!這樣我們的目的就完美實作啦~

2 讀入資料

import pandas as pd
           
df = df[['device_uuid', '标簽']]
df.head()
           
device_uuid 标簽
CQk5NTM4ZTg3MzhhZDYxNjBmCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,...
1 CQlhNWY2YWVjODFhYWNjOTFjCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,...
2 CQkxY2M4MTMyMDgxMjhhMzljCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,...
3 CQlmNWNkY2U0NDM2MTBjYjkyCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,...
4 CQliY2U2OGM0ODUxZDdkNDEJMDgxNWY4OWI1NjIwMWUwNA... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,...

3分詞處理

填坑總結:

  • 一開始進行分詞的時候直接就是使用結巴分詞,但還應該考慮兩方面的内容:
  • 一個是增加停用詞,即如果出現這些詞就删去,比如的,标點符号等等。
  • 另一個則是自定義詞典,不同的業務場景會對應不一樣的詞典,比如這個外部資料源就會有“打車出行”等專有詞彙,如果不加入詞典就會分成“打車”“出行”兩個詞彙,是以下面分詞部分也會着重說下這兩塊如何去做!

3.1 先原始分詞

import jieba
def chinese_word_cut(mytext):
    return " ".join(jieba.cut(mytext))
           
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/vx/np6lccw52hdfcz_2qswpfhch0000gn/T/jieba.cache
Loading model cost 0.682 seconds.
Prefix dict has been built succesfully.





'女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 , 0 - 6 歲 小孩 父母 , 0 - 14 歲 小孩 父母 , 消費水準 _ 高 , 已婚 , 白領 , 大陸 , 二線 城市 , 異性戀 , 有車 , 金融 理财 , 投資 理财 , 投資 理财 - 低 , 記賬 , 記賬 - 低 , 銀行 , 銀行 - 中 , 信用卡 , 信用卡 - 高 , 購物 , 網購 , 網購 - 高 , 海淘 , 海淘 - 低 , 團購 , 團購 - 低 , 新聞 閱讀 , 新聞資訊 , 新聞 · 資訊 - 高 , 影音 , 電視直播 , 電視直播 - 中 , 線上視訊 , 線上視訊 - 低 , 線上音樂 , 線上音樂 - 低 , 聊天 社交 , 聊天 , 聊天 - 低 , 生活 服務 , 搬家 , 運動 健康 , 健康 醫療 , 健康 醫療 - 低 , 圖像 , 拍攝 美化 , 拍攝 美化 - 低 , 麗人 母嬰 , 美容 美妝 , 美容 美妝 - 中 , 生活 實用 , 打車 出行 , 打車 出行 - 低 , 地圖 導航 , 地圖 導航 - 低 , 天氣 查詢 , 天氣 查詢 - 中 , 效率 辦公 , 存儲 雲盤 , 存儲 雲盤 - 低 , 遊戲 , 經營政策 , 數位科技 _ 綜合 科技 , 數位科技 _ 手機 , 數位科技 _ 電腦 , 數位科技 _ 數位産品 , 汽車 交通 _ 汽車 綜合 資訊 , 三星 , 中國移動 , 中文 簡體 , 居住地 _ 山西省 , 居住地 _ 太原市 , 家鄉 _ 太原市'
           
  • 可以看到,有些專有的詞彙我是不想讓其分開的,比如“打車出行”,“美容美妝”。
  • 另外逗号我們是不希望有的

3.2 引入常見停用詞

list_100 = list(range(100))
list_100_str = list(map(lambda x: str(x), list_100))
list_100_str[:10]
           
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
           
#從檔案導入停用詞表
stpwrdpath = "stop_words.txt"
stpwrd_dic = open(stpwrdpath, encoding='gbk') # 加上編碼,不然就不會顯示中文了!
stpwrd_content = stpwrd_dic.read()
# print(stpwrd_content)
#将停用詞表轉換為list 
stpwrdlst = stpwrd_content.splitlines()
# 在停用詞裡面再把年齡 也就是具體的數字全部去掉 是以停用詞加上0-100的數字!【為什麼呢?因為單獨提取出了年齡】
stpwrdlst = stpwrdlst + list_100_str # 先測試這一版 如果不對 就要把數字變成字元[修改了!]
print(stpwrdlst[:5]) # 看前五個停用詞是啥
stpwrd_dic.close()
           
[',', '?', '、', '。', '“']
           
def word_cut(mytext):
    return " ".join(x for x in jieba.cut(mytext) if x not in stpwrdlst)
           
'女 歲 父母 歲 小孩 父母 媽媽 母嬰 歲 小孩 父母 歲 小孩 父母 消費水準 高 已婚 白領 大陸 二線 城市 異性戀 有車 金融 理财 投資 理财 投資 理财 低 記賬 記賬 低 銀行 銀行 中 信用卡 信用卡 高 購物 網購 網購 高 海淘 海淘 低 團購 團購 低 新聞 閱讀 新聞資訊 新聞 資訊 高 影音 電視直播 電視直播 中 線上視訊 線上視訊 低 線上音樂 線上音樂 低 聊天 社交 聊天 聊天 低 生活 服務 搬家 運動 健康 健康 醫療 健康 醫療 低 圖像 拍攝 美化 拍攝 美化 低 麗人 母嬰 美容 美妝 美容 美妝 中 生活 實用 打車 出行 打車 出行 低 地圖 導航 地圖 導航 低 天氣 查詢 天氣 查詢 中 效率 辦公 存儲 雲盤 存儲 雲盤 低 遊戲 經營政策 數位科技 綜合 科技 數位科技 手機 數位科技 電腦 數位科技 數位産品 汽車 交通 汽車 綜合 資訊 三星 中國移動 中文 簡體 居住地 山西省 居住地 太原市 家鄉 太原市'
           

總結:可以看到一些停用詞就被剔除了,完美!但是一般這個停用詞在LDA模型中不是這裡使用哈,而是在後面搭建LDA模型的時候—關鍵詞提取和向量轉換時,進行去除,不過本質都是一樣的哈!

3.3 自定義詞典

上面的停用詞解決了剔除一些我們不想要的字元,那我們如何不去拆解一些我們本就不想去拆分開的字元呢?這時候自定義詞典就可以閃亮登場了!

import jieba
jieba.load_userdict('dic_netease.txt')
           

直接将自定義的詞彙粘貼到txt中即可,如果樣本量很大,可以通過代碼實作,少直接複制粘貼。上面直接導入即可!

現在已經導入了自定義詞典了!讓我們來看看分詞後有什麼神奇的效果?

'女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 , 0 - 6 歲 小孩 父母 , 0 - 14 歲 小孩 父母 , 消費水準_高 , 已婚 , 白領 , 大陸 , 二線城市 , 異性戀 , 有車 , 金融理财 , 投資理财 , 投資理财 - 低 , 記賬 , 記賬 - 低 , 銀行 , 銀行 - 中 , 信用卡 , 信用卡 - 高 , 購物 , 網購 , 網購 - 高 , 海淘 , 海淘 - 低 , 團購 , 團購 - 低 , 新聞閱讀 , 新聞資訊 , 新聞 · 資訊 - 高 , 影音 , 電視直播 , 電視直播 - 中 , 線上視訊 , 線上視訊 - 低 , 線上音樂 , 線上音樂 - 低 , 聊天社交 , 聊天 , 聊天 - 低 , 生活服務 , 搬家 , 運動健康 , 健康醫療 , 健康醫療 - 低 , 圖像 , 拍攝美化 , 拍攝美化 - 低 , 麗人母嬰 , 美容美妝 , 美容美妝 - 中 , 生活實用 , 打車出行 , 打車出行 - 低 , 地圖導航 , 地圖導航 - 低 , 天氣查詢 , 天氣查詢 - 中 , 效率辦公 , 存儲雲盤 , 存儲雲盤 - 低 , 遊戲 , 經營政策 , 數位科技 _ 綜合科技 , 數位科技 _ 手機 , 數位科技 _ 電腦 , 數位科技 _ 數位産品 , 汽車交通 _ 汽車綜合資訊 , 三星 , 中國移動 , 中文簡體 , 居住地 _ 山西省 , 居住地 _ 太原市 , 家鄉 _ 太原市'
           
  • 可以看到打車出現已經在一起了!完美!自定義詞典就是這麼幹!

3.4 批量對這批資料進行分詞處理

df['cut_label'] = df['标簽'].map(chinese_word_cut)
df['cut_label'].head()
           
0    女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,...
1    女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,...
2    女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,...
3    女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,...
4    女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,...
Name: cut_label, dtype: object
           
'女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 , 0 - 6 歲 小孩 父母 , 0 - 14 歲 小孩 父母 , 消費水準_高 , 已婚 , 白領 , 大陸 , 二線城市 , 異性戀 , 有車 , 金融理财 , 投資理财 , 投資理财 - 低 , 記賬 , 記賬 - 低 , 銀行 , 銀行 - 中 , 信用卡 , 信用卡 - 高 , 購物 , 網購 , 網購 - 高 , 海淘 , 海淘 - 低 , 團購 , 團購 - 低 , 新聞閱讀 , 新聞資訊 , 新聞 · 資訊 - 高 , 影音 , 電視直播 , 電視直播 - 中 , 線上視訊 , 線上視訊 - 低 , 線上音樂 , 線上音樂 - 低 , 聊天社交 , 聊天 , 聊天 - 低 , 生活服務 , 搬家 , 運動健康 , 健康醫療 , 健康醫療 - 低 , 圖像 , 拍攝美化 , 拍攝美化 - 低 , 麗人母嬰 , 美容美妝 , 美容美妝 - 中 , 生活實用 , 打車出行 , 打車出行 - 低 , 地圖導航 , 地圖導航 - 低 , 天氣查詢 , 天氣查詢 - 中 , 效率辦公 , 存儲雲盤 , 存儲雲盤 - 低 , 遊戲 , 經營政策 , 數位科技 _ 綜合科技 , 數位科技 _ 手機 , 數位科技 _ 電腦 , 數位科技 _ 數位産品 , 汽車交通 _ 汽車綜合資訊 , 三星 , 中國移動 , 中文簡體 , 居住地 _ 山西省 , 居住地 _ 太原市 , 家鄉 _ 太原市'
           

4 提取特征

  • 現在已經進行了分詞處理,但其實從原始的文本資料中我們可以做一些特征的提取,比如年齡,小孩年齡,是否為父母等,而這些特征将會對我們起到一定的輔助作用。

4.1 先從資料中提取出高的标簽

使用正規表達式進行提取“高”标簽

4.1.1 單個實驗

df_demo = df['标簽'][0]
df_demo
           
'女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,消費水準_高,已婚,白領,大陸,二線城市,異性戀,有車,金融理财,投資理财,投資理财-低,記賬,記賬-低,銀行,銀行-中,信用卡,信用卡-高,購物,網購,網購-高,海淘,海淘-低,團購,團購-低,新聞閱讀,新聞資訊,新聞·資訊-高,影音,電視直播,電視直播-中,線上視訊,線上視訊-低,線上音樂,線上音樂-低,聊天社交,聊天,聊天-低,生活服務,搬家,運動健康,健康醫療,健康醫療-低,圖像,拍攝美化,拍攝美化-低,麗人母嬰,美容美妝,美容美妝-中,生活實用,打車出行,打車出行-低,地圖導航,地圖導航-低,天氣查詢,天氣查詢-中,效率辦公,存儲雲盤,存儲雲盤-低,遊戲,經營政策,數位科技_綜合科技,數位科技_手機,數位科技_電腦,數位科技_數位産品,汽車交通_汽車綜合資訊,三星,中國移動,中文簡體,居住地_山西省,居住地_太原市,家鄉_太原市'
           
import re
split=re.compile("[^,]*高")
try:
    result=split.findall(df_demo)
except:
    result=''
           
result
           
['消費水準_高', '信用卡-高', '網購-高', '新聞·資訊-高']
           

4.1.2 封裝函數

import re
def ExtraHigh(s):
    split=re.compile("[^,]*高")
    try:
        result=split.findall(s)
    except:
        result=''
    return result
           
df['high_label'] = df['标簽'].apply(ExtraHigh)
df['high_label'][10:20]
           
10              [消費水準_高, 商務旅行-高, 愛車-高, 新聞·資訊-高, 電視直播-高]
11              [消費水準_高, 新聞·資訊-高, 聽書-高, 線上視訊-高, 線上音樂-高]
12                            [消費水準_高, 新聞·資訊-高, 天氣查詢-高]
13                [消費水準_高, 愛車-高, 新聞·資訊-高, 社群-高, 拍攝美化-高]
14    [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高, 婚戀-高, 打車出行-高, 天...
15    [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高, 婚戀-高, 打車出行-高, 天...
16    [消費水準_高, 銀行-高, 網購-高, 愛車-高, 新聞·資訊-高, 音樂播放器-高, 視...
17    [消費水準_高, 銀行-高, 網購-高, 愛車-高, 新聞·資訊-高, 音樂播放器-高, 視...
18    [消費水準_高, 投資理财-高, 網貸p2p-高, 網購-高, 快遞-高, 中國小教育-高,...
19    [消費水準_高, 網購-高, 團購-高, 休閑旅行-高, 新聞·資訊-高, 線上視訊-高, ...
Name: high_label, dtype: object
           

4.2 提取使用者年齡

import re
def ExtraAge(s):
    split=re.compile("[^,]*歲[,]")
    try:
        result=split.findall(s)
    except:
        result=''
    return result
           
['25-34歲,']
           
df['age'] = df['标簽'].map(ExtraAge)
(df['age'].map(str)).value_counts()
           
['25-34歲,']    13712
['18-24歲,']     5222
['35-44歲,']     2826
Name: age, dtype: int64
           

4.3 提取小孩年齡

import re
def ExtraAgeKid(s):
    split=re.compile("[^,]*小孩父母")
    try:
        result=split.findall(s)
        if result == []:
            result = '無'
    except:
        result=''
    return result
           
['0-3歲小孩父母', '0-6歲小孩父母', '0-14歲小孩父母']
           
df['age_kid'] = df['标簽'].map(ExtraAgeKid)
(df['age_kid'].map(str)).value_counts()
           
無                                                     13757
['0-3歲小孩父母', '0-6歲小孩父母', '0-14歲小孩父母']                  4301
['3-14歲小孩父母', '0-14歲小孩父母']                             2127
['3-6歲小孩父母', '0-6歲小孩父母', '3-14歲小孩父母', '0-14歲小孩父母']     1504
['0-6歲小孩父母', '3-14歲小孩父母', '0-14歲小孩父母']                   41
['0-6歲小孩父母', '0-14歲小孩父母']                                30
Name: age_kid, dtype: int64
           

4.4 提取性别

def ExtraSex(x):
    return df['标簽'][x][:1]
           
df.index = range(len(df))
df['gender'] = df.index.map(ExtraSex)
print(df['gender'].head())
df['gender'].value_counts()
           
0    女
1    女
2    女
3    女
4    女
Name: gender, dtype: object





女    10935
男    10825
Name: gender, dtype: int64
           

5 思路1:使用兩種關鍵詞提取方法

關于TF-IDF和TEXT-RANK的具體原理見之前的部落格:機器學習 | TF-IDF和TEXT-RANK的差別

5.1 TF-IDF

'女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 , 0 - 6 歲 小孩 父母 , 0 - 14 歲 小孩 父母 , 消費水準_高 , 已婚 , 白領 , 大陸 , 二線城市 , 異性戀 , 有車 , 金融理财 , 投資理财 , 投資理财 - 低 , 記賬 , 記賬 - 低 , 銀行 , 銀行 - 中 , 信用卡 , 信用卡 - 高 , 購物 , 網購 , 網購 - 高 , 海淘 , 海淘 - 低 , 團購 , 團購 - 低 , 新聞閱讀 , 新聞資訊 , 新聞 · 資訊 - 高 , 影音 , 電視直播 , 電視直播 - 中 , 線上視訊 , 線上視訊 - 低 , 線上音樂 , 線上音樂 - 低 , 聊天社交 , 聊天 , 聊天 - 低 , 生活服務 , 搬家 , 運動健康 , 健康醫療 , 健康醫療 - 低 , 圖像 , 拍攝美化 , 拍攝美化 - 低 , 麗人母嬰 , 美容美妝 , 美容美妝 - 中 , 生活實用 , 打車出行 , 打車出行 - 低 , 地圖導航 , 地圖導航 - 低 , 天氣查詢 , 天氣查詢 - 中 , 效率辦公 , 存儲雲盤 , 存儲雲盤 - 低 , 遊戲 , 經營政策 , 數位科技 _ 綜合科技 , 數位科技 _ 手機 , 數位科技 _ 電腦 , 數位科技 _ 數位産品 , 汽車交通 _ 汽車綜合資訊 , 三星 , 中國移動 , 中文簡體 , 居住地 _ 山西省 , 居住地 _ 太原市 , 家鄉 _ 太原市'
           

5.1.1 不帶得分

for keyword in extract_tags(df['cut_label'][0], topK=5): # k可選 也可以選擇要不要把得分打出來
    print('%s' % (keyword))
           
數位科技
電視直播
線上視訊
父母
投資理财
           
Help on method extract_tags in module jieba.analyse.tfidf:

extract_tags(sentence, topK=20, withWeight=False, allowPOS=(), withFlag=False) method of jieba.analyse.tfidf.TFIDF instance
    Extract keywords from sentence using TF-IDF algorithm.
    Parameter:
        - topK: return how many top keywords. `None` for all possible words.
        - withWeight: if True, return a list of (word, weight);
                      if False, return a list of words.
        - allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v','nr'].
                    if the POS of w is not in this list,it will be filtered.
        - withFlag: only work with allowPOS is not empty.
                    if True, return a list of pair(word, weight) like posseg.cut
                    if False, return a list of words
           

5.1.2 帶得分

for keyword, weight in extract_tags(df['cut_label'][0], topK=4, withWeight=True): # k可選 也可以選擇要不要把得分打出來
    print('%s : %s' % (keyword, weight))
           
數位科技 : 0.5805507899516483
電視直播 : 0.29027539497582416
線上視訊 : 0.29027539497582416
父母 : 0.2807938508703297
           

5.1.3 批量求解

def ExtrWord(s):
    e1 = extract_tags(s, topK=5)
    return e1
           
10      [電視直播, 線上視訊, 外賣, 投資理财, 網購]
11      [線上視訊, 投資理财, 網購, 海淘, 商務旅行]
12      [線上視訊, 外賣, 網購, 線上教育, 休閑旅行]
13    [電視直播, 線上視訊, 網購, 違章查詢, 線上音樂]
14      [金融财經, 線上視訊, 外賣, 網購, 休閑旅行]
Name: tf_word, dtype: object
           

5.2 TEXT-RANK

for keyword, weight in textrank(df['cut_label'][0], topK=5, withWeight=True):
    print('%s %s' % (keyword, weight))
           
數位科技 1.0
父母 0.6121734117319969
電視直播 0.5564012710643426
網購 0.54952782148969
居住地 0.49139059826998766
           
def TextRank(s):
    e1 = textrank(s, topK=5)
    return e1
           
10      [電視直播, 線上視訊, 外賣, 投資理财, 網購]
11      [線上視訊, 投資理财, 網購, 海淘, 商務旅行]
12      [線上視訊, 外賣, 網購, 線上教育, 休閑旅行]
13    [電視直播, 線上視訊, 網購, 違章查詢, 線上音樂]
14      [金融财經, 線上視訊, 外賣, 網購, 休閑旅行]
Name: tf_word, dtype: object
           

6 思路2:使用LDA主題模型

通過上述的工作我們已經完成了關鍵詞的提取,但是還是無法看出這批使用者有哪幾大類興趣點,于是LDA模型登場!

6.1 什麼是LDA模型?

在機器學習領域,LDA是兩個常用模型的簡稱:

  • Linear Discriminant Analysis 線性判别分析【一種降維方法 後面和PCA一起在部落格補充】https://www.cnblogs.com/pinard/p/6244265.html
  • Latent Dirichlet Allocation。隐含狄利克雷分布

現在讨論的LDA僅指代Latent Dirichlet Allocation. LDA在主題模型中占有非常重要的地位,常用來文本分類。

定義:

  • LDA(Latent Dirichlet Allocation)是一種文檔主題生成模型,也稱為一個三層貝葉斯機率模型,包含詞、主題和文檔三層結構。
  • 作用:尋找多個文檔記憶體在的潛在主題。
  • LDA是一種非監督機器學習方法,可以用來識别大規模文檔集(document collection)或語料庫(corpus)中潛藏的主題資訊。它采用了詞袋(bag of words)的方法,這種方法将每一篇文檔視為一個詞頻向量,進而将文本資訊轉化為了易于模組化的數字資訊。每一篇文檔代表了一些主題所構成的一個機率分布,而每一個主題又代表了很多單詞所構成的一個機率分布。

6.2 數學推導

詳情見 :

  • https://blog.csdn.net/v_JULY_v/article/details/41209515
  • https://zhuanlan.zhihu.com/p/31470216

6.3 LDA 中主題數目如何确定?

6.3.1 業務上:手動調參

在 LDA 中,主題的數目沒有一個固定的最優解。模型訓練時,需要事先設定主題數,根據訓練出來的結果,手動調參(原因在于不同業務對于生成topic的要求是存在差異的),要優化主題數目,進而優化文本分類結果。

6.3.2 學術上

  1. 用perplexity-topic number曲線
  • LDA有一個自己的評價标準叫Perplexity(困惑度),可以了解為,對于一篇文檔d,我們的模型對文檔d屬于哪個topic有多不确定,這個不确定程度就是Perplexity。
  • 其他條件固定的情況下,topic越多,則Perplexity越小,但是容易過拟合。
  1. 用topic_number-logP(w|T)曲線
  2. 基于密度的自适應最優LDA模型選擇方法
  • 選取初始主題數K值,得到初始模型,計算各topic之間的相似度
  • 增加或減少K的值,重新訓練得到模型,再次計算topic之間的相似度
  • 重複疊代第二步直到得到最優的K
  1. 利用HDP(層次狄利克雷過程)

6.4 LDA模型的具體實作

device_uuid 标簽 cut_label high_label age age_kid gender
CQk5NTM4ZTg3MzhhZDYxNjBmCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,... 女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,... [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高] [25-34歲,] [0-3歲小孩父母, 0-6歲小孩父母, 0-14歲小孩父母]
1 CQlhNWY2YWVjODFhYWNjOTFjCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,... 女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,... [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高] [25-34歲,] [0-3歲小孩父母, 0-6歲小孩父母, 0-14歲小孩父母]
2 CQkxY2M4MTMyMDgxMjhhMzljCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,... 女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,... [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高] [25-34歲,] [0-3歲小孩父母, 0-6歲小孩父母, 0-14歲小孩父母]
3 CQlmNWNkY2U0NDM2MTBjYjkyCTA4MTVmODliNTYyMDFlMD... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,... 女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,... [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高] [25-34歲,] [0-3歲小孩父母, 0-6歲小孩父母, 0-14歲小孩父母]
4 CQliY2U2OGM0ODUxZDdkNDEJMDgxNWY4OWI1NjIwMWUwNA... 女,25-34歲,父母,0-3歲小孩父母,媽媽,母嬰,0-6歲小孩父母,0-14歲小孩父母,... 女 , 25 - 34 歲 , 父母 , 0 - 3 歲 小孩 父母 , 媽媽 , 母嬰 ,... [消費水準_高, 信用卡-高, 網購-高, 新聞·資訊-高] [25-34歲,] [0-3歲小孩父母, 0-6歲小孩父母, 0-14歲小孩父母]
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
           

6.4.1 定義提取特征關鍵詞的數量

6.4.2 關鍵詞提取和向量轉換

tf_vectorizer = CountVectorizer(strip_accents='unicode',
                               max_features=n_features,
                               stop_words=stpwrdlst, # 停用詞用在這!
                               max_df=0.5,
                               min_df=10)
tf = tf_vectorizer.fit_transform(df['cut_label'])
           
n_topics = 5
lda = LatentDirichletAllocation(n_topics=n_topics,
                               max_iter=10,
                                learning_method='online',
                               learning_offset=50,
                               random_state=23)
           

6.4.3 開始進行LDA拟合資料 提取主題

/Users/apple/anaconda3/lib/python3.6/site-packages/sklearn/decomposition/online_lda.py:314: DeprecationWarning: n_topics has been renamed to n_components in version 0.19 and will be removed in 0.21
  DeprecationWarning)





LatentDirichletAllocation(batch_size=128, doc_topic_prior=None,
             evaluate_every=-1, learning_decay=0.7,
             learning_method='online', learning_offset=50,
             max_doc_update_iter=100, max_iter=10, mean_change_tol=0.001,
             n_components=10, n_jobs=None, n_topics=5, perp_tol=0.1,
             random_state=23, topic_word_prior=None,
             total_samples=1000000.0, verbose=0)
           

6.4.4 定義函數顯示每個主題裡面前若幹個關鍵詞

def print_top_words(model, feature_names, n_top_words):
    for topic_idx, topic in enumerate(model.components_):
        print("Topic #%d:" % topic_idx)
        print(" ".join([feature_names[i]
                        for i in topic.argsort()[:-n_top_words - 1:-1]]))
    print()
           

注:函數argsort的補充

作用:argsort函數傳回的是數組值從小到大的索引值

import numpy as np
a = np.array([1,3,4,5,7,2,6])
a.argsort() 
# 正常排序是1 2 3 4 5 6 7
# 對應的索引是 0 5 1  2 3 6 4
           
array([0, 5, 1, 2, 3, 6, 4])
           
n_top_words = 5
tf_feature_names = tf_vectorizer.get_feature_names()
print_top_words(lda, tf_feature_names, n_top_words)
           
Topic #0:
美容美妝 數位科技 金融财經 存儲雲盤 休閑旅行
Topic #1:
娛樂 生活 體育 綜合娛樂 科技
Topic #2:
打車出行 休閑旅行 外賣 郵箱 存儲雲盤
Topic #3:
買車 汽車 愛車 汽車交通 有車
Topic #4:
父母 小孩 育兒社群 美容美妝 團購
           

6.4.5 可視化展示

import pyLDAvis
import pyLDAvis.sklearn
pyLDAvis.enable_notebook()
pyLDAvis.sklearn.prepare(lda, tf, tf_vectorizer)
           

6.4.6 如何反向定位到每一個人屬于哪一個主題

/Users/apple/anaconda3/lib/python3.6/site-packages/sklearn/decomposition/online_lda.py:314: DeprecationWarning: n_topics has been renamed to n_components in version 0.19 and will be removed in 0.21
  DeprecationWarning)
           
docs
           
array([[0.47307049, 0.00389658, 0.2138838 , 0.00398295, 0.30516618],
       [0.47307049, 0.00389658, 0.2138838 , 0.00398295, 0.30516618],
       [0.47307049, 0.00389658, 0.2138838 , 0.00398295, 0.30516618],
       ...,
       [0.1610961 , 0.00255204, 0.25369255, 0.19298993, 0.38966939],
       [0.31746614, 0.00457675, 0.27187681, 0.00463559, 0.40144471],
       [0.06701105, 0.00546047, 0.44921981, 0.00561896, 0.4726897 ]])
           
  • 列1~列5 對應每一篇文檔屬于主題1~5的機率
  • 現在取一行最大就可以對應一篇文檔屬于哪個主題的可能性最大
def CalTopic(docs, i):
    # docs:訓練好的模型拟合原有資料之後 得到每一篇文檔對應到不同主題的機率
    # i表示文檔i
    topic_most_pr = docs[i].argmax()
    return topic_most_pr
           
df['topic'] = df.index.map(lambda x: CalTopic(docs, x))
df['topic'].value_counts()
           
4    7021
2    6744
0    4865
3    2042
1    1088
Name: topic, dtype: int64
           

注:上述産生的0~4主題可以和下圖對應主題對應上!

機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考

但是我們還得去做一件事情就是将這個主題和可視化中的圖對應上即可!

具體方法見下面

df['topic'] = df['topic'].map(str) 
dic_top = {'0':3, '1':5, '2':1, '3':4, '4': 2}
df['topic_pic'] = df['topic'].map(dic_top)
df['topic_pic'].value_counts()
           
2    7021
1    6744
3    4865
4    2042
5    1088
Name: topic_pic, dtype: int64
           

6.4.7 結果展示

是一個動态互動的網頁結果,但是還沒找到很好地方法直接分享連結。

現在分别把每一類主題圖檔展示:

(注:下圖為demo結果,是以樣本量可能和上述有差異)

  • 主題解釋:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考
    現在确定了主題,就可以實作一開始的兩個目的:
  • 一方面補充使用者的标簽次元
  • 一方面定位到不同主題的人群進行關聯的推送相應内容
  • 主題1:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考
  • 主題2:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考
  • 主題3:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考
  • 主題4:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考
  • 主題 5:
    機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考

6.4.8 資料探索

機器學習 | LDA主題模型1前言2 讀入資料3分詞處理4 提取特征5 思路1:使用兩種關鍵詞提取方法6 思路2:使用LDA主題模型7 參考

6.5 LDA模型的封裝

from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.externals import joblib  # 目的:儲存模型 也可以選擇pickle等儲存模型,請随意
from sklearn.decomposition import LatentDirichletAllocation
import pyLDAvis
import pyLDAvis.sklearn

def print_top_words(model, feature_names, n_top_words):
    # 函數功能:列印每個主題的前幾個關鍵詞
    for topic_idx, topic in enumerate(model.components_):
        print("Topic #%d:" % topic_idx)
        print(" ".join([feature_names[i]
                        for i in topic.argsort()[:-n_top_words - 1:-1]]))
    print()
    
def CalTopic(docs, i):
    # 函數作用:定位每篇文檔的主題
    # docs:訓練好的模型拟合原有資料之後 得到每一篇文檔對應到不同主題的機率
    # i表示文檔i
    topic_most_pr = docs[i].argmax()
    return topic_most_pr

def LdaModel(df, n_topics, iter_num):
    # df已經準備好的資料 有cut_label列
    # n_topics:主題數
    # iter_num:疊代次數
    
    # 1 資料準備
#     df = pd.read_excel('LDA模型資料準備.xlsx')

    # 2 停用詞導入及增加
    list_100 = list(range(100))
    list_100_str = list(map(lambda x: str(x), list_100))
    # 從檔案導入停用詞表
    stpwrdpath = "stop_words.txt"
    stpwrd_dic = open(stpwrdpath, encoding='gbk') # 加上編碼,不然就不會顯示中文了!
    stpwrd_content = stpwrd_dic.read()
    # print(stpwrd_content)
    #将停用詞表轉換為list 
    stpwrdlst = stpwrd_content.splitlines()
    # 在停用詞裡面再把年齡 也就是具體的數字全部去掉 是以停用詞加上0-100的數字!
    stpwrdlst = stpwrdlst + list_100_str # 先測試這一版 如果不對 就要把數字變成字元[修改了!]
    stpwrd_dic.close()
    
    # 3 關鍵字提取和向量轉換
    n_features = 1000
    tf_vectorizer = CountVectorizer(strip_accents = 'unicode',
                               max_features = n_features,
                               stop_words = stpwrdlst,
                               max_df = 0.5,
                               min_df = 10
                               )
    tf = tf_vectorizer.fit_transform(df['cut_label'])
    
    # 存儲上述tf 節省時間
#     joblib.dump(tf_vectorizer,tf_ModelPath )
    # #得到存儲的tf_vectorizer,節省預處理時間
#     tf_vectorizer = joblib.load(tf_ModelPath)
#     tf = tf_vectorizer.fit_transform(df['cut_label'])
    
    # 4 搭建LDA模型
    lda = LatentDirichletAllocation(n_components = n_topics,
                               max_iter=iter_num,
                                learning_method='online',
                               learning_offset=50,
                               random_state=23)
    
    # 5 拟合模型
    lda.fit(tf) # tf即為Document_word Sparse Matrix  
    
    # 6 檢視模型結果
    n_top_words = 10
    tf_feature_names = tf_vectorizer.get_feature_names()
    print_top_words(lda, tf_feature_names, n_top_words)
    
    # 7 可視化展示的資料準備
    pyLDAvis.enable_notebook()
    pyLDAvis.sklearn.prepare(lda, tf, tf_vectorizer)
    data = pyLDAvis.sklearn.prepare(lda, tf, tf_vectorizer)
    
    # 8 定位到每個人的主題
    docs = lda.fit_transform(tf)
    # 重新定義索引
    df.index = range(len(df))
    df['topic'] = df.index.map(lambda x: CalTopic(docs, x))
    
    return docs, data, df
           
docs_, data_, df_ = LdaModel(df, 5, 50) # 表示5個主題 疊代50次
pyLDAvis.show(data_)
           

後續可以按照上述方法定位到每一個人的主題 然後再去做進一步的資料探索

7 參考

  • https://zhuanlan.zhihu.com/p/31470216
  • http://www.voidcn.com/article/p-zxtbxynk-kn.html
  • https://www.zhihu.com/question/32286630
  • https://blog.csdn.net/v_july_v/article/details/41209515
  • https://www.cnblogs.com/pinard/p/6831308.html