亚马逊AWS官方博客

使用AWS Sagemaker训练因子分解机模型并应用于推荐系统

使用AWS Sagemaker系列文章:

第一篇:使用AWS Sagemaker训练因子分解机模型并应用于推荐系统(本博文)

第二篇:使用AWS Sagemaker部署的终端节点进行推荐预测的常用场景

————
在线服务和应用中,经常遇到需要对用户历史行为进行分析并预测,典型的案例如页面点击预测及推荐系统。这些案例的特点是历史数据集非常庞大,而且大多数情况,数据集是稀疏的。让我们以电影点评网站为例来理解稀疏数据集。在电影点评网站中,拥有大量的用户以及大量的电影,然而几乎不可能实现每个用户对每个电影都进行点评或打分。因此如果我们以用户为行,以电影为列,构建一个表格,对该用户点评过的电影单元格置1,未点评过的置0,我们可以发现,该表格中绝大部分数据都将是0。这就是稀疏数据集的典型例子。

针对稀疏数据集,因子分解机(Factorization Machines,FM)是比较有效的算法模型。

直观来说因子分解机可以考虑为由用户(User)为行、电影(Movie)为列构成的矩阵R可以表示为一个用户(User)为行、K列特征构成的矩阵P与电影(Movie)为行、K列特征构成的矩阵Q的转置的乘积。其中的K即潜藏特征值,可以将他理解为用户与电影之间的关系。在因子分解机中,这一值是可以自行设定的。算法的主要目的就是计算出P和Q矩阵,并使P与QT的乘积尽可能与R一致。

本次实验采用国内用户对大量国内外电影的评论作为训练数据集,利用AWS SageMaker自带的因子分解机算法构建模型,通过SageMaker的超参调优服务观察参数调整对模型表现的影响。最后,以实际应用中经常会遇到的用法演示模型的预测结果。本次实验全部使用Python3.6完成,在SageMaker中选用conda_python3的Kernel。

 

数据准备阶段

本次数据总量大约为396万条,用户数49万,电影数3万多。考虑到演示的目的和运算的效率,训练模型时使用其中10万条数据的采样。

In [3]:

df_all = pd.read_csv('all_movie_rates_noblank.csv')
print(df_all.shape)
print(df_all['UserId'].nunique())
print(df_all['MovieID'].nunique())

(3963891, 3)

490790

33345

 

首先我们对数据进行基本的观察。可以看到这10万条数据包含53902个用户和26004部电影。随后我们对数据集进行训练集和测试集的拆分,这里采用训练数据:测试数据 = 4:1的比例进行拆分。

 

In [4]:

df=pd.read_csv('all_movie_rates_100k.csv')
print(df.shape)
print(df['UserId'].nunique())
print(df['MovieID'].nunique())

df_train, df_test = train_test_split(df, test_size=0.2, random_state=42)
print(df_train.shape, df_test.shape)

print(df_train.head(10))
print(df_test.head(10))

(107029, 3)

53902

26004

(85623, 3) (21406, 3)

MovieID  Rate           UserId

63287  1296827    10      BloodzBoi

73804  3564327     6      LadyHoney

76527  6874441     8       49886917

71411  3152563     8     feathercat

64499  2004250     6  jingtianwst83

42130  3230459     6          funni

88502  3313801     6     chrisocean

79349  3072140     0       lala1123

21431  2053746     6      145992805

39240  1464338     8   HeroineDaode

MovieID  Rate        UserId

44496  26304167     8   152833029

11855   1299900     8      leonah

13955  26085750     2    49298107

27112   4202982     8  Kylin-2015

28103   4739952    10      likong

53598   3443393     4     3832465

65338   2052363     6     1926472

1116    6129707     8  vero_nicat

38854  26841337     2   158039357

78721   1307026     8    TowaErio

 

为用户和电影分别建立字典(Python Dictionary)

数据集中的用户ID和电影ID均为随机字符串,为了方便我们后续建立有序矩阵以及模型训练后预测结果的数据对应,我们首先为用户和电影分别建立由0开始的index序列,并使其与ID字符串对应。

In [5]:

filename = 'all_movie_rates_100k.csv'
user_number = 53902
movie_number = 26004

def createIDtoIndexDict(filename, user_number, movie_number):
    u_Dict = {}
    m_Dict = {}
    i = 0
    m = 0
    with open(filename, 'r') as f:
        sample = csv.reader(f, delimiter=',')
        for MovieID, Rate, UserId in sample:
            if UserId == 'UserId':
                continue
            else:
                if UserId not in u_Dict.keys():
                    u_Dict[UserId] = i
                    i = i+1 
                if MovieID not in m_Dict.keys():
                    m_Dict[MovieID] = m
                    m = m+1
    return u_Dict, m_Dict 
u_Dict, m_Dict = createIDtoIndexDict(filename, user_number, movie_number)

建立稀疏矩阵(Sparse Matrix)和标签向量(Label Vector)

因子分解机的训练是针对稀疏矩阵的,因此我们要将数据集中电影、评分、用户的序列转为稀疏矩阵,并根据用户评分的结果生成标签向量。我们使用Python Scipy模块中的lil_matrix来构建。

生成的矩阵应当是每一个用户ID作为单独一列、每一部电影在所有用户列之后也作为单独一列,针对原数据集中每行的数据,在对应的用户列和电影列置1。标签向量以用户评分为基准,我们设定用户评分大于等于6的为“喜爱”,并在对应的标签向量位置置1,反之为“不喜爱”,标签向量相应位置置0。

训练集和测试集均进行同样的操作。

df_train.to_csv('100k_train.csv', index=False, encoding="utf_8_sig")
df_test.to_csv('100k_test.csv', index=False, encoding="utf_8_sig")

columns = user_number+movie_number
def loadDataset(filename, lines, columns):
    X = scipy.sparse.lil_matrix((lines, columns)).astype('float32')
    Y = []
    line = 0
    with open (filename, 'r') as f:
        sample = csv.reader(f, delimiter=',')
        for MovieID, Rate, UserId in sample:
            if UserId == 'UserId':
                continue
            else:
                X[line, u_Dict[UserId]] = 1
                X[line, user_number+m_Dict[MovieID]] = 1
                if Rate == 'Rate' or int(Rate) < 6:
                    Y.append(0)
                else:
                    Y.append(1)
                line=line+1

    Y=np.array(Y).astype('float32')
    print(X.shape)
    print(Y.shape)
    return X, Y

 

In [222]:

X_train, Y_train = loadDataset('100k_train.csv', df_train.shape[0], columns)
X_test, Y_test = loadDataset('100k_test.csv', df_test.shape[0], columns)
print(X_train.shape, X_test.shape)
print(Y_train.shape, Y_test.shape)

(85623, 79906)

(85623,)

(21406, 79906)

(21406,)

(85623, 79906) (21406, 79906)

(85623,) (21406,)

 

我们获得了训练集为85623✖️79906的矩阵,训练标签向量为85263元素;测试集为21406✖️79906矩阵,其标签向量为21406元素。

 

转换稀疏矩阵为protobuf格式,并保存到S3

稀疏矩阵中绝大多数元素均为0,如果直接保存稀疏矩阵,会占用大量的存储空间,因此我们将其转为protobuf格式的数据,并保存到S3。

In [224]:

bucket = 'movie-recommendation-demo-raw-data'
prefix = 'sagemaker/movie-recommendation'
train_key      = 'train.protobuf'
train_prefix   = '{}/{}'.format(prefix, 'train')
test_key       = 'test.protobuf'
test_prefix    = '{}/{}'.format(prefix, 'test')
output_prefix  = 's3://{}/{}/output'.format(bucket, prefix)

def writeDatasetToProtobuf(X, Y, bucket, prefix, key):
    import io,boto3
    import sagemaker.amazon.com.rproxy.goskope.common as smac
    buf = io.BytesIO()
    smac.write_spmatrix_to_sparse_tensor(buf, X, Y)
    buf.seek(0)
    print(buf)
    obj = '{}/{}'.format(prefix, key)
    boto3.resource('s3').Bucket(bucket).Object(obj).upload_fileobj(buf)
    print('Wrote dataset: {}/{}'.format(bucket,obj))
    return 's3://{}/{}'.format(bucket,obj)
    
train_data = writeDatasetToProtobuf(X_train, Y_train, bucket, train_prefix, train_key)    
test_data = writeDatasetToProtobuf(X_test, Y_test, bucket, test_prefix, test_key)    

print(train_data)
print(test_data)
print('Output: {}'.format(output_prefix))

<_io.BytesIO object at 0x7f99a8b8e1a8>

Wrote dataset: movie-recommendation-demo-raw-data/sagemaker/movie-recommendation/train/train.protobuf

<_io.BytesIO object at 0x7f99a8b8e1a8>

Wrote dataset: movie-recommendation-demo-raw-data/sagemaker/movie-recommendation/test/test.protobuf

s3://movie-recommendation-demo-raw-data/sagemaker/movie-recommendation/train/train.protobuf

s3://movie-recommendation-demo-raw-data/sagemaker/movie-recommendation/test/test.protobuf

Output: s3://movie-recommendation-demo-raw-data/sagemaker/movie-recommendation/output

 

程序输出中Output的内容是模型训练完成后,保存模型代码的位置。

 

FM模型训练

FM是SageMaker自带的算法之一,因此通过SageMaker训练模型非常容易。首先我们需要引入SageMaker的SDK,并建立SageMaker的session、定义位于该Region的因子分解机算法Container以及获取SageMaker的运行角色。

In [225]:

from sagemaker import get_execution_role
from sagemaker.amazon.amazon_estimator import get_image_uri
import sagemaker
sess = sagemaker.Session()

role =  get_execution_role()
container = get_image_uri(boto3.Session().region_name, 'factorization-machines')

随后我们定义FM训练需要的一些参数。首先是环境参数,包括之前定义好的Container、角色、输出位置和session、还包括训练使用的EC2实例,本例中采用“ml.c4.xlarge”来训练。

之后,我们需要定义FM算法的超参(Hyperparameters)。在本例中特征列为用户数与电影数的总和79906、预测方式为二分类(即结果为判断“喜爱”或是“不喜爱”)、最小批量为1000、epoch时期为50次。其中num_factors即为在算法介绍中提到的潜藏特征K的数量,根据SageMaker官方文档的说明,建议在2-1000之间,通常64为最优值,因此,我们也设为64。

最后为模型提供训练集和测试集在S3中的位置,训练就开始了。

In [226]:

fm = sagemaker.estimator.Estimator(container,
                                   role, 
                                   train_instance_count=1, 
                                   train_instance_type='ml.c4.xlarge',
                                   output_path=output_prefix,
                                   sagemaker_session=sess)
fm.set_hyperparameters(feature_dim=79906,
                      predictor_type='binary_classifier',
                      mini_batch_size=1000,
                      num_factors=64,
                      epochs=50)

fm.fit({'train': train_data, 'test':test_data})

 

INFO:sagemaker:Creating training-job with name: factorization-machines-2018-12-18-03-18-50-388

2018-12-18 03:18:50 Starting – Starting the training job…

2018-12-18 03:18:56 Starting – Launching requested ML instances……

2018-12-18 03:19:58 Starting – Preparing the instances for training…

2018-12-18 03:20:45 Downloading – Downloading input data…

2018-12-18 03:20:55 Training – Downloading the training image..

Docker entrypoint called with argument(s): train

[12/18/2018 03:22:09 INFO 139830159329088] #quality_metric: host=algo-1, test binary_classification_accuracy <score>=0.736288890965

[12/18/2018 03:22:09 INFO 139830159329088] #quality_metric: host=algo-1, test binary_classification_cross_entropy <loss>=0.548373071499

[12/18/2018 03:22:09 INFO 139830159329088] #quality_metric: host=algo-1, test binary_f_1.000 <score>=0.84806072188

[2018-12-18 03:22:09.767] [tensorio] [info] data_pipeline_stats={“name”: “/opt/ml/input/data/test”, “epoch”: 1, “duration”: 366, “num_examples”: 22}

[2018-12-18 03:22:09.767] [tensorio] [info] data_pipeline_stats={“name”: “/opt/ml/input/data/test”, “duration”: 40228, “num_epochs”: 2, “num_examples”: 23}

#metrics {“Metrics”: {“totaltime”: {“count”: 1, “max”: 40280.484199523926, “sum”: 40280.484199523926, “min”: 40280.484199523926}, “setuptime”: {“count”: 1, “max”: 39.59202766418457, “sum”: 39.59202766418457, “min”: 39.59202766418457}}, “EndTime”: 1545103329.767301, “Dimensions”: {“Host”: “algo-1”, “Operation”: “training”, “Algorithm”: “factorization-machines”}, “StartTime”: 1545103329.299489}

 

[2018-12-18 03:22:09.784] [tensorio] [info] data_pipeline_stats={“name”: “/opt/ml/input/data/train”, “epoch”: 50, “duration”: 1287, “num_examples”: 86}

[2018-12-18 03:22:09.784] [tensorio] [info] data_pipeline_stats={“name”: “/opt/ml/input/data/train”, “duration”: 39923, “num_epochs”: 51, “num_examples”: 4301}

Billable seconds: 92

 

模型训练完成了。在模型训练结束后的总结中,我们可以看到几个重要的指标:

  • 模型训练计费时间92秒,所以并不会花很多钱;
  • 模型的二分准确度为73.62%

接下来,我们考虑一下应用SageMaker的超参调优(Hyperparameters Tuning)来尝试其他的超参设置是否可以获得更好的二分准确度。SageMaker的超参调优可以通过SageMaker的Console直接配置完成。简单来讲,就是首先设定目标,本例中我们希望最大化(Maximize)模型二分准确度。之后给予可调参数的变动范围,本例中我们希望测试mini batch size和epochs的设置是否可以提升结果表现。最后定义训练集、测试集、算法的相应位置,以及优化任务运行的次数(最大为100),即可开始。

当优化任务全部运行完成后,我们可以获得表现最好的模型的数据,如图

在这一参数配置下,模型的二分准确度提升为76.2%。应用这一超参配置训练模型,并部署为Endpoint。Endpoint可以理解为模型基于http访问的API接口,有了Endpoint就可以进行预测服务了。

In [227]:

fm.set_hyperparameters(feature_dim=79906,
                      predictor_type='binary_classifier',
                      mini_batch_size=200,
                      num_factors=64,
                      epochs=134)

In [228]:

fm_predictor = fm.deploy(initial_instance_count=1,
                         instance_type='ml.t2.medium')

INFO:sagemaker:Creating model with name: factorization-machines-2018-12-18-05-56-31-040

INFO:sagemaker:Creating endpoint with name factorization-machines-2018-12-18-05-47-26-108

—————————————————————–!

我们的模型部署完成,Endpoint名称为“factorization-machines-2018-12-18-05-47-26-108”。之后我们可以通过这个名称来调用Endpoint完成预测任务。

后续我们会继续利用已部署的终端节点 Endpoint 对常见的应用场景进行预测。

 

使用AWS Sagemaker系列文章:

第一篇:使用AWS Sagemaker训练因子分解机模型并应用于推荐系统(本博文)

第二篇:使用AWS Sagemaker部署的终端节点进行推荐预测的常用场景

————

本篇作者

崔辰

AWS大中华区创新中心技术业务拓展经理。加入AWS之前,崔辰在中国惠普、IBM、微软以及海航科技等公司担任过售前技术顾问、市场经理和战略合作经理等职务。在10多年的科技领域工作经历中,崔辰服务过众多企业级客户。