返回文章列表

Jaccard相似度遊戲推薦系統實作

本文實作根據 Jaccard 相似度的遊戲推薦系統,利用 igraph 函式庫計算使用者間的相似度,並根據最相似使用者的遊戲紀錄推薦新遊戲。同時探討知識圖譜的建構與資料清理技巧,包含如何處理文字資料、設計圖譜架構以及使用 Python 進行資料前置處理。

推薦系統 圖演算法

在遊戲推薦系統中,利用使用者間的相似度來推測喜好已成為常見做法。本文介紹如何使用 Jaccard 相似度和 igraph 函式庫,開發一個根據圖演算法的遊戲推薦系統。系統的核心概念是計算使用者共同遊玩遊戲的比例,藉此找出與目標使用者最相似的其他使用者,並參考相似使用者的遊玩紀錄,推薦目標使用者尚未接觸過的遊戲。程式碼中,make_recommendations 函式接受圖、目標使用者與最低遊玩時數等引數,透過 prune_graph 函式移除遊玩時數過低的遊戲紀錄,確保推薦結果的準確性。接著,系統會計算目標使用者與其他所有使用者的 Jaccard 相似度,找出最相似使用者,並根據其遊玩紀錄產生推薦清單,過濾掉目標使用者已擁有的遊戲。

進階推薦系統:根據Jaccard相似度的遊戲推薦

為了更聰明地推薦遊戲,我們將實作一個根據節點相似度的解決方案。節點相似度比較兩個節點的鄰近節點並傳回一個分數。在我們的實作中,我們將使用igraph中的similarity_jaccard()函式來計算Jaccard相似度。該分數等於兩個節點的共同鄰居數量除以至少與兩個節點之一相鄰的鄰居數量。結果將是一個介於0和1之間的數字,其中0表示沒有相似度,1表示節點具有相同的鄰居。

在我們的案例中,Steam圖中的使用者節點與遊戲節點相鄰。因此,與其他使用者節點具有高Jaccard相似度的使用者節點表示他們玩了相似的遊戲。我們可以利用這些資訊為使用者做出新的推薦。現在,我們將逐步簡化這個過程:

定義推薦函式

首先,讓我們定義一個新的函式make_recommendations()。我們的函式將接受一個igraph圖g和一個特定的使用者user,並根據圖的邊緣權重指定遊戲需要被玩的最低小時數min_hours,以參與我們的推薦。這樣,當使用者購買了一個遊戲但只玩了很短的時間,我們可以假設他們不喜歡這個遊戲,並且不使用這些資訊來推薦新的遊戲:

def make_recommendations(g, user, min_hours):

內容解密:

  • make_recommendations函式接受三個引數:g(igraph圖)、user(特定使用者)和min_hours(最低遊戲時間)。
  • 這個函式的主要目的是根據使用者的遊戲歷史和其他使用者的相似度來推薦新的遊戲。

查詢使用者節點並修剪圖

  1. 首先,我們需要使用g.vs.select()查詢圖中internal_id等於user的節點,以找到由user引數表示的節點。這將傳回一個元素的列表,因此我們可以使用列表切片存取第一個也是唯一的元素,並找到該節點的index屬性,這將是igraph ID:
user_node = g.vs.select(internal_id_eq=user)[0].index

內容解密:

  • 使用g.vs.select()根據internal_id找到對應的使用者節點。
  • user_node變數儲存了該使用者節點在igraph中的ID。
  1. 然後,我們將使用另一個自定義方法prune_graph(),根據遊戲被玩的時數來刪除圖中的邊緣。我們將在定義完make_recommendations()的剩餘部分後再回來看這個方法:
g = prune_graph(g, min_hours)

內容解密:

  • prune_graph()函式根據指定的最低遊戲時間修剪圖,刪除不符合條件的遊戲記錄。
  • 修剪後的圖可以用於進行推薦。

計算Jaccard相似度並找出最相似使用者

現在,修改後的圖可以用於進行推薦。要比較其他使用者節點,我們需要找到型別等於’source’的節點,這代表了我們之前在mysql_to_graph()方法中定義的使用者。我們可以使用g.vs.select()找到’source’型別的節點。然後,我們需要設定一個列表的列表,其中包含要比較的節點對。我們希望將指定的使用者與圖中的所有其他使用者進行比較,因此我們將寫一個列表推導式來建立一個列表的列表,其中每個元素是所選使用者的igraph ID和另一個使用者的igraph ID的二元素列表。我們不想將所選使用者節點與其自身進行比較,因此我們將在推導式中使用條件!=來防止這種情況:

other_user_nodes = g.vs.select(type_eq='source')
pairs = [[user_node, other_user.index] for other_user in other_user_nodes if other_user.index != user_node]

內容解密:

  • other_user_nodes儲存了所有型別為’source’(即使用者)的節點。
  • pairs列表包含了所有需要比較的使用者對,其中每個元素是一個二元素列表,包含當前使用者和其他使用者的igraph ID。

現在,有了要比較的列表,我們可以執行igraph的similarity_jaccard()來為我們的節點對生成相似度分數。在這種情況下,由於我們的邊緣從使用者到遊戲是有向的,我們對“out”方向感興趣,這是在similarity_jaccard()mode引數中指定的。我們的相似度分數將作為列表傳回,因此需要將它們與相關的節點ID連結起來。我們可以使用列表推導式來實作這一點,將每個列表中的第二個元素(即被比較的使用者igraph ID)與分數透過zip()連結起來:

similarities = g.similarity_jaccard(pairs=pairs, mode='out')
node_similarity = [[pair[1], similarity] for pair, similarity in zip(pairs, similarities)]

內容解密:

  • similarities儲存了根據Jaccard相似度計算出的分數。
  • node_similarity列表將每個其他使用者的igraph ID與其與當前使用者的相似度分數對應起來。

排序並找出最相似使用者

我們希望找到與指定使用者最相似的使用者,因此需要按照降序對我們的node_similarity列表進行排序。我們可以使用Python的sorted()方法,並使用匿名函式按照node_similarity中每個列表的第二個元素進行排序。我們將指定reverse=True以降序排序列表。然後,我們可以透過存取排序後列表的第一個元素來取得最相似使用者的igraph ID:

node_similarity = sorted(node_similarity, key=lambda x: x[1], reverse=True)
most_similar_node = node_similarity[0][0]

內容解密:

  • node_similarity按照相似度分數降序排序。
  • most_similar_node儲存了最相似使用者的igraph ID。

取得遊戲推薦

有了最相似使用者的igraph ID,我們可以使用g.neighbors()查詢這個使用者玩過的遊戲,並透過查詢internal_id節點屬性來存取這些遊戲對應的Steam ID。我們也可以對原始指定的使用者做同樣的事情,以便我們有兩個遊戲列表,分別是相似使用者玩過的遊戲和原始使用者擁有的遊戲。最後,讓我們使用最後一個列表推導式來找出我們的game_recommendations列表中不在原始使用者的owned_games列表中的所有遊戲,並將這個列表作為新遊戲傳回:

game_recommendations = g.vs[g.neighbors(most_similar_node)]['internal_id']
owned_games = g.vs[g.neighbors(user_node)]['internal_id']
new_games = [game for game in game_recommendations if game not in owned_games]
return new_games

內容解密:

  • game_recommendations儲存了最相似使用者玩過的遊戲ID。
  • owned_games儲存了當前使用者擁有的遊戲ID。
  • new_games是為當前使用者推薦的新遊戲列表,透過排除已經擁有的遊戲得到。

知識圖譜的建構

本章節將進一步拓展您的知識,並介紹知識圖譜的概念。在學習知識圖譜的過程中,您將親手實踐如何清理資料以準備將其匯入圖譜。這將教您資料科學和圖譜建模中隱藏的一面,即您將花費大量時間清理資料並準備開始建模。此外,我們還將教您將資料匯入圖譜的最佳方法。在此之後,您就可以分析您的知識圖譜,並透過社群檢測技術進一步擴充套件它。

社群檢測通常用於發現網路中相似專案的群組或叢集。例如,這些方法可以用來發現社交媒體上圍繞某種敘事發帖的有影響力的群組,或者在我們將要使用的例子中,用於比較研究摘要時尋找相似的文獻。

技術需求

我們將使用 Jupyter Notebook 來執行我們的程式設計練習,這需要 python>=3.8.0。此外,您需要在您的環境中使用 pip install 命令安裝以下套件:

  • igraph==0.9.8
  • spacy==3.4.4
  • scispacy==0.5.1
  • matplotlib

或者,您可以在支援的 requirements.txt 檔案中執行 pip install –r requirements.txt 來安裝本章節的所有依賴套件。

知識圖譜簡介

在科學和醫學等複雜領域,特定主題上的資料和文獻數量之龐大難以估計。對於成熟的公司和行業來說,隨著時間的推移,以文字資訊形式存在的機構知識不斷積累,也變得過於龐大而難以有效傳播。在這兩種情況下,知識圖譜可能有助於緩解與過多分散資訊相關的問題。

知識圖譜的目標是以合理且可搜尋的方式將相關資訊、文字和檔案連結在一起。

在根據文字的知識圖譜中,圖中的連結通常代表相關的檔案或文章。文字處理和自然語言處理(NLP)本身就是巨大的領域,因此在本章節中,我們將保持與文字相關的方法簡單。當然,文字資料的品質對準備知識圖譜匯入資料有很大影響。我們將在這裡使用簡單的 .txt 檔案,但檔案(尤其是來自較舊來源的檔案)可能以影像、PDF 或類別似格式儲存,使得文字提取成為連線資料的一大挑戰。

清理資料以準備知識圖譜匯入

在匯入資料之前,我們需要對其進行清理。這包括但不限於移除不必要的字元、處理缺失值、以及進行必要的格式轉換。

import spacy

# 載入預訓練模型
nlp = spacy.load("en_core_sci_sm")

# 示例文字
text = "Your text here."

# 處理文字
doc = nlp(text)

# 提取實體
entities = [(ent.text, ent.label_) for ent in doc.ents]
print(entities)

將資料匯入知識圖譜

一旦資料被清理,我們就可以將其匯入知識圖譜。這涉及建立節點和邊,以代表實體及其關係。

import igraph as ig

# 建立一個空圖
g = ig.Graph(directed=True)

# 新增節點
g.add_vertices(2)

# 新增邊
g.add_edges([(0, 1)])

# 列印圖的摘要
print(g.summary())

知識圖譜分析與社群檢測

在建立知識圖譜後,我們可以對其進行分析,以發現有趣的模式和結構。社群檢測是一種用於發現網路中群組或叢集的方法。

# 社群檢測示例
communities = g.community_infomap()

# 列印社群
for community in communities:
    print(community)

知識圖譜的建構與資料清理

知識圖譜是一種用於表示實體之間關係的圖結構資料,通常與本體論(ontology)相關聯。本體論是一系列相互關聯的概念集合,這些概念往往圍繞著特定的主題。因此,本體論也可以被視為一種圖結構,其中概念是節點,而連結代表了這些概念之間的關係。

知識圖譜與本體論的關聯

本體論通常具有層次結構,其中一個廣泛的概念可能涵蓋多個較為具體的概念。例如,如果我們要建立一個哺乳動物的分類別法,我們可能會在哺乳動物和狗之間,以及哺乳動物和貓之間建立關係。進一步地,由於貓本身也是一個廣泛的概念,我們還可以在貓和老虎、貓和獵豹之間建立關係。

在構建知識圖譜時,我們可以使用本體論來定位檔案中的概念。當相似的概念出現在兩篇檔案中時,我們可能會將這兩篇檔案連結起來。根據所使用的文字處理技術的複雜程度,這可以簡單到匹配單詞,也可以透過使用自然語言處理(NLP)工具(如詞形還原)來輔助匹配。在知識圖譜建立方面,這可以被視為一種自上而下的方法——我們知道我們正在尋找哪些術語以及它們之間的關係,但我們不會發現任何新的概念。

程式碼例項:資料清理

import csv

# 開啟檔案並讀取資料
with open('CH04/data/20k_abstracts.txt') as c:
    reader = csv.reader(c, delimiter='\t')
    data = [line for line in reader if line != []]

# 初始化空列表和字串以儲存清理後的資料
clean_data = []
abstract = ''

# 遍歷資料並清理
for line in data[1:]:
    if len(line) == 1:
        clean_data.append(abstract.strip())  # 移除前導和尾隨空白
        abstract = ''
    else:
        abstract += ' ' + line[1]

# 將清理後的資料寫入新檔案
with open('CH04/data/20k_abstracts_clean.csv', 'w', newline='') as d:
    writer = csv.writer(d)
    for idx, abstract in enumerate(clean_data):
        writer.writerow([idx, abstract])  # 新增ID欄位

內容解密:

  1. 匯入csv模組:使用import csv匯入Python的內建csv處理模組,用於讀取和寫入CSV檔案。
  2. 開啟並讀取原始資料:使用open函式開啟20k_abstracts.txt檔案,並透過csv.reader以分號分隔的方式讀取資料,將非空行儲存到data列表中。
  3. 初始化變數:建立空列表clean_data用於儲存清理後的摘要文字,並初始化空字串abstract用於暫存當前摘要的文字。
  4. 遍歷資料並清理:透過遍歷data列表(從第二行開始),根據行的長度判斷是否為新摘要的開始。若行長度為1,表示遇到新的摘要ID,將之前累積的abstract加入clean_data並重置abstract。否則,將當前行的第二個元素(即摘要文字)追加到abstract中。
  5. 寫入清理後的資料:將清理後的摘要文字連同自動生成的ID一起寫入新的CSV檔案中,使用enumerate為每個摘要分配一個唯一的ID。

自下而上的本體論構建方法

與自上而下的方法相反,我們也可以利用檔案中的文字來建立知識圖譜中的本體論。這種方法被稱為自下而上的方法。在這種方法中,我們假設如果概念或術語在文字中共現(在同一位置出現),它們更有可能相關。例如,在一篇關於國際旅行的檔案中,您很可能會看到“機場”、“飛機”等術語,而不太可能看到與太空旅行相關的術語,如“火箭”或“月球”。利用這些想法,可以使用術語的共現來連結概念並開始構建一個基本的本體論。

知識圖譜的建構

在本章中,我們將結合自上而下和自下而上的方法來建立一個知識圖譜。我們將使用來自PubMed的公開研究論文摘要和一個醫學術語函式庫。透過使用摘要文字來連結共現的術語,可以為這兩者增加價值。接下來,我們將討論如何清理資料以準備建立知識圖譜。

知識圖譜建構

在完成資料清理後,我們可以開始設計圖譜匯入的方法。這是辛苦工作的成果展現。正如人們所說,資料科學家的工作從清理第一個資料集開始。現在,讓我們準備將資料匯入知識圖譜。

將資料匯入知識圖譜

在直接建立知識圖譜之前,有很多事情需要考慮。和前面的章節一樣,我們需要先考慮要產生的圖譜結構。然後,我們需要處理摘要以擷取感興趣的術語。接著,一旦我們有了術語,就可以建立一個邊緣列表,以便匯入到 igraph 中。

將資料正確匯入知識圖譜至關重要,這取決於如何概念性和實際地設計圖譜架構。下面的部分將展示如何設計架構,以確保知識圖譜按照預期運作。

設計知識圖譜架構

在直接進行資料匯入之前,我們必須考慮知識圖譜的結構。對於我們的案例,我們感興趣的是連線相關的檔案和概念。

就節點而言,我們同時擁有摘要和術語。我們的摘要只有 ID 和文字。我們的術語需要類別似的處理,儘管文字較短。因此,我們的節點屬性很簡單,每個節點只需要兩個不同的屬性。

重要注意事項

值得注意的是,我們的摘要通常是非常大的文字欄位,超過 1,000 個字元。在本章中,由於整體圖譜的規模相對較小,將摘要文字新增為節點屬性並不會成為問題。然而,在擁有數百萬個節點的大型圖譜中,如此龐大的屬性可能會大幅增加圖譜在記憶體中的大小,並影響效能。在非常大的圖譜中,通常會在節點上儲存代表屬性的 ID,而將文字資料儲存在單獨的、根據鍵值的非圖資料函式庫中,例如 SQL。存取文字本身,而不是查詢圖譜結構,可以由單獨的高效能程式處理。

圖譜異質性

我們的假設圖譜中存在兩種型別的實體,即摘要和術語,這意味著我們的圖譜是異質的(包含多種型別的物件或多種型別的連結)。當我們在摘要中找到術語時,將它們連結到它們所在的摘要是有意義的。這種單一型別的關係足以建立一個基本的知識圖譜,其中實體存在於摘要中。

當唯一的連結型別是在兩種不同型別的節點之間時,這被稱為二部圖。我們也可能對特定摘要中術語的頻率感興趣,以表示摘要與概念之間的關聯強度。這可以用於在後續分析中過濾圖譜,類別似於第 3 章「資料模型轉換 – 關聯式到圖資料函式庫」中的做法。

下面的圖表展示了一個二部知識圖譜關係的例子,其中術語和摘要之間存在 FOUND_IN 邊緣。本質上,這說明瞭哪些術語出現在哪些摘要中:

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333

title 圖譜異質性

rectangle "圖譜異質性" as n1
rectangle "實作" as n2
rectangle "應用" as n3

n1 --> n2
n2 --> n3

@enduml

此圖示說明瞭二部知識圖譜關係,其中術語和摘要之間存在 FOUND_IN 邊緣。

前面的圖表顯示,我們可以在每個術語和摘要之間建立關係,並設定 FOUND_IN 屬性。因此,我們本質上是在說「給我一個出現在摘要中的術語」,並使用一個方向性的邊緣。

內容解密:

  1. 圖表展示了二部知識圖譜的基本結構。
  2. 術語和摘要之間的 FOUND_IN 關係代表了術語出現在摘要中的事實。
  3. 這種結構可以用於後續的分析和查詢。

在開始匯入資料之前,設計一個清晰合理的知識圖譜架構是非常重要的。這不僅有助於正確地組織資料,也能夠提高後續查詢和分析的效率。對於複雜的大型圖譜,還需要考慮到效能和儲存的問題。採用適當的設計和技術,可以有效地構建和管理知識圖譜,以支援各種應用和分析需求。

程式碼範例

import csv

# 開啟檔案
with open('clean_abstracts.csv', 'w', newline='') as d:
    writer = csv.writer(d)
    for i, line in enumerate(clean_data):
        writer.writerow([i, line])

內容解密:

  1. 程式碼使用 csv 模組將清理後的摘要寫入 CSV 檔案。
  2. enumerate 用於為每個摘要分配一個唯一的 ID。
  3. writerow 方法用於將 ID 和對應的摘要寫入 CSV 檔案。
  4. 這樣可以確保每個摘要都有一個唯一的識別符號,以便於後續處理和分析。