本文主要介绍如何在昇腾上使用pytorch对推荐系统中经典的网络模型Din进行训练的实战讲解,使用数据集是Amazon中book数据集,主要内容分为以下几个模块:

  • Din网络创新点介绍
  • Din网络架构剖析及搭建

    • Activation Unit介绍
    • Attention模块
    • Din网络构建
  • 使用Amazon-book数据集训练Din网络实战

    • Amazon-book数据集介绍
    • Amazon-book数据集预处理
    • 训练集与测试集划分
    • 模型训练过程定义
    • 使用Amazon-book训练wideWeep模型
    • 评估模型性能

CTR(Click-through rate prediction)是工业应用(如在线广告)中的一项重要任务,处理这一类任务较为常用的方法是基于深度学习的方式,通过类似多层感知机的方式,将大规模的稀疏输入特征向量映射为低维的嵌入向量后以组为单位将其转化为固定长度的向量,最后将这些向量进行拼接输入到多层感知机中来学习特征之间的非线性关系。

在embedding阶段,所有的离散特征都要被映射成一个固定长度的低维稠密向量。离散特征一般有单值特征和多值特征,分别要做one-hot编码和multi-hot编码,单值特征没什么问题,直接映射成embedding向量即可,而对于多值特征,比如:用户点击过的item序列,用户点击过item类目的序列,通常的操作都是每个item映射成一个embedding向量,然后做一个sum/average pooling,最终得到一个embedding向量。

从图中可以看出在做多值特征pooling的时候并不会考虑每个item的重要性,但是在实际的应用过程中我们知道一个爱游泳的人,之前购买书籍、冰淇淋、薯片、游泳镜。给他推荐护目镜后他是否会点击这次推荐广告,跟之前购买的书籍、薯片与冰淇淋无关,而是跟他以前购买的游泳帽相关。也就是在这一次CTR预估中,部分历史数据(游泳镜)起到了决定作用,而其他的基本都无用,所以最好的方式就是给每个item一个权重。

DIN网络给每个item的权重是通过目标广告与该商品item之间的相关性决定,比如在上述例子中,候选广告推荐用户是否点击是护目镜,与用户购买的书籍、冰淇淋与薯片相关性较少,与游泳镜的相关性更大。

Din网络创新点介绍

  • 将注意力机制应用于用户兴趣建模,可以更好评估不同项的重要性差异,自适应的学习用户每个历史行为项的权重,使能模型可以更好的捕捉用户对不同项的兴趣。
  • 通过使用局部激活单元方式,来计算用户对每个历史行为的兴趣权重,是注意力机制中有效实现方式。
  • 采用Mini-batch Perceptron Regularizer节省参数计算量,避免过拟合。
  • 使用Data-dependent Activation Function也就是自适用的数据激活函数

Din网络架构剖析及搭建

当前广告模型基本上遵循的都是embedding\&MLP组合模式(图中basemodel部分),而Din网络通过在embedding层与MLP之间加入Attention机制有效的解决了推荐广告与用户历史购买行为之间相关性的问题,其网络架构重点由两部分组成,一个是激活模块(图中的Activation Unit),另一个是Attention模块(图中的Sumpooling),接下来将分别介绍这两部分的原理及如何使用pytorch搭建这两部分从而构建整个Din网络。

Activation Unit介绍

Activation Unit作为Din网络核心模块,通过将用户历史点击过的商品Id(Inputs from User)与候选广告商品的Id(Inputs from Ad)通过Out Product的方式计算相关性。计算相关性后通过concat的方式将原始输入与相关性计算后的输入进行shortcut连接载通过激活与线性层进行输出。关于'PRelu'与'Dice'模块也就是Din网络中引入的自适适应的数据激活函数。

Prelu激活函数计算公式:

$$ f(x)= \\begin{cases} s\& \\text{if s\> 0}\\ 1\& \\text{if s \<= 0} \\end{cases} = p(s) .s + (1-p(s)).as $$

Dice激活函数计算公式:

$$ f(x)= p(s) .s + (1-p(s)).as $$

两者计算表达式一致,其通过p(s)的计算公式不一样来控制激活的取值,关于两者p(s)的取值如下所示:

Dice中p(s)计算公式:

$$ p(s) = \\frac{1}{(1 + e\^{-\\frac{s-E\[s\]}{ \\sqrt{Var\[s\] + epsilon} }})} $$

式中E[s]和Var[s]分别是均值与方差,epsilon是一个常量,默认取值是10^-8。

Dice是PReLu的推广,其核心思想是根据输入数据的分布自适应地调整校正点。Dice可以平滑地在两个通道之间切换,并且当E[s] = 0并Var[s]=0时,Dice退化为PReLU,Dice通过定义一个Dice类实现,'forward()'函数中实现了上述定义公式的功能。

# python自定义包
import os
import pandas as pd
import numpy as np
# 导入pytorch相关包
import torch
import torch.nn as nn
# 根据公式计算p(s)
class Dice(nn.Module):
    def __init__(self):
        super(Dice, self).__init__()
        self.alpha = nn.Parameter(torch.zeros((1,)))
        self.epsilon = 1e-9

    def forward(self, x):
        # 公式中分母部分e的指数部分计算
        norm_x = (x - x.mean(dim=0)) / torch.sqrt(x.var(dim=0) + self.epsilon)
        p = torch.sigmoid(norm_x)
        x = self.alpha * x.mul(1-p) + x.mul(p)

        return x

整个激活模块通过ActivationUnit类实现,该模块功能是计算用户购买行为与推荐目标之间的注意力系数,比如说用户虽然用户买了这个东西,但是这个东西实际上和推荐目标之间没啥关系,也不重要,所以要乘以一个小权重,如果购买的东西跟推荐目标之间有直接因果关系,则需要乘以一个较大的权重。

class ActivationUnit(nn.Module):
    def __init__(self, embedding_dim, dropout=0.2, fc_dims = [32, 16]):
        super(ActivationUnit, self).__init__()
        # 1.初始化fc层
        fc_layers = []
        # 2.输入特征维度这里将输入的embedding_dim乘以4是将中间层的节点数放大了4倍,因为将4个模块的向量进行了concat
        input_dim = embedding_dim*4
        # 3.fc层内容:全连接层(4*embedding, 32)--->激活函数->dropout->全连接层(32,16)->.....->全连接层(16,1)
        for fc_dim in fc_dims:
            fc_layers.append(nn.Linear(input_dim, fc_dim))
            fc_layers.append(Dice())
            fc_layers.append(nn.Dropout(p = dropout))
            input_dim = fc_dim

        fc_layers.append(nn.Linear(input_dim, 1))
        # 4.将上面定义的fc层,整合到sequential中
        self.fc = nn.Sequential(*fc_layers)

    def forward(self, query, user_behavior):
        """
        query:targe目标的embedding ->(输入维度) batch*1*embed 
        user_behavior:行为特征矩阵 ->(输入维度) batch*seq_len*embed
        out:预测目标与历史行为之间的注意力系数
        """
        # 1.获取用户历史行为序列长度
        seq_len = user_behavior.shape[1]
        # 2.序列长度*embedding
        queries = torch.cat([query] * seq_len, dim=1)
        # 3.前面的把四个embedding合并成一个(4*embedding)的向量,
        #  第一个向量是目标商品的向量,第二个向量是用户行为的向量,
        #  至于第三个和第四个则是他们的相减和相乘(这里猜测是为了添加一点非线性数据用于全连接层,充分训练)
        attn_input = torch.cat([queries, user_behavior, queries - user_behavior, 
                                queries * user_behavior], dim = -1)
        out = self.fc(attn_input)
        return out

Attention模块

Attention模块是Din网络中注意力序列层,其功能是计算用户行为与预测目标之间的系数,并将所有的向量进行相加,这里的目的是计算出用户的兴趣的向量。通过'AttentionPoolingLayer'这个类实现该功能。主要通过'active_unit()'引入了目标和历史行为之间的相关性。

class AttentionPoolingLayer(nn.Module):

    def __init__(self, embedding_dim,  dropout):
        super(AttentionPoolingLayer, self).__init__()
        self.active_unit = ActivationUnit(embedding_dim = embedding_dim, 
                                          dropout = dropout)

    def forward(self, query_ad, user_behavior, mask):
        """
        query_ad:targe目标x的embedding   -> (输入维度) batch*1*embed
        user_behavior:行为特征矩阵     -> (输入维度) batch*seq_len*embed
        mask:被padding为0的行为置为false  -> (输入维度) batch*seq_len*1
        output:用户行为向量之和,反应用户的爱好
        """
        # 1.计算目标和历史行为之间的相关性
        attns = self.active_unit(query_ad, user_behavior)
        # 2.注意力系数乘以行为 
        output = user_behavior.mul(attns.mul(mask))
        # 3.历史行为向量相加
        output = user_behavior.sum(dim=1)
        return output

Din网络构建

基于上述搭建好的Activation Unit与AttentionPoolingLayer模块,可以构建DeepInterestNet网络,本文实现的网络主要是用在Amazon-book数据集上进行测试,其主要功能是用户最近的历史40个购买物品是xxx时,购买y的概率是多少?

class DeepInterestNet(nn.Module):

    def __init__(self, feature_dim, embed_dim, mlp_dims, dropout):
        super(DeepInterestNet, self).__init__()
        # 1.特征维度,就是输入的特征有多少个类
        self.feature_dim = feature_dim
        # 2.embeding层,将特征数值转化为向量
        self.embedding = nn.Embedding(feature_dim+1, embed_dim)
        # 3.注意力计算层(论文核心)
        self.AttentionActivate = AttentionPoolingLayer(embed_dim, dropout)
        # 4.定义fc层
        fc_layers = []
        # 5.该层的输入为历史行为的embedding,和目标的embedding,所以输入维度为2*embedding_dim
        #  全连接层(2*embedding,fc_dims[0])--->激活函数->dropout->全连接层(fc_dims[0],fc_dims[1])->.....->全连接层(fc_dims[n],1)
        input_dim = embed_dim * 2
        for fc_dim in mlp_dims:
            fc_layers.append(nn.Linear(input_dim, fc_dim))
            fc_layers.append(nn.ReLU())
            fc_layers.append(nn.Dropout(p = dropout))
            input_dim = fc_dim
        fc_layers.append(nn.Linear(input_dim, 1))
        # 6.将所有层封装
        self.mlp = nn.Sequential(*fc_layers)

    def forward(self, x):
        """
        x输入(behaviors*40,ads*1) ->(输入维度) batch*(behaviors+ads) 
        """
        # 1.排除掉推荐目标
        behaviors_x = x[:,:-1]
        # 2.记录之前填充为0的行为位置
        mask = (behaviors_x > 0).float().unsqueeze(-1)
        # 3.获取推荐的目标
        ads_x = x[:,-1]
        # 4.对推荐目标进行向量嵌入
        query_ad = self.embedding(ads_x).unsqueeze(1)
        # 5.对用户行为进行embeding,注意这里的维度为(batch*历史行为长度*embedding长度)
        user_behavior = self.embedding(behaviors_x)
        # 6.矩阵相乘,将那些行为为空的地方全部写为0
        user_behavior = user_behavior.mul(mask)
        # 7.将用户行为乘上注意力系数,再把所有行为记录向量相加
        user_interest = self.AttentionActivate(query_ad, user_behavior, mask)
        # 8.将计算后的用户行为行为记录和推荐的目标进行拼接
        concat_input = torch.cat([user_interest, query_ad.squeeze(1)], dim = 1)
        # 9.输入用户行为和目标向量,计算预测得分
        out = self.mlp(concat_input)
        # 10.sigmoid激活函数
        out = torch.sigmoid(out.squeeze(1))
        return out

使用Amazon-book数据集训练Din网络实战

综上所述,我们已经构建好了一个Din模型,现在可以尝试使用Amazon数据集来训练该网络,首先我们先料了解一下什么Amazon数据集是什么?

Amazon-book数据集介绍

Amazon-Book数据集是由亚马逊公司提供的大规模图书评论数据集,包含了亚马逊2000年至2014年之间大量的图书评论信息。是当前最大、最全面的图书评论数据集之一。

Amazon-Book数据集主要包括以下内容:

  • 用户ID:标识每个参与评论的用户。
  • 物品ID:标识每本书的唯一标识符。
  • 评论文本:用户对书籍的具体评论内容。
  • 评分:用户对书籍的评分,通常在1到5分之间。

这些数据提供了丰富的信息,可以用于分析用户行为、优化产品设计、改进营销策略等‌

# 使用padas库加载amazon-book数据集
data = pd.read_csv('/home/pengyongrong/workspace/DinModel/DIN-CODE/amazon-books-100k.txt')

将加载好的数据集进行打印,数据总共包含89999个样例,'label'表示用户是否会点击购买推荐的广告商品,0表示不点击、1表示点击;'userID'是用来标记用户的;'itemID'用来标识书籍;'cateID'标识书籍属的类别;'hit_item_list‌'指的是推荐给用户的商品或内容列表;‌hit_cate_list指的是推荐给用户的商品内容所述的类别。

data

| | label | userID | itemID | cateID | hist_item_list | hist_cate_list |
| 0 | 0 | AZPJ9LUT0FEPY | B00AMNNTIA | Literature \& Fiction | 0307744434|0062248391|0470530707|0978924622|15... | Books|Books|Books|Books|Books |
| 1 | 1 | AZPJ9LUT0FEPY | 0800731603 | Books | 0307744434|0062248391|0470530707|0978924622|15... | Books|Books|Books|Books|Books |
| 2 | 0 | A2NRV79GKAU726 | B003NNV10O | Russian | 0814472869|0071462074|1583942300|0812538366|B0... | Books|Books|Books|Books|Baking|Books|Books |
| 3 | 1 | A2NRV79GKAU726 | B000UWJ91O | Books | 0814472869|0071462074|1583942300|0812538366|B0... | Books|Books|Books|Books|Baking|Books|Books |
| 4 | 0 | A2GEQVDX2LL4V3 | 0321334094 | Books | 0743596870|0374280991|1439140634|0976475731 | Books|Books|Books|Books |
| ... | ... | ... | ... | ... | ... | ... |
| 89994 | 0 | A3CV7NJJC20JTB | 098488789X | Books | 034545197X|0765326396|1605420832|1451648448 | Books|Books|Books|Books |
| 89995 | 1 | A3CV7NJJC20JTB | 0307381277 | Books | 034545197X|0765326396|1605420832|1451648448 | Books|Books|Books|Books |
| 89996 | 0 | A208PSIK2APSKN | 0957496184 | Books | 0515140791|147674355X|B0055ECOUA|B007JE1B1C|B0... | Books|Books|Bibles|Literature \& Fiction|Litera... |
| 89997 | 1 | A208PSIK2APSKN | 1480198854 | Books | 0515140791|147674355X|B0055ECOUA|B007JE1B1C|B0... | Books|Books|Bibles|Literature \& Fiction|Litera... |

899980A1GRLKG8JA19OAB0095VGR4ILiterature \& Fiction031612091X\0399163832\1442358238\1118017447Books\Books\Books\Books

89999 rows × 6 columns

通过对数据集分析发现,许多商品id只出现了一次,因此在编码的时候以类别作为编码和预测的目标输入给模型进行训练。由于每一个用户的历史购买行为均不一致,因此会导致长度也会不一样,这里截取了40个历史行为作为标准,用来获取用户历史购买信息与推荐广告商品的相关性,如果不足40则填充0到40,若大于40个行为,则截取最近40个行为(也就是后40个行为)。

'cate_encoder'是全局变量,用来编码类别标签的,在后续测试过程中也会用到。整个预处理过程分为8个步骤均在代码出注解。

Amazon-book数据集预处理

#导入sklearn相关包,主要用到LabelEncoder用来自动编码类别
from sklearn.preprocessing import LabelEncoder

cate_encoder = None
def AmazonBookPreprocess(dataframe, seq_len=40):
    """
    数据集处理
    dataframe: 未处理的数据集
    seq_len: 数据序列长度
    data: 处理好的数据集
    """
    # 1.按'|'切割,用户历史购买数据,获取item的序列和类别的序列
    data = dataframe.copy()
    data['hist_item_list'] = dataframe.apply(lambda x: x['hist_item_list'].split('|'), axis=1)
    data['hist_cate_list'] = dataframe.apply(lambda x: x['hist_cate_list'].split('|'), axis=1)

    # 2.获取cate的所有种类,为每个类别设置一个唯一的编码
    cate_list = list(data['cateID'])
    _ = [cate_list.extend(i) for i in data['hist_cate_list'].values]

    # 3.将编码去重,'0' 作为padding的类别
    cate_set = set(cate_list + ['0'])

    # 4.截取用户行为的长度,也就是截取hist_cate_list的长度,生成对应的列名
    cols = ['hist_cate_{}'.format(i) for i in range(seq_len)]

    # 5.截取前40个历史行为,如果历史行为不足40个则填充0
    def trim_cate_list(x):
        if len(x) > seq_len:
            # 5.1历史行为大于40, 截取后40个行为
            return pd.Series(x[-seq_len:], index=cols)
        else:
            # 5.2历史行为不足40, padding到40个行为
            pad_len = seq_len - len(x)
            x = x + ['0'] * pad_len
            return pd.Series(x, index=cols)

    # 6.预测目标的类别
    labels = data['label']
    data = data['hist_cate_list'].apply(trim_cate_list).join(data['cateID'])

    # 7.生成类别对应序号的编码器,如book->1,Russian->2这样
    global cate_encoder
    cate_encoder = LabelEncoder().fit(list(cate_set))
    print("cate_encoder is:  ", cate_encoder)

    # 8.这里分为两步,第一步为把类别转化为数值,第二部为拼接上label
    data = data.apply(cate_encoder.transform).join(labels)
    return data
# 对数据执行预处理操作,该操作由于是对表格逐行进行处理,因此可能会需要一点时间
data = AmazonBookPreprocess(data)
cate_encoder is:   LabelEncoder()

将data信息进行打印可以看到用户历史购买的序列与label数据集存储在data中总共包含40个用户历史行为类别信息,大部分用户历史购买数据长度均未达到40,因此在每一个用户后填充0。

每一行中具体的数字表示表示用户购买物品所属的类别,比如0行中138、138、138、138、138 表示用户0购买5个历史物品均属于138这个类别,推荐的商品类别是734,因此label值=0,表示用户不会点击购买推荐的734商品。

而第1行恰恰相反,138、138、138、138、138 表示用户1购买5个历史物品均属于138这个类别,推荐的商品类别是138,因此label值=1,表示用户很大概率会点击购买推荐的734商品。

data

| | hist_cate_0 | hist_cate_1 | hist_cate_2 | hist_cate_3 | hist_cate_4 | hist_cate_5 | hist_cate_6 | hist_cate_7 | hist_cate_8 | hist_cate_9 | ... | hist_cate_32 | hist_cate_33 | hist_cate_34 | hist_cate_35 | hist_cate_36 | hist_cate_37 | hist_cate_38 | hist_cate_39 | cateID | label |
| 0 | 138 | 138 | 138 | 138 | 138 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 734 | 0 |
| 1 | 138 | 138 | 138 | 138 | 138 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 1 |
| 2 | 138 | 138 | 138 | 138 | 95 | 138 | 138 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1071 | 0 |
| 3 | 138 | 138 | 138 | 138 | 95 | 138 | 138 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 1 |
| 4 | 138 | 138 | 138 | 138 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 89994 | 138 | 138 | 138 | 138 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 0 |
| 89995 | 138 | 138 | 138 | 138 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 1 |
| 89996 | 138 | 138 | 115 | 734 | 734 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 0 |
| 89997 | 138 | 138 | 115 | 734 | 734 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 138 | 1 |

89998138138138138000000...000000007340

89999 rows × 42 columns

训练集与测试集划分

通过上述预处理后,我们得到了用户的历史输入数据与推商品的关联数据集,接下来我们需要将上述数据集进行划分以训练集、验证集与测试集。以便模型在训练好的能拥有更好泛化性与可靠性。

训练集与测试集的划分采用'train_test_split',该方法集成在'sklearn.model_selection'库中,提供了数据切分、交叉验证与网格搜索等功能,本文用到的是数据划分功能。

将模型输入与输出进行组合划分训练集、测试集与验证集三个集合,首先从总体数据量上将训练集与测试集以8:2(test_size = 0.2)的比例进行划分,然后再讲训练集中数据按照7.5:2.5(test_size = 0.25)比例划分训练集与验证集。

import torch.utils.data as Data
from sklearn.model_selection import train_test_split

#模型输入
data_X = data.iloc[:,:-1]
#模型输出
data_y = data.label.values

#划分训练集,测试集,验证集
tmp_X, test_X, tmp_y, test_y = train_test_split(data_X, data_y, test_size = 0.2, random_state=42, stratify=data_y)
train_X, val_X, train_y, val_y = train_test_split(tmp_X, tmp_y, test_size = 0.25, random_state=42, stratify=tmp_y)
dis_test_x = test_X
dis_test_y = test_y

# numpy转化为torch
train_X = torch.from_numpy(train_X.values).long()
val_X = torch.from_numpy(val_X.values).long()
test_X = torch.from_numpy(test_X.values).long()

train_y = torch.from_numpy(train_y).long()
val_y = torch.from_numpy(val_y).long()
test_y = torch.from_numpy(test_y).long()

# 设置dataset
train_set = Data.TensorDataset(train_X, train_y)
val_set = Data.TensorDataset(val_X, val_y)
test_set = Data.TensorDataset(test_X, test_y)

# 设置数据集加载器,用于模型训练,按批次输入数据
train_loader = Data.DataLoader(dataset=train_set, batch_size=32, shuffle=True)
val_loader = Data.DataLoader(dataset=val_set, batch_size=32, shuffle=False)
test_loader = Data.DataLoader(dataset=test_set, batch_size=32, shuffle=False)

模型训练过程定义

上述步骤已经分别将模型构建与数据集处理完成,接下来就是需要对模型进行训练。由于本文实验数据与模型需要运行在npu上,因此需要指定'device'是'npu',此外,该模型实现使用pytorch,目前npu对pytorch适配度非常高,可以使用'transfer_to_npu'将pytorch模型无感迁移到npu上运行。

import torch_npu
from torch_npu.contrib import transfer_to_npu
device="npu"
/home/pengyongrong/miniconda3/envs/ctrExperiments/lib/python3.9/site-packages/torch_npu/contrib/transfer_to_npu.py:211: ImportWarning: 
    *************************************************************************************************************
    The torch.Tensor.cuda and torch.nn.Module.cuda are replaced with torch.Tensor.npu and torch.nn.Module.npu now..
    The torch.cuda.DoubleTensor is replaced with torch.npu.FloatTensor cause the double type is not supported now..
    The backend in torch.distributed.init_process_group set to hccl now..
    The torch.cuda.* and torch.cuda.amp.* are replaced with torch.npu.* and torch.npu.amp.* now..
    The device parameters have been replaced with npu in the function below:
    torch.logspace, torch.randint, torch.hann_window, torch.rand, torch.full_like, torch.ones_like, torch.rand_like, torch.randperm, torch.arange, torch.frombuffer, torch.normal, torch._empty_per_channel_affine_quantized, torch.empty_strided, torch.empty_like, torch.scalar_tensor, torch.tril_indices, torch.bartlett_window, torch.ones, torch.sparse_coo_tensor, torch.randn, torch.kaiser_window, torch.tensor, torch.triu_indices, torch.as_tensor, torch.zeros, torch.randint_like, torch.full, torch.eye, torch._sparse_csr_tensor_unsafe, torch.empty, torch._sparse_coo_tensor_unsafe, torch.blackman_window, torch.zeros_like, torch.range, torch.sparse_csr_tensor, torch.randn_like, torch.from_file, torch._cudnn_init_dropout_state, torch._empty_affine_quantized, torch.linspace, torch.hamming_window, torch.empty_quantized, torch._pin_memory, torch.autocast, torch.load, torch.Generator, torch.Tensor.new_empty, torch.Tensor.new_empty_strided, torch.Tensor.new_full, torch.Tensor.new_ones, torch.Tensor.new_tensor, torch.Tensor.new_zeros, torch.Tensor.to, torch.nn.Module.to, torch.nn.Module.to_empty
    *************************************************************************************************************
  warnings.warn(msg, ImportWarning)

通常来说,训练一个模型通常需要定义损失函数、定义优化器、定义模型参数可更新及遍历数据集训练模型四个步骤。前三个步骤比较直接,最后一个步骤在使用数据集训练模型过程中,需要根据输入数据给到模型获得预测的结果,计算损失、反向传播后进行参数更新。

据上述分析,我们定义了'train(model)'函数来实现整个模型的训练过程,入参为'model'表示需要训练的模型,具体的步骤解释均在代码中注解。

import torch.optim as optim
import torch.nn.functional as F
from sklearn.metrics import roc_auc_score
def train(model):
    # 1.设置迭代次数训练模型
    for epoch in range(epoches):
        train_loss = []
        # 1.1设置二分类交叉熵损失函数
        criterion = nn.BCELoss()
        # 1.2设置adam优化器
        optimizer = optim.Adam(model.parameters(), lr = 0.001)
        # 1.3设置模型训练,此时模型参数可以更新
        model.train()
        # 1.4遍历训练数据集,获取每个梯度的大小,输入输出
        for batch, (x, y) in enumerate(train_loader):
            # 1.4.1如果有gpu则把数据放入显存中计算,没有的话用cpu计算
            x=x.to(device)
            y=y.to(device)
            # 1.4.2数据输入模型
            pred = model(x)
            # 1.4.3计算损失
            loss = criterion(pred, y.float().detach())
            # 1.4.4优化器梯度清空
            optimizer.zero_grad()
            # 1.4.5方向传播,计算梯度
            loss.backward()
            # 1.4.6优化器迭代模型参数
            optimizer.step()
            # 1.4.7记录模型损失数据
            train_loss.append(loss.item())
        # 1.5模型固化,不修改梯度
        model.eval()
        val_loss = []
        prediction = []
        y_true = []
        with torch.no_grad():
          # 1.6遍历验证数据集,获取每个梯度的大小,输入输出
          for batch, (x, y) in enumerate(val_loader):
              # 1.6.1如果有gpu则把数据放入显存中计算,没有的话用cpu计算
              x=x.to(device)
              y=y.to(device)
              # 1.6.2模型预测输入
              pred = model(x)
              # 1.6.3计算损失函数
              loss = criterion(pred, y.float().detach())
              val_loss.append(loss.item())
              prediction.extend(pred.tolist())
              y_true.extend(y.tolist())
        # 1.7计算auc得分
        val_auc = roc_auc_score(y_true=y_true, y_score=prediction)
        # 1.8输出模型训练效果
        print ("EPOCH %s train loss : %.5f   validation loss : %.5f   validation auc is %.5f" % (epoch, np.mean(train_loss), np.mean(val_loss), val_auc))        
    return train_loss, val_loss, val_auc

使用Amazon-book训练wideWeep模型

# 计算出现的最大类别编码是多少,目的为统计一共有多少个商品类别
fields = data.max().max()
# 定义din模型
model = DeepInterestNet(feature_dim=fields, embed_dim=8, mlp_dims=[64,32], dropout=0.2).to(device)
# 迭代次数
epoches = 5
# 模型训练
_= train(model)
EPOCH 0 train loss : 0.69174   validation loss : 0.68478   validation auc is 0.53995
EPOCH 1 train loss : 0.68359   validation loss : 0.67958   validation auc is 0.57892
EPOCH 2 train loss : 0.67868   validation loss : 0.67763   validation auc is 0.58612
EPOCH 3 train loss : 0.67605   validation loss : 0.67401   validation auc is 0.59358
EPOCH 4 train loss : 0.67428   validation loss : 0.67516   validation auc is 0.59542

评估模型性能

通过上述步骤我们将整个模型在Amazon-book数据集上进行了训练,并且得到了较不错的准确率,接下来我们使用测试集中的某个样例对模型进行测试。

#从换分的测试数据集中取出一个数据,从该数据可以看出用户感兴趣的类别是Books,推荐的类别也是Books因此,用户很有可能会点击购买该商品对应label值是1。
dis_test_x.apply(cate_encoder.inverse_transform).reset_index().head(1)

| | index | hist_cate_0 | hist_cate_1 | hist_cate_2 | hist_cate_3 | hist_cate_4 | hist_cate_5 | hist_cate_6 | hist_cate_7 | hist_cate_8 | ... | hist_cate_31 | hist_cate_32 | hist_cate_33 | hist_cate_34 | hist_cate_35 | hist_cate_36 | hist_cate_37 | hist_cate_38 | hist_cate_39 | cateID |

053523BooksBooksBooksBooks00000...000000000Books

1 rows × 42 columns

#由于本轮实验需要运行在npu上,因此需要将模型输入的向量加载到npu设备上
inputTensor = test_X[0].to(device)
inputTensor
tensor([138, 138, 138, 138,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
          0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
          0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 138],
       device='npu:0')
#将输入给到模型,得到预测购买的概率为0.4946,该值与标签label=1,有较大差距是因为模型只迭代训练5次,可以增加训练迭代次数以后再进行测试。
model(torch.unsqueeze(inputTensor, 0))
Warning: Device do not support double dtype now, dtype cast repalce with float.
tensor([0.4946], device='npu:0', grad_fn=<SigmoidBackward0>)

内存使用情况: 整个训练过程的内存使用情况可以通过"npu-smi info"命令在终端查看,因此本文实验只用到了单个npu卡(也就是chip 0),内存占用约141M,对内存、精度或性能优化有兴趣的可以自行尝试进行优化,这里运行过程中也有其他程序在运行,因此本实验用到的网络所需要的内存已单独框出来。

Reference

[1] Zhou, Guorui , et al. "Deep Interest Network for Click-Through Rate Prediction." (2017).


永荣带你玩转昇腾
1 声望0 粉丝