• DeepFM(3) torch实现


    前言

    源码在这个作者的github
    https://github.com/EternalStarICe/recommendation-system-model
    本文为其一个读后感。

    1、数据加载


    原始数据,除去Id和label列;13列数值特征前缀为I,26列类别特征前缀为C。

    def load_data():
        # file_path = '../../DeepRecommendationModel/code/data/criteo_sample.txt'
        file_path = 'train.txt'
        raw_data = pd.read_csv(file_path)
        raw_data.drop(['Id'], axis=1, inplace=True)
        cat_cols = [col for col in raw_data.columns.values if 'C' in col] # 类别特征列列名列表
        num_cols = [col for col in raw_data.columns.values if 'I' in col] # 数值特征列列名列表
        return raw_data, cat_cols, num_cols
    

    2、数据预处理

    第一步得到的数据,数值特征保留原始值,类别特征进行LabelEncoder编码。

    def preprocess_data(df, cat_cols, num_cols):
        df_cp = df.copy()
        df_cp[num_cols] = df_cp[num_cols].fillna(0.0) # 数值特征缺失值填充为0
        for col in num_cols:
            df_cp[col] = df_cp[col].apply(lambda x: np.log(x + 1) if x > -1 else -1)
    
        df_cp[cat_cols] = df_cp[cat_cols].fillna("-1") # 类别特征缺失值填充为-1
        for col in cat_cols:
            encoder = LabelEncoder()
            df_cp[col] = encoder.fit_transform(df_cp[col]) # 对目标标签进行编码,值在0到n_class -1之间
        return df_cp[cat_cols + num_cols]
    train_data = preprocess_data(raw_data, cat_cols, num_cols)
    train_data['label'] = raw_data['Label']
    

    3、类别特征个数统计

    def get_cat_tuple_list(df, cat_cols):
        cat_tuple = namedtuple('cat_tuple', ('name', 'vocab_size')) # Returns a new subclass of tuple with named fields。该元组可以通过name和vocab_size获取对应数据。
        cat_tuple_list = [cat_tuple(name=col, vocab_size=df[col].nunique()) for col in cat_cols]
        return cat_tuple_list
    cat_tuple_list = get_cat_tuple_list(train_data, cat_cols)
    

    4、训练集生成

    X = torch.tensor(train_data.drop(['label'], axis=1).values, dtype=torch.float)
    y = torch.tensor(train_data.label.values, dtype=torch.long)
    dataset = Data.TensorDataset(X, y) # TensorDataset对给定的tensor数据(样本和标签),将它们包装成dataset
    data_iter = Data.DataLoader(dataset=dataset, batch_size=128, shuffle=True) 
    

    x的值:[26个类别特征编码后的索引值,13个原始数值特征],长度为39。
    y [1,0,...]

    5、DeepFM模型

    模型初始化传入的参数:
    num_cols:类别特征名列表
    cat_cols:数值特征名列表
    cat_tuple_list:列表特征每个特征的维度
    emb_len:嵌入矩阵维度k

    class DeepFM(nn.Module):
        def __init__(self, num_cols, cat_cols, cat_tuple_list, emb_len=4):
            super(DeepFM, self).__init__()
            self.cat_col_len = len(cat_cols) # 类别特征26个
            self.num_col_len = len(num_cols) # 数值特征13个
            self.cat_tuple_list = cat_tuple_list # [(name="",vocab_size=10),...]
            self.emb_len = emb_len # k=4
            self.num_cols = num_cols
            self.cat_cols = cat_cols
            self.deep_input_dim = self.num_col_len + self.emb_len * self.cat_col_len # 神经网络输入层维度:数值特征个数 + 类别特征个数*嵌入维度。类别特征one-hot编码后再嵌入,数值特征维度为1。
            # three part: linear, fm, dnn
    
            # linear
            self.fm_linear_embeddings = nn.ModuleList()
            for fc in self.cat_tuple_list:
                self.fm_linear_embeddings.append(nn.Embedding(fc.vocab_size, 1)) # 每个类别特征得到一维输出。26个类别特征,需要优化26个一阶嵌入矩阵。
    
            self.linear_dense = nn.Linear(self.num_col_len, 1) # 不同与每个类别特征都有一维输出,13数值特征得到一维输出。这里相当于wx,13个x对应13个w_i。
    
            # FM
            self.fm_embeddings = nn.ModuleList()
            for fc in self.cat_tuple_list: # FM部分只处理了类别特征。有26个二阶嵌入矩阵
                self.fm_embeddings.append(nn.Embedding(fc.vocab_size, emb_len))
    
            # DNN部分有4个线性层,最后一层一个神经元,得到最后输出概率
            self.deep_linear1 = nn.Linear(self.deep_input_dim, 128)
            self.deep_linear2 = nn.Linear(128, 64)
            self.deep_linear3 = nn.Linear(64, 32)
            self.deep_final_linear = nn.Linear(32, 1)
            self.relu = nn.ReLU()
            self.sigmoid = nn.Sigmoid()
            self.dropout1 = nn.Dropout(0.5)
            self.dropout2 = nn.Dropout(0.3)
            self.dropout3 = nn.Dropout(0.1)
    
            # total dense
            self.final_linear = nn.Linear(3, 2)
    

    2、前向传播
    输入的x为:[类别特征为特征编码之后的index,数值特征为原始值]

    def FM(x): # x:(batch, feature_num, emb_len) 相当于公式中的xi,f*vi。根据公式,左边部分为求和之后取平方。右边部分为平方之后再求和。
        left = torch.square(torch.sum(x, dim=1, keepdim=True)) # (batch, 1, emb_len)
        right = torch.sum(torch.square(x), dim=1, keepdim=True) # (batch, 1, emb_len)
        res = 0.5 * torch.sum((left - right), dim=2) # (batch, 1) # 最后fm的输出为1/2*sum(左-右)
        return res
    
    
    def forward(self, x):
        # print(self.deep_input_dim)
        cat_x = x[:, :self.cat_col_len] 
        num_x = x[:, self.cat_col_len:]
    
        # linear
        linear_num_output = self.linear_dense(num_x) # 13个数值特征的输出
        linear_output_list = []
        for i in range(se lf.cat_col_len):
            linear_output_list.append(self.fm_linear_embeddings[i](cat_x[:, i].long())) # 第i个类别特征经过一阶嵌入层之后的输出。计算过程为从嵌入矩阵中根据类别编码进行查找,得到对应行的那个值。
        linear_cat_output = linear_output_list[0]
        for i in range(1, self.cat_col_len):
            linear_cat_output += linear_output_list[i] # 所有类别特征的一阶输出求和
        linear_output = linear_cat_output + linear_num_output # 类别特征和数值特征求和,其shape为(batch_size,1)。v 
    
        # FM
        fm_input = self.fm_embeddings[0](cat_x[:, 0].long()).unsqueeze(dim=1) # a.unsqueeze(N) 就是在a中指定位置N加上一个维数为1的维度。第1个类别特征的二阶输出。torch.Size([batch_size, 1, 4])
        for i in range(1, self.cat_col_len):
            fm_input = torch.cat([fm_input, self.fm_embeddings[i](cat_x[:, i].long()).unsqueeze(dim=1)], dim=1) # 按照axis=1的维度进行拼接,所以拼接后的维度为[batch_size,26,4]
        fm_output = FM(fm_input) # 最终fm_input维度为:torch.Size([batch_size, 26, 4])。fm_output的维度为torch .Size([batch_size, 1])
    
        # dnn
    
        for i in range(self.cat_col_len): # 此时DNN的输入数据为:[数值特征原始值,类别特征经过二阶矩阵嵌入之后的值]
            num_x = torch.cat([num_x, self.fm_embeddings[i](cat_x[:, i].long())], dim=1)
        # print(num_x.shape)
        deep_output = self.dropout1(self.relu(self.deep_linear1(num_x)))
        deep_output = self.dropout2(self.relu(self.deep_linear2(deep_output)))
        deep_output = self.dropout3(self.relu(self.deep_linear3(deep_output)))
        deep_output = self.deep_final_linear(deep_output) # DNN的输出维度:torch.Size([batch_size, 1])
    
        # 总和
        output = self.sigmoid(linear_output + fm_output + deep_output) # torch.Size([batch_size, 1])
        return torch.cat([output, 1 - output], dim=1)  #
        # return self.final_linear(torch.cat([linear_output, fm_output, deep_output], dim=1))   
    

    fm部分注释:

    fm_input = self.fm_embeddings[0](cat_x[:, 0].long()).unsqueeze(dim=1)
    

    这部分的输出shape为:torch.Size([batch_size, 1, 4]),其对应fm公式中的(v_{i,f}x_i)。之前FM的代码里(v)是一个矩阵[特征数,隐向量维度]。现在有26个(v),每个(v)有不同的特征数,因为每个类别特征编码之后的个数不同,但是有相同的隐向量维度。

    left = torch.square(torch.sum(x, dim=1, keepdim=True)) # (batch, 1, emb_len)
    

    x即为26个fm_input拼接成的长tensor,其形状为torch.Size([batch_size, 26, 4])。所谓DNN和FM共享权值,也就是共享x向量,继而共同训练嵌入矩阵(v)。对应的fm公式为:(v_{i,f}x_i)(i)代表26个嵌入矩阵的第i个,(x_i)代表第i个类别特征的编码后的索引值;二者相乘,表示从嵌入矩阵(v_i)取对应编码索引那行的数据,该行数据维度为[1,4],所以26行拼接得到的维度为 [26,4]。对拼接后的向量进行求和,便是26个特征的交互。
    left对应的fm公式为:((sum_{i=1}^{n}{v_{i,f}x_i} )^2)。先求和再平方。

    right = torch.sum(torch.square(x), dim=1, keepdim=True) # (batch, 1, emb_len)
    

    right对应的fm公式为:(sum_{i=1}^{n}{v_{i,f}^2 x_i^2})

    res = 0.5 * torch.sum((left - right), dim=2) # (batch, 1)
    

    res中的torch.sum(dim=2)对应fm公式中的(sum_{f=1}^{k})。在dim=2的维度进行求和,即维数为4的那个维度,隐向量求和。

    DNN部分解释

    DNN的输入数据为:[数值特征原始值,类别特征经过矩阵嵌入之后的值]。但是计算FM二阶特征交互的时候使用的只是类别特征经过矩阵嵌入之后的值。有一个考虑,在DeepFM中的fm,使用的不再是一个嵌入矩阵,而是多个,并且每个嵌入矩阵是针对类别特征进行embedding_lookup,即进行无需one-hot向量的embedding操作。所以数值特征被忽视了。在DNN阶段又将其加上了。

  • 相关阅读:
    线程池的优雅关闭实践
    InheritableThreadLocal原理解析
    线程池踩坑
    两个线程通讯(生产-卖面包问题)
    谈谈redis的热key问题如何解决
    中国软件杯选题A1数据智能分析报告系统
    《程序员的思维修炼》读后感
    《算法导论》读后感
    《重构》读后感
    《代码整洁之道》读后感
  • 原文地址:https://www.cnblogs.com/leimu/p/14606583.html
Copyright © 2020-2023  润新知