用户特征提取网络
用户特征网络主要包括:
- 将用户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特征提取代码实现:
- 输入的用户ID是: [3511 4125]
- 用户ID的特征是: [[0.01574198 0. 0. 0. 0.02548438 0.01829206
- 0. 0.00267444 0.03974488 0. 0.0125479 0.01635006
- 0. 0.01348757 0.00099145 0.00921841 0.02927484 0.0277753
- 0.02781798 0. 0.00259031 0. 0.00221091 0.
- 0. 0. 0. 0. 0. 0.
- 0.01300182 0.02627602]
- [0. 0.01970843 0.00200395 0. 0.02862134 0.
- 0. 0.01341773 0.01240196 0. 0.03665136 0.02436131
- 0. 0.02451975 0. 0.00382315 0. 0.
- 0.01831124 0. 0. 0. 0.00175647 0.0095302
- 0.00249144 0. 0.00717024 0. 0. 0.
- 0. 0.0216652 ]]
- 其形状是: [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等特征,实现代码均是类似的。
下面是用户性别特征提取实现:
usr_gender_data = np.array((0, 1)).reshape(-1).astype('int64')
print("输入的用户性别是:", usr_gender_data)
# 创建飞桨动态图的工作空间
with dygraph.guard():
# 用户的性别用0, 1 表示
# 性别最大ID是1,所以Embedding层size的第一个参数设置为1 + 1 = 2
USR_ID_NUM = 2
# 对用户性别信息做映射,并紧接着一个FC层
USR_GENDER_DICT_SIZE = 2
usr_gender_emb = Embedding([USR_GENDER_DICT_SIZE, 16])
usr_gender_fc = Linear(input_dim=16, output_dim=16)
usr_gender_var = dygraph.to_variable(usr_gender_data)
usr_gender_feat = usr_gender_fc(usr_gender_emb(usr_gender_var))
usr_gender_feat = fluid.layers.relu(usr_gender_feat)
print("用户性别特征的数据特征是:", usr_gender_feat.numpy(), "\n其形状是:", usr_gender_feat.shape)
print("\n性别 0 对应的特征是:", usr_gender_feat.numpy()[0, :])
- 输入的用户性别是: [0 1]
- 用户性别特征的数据特征是: [[0.023137 0. 0. 0.05907416 0.1018934 0.
- 0. 0.00924867 0.32423887 0.27837008 0.5539641 0.
- 0. 0. 0. 0.09197924]
- [0.23991962 0.25170493 0. 0.47101367 0.0232828 0.
- 0. 0.17549343 0.29906213 0.15026219]]
- 其形状是: [2, 16]
- 性别 0 对应的特征是: [0.023137 0. 0. 0.05907416 0.1018934 0.
- 0. 0.00924867 0.32423887 0.27837008 0.5539641 0.
- 0. 0. 0. 0.09197924]
- 性别 1 对应的特征是: [0.23991962 0.25170493 0. 0.47101367 0.0232828 0.
- 0. 0.0052276 0. 0.10090257 0.4415601 0.
- 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 18]
- 用户年龄特征的数据特征是: [[0. 0.06744663 0.07139666 0.22798921 0.00418518 0.11958582
- 0. 0.0862837 0. 0. 0. 0.
- 0. 0. 0. 0.10500401]
- [0.02628775 0.01366574 0.27162912 0.18385436 0. 0.03725404
- 0. 0.00666845 0.1811573 0.01687878 0. 0.06251942
- 0.02582079 0.00176389 0. 0. ]]
- 其形状是: [2, 16]
- 年龄 1 对应的特征是: [0. 0.06744663 0.07139666 0.22798921 0.00418518 0.11958582
- 0. 0.0862837 0. 0. 0. 0.
- 0. 0. 0. 0.10500401]
- 年龄 18 对应的特征是: [0.02628775 0.01366574 0.27162912 0.18385436 0. 0.03725404
- 0. 0.00666845 0.1811573 0.01687878 0. 0.06251942
- 0.02582079 0.00176389 0. 0. ]
参考用户年龄的处理方式实现用户职业的特征提取,同样采用Embedding层和全连接层的方式提取特征。由上一节信息可以得知用户职业的最大数字表示是20。
# 自定义一个用户职业数据
usr_job_data = np.array((0, 20)).reshape(-1).astype('int64')
print("输入的用户职业是:", usr_job_data)
# 创建飞桨动态图的工作空间
with dygraph.guard():
# 对用户职业信息做映射,并紧接着一个Linear层
# 用户职业的最大ID是20,所以Embedding层size的第一个参数设置为20 + 1 = 21
USR_JOB_DICT_SIZE = 20 + 1
usr_job_emb = Embedding([USR_JOB_DICT_SIZE, 16])
usr_job_fc = Linear(input_dim=16, output_dim=16)
usr_job = dygraph.to_variable(usr_job_data)
usr_job_feat = usr_job_emb(usr_job)
usr_job_feat = usr_job_fc(usr_job_feat)
usr_job_feat = fluid.layers.relu(usr_job_feat)
print("用户年龄特征的数据特征是:", usr_job_feat.numpy(), "\n其形状是:", usr_job_feat.shape)
print("职业 20 对应的特征是:", usr_job_feat.numpy()[1, :])
- 输入的用户职业是: [ 0 20]
- 用户年龄特征的数据特征是: [[0. 0.40867782 0.24240115 0.19596662 0. 0.
- 0.11957636 0. 0. 0. 0. 0.
- 0.41132662 0.20574303 0. 0. ]
- [0. 0. 0. 0.18979335 0.00341304 0.
- 0.15170634 0. 0.40536746 0.01424695 0. 0.00384581
- 0. 0.1786537 0. 0.01656975]]
- 其形状是: [2, 16]
- 职业 0 对应的特征是: [0. 0.40867782 0.24240115 0.19596662 0. 0.
- 0.11957636 0. 0. 0. 0. 0.
- 0.41132662 0.20574303 0. 0. ]
- 职业 20 对应的特征是: [0. 0. 0. 0.18979335 0.00341304 0.
- 0.15170634 0. 0.40536746 0.01424695 0. 0.00384581
- 0. 0.1786537 0. 0.01656975]
特征融合是一种常用的特征增强手段,通过结合不同特征的长处,达到取长补短的目的。简单的融合方法有:特征(加权)相加、特征级联、特征正交等等。此处使用特征融合是为了将用户的多个特征融合到一起,用单个向量表示每个用户,更方便计算用户与电影的相似度。上文使用Embedding加全连接的方法,分别得到了用户ID、年龄、性别、职业的特征向量,可以使用全连接层将每个特征映射到固定长度,然后进行相加,得到融合特征。
- 用户融合后特征的维度是: [2, 200]
- 一是用户每个特征数据维度不一致,无法直接相加;
- 二是用户每个特征仅使用了一层全连接层,提取特征不充分,多使用一层全连接层能进一步提取特征。而且,这里用高维度(200维)的向量表示用户特征,能包含更多的信息,每个用户特征之间的区分也更明显。
上述实现中需要对每个特征都使用一个全连接层,实现较为复杂,一种简单的替换方式是,先将每个用户特征沿着长度维度进行级联,然后使用一个全连接层获得整个的用户特征向量,两种方式的对比见下图:
图:特征方式1-特征逐个全连接后相加
图:特征方式2-特征级联后使用全连接
两种方式均可实现向量的合并,虽然两者的数学公式不同,但它们的表达能力是类似的。
下面是方式2的代码实现。
with dygraph.guard():
usr_combined = Linear(80, 200, act='tanh')
# 收集所有的用户特征
_features = [usr_id_feat, usr_job_feat, usr_age_feat, usr_gender_feat]
print("打印每个特征的维度:", [f.shape for f in _features])
_features = [k.numpy() for k in _features]
_features = [dygraph.to_variable(k) for k in _features]
# 对特征沿着最后一个维度级联
usr_feat = fluid.layers.concat(input=_features, axis=1)
usr_feat = usr_combined(usr_feat)
print("用户融合后特征的维度是:", usr_feat.shape)
- 打印每个特征的维度: [[2, 32], [2, 16], [2, 16], [2, 16]]
- 用户融合后特征的维度是: [2, 200]
上述代码中,我们使用了这个API,该API有两个参数,一个是列表形式的输入数据,另一个是axis,表示沿着第几个维度将输入数据级联到一起。
至此我们已经完成了用户特征提取网络的设计,包括ID特征提取、性别特征提取、年龄特征提取、职业特征提取和特征融合模块,下面我们将所有的模块整合到一起,放到Python类中,完整代码实现如下:
- ##Total dataset instances: 1000209
- ##MovieLens dataset information:
- usr num: 6040
- movies num: 3883
- 输入的用户ID数据:[2928]
- 性别数据:[0]
- 年龄数据:[25]
- 职业数据[2]
- [[1, 32], [1, 16], [1, 16], [1, 16]]
- 计算得到的用户特征维度是: [1, 200]