作者:阿水,北京航空航天大學,Datawhale成員
本文以CCF大資料與計算智能大賽(CCF BDCI)圖書推薦系統競賽為實踐背景,使用Paddle建構使用者與圖書的打分模型,借助Embedding層來完成具體的比對過程。背景回複 211208 可擷取完整代碼。
代碼位址:
https://aistudio.baidu.com/aistudio/projectdetail/2556840

實踐背景
賽題背景
随着新型網際網路的發展,人類逐漸進入了資訊爆炸時代。新型電商網絡面臨的問題也逐漸轉為如何讓使用者從海量的商品中挑選到自己想要的目标。推薦系統正是在網際網路快速發展之後的産物。
為幫助電商系統識别使用者需求,為使用者提供其更加感興趣的資訊,進而為使用者提供更好的服務,需要依據真實的圖書閱讀資料集,利用機器學習的相關技術,建立一個圖書推薦系統。用于為使用者推薦其可能進行閱讀的資料,進而在産生商業價值的同時,提升使用者的閱讀體驗,幫助建立全民讀書的良好社會風氣。
賽題任務
依據真實世界中的使用者-圖書互動記錄,利用機器學習相關技術,建立一個精确穩定的圖書推薦系統,預測使用者可能會進行閱讀的書籍。
賽題資料
資料集來自公開資料集Goodbooks-10k,包含網站Goodreads中對10,000本書共約6,000,000條評分。為了預測使用者下一個可能的互動對象,資料集已經處理為隐式互動資料集。該資料集廣泛的應用于推薦系統中。
資料檔案夾包含3個檔案,依次為:
- 訓練集: train.csv 訓練資料集,為使用者-圖書互動記錄
- 測試集: test.csv 測試資料集,隻有需要進行預測使用者ID
- 送出樣例: submission.csv 僅有兩個字段user_id/item_id
解題思路
使用深度學習模型建構隐式推薦算法模型,并建構負樣本,最終按照模型輸出的評分進行排序,做出最終的推薦。具體可以分為以下幾個步驟:
- 步驟1:讀取資料,對使用者和圖書進行編碼;
- 步驟2:利用訓練集建構負樣本;
- 步驟3:使用Paddle建構打分模型;
- 步驟4:對測試集資料進行預測;
步驟1:讀取資料集
首先我們使用pandas讀取資料集,并對資料的字段進行編碼。這裡可以手動構造編碼過程,也可以使用LabelEncoder來完成。
這一步驟的操作目的是将對使用者和圖書編碼為連續的數值,原始的取值并不是連續的,這樣可以減少後續模型所需要的空間。
步驟2:建構負樣本
由于原始訓練集中都是記錄的是使用者已有的圖書記錄,并不存在負樣本。而在預測階段我們需要預測使用者下一個圖書,此時的預測空間是使用者對所有圖書的關系。
這裡建構負樣本的操作非常粗暴,直接是選擇使用者在訓練集中沒有圖書。這裡可以先使用協同過濾的思路來建構負樣本,即将負樣本是相似使用者都沒有記錄的圖書。
步驟3:Paddle搭建打分模型
這裡使用Paddle建構使用者與圖書的打分模型,借助Embedding層來完成具體的比對過程。這裡用最簡單的dot來完成比對,沒有建構複雜的模型。
步驟4:對測試集進行預測
首先将測試集資料轉為模型需要的格式,然後一行代碼完成預測即可,然後轉換為送出格式。
改進思路
由于現有的代碼寫的比較基礎,是以有很多改進的步驟:
- 對模型精度進行改進,可以考慮建構更加複雜的模型,并對訓練集負樣本構造過程進行改進。
- 對模型使用記憶體,可以考慮使用Numpy代替Pandas的操作。
代碼實踐
讀取資料集
# 檢視目前挂載的資料集目錄, 該目錄下的變更重新開機環境後會自動還原
# View dataset directory.
# This directory will be recovered automatically after resetting environment.
!unzip /home/aistudio/data/data114712/train_dataset.zip
Archive: /home/aistudio/data/data114712/train_dataset.zip
inflating: train_dataset.csv
!cp /home/aistudio/data/data114712/test_dataset.csv ./
!head train_dataset.csv
user_id,item_id
import pandas as pd
import numpy as np
import paddle
import paddle.nn as nn
from paddle.io import Dataset
df = pd.read_csv('train_dataset.csv')
user_ids = df["user_id"].unique().tolist()
user2user_encoded = {x: i for i, x in enumerate(user_ids)}
userencoded2user = {i: x for i, x in enumerate(user_ids)}
book_ids = df["item_id"].unique().tolist()
book2book_encoded = {x: i for i, x in enumerate(book_ids)}
book_encoded2book = {i: x for i, x in enumerate(book_ids)}
df["user"] = df["user_id"].map(user2user_encoded)
df["movie"] = df["item_id"].map(book2book_encoded)
num_users = len(user2user_encoded)
num_books = len(book_encoded2book)
user_book_dict = df.iloc[:].groupby(['user'])['movie'].apply(list)
user_book_dict
user
構造負樣本
neg_df = []
book_set = set(list(book_encoded2book.keys()))
for user_idx in user_book_dict.index:
book_idx = book_set - set(list(user_book_dict.loc[user_idx]))
book_idx = list(book_idx)
neg_book_idx = np.random.choice(book_idx, 100)
for x in neg_book_idx:
neg_df.append([user_idx, x])
neg_df = pd.DataFrame(neg_df, columns=['user', 'movie'])
neg_df['label'] = 0
df['label'] = 1
train_df = pd.concat([df[['user', 'movie', 'label']],
neg_df[['user', 'movie', 'label']]], axis=0)
train_df = train_df.sample(frac=1)
del df;
自定義資料集
# 自定義資料集
# 映射式(map-style)資料集需要繼承paddle.io.Dataset
class SelfDefinedDataset(Dataset):
def __init__(self, data_x, data_y, mode = 'train'):
super(SelfDefinedDataset, self).__init__()
self.data_x = data_x
self.data_y = data_y
self.mode = mode
def __getitem__(self, idx):
if self.mode == 'predict':
return self.data_x[idx]
else:
return self.data_x[idx], self.data_y[idx]
def __len__(self):
return len(self.data_x)
from sklearn.model_selection import train_test_split
x_train, x_val, y_train, y_val = train_test_split(train_df[['user', 'movie']].values,
train_df['label'].values.astype(np.float32).reshape(-1, 1))
traindataset = SelfDefinedDataset(x_train, y_train)
for data, label in traindataset:
print(data.shape, label.shape)
print(data, label)
break
train_loader = paddle.io.DataLoader(traindataset, batch_size = 1280*4, shuffle = True)
for batch_id, data in enumerate(train_loader):
x_data = data[0]
y_data = data[1]
print(x_data.shape)
print(y_data.shape)
break
val_dataset = SelfDefinedDataset(x_val, y_val)
val_loader = paddle.io.DataLoader(val_dataset, batch_size = 1280*4, shuffle = True)
for batch_id, data in enumerate(val_loader):
x_data = data[0]
y_data = data[1]
print(x_data.shape)
print(y_data.shape)
break
定義模型
EMBEDDING_SIZE = 32
class RecommenderNet(nn.Layer):
def __init__(self, num_users, num_movies, embedding_size):
super(RecommenderNet, self).__init__()
self.num_users = num_users
self.num_movies = num_movies
self.embedding_size = embedding_size
weight_attr_user = paddle.ParamAttr(
regularizer = paddle.regularizer.L2Decay(1e-6),
initializer = nn.initializer.KaimingNormal()
)
self.user_embedding = nn.Embedding(
num_users,
embedding_size,
weight_attr=weight_attr_user
)
self.user_bias = nn.Embedding(num_users, 1)
weight_attr_movie = paddle.ParamAttr(
regularizer = paddle.regularizer.L2Decay(1e-6),
initializer = nn.initializer.KaimingNormal()
)
self.movie_embedding = nn.Embedding(
num_movies,
embedding_size,
weight_attr=weight_attr_movie
)
self.movie_bias = nn.Embedding(num_movies, 1)
def forward(self, inputs):
user_vector = self.user_embedding(inputs[:, 0])
user_bias = self.user_bias(inputs[:, 0])
movie_vector = self.movie_embedding(inputs[:, 1])
movie_bias = self.movie_bias(inputs[:, 1])
dot_user_movie = paddle.dot(user_vector, movie_vector)
x = dot_user_movie + user_bias + movie_bias
x = nn.functional.sigmoid(x)
return x
model = RecommenderNet(num_users, num_books, EMBEDDING_SIZE)
model = paddle.Model(model)
optimizer = paddle.optimizer.Adam(parameters=model.parameters(), learning_rate=0.003)
loss = nn.BCELoss()
metric = paddle.metric.Precision()
## 設定visualdl路徑
log_dir = './visualdl'
callback = paddle.callbacks.VisualDL(log_dir=log_dir)
model.prepare(optimizer, loss, metric)
model.fit(train_loader, val_loader, epochs=5, save_dir='./checkpoints', verbose=1, callbacks=callback)
預測測試集
test_df = []
with open('sub.csv', 'w') as up:
up.write('user_id,item_id\n')
book_set = set(list(book_encoded2book.keys()))
for idx in range(int(len(user_book_dict)/1000) +1):
test_user_idx = []
test_book_idx = []
for user_idx in user_book_dict.index[idx*1000:(idx+1)*1000]:
book_idx = book_set - set(list(user_book_dict.loc[user_idx]))
book_idx = list(book_idx)
test_user_idx += [user_idx] * len(book_idx)
test_book_idx += book_idx
test_data = np.array([test_user_idx, test_book_idx]).T
test_dataset = SelfDefinedDataset(test_data, data_y=None, mode='predict')
test_loader = paddle.io.DataLoader(test_dataset, batch_size=1280, shuffle = False)
test_predict = model.predict(test_loader, batch_size=1024)
test_predict = np.concatenate(test_predict[0], 0)
test_data = pd.DataFrame(test_data, columns=['user', 'book'])
test_data['label'] = test_predict
for gp in test_data.groupby(['user']):
with open('sub.csv', 'a') as up:
u = gp[0]
b = gp[1]['book'].iloc[gp[1]['label'].argmax()]
up.write(f'{userencoded2user[u]}, {book_encoded2book[b]}\n')
del test_data, test_dataset, test_loader