圖資料庫實戰入門 —— 一個簡單的電影推薦系統實現
來自專欄圖資料庫雜談11 人贊了文章
簡介
日常生活中有各種各樣的推薦系統,比如電子購物網站根據用戶歷史消費習慣和瀏覽記錄進行商品推薦,以及在線視頻網站根據用戶歷史觀看記錄進行視頻推薦。今天,我們就來使用TigerGraph圖資料庫技術設計一個簡單的電影推薦系統。
準備工作
請在這裡下載並安裝TigerGraph終身免費的開發者版本(Developer Edition):https://www.tigergraph.com/download/
註冊用戶後會收到一封包含下載鏈接和安裝步驟的郵件。安裝過程大概需要15分鐘。
由於今天使用的數據集規模對於TigerGraph系統來說實在是小case,個人筆記本電腦的虛擬機就可以輕鬆駕馭。我們將TigerGraph系統安裝在MacBook Pro搭載的虛擬機中。MacBook Pro配置為:CPU 2.5 GHz Intel Core i7, 內存16GB, 硬碟500GB Flash Storage。VMWare Fusion虛擬機配置為CPU 4 processor cores,10GB內存,100GB硬碟,操作系統為xubuntu 18.04。
我們使用MovieLens 20M數據集,該數據集相關工作請參見引用資料[1]。該數據集包含了138,000位用戶針對27,000部電影的2000萬條評分記錄。數據集的下載地址為:https://grouplens.org/datasets/movielens/20m/
解壓縮ml-20m.zip壓縮包後,我們來熟悉一下該數據集的數據格式。
movies.csv:
該文件總共有27,279行,除第1行是表頭外,每行用3列表示一部電影,分別為電影id(movieId)、電影名稱(title)和電影類型(genres)。需要注意的是該csv文件用逗號分隔不同的列,而為了處理電影名稱中包含的逗號,使用雙引號(")轉義title列。
ratings.csv:
該文件總共有20,000,264行,除第1行是表頭外,每行用4列表示一位用戶對一部電影的評分,分別為用戶id(userId)、電影id(movieId)、評分(rating)和評分時間(timestamp)。這裡的評分時間是用unix時間戳表示的。
在這個數據集中並沒有提供用戶的個人信息,可能是出於保護用戶隱私的考慮。因此在後面的可視化展示中我們看到的用戶數據都是被一個數字表示的。
其他幾個文件提供了電影標籤等信息,這在我們今天的簡單電影推薦演算法中不會被使用。如果你有興趣設計更加複雜的推薦演算法,可以嘗試將它們引入到推薦模型中獲得更好的推薦效果。
創建圖模型
圖模型由若干節點類型(vertex type)和若干邊類型(edge type)組成。可以指定邊類型的源節點類型(source vertex type)和目標節點類型(target vertex type)。圖模型是對現實世界的問題的一種直觀的抽象。
我們很容易建立電影推薦問題的模型,模型中有兩種節點類型:人(person)和電影(movie),以及一種邊類型:打分(rate)。rate的源節點類型為person,目標節點類型為movie。
我們使用GUI集成開發工具GraphStudio創建圖模型。打開瀏覽器,在地址欄輸入安裝TigerGraph機器的IP+14240埠訪問GraphStudio,載入完成後點擊左側導航欄的Design Schema項進入創建圖模型頁面:
單擊下圖中黃色箭頭所指的工具欄中的按鈕即可添加節點類型,在彈出的窗口中設置節點類型名稱、主鍵(primary id)名稱和類型、屬性(attribute)名稱和類型,並根據語義選擇節點類型的顏色和圖標。我們首先添加person節點類型:
然後添加movie節點類型:
添加完畢後可以通過滑鼠拖動調整節點類型的位置:
單擊下圖第一步中黃色箭頭所指的工具欄中的按鈕進入添加邊類型模式,然後如第二步點擊源節點類型,然後如第三步點擊目標節點類型。
在彈出的窗口中設置邊類型名稱、邊類型的有向性(directed)、屬性(attribute)名稱和類型,並可以選擇邊類型的顏色。我們輸入rate邊類型的信息:
至此,我們完成了圖模型的創建。可以用滑鼠滾輪縮放圖模型,也可以用滑鼠按住工作面板的空白處拖動整個圖模型。點擊工具欄中的發布按鈕將圖模型發布到TigerGraph系統中。整個發布大概需要2分鐘。
創建數據映射
數據映射(data mapping)指建立數據模型之間的元素的對應關係。在電影推薦的這個實例中,我們需要建立從csv文件代表的數據模型到圖模型之間的對應關係。
這裡需要弄清楚模型和元素之間的關係,這種關係類似於面向對象程序設計中類(class)與實例(instance)之間的關係。我們剛剛創建的圖模型描述了這些類之間的關係,而我們接下來要向圖中載入的數據(元素)則是具體的每一個人、每一部電影和每條某人對某電影的打分。
在由movies.csv文件和ratings.csv文件組成的模型中,文件表頭的語義代表了該模型的結構。movies.csv文件除表頭以外的每行數據代表了一個電影元素,我們需要將它映射到圖模型中的電影元素。ratings.csv文件除表頭以外的每行數據包含了一個(可能重複出現的)人元素和一個打分元素,我們需要將它映射到圖模型的人元素和打分元素。
點擊左側導航欄的Map Data To Graph項進入創建數據映射頁面:
我們需要將數據文件上傳到TigerGraph後台。這裡有兩種方式:對於小於500MB的文件,可以直接通過GUI上傳。點擊下圖中黃色箭頭1所指的工具欄添加數據源按鈕,在彈出的窗口中點擊黃色箭頭2所指的上傳文件按鈕,選擇本機解壓縮後的ml-20m數據集中的movies.csv文件上傳。上傳完成後在文件列表中會顯示該文件:
然後我們將該數據源添加到工作面板上。在Files on server列表中點擊movies.csv文件,GraphStudio後台用演算法智能分析數據並推斷出文件的分隔符(delimiter)、換行符(end of line)和是否有表頭(has header)。GraphStudio的這個推斷是儘力而為的,如果不準確,用戶可以自由修改這些設置,文件會馬上被重新分行分列。需要注意的是GraphStudio不對轉義字元(escape character)做推斷。回顧前文我們提到movies.csv對於title列數據有逗號的情況採用雙引號轉義,所以我們需要在轉義字元下拉列表中選擇雙引號("):
點擊添加之後,movies.csv作為一個數據源被添加到工作面板上,表示為一個文件圖標。用戶可以按住這個圖標拖動到任何想要的位置:
下面我們添加ratings.csv文件。出於保護系統資源的考慮,GraphStudio限制通過GUI上傳單個文件不能超過500MB,而ratings.csv剛好超過了這個限制,因此我們通過scp命令(或者任意其他方式)直接將該文件傳到虛擬機的
/home/tigergraph/tigergraph/loadingData/
文件夾內(如果你在安裝TigerGraph過程中沒有使用默認的用戶名或安裝路徑,則請將文件上傳到相應的路徑)。
再次點擊工具欄添加數據源按鈕,在彈出的窗口中選擇ratings.csv文件添加到工作面板:
好了,我們已經添加了所需的數據源,接下來創建數據源到圖模型之間的數據映射。
首先我們將movies.csv映射到movie節點類型。點擊工具欄中的映射數據到圖模型按鈕,然後點擊數據源(movies.csv)圖標,然後點擊目標節點類型(movie)。這時候一條數據映射關係就被創建了:
接下來,我們需要填充映射關係的內容,即數據源中的數據如何映射到目標節點(或邊)的屬性。在右側工作面板中,表示數據源movies.csv的表和表示movie節點類型的表已經靜靜地等在那裡了。建立映射的方式非常直觀,先點擊數據源表中的某一行(對應於csv文件中的某一列,這種旋轉90度的表達方式是ETL中普遍採用的可視化方式),再點擊節點類型屬性表中的某一行(對應節點的主鍵或某個屬性),就完成了一個屬性映射。這裡我們建立了三條屬性映射,你可能注意到原來顯示在左側工作面板該數據映射上面的錯誤信息消失了,這是因為你創建了對於movie節點類型的主鍵的映射。對於節點來說,主鍵映射是必須的。而屬性可以不被映射,在這種情況下當數據載入時這些未被映射的屬性會使用默認值。
最後,我們建立ratings.csv到rate邊類型的數據映射。重複與上面類似的操作,最終的映射結果為:
至此,我們完成了數據映射的創建。需要注意的是我們並沒有映射從ratings.csv到person節點類型的映射,這是因為在ratings.csv映射到rate邊類型的時候我們建立了userId列到rate邊的源節點類型person的屬性映射,這會在後續的數據載入中自動創建以ratings.csv中該列數據值為主鍵的person類型的節點。在TigerGraph系統中所有載入到相同類型的具有相同主鍵的節點會被合併為一個節點,默認的合併規則為屬性覆蓋。
最後,點擊左上角的發布按鈕將數據映射發布到TigerGraph系統。發布所需時間和數據映射的個數相關,這裡大概需要6秒:
載入數據
接下來我們讓TigerGraph系統根據我們創建的數據映射載入數據。點擊左側導航欄的Load Data項進入載入數據頁面,點擊工具欄中的開始載入按鈕:
載入整個數據集耗時僅2分鐘,這僅僅是在個人蘋果筆記本的虛擬機上就能達到的載入速度!載入完畢後我們通過右側的統計信息看到總共載入了16.5萬個節點和4000萬條邊。之前我們說總共有2000萬條打分記錄,而每條記錄在TigerGraph圖資料庫中被載入到一條rate邊和一條reverse_rate反向邊,因此總共有4000萬條邊。
瀏覽圖數據
下面我們利用GraphStudio內置的一些圖數據瀏覽功能直觀的感受一下剛剛載入的數據。點擊左側導航欄的Explore Graph項進入瀏覽圖數據頁面:
首先我們點擊拾取節點(Pick Vertices)按鈕從圖數據中拾取5個person節點和5個movie節點。這裡的拾取不是隨機的,因此每次拾取會返回相同的結果。如果你想要更多的節點,可以修改Enter a number中的數字。這裡最大可以輸入500。如果你知道節點的主鍵,可以在Enter vertex id輸入框中輸入主鍵的值,然後點擊旁邊的Search按鈕拾取那個節點。配置(Configuration)可以控制拾取節點的類型範圍,默認是從全部類型中拾取。你也可以勾選取消一些類型。
默認情況下所有節點顯示的標籤都是它們的主鍵。你可以修改設置顯示其他屬性,我們設置movie類型的節點顯示它們的title屬性:
完成修改之後,可以看到工作面板中的movie節點的標題被顯示出來了,可視化變得更加直觀。
切換到黑色的縱嚮導航欄第三個最短路徑項。點擊選擇起始節點(Choose starting vertex)輸入框,再隨意點擊工作面板中的一個節點。再點擊選擇目標節點(Choose destination vertex)輸入框,再隨意選擇工作面板中的另一個節點。
點擊查找路徑(Find Paths)按鈕,TigerGraph瞬間找到了兩點之間的一條最短路徑,長度為4。請嘗試修改設置將打分顯示在rate邊的標籤上~
你可以繼續嘗試瀏覽圖數據頁面的其他功能。相信你會通過一些操作發現這是一個非常密集的圖(點與點之間有極多的連接路徑)。
好了,現在你已經建立了圖模型並成功載入了ml-20m數據集,並且通過瀏覽圖數據直觀的感受了person節點和movie節點是如何通過rate邊互相連接的。你可能會驚奇的發現:哇塞,我居然沒有寫一行代碼!
電影推薦演算法實現
下面就是最激動人心的部分了,你將很快用GSQL查詢語言實現一個電影推薦的演算法。
我們將實現引用資料[2]中的電影推薦演算法。首先簡單的介紹一下這個演算法。
演算法的輸入為需要被推薦電影的person節點p,以及兩個整數參數k1,k2。
演算法的輸出為一個movie節點集合,表示被推薦的電影,最多會推薦k2部電影。每個電影對應一個浮點數的推薦係數,係數越大則推薦越強烈。
演算法的過程為:首先找到p打過分的所有電影集合m。然後找到所有對集合m中的電影打過分的p之外的人,計算這些人與p的看電影品味的相似度(餘弦相似度),從中選出最相似的k1個人。找到這k1個人打過分的電影中p沒有打過分的電影集合m2,計算這k1個人對m2中的電影打分的平均值,選擇其中平均值最高的k2部電影推薦給p,將平均分作為推薦係數。
這裡涉及到餘弦相似度的概念。兩個n維向量A和B的餘弦相似度為:
在電影推薦的例子中,給定兩個人p1和p2,他們的品味相似度可以由兩人共同打過分的電影作為向量空間,由二人對這些電影的打分作為向量,通過計算兩個向量的餘弦相似度得到。由於所有的打分都在0.5到5之間(0.5分為一檔),計算出來的相似度是一個(0, 1]的浮點數。
點擊左側導航欄的Write Queries項進入編寫查詢頁面:
點擊添加查詢按鈕,在彈出的窗口中輸入查詢名稱:RecommendMovie。
GraphStudio為我們生成了查詢的模板。我們根據上文演算法描述將查詢的參數寫為:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ...}
它表示我們要通過與p品味最相似的k1個人為p推薦k2部電影。
在GSQL中,圖運算的基本單元為從一個節點集合開始,經過由這些點、它們的出邊(outgoing edges)和出邊的終端節點(target vertex)集合所構成的所有點-邊-點的三元組集合上進行計算,並輸出一個點集(可以是起始節點集或終端節點集)。在這個過程中可以在運行時在節點上綁定重載過=和+=運算符的累加器(accumulator)記錄對三元組在起始節點集或終端節點集上進行reduce操作得到的結果。以上這段描述比較抽象,下面的例子中我們會進一步解釋。
我們構建一個只包含p節點一個元素的節點集PSet:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { PSet = { p };}
然後,我們用下面的基本圖計算單元找到p打過分的所有電影集合,並將其命名為PRatedMovies。我們可以看到這個基本圖計算單元的輸出是終端節點集m。這裡的別名(r和m)是局部有效的,因此在其他基本圖計算單元中可以被重用。
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { PSet = { p }; PRatedMovies = SELECT m FROM PSet -(rate:r)-> movie:m;}
我們希望在在PRatedMovies上打一個布爾的標記,表示p已經為這個電影打過分了,所以之後就不再推薦這個電影了。我們可以定義一個或累加器(OrAccum)命名為@rated,並在計算單元中的ACCUM子句中在movie節點上打一個true的標記:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { OrAccum @rated; PSet = { p }; PRatedMovies = SELECT m FROM PSet -(rate:r)-> movie:m ACCUM m.@rated = true;}
接下來我們從PRatedMovies集合出發通過打分邊的反向邊(reverse_rate)找到所有對p打過分的電影打過分的p以外的人,並命名為PeopleRatedSameMovies:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... PeopleRatedSameMovies = SELECT tgt FROM PRatedMovies:m -(reverse_rate:r)-> person:tgt WHERE tgt != p;}
下面我們計算PeopleRatedSameMovies集合中的每個person節點和p的餘弦相似度。計算髮生在第二個基本圖計算單元里,需要知道p對每部電影的打分,所以我們修改第一個基本圖計算單元,將p對電影的打分傳遞到p打過分的電影上的一個叫@ratingByP的求和累加器里:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { OrAccum @rated; SumAccum<double> @ratingByP; PSet = { p }; PRatedMovies = SELECT m FROM PSet -(rate:r)-> movie:m ACCUM m.@rated = true, m.@ratingByP = r.rating; ...}
我們要計算p和PeopleRatedSameMovies集合中每一個人q的餘弦相似度,回顧上文的餘弦相似度公式,打分向量空間為p和q共同打過分的電影,p對這些電影的打分向量為A,q的打分向量為B,那麼我們可以定義4個新的求和累加器(SumAccum):@lengthA, @lengthB, @dotProductAB和@cosineSimilarity,並且在第二個基本圖計算單元的ACCUM子句中計算@lengthA^2, @lengthB^2和@dotProductAB:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... SumAccum<double> @lengthA, @lengthB, @dotProductAB; SumAccum<double> @cosineSimilarity; ... PeopleRatedSameMovies = SELECT tgt FROM PRatedMovies:m -(reverse_rate:r)-> person:tgt WHERE tgt != p ACCUM tgt.@lengthA += m.@ratingByP * m.@ratingByP, tgt.@lengthB += r.rating * r.rating, tgt.@dotProductAB += m.@ratingByP * r.rating;}
然後,我們在POST-ACCUM子句中完成@cosineSimilarity的計算。由於POST-ACCUM是在節點集合三元組所有的累加操作reduce完成之後,這時候tgt上面已經擁有了最終的@lengthA^2,@lengthB^2和@dotProductAB的值了:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... PeopleRatedSameMovies = ... POST-ACCUM tgt.@cosineSimilarity = tgt.@dotProductAB / sqrt(tgt.@lengthA) / sqrt(tgt.@lengthB);}
我們通過ORDER BY子句和LIMIT子句選擇與p品味最相似的k1個人:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... PeopleRatedSameMovies = ... ORDER BY tgt.@cosineSimilarity DESC LIMIT k1;}
接下來,我們從PeopleRatedSameMovies集合出發,找到這些人打過分並且p沒有打過分的電影集合:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... RecommendedMovies = SELECT m FROM PeopleRatedSameMovies -(rate:r)-> movie:m WHERE m.@rated == false;}
我們添加一個平均值累加器(AvgAccum)名為@recommendScore用來累計這k1個人對這些電影的平均打分,並且根據打分從高到低排序,並選出其中得分最高的k2部電影作為推薦結果:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... AvgAccum @recommendScore; ... RecommendedMovies = SELECT m FROM PeopleRatedSameMovies -(rate:r)-> movie:m WHERE m.@rated == false ACCUM m.@recommendScore += r.rating ORDER BY m.@recommendScore DESC LIMIT k2;}
最後,我們輸出RecommendedMovies的title和@recommendScore作為結果:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { ... PRINT RecommendedMovies.title, RecommendedMovies.@recommendScore;}
耶!我們完成了整個查詢!來看一下完整的查詢吧:
CREATE QUERY RecommendMovie( vertex<person> p, int k1, int k2) FOR GRAPH MyGraph { OrAccum @rated; SumAccum<double> @ratingByP; SumAccum<double> @lengthA, @lengthB, @dotProductAB; SumAccum<double> @cosineSimilarity; AvgAccum @recommendScore; PSet = { p }; PRatedMovies = SELECT m FROM PSet -(rate:r)-> movie:m ACCUM m.@rated = true, m.@ratingByP = r.rating; PeopleRatedSameMovies = SELECT tgt FROM PRatedMovies:m -(reverse_rate:r)-> person:tgt WHERE tgt != p ACCUM tgt.@lengthA += m.@ratingByP * m.@ratingByP, tgt.@lengthB += r.rating * r.rating, tgt.@dotProductAB += m.@ratingByP * r.rating POST-ACCUM tgt.@cosineSimilarity = tgt.@dotProductAB / sqrt(tgt.@lengthA) / sqrt(tgt.@lengthB) ORDER BY tgt.@cosineSimilarity DESC LIMIT k1; RecommendedMovies = SELECT m FROM PeopleRatedSameMovies -(rate:r)-> movie:m WHERE m.@rated == false ACCUM m.@recommendScore += r.rating ORDER BY m.@recommendScore DESC LIMIT k2; PRINT RecommendedMovies.title, RecommendedMovies.@recommendScore;}
好了,我們點擊工具欄上的保存按鈕保存查詢,然後點擊安裝查詢按鈕安裝查詢。整個安裝大約需要一分鐘:
安裝成功後,點擊工具欄上的運行查詢按鈕,由於這個查詢需要輸入參數,參數面板會彈出。輸入一個人的id(比如238),給定k1(比如50)和k2(比如20),然後點擊參數面板下方的運行查詢按鈕,很快結果面板中顯示了推薦的電影:
點擊結果面板左側的顯示JSON項,可以查看JSON格式的查詢返回結果:
厲害了,你已經搭建完成了一個簡單的電影推薦系統!接下來,你可以通過TigerGraph系統提供的RESTFul介面把這個推薦系統和你的前端的應用直接進行集成。
TigerGraph的默認RESTFul服務埠為9000,安裝的GSQL查詢全部可以通過http GET的方式訪問,只要在URL中加入查詢的參數。下面這個curl命令就是一個調用的例子(將MACHINE-IP替換為你的安裝TigerGraph系統的機器的IP)
curl http://<MACHINE-IP>:9000/query/MyGraph/RecommendMovie?p=238&k1=50&k2=20
總結
本文介紹了如何使用TigerGraph圖資料庫快速搭建一個簡單的電影推薦系統。由於篇幅限制,這裡只涉及到了部分TigerGraph圖資料庫的功能,很多核心功能如實時數據更新以及GSQL強大而全面的語法都沒有涉及,GraphStudio中很多更加酷炫的功能也沒有演示。至於而本文採用的推薦演算法也是非常樸素的。如果這引起了你的興趣,歡迎閱讀TigerGraph開發文檔,以及拓展閱讀中的其他資料!
企業版的TigerGraph圖資料庫支持分散式、在線圖模式變更(online schema change)、多圖(mutiple graphs)以及眾多企業級功能。關於企業版與開發者版本的區別,詳見這裡:Download and Compare TigerGraph Editions, Experience the Real-Time Graph Database First Hand
引用資料:
- F. Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4, Article 19 (December 2015), 19 pages. DOI=<The MovieLens Datasets>
- 字鳳芹, 牛進, 畢柱蘭, 沈加敏. 基於圖資料庫的電影推薦系統設計. 軟體導刊. 2016(1):144-6.
拓展閱讀:
- 《一起學圖資料庫》之一:圖資料庫介紹
- 性能炸裂!圖資料庫TigerGraph開發者版本入門
- TigerGraph GSQL 入門
- TigerGraph開發文檔
- TigerGraph基準測試報告(英文原版)
推薦閱讀:
※跨域社交推薦:如何透過用戶社交信息「猜你喜歡」?
※演算法相關的文章整理
※推01,你們是不是都感覺自己少了個推薦系統?
※推薦系統:Next Basket Recommendation
※Recommend System : Video Suggestion and Discovery for YouTube Taking Random Walks Through the View