120分钟吃掉DIEN深度兴趣演化网络

十九、帧间预测编码_5、skip模式和direct模式 帧间预测的几种特殊模式: P_Skip: 即COPY模式,在该模式下无运动矢量残差,无像素预测残差。重建时以MVP作为实际运动矢量,以预测像素块作为重建; B_Skip:同P_Skip类似的是该模式下同样无运动矢量残差,无像素预测残差。重建时,以Direct模式获取双向MV,以预测像素块作为重建; B_Direct:B_Direct模式分为B_Direct_8x8和B_Direct_16x16,分别表示整个宏块和一个子块按照B_Direct模式编码。该模式下,无运动矢量残差,有像素预测残差。重建时,以D. 阅读详情

阿里妈妈在CTR预估领域有3篇比较有名的文章。

2017年的深度兴趣网络, DIN(DeepInterestNetwork)。

2018年的深度兴趣演化网络, DIEN(DeepInterestEvolutionNetWork)。

2019年的深度会话兴趣网络, DSIN(DeepSessionInterestNetWork)。

这3篇文章的主要思想和相互关系用一句话分别概括如下:

第1篇DIN说,用户的行为日志中只有一部分和当前候选广告有关。可以利用Attention机制从用户行为日志中建模出和当前候选广告相关的用户兴趣表示。我们试过涨点了嘻嘻嘻。

第2篇DIEN说,用户最近的行为可能比较远的行为更加重要。可以用循环神经网络GRU建模用户兴趣随时间的演化。我们试过也涨点了嘿嘿嘿。

第3篇DSIN说,用户在同一次会话中的行为高度相关,在不同会话间的行为则相对独立。可以把用户行为日志按照时间间隔分割成会话并用SelfAttention机制建模它们之间的相互作用。我们试过又涨点了哈哈哈。

参考材料:

  • DIEN论文:https://arxiv.org/pdf/1809.03672.pdf

  • DIN+DIEN,机器学习唯一指定涨点技Attention:https://zhuanlan.zhihu.com/p/431131396

  • 从DIN到DIEN看阿里CTR算法的进化脉络:https://zhuanlan.zhihu.com/p/78365283

  • 代码实现参考:https://github.com/GitHub-HongweiZhang/prediction-flow

上一篇文章我们介绍了DIN, 本篇文章我们介绍DIEN。

DIEN这篇文章的主要创新之处有3点:

  • 一是引入GRU来从用户行为日志序列中自然地抽取每个行为日志对应的用户兴趣表示(兴趣抽取层)。

  • 二是设计了一个辅助loss层,通过做一个辅助任务(区分真实的用户历史点击行为和负采样的非用户点击行为)来强化用户兴趣表示的学习。

  • 三是将注意力机制和GRU结构结合起来(AUGRU: Attention UPdate GRU),来建模用户兴趣的时间演化得到最终的用户表示(兴趣演化层)。

其中引入辅助Loss的技巧是神经网络涨点非常通用的一种高级技巧,值得我们学习。

公众号后台回复关键词:DIEN,获取本文全部源码和Movielens数据集下载链接。

一,DIEN原理解析

DIEN的主要出发点是,用户最近的行为可能比较远的行为更加重要。可以用循环神经网络GRU建模用户兴趣随时间的演化。

DIEN选择的是不容易梯度消失且较快的GRU。

b4831ad04a1937e9f8c3af3d9ae6b675.jpeg

1, 兴趣抽取层

图中的 是用户的行为序列,而 是对应的embedding。随着自然发生的顺序, 被输入GRU中,这就是兴趣抽取层。

也是DIEN的第一条创新:引入GRU来从用户行为日志序列中自然地抽取每个行为日志对应的用户兴趣表示(兴趣抽取层)。

2,辅助loss

如果忽略上面的AUGRU环节,GRU中的隐状态 就应该成为用户的行为序列最后的表示。

如果直接就这样做,也不是不可以,但是学习到的东西可能不是我们想要的用户兴趣表示,或者说很难学习到有意义的信息。

因为 的迭代经过了很多步,然后还要和其他特征做拼接,然后还要经过MLP,最后才得到输出去计算Loss。

这样的结果就是最后来了一个正样本或负样本,反向传播很难归因到 上。

基于此DIEN给出了第二个要点:使用辅助Loss来强化的学习。

我们来看看这个辅助Loss是怎么做的?这里设计了一个辅助任务,使用来区分真实的用户历史点击行为和负采样的非用户点击行为。

由于 代表着 t 时刻的用户兴趣表示,我们可以用它来预测 t+1时刻的广告用户是否点击。

因为用户行为日志中都是用户点击过的广告(正样本, ),所以我们可以从全部的广告中给用户采样同样数量的用户没有点击过的广告作为负样本。

结合和 , 作为输入, 我们可以做一个二分类的辅助任务。

这个辅助任务给在每个t时刻都提供了一个监督信号,使得能够更好地成为用户兴趣的抽取表示。

真实应用场合下,你把开始的输入和最后的要求告诉网络,它就能给你一个好的结果的情况非常少。

大多数时候是需要你去控制每一步的输入输出,每一步的loss才能防止网络各种偷懒作弊。

辅助loss能够使得网络更受控制,向我们需要的方向发展,非常建议大家在实际业务中多试试辅助loss。

3,兴趣演化层

通过兴趣抽取层和辅助loss,我们得到了每个t时刻用户的一般兴趣表示。

注意这个兴趣表示是一般性的,还没有和我们的候选广告做Attention关联。

在DIN中,我们通过Attention机制构建了和候选广告相关的用户兴趣表示。

而在DIEN中,我们希望建立的是和和候选广告相关,并且和时间演化相关的用户兴趣表示。

DIEN通过结合Attention机制和GRU结构来做到这一点,这就是第三点创新AUGRU : Attention UPdate Gate GRU。

下面我们进行详细讲解。

一般地,各种RNN序列模型层(SimpleRNN,GRU,LSTM等)可以用函数表示如下:

这个公式的含义是:t时刻循环神经网络的输出向量由t-1时刻的输出向量和t时刻的输入变换而来。

为了结合Attention机制和GRU结构,我们需要设计这样的一个有三种输入的序列模型

这里的是 t时刻的用户兴趣表示输入 和候选广告计算出的attention 得分,是个标量。

我们先看看 GRU的 具体函数形式:

402 Payment Required

公式中的小圈表示哈达玛积,也就是两个向量逐位相乘。

其中(1)式和(2)式计算的是更新门和重置门,是两个长度和相同的向量。

更新门用于控制每一步被更新的比例,更新门越大,更新幅度越大。

重置门用于控制更新候选向量中前一步的状态被重新放入的比例,重置门越大,更新候选向量中被重新放进来的比例越大。

注意到(4)式 实际上和ResNet的残差结构是相似的,都是 f(x) = x + g(x) 的形式,可以有效地防止长序列学习反向传播过程中梯度消失问题。

如何在GRU的基础上把attention得分融入进来呢?有以下一些非常自然的想法:

  • 1, 用缩放输入, 这就是AIGRU: Attention Input GRU。其含义是相关性高的在输入端进行放大。

  • 2, 用代替GRU的更新门,这就是AGRU: Attention based GRU。其含义是用直接用相关性作为更新幅度。

  • 3, 用缩放GRU的更新门,这就是AUGRU:  Attention Update Gate GRU。其含义是用用相关性缩放更新幅度。

AIGRU实际上并没有改变GRU的结构,只是改变了其输入,这种方式对Attention的使用比较含蓄,我把每个历史广告的相关性强弱通过输入告诉GRU,GRU你就给我好好学吧,希望你把相关性强的广告多长点到脑子里。但是这种方式效果不是很理想,即使是相关性为0的历史广告,也会对进行更新。

AGRU是改变了GRU的结构的,并且对Attention的使用非常激进,完全删掉了GRU原有的的更新门,GRU你的脑子归Attention管了,遇到相关性高的广告,一定大大地记上一笔。不过AGRU也有一个缺陷,那就是Attention得分实际上是个标量,无法反应不同维度的差异。

AUGRU也是改变了GRU的结构的,并且对Attention的使用比较折衷,让Attention缩放GRU原有的更新幅度。GRU我给你找了个搭档Attention,你更新前先问问它,你两一起决定该迈多大的步子吧。

DIEN论文中通过对比实验发现AUGRU的效果最好。

我们看看AUGRU的核心实现代码。基本上和公式是一致的,应用了F.linear函数来实现矩阵乘法和加偏置。

import torch 
from torch import nn 

class AttentionUpdateGateGRUCell(nn.Module):
    def __init__(self, input_size, hidden_size, bias=True):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.bias = bias
        # (Wu|Wr|Wn)
        self.weight_ih = nn.Parameter(
            torch.Tensor(3 * hidden_size, input_size))
        # (Uu|Ur|Un)
        self.weight_hh = nn.Parameter(
            torch.Tensor(3 * hidden_size, hidden_size))
        if bias:
            # (b_iu|b_ir|b_in)
            self.bias_ih = nn.Parameter(torch.Tensor(3 * hidden_size))
            # (b_hu|b_hr|b_hn)
            self.bias_hh = nn.Parameter(torch.Tensor(3 * hidden_size))
        else:
            self.register_parameter('bias_ih', None)
            self.register_parameter('bias_hh', None)
        self.reset_parameters()

    def reset_parameters(self):
        stdv = 1.0 / (self.hidden_size)**0.5
        for weight in self.parameters():
            nn.init.uniform_(weight, -stdv, stdv)
            
    def forward(self, x, hx, att_score):
        gi = F.linear(x, self.weight_ih, self.bias_ih)
        gh = F.linear(hx, self.weight_hh, self.bias_hh)
        i_r, i_u, i_n = gi.chunk(3, 1)
        h_r, h_u, h_n = gh.chunk(3, 1)

        resetgate = torch.sigmoid(i_r + h_r)
        updategate = torch.sigmoid(i_u + h_u)
        newgate = torch.tanh(i_n + resetgate * h_n)

        updategate = att_score.view(-1, 1) * updategate
        hy = (1-updategate)*hx +  updategate*newgate

        return hy

二,DIEN的pytorch实现

下面是一个DIEN模型的完整pytorch实现。许多代码和DIN的实现是一样的。

这里的AttentionGroup类用来建立候选广告属性,历史广告属性,以及负采样的广告属性的pair关系。

import torch
import torch.nn as nn
import torch.nn.functional as F 
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
from collections import OrderedDict

class MaxPooling(nn.Module):
    def __init__(self, dim):
        super(MaxPooling, self).__init__()
        self.dim = dim

    def forward(self, input):
        return torch.max(input, self.dim)[0]


class SumPooling(nn.Module):
    def __init__(self, dim):
        super(SumPooling, self).__init__()
        self.dim = dim

    def forward(self, input):
        return torch.sum(input, self.dim)

class Dice(nn.Module):
    """
    The Data Adaptive Activation Function in DIN, a generalization of PReLu.
    """
    def __init__(self, emb_size, dim=2, epsilon=1e-8):
        super(Dice, self).__init__()
        assert dim == 2 or dim == 3

        self.bn = nn.BatchNorm1d(emb_size, eps=epsilon)
        self.sigmoid = nn.Sigmoid()
        self.dim = dim
        
        # wrap alpha in nn.Parameter to make it trainable
        self.alpha = nn.Parameter(torch.zeros((emb_size,))) if self.dim == 2 else nn.Parameter(
            torch.zeros((emb_size, 1)))


    def forward(self, x):
        assert x.dim() == self.dim
        if self.dim == 2:
            x_p = self.sigmoid(self.bn(x))
            out = self.alpha * (1 - x_p) * x + x_p * x
        else:
            x = torch.transpose(x, 1, 2)
            x_p = self.sigmoid(self.bn(x))
            out = self.alpha * (1 - x_p) * x + x_p * x
            out = torch.transpose(out, 1, 2)
        return out

    
class Identity(nn.Module):
    def __init__(self):
        super().__init__()
    def forward(self, x):
        return x
    
def get_activation_layer(name, hidden_size=None, dice_dim=2):
    name = name.lower()
    name_dict = {x.lower():x for x in dir(nn) if '__' not in x and 'Z'>=x[0]>='A'}
    if name=="linear":
        return Identity()
    elif name=="dice":
        assert dice_dim
        return Dice(hidden_size, dice_dim)
    else:
        assert name in name_dict, f'activation type {name} not supported!'
        return getattr(nn,name_dict[name])()
    
def init_weights(model):
    if isinstance(model, nn.Linear):
        if model.weight is not None:
            nn.init.kaiming_uniform_(model.weight.data)
        if model.bias is not None:
            nn.init.normal_(model.bias.data)
    elif isinstance(model, (nn.BatchNorm1d,nn.BatchNorm2d,nn.BatchNorm3d)):
        if model.weight is not None:
            nn.init.normal_(model.weight.data, mean=1, std=0.02)
        if model.bias is not None:
            nn.init.constant_(model.bias.data, 0)
    else:
        pass


class MLP(nn.Module):
    def __init__(self, input_size, hidden_layers,
                 dropout=0.0, batchnorm=True, activation='relu'):
        super(MLP, self).__init__()
        modules = OrderedDict()
        previous_size = input_size
        for index, hidden_layer in enumerate(hidden_layers):
            modules[f"dense{index}"] = nn.Linear(previous_size, hidden_layer)
            if batchnorm:
                modules[f"batchnorm{index}"] = nn.BatchNorm1d(hidden_layer)
            if activation:
                modules[f"activation{index}"] = get_activation_layer(activation,hidden_layer,2)
            if dropout:
                modules[f"dropout{index}"] = nn.Dropout(dropout)
            previous_size = hidden_layer
        self.mlp = nn.Sequential(modules)

    def forward(self, x):
        return self.mlp(x)


class AttentionGRUCell(nn.Module):
    def __init__(self, input_size, hidden_size, bias=True):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.bias = bias
        # (Wr|Wn)
        self.weight_ih = nn.Parameter(
            torch.Tensor(2 * hidden_size, input_size))
        # (Ur|Un)
        self.weight_hh = nn.Parameter(
            torch.Tensor(2 * hidden_size, hidden_size))
        if bias:
            # (b_ir|b_in)
            self.bias_ih = nn.Parameter(torch.Tensor(2 * hidden_size))
            # (b_hr|b_hn)
            self.bias_hh = nn.Parameter(torch.Tensor(2 * hidden_size))
        else:
            self.register_parameter('bias_ih', None)
            self.register_parameter('bias_hh', None)
        self.reset_parameters()

    def reset_parameters(self):
        stdv = 1.0 / (self.hidden_size)**0.5
        for weight in self.parameters():
            nn.init.uniform_(weight, -stdv, stdv)

    def forward(self, x, hx, att_score):

        gi = F.linear(x, self.weight_ih, self.bias_ih)
        gh = F.linear(hx, self.weight_hh, self.bias_hh)
        i_r, i_n = gi.chunk(2, 1)
        h_r, h_n = gh.chunk(2, 1)

        resetgate = torch.sigmoid(i_r + h_r)
        newgate = torch.tanh(i_n + resetgate * h_n)
        att_score = att_score.view(-1, 1)
        hy = (1. - att_score) * hx + att_score * newgate
        
        return hy


class AttentionUpdateGateGRUCell(nn.Module):
    def __init__(self, input_size, hidden_size, bias=True):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.bias = bias
        # (Wu|Wr|Wn)
        self.weight_ih = nn.Parameter(
            torch.Tensor(3 * hidden_size, input_size))
        # (Uu|Ur|Un)
        self.weight_hh = nn.Parameter(
            torch.Tensor(3 * hidden_size, hidden_size))
        if bias:
            # (b_iu|b_ir|b_in)
            self.bias_ih = nn.Parameter(torch.Tensor(3 * hidden_size))
            # (b_hu|b_hr|b_hn)
            self.bias_hh = nn.Parameter(torch.Tensor(3 * hidden_size))
        else:
            self.register_parameter('bias_ih', None)
            self.register_parameter('bias_hh', None)
        self.reset_parameters()

    def reset_parameters(self):
        stdv = 1.0 / (self.hidden_size)**0.5
        for weight in self.parameters():
            nn.init.uniform_(weight, -stdv, stdv)
            
    def forward(self, x, hx, att_score):
        gi = F.linear(x, self.weight_ih, self.bias_ih)
        gh = F.linear(hx, self.weight_hh, self.bias_hh)
        i_u,i_r, i_n = gi.chunk(3, 1)
        h_u,h_r, h_n = gh.chunk(3, 1)

        updategate = torch.sigmoid(i_u + h_u)
        resetgate = torch.sigmoid(i_r + h_r)
        newgate = torch.tanh(i_n + resetgate * h_n)

        updategate = att_score.view(-1, 1) * updategate
        hy = (1-updategate)*hx +  updategate*newgate

        return hy



class DynamicGRU(nn.Module):
    def __init__(self, input_size, hidden_size, bias=True, gru_type='AGRU'):
        super(DynamicGRU, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        if gru_type == 'AGRU':
            self.rnn = AttentionGRUCell(input_size, hidden_size, bias)
        elif gru_type == 'AUGRU':
            self.rnn = AttentionUpdateGateGRUCell(
                input_size, hidden_size, bias)

    def forward(self, x, att_scores, hx=None):
        is_packed_input = isinstance(x, nn.utils.rnn.PackedSequence)
        if not is_packed_input:
            raise NotImplementedError(
                "DynamicGRU only supports packed input")

        is_packed_att_scores = isinstance(att_scores, nn.utils.rnn.PackedSequence)
        if not is_packed_att_scores:
            raise NotImplementedError(
                "DynamicGRU only supports packed att_scores")

        x, batch_sizes, sorted_indices, unsorted_indices = x
        att_scores, _, _, _ = att_scores

        max_batch_size = batch_sizes[0]
        max_batch_size = int(max_batch_size)

        if hx is None:
            hx = torch.zeros(
                max_batch_size, self.hidden_size,
                dtype=x.dtype, device=x.device)

        outputs = torch.zeros(
            x.size(0), self.hidden_size,
            dtype=x.dtype, device=x.device)

        begin = 0
        for batch in batch_sizes:
            new_hx = self.rnn(
                x[begin: begin + batch],
                hx[0:batch],
                att_scores[begin: begin + batch])
            outputs[begin: begin + batch] = new_hx
            hx = new_hx
            begin += batch

        return nn.utils.rnn.PackedSequence(
            outputs, batch_sizes, sorted_indices, unsorted_indices)
    

class Attention(nn.Module):
    def __init__(
            self,
            input_size,
            hidden_layers,
            dropout=0.0,
            batchnorm=True,
            activation='prelu',
            return_scores=False):
        
        super().__init__()
        self.return_scores = return_scores
        
        self.mlp = MLP(
            input_size=input_size * 4,
            hidden_layers=hidden_layers,
            dropout=dropout,
            batchnorm=batchnorm,
            activation=activation)
        self.fc = nn.Linear(hidden_layers[-1], 1)

    def forward(self, query, keys, keys_length):
        """
        Parameters
        ----------
        query: 2D tensor, [Batch, Hidden]
        keys: 3D tensor, [Batch, Time, Hidden]
        keys_length: 1D tensor, [Batch]

        Returns
        -------
        outputs: 2D tensor, [Batch, Hidden]
        """
        batch_size, max_length, dim = keys.size()

        query = query.unsqueeze(1).expand(-1, max_length, -1)

        din_all = torch.cat(
            [query, keys, query - keys, query * keys], dim=-1)

        din_all = din_all.view(batch_size * max_length, -1)

        outputs = self.mlp(din_all)

        outputs = self.fc(outputs).view(batch_size, max_length)  # [B, T]

        # Scale
        outputs = outputs / (dim ** 0.5)

        # Mask
        mask = (torch.arange(max_length, device=keys_length.device).repeat(
            batch_size, 1) < keys_length.view(-1, 1))
        outputs[~mask] = -np.inf

        # Activation
        outputs = F.softmax(outputs, dim=1)  #DIN uses sigmoid,DIEN uses softmax; [B, T]

        if not self.return_scores:
            # Weighted sum
            outputs = torch.matmul(
                outputs.unsqueeze(1), keys).squeeze()  # [B, H]
        return outputs 
    
class AuxiliaryNet(nn.Module):
    def __init__(self, input_size, hidden_layers, activation='sigmoid'):
        super().__init__()
        modules = OrderedDict()
        previous_size = input_size
        for index, hidden_layer in enumerate(hidden_layers):
            modules[f"dense{index}"] = nn.Linear(previous_size, hidden_layer)
            if activation:
                modules[f"activation{index}"] = get_activation_layer(activation)
            previous_size = hidden_layer
        modules["final_layer"] = nn.Linear(previous_size, 1)
        self.mlp = nn.Sequential(modules)

    def forward(self, x):
        return torch.sigmoid(self.mlp(x))


class Interest(nn.Module):
    SUPPORTED_GRU_TYPE = ['GRU', 'AIGRU', 'AGRU', 'AUGRU']

    def __init__(
            self,
            input_size,
            gru_type='AUGRU',
            gru_dropout=0.0,
            att_hidden_layers=[80, 40],
            att_dropout=0.0,
            att_batchnorm=True,
            att_activation='prelu',
            use_negsampling=False):
        super(Interest, self).__init__()
        if gru_type not in Interest.SUPPORTED_GRU_TYPE:
            raise NotImplementedError(f"gru_type: {gru_type} is not supported")

        self.gru_type = gru_type
        self.use_negsampling = use_negsampling

        self.interest_extractor = nn.GRU(
            input_size=input_size,
            hidden_size=input_size,
            batch_first=True,
            bidirectional=False)

        if self.use_negsampling:
            self.auxiliary_net = AuxiliaryNet(
                input_size * 2, hidden_layers=[100, 50])

        if gru_type == 'GRU':
            self.attention = Attention(
                input_size=input_size,
                hidden_layers=att_hidden_layers,
                dropout=att_dropout,
                batchnorm=att_batchnorm,
                activation=att_activation)
            
            self.interest_evolution = nn.GRU(
                input_size=input_size,
                hidden_size=input_size,
                batch_first=True,
                bidirectional=False)
                
        elif gru_type == 'AIGRU':
            self.attention = Attention(
                input_size=input_size,
                hidden_layers=att_hidden_layers,
                dropout=att_dropout,
                batchnorm=att_batchnorm,
                activation=att_activation,
                return_scores=True)

            self.interest_evolution = nn.GRU(
                input_size=input_size,
                hidden_size=input_size,
                batch_first=True,
                bidirectional=False)
            
        elif gru_type == 'AGRU' or gru_type == 'AUGRU':
            self.attention = Attention(
                input_size=input_size,
                hidden_layers=att_hidden_layers,
                dropout=att_dropout,
                batchnorm=att_batchnorm,
                activation=att_activation,
                return_scores=True)

            self.interest_evolution = DynamicGRU(
                input_size=input_size,
                hidden_size=input_size,
                gru_type=gru_type)

    @staticmethod
    def get_last_state(states, keys_length):
        # states [B, T, H]
        batch_size, max_seq_length, hidden_size = states.size()

        mask = (torch.arange(max_seq_length, device=keys_length.device).repeat(
            batch_size, 1) == (keys_length.view(-1, 1) - 1))

        return states[mask]

    def cal_auxiliary_loss(
            self, states, click_seq, noclick_seq, keys_length):
        # states [B, T, H]
        # click_seq [B, T, H]
        # noclick_seq [B, T, H]
        # keys_length [B]
        batch_size, max_seq_length, embedding_size = states.size()

        mask = (torch.arange(max_seq_length, device=states.device).repeat(
            batch_size, 1) < keys_length.view(-1, 1)).float()

        click_input = torch.cat([states, click_seq], dim=-1)
        noclick_input = torch.cat([states, noclick_seq], dim=-1)
        embedding_size = embedding_size * 2

        click_p = self.auxiliary_net(
            click_input.view(
                batch_size * max_seq_length, embedding_size)).view(
                    batch_size, max_seq_length)[mask > 0].view(-1, 1)
        click_target = torch.ones(
            click_p.size(), dtype=torch.float, device=click_p.device)

        noclick_p = self.auxiliary_net(
            noclick_input.view(
                batch_size * max_seq_length, embedding_size)).view(
                    batch_size, max_seq_length)[mask > 0].view(-1, 1)
        noclick_target = torch.zeros(
            noclick_p.size(), dtype=torch.float, device=noclick_p.device)

        loss = F.binary_cross_entropy(
            torch.cat([click_p, noclick_p], dim=0),
            torch.cat([click_target, noclick_target], dim=0))

        return loss

    def forward(self, query, keys, keys_length, neg_keys=None):
        """
        Parameters
        ----------
        query: 2D tensor, [Batch, Hidden]
        keys: 3D tensor, [Batch, Time, Hidden]
        keys_length: 1D tensor, [Batch]
        neg_keys: 3D tensor, [Batch, Time, Hidden]

        Returns
        -------
        outputs: 2D tensor, [Batch, Hidden]
        """
        batch_size, max_length, dim = keys.size()

        packed_keys = pack_padded_sequence(
            keys,
            lengths=keys_length.squeeze().cpu(),
            batch_first=True,
            enforce_sorted=False)

        packed_interests, _ = self.interest_extractor(packed_keys)

        aloss = None
        if (self.gru_type != 'GRU') or self.use_negsampling:
            interests, _ = pad_packed_sequence(
                packed_interests,
                batch_first=True,
                padding_value=0.0,
                total_length=max_length)

            if self.use_negsampling:
                aloss = self.cal_auxiliary_loss(
                    interests[:, :-1, :],
                    keys[:, 1:, :],
                    neg_keys[:, 1:, :],
                    keys_length - 1)

        if self.gru_type == 'GRU':
            packed_interests, _ = self.interest_evolution(packed_interests)

            interests, _ = pad_packed_sequence(
                packed_interests,
                batch_first=True,
                padding_value=0.0,
                total_length=max_length)

            outputs = self.attention(query, interests, keys_length)

        elif self.gru_type == 'AIGRU':
            # attention
            scores = self.attention(query, interests, keys_length)
            interests = interests * scores.unsqueeze(-1)

            packed_interests = pack_padded_sequence(
                interests,
                lengths=keys_length.squeeze().cpu(),
                batch_first=True,
                enforce_sorted=False)
            _, outputs = self.interest_evolution(packed_interests)
            outputs = outputs.squeeze()

        elif self.gru_type == 'AGRU' or self.gru_type == 'AUGRU':
            # attention
            scores = self.attention(query, interests, keys_length)

            packed_interests = pack_padded_sequence(
                interests,
                lengths=keys_length.squeeze().cpu(),
                batch_first=True,
                enforce_sorted=False)

            packed_scores = pack_padded_sequence(
                scores,
                lengths=keys_length.squeeze().cpu(),
                batch_first=True,
                enforce_sorted=False)

            outputs, _ = pad_packed_sequence(
                self.interest_evolution(
                    packed_interests, packed_scores), batch_first=True)
            # pick last state
            outputs = Interest.get_last_state(
                outputs, keys_length.squeeze())

        return outputs, aloss
    
class AttentionGroup(object):
    def __init__(self, name, pairs,
                 hidden_layers, activation='dice', att_dropout=0.0,
                 gru_type='AUGRU', gru_dropout=0.0):
        self.name = name
        self.pairs = pairs
        self.hidden_layers = hidden_layers
        self.activation = activation
        self.att_dropout = att_dropout
        self.gru_type = gru_type
        self.gru_dropout = gru_dropout

        self.related_feature_names = set()
        self.neg_feature_names = set()
        for pair in pairs:
            self.related_feature_names.add(pair['ad'])
            self.related_feature_names.add(pair['pos_hist'])
            if 'neg_hist' in pair:
                self.related_feature_names.add(pair['neg_hist'])
                self.neg_feature_names.add(pair['neg_hist'])

    def is_attention_feature(self, feature_name):
        if feature_name in self.related_feature_names:
            return True
        return False

    def is_neg_sampling_feature(self, feature_name):
        if feature_name in self.neg_feature_names:
            return True
        return False

    @property
    def pairs_count(self):
        return len(self.pairs)
    
class DIEN(nn.Module):
    def __init__(self, num_features,cat_features,seq_features, 
                 cat_nums,embedding_size, attention_groups,
                 mlp_hidden_layers, mlp_activation='prelu', mlp_dropout=0.0,
                 use_negsampling = False,
                 d_out = 1
                 ):
        super().__init__()
        self.num_features = num_features
        self.cat_features = cat_features
        self.seq_features = seq_features
        self.cat_nums = cat_nums 
        self.embedding_size = embedding_size
        
        self.attention_groups = attention_groups
        
        self.mlp_hidden_layers = mlp_hidden_layers
        self.mlp_activation = mlp_activation
        self.mlp_dropout = mlp_dropout
        
        self.d_out = d_out
        self.use_negsampling = use_negsampling
        
        #embedding
        self.embeddings = OrderedDict()
        for feature in self.cat_features+self.seq_features:
            self.embeddings[feature] = nn.Embedding(
                self.cat_nums[feature], self.embedding_size, padding_idx=0)
            self.add_module(f"embedding:{feature}",self.embeddings[feature])

        self.sequence_poolings = OrderedDict()
        self.attention_poolings = OrderedDict()
        total_embedding_sizes = 0
        for feature in self.cat_features:
            total_embedding_sizes += self.embedding_size
        for feature in self.seq_features:
            if not self.is_neg_sampling_feature(feature):
                total_embedding_sizes += self.embedding_size
        
        #sequence_pooling
        for feature in self.seq_features:
            if not self.is_attention_feature(feature):
                self.sequence_poolings[feature] = MaxPooling(1)
                self.add_module(f"pooling:{feature}",self.sequence_poolings[feature])

        #attention_pooling
        for attention_group in self.attention_groups:
            self.attention_poolings[attention_group.name] = (
                self.create_attention_fn(attention_group))
            self.add_module(f"attention_pooling:{attention_group.name}",
                self.attention_poolings[attention_group.name])

        total_input_size = total_embedding_sizes+len(self.num_features)
        
        self.mlp = MLP(
            total_input_size,
            mlp_hidden_layers,
            dropout=mlp_dropout, batchnorm=True, activation=mlp_activation)
        
        self.final_layer = nn.Linear(mlp_hidden_layers[-1], self.d_out)
        self.apply(init_weights)
        
        
    def forward(self, x):
        final_layer_inputs = list()

        # linear
        number_inputs = list()
        for feature in self.num_features:
            number_inputs.append(x[feature].view(-1, 1))

        embeddings = OrderedDict()
        for feature in self.cat_features:
            embeddings[feature] = self.embeddings[feature](x[feature])

        for feature in self.seq_features:
            if not self.is_attention_feature(feature):
                embeddings[feature] = self.sequence_poolings[feature](
                    self.embeddings[feature](x[feature]))

        auxiliary_losses = []
        for attention_group in self.attention_groups:
            query = torch.cat(
                [embeddings[pair['ad']]
                 for pair in attention_group.pairs],
                dim=-1)
            pos_hist = torch.cat(
                [self.embeddings[pair['pos_hist']](
                    x[pair['pos_hist']]) for pair in attention_group.pairs],
                dim=-1)
            
            #hist_length = torch.sum(hist>0,axis=1)
            keys_length = torch.min(torch.cat(
                [torch.sum(x[pair['pos_hist']]>0,axis=1).view(-1, 1)
                 for pair in attention_group.pairs],
                dim=-1), dim=-1)[0]
    
            neg_hist = None
            if self.use_negsampling:
                neg_hist = torch.cat(
                    [self.embeddings[pair['neg_hist']](
                        x[pair['neg_hist']])
                     for pair in attention_group.pairs],
                    dim=-1)
                
            embeddings[attention_group.name], tmp_loss = (
                self.attention_poolings[attention_group.name](
                    query, pos_hist, keys_length, neg_hist))
            if tmp_loss is not None:
                auxiliary_losses.append(tmp_loss)

        emb_concat = torch.cat(number_inputs + [
            emb for emb in embeddings.values()], dim=-1)

        final_layer_inputs = self.mlp(emb_concat)

        output = self.final_layer(final_layer_inputs)
        
        auxiliary_avg_loss = None
        if auxiliary_losses:
            auxiliary_avg_loss = auxiliary_losses[0]
            size = len(auxiliary_losses)
            for i in range(1, size):
                auxiliary_avg_loss += auxiliary_losses[i]
            auxiliary_avg_loss /= size
            
        if  self.d_out==1:
            output = output.squeeze() 
            
        return output, auxiliary_avg_loss

    def create_attention_fn(self, attention_group):
        return Interest(
            attention_group.pairs_count * self.embedding_size,
            gru_type=attention_group.gru_type,
            gru_dropout=attention_group.gru_dropout,
            att_hidden_layers=attention_group.hidden_layers,
            att_dropout=attention_group.att_dropout,
            att_activation=attention_group.activation,
            use_negsampling=self.use_negsampling)
    
    def is_attention_feature(self, feature):
        for group in self.attention_groups:
            if group.is_attention_feature(feature):
                return True
        return False

    def is_neg_sampling_feature(self, feature):
        for group in self.attention_groups:
            if group.is_neg_sampling_feature(feature):
                return True
        return False

三,Movielens数据集完整范例

下面是一个基于Movielens评价数据集的DIEN完整范例,根据用户过去对一些电影的评价结果,来预测用户对候选电影是否会给好评。

这个数据集不大,用CPU就能跑。😁

1,准备数据

import numpy as np 
import pandas as pd 
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import QuantileTransformer
from sklearn.pipeline import Pipeline, FeatureUnion 
from sklearn.impute import SimpleImputer 
from collections import Counter

class CategoryEncoder(BaseEstimator, TransformerMixin):
    
    def __init__(self, min_cnt=5, word2idx=None, idx2word=None):
        super().__init__() 
        self.min_cnt = min_cnt
        self.word2idx = word2idx if word2idx else dict()
        self.idx2word = idx2word if idx2word else dict()

    def fit(self, x, y=None):
        if not self.word2idx:
            counter = Counter(np.asarray(x).ravel())

            selected_terms = sorted(
                list(filter(lambda x: counter[x] >= self.min_cnt, counter)))

            self.word2idx = dict(
                zip(selected_terms, range(1, len(selected_terms) + 1)))
            self.word2idx['__PAD__'] = 0
            if '__UNKNOWN__' not in self.word2idx:
                self.word2idx['__UNKNOWN__'] = len(self.word2idx)

        if not self.idx2word:
            self.idx2word = {
                index: word for word, index in self.word2idx.items()}

        return self

    def transform(self, x):
        transformed_x = list()
        for term in np.asarray(x).ravel():
            try:
                transformed_x.append(self.word2idx[term])
            except KeyError:
                transformed_x.append(self.word2idx['__UNKNOWN__'])

        return np.asarray(transformed_x, dtype=np.int64)

    def dimension(self):
        return len(self.word2idx)

class SequenceEncoder(BaseEstimator, TransformerMixin):
    def __init__(self, sep=' ', min_cnt=5, max_len=None,
                 word2idx=None, idx2word=None):
        super().__init__() 
        self.sep = sep
        self.min_cnt = min_cnt
        self.max_len = max_len

        self.word2idx = word2idx if word2idx else dict()
        self.idx2word = idx2word if idx2word else dict()

    def fit(self, x, y=None):
        if not self.word2idx:
            counter = Counter()

            max_len = 0
            for sequence in np.array(x).ravel():
                words = sequence.split(self.sep)
                counter.update(words)
                max_len = max(max_len, len(words))

            if self.max_len is None:
                self.max_len = max_len

            # drop rare words
            words = sorted(
                list(filter(lambda x: counter[x] >= self.min_cnt, counter)))

            self.word2idx = dict(zip(words, range(1, len(words) + 1)))
            self.word2idx['__PAD__'] = 0
            if '__UNKNOWN__' not in self.word2idx:
                self.word2idx['__UNKNOWN__'] = len(self.word2idx)

        if not self.idx2word:
            self.idx2word = {
                index: word for word, index in self.word2idx.items()}

        if not self.max_len:
            max_len = 0
            for sequence in np.array(x).ravel():
                words = sequence.split(self.sep)
                max_len = max(max_len, len(words))
            self.max_len = max_len

        return self

    def transform(self, x):
        transformed_x = list()

        for sequence in np.asarray(x).ravel():
            words = list()
            for word in sequence.split(self.sep):
                try:
                    words.append(self.word2idx[word])
                except KeyError:
                    words.append(self.word2idx['__UNKNOWN__'])

            transformed_x.append(
                np.asarray(words[0:self.max_len], dtype=np.int64))

        return np.asarray(transformed_x, dtype=object)
    
    def dimension(self):
        return len(self.word2idx)

    def max_length(self):
        return self.max_len
from sklearn.preprocessing import QuantileTransformer
from sklearn.pipeline import Pipeline 
from sklearn.impute import SimpleImputer 
from tqdm import tqdm 

dftrain = pd.read_csv("../data/ml_1m/train.csv")
dfval = pd.read_csv("../data/ml_1m/test.csv")

for col in ["movieId","histHighRatedMovieIds","negHistMovieIds","genres"]:
    dftrain[col] = dftrain[col].astype(str)
    dfval[col] = dfval[col].astype(str)

num_features = ['age']
cat_features = ['gender', 'movieId', 'occupation', 'zipCode']
seq_features = ['genres', 'histHighRatedMovieIds', 'negHistMovieIds']

num_pipe = Pipeline(steps = [('impute',SimpleImputer()),('quantile',QuantileTransformer())])

encoders = {}

print("preprocess number features...")
dftrain[num_features] = num_pipe.fit_transform(dftrain[num_features]).astype(np.float32)
dfval[num_features] = num_pipe.transform(dfval[num_features]).astype(np.float32)

print("preprocess category features...")
for col in tqdm(cat_features):
    encoders[col] = CategoryEncoder(min_cnt=5)
    dftrain[col]  = encoders[col].fit_transform(dftrain[col])
    dfval[col] =  encoders[col].transform(dfval[col])
    
print("preprocess sequence features...")
for col in tqdm(seq_features):
    encoders[col] = SequenceEncoder(sep="|",min_cnt=5)
    dftrain[col]  = encoders[col].fit_transform(dftrain[col])
    dfval[col] =  encoders[col].transform(dfval[col])
    
from collections import OrderedDict
from itertools import chain
from torch.utils.data import Dataset,DataLoader 

class Df2Dataset(Dataset):
    def __init__(self, dfdata, num_features, cat_features,
                 seq_features, encoders, label_col="label"):
        self.dfdata = dfdata
        self.num_features = num_features
        self.cat_features = cat_features 
        self.seq_features = seq_features
        self.encoders = encoders
        self.label_col = label_col
        self.size = len(self.dfdata)

    def __len__(self):
        return self.size

    @staticmethod
    def pad_sequence(sequence,max_length):
        #zero is special index for padding
        padded_seq = np.zeros(max_length, np.int32)
        padded_seq[0: sequence.shape[0]] = sequence
        return padded_seq

    def __getitem__(self, idx):
        record = OrderedDict()
        for col in self.num_features:
            record[col] = self.dfdata[col].iloc[idx].astype(np.float32)
            
        for col in self.cat_features:
            record[col] = self.dfdata[col].iloc[idx].astype(np.int64)
            
        for col in self.seq_features:
            seq = self.dfdata[col].iloc[idx]
            max_length = self.encoders[col].max_length()
            record[col] = Df2Dataset.pad_sequence(seq,max_length)

        if self.label_col is not None:
            record['label'] = self.dfdata[self.label_col].iloc[idx].astype(np.float32)
        return record

    def get_num_batches(self, batch_size):
        return np.ceil(self.size / batch_size)
    
ds_train = Df2Dataset(dftrain, num_features, cat_features, seq_features, encoders)
ds_val = Df2Dataset(dfval,num_features, cat_features, seq_features, encoders)
dl_train = DataLoader(ds_train, batch_size=128,shuffle=True)
dl_val = DataLoader(ds_val,batch_size=128,shuffle=False)

cat_nums = {k:v.dimension() for k,v in encoders.items()}
for batch in dl_train:
    break
print(cat_nums)

2,定义模型

def create_net():
    augru_attention_groups_with_neg = [
    AttentionGroup(
        name='group1',
        pairs=[{'ad': 'movieId', 'pos_hist': 'histHighRatedMovieIds', 'neg_hist': 'negHistMovieIds'}],
        hidden_layers=[16, 8], att_dropout=0.1, gru_type='AUGRU')
    ]

    net = DIEN(num_features=num_features,
           cat_features=cat_features,
           seq_features=seq_features,
           cat_nums = cat_nums,
           embedding_size=16,
           attention_groups=augru_attention_groups_with_neg,
           mlp_hidden_layers=[32,16],
           mlp_activation="prelu",
           mlp_dropout=0.25,
           use_negsampling=True,
           d_out=1
           )
    
    return net 

net = create_net() 

out,aloss = net.forward(batch)

from torchkeras.summary import summary 
summary(net,input_data=batch);

3,训练模型

import os,sys,time
import numpy as np
import pandas as pd
import datetime 
from tqdm import tqdm 

import torch
from torch import nn 
from accelerate import Accelerator
from copy import deepcopy


def printlog(info):
    nowtime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print("\n"+"=========="*8 + "%s"%nowtime)
    print(str(info)+"\n")
    
class StepRunner:
    def __init__(self, net, loss_fn,stage = "train", metrics_dict = None, 
                 optimizer = None, lr_scheduler = None,
                 accelerator = None
                 ):
        self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
        self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
        self.accelerator = accelerator
    
    def __call__(self, batch):
        #loss
        preds,aloss = self.net(batch)
        loss = self.loss_fn(preds,batch["label"])+aloss

        #backward()
        if self.optimizer is not None and self.stage=="train":
            if self.accelerator is  None:
                loss.backward()
            else:
                self.accelerator.backward(loss)
            self.optimizer.step()
            if self.lr_scheduler is not None:
                self.lr_scheduler.step()
            self.optimizer.zero_grad()
            
        #metrics
        step_metrics = {self.stage+"_"+name:metric_fn(preds, batch["label"]).item() 
                        for name,metric_fn in self.metrics_dict.items()}
        return loss.item(),step_metrics
    
    
class EpochRunner:
    def __init__(self,steprunner):
        self.steprunner = steprunner
        self.stage = steprunner.stage
        self.steprunner.net.train() if self.stage=="train" else self.steprunner.net.eval()
        
    def __call__(self,dataloader):
        total_loss,step = 0,0
        loop = tqdm(enumerate(dataloader), total =len(dataloader))
        for i, batch in loop:
            if self.stage=="train":
                loss, step_metrics = self.steprunner(batch)
            else:
                with torch.no_grad():
                    loss, step_metrics = self.steprunner(batch)

            step_log = dict({self.stage+"_loss":loss},**step_metrics)

            total_loss += loss
            step+=1
            if i!=len(dataloader)-1:
                loop.set_postfix(**step_log)
            else:
                epoch_loss = total_loss/step
                epoch_metrics = {self.stage+"_"+name:metric_fn.compute().item() 
                                 for name,metric_fn in self.steprunner.metrics_dict.items()}
                epoch_log = dict({self.stage+"_loss":epoch_loss},**epoch_metrics)
                loop.set_postfix(**epoch_log)

                for name,metric_fn in self.steprunner.metrics_dict.items():
                    metric_fn.reset()
        return epoch_log

class KerasModel(torch.nn.Module):
    def __init__(self,net,loss_fn,metrics_dict=None,optimizer=None,lr_scheduler = None):
        super().__init__()
        self.accelerator = Accelerator()
        self.history = {}
        
        self.net = net
        self.loss_fn = loss_fn
        self.metrics_dict = nn.ModuleDict(metrics_dict) 
        
        self.optimizer = optimizer if optimizer is not None else torch.optim.Adam(
            self.parameters(), lr=1e-2)
        self.lr_scheduler = lr_scheduler
        
        self.net,self.loss_fn,self.metrics_dict,self.optimizer = self.accelerator.prepare(
            self.net,self.loss_fn,self.metrics_dict,self.optimizer)

    def forward(self, x):
        if self.net:
            return self.net.forward(x)[0]
        else:
            raise NotImplementedError


    def fit(self, train_data, val_data=None, epochs=10, ckpt_path='checkpoint.pt', 
            patience=5, monitor="val_loss", mode="min"):
        
        train_data = self.accelerator.prepare(train_data)
        val_data = self.accelerator.prepare(val_data) if val_data else []

        for epoch in range(1, epochs+1):
            printlog("Epoch {0} / {1}".format(epoch, epochs))
            
            # 1,train -------------------------------------------------  
            train_step_runner = StepRunner(net = self.net,stage="train",
                    loss_fn = self.loss_fn,metrics_dict=deepcopy(self.metrics_dict),
                    optimizer = self.optimizer, lr_scheduler = self.lr_scheduler,
                    accelerator = self.accelerator)
            train_epoch_runner = EpochRunner(train_step_runner)
            train_metrics = train_epoch_runner(train_data)
            
            for name, metric in train_metrics.items():
                self.history[name] = self.history.get(name, []) + [metric]

            # 2,validate -------------------------------------------------
            if val_data:
                val_step_runner = StepRunner(net = self.net,stage="val",
                    loss_fn = self.loss_fn,metrics_dict=deepcopy(self.metrics_dict),
                    accelerator = self.accelerator)
                val_epoch_runner = EpochRunner(val_step_runner)
                with torch.no_grad():
                    val_metrics = val_epoch_runner(val_data)
                val_metrics["epoch"] = epoch
                for name, metric in val_metrics.items():
                    self.history[name] = self.history.get(name, []) + [metric]
            
            # 3,early-stopping -------------------------------------------------
            arr_scores = self.history[monitor]
            best_score_idx = np.argmax(arr_scores) if mode=="max" else np.argmin(arr_scores)
            if best_score_idx==len(arr_scores)-1:
                torch.save(self.net.state_dict(),ckpt_path)
                print("<<<<<< reach best {0} : {1} >>>>>>".format(monitor,
                     arr_scores[best_score_idx]),file=sys.stderr)
            if len(arr_scores)-best_score_idx>patience:
                print("<<<<<< {} without improvement in {} epoch, early stopping >>>>>>".format(
                    monitor,patience),file=sys.stderr)
                self.net.load_state_dict(torch.load(ckpt_path))
                break 
            
        return pd.DataFrame(self.history)

    @torch.no_grad()
    def evaluate(self, val_data):
        val_data = self.accelerator.prepare(val_data)
        val_step_runner = StepRunner(net = self.net,stage="val",
                    loss_fn = self.loss_fn,metrics_dict=deepcopy(self.metrics_dict),
                    accelerator = self.accelerator)
        val_epoch_runner = EpochRunner(val_step_runner)
        val_metrics = val_epoch_runner(val_data)
        return val_metrics
        
       
    @torch.no_grad()
    def predict(self, dataloader):
        dataloader = self.accelerator.prepare(dataloader)
        self.net.eval()
        result = torch.cat([self.forward(t) for t in dataloader])
        return result.data
from torchkeras.metrics import AUC

loss_fn = nn.BCEWithLogitsLoss()

metrics_dict = {"auc":AUC()}

optimizer = torch.optim.Adam(net.parameters(), lr=0.002, weight_decay=0.001) 
#lr_scheduler = torch.optim.lr_scheduler.CyclicLR(optimizer, base_lr=0.01, max_lr=0.1)

model = KerasModel(net,
                   loss_fn = loss_fn,
                   metrics_dict= metrics_dict,
                   optimizer = optimizer,
                   #lr_scheduler = lr_scheduler
                  )
dfhistory = model.fit(train_data=dl_train,val_data=dl_val,epochs=100, patience=10,
                      monitor = "val_auc",mode="max",ckpt_path='checkpoint.pt')
71628e74a5a9b3f144c2d2405f929506.jpeg

4,评估模型

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

import matplotlib.pyplot as plt

def plot_metric(dfhistory, metric):
    train_metrics = dfhistory["train_"+metric]
    val_metrics = dfhistory['val_'+metric]
    epochs = range(1, len(train_metrics) + 1)
    plt.plot(epochs, train_metrics, 'bo--')
    plt.plot(epochs, val_metrics, 'ro-')
    plt.title('Training and validation '+ metric)
    plt.xlabel("Epochs")
    plt.ylabel(metric)
    plt.legend(["train_"+metric, 'val_'+metric])
    plt.show()
plot_metric(dfhistory,"loss")
efe0738495d3d599e77f24b74e16bb14.jpeg
plot_metric(dfhistory,"auc")
0cf1cc1302005155138e10aee4907270.jpeg
model.evaluate(dl_val)

{'val_loss': 0.7020544648170471, 'val_auc': 0.6469045281410217}

5,使用模型

from sklearn.metrics import roc_auc_score 

labels = torch.tensor([x["label"] for x in ds_val])
preds = model.predict(dl_val)
val_auc = roc_auc_score(labels.cpu().numpy(),preds.cpu().numpy())
print(val_auc)
0.6469045283797497

6,保存模型

torch.save(model.net.state_dict(),"best_dien.pt")
net_clone = create_net()
net_clone.load_state_dict(torch.load("best_dien.pt"))
net_clone.eval()
labels = torch.tensor([x["label"] for x in ds_val])
preds = torch.cat([net_clone(x)[0].data for x in dl_val]) 
val_auc = roc_auc_score(labels.cpu().numpy(),preds.cpu().numpy())
print(val_auc)
0.6469045283797497

可以看到,相比DIN模型,验证集AUC从0.6392涨到了0.6469. 又是一波基操勿6!

公众号后台回复关键词:DIEN,获取本文全部源码和Movielens数据集下载链接。

以上。

万水千山总是情,点个在看行不行?

8b3ac5af42a9d88251ab1f42bb480e84.png

100天精通Python(爬虫篇)——第118天:selenium自动化操作浏览器基础大总结 一、Selenium框架环境搭建 1. 下载模块 2. 安装浏览器驱动WebDriver 二、基础操作 1. 打开浏览器 2. 无界面模式 3. 元素定位 4. 元素操作 5. 前进后退 6. 执行js 7.页面等待 隐式等待(常用) 显式等待(了解) 三、进阶操作 1. 使用代理ip 2. 修改请求头 3. 隐藏指纹特征..................... 阅读详情

相关推荐

红外气体检测方案解析:从NDIR原理到物联网终端设计实践

气体检测是工业安全与环境监测的核心需求,其技术原理主要基于气体分子对特定物理或化学信号的响应。非色散红外(NDIR)技术作为一种物理检测方法,通过测量气体分子对特定波长红外光的吸收来定量浓度,因其不依赖化学反应,具备寿命长、抗干扰强和响应快等优势。在工程实践中,红外气体检测方案的价值在于为物联网终端设备提供了高可靠、低功耗的感知能力,广泛应用于工业过程监控、环境空气质量监测以及智能家居安全预警等场景。本文以一套集成了Microchip MCU、Axetris红外光源和Pyreos热释电传感器的参考设计为例,

weixin_30954817的博客 396

PyTorch 深度学习实战 | DIEN 模拟兴趣演化的序列网络

DIEN 模拟兴趣演化的序列网络

不断学习,不断进步,提高自己 2825

手把手教你用VM实现欧姆龙PLC数据采集(含Fins协议配置详解)

本文详细介绍了如何使用VM(VisionMaster)实现与欧姆龙PLC的数据采集,重点解析了Fins协议(包括Fins TCP/UDP)的配置步骤。从网络环境搭建、PLC的IP地址设置,到VM端Fins通讯通道的详细配置与数据点映射,提供了完整的实战指南。文章还涵盖了数据收发解析、常见问题排查清单,帮助工程师快速完成视觉系统与PLC的稳定集成。

weixin_29171087的博客 189

pytorch_lesson13.5 Xavier方法与kaiming方法(HE初始化)解决激活函数sigmoid or tanh/relu梯度不均匀的问题

提示:仅仅是学习记录笔记,搬运了学习课程的ppt内容,本意不是抄袭!望大家不要误解!纯属学习记录笔记!!!!!! 文章目录前言一、Xavier方法1.Xavier初始化参数方法基础理论 前言 本部分内容介绍的是:参数初始化优化方法,也就是针对tanh和Sigmoid激活函数的Xavier方法,以及针对ReLU激活函数的Kaiming方法(HE初始化)。 一、Xavier方法 1.Xavier初始化参数方法基础理论 回顾Glorot条件,我们要求正向传播时候数据流经每一层前后的方差一致,并且反向传播时.

weixin_51589123的博客 2952

vins-mono保存、重载地图、evo工具测试

下面咱们来对vins-mono的重定位vins-mono官方源码地址:https://github.com/HKUST-Aerial-Robotics/VINS-Mono https://blog.youkuaiyun.com/learning_tortosie/article/details/83182258 运行这条: roslaunch benchmark_publisher publish.launc...

追 1万+

从入门到精通:dirmap 高级 Web 目录扫描实战指南

本文提供了一份从环境搭建到高级配置的dirmap Web目录扫描实战指南。详细解析了工具安装、核心配置文件dirmap.conf的深度调优,包括递归扫描、四种扫描模式(字典、爆破、爬虫、Fuzz)的实战应用,并演示了分阶段资产发现的完整工作流程,帮助安全人员高效发现隐藏目录与敏感文件。

rnn9storyteller的博客 482

DIEN-序列模型与推荐系统的结合

一:DIEN的进化动机 1,序列信息加强了最近行为对下次行为预测的影响。 2,序列模型能够学习到购买趋势的信息 如果放弃了序列信息,则模型学习时间和趋势这类信息的能力就会变弱,推荐模型就仍是基于用户所有购买历史的综合推荐,而不是针对下一次购买的推荐,显然从业务角度讲,后者才是推荐系统真正的目标。 二:DIEN的模型结构 从整体上看,模型仍然是输入层+embedding层+链接层+全链接神经网络层+输出层。 ...

qq_39691463的博客 1639

使用electron静默打印

使用electron静默打印 1.使用electron打印的理由 很多情况下程序中使用的打印都是用户无感知的。并且想要灵活的控制打印内容,往往需要借助打印机给我们提供的api再进行开发,这种开发方式非常繁琐,并且开发难度较大。 electron提供的打印api可以非常灵活的控制打印设置的显示,并且可以通过html来书写打印内容。 2.api ele...

w1366352655的博客 945

DIEN:深度兴趣演化网络

为了在效率与性能之间找到平衡,作者使用GRU(门控循环单元)来建模行为之间的依赖关系,GRU的输入是按时间顺序排列的用户行为。通过辅助损失和改进的GRU架构,DIEN能够更好地处理长序列行为数据,并优化用户兴趣的表示,推动广告推荐系统的发展。具体而言,兴趣演化层通过对每个GRU步骤的局部激活,使得与目标项相关的兴趣得以强化,减少来自兴趣漂移的干扰。兴趣演化层的目标是通过建模兴趣的演化过程,提高目标项CTR的预测准确度。在电商系统中,用户行为承载着潜在的兴趣,用户在每次行为后,兴趣都会发生变化。

Mike的博客 1442

【C语言学习疑难杂症】第20期:0x00007FF7F2821964处引发的异常0xC0000005写入位置 0x0000000000000

检查代码中是否存在野指针访问的情况,确保在进行写入操作之前,内存已经被正确分配或初始化,并且在使用完毕后进行了正确的释放。空指针访问:代码尝试在空指针地址(0x0000000000000000)处进行写入操作,导致访问冲突。检查代码中是否存在数组越界的情况,确保在进行写入操作之前,数组的索引值在合法范围内。检查代码中是否存在空指针访问的情况,确保在进行写入操作之前,指针已经被正确初始化。野指针访问:代码尝试在已释放或未初始化的内存地址进行写入操作,导致访问冲突。

学无止境 828

华为OD机试真题 新系统2026-04-01 C++ 实现【勇攀数字高峰】

请你在给定的数字地形图中寻找登山路径,数字代表当前位置的海拔高度,要求从最低海拔出发,不断攀登,最终到达最高山峰。你需要寻找所有满足条件的登山路径。地图已经保证最低海拔和最高山峰都只有一个。

MISAYAONE的博客 384

推荐系统(十二)阿里深度兴趣网络(二):DIEN模型(Deep Interest Evolution Network)

目前的CTR预估模型都直接把用户行为当做用户兴趣,简单的做个embedding后就进入到MLP中,并没有挖掘潜藏在这些行为之后用户的兴趣。DIEN则通过用户行为序列挖掘背后隐藏的用户兴趣及进化过程。

天泽28的专栏 1万+

PCL SAC-IA 初始配准算法【2025最新版】

采样一致性初始配准算法,简称SAC_IA。博客长期更新,本文最新更新时间为:2025年1月1日。新增参数设置建议

点云侠的博客 37万+

DIEN算法介绍

参考文献 Deep Interest Evolution Network for Click-Through Rate Prediction DIEN,全名Deep Interest Evolution Network,中文名为深度兴趣进化网络,在DIN中加了个E,Evolution,要知道,进化论也就是Evolution啊,把进化论放到名字里的算法,从此可见一斑。虽然人家其实跟进化论没啥关系,具...

suspend2014的博客 6113

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

推荐算法(十)——阿里深度兴趣进化网络 DIEN

目录1 介绍2 原理2.1 Behavior Layer2.2 Interest Extractor Layer2.3 Interest Evolving Layer3 总结写在最后 1 介绍 本文为 推荐系统专栏 的第十篇文章,也是阿里三部曲 DIN、DIEN、DSIN中的第二篇。 上篇文章介绍的 DIN,通过引入 Attention Layer 赋予用户行为不同的重要性权重,但却忽略了行为发生的时间顺序。 本篇的深度兴趣进化网络 DIEN,就是针对行为的时间顺序进行建模,挖掘用户的兴趣及兴趣变化趋势。

予以初始的博客 2106

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

推荐系统 之 DIEN

1. DIEN的由来 其实,之前我们讨论的所有模型都是基于用户的过往历史进行物品的推荐,但是这个想法往往缺少了一个很重要的维度去模拟我们日常挑选商品的行为,那就是时间序列信息。为什么说时间序列信息对推荐来说是有价值的呢?一个典型的电商用户的现象可以说明这一点,我们在日常消费,或者日常逛淘宝的时候,其实发现自己的兴趣迁移是非常快的,例如用户在挑选一双篮球鞋的时候,这位用户上周的行为序列都会集中在篮球鞋整个品类的商品上,但在他完成购买后,本周他的购物兴趣可能变成买一个机械键盘了。但是用以往的模...

Francis_s的博客 1919

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

CTR之行为序列建模用户兴趣:DIEN

在CTR模型中,**通过用户的行为数据捕获用户的潜在兴趣是非常重要的**。另外,考虑到外在环境和内在认知的变化,**用户的兴趣是随着时间在动态演变的**。

sgyuanshi的博客 2579

pytorch中.weight.data与.weight的区别

pytorch中.weight.data与.weight 查看两个方法的类型 .weight.data:得到的是一个Tensor的张量(向量) .weight:得到的是一个parameter的变量,可以计算梯度的 查看两个方法的值

kevin的博客 1万+

DIEN论文解读

摘要:对于CTR预测模型,捕获用户行为数据背后潜在的用户兴趣是有必要的。此外,考虑到外部环境变化和内部认知的变化,用户兴趣会随着时间的推移而动态变化。目前,有一些CTR预估模型用于兴趣建模,但大多数都直接将行为表征作为兴趣,缺乏对具体行为背后的潜在兴趣进行针对性建模。此外,很少有工作考虑到兴趣的变化趋势。本文中,我们提出了一种新的模型——深度兴趣演化网络(DIEN)。具体来说,我们设计兴趣提取器层(interest extractor layer)以从历史行为序列中捕获时序兴趣。在这一层,我们引入辅助损失(

qq_35985044的博客 961

推荐系统之DIEN

推荐系统之DIEN 主要贡献: • We focus on interest evolving phenomenon in e-commerce system, and propose a new structure of network to model interest evolving process. The model for interest evolution leads to more expressive interest representation and more precise

张先生-您好的博客 921

DIN模型与DIEN模型总结

DIN模型(Deep Interest Network) DIN模型是阿里妈妈团队提出的CTR预估模型,虽然是几年前提出的,但是现在应用仍比较广泛。 原论文地址:Deep Interest Network for Click-Through Rate Prediction 对论文的核心总结:利用用户历史行为序列信息,使用类似Attetion的机制动态构建用户兴趣embeeding,使得模型能够捕获用户的兴趣。 该论文的主要贡献: 提出DIN模型,利用用户兴趣信息进行建模。 提出mini-batch awa

rotation博客 6107

MindSpore实现DIEN网络

MindSpore实现DIEN网络代码仓库地址DIEN网络模型概述数据预处理模型构建一:行为序列层(Behavior Layer)二:兴趣提取层(Interest Extractor Layer)三:兴趣进化层(Interest Evolving Layer)四:全连接层五:代码实现 代码仓库地址 https://github.com/sduzjp/DIEN DIEN网络模型概述 DIEN(Deep Interest Evolution Network)是阿里提出的应用于CTR预测的深度兴趣进化网络。 论文

zjp的博客 635

深度兴趣网络模型探索——DIN+DIEN+DSIN

目录 DIN 背景 din模型结构提出 自适应正则提出 相关延伸 系统概述 推荐流程介绍 特征工程 基本模型结构 DIN模型 自适应正则 DIEN 背景 DIEN模型提出 DIEN关键点 DIEN模型 兴趣提取层 兴趣演化层 DIEN模型结构 DSIN 背景 DSIN模型提出 DSIN改进点 DSIN模型 会话划分层 会话兴趣提取层 会话兴趣...

baymax_007的博客 1万+

Python实现基于深度学习的电影推荐系统

接着,基础的协同过滤算法通过分析用户的历史行为,找出具有相似口味的用户,并推荐他们喜欢的电影给目标用户。该系统利用深度学习技术,根据用户的观影历史和偏好,为每个用户提供量身定制的电影推荐[1]。随着技术的不断进步,未来的电影推荐系统可能会更加智能化和个性化。数据集应包含用户ID、电影ID、用户对电影的评分以及其他相关信息,如电影类型、导演、演员等。此外,设计了高效的推荐服务,能够快速响应新用户和新评分,实现实时推荐[1]。最后,我们可以创建一个简单的用户界面,允许用户输入他们的偏好,并显示推荐的电影。

weixin_46863529的博客 1453

阿里 CTR模型 DIEN 代码解读

前提 github:https://github.com/mouna99/dien/tree/1f314d16aa1700ee02777e6163fb8ca94e3d2810/script 阿里CTR模型三剑客即 (1)Deep Interest Network for Click-Through Rate Prediction (2)Deep Interest Evolution Network for Click-Through Rate Prediction (3)Deep Session

爱吃火锅的博客 3125

[CTR模型] DIEN(Deep Interest Evolution Network)模型解读与Deepctr实现

文章目录背景基本原理1. 模型的输入2. Interest Extractor Layer3. Interest Evolving LayerDeepctr实现 背景 DIEN(Deep Interest Evolution Network)是DIN的改进版,先回顾一下DIN模型: DIN模型的核心思想是:在用户兴趣多样性的推荐背景下,利用用户的历史行为数据,通过Attention机制来捕捉被推荐物品的相对兴趣。DIN模型没有考虑到用户的兴趣的动态变化,如何在CTR中捕捉到用户兴趣的发展变化,就是DIEN

zhong_ddbb的博客 2825
上一篇: 100分钟吃掉DIN深度兴趣网络
下一篇: einsum is all you needed
算法美食屋
博客等级 码龄9年 2万+粉丝 154原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值