用户特征提取网络

理解Embedding后,我们就可以开始构建提取用户特征的神经网络了。

用户特征提取网络 - 图1

用户特征网络主要包括:

  • 将用户ID数据映射为向量表示,通过全连接层得到ID特征。
  • 将用户性别数据映射为向量表示,通过全连接层得到性别特征。
  • 将用户职业数据映射为向量表示,通过全连接层得到职业特征。
  • 将用户年龄数据影射喂向量表示,通过全连接层得到年龄特征。
  • 融合ID、性别、职业、年龄特征,得到用户的特征表示。

在用户特征计算网络中,我们对每个用户数据做embedding处理,然后经过一个全连接层,激活函数使用ReLU,得到用户所有特征后,将特征整合,经过一个全连接层得到最终的用户数据特征,该特征的维度是200维,用于和电影特征计算相似度。

1. 提取用户ID特征

开始构建用户ID的特征提取网络,ID特征提取包括两个部分,首先,使用Embedding将用户ID映射为向量,然后,使用一层全连接层和relu激活函数进一步提取用户ID特征。 相比较于电影类别、电影名称,用户ID只包含一个数字,数据更为简单。这里需要考虑将用户ID映射为多少维度的向量合适,使用维度过大的向量表示用户ID容易造成信息冗余,维度过低又不足以表示该用户的特征。理论上来说,如果使用二进制表示用户ID,用户最大ID是6040,小于2的13次方,因此,理论上使用13维度的向量已经足够了,为了让不同ID的向量更具区分性,我们选择将用户ID映射为维度为32维的向量。

下面是用户ID特征提取代码实现:

  1. # 自定义一个用户ID数据
  2. usr_id_data = np.random.randint(0, 6040, (2)).reshape((-1)).astype('int64')
  3. print("输入的用户ID是:", usr_id_data)
  4. # 创建飞桨动态图的工作空间
  5. with dygraph.guard():
  6. USR_ID_NUM = 6040 + 1
  7. # 定义用户ID的embedding层和fc层
  8. usr_emb = Embedding([USR_ID_NUM, 32], is_sparse=False)
  9. usr_fc = Linear(input_dim=32, output_dim=32)
  10. usr_id_var = dygraph.to_variable(usr_id_data)
  11. usr_id_feat = usr_fc(usr_emb(usr_id_var))
  12. usr_id_feat = fluid.layers.relu(usr_id_feat)
  13. print("用户ID的特征是:", usr_id_feat.numpy(), "\n其形状是:", usr_id_feat.shape)
  1. 输入的用户ID是: [3511 4125]
  2. 用户ID的特征是: [[0.01574198 0. 0. 0. 0.02548438 0.01829206
  3. 0. 0.00267444 0.03974488 0. 0.0125479 0.01635006
  4. 0. 0.01348757 0.00099145 0.00921841 0.02927484 0.0277753
  5. 0.02781798 0. 0.00259031 0. 0.00221091 0.
  6. 0. 0. 0. 0. 0. 0.
  7. 0.01300182 0.02627602]
  8. [0. 0.01970843 0.00200395 0. 0.02862134 0.
  9. 0. 0.01341773 0.01240196 0. 0.03665136 0.02436131
  10. 0. 0.02451975 0. 0.00382315 0. 0.
  11. 0.01831124 0. 0. 0. 0.00175647 0.0095302
  12. 0.00249144 0. 0.00717024 0. 0. 0.
  13. 0. 0.0216652 ]]
  14. 其形状是: [2, 32]

注意到,将用户ID映射为one-hot向量时,Embedding层参数size的第一个参数是,在用户的最大ID基础上加上1。原因很简单,从上一节数据处理已经发现,用户ID是从1开始计数的,最大的用户ID是6040。并且已经知道通过Embedding映射输入数据时,是先把输入数据转换成one-hot向量。向量中只有一个 1 的向量才被称为one-hot向量,比如,0 用四维的on-hot向量表示是[1, 0 ,0 ,0],同时,4维的one-hot向量最大只能表示3。所以,要把数字6040用one-hot向量表示,至少需要用6041维度的向量。

接下来我们会看到,类似的Embeding层也适用于处理用户性别、年龄和职业,以及电影ID等特征,实现代码均是类似的。

2. 提取用户性别特征

接下来构建用户性别的特征提取网络,同用户ID特征提取步骤,使用Embedding层和全连接层提取用户性别特征。用户性别不同于用户ID数据具备数千数万种不同数据,性别只有两种可能,不需要使用高维度的向量表示用户性别特征,这里我们将用户性别用为16维的向量表示。

下面是用户性别特征提取实现:

  1. # 自定义一个用户性别数据
  2. usr_gender_data = np.array((0, 1)).reshape(-1).astype('int64')
  3. print("输入的用户性别是:", usr_gender_data)
  4. # 创建飞桨动态图的工作空间
  5. with dygraph.guard():
  6. # 用户的性别用0, 1 表示
  7. # 性别最大ID是1,所以Embedding层size的第一个参数设置为1 + 1 = 2
  8. USR_ID_NUM = 2
  9. # 对用户性别信息做映射,并紧接着一个FC层
  10. USR_GENDER_DICT_SIZE = 2
  11. usr_gender_emb = Embedding([USR_GENDER_DICT_SIZE, 16])
  12. usr_gender_fc = Linear(input_dim=16, output_dim=16)
  13. usr_gender_var = dygraph.to_variable(usr_gender_data)
  14. usr_gender_feat = usr_gender_fc(usr_gender_emb(usr_gender_var))
  15. usr_gender_feat = fluid.layers.relu(usr_gender_feat)
  16. print("用户性别特征的数据特征是:", usr_gender_feat.numpy(), "\n其形状是:", usr_gender_feat.shape)
  17. print("\n性别 0 对应的特征是:", usr_gender_feat.numpy()[0, :])
  18. print("性别 1 对应的特征是:", usr_gender_feat.numpy()[1, :])
  1. 输入的用户性别是: [0 1]
  2. 用户性别特征的数据特征是: [[0.023137 0. 0. 0.05907416 0.1018934 0.
  3. 0. 0.00924867 0.32423887 0.27837008 0.5539641 0.
  4. 0. 0. 0. 0.09197924]
  5. [0.23991962 0.25170493 0. 0.47101367 0.0232828 0.
  6. 0. 0.0052276 0. 0.10090257 0.4415601 0.
  7. 0. 0.17549343 0.29906213 0.15026219]]
  8. 其形状是: [2, 16]
  9.  
  10. 性别 0 对应的特征是: [0.023137 0. 0. 0.05907416 0.1018934 0.
  11. 0. 0.00924867 0.32423887 0.27837008 0.5539641 0.
  12. 0. 0. 0. 0.09197924]
  13. 性别 1 对应的特征是: [0.23991962 0.25170493 0. 0.47101367 0.0232828 0.
  14. 0. 0.0052276 0. 0.10090257 0.4415601 0.
  15. 0. 0.17549343 0.29906213 0.15026219]

3. 提取用户年龄特征

然后构建用户年龄的特征提取网络,同样采用Embedding层和全连接层的方式提取特征。

前面我们了解到年龄数据分布是:

  • 1: “Under 18”
  • 18: “18-24”
  • 25: “25-34”
  • 35: “35-44”
  • 45: “45-49”
  • 50: “50-55”
  • 56: “56+”

得知用户年龄最大值为56,这里仍将用户年龄用16维的向量表示。

  1. # 自定义一个用户年龄数据
  2. usr_age_data = np.array((1, 18)).reshape(-1).astype('int64')
  3. print("输入的用户年龄是:", usr_age_data)
  4. # 创建飞桨动态图的工作空间
  5. with dygraph.guard():
  6. # 对用户年龄信息做映射,并紧接着一个Linear层
  7. # 年龄的最大ID是56,所以Embedding层size的第一个参数设置为56 + 1 = 57
  8. USR_AGE_DICT_SIZE = 56 + 1
  9. usr_age_emb = Embedding([USR_AGE_DICT_SIZE, 16])
  10. usr_age_fc = Linear(input_dim=16, output_dim=16)
  11. usr_age = dygraph.to_variable(usr_age_data)
  12. usr_age_feat = usr_age_emb(usr_age)
  13. usr_age_feat = usr_age_fc(usr_age_feat)
  14. usr_age_feat = fluid.layers.relu(usr_age_feat)
  15. print("用户年龄特征的数据特征是:", usr_age_feat.numpy(), "\n其形状是:", usr_age_feat.shape)
  16. print("\n年龄 1 对应的特征是:", usr_age_feat.numpy()[0, :])
  17. print("年龄 18 对应的特征是:", usr_age_feat.numpy()[1, :])
  1. 输入的用户年龄是: [ 1 18]
  2. 用户年龄特征的数据特征是: [[0. 0.06744663 0.07139666 0.22798921 0.00418518 0.11958582
  3. 0. 0.0862837 0. 0. 0. 0.
  4. 0. 0. 0. 0.10500401]
  5. [0.02628775 0.01366574 0.27162912 0.18385436 0. 0.03725404
  6. 0. 0.00666845 0.1811573 0.01687878 0. 0.06251942
  7. 0.02582079 0.00176389 0. 0. ]]
  8. 其形状是: [2, 16]
  9.  
  10. 年龄 1 对应的特征是: [0. 0.06744663 0.07139666 0.22798921 0.00418518 0.11958582
  11. 0. 0.0862837 0. 0. 0. 0.
  12. 0. 0. 0. 0.10500401]
  13. 年龄 18 对应的特征是: [0.02628775 0.01366574 0.27162912 0.18385436 0. 0.03725404
  14. 0. 0.00666845 0.1811573 0.01687878 0. 0.06251942
  15. 0.02582079 0.00176389 0. 0. ]

4. 提取用户职业特征

参考用户年龄的处理方式实现用户职业的特征提取,同样采用Embedding层和全连接层的方式提取特征。由上一节信息可以得知用户职业的最大数字表示是20。

  1. # 自定义一个用户职业数据
  2. usr_job_data = np.array((0, 20)).reshape(-1).astype('int64')
  3. print("输入的用户职业是:", usr_job_data)
  4. # 创建飞桨动态图的工作空间
  5. with dygraph.guard():
  6. # 对用户职业信息做映射,并紧接着一个Linear层
  7. # 用户职业的最大ID是20,所以Embedding层size的第一个参数设置为20 + 1 = 21
  8. USR_JOB_DICT_SIZE = 20 + 1
  9. usr_job_emb = Embedding([USR_JOB_DICT_SIZE, 16])
  10. usr_job_fc = Linear(input_dim=16, output_dim=16)
  11. usr_job = dygraph.to_variable(usr_job_data)
  12. usr_job_feat = usr_job_emb(usr_job)
  13. usr_job_feat = usr_job_fc(usr_job_feat)
  14. usr_job_feat = fluid.layers.relu(usr_job_feat)
  15. print("用户年龄特征的数据特征是:", usr_job_feat.numpy(), "\n其形状是:", usr_job_feat.shape)
  16. print("\n职业 0 对应的特征是:", usr_job_feat.numpy()[0, :])
  17. print("职业 20 对应的特征是:", usr_job_feat.numpy()[1, :])
  1. 输入的用户职业是: [ 0 20]
  2. 用户年龄特征的数据特征是: [[0. 0.40867782 0.24240115 0.19596662 0. 0.
  3. 0.11957636 0. 0. 0. 0. 0.
  4. 0.41132662 0.20574303 0. 0. ]
  5. [0. 0. 0. 0.18979335 0.00341304 0.
  6. 0.15170634 0. 0.40536746 0.01424695 0. 0.00384581
  7. 0. 0.1786537 0. 0.01656975]]
  8. 其形状是: [2, 16]
  9.  
  10. 职业 0 对应的特征是: [0. 0.40867782 0.24240115 0.19596662 0. 0.
  11. 0.11957636 0. 0. 0. 0. 0.
  12. 0.41132662 0.20574303 0. 0. ]
  13. 职业 20 对应的特征是: [0. 0. 0. 0.18979335 0.00341304 0.
  14. 0.15170634 0. 0.40536746 0.01424695 0. 0.00384581
  15. 0. 0.1786537 0. 0.01656975]

5. 融合用户特征

特征融合是一种常用的特征增强手段,通过结合不同特征的长处,达到取长补短的目的。简单的融合方法有:特征(加权)相加、特征级联、特征正交等等。此处使用特征融合是为了将用户的多个特征融合到一起,用单个向量表示每个用户,更方便计算用户与电影的相似度。上文使用Embedding加全连接的方法,分别得到了用户ID、年龄、性别、职业的特征向量,可以使用全连接层将每个特征映射到固定长度,然后进行相加,得到融合特征。

  1. with dygraph.guard():
  2. FC_ID = Linear(32, 200, act='tanh')
  3. FC_GENDER = Linear(16, 200, act='tanh')
  4. FC_AGE = Linear(16, 200, act='tanh')
  5. FC_JOB = Linear(16, 200, act='tanh')
  6. # 收集所有的用户特征
  7. _features = [usr_id_feat, usr_job_feat, usr_age_feat, usr_gender_feat]
  8. _features = [k.numpy() for k in _features]
  9. _features = [dygraph.to_variable(k) for k in _features]
  10. id_feat = FC_ID(_features[0])
  11. job_feat = FC_JOB(_features[1])
  12. age_feat = FC_AGE(_features[2])
  13. genger_feat = FC_GENDER(_features[-1])
  14. # 对特征求和
  15. usr_feat = id_feat + job_feat + age_feat + genger_feat
  16. print("用户融合后特征的维度是:", usr_feat.shape)
  1. 用户融合后特征的维度是: [2, 200]

这里使用全连接层进一步提取特征,而不是直接相加得到用户特征的原因有两点:

  • 一是用户每个特征数据维度不一致,无法直接相加;
  • 二是用户每个特征仅使用了一层全连接层,提取特征不充分,多使用一层全连接层能进一步提取特征。而且,这里用高维度(200维)的向量表示用户特征,能包含更多的信息,每个用户特征之间的区分也更明显。

上述实现中需要对每个特征都使用一个全连接层,实现较为复杂,一种简单的替换方式是,先将每个用户特征沿着长度维度进行级联,然后使用一个全连接层获得整个的用户特征向量,两种方式的对比见下图:

用户特征提取网络 - 图2

图:特征方式1-特征逐个全连接后相加

用户特征提取网络 - 图3

图:特征方式2-特征级联后使用全连接

两种方式均可实现向量的合并,虽然两者的数学公式不同,但它们的表达能力是类似的。

下面是方式2的代码实现。

  1. with dygraph.guard():
  2. usr_combined = Linear(80, 200, act='tanh')
  3. # 收集所有的用户特征
  4. _features = [usr_id_feat, usr_job_feat, usr_age_feat, usr_gender_feat]
  5. print("打印每个特征的维度:", [f.shape for f in _features])
  6. _features = [k.numpy() for k in _features]
  7. _features = [dygraph.to_variable(k) for k in _features]
  8. # 对特征沿着最后一个维度级联
  9. usr_feat = fluid.layers.concat(input=_features, axis=1)
  10. usr_feat = usr_combined(usr_feat)
  11. print("用户融合后特征的维度是:", usr_feat.shape)
  1. 打印每个特征的维度: [[2, 32], [2, 16], [2, 16], [2, 16]]
  2. 用户融合后特征的维度是: [2, 200]

上述代码中,我们使用了fluid.layers.concat()这个API,该API有两个参数,一个是列表形式的输入数据,另一个是axis,表示沿着第几个维度将输入数据级联到一起。

至此我们已经完成了用户特征提取网络的设计,包括ID特征提取、性别特征提取、年龄特征提取、职业特征提取和特征融合模块,下面我们将所有的模块整合到一起,放到Python类中,完整代码实现如下:

  1. import random
  2. class Model(dygraph.layers.Layer):
  3. def __init__(self, use_poster, use_mov_title, use_mov_cat, use_age_job):
  4. super(Model, self).__init__()
  5. # 将传入的name信息和bool型参数添加到模型类中
  6. self.use_mov_poster = use_poster
  7. self.use_mov_title = use_mov_title
  8. self.use_usr_age_job = use_age_job
  9. self.use_mov_cat = use_mov_cat
  10. # 使用上节定义的数据处理类,获取数据集的信息,并构建训练和验证集的数据迭代器
  11. Dataset = MovieLen(self.use_mov_poster)
  12. self.Dataset = Dataset
  13. self.trainset = self.Dataset.train_dataset
  14. self.valset = self.Dataset.valid_dataset
  15. self.train_loader = self.Dataset.load_data(dataset=self.trainset, mode='train')
  16. self.valid_loader = self.Dataset.load_data(dataset=self.valset, mode='valid')
  17. """ define network layer for embedding usr info """
  18. USR_ID_NUM = Dataset.max_usr_id + 1
  19. # 对用户ID做映射,并紧接着一个FC层
  20. self.usr_emb = Embedding([USR_ID_NUM, 32], is_sparse=False)
  21. self.usr_fc = Linear(32, 32)
  22. # 对用户性别信息做映射,并紧接着一个FC层
  23. USR_GENDER_DICT_SIZE = 2
  24. self.usr_gender_emb = Embedding([USR_GENDER_DICT_SIZE, 16])
  25. self.usr_gender_fc = Linear(16, 16)
  26. # 对用户年龄信息做映射,并紧接着一个FC层
  27. USR_AGE_DICT_SIZE = Dataset.max_usr_age + 1
  28. self.usr_age_emb = Embedding([USR_AGE_DICT_SIZE, 16])
  29. self.usr_age_fc = Linear(16, 16)
  30. # 对用户职业信息做映射,并紧接着一个FC层
  31. USR_JOB_DICT_SIZE = Dataset.max_usr_job + 1
  32. self.usr_job_emb = Embedding([USR_JOB_DICT_SIZE, 16])
  33. self.usr_job_fc = Linear(16, 16)
  34. # 新建一个FC层,用于整合用户数据信息
  35. self.usr_combined = Linear(80, 200, act='tanh')
  36. # 定义计算用户特征的前向运算过程
  37. def get_usr_feat(self, usr_var):
  38. """ get usr features"""
  39. # 获取到用户数据
  40. usr_id, usr_gender, usr_age, usr_job = usr_var
  41. # 将用户的ID数据经过embedding和FC计算,得到的特征保存在feats_collect中
  42. feats_collect = []
  43. usr_id = self.usr_emb(usr_id)
  44. usr_id = self.usr_fc(usr_id)
  45. usr_id = fluid.layers.relu(usr_id)
  46. feats_collect.append(usr_id)
  47. # 计算用户的性别特征,并保存在feats_collect中
  48. usr_gender = self.usr_gender_emb(usr_gender)
  49. usr_gender = self.usr_gender_fc(usr_gender)
  50. usr_gender = fluid.layers.relu(usr_gender)
  51. feats_collect.append(usr_gender)
  52. # 选择是否使用用户的年龄-职业特征
  53. if self.use_usr_age_job:
  54. # 计算用户的年龄特征,并保存在feats_collect中
  55. usr_age = self.usr_age_emb(usr_age)
  56. usr_age = self.usr_age_fc(usr_age)
  57. usr_age = fluid.layers.relu(usr_age)
  58. feats_collect.append(usr_age)
  59. # 计算用户的职业特征,并保存在feats_collect中
  60. usr_job = self.usr_job_emb(usr_job)
  61. usr_job = self.usr_job_fc(usr_job)
  62. usr_job = fluid.layers.relu(usr_job)
  63. feats_collect.append(usr_job)
  64. # 将用户的特征级联,并通过FC层得到最终的用户特征
  65. print([f.shape for f in feats_collect])
  66. usr_feat = fluid.layers.concat(feats_collect, axis=1)
  67. usr_feat = self.usr_combined(usr_feat)
  68. return usr_feat
  69. #下面使用定义好的数据读取器,实现从用户数据读取到用户特征计算的流程:
  70. ## 测试用户特征提取网络
  71. with dygraph.guard():
  72. model = Model("Usr", use_poster=False, use_mov_title=True, use_mov_cat=True, use_age_job=True)
  73. model.eval()
  74. data_loader = model.train_loader
  75. for idx, data in enumerate(data_loader()):
  76. # 获得数据,并转为动态图格式,
  77. usr, mov, score = data
  78. # print(usr.shape)
  79. # 只使用每个Batch的第一条数据
  80. usr_v = [[var[0]] for var in usr]
  81. print("输入的用户ID数据:{}\n性别数据:{} \n年龄数据:{} \n职业数据{}".format(*usr_v))
  82. usr_v = [dygraph.to_variable(np.array(var)) for var in usr_v]
  83. usr_feat = model.get_usr_feat(usr_v)
  84. print("计算得到的用户特征维度是:", usr_feat.shape)
  85. break
  1. ##Total dataset instances: 1000209
  2. ##MovieLens dataset information:
  3. usr num: 6040
  4. movies num: 3883
  5. 输入的用户ID数据:[2928]
  6. 性别数据:[0]
  7. 年龄数据:[25]
  8. 职业数据[2]
  9. [[1, 32], [1, 16], [1, 16], [1, 16]]
  10. 计算得到的用户特征维度是: [1, 200]

上面使用了向量级联+全连接的方式实现了四个用户特征向量的合并,在下面处理电影特征的部分我们会看到使用另外一种向量合并的方式(向量相加)处理电影类型的特征(6个向量合并成1个向量)。