转载自AI Studio
项目链接https://aistudio.baidu.com/aistudio/projectdetail/2556840

赛题背景

https://www.datafountain.cn/competitions/542

随着新型互联网的发展,人类逐渐进入了信息爆炸时代。新型电商网络面临的问题也逐渐转为如何让用户从海量的商品中挑选到自己想要的目标。推荐系统正是在互联网快速发展之后的产物。

为帮助电商系统识别用户需求,为用户提供其更加感兴趣的信息,从而为用户提供更好的服务,需要依据真实的图书阅读数据集,利用机器学习的相关技术,建立一个图书推荐系统。用于为用户推荐其可能进行阅读的数据,从而在产生商业价值的同时,提升用户的阅读体验,帮助创建全民读书的良好社会风气。

赛题任务

依据真实世界中的用户-图书交互记录,利用机器学习相关技术,建立一个精确稳定的图书推荐系统,预测用户可能会进行阅读的书籍。

赛题数据

数据简介

数据集来自公开数据集Goodbooks-10k,包含网站Goodreads中对10,000本书共约6,000,000条评分。为了预测用户下一个可能的交互对象,数据集已经处理为隐式交互数据集。该数据集广泛的应用于推荐系统中。

数据说明

数据文件夹包含4个文件,依次为:

文件类别文件名文件内容
训练集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:对测试集进行预测

首先将测试集数据转为模型需要的格式,然后一行代码完成预测即可,然后转换为提交格式。

改进思路

由于现有的代码写的比较基础,所以有很多改进的步骤:

  1. 对模型精度进行改进,可以考虑构建更加复杂的模型,并对训练集负样本构造过程进行改进。
  2. 对模型使用内存,可以考虑使用Numpy代替Pandas的操作。
!cp /home/aistudio/data/data114712/test_dataset.csv ./
!head train_dataset.csv
user_id,item_id
0,257
0,267
0,5555
0,3637
0,1795
0,866
0,46
0,2737
0,4690

0,4690

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()

# 从新编码user 和 book,类似标签编码的过程
# 此步骤主要为减少id的编码空间
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 与 电影的对应关系
user_book_dict
user
0        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
1        [115, 116, 117, 118, 119, 120, 21, 121, 122, 1...
2        [30, 136, 171, 167, 165, 164, 159, 163, 166, 1...
3        [260, 261, 262, 263, 264, 53, 265, 266, 267, 2...
4        [333, 334, 335, 336, 337, 338, 339, 340, 341, ...
                               ...                        
53419    [2133, 941, 610, 2014, 559, 2973, 914, 1745, 1...
53420    [6, 378, 119, 197, 41, 20, 45, 137, 46, 48, 36...
53421    [5302, 2085, 2082, 2083, 2072, 2073, 2070, 208...
53422    [6792, 8991, 8584, 326, 5248, 4994, 6739, 6738...
53423    [7626, 6201, 6186, 8161, 8158, 8159, 5961, 609...
Name: movie, Length: 53424, dtype: object

构造负样本

# 随机挑选数据集作为负样本,负样本只需要对没有看的电影进行随机采样
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

# 测试dataloder读取
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
(2,) (1,)
[28798  8063] [0.]
[5120, 2]
[5120, 1]
[5120, 2]
[5120, 1]

定义模型

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)
W1101 21:24:14.437119   128 device_context.cc:404] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W1101 21:24:14.442728   128 device_context.cc:422] device: 0, cuDNN Version: 7.6.


The loss value printed in the log is the current step, and the metric is the average value of previous steps.
Epoch 1/5


/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/utils.py:77: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  return (isinstance(seq, collections.Sequence) and


step 1643/1643 [==============================] - loss: 0.4193 - precision: 0.7936 - 55ms/step        
save checkpoint at /home/aistudio/checkpoints/0
Eval begin...
step 548/548 [==============================] - loss: 0.4078 - precision: 0.8295 - 51ms/step        
Eval samples: 2803008
Epoch 2/5
step 1643/1643 [==============================] - loss: 0.3491 - precision: 0.8462 - 54ms/step         
save checkpoint at /home/aistudio/checkpoints/1
Eval begin...
step 548/548 [==============================] - loss: 0.3510 - precision: 0.8518 - 52ms/step         
Eval samples: 2803008
Epoch 3/5
step  740/1643 [============>.................] - loss: 0.3267 - precision: 0.8648 - ETA: 47s - 53ms/st

预测测试集

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
Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐