电影特征提取网络

接下来我们构建提取电影特征的神经网络,与用户特征网络结构不同的是,电影的名称和类别均有多个数字信息,我们构建网络时,对这两类特征的处理方式也不同。

电影特征提取网络 - 图1

电影特征网络主要包括:

  • 将电影ID数据映射为向量表示,通过全连接层得到ID特征。
  • 将电影类别数据映射为向量表示,对电影类别的向量求和得到类别特征。
  • 将电影名称数据映射为向量表示,通过卷积层计算得到名称特征。

1. 提取电影ID特征

与计算用户ID特征的方式类似,我们通过如下方式实现电影ID特性提取。根据上一节信息得知电影ID的最大值是3952。

  1. # 自定义一个电影ID数据
  2. mov_id_data = np.array((1, 2)).reshape(-1).astype('int64')
  3. with dygraph.guard():
  4. # 对电影ID信息做映射,并紧接着一个FC层
  5. MOV_DICT_SIZE = 3952 + 1
  6. mov_emb = Embedding([MOV_DICT_SIZE, 32])
  7. mov_fc = Linear(32, 32)
  8. print("输入的电影ID是:", mov_id_data)
  9. mov_id_data = dygraph.to_variable(mov_id_data)
  10. mov_id_feat = mov_fc(mov_emb(mov_id_data))
  11. mov_id_feat = fluid.layers.relu(mov_id_feat)
  12. print("计算的电影ID的特征是", mov_id_feat.numpy(), "\n其形状是:", mov_id_feat.shape)
  13. print("\n电影ID为 {} 计算得到的特征是:{}".format(mov_id_data.numpy()[0], mov_id_feat.numpy()[0]))
  14. print("电影ID为 {} 计算得到的特征是:{}".format(mov_id_data.numpy()[1], mov_id_feat.numpy()[1]))
  1. 输入的电影ID是: [1 2]
  2. 计算的电影ID的特征是 [[0.00380746 0. 0.00747952 0. 0.01460832 0.
  3. 0. 0.02644686 0.00881469 0.02714742 0. 0.
  4. 0. 0.00490084 0. 0.011464 0. 0.02438358
  5. 0. 0.05181156 0.00271468 0.02482769 0.00856254 0.
  6. 0. 0. 0. 0.00127167 0. 0.
  7. 0.01114944 0.00792265]
  8. [0.02830652 0. 0. 0.00845328 0.01861141 0.
  9. 0.05202583 0. 0.00567936 0.00591309 0.01148433 0.
  10. 0. 0.01830137 0.02531591 0.00357616 0. 0.
  11. 0.02856203 0. 0.01485681 0. 0.03657161 0.00311763
  12. 0.02794975 0.01535434 0. 0.01469669 0. 0.01319524
  13. 0.00011042 0. ]]
  14. 其形状是: [2, 32]
  15.  
  16. 电影ID 1 计算得到的特征是:[0.00380746 0. 0.00747952 0. 0.01460832 0.
  17. 0. 0.02644686 0.00881469 0.02714742 0. 0.
  18. 0. 0.00490084 0. 0.011464 0. 0.02438358
  19. 0. 0.05181156 0.00271468 0.02482769 0.00856254 0.
  20. 0. 0. 0. 0.00127167 0. 0.
  21. 0.01114944 0.00792265]
  22. 电影ID 2 计算得到的特征是:[0.02830652 0. 0. 0.00845328 0.01861141 0.
  23. 0.05202583 0. 0.00567936 0.00591309 0.01148433 0.
  24. 0. 0.01830137 0.02531591 0.00357616 0. 0.
  25. 0.02856203 0. 0.01485681 0. 0.03657161 0.00311763
  26. 0.02794975 0.01535434 0. 0.01469669 0. 0.01319524
  27. 0.00011042 0. ]

2. 提取电影类别特征

与电影ID数据不同的是,每个电影有多个类别,提取类别特征时,如果对每个类别数据都使用一个全连接层,电影最多的类别数是6,会导致类别特征提取网络参数过多而不利于学习。我们对于电影类别特征提取的处理方式是:

  • 通过Embedding网络层将电影类别数字映射为特征向量;
  • 对Embedding后的向量沿着类别数量维度进行求和,得到一个类别映射向量;
  • 通过一个全连接层计算类别特征向量。

数据处理章节已经介绍到,每个电影的类别数量是不固定的,且一个电影最大的类别数量是6,类别数量不足6的通过补0到6维。因此,每个类别的数据维度是6,每个电影类别有6个Embedding向量。我们希望用一个向量就可以表示电影类别,可以对电影类别数量维度降维, 这里对6个Embedding向量通过求和的方式降维,得到电影类别的向量表示。

下面是电影类别特征提取的实现方法:

  1. # 自定义一个电影类别数据
  2. mov_cat_data = np.array(((1, 2, 3, 0, 0, 0), (2, 3, 4, 0, 0, 0))).reshape(2, -1).astype('int64')
  3. with dygraph.guard():
  4. # 对电影ID信息做映射,并紧接着一个Linear层
  5. MOV_DICT_SIZE = 6 + 1
  6. mov_emb = Embedding([MOV_DICT_SIZE, 32])
  7. mov_fc = Linear(32, 32)
  8. print("输入的电影类别是:", mov_cat_data[:, :])
  9. mov_cat_data = dygraph.to_variable(mov_cat_data)
  10. # 1. 通过Embedding映射电影类别数据;
  11. mov_cat_feat = mov_emb(mov_cat_data)
  12. # 2. 对Embedding后的向量沿着类别数量维度进行求和,得到一个类别映射向量;
  13. mov_cat_feat = fluid.layers.reduce_sum(mov_cat_feat, dim=1, keep_dim=False)
  14. # 3. 通过一个全连接层计算类别特征向量。
  15. mov_cat_feat = mov_fc(mov_cat_feat)
  16. mov_cat_feat = fluid.layers.relu(mov_cat_feat)
  17. print("计算的电影类别的特征是", mov_cat_feat.numpy(), "\n其形状是:", mov_cat_feat.shape)
  18. print("\n电影类别为 {} 计算得到的特征是:{}".format(mov_cat_data.numpy()[0, :], mov_cat_feat.numpy()[0]))
  19. print("\n电影类别为 {} 计算得到的特征是:{}".format(mov_cat_data.numpy()[1, :], mov_cat_feat.numpy()[1]))
  1. 输入的电影类别是: [[1 2 3 0 0 0]
  2. [2 3 4 0 0 0]]
  3. 计算的电影类别的特征是 [[0.90278137 0. 0.94548154 0. 0.7049405 0.
  4. 0.27492756 0.03842919 0.9897252 1.01082 0. 0.3386654
  5. 0.18409352 0.82094765 0.5298293 0. 0.2218847 0.
  6. 0. 0. 1.3233504 0.04408928 1.1701669 0.2378062
  7. 0. 0. 1.1962037 0.7447211 0. 0.
  8. 0. 0. ]
  9. [1.0059301 0. 0.8874374 0. 0.65209347 0.
  10. 1.2931696 0.31240582 0.87398815 0.78633493 0. 0.76689285
  11. 0.41179708 0.46684998 0.26156023 0. 0.3482998 0.
  12. 0. 0. 1.332893 0. 1.0292114 0.43722948
  13. 0. 0.08801231 0.31832567 0.30345434 0.5541737 0.
  14. 0. 0. ]]
  15. 其形状是: [2, 32]
  16.  
  17. 电影类别为 [1 2 3 0 0 0] 计算得到的特征是:[0.90278137 0. 0.94548154 0. 0.7049405 0.
  18. 0.27492756 0.03842919 0.9897252 1.01082 0. 0.3386654
  19. 0.18409352 0.82094765 0.5298293 0. 0.2218847 0.
  20. 0. 0. 1.3233504 0.04408928 1.1701669 0.2378062
  21. 0. 0. 1.1962037 0.7447211 0. 0.
  22. 0. 0. ]
  23.  
  24. 电影类别为 [2 3 4 0 0 0] 计算得到的特征是:[1.0059301 0. 0.8874374 0. 0.65209347 0.
  25. 1.2931696 0.31240582 0.87398815 0.78633493 0. 0.76689285
  26. 0.41179708 0.46684998 0.26156023 0. 0.3482998 0.
  27. 0. 0. 1.332893 0. 1.0292114 0.43722948
  28. 0. 0.08801231 0.31832567 0.30345434 0.5541737 0.
  29. 0. 0. ]

因为待合并的6个向量具有相同的维度,所以直接按位相加即可得到综合的向量表示。当然,我们也可以采用向量级联的方式,将6个32维的向量级联成192维的向量,再通过全连接层压缩成32维度,代码实现上要臃肿一些。

3. 提取电影名称特征

与电影类别数据一样,每个电影名称具有多个单词。 我们对于电影名称特征提取的处理方式是:

  • 通过Embedding映射电影名称数据,得到对应的特征向量;
  • 对Embedding后的向量使用卷积层+全连接层进一步提取特征;
  • 对特征进行降采样,降低数据维度;

提取电影名称特征时,使用了卷积层加全连接层的方式提取特征。这是因为电影名称单词较多,电影名称的最大单词数量是15,如果采用和电影类别同样的处理方式,即沿着数量维度求和,显然会损失很多信息,考虑到15这个维度较高,可以使用卷积层进一步提取特征,同时通过控制卷积层的步长,降低电影名称特征的维度。

只是简单的经过一两层卷积全连接层后,特征的维度依然很大,为了得到更低维度的特征向量,有两种方式,一种是利用求和降采样的方式,另一种是继续使用神经网络层进行特征提取并逐渐降低特征维度。这里,我们采用“简单求和”的降采样方式,来降低电影名称特征的维度,通过飞桨的reduce_sum API实现。

下面是提取电影名称特征的代码实现:

  1. # 自定义两个电影名称数据
  2. mov_title_data = np.array(((1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
  3. (2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))).reshape(2, 1, 15).astype('int64')
  4. with dygraph.guard():
  5. # 对电影名称做映射,紧接着FC和pool层
  6. MOV_TITLE_DICT_SIZE = 1000 + 1
  7. mov_title_emb = Embedding([MOV_TITLE_DICT_SIZE, 32], is_sparse=False)
  8. mov_title_conv = Conv2D(1, 1, filter_size=(3, 1), stride=(2, 1), padding=0, act='relu')
  9. # 使用 3 * 3卷积层代替全连接层
  10. mov_title_conv2 = Conv2D(1, 1, filter_size=(3, 1), stride=1, padding=0, act='relu')
  11. mov_title_data = dygraph.to_variable(mov_title_data)
  12. print("电影名称数据的输入形状: ", mov_title_data.shape)
  13. # 1. 通过Embedding映射电影名称数据;
  14. mov_title_feat = mov_title_emb(mov_title_data)
  15. print("输入通过Embedding层的输出形状: ", mov_title_feat.shape)
  16. # 2. 对Embedding后的向量使用卷积层进一步提取特征;
  17. mov_title_feat = mov_title_conv(mov_title_feat)
  18. print("第一次卷积之后的特征输出形状: ", mov_title_feat.shape)
  19. mov_title_feat = mov_title_conv2(mov_title_feat)
  20. print("第二次卷积之后的特征输出形状: ", mov_title_feat.shape)
  21. batch_size = mov_title_data.shape[0]
  22. # 3. 最后对特征进行降采样,;
  23. mov_title_feat = fluid.layers.reduce_sum(mov_title_feat, dim=2, keep_dim=False)
  24. print("reduce_sum降采样后的特征输出形状: ", mov_title_feat.shape)
  25. mov_title_feat = fluid.layers.relu(mov_title_feat)
  26. mov_title_feat = fluid.layers.reshape(mov_title_feat, [batch_size, -1])
  27. print("电影名称特征的最终特征输出形状:", mov_title_feat.shape)
  28. print("\n计算的电影名称的特征是", mov_title_feat.numpy(), "\n其形状是:", mov_title_feat.shape)
  29. print("\n电影名称为 {} 计算得到的特征是:{}".format(mov_title_data.numpy()[0,:, 0], mov_title_feat.numpy()[0]))
  30. print("\n电影名称为 {} 计算得到的特征是:{}".format(mov_title_data.numpy()[1,:, 0], mov_title_feat.numpy()[1]))
  1. 电影名称数据的输入形状: [2, 1, 15]
  2. 输入通过Embedding层的输出形状: [2, 1, 15, 32]
  3. 第一次卷积之后的特征输出形状: [2, 1, 7, 32]
  4. 第二次卷积之后的特征输出形状: [2, 1, 5, 32]
  5. reduce_sum降采样后的特征输出形状: [2, 1, 32]
  6. 电影名称特征的最终特征输出形状: [2, 32]
  7.  
  8. 计算的电影名称的特征是 [[0.0320248 0.03832126 0. 0. 0. 0.
  9. 0. 0.01488265 0. 0. 0. 0.
  10. 0. 0.03727353 0.02013248 0. 0. 0.01441978
  11. 0.00365456 0. 0.00116357 0.00783006 0. 0.
  12. 0. 0. 0. 0. 0. 0.0047379
  13. 0.02190487 0. ]
  14. [0.02940748 0.03069749 0. 0. 0. 0.
  15. 0. 0.01248995 0. 0.0015157 0. 0.
  16. 0. 0.05366978 0. 0. 0. 0.0243385
  17. 0. 0. 0.00031154 0.00477934 0. 0.
  18. 0. 0. 0. 0. 0. 0.
  19. 0.00916854 0. ]]
  20. 其形状是: [2, 32]
  21.  
  22. 电影名称为 [1] 计算得到的特征是:[0.0320248 0.03832126 0. 0. 0. 0.
  23. 0. 0.01488265 0. 0. 0. 0.
  24. 0. 0.03727353 0.02013248 0. 0. 0.01441978
  25. 0.00365456 0. 0.00116357 0.00783006 0. 0.
  26. 0. 0. 0. 0. 0. 0.0047379
  27. 0.02190487 0. ]
  28.  
  29. 电影名称为 [2] 计算得到的特征是:[0.02940748 0.03069749 0. 0. 0. 0.
  30. 0. 0.01248995 0. 0.0015157 0. 0.
  31. 0. 0.05366978 0. 0. 0. 0.0243385
  32. 0. 0. 0.00031154 0.00477934 0. 0.
  33. 0. 0. 0. 0. 0. 0.
  34. 0.00916854 0. ]

上述代码中,通过Embedding层已经获得了维度是[batch, 1, 15, 32]电影名称特征向量,因此,该特征可以视为是通道数量为1的特征图,很适合使用卷积层进一步提取特征。这里我们使用两个3 x 1大小的卷积核的卷积层提取特征,输出通道保持不变,仍然是1。特征维度中15是电影名称数量的维度,使用3 x 1的卷积核,由于卷积感受野的原因,进行卷积时会综合多个名称的特征,同时设置卷积的步长参数stride为(2, 1),即可对名称数量维度降维,且保持每个名称的向量长度不变,防止过度压缩每个名称特征的信息。

从输出结果来看,第一个卷积层之后的输出特征维度依然较大,可以使用第二个卷积层进一步提取特征。获得第二个卷积的特征后,特征的维度已经从7 x 32,降低到了5 x 32,因此可以直接使用求和(向量按位相加)的方式沿着电影名称维度进行降采样(532 -> 132),得到最终的电影名称特征向量。

需要注意的是,降采样后的数据尺寸依然比下一层要求的输入向量多出一维 [2, 1, 32],所以最终输出前需调整下形状。

4. 融合电影特征

与用户特征融合方式相同,电影特征融合采用特征级联加全连接层的方式,将电影特征用一个200维的向量表示。

  1. with dygraph.guard():
  2. mov_combined = Linear(96, 200, act='tanh')
  3. # 收集所有的用户特征
  4. _features = [mov_id_feat, mov_cat_feat, mov_title_feat]
  5. _features = [k.numpy() for k in _features]
  6. _features = [dygraph.to_variable(k) for k in _features]
  7. # 对特征沿着最后一个维度级联
  8. mov_feat = fluid.layers.concat(input=_features, axis=1)
  9. mov_feat = mov_combined(mov_feat)
  10. print("用户融合后特征的维度是:", mov_feat.shape)
  1. 用户融合后特征的维度是: [2, 200]

至此已经完成了电影特征提取的网络设计,包括电影ID特征提取、电影类别特征提取、电影名称特征提取。

下面将这些模块整合到一个Python类中,完整代码如下:

  1. class MovModel(dygraph.layers.Layer):
  2. def __init__(self, use_poster, use_mov_title, use_mov_cat, use_age_job):
  3. super(MovModel, self).__init__()
  4. # 将传入的name信息和bool型参数添加到模型类中
  5. self.use_mov_poster = use_poster
  6. self.use_mov_title = use_mov_title
  7. self.use_usr_age_job = use_age_job
  8. self.use_mov_cat = use_mov_cat
  9. # 获取数据集的信息,并构建训练和验证集的数据迭代器
  10. Dataset = MovieLen(self.use_mov_poster)
  11. self.Dataset = Dataset
  12. self.trainset = self.Dataset.train_dataset
  13. self.valset = self.Dataset.valid_dataset
  14. self.train_loader = self.Dataset.load_data(dataset=self.trainset, mode='train')
  15. self.valid_loader = self.Dataset.load_data(dataset=self.valset, mode='valid')
  16. """ define network layer for embedding usr info """
  17. # 对电影ID信息做映射,并紧接着一个Linear层
  18. MOV_DICT_SIZE = Dataset.max_mov_id + 1
  19. self.mov_emb = Embedding([MOV_DICT_SIZE, 32])
  20. self.mov_fc = Linear(32, 32)
  21. # 对电影类别做映射
  22. CATEGORY_DICT_SIZE = len(Dataset.movie_cat) + 1
  23. self.mov_cat_emb = Embedding([CATEGORY_DICT_SIZE, 32], is_sparse=False)
  24. self.mov_cat_fc = Linear(32, 32)
  25. # 对电影名称做映射
  26. MOV_TITLE_DICT_SIZE = len(Dataset.movie_title) + 1
  27. self.mov_title_emb = Embedding([MOV_TITLE_DICT_SIZE, 32], is_sparse=False)
  28. self.mov_title_conv = Conv2D(1, 1, filter_size=(3, 1), stride=(2,1), padding=0, act='relu')
  29. self.mov_title_conv2 = Conv2D(1, 1, filter_size=(3, 1), stride=1, padding=0, act='relu')
  30. # 新建一个Linear层,用于整合电影特征
  31. self.mov_concat_embed = Linear(96, 200, act='tanh')
  32. # 定义电影特征的前向计算过程
  33. def get_mov_feat(self, mov_var):
  34. """ get movie features"""
  35. # 获得电影数据
  36. mov_id, mov_cat, mov_title, mov_poster = mov_var
  37. feats_collect = []
  38. # 获得batchsize的大小
  39. batch_size = mov_id.shape[0]
  40. # 计算电影ID的特征,并存在feats_collect中
  41. mov_id = self.mov_emb(mov_id)
  42. mov_id = self.mov_fc(mov_id)
  43. mov_id = fluid.layers.relu(mov_id)
  44. feats_collect.append(mov_id)
  45. # 如果使用电影的种类数据,计算电影种类特征的映射
  46. if self.use_mov_cat:
  47. # 计算电影种类的特征映射,对多个种类的特征求和得到最终特征
  48. mov_cat = self.mov_cat_emb(mov_cat)
  49. print(mov_title.shape)
  50. mov_cat = fluid.layers.reduce_sum(mov_cat, dim=1, keep_dim=False)
  51. mov_cat = self.mov_cat_fc(mov_cat)
  52. feats_collect.append(mov_cat)
  53. if self.use_mov_title:
  54. # 计算电影名字的特征映射,对特征映射使用卷积计算最终的特征
  55. mov_title = self.mov_title_emb(mov_title)
  56. mov_title = self.mov_title_conv2(self.mov_title_conv(mov_title))
  57. mov_title = fluid.layers.reduce_sum(mov_title, dim=2, keep_dim=False)
  58. mov_title = fluid.layers.relu(mov_title)
  59. mov_title = fluid.layers.reshape(mov_title, [batch_size, -1])
  60. feats_collect.append(mov_title)
  61. # 使用一个全连接层,整合所有电影特征,映射为一个200维的特征向量
  62. mov_feat = fluid.layers.concat(feats_collect, axis=1)
  63. mov_feat = self.mov_concat_embed(mov_feat)
  64. return mov_feat

由上述电影特征处理的代码可以观察到:

  • 电影ID特征的计算方式和用户ID的计算方式相同。
  • 对于包含多个元素的电影类别数据,采用将所有元素的映射向量求和的结果作为最终的电影类别特征表示。考虑到电影类别的数量有限,这里采用简单的求和特征融合方式。
  • 对于电影的名称数据,其包含的元素数量多于电影种类元素数量,则采用卷积计算的方式,之后再将计算的特征沿着数据维度进行求和。读者也可自行设计这部分特征计算网络,并观察最终训练结果。

下面使用定义好的数据读取器,实现从电影数据中得到电影特征的计算流程:

  1. ## 测试电影特征提取网络
  2. with dygraph.guard():
  3. model = MovModel("Mov", use_poster=False, use_mov_title=True, use_mov_cat=True, use_age_job=True)
  4. model.eval()
  5. data_loader = model.train_loader
  6. for idx, data in enumerate(data_loader()):
  7. # 获得数据,并转为动态图格式,
  8. usr, mov, score = data
  9. # 只使用每个Batch的第一条数据
  10. mov_v = [var[0:1] for var in mov]
  11. _mov_v = [np.squeeze(var[0:1]) for var in mov]
  12. print("输入的电影ID数据:{}\n类别数据:{} \n名称数据:{} ".format(*_mov_v))
  13. mov_v = [dygraph.to_variable(var) for var in mov_v]
  14. mov_feat = model.get_mov_feat(mov_v)
  15. print("计算得到的电影特征维度是:", mov_feat.shape)
  16. break
  1. ##Total dataset instances: 1000209
  2. ##MovieLens dataset information:
  3. usr num: 6040
  4. movies num: 3883
  5. 输入的电影ID数据:2716
  6. 类别数据:[ 3 11 0 0 0 0]
  7. 名称数据:[3838 0 0 0 0 0 0 0 0 0 0 0 0 0
  8. 0]
  9. [1, 1, 15]
  10. 计算得到的电影特征维度是: [1, 200]