構建一個增量推薦系統

2020-04-18   AI公園

作者:Dimitris Poulopoulos

編譯:ronghuaiyang

導讀

推薦系統應該要可以適應發生的變化。

需求或先決條件

雖然我會儘量少用數學術語,但這篇文章希望讀者熟悉一些概念,比如用戶—項目交互矩陣、矩陣分解、嵌入空間,以及基本的機器學習術語。這個故事不是介紹推薦系統。這是對它們的增量變換的介紹。無論如何,這個故事都是針對機器學習和推薦系統領域的初學者的。

介紹

啟動一個機器學習項目,數據科學家收集數據,處理它們,訓練一個模型並將其部署到生產中。當模型的性能開始變差時,數據科學家通常會從頭開始重複這個循環。只是這一次,他們有了新的數據來更新模型並提高其性能。然而,這通常是一種反生產實踐,也是一種低效的實踐,特別是在對當前數據做出決策是至關重要的業務領域。

現在,進入推薦系統的世界,用戶的偏好經常根據季節、預算、時尚趨勢等變化。此外,到達的客戶和新的庫存產生了所謂的冷啟動問題,即系統沒有足夠的信息來匹配消費者與產品或服務。一個推薦系統應該在這些變化發生的時候理想地適應這些變化,修改它的模型以始終代表當前狀態,同時需要一次數據傳遞。這就是增量學習的概念。

這個故事是這個系列的第一部分,在這個系列中,我們將探索如何將增量學習的思想應用到推薦系統中,並使用實際操作的方法。在第一部分中,我們使用一個基於Pytorch的python庫CF Step,重現了Joao Vinagre等人發表的*「Fast incremental matrix factorization for recommendation with positive-only feedback」*中的結果。接下來,我們將通過應用一些技巧來達到更高的目標。

快速增量矩陣分解

我們將要實現的算法將使用隱式的、只有正的反饋。讓我們試著揭開這兩個關鍵詞的神秘面紗。「隱性反饋」指的是,用戶從未對他們接觸過的內容表達過直接的意見(如評分)。隱式反饋的一個例子是客戶購買產品的次數,或者他們看電影的時間。顧客購買產品或使用服務越多,我們就越有信心認為這是一種偏好。只有正的反饋通常與隱式反饋一起出現。這是因為,在隱式反饋的情況下,我們很少知道什麼構成了消極的交互。一個用戶不與一個項目交互沒有什麼意義。想像一個超市裡的消費者。如果客戶還沒有購買特定的產品,我們無法確定原因。

回到我們的實現中,僅為正數意味著用戶—物品交互矩陣R只包含布爾值,其中true表示默認值,false表示缺失值。這個假設有兩個主要含義,保持R的稀疏性,因為在訓練期間只使用正反饋,對於任意的用戶—物品交互,false值的地方都是完全有效的推薦候選。

算法&方案

現在讓我們進一步研究一下本文提出的增量隨機梯度下降(ISGD)算法。

ISGD — 增量 SGD

我們擁有的數據是元組或者說用戶—物品交互。記住這些都是正的交互。算法的輸入是三個數字,feat這是用戶或物品嵌入空間的維度,λ,即正則化係數,還有η,學習率。算法的輸出是兩個嵌入矩陣:用戶矩陣A和物品矩陣B。這些矩陣的維數,A是number_of_users x feat,B是number_of_items x feat 。然後我們有幾個步驟:

  • 檢查活躍用戶是否已知。如果不是,則創建一個具有隨機潛在特徵的新用戶,該用戶來自均值為0、標準差為1的正態分布。對活躍物品執行相同的操作。
  • 計算損失。因為我們只需要處理正反饋,所以目標總是「1」。因此,我們只需要從「1」中減去我們的預測。
  • 使用通用的更新規則更新活躍用戶的潛在特徵(用戶嵌入矩陣中的參數)。對活躍物品執行相同的操作。
  • 轉到下一個數據點。這樣,我們就可以處理任意長度的流數據。

實現&評估

對於這個實現,我們將使用CF Step Python庫和著名的Movielens數據集。CF Step是一個開源的庫,用python編寫,以Pytorch為基礎,支持實現快速增量式學習推薦系統。該庫是歐洲研究項目CloudDBAppliance的副產品。你可以很容易地運行以下命令安裝庫:

pip install cf-step

接下來,下載movielens 1m數據集並提取ratings.dat文件放到一個方便的位置,例如,Linux中的tmp文件夾。對於這個實現,我們只需要這個文件。其餘的文件(users.dat和movies.dat)包含了用戶和電影的元數據。我們使用pandas加載的文件到內存:

# load the data
col_names = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings_df = pd.read_csv('/tmp/ratings.dat', delimiter='::', names=col_names, engine='python')

# transform users and movies to categorical features
ratings_df['user_id'] = ratings_df['user_id'].astype('category')
ratings_df['movie_id'] = ratings_df['movie_id'].astype('category')

# use the category codes to avoid creating separate vocabularies
ratings_df['user_code'] = ratings_df['user_id'].cat.codes.astype(int)
ratings_df['movie_code'] = ratings_df['movie_id'].cat.codes.astype(int)

ratings_df.head()

如你所見,我們將用戶和電影IDs轉換為類別,以便提取類別編碼。現在,我們不必為嵌入矩陣的生成創建單獨的詞彙表。我們只需要處理用戶和電影編碼,而不是IDs。最後,我們通過這個dataframe在編碼和IDs之間建立連接,以查找原始用戶和電影。現在,讓我們看看我們正在處理的用戶和電影的數量。

n_users = ratings_df['user_code'].max() + 1
n_movies = ratings_df['movie_code'].max() + 1

如果我們列印這些數字,我們將看到我們有6040個用戶和3706個電影。接下來,我們將按照timestamp對數據進行排序,以模擬流事件。

data_df = ratings_df.sort_values(by='timestamp')

如前所述,該算法只支持正反饋。因此,我們將把5的評分視為積極的反饋,並丟棄其他的評分。我們想用1表示喜歡,用0表示不喜歡,並創建一個名為preference的新列來保存它們。然後,我們可以只過濾preference == 1。

# more than 4 -> 1, less than 5 -> 0
data_df['preference'] = np.where(data_df['rating'] > 4, 1, 0)

# keep only ones and discard the others
data_df_cleaned = data_df[(data_df['preference'] == 1)]
data_df_cleaned.head()

接下來,讓我們初始化我們的模型。為此,我們需要一個模型架構、一個目標函數(即損失函數)和一個優化器。我們將使用SimpleCF網絡作為模型架構,這是CF Step提供的內置神經網絡架構。對於目標函數,我們將使用一個簡單的lambda函數,它接受一個prediction和一個target,並從target中減去prediction。在我們的例子中,target總是1。對於優化器,我們將使用Pytorch的SGD實現。我們選擇的因子數為128,學習率為0.06。現在我們準備初始化Step模型。

net = SimpleCF(n_users, n_movies, factors=128, init=torch.nn.init.normal_, mean=0., std=.1)
objective = lambda pred, target: target - pred
optimizer = SGD(net.parameters(), lr=6e-2)
device = 'cuda' if torch.cuda.is_available() else 'cpu'

model = Step(net, objective, optimizer, device=device)

評估方法如下:

  • 通過對前20%的數據進行訓練來提升模型。
  • 模擬數據流,並使用recall@k 作為度量來評估模型的性能。
  • 如果用戶是已知的,作出預測,並為這個預測計算recall@k 。然後,使用這種用戶—物品交互逐步地訓練增量算法。
  • 如果用戶未知,使用這種用戶—物品交互,訓練增量算法,。

為此,讓我們取前20%的數據,創建數據加載器和批次來擬合模型。

pct = int(data_df_cleaned.shape[0] * .2)
bootstrapping_data = data_df_cleaned[:pct]

features = ['user_code', 'movie_code', 'rating']
target = ['preference']
data_set = TensorDataset(torch.tensor(bootstrapping_data[features].values), torch.tensor(bootstrapping_data[target].values))
data_loader = DataLoader(data_set, batch_size=BATCH_SIZE, shuffle=False)

model.batch_fit(data_loader)

然後,我們獲取剩餘的數據並創建一個不同的數據集。

# get the remaining data
data_df_step = data_df_cleaned.drop(bootstrapping_data.index)
data_df_step = data_df_step.reset_index(drop=True)

# create the DataLoader
stream_data_set = TensorDataset(torch.tensor(data_df_step[features].values), torch.tensor(data_df_step[target].values))
stream_data_loader = DataLoader(stream_data_set, batch_size=1, shuffle=False)

最後,模擬流並使用recall@10對模型進行評估。這個步驟在GPU上需要5到6分鐘。

k = 10  # we keep only the top 10 recommendations
recalls = []
known_users = []

with tqdm(total=len(stream_data_loader)) as pbar:
for idx, (features, preferences) in enumerate(stream_data_loader):
itr = idx + 1

user = features[:, 0]
item = features[:, 1]
rtng = features[:, 2]
pref = preferences

if user.item() in known_users and rtng.item() == 5:
predictions = model.predict(user, k)
recall = recall_at_k(predictions.tolist(), item.tolist(), k)
recalls.append(recall)
model.step(user, item, rtng, pref)
else:
model.step(user, item, rtng, pref)

known_users.append(user.item())
pbar.update(1)

我們可以可視化我們的訓練結果。為此,我們將使用一個5k滑動窗口的移動平均值,就像他們在發布的論文中所做的那樣。我們可以看到,對於movielens數據集,下圖與論文中給出的結果一致。要保存模型,使用model.save()內置方法並傳遞一個有效的路徑。

總結

在這個故事中,我們展示了增量學習在推薦系統中的重要性,並重現了Joao Vinagre等人發表的*「Fast incremental matrix factorization for recommendation with positive-only feedback」*的結果。我們介紹了CF Step python庫,這是一個開源庫,可以快速實現增量學習推薦系統。在下一章,我們將進一步討論這個問題,並嘗試提高算法的準確性。


英文原文:https://towardsdatascience.com/building-an-incremental-recommender-system-8836e30afaef