《推薦系統實踐》樣章:如何利用使用者標籤資料(二)

王軍花發表於2011-12-23

基於標籤的推薦系統

使用者用標籤來描述自己對物品的看法,因此,標籤成為了聯絡使用者和物品的紐帶。因此,標籤資料是反應使用者興趣的重要資料來源,而如何利用使用者的標籤資料來提高使用者個性化推薦結果的質量,是推薦系統研究的重要問題。

在如何利用標籤資料的問題上,豆瓣無疑是這方面的代表。豆瓣將標籤系統融入到他們的整個產品線中。下面以豆瓣讀書為例進行介紹。首先,在每本書的頁面上,都提供了一個叫做“豆瓣成員常用標籤”的應用,它給出了這本書上使用者最常打的標籤。同時,在使用者希望給書做評價時,豆瓣也會讓使用者給圖書打標籤。最後,在最終的個性化推薦結果裡,豆瓣利用標籤將使用者的推薦結果做了聚類,顯示了不同標籤下使用者的推薦結果,從而增加了推薦的多樣性和可解釋性。

一個使用者標籤行為的資料集一般由一個三元組的集合表示,其中記錄(u, i, b) 表示使用者u給物品i打上了標籤b。當然,使用者的真實的標籤行為資料遠遠比三元組表示的要複雜,比如使用者標籤的時間、使用者的屬性資料、物品的屬性資料等。但是,在本章中,為了集中討論標籤資料,我們只考慮上面定義的三元組形式的資料,即使用者的每一次標籤行為都用一個三元組(使用者,物品,標籤)來表示。

在下面的各節中,我們將利用Delicious的資料集,討論如何利用使用者標籤資料進行個性化推薦的各種演算法。

實驗設定


我們將Delicious的資料集按照9:1隨機分成訓練集R和測試集T。這裡分割的鍵值是使用者和物品,不包括標籤,也就是說,使用者對物品的多個標籤記錄要麼都被分進訓練集,要麼都被分進測試集。

在分完資料集後,我們將通過學習訓練集中的使用者標籤資料,來預測測試集上使用者會給什麼物品打標籤。對於使用者u,令R(u)是給使用者u的長度為N的推薦列表,裡面包含著我們認為使用者會打標籤的物品。而另T(u)是測試集中使用者u實際上打過標籤的物品集合。然後,我們利用準確率/召回率(Precision/Recall)來評測個性化推薦演算法的準確率:

enter image description here

如果用Python實現上面的兩個指標,我們可以通過如下的程式碼:

def PrecisionRecall(test_data, recommendations, N):
   hit = 0
   n_recall = 0
   n_precision = 0
   for user,ru in recommendations.items():
       tu = test_data[user]
       for item in sorted(ru.items(), key=itemgetter(1), reverse=True)[0:N]:
           if item in tu:
               hit += 1
           n_precision += 1
       n_recall += len(tu)
   recall = hit / (n_recall * 1.0)
   precision = hit / (n_precision * 1.0)
   return list(recall, precision)

同時,為了全面評測個性化推薦的效能,我們同時評測了推薦結果的覆蓋度(Coverage)、多樣性(Diversity)和新穎度。

覆蓋度我們通過如下的公式和程式碼計算:

enter image description here

def Coverage(train_data, test_data, recommendations, N):
   total_items = set()
   recommend_items = set()
   for user, items in train_data.items():
       for item in items:
           total_items.add(item)
   for user, ru in recommendations.items():
       for item, weight in sorted(ru.items(), key=itemgetter(1), reverse=True)[0:N]:
           recommend_items.add(item)
   return (len(recommend_items) * 1.0) / (len(total_items) * 1.0);

關於多樣性,我們在第1章中討論過,多樣性的定義取決於相似度的定義。在本章中,我們用物品的標籤向量的餘弦相似度來度量物品之間的相似度。對於每個物品i,item_tags[i]儲存了物品i的標籤向量,其中item_tags[i][b]是物品i上標籤b被打的次數,那麼物品i和j的餘弦相似度可以通過如下的程式計算:

def CosineSim(item_tags, i, j):
   ret = 0
   for b,wib in item_tags[i].items():
       if b in item_tags[j]:
           ret += wib * item_tags[j][b]
   ni = 0
   nj = 0
   for b, w in item_tags[i].items():
       ni += w * w
   for b, w in item_tags[j].items():
       nj += w * w
   if ret == 0:
       return 0
   return ret / math.sqrt(ni * nj)

在得到物品之間的相似度度量後,我們通過如下的公式來計算一個推薦列表的多樣性:

enter image description here

如果用程式實現,程式碼如下:

def Diversity(item_tags, recommend_items):
   ret = 0
   n = 0
   for i in recommend_items.keys():
       for j in recommend_items.keys():
           if i == j:
               continue
           ret += CosineSim(item_tags, i, j)
           n += 1
   return ret / (n * 1.0)

而推薦系統的多樣性定義為所有使用者的推薦列表的多樣性的平均值。

至於推薦結果的新穎性,這裡我們簡單地用推薦結果的平均熱門程度(AveragePopularity)來度量。對於物品i,定義它的熱門度item_pop(i)為給這個物品打過標籤的使用者數。而對推薦系統,我們定義它的新穎度如下:

enter image description here

如果用程式實現,程式碼如下:

def AveragePopularity(item_pop, recommend_results):
   ret = 0
   n = 0
   for u, recommend_items in recommend_results.items():
       for item in recommend_items.keys():
           ret += math.log(1 + item_pop [item])
           n += 1
   return ret / (n * 1.0)

一個最簡單的演算法


當拿到了使用者標籤行為資料時,大家都可以想到一個最簡單的演算法來給使用者推薦個性化的物品。這個演算法的描述如下所示。

  • 統計每個使用者最常用的標籤。

  • 對於每個標籤,統計被打過這個標籤的次數最多的物品。

  • 對於一個使用者,首先找到他常用的標籤,然後對於這些常用標籤,找到具有這些標籤的最熱門的物品,推薦給這個使用者。

如果用公式描述上面的演算法,那麼使用者u對物品i的興趣可以用如下的公式度量:

enter image description here

這裡,B(u)是使用者u打過的標籤集合,B(i)是物品i被打過的標籤集合, enter image description here是使用者u打過標籤b的次數,n_(b,i) 是物品i被打過標籤b的次數。本章用SimpleTagBased來標記這個演算法。

在Python中,我們用 records 來儲存標籤資料的三元組,其中

records[i] = [user, item, tag]

用 user_tags 來儲存 enter image description here,其中user_tags[u][b] = enter image description here

用 tag_items來儲存enter image description here,其中tag_items[b][i] = enter image description here

如下的程式可以從records統計出user_tags和tag_items:

def InitStat(records):
   user_tags = dict()
   tag_items = dict()
   for user, item, tag in records.items():
       if user not in user_tags:
           user_tags[user] = dict()
       if tag not in user_tags[user]:
           user_tags[user][tag] = 1
       else:
           user_tags[user][tag] += 1

       if tag not in tag_items:
           tag_items[tag] = dict()
       if item not in tag_items[tag]:
           tag_items[tag][item] = 1
       else:
           tag_items[tag][item] += 1

統計出user_tags和tag_items之後,可以通過如下程式對使用者進行個性化推薦:

def Recommend(user):
   recommend_items = dict()
   for tag, wut in user_tags[user].items():
       for item, wti in tag_items[tag].items():
           if item not in recommend_items:
               recommend_items[item] = wut * wti
           else:
               recommend_items[item] += wut * wti
   return recommend_items

我們在Delicious資料集上對上面的演算法進行評測,結果如表2所示。

表2 簡單的基於標籤的推薦演算法在Delicious資料集上的評測結果

enter image description here

演算法的改進


我們再來回顧一下上面提出的簡單演算法,該演算法通過如下公式預測使用者u對物品i的興趣:

enter image description here

仔細研究上面的公式,可以發現上面的公式有很多缺點。下面我們將逐條分析該演算法的缺點,並提出改進意見。

歸一化

如果我們從概率論的角度出發,認為使用者u喜歡物品i的概率取決於u曾經打過的標籤,那麼我們會得到如下的概率公式:

enter image description here

這個公式和SimpleTagBased演算法的公式相比,對引數做了歸一化,而且他的解釋也是從概率的角度出發,更加明確,本章用NormTagBased來代表這個演算法。表3給出了SimpleTagBased演算法和NormTagBased演算法在各種指標上的實驗結果的比較。

表3 SimpleTagBased演算法和NormTagBased演算法的比較

enter image description here

如表3所示,經過歸一化之後的NormTagBased演算法無論在召回率/準確率,還是在覆蓋度、多樣性和熱門程度等指標上,均優於SimpleTagBased演算法。因此,NormTagBased演算法是對SimpleTagBased的演算法的一個有效的改進。

資料稀疏性

在前面的演算法中,使用者興趣和物品的聯絡是通過B(u)∩B(i)中的標籤建立的。但是,如果這個使用者是新使用者,或者物品是新物品,那麼這個集合(B(u)∩B(i))中的標籤數量會很少。為了提高推薦的準確率,我們可能要對標籤集合做擴充套件,比如使用者曾經用過“推薦系統”這個標籤,我們可以將這個標籤的相似標籤也加入到使用者標籤集合中,比如“個性化”,“協同過濾”等標籤。

為了說明資料稀疏性對效能的影響,我們將使用者按照打過的標籤數分成兩組。第一組使用者打過10次以下的標籤,而第二組使用者打過超過10次標籤,我們分別統計這兩組使用者的推薦結果的準確率和召回率,結果如表4所示。

表4 不同活躍度的使用者的召回率/準確率對比 enter image description here

[具體實驗結果待正式發表時公佈]

進行標籤擴充套件有很多方法,其中著名的有話題模型(Topic Model)。不過這裡我們遵循簡單的原則,只介紹一種基於鄰域的方法。

標籤擴充套件的本質是對每個標籤找到和它相似的標籤,也就是計算標籤之間的相似度。最簡單的相似度可以是同義詞。如果我們有一個同義詞詞典,就可以根據這個詞典來進行標籤擴充套件。如果沒有這個詞典,我們還是可以從資料中統計出標籤的相似度。

如果認為同一個物品上的不同標籤具有某種相似度的話,那麼如果兩個標籤同時出現在很多物品的標籤集合中,就可以認為這兩個標籤具有較大的相似度。對於標籤b,令N(b)為有標籤b的物品的集合,n_(b,i)為給物品i打上標籤b的使用者數,可以通過如下的餘弦相似度公式計算標籤b和標籤b'的相似度:

enter image description here

[具體實驗結果待正式發表時公佈]

標籤清理

不是所有的標籤都能反應使用者的興趣。比如,在一個視訊網站中,使用者可能對一個視訊賦予了一個表示情緒的標籤,比如“不好笑”(no funny)。但我們不能因此認為使用者對“不好笑”有興趣,並且給使用者推薦其他具有“不好笑”這個標籤的視訊。相反,如果使用者對視訊打過“成龍”這個標籤,我們可以據此認為使用者對成龍的電影感興趣,從而給使用者推薦成龍其他的電影。同時,標籤系統裡經常出現詞形不同、詞義相同的標籤,比如recommender system和recommendation engine就是兩個同義詞。

標籤清理的另一個重要意義在於用標籤作為推薦解釋。如果我們要把標籤呈現給使用者,作為給使用者推薦某一個物品的解釋時,對標籤的質量要求就很高。首先,這些標籤不能包含沒有意義的停止詞或者表示情緒的詞,其次這些推薦解釋裡不能包含很多相同意義的詞語。

本章我們使用的標籤清理的方法有以下幾種。

  • 去除詞頻很高的停止詞。

  • 去除因詞根不同造成的同義詞,比如 recommender system和recommendation system。

  • 去除因分隔符造成的同義詞,比如 collaborative_filtering和collaborative-filtering。

[具體實驗結果待正式發表時公佈]

為了控制標籤的質量,很多網站也採用了讓使用者反饋的思想,即讓使用者來告訴系統某個標籤是否合適。MovieLens在他們的實驗系統中就採用了這種方法,關於這方面的研究可以參考GroupLens的Shilad同學的博士論文 。此外,電影推薦網站Jinni也採用了這種方式(如圖9所示)。當然,Jinni不屬於UGC的標籤系統,它給電影的標籤是專家賦予的,因此它讓使用者對標籤反饋其實是想融合專家和廣大使用者的知識。

enter image description here

圖9 Jinni允許使用者對編輯給的標籤進行反饋

基於圖的推薦演算法


前面討論的簡單演算法很容易懂,也容易實現,但缺點是不夠系統化和理論化。因此,在這一節中,我們主要討論如何利用圖模型來做基於標籤資料的個性化推薦。

首先,我們需要將使用者的標籤行為表示到一個圖上。我們知道,圖是由頂點、邊和邊上的權重組成的。而在使用者標籤資料集上,有三種不同的元素:使用者、物品和標籤。因此,我們需要定義三種不同的頂點:使用者頂點、物品頂點和標籤頂點。然後,如果我們得到一個表示使用者u給物品i打了標籤b的使用者標籤行為(u,i,b),那麼,最自然的想法就是在圖中增加三條邊,首先在使用者u對應的頂點v(u)和物品i對應的頂點v(i)之間需要增加一條邊(如果這兩個頂點已經有邊相連,那麼就應該將邊的權重加1),同理,在v(u)和v(b)之間需要增加一條邊, v(i)和v(b)之間也需要邊相連線。

圖10是一個簡單的使用者-物品-標籤圖的例子。

enter image description here

圖10 一個簡單的使用者-物品-標籤圖的例子

通過使用者標籤行為構造出圖之後,為使用者u推薦物品的問題就轉化為計算圖上所有物品節點相對於使用者節點v(u)的相關度排名的問題。圖上的排名演算法很多,其中最著名的就是PageRank演算法。

PageRank演算法最初是用來對網際網路上的網頁進行排名的演算法。網頁通過超級連結形成了圖。PageRank假設使用者從所有網頁裡隨機挑出一個網頁,然後開始通過超級連結進行網上衝浪。到達每個網頁後,使用者首先會以d的概率繼續衝浪,而在衝浪時,使用者會以同等的概率在當前網頁的所有超級連結中隨機挑選一個進入下一個網頁。那麼,在這種模擬下,最終每個網頁都會有一個被使用者訪問到的穩定概率,而這個概率就是網頁的排名。

PageRank演算法通過如下的迭代關係式來計算網頁的權重:

enter image description here

其中PR(i)是網頁i的排名,d是使用者每次繼續衝浪的概率,N是所有網頁的總數。in(i)是指向網頁i的所有網頁的集合,out(j)是網頁j鏈向的網頁的集合。

下面我們舉一個簡單的例子來說明PageRank演算法,我們用圖11所示的例子來演示一下PageRank的迭代過程。

enter image description here

圖11 說明PageRank演算法的圖例

(1)一開始,每個頂點的排名都是一樣的,PR(A) = PR(B) = PR(C) = PR(D) = PR(E) = 1 / 5,令d = 0.85。

(2)根據前面的迭代關係式有

PR(A) = (1 – 0.85) / 5 + 0.85 * (PR(B) / 2) = 0.115
PR(B) = (1 – 0.85) / 5 + 0.85 * (PR(C) / 1) = 0.2
PR(C) = (1 – 0.85) / 5 + 0.85 * (PR(A) / 2 + PR(D) / 2 + PR(E) / 2) = 0.285
PR(D) = (1 – 0.85) / 5 + 0.85 * (PR(E) / 2) = 0.115
PR(E) = (1 – 0.85) / 5 + 0.85 * (PR(A) / 2 + PR(B) / 2 + PR(D) / 2) = 0.285

(3)繼續按照前面的迭代關係式,有

PR(A) = (1 – 0.85) / 5 + 0.85 * (PR(B) / 2) = 0.115
PR(B) = (1 – 0.85) / 5 + 0.85 * (PR(C) / 1) = 0.27225
PR(C) = (1 – 0.85) / 5 + 0.85 * (PR(A) / 2 + PR(D) / 2 + PR(E) / 2) = 0.248875
PR(D) = (1 – 0.85) / 5 + 0.85 * (PR(E) / 2) = 0.151125
PR(E) = (1 – 0.85) / 5 + 0.85 * (PR(A) / 2 + PR(B) / 2 + PR(D) / 2) = 0.21275

我們可以按照上面的步驟一步步迭代下去,最終得到所有頂點的PageRank排名。

但是,從上面的描述可以看到,PageRank只是計算了所有頂點的全域性排名,並不能用來計算一個頂點相對於另一個頂點的相關度排名。因此,很多研究人員對PageRank做出了修改,其中一個著名的修改就是TopicRank演算法。

PageRank演算法認為,使用者每次都是從所有頂點中以相同的概率隨機挑選一個頂點,然後開始隨機遊走,而且在每次隨機遊走經過每個頂點時,都會有1 - d的概率停止遊走。那麼,如果我們要計算所有點相對於某一個頂點的相關度排名,我們可以假設使用者每次都從某一個頂點v出發,然後在每次隨機遊走經過每個頂點時都以1-d的概率停止遊走,從v重新開始。 那麼,最終每個頂點被訪問的概率就是這些頂點和v的相關度排名。

PageRank可以用來給圖中的頂點進行全域性的排名,但它無法用來給每個使用者個性化的對所有物品排序。為了解決個性化排名的問題,史丹佛大學的Haveliwala提出了TopicRank的演算法 ,這個演算法可以用來做個性化排序,因此本文將其稱為PersonalRank。PersonalRank的迭代公式如下:

enter image description here

可以看到,PersonalRank和PageRank的區別在於用ri代替了1/N,也就是說,從不同的點重新開始的概率不同了。那麼,假設如果我們要計算所有頂點和頂點k的相關度排名,我們可以定義enter image description here如下:

enter image description here

然後利用上面的迭代公式,就可以計算出所有頂點相對於k的相關度排名。我們將這裡的enter image description here稱為頂點i的啟動概率。

回到給使用者推薦物品這個問題上來,在我們構造出使用者-物品-標籤的圖之後,如果我們要給使用者u做推薦,我們可以令頂點v(u)的啟動概率為1,而其他頂點的啟動概率為0。然後用上面的迭代公式來計算所有物品對應的頂點相對於v(u)的排名。

下面兩段Python程式碼給出瞭如何從使用者行為記錄集合tagging_records中構建圖,以及如何在圖上給使用者進行推薦。

def BuildGraph(tagging_records):
   graph = dict()
   for user, item, tag in tagging_records:
       addToMat(graph, ‘u:’+user, ‘i:’+item, 1)
       addToMat(graph, ‘i:’+item, ‘u:’+user, 1)
       addToMat(graph, ‘u:’+user, ‘t:’+tag, 1)
       addToMat(graph, ‘t:’+tag, ‘u:’+user, 1)
       addToMat(graph, ‘t:’+tag, ‘i:’+item, 1)
       addToMat(graph, ‘i:’+item, ‘t:’+tag, 1)

def Recommend(user, d, K):
   rank = dict()
   rank[‘u:’+user] = 1
   for step in range(0:K):
       for i, ri in rank.items():
           for j, wij in graph[i]:
               tmp_rank[j] += d * ri * wij
       tmp_rank[‘u:’ + user] = (1 – d)
       sum_weight = sum(tmp_rank.values())
       rank = dict()
       for i, ri in tmp_rank.items():
           rank[i] = ri / sum_weight
       tmp_rank = dict()
   return rank

這裡,d是前面提到的繼續隨機遊走的概率,K是迭代的次數。在上面從某一個使用者節點開始隨機遊走時,迭代K步最多可以走到離該使用者節點距離為K之內的所有頂點,而其他頂點的權重為0。

在傳統的PersonalRank中,我們需要迭代很多次,直到所有頂點的權重都穩定了為止。但是,如果我們為每個使用者做推薦,都需要在全圖上進行迭代,直到全圖的所有頂點的權重都收斂,這樣的時間複雜度太大了。因此,我們在實際的應用中一般只迭代比較少的次數。

用圖模型解釋前面的簡單演算法

在介紹了圖模型後,我們可以用圖模型來重新看待前面提到的簡單的演算法。在那個演算法中,使用者對物品的興趣通過如下的公式計算:

enter image description here

這個公式認為使用者對物品的興趣通過標籤來傳遞,因此這個公式可以通過一個比前面簡單的圖來建模(記為SimpleTagGraph)。給定使用者標籤行為記錄(u,i,b),SimpleTagGraph會增加兩條有向邊,一條由使用者節點v(u)指向標籤節點v(b),另一條由標籤節點v(b)指向物品節點v(i)。

圖12就是一個簡單的SimpleGraph的例子。在構建了SimpleGraph後,利用前面的PersonalRank演算法,令K = 1,就是我們前面提出的簡單推薦演算法。

enter image description here

圖12 SimpleGraph的例子

[相關實驗:發表時公佈

A. 迭代次數K對精度的影響

B. 邊權重的定義對精度的影響

]

基於標籤的推薦解釋


基於標籤的推薦的最大好處是可以利用標籤來做推薦解釋,這方面的代表性應用是豆瓣的個性化推薦系統。圖13展示了豆瓣讀書的個性化推薦介面。

enter image description here

圖13 豆瓣讀書的個性化推薦應用“豆瓣猜”的介面

如圖13所示,豆瓣讀書推薦結果包括兩個部分。上面是一個標籤雲,表示使用者的興趣分佈,標籤的尺寸越大,表示使用者對這個標籤相關的圖書越感興趣。比如圖13顯示了我在豆瓣的閱讀興趣,從上方的標籤雲可以看到,豆瓣認為我對“程式設計”、“機器學習”、“軟體開發”感興趣,這是因為我看了很多IT技術方面的圖書,豆瓣認為我對“東野圭吾”感興趣,是因為我看了好幾本他的偵探小說,同時因為我對人文學科比較感興趣,所以豆瓣認為我對“傳記”、“文化”比較感興趣。單擊每一個標籤雲中的標籤,都可以在標籤雲下方得到和這個標籤相關的圖書推薦,比如圖13就是機器學習相關的圖書推薦。

豆瓣這樣組織推薦結果頁面有很多好處。首先這樣提高了推薦結果的多樣性。我們知道,一個使用者的興趣在長時間內是很廣泛的,但在某一天又比較具體。因此,我們如果想在某一天擊中使用者當天的興趣,是非常困難的。而豆瓣通過標籤雲,展示了使用者的所有興趣,然後讓使用者自己根據他今天的興趣選擇相關的標籤,得到推薦結果,從而極大地提高了推薦結果的多樣性,使得推薦結果更容易滿足使用者多樣的興趣。

同時,標籤雲也提供了推薦解釋的作用。使用者通過這個介面可以知道豆瓣給自己推薦的每一本書都是基於它認為自己對某個標籤感興趣。而對於每個標籤,使用者總能通過回憶自己之前的行為來知道自己是否真的對這個標籤感興趣。

我們知道,要讓使用者直接覺得推薦結果是有道理的,是很困難的。而豆瓣將推薦結果的可解釋性拆分成了兩個部分,首先讓使用者覺得標籤雲是有道理的,然後讓使用者覺得從某個標籤推薦出某本書也是有道理的。因為生成讓使用者覺得有道理的標籤雲比生成讓使用者覺得有道理的推薦圖書更加簡單,標籤和書的關係就更容易讓使用者覺得有道理,從而讓使用者最終覺得推薦出來的書也是很有道理的。

上一篇

相關文章