现实中的特征交叉数据往往非常复制且非线性,并不仅限于二者的交互,分解机通常用于二阶特征交互,理论上将分解机也可以对更高阶进行建模,但是由于数值不稳定和计算复杂性高通常不使用。为了处理高级的交互关系,一个做法是将分解机和深度神经网络结合,用深度神经网可以应对非线性的情况,本篇介绍一个称为深度分解机器(DeepFM)的代表性模型,该模型将FM和深度神经网络相结合。

1. 模型架构

DeepFM 有并行的 FM组件以及deep组件组成。 FM和用于二阶特征交互的 2-way 分解机相同。 deep 模块是一个多层感知器,用于捕获高阶特征相互作用和非线性关系。 这两个分量共享相同的输入/嵌入并且在最终预测的时候将输出汇总。值得一提的是,DeepFM的理念类似于可同时记忆和概括的Wide&Deep架构。DeepFM与Wide&Deep模型相比的优势在于,它可以通过自动识别特征组合来减少手工特征工程的工作量。

为了简洁起见,我们省略了FM 的描述,并将FM的输出表示为 Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图1%7D#card=math&code=%5Chat%7By%7D%5E%7B%28FM%29%7D)。让 Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图2 表示第 Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图3field的潜在特征向量。 deep组件的输入是使用稀疏分类特征输入查找的所有字段的密集嵌入的串联,表示为:

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图4%7D%20%20%3D%20%5B%5Cmathbf%7Be%7D_1%2C%20%5Cmathbf%7Be%7D_2%2C%20…%2C%20%5Cmathbf%7Be%7D_f%5D%2C%0A#card=math&code=%5Cmathbf%7Bz%7D%5E%7B%280%29%7D%20%20%3D%20%5B%5Cmathbf%7Be%7D_1%2C%20%5Cmathbf%7Be%7D_2%2C%20…%2C%20%5Cmathbf%7Be%7D_f%5D%2C%0A)

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图5 是field数。 然后将其馈入以下神经网络:

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图6%7D%20%20%3D%20%5Calpha(%5Cmathbf%7BW%7D%5E%7B(l)%7D%5Cmathbf%7Bz%7D%5E%7B(l-1)%7D%20%2B%20%5Cmathbf%7Bb%7D%5E%7B(l)%7D)%2C%0A#card=math&code=%5Cmathbf%7Bz%7D%5E%7B%28l%29%7D%20%20%3D%20%5Calpha%28%5Cmathbf%7BW%7D%5E%7B%28l%29%7D%5Cmathbf%7Bz%7D%5E%7B%28l-1%29%7D%20%2B%20%5Cmathbf%7Bb%7D%5E%7B%28l%29%7D%29%2C%0A)

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图7 是激活功能。 Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图8Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图9Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图10 层的权重和偏差。让 Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图11 表示预测的输出。DeepFM的最总预测是 FM 和 DNN输出的整合:

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图12%7D%20%2B%20%5Chat%7By%7D%5E%7B(DNN)%7D)%2C%0A#card=math&code=%5Chat%7By%7D%20%3D%20%5Csigma%28%5Chat%7By%7D%5E%7B%28FM%29%7D%20%2B%20%5Chat%7By%7D%5E%7B%28DNN%29%7D%29%2C%0A)

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图13 是 sigmoid 函数。 DeepFM 模型结构见下图:

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图14

DeepFM不是将深度神经网络与FM相结合的唯一方法。我们还可以在特征相互作用上添加非线性层

  1. from collections import defaultdict
  2. from d2l import mxnet as d2l
  3. from mxnet import gluon, np,npx,init,autograd
  4. from mxnet.gluon import nn
  5. from plotly import graph_objs as go
  6. import pandas as pd
  7. import sys
  8. import os
  9. npx.set_np()

2. DeepFM模型实现

DeepFM的实现与FM相似。我们保持FM部分不变,并使用MLP模块relu作为激活功能。Dropout也用于规范化模型。可以使用mlp_dims超参数调整MLP的神经元数量。

  1. class DeepFM(nn.Block):
  2. def __init__(self, field_dims, num_factors, mlp_dims, drop_rate=0.1):
  3. super(DeepFM, self).__init__()
  4. num_inputs = int(sum(field_dims))
  5. self.embedding = nn.Embedding(num_inputs, num_factors)
  6. self.fc = nn.Embedding(num_inputs, 1)
  7. self.linear_layer = nn.Dense(1, use_bias=True)
  8. input_dim = self.embed_output_dim = len(field_dims) * num_factors
  9. self.mlp = nn.Sequential()
  10. for dim in mlp_dims:
  11. self.mlp.add(nn.Dense(dim, 'relu', True, in_units=input_dim))
  12. self.mlp.add(nn.Dropout(rate=drop_rate))
  13. input_dim = dim
  14. self.mlp.add(nn.Dense(in_units=input_dim, units=1))
  15. def forward(self, x):
  16. embed_x = self.embedding(x)
  17. square_of_sum = np.sum(embed_x, axis=1) ** 2
  18. sum_of_square = np.sum(embed_x ** 2, axis=1)
  19. inputs = np.reshape(embed_x, (-1, self.embed_output_dim))
  20. x = self.linear_layer(self.fc(x).sum(1)) \
  21. + 0.5 * (square_of_sum - sum_of_square).sum(1, keepdims=True) \
  22. + self.mlp(inputs)
  23. x = npx.sigmoid(x)
  24. return x

3. 加载数据

使用之前的广告数据。

  1. batch_size = 2048
  2. data_dir = d2l.download_extract('ctr')
  3. train_data = d2l.CTRDataset(os.path.join(data_dir, 'train.csv'))
  4. test_data = d2l.CTRDataset(os.path.join(data_dir, 'test.csv'),
  5. feat_mapper=train_data.feat_mapper,
  6. defaults=train_data.defaults)
  7. field_dims = train_data.field_dims
  8. train_iter = gluon.data.DataLoader(
  9. train_data, shuffle=True, last_batch='rollover', batch_size=batch_size,
  10. num_workers=d2l.get_dataloader_workers())
  11. test_iter = gluon.data.DataLoader(
  12. test_data, shuffle=False, last_batch='rollover', batch_size=batch_size,
  13. num_workers=d2l.get_dataloader_workers())

4. 训练

同样使用之前的训练函数进行训练。

  1. def accuracy(y_hat, y):
  2. if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
  3. y_hat = y_hat.argmax(axis=1)
  4. cmp = y_hat.astype(y.dtype) == y
  5. return float(cmp.sum())
  6. def train_batch(net, features, labels, loss, trainer, devices, split_f=d2l.split_batch):
  7. X_shards, y_shards = split_f(features, labels, devices)
  8. with autograd.record():
  9. pred_shards = [net(X_shard) for X_shard in X_shards]
  10. ls = [loss(pred_shard, y_shard) for pred_shard, y_shard
  11. in zip(pred_shards, y_shards)]
  12. for l in ls:
  13. l.backward()
  14. # ignore_stale_grad代表可以使用就得梯度参数
  15. trainer.step(labels.shape[0], ignore_stale_grad=True)
  16. train_loss_sum = sum([float(l.sum()) for l in ls])
  17. train_acc_sum = sum(accuracy(pred_shard, y_shard)
  18. for pred_shard, y_shard in zip(pred_shards, y_shards))
  19. return train_loss_sum, train_acc_sum
  20. def train(net, train_iter, test_iter, loss, trainer, num_epochs,
  21. devices=d2l.try_all_gpus(), split_f=d2l.split_batch):
  22. num_batches, timer = len(train_iter), d2l.Timer()
  23. epochs_lst, loss_lst, train_acc_lst, test_acc_lst = [],[],[],[]
  24. for epoch in range(num_epochs):
  25. metric = d2l.Accumulator(4)
  26. for i, (features, labels) in enumerate(train_iter):
  27. timer.start()
  28. l, acc = train_batch(
  29. net, features, labels, loss, trainer, devices, split_f)
  30. metric.add(l, acc, labels.shape[0], labels.size)
  31. timer.stop()
  32. if (i + 1) % (num_batches // 5) == 0:
  33. epochs_lst.append(epoch + i / num_batches)
  34. loss_lst.append(metric[0] / metric[2])
  35. train_acc_lst.append(metric[1] / metric[3])
  36. test_acc_lst.append(d2l.evaluate_accuracy_gpus(net, test_iter, split_f))
  37. if((epoch+1)%5==0):
  38. print(f"[epock {epoch+1}] train loss: {metric[0] / metric[2]:.3f} train acc: {metric[1] / metric[3]:.3f}",
  39. f" test_loss: {test_acc_lst[-1]:.3f}")
  40. print(f'loss {metric[0] / metric[2]:.3f}, train acc '
  41. f'{metric[1] / metric[3]:.3f}, test acc {test_acc_lst[-1]:.3f}')
  42. print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
  43. f'{str(devices)}')
  44. fig = go.Figure()
  45. fig.add_trace(go.Scatter(x=epochs_lst, y=loss_lst, name='train loss'))
  46. fig.add_trace(go.Scatter(x=epochs_lst, y=train_acc_lst, name='train acc'))
  47. fig.add_trace(go.Scatter(x=list(range(1,len(test_acc_lst)+1)), y=test_acc_lst, name='test acc'))
  48. fig.update_layout(width=580, height=400, xaxis_title='epoch', yaxis_range=[0, 1])
  49. fig.show()
  50. def train_fine_tuning(net, learning_rate, batch_size=64, num_epochs=5):
  51. train_iter = gluon.data.DataLoader(train_imgs.transform_first(train_augs), batch_size, shuffle=True)
  52. test_iter = gluon.data.DataLoader(test_imgs.transform_first(test_augs), batch_size)
  53. net.collect_params().reset_ctx(npx.gpu())
  54. net.hybridize()
  55. loss = gluon.loss.SoftmaxCrossEntropyLoss()
  56. trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': learning_rate, 'wd': 0.001})
  57. train(net, train_iter, test_iter, loss, trainer, num_epochs, [npx.gpu()])

我们将DeepFM的MLP组件设置为具有金字塔结构(30-20-10)的三层密集网络。所有其他超参数与FM相同。与FM相比,DeepFM收敛更快,并且性能更高。

devices = d2l.try_all_gpus()
net = DeepFM(field_dims, num_factors=10, mlp_dims=[30, 20, 10])
net.initialize(init.Xavier(), ctx=devices)
lr, num_epochs, optimizer = 0.01, 30, 'adam'
trainer = gluon.Trainer(net.collect_params(), optimizer,
                        {'learning_rate': lr})
loss = gluon.loss.SigmoidBinaryCrossEntropyLoss()
train(net, train_iter, test_iter, loss, trainer, num_epochs, devices)

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图15

Mxnet (47): 深度分解机(DeepFM)处理广告推荐 - 图16

5.参考

https://d2l.ai/chapter_recommender-systems/deepfm.html

6.代码

github