模型训练
首先需要定义好训练的参数,包括是否使用GPU、设置损失函数、选择优化器以及学习率等。 在本次实验中,由于数据较为简单,我们选择在CPU上训练,优化器使用Adam,学习率设置为0.01,一共训练5个epoch。
然而,针对推荐算法的网络,如何设计损失函数呢?在CV和NLP章节中我们了解,分类可以用交叉熵损失函数,损失函数的大小可以衡量出算法当前分类的准确性。在推荐算法中,没有一个准确的度量既能衡量推荐的好坏,并具备可导性质,又能监督神经网络的训练。在电影推荐中,可以作为标签的只有评分数据,因此,我们可以用评分数据作为监督信息,神经网络的输出作为预测值,使用均方差(Mean Square Error)损失函数去训练网络模型。
注:使用均方差损失函数即使用回归的方法完成模型训练。电影的评分数据只有5个,是否可以使用分类损失函数完成训练?事实上,评分数据应该是一个连续数据,比如,评分3和评分4是接近的,如果使用分类的方法,评分3和评分4是两个类别,容易割裂评分间的连续性。
整个训练过程和一般的模型训练大同小异,不再赘述。
def train(model):
# 配置训练参数
use_gpu = False
lr = 0.01
Epoches = 10
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
with fluid.dygraph.guard(place):
# 启动训练
model.train()
# 获得数据读取器
data_loader = model.train_loader
# 使用adam优化器,学习率使用0.01
opt = fluid.optimizer.Adam(learning_rate=lr, parameter_list=model.parameters())
for epoch in range(0, Epoches):
for idx, data in enumerate(data_loader()):
# 获得数据,并转为动态图格式
usr, mov, score = data
usr_v = [dygraph.to_variable(var) for var in usr]
mov_v = [dygraph.to_variable(var) for var in mov]
scores_label = dygraph.to_variable(score)
# 计算出算法的前向计算结果
_, _, scores_predict = model(usr_v, mov_v)
# 计算loss
loss = fluid.layers.square_error_cost(scores_predict, scores_label)
avg_loss = fluid.layers.mean(loss)
if idx % 500 == 0:
print("epoch: {}, batch_id: {}, loss is: {}".format(epoch, idx, avg_loss.numpy()))
# 损失函数下降,并清除梯度
avg_loss.backward()
opt.minimize(avg_loss)
model.clear_gradients()
# 每个epoch 保存一次模型
fluid.save_dygraph(model.state_dict(), './checkpoint/epoch'+str(epoch))
# 启动训练
with dygraph.guard():
use_poster, use_mov_title, use_mov_cat, use_age_job = False, True, True, True
model = Model('Recommend', use_poster, use_mov_title, use_mov_cat, use_age_job)
train(model)
- ##Total dataset instances: 1000209
- ##MovieLens dataset information:
- usr num: 6040
- movies num: 3883
- epoch: 0, batch_id: 0, loss is: [10.873174]
- epoch: 0, batch_id: 500, loss is: [0.9738145]
- epoch: 0, batch_id: 1000, loss is: [0.7016272]
- epoch: 0, batch_id: 1500, loss is: [1.0097994]
- epoch: 0, batch_id: 2000, loss is: [0.8981987]
- epoch: 0, batch_id: 2500, loss is: [0.8226846]
- epoch: 0, batch_id: 3000, loss is: [0.7943625]
- epoch: 0, batch_id: 3500, loss is: [0.88057446]
- epoch: 1, batch_id: 0, loss is: [0.8270193]
- epoch: 1, batch_id: 500, loss is: [0.711991]
- epoch: 1, batch_id: 1000, loss is: [0.97378314]
- epoch: 1, batch_id: 1500, loss is: [0.8741553]
- epoch: 1, batch_id: 2000, loss is: [0.873245]
- epoch: 1, batch_id: 2500, loss is: [0.8631375]
- epoch: 1, batch_id: 3000, loss is: [0.88147044]
- epoch: 1, batch_id: 3500, loss is: [0.9457144]
- epoch: 2, batch_id: 0, loss is: [0.7810389]
- epoch: 2, batch_id: 500, loss is: [0.9161325]
- epoch: 2, batch_id: 1000, loss is: [0.85070896]
- epoch: 2, batch_id: 1500, loss is: [0.83222216]
- epoch: 2, batch_id: 2000, loss is: [0.82739747]
- epoch: 2, batch_id: 2500, loss is: [0.7739769]
- epoch: 2, batch_id: 3000, loss is: [0.7288972]
- epoch: 2, batch_id: 3500, loss is: [0.71740997]
- epoch: 3, batch_id: 0, loss is: [0.7740326]
- epoch: 3, batch_id: 500, loss is: [0.79047513]
- epoch: 3, batch_id: 1000, loss is: [0.7714803]
- epoch: 3, batch_id: 1500, loss is: [0.7388534]
- epoch: 3, batch_id: 2000, loss is: [0.8264959]
- epoch: 3, batch_id: 2500, loss is: [0.65038306]
- epoch: 3, batch_id: 3000, loss is: [0.9168469]
- epoch: 3, batch_id: 3500, loss is: [0.8613069]
- epoch: 4, batch_id: 0, loss is: [0.7578842]
- epoch: 4, batch_id: 500, loss is: [0.89679146]
- epoch: 4, batch_id: 1000, loss is: [0.674494]
- epoch: 4, batch_id: 1500, loss is: [0.7206632]
- epoch: 4, batch_id: 2000, loss is: [0.7801018]
- epoch: 4, batch_id: 2500, loss is: [0.8618671]
- epoch: 4, batch_id: 3000, loss is: [0.8478118]
- epoch: 4, batch_id: 3500, loss is: [1.0286447]
- epoch: 5, batch_id: 0, loss is: [0.7023648]
- epoch: 5, batch_id: 500, loss is: [0.8227848]
- epoch: 5, batch_id: 1000, loss is: [0.88415223]
- epoch: 5, batch_id: 1500, loss is: [0.78416216]
- epoch: 5, batch_id: 2000, loss is: [0.7939043]
- epoch: 5, batch_id: 2500, loss is: [0.7428185]
- epoch: 5, batch_id: 3000, loss is: [0.745026]
- epoch: 5, batch_id: 3500, loss is: [0.76115835]
- epoch: 6, batch_id: 0, loss is: [0.83740556]
- epoch: 6, batch_id: 500, loss is: [0.816216]
- epoch: 6, batch_id: 1000, loss is: [0.8149048]
- epoch: 6, batch_id: 1500, loss is: [0.8676525]
- epoch: 6, batch_id: 2000, loss is: [0.88345516]
- epoch: 6, batch_id: 2500, loss is: [0.7371645]
- epoch: 6, batch_id: 3000, loss is: [0.7923065]
- epoch: 6, batch_id: 3500, loss is: [1.0073752]
- epoch: 7, batch_id: 0, loss is: [0.8476094]
- epoch: 7, batch_id: 500, loss is: [1.0047569]
- epoch: 7, batch_id: 1000, loss is: [0.80412626]
- epoch: 7, batch_id: 1500, loss is: [0.939283]
- epoch: 7, batch_id: 2000, loss is: [0.6579713]
- epoch: 7, batch_id: 2500, loss is: [0.7478874]
- epoch: 7, batch_id: 3000, loss is: [0.78322697]
- epoch: 7, batch_id: 3500, loss is: [0.8548964]
- epoch: 8, batch_id: 0, loss is: [0.8920554]
- epoch: 8, batch_id: 500, loss is: [0.69566244]
- epoch: 8, batch_id: 1000, loss is: [0.94016606]
- epoch: 8, batch_id: 1500, loss is: [0.7755744]
- epoch: 8, batch_id: 2000, loss is: [0.8520398]
- epoch: 8, batch_id: 2500, loss is: [0.77818584]
- epoch: 8, batch_id: 3000, loss is: [0.78463334]
- epoch: 8, batch_id: 3500, loss is: [0.8538652]
- epoch: 9, batch_id: 0, loss is: [0.9502439]
- epoch: 9, batch_id: 500, loss is: [0.8200456]
- epoch: 9, batch_id: 1000, loss is: [0.8938134]
- epoch: 9, batch_id: 1500, loss is: [0.8098132]
- epoch: 9, batch_id: 2000, loss is: [0.87928975]
- epoch: 9, batch_id: 2500, loss is: [0.7887068]
- epoch: 9, batch_id: 3000, loss is: [0.93909657]
- epoch: 9, batch_id: 3500, loss is: [0.69399315]
从训练结果来看,loss保持在0.9左右就难以下降了,主要是因为使用的均方差loss,计算得到预测评分和真实评分的均方差,真实评分的数据是1-5之间的整数,评分数据较大导致计算出来的loss也偏大。
不过不用担心,我们只是通过训练神经网络提取特征向量,loss只要收敛即可。
对训练的模型在验证集上做评估,除了训练所使用的Loss之外,还有两个选择:
- 评分预测精度ACC(Accuracy):将预测的float数字转成整数,计算和真实评分的匹配度。评分误差在0.5分以内的算正确,否则算错误。
- 评分预测误差(Mean Absolut Error)MAE:计算和真实评分之间的平均绝对误差。
下面是使用训练集评估这两个指标的代码实现。
def evaluation(model, params_file_path):
use_gpu = False
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
with fluid.dygraph.guard(place):
model_state_dict, _ = fluid.load_dygraph(params_file_path)
model.load_dict(model_state_dict)
model.eval()
acc_set = []
avg_loss_set = []
for idx, data in enumerate(model.valid_loader()):
usr, mov, score_label = data
usr_v = [dygraph.to_variable(var) for var in usr]
mov_v = [dygraph.to_variable(var) for var in mov]
_, _, scores_predict = model(usr_v, mov_v)
pred_scores = scores_predict.numpy()
avg_loss_set.append(np.mean(np.abs(pred_scores - score_label)))
diff = np.abs(pred_scores - score_label)
diff[diff>0.5] = 1
acc = 1 - np.mean(diff)
acc_set.append(acc)
return np.mean(acc_set), np.mean(avg_loss_set)
param_path = "./checkpoint/epoch"
for i in range(10):
acc, mae = evaluation(model, param_path+str(i))
print("ACC:", acc, "MAE:", mae)
- ACC: 0.2805188926366659 MAE: 0.7952824
- ACC: 0.2852882689390427 MAE: 0.7941532
- ACC: 0.2824734888015649 MAE: 0.79572767
- ACC: 0.2776615373599224 MAE: 0.80148673
- ACC: 0.2799660603205363 MAE: 0.8010404
- ACC: 0.2806148324257288 MAE: 0.8026996
- ACC: 0.2807383934656779 MAE: 0.80340725
- ACC: 0.2749944688417973 MAE: 0.80362296
- ACC: 0.280727839240661 MAE: 0.80528593
- ACC: 0.2924909143111645 MAE: 0.79743403
上述结果中,我们采用了ACC和MAE指标测试在验证集上的评分预测的准确性,其中ACC值越大越好,MAE值越小越好。
可以看到ACC和MAE的值不是很理想,但是这仅仅是对于评分预测不准确,不能直接衡量推荐结果的准确性。考虑到我们设计的神经网络是为了完成推荐任务而不是评分任务,所以总结一下:
1. 只针对预测评分任务来说,我们设计的神经网络结构和损失函数是不合理的,导致评分预测不理想;
2. 从损失函数的收敛可以知道网络的训练是有效的。评分预测的好坏不能反应推荐结果的好坏。
到这里,我们已经完成了推荐算法的前三步,包括:1. 数据的准备,2. 神经网络的设计,3. 神经网络的训练。
目前还需要完成剩余的两个步骤:1. 提取用户、电影数据的特征并保存到本地, 2. 利用保存的特征计算相似度矩阵,利用相似度完成推荐。
下面,我们利用训练的神经网络提取数据的特征,进而完成电影推荐,并观察推荐结果是否令人满意。