用户特征提取网络

    用户特征网络主要包括:

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

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

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

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

    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等特征,实现代码均是类似的。

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

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

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

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

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

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

    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. ]

    参考用户年龄的处理方式实现用户职业的特征提取,同样采用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("职业 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]

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

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

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

    用户特征提取网络 - 图2

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

    图:特征方式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]

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

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

    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]