• 从零开始的FM(2) pytorch实现ctr模型线上部署


    在(1)中介绍了FM模型的理论和python实现二分类模型。作为用于CTR预估的模型之一,FM重点在于实现ctr。

    一、数据集

    电商数据中的用户行为日志数据。召回完成,在排序阶段,需要考虑用户特征和物品特征,用户特征来源于用户画像,物品特征来源于物品自身固有属性;用户画像一部分是通过物品画像得到。

    1、物品画像

    在电商领域,以脐橙为例,物品画像通常包含如下维度:
    1、关键词:商品标题和详情页的文字部分提取关键词(topN),其数据格式为 keyword = [(光滑,0.32),(饱满,0.34),...]
    2、实体词:商品标题和详情页的文字部分提起实体词(topN),其数据格式为:entity=["云南省","赣南",...]
    3、价格:商品本身的价格是一个连续型特征,进行分桶处理为类别特征,如果划分10个区间,则商品的价格为10个特征之一:price=[0,0,1,0,...]
    4、分类:脐橙本身又分为了多个种类,每个脐橙属于一个分类。如果区分大分类和小分类,则可得到两个分类特征。分类为类别特征:category=[0,1,0,...]
    5、产地: 商品产地为类别特征:area=[0,1,0,...]
    如果商品有和时间或者节气、节日等强相关的特征,可以将其加入物品画像。

    2、用户画像

    很明显,用户画像是基于物品画像的。用户购买、收藏、点击等行为日志通过ios端或者android客户端埋点获得,过滤清洗之后存入hdfs,供后续推荐算法使用。

    1、关于日志

    问题1:需要考虑用户哪些行为是有价值的。很明显,用户购买、收藏了某个商品,他是喜欢这个商品的。那么构建用户画像只使用这类日志可以么?答案是No,因为对于一个日活有限的电商平台而言,这类日志很少。一般要加上浏览日志。
    问题2:用户浏览、购买、收藏了一个商品,产生了2+N(代表多次浏览该商品)条日志,日志如何处理。一个方案,只保留购买行为的日志,因为购买和用户喜欢是最强相关的。
    问题3:日志时间。如果喜欢浏览了商品A2秒,商品B20秒,如何确定其喜好。设定阈值,用户点进去一个商品然后快速返回,不能表示其喜好该商品。

    2、用户画像字段

    这里,用户画像直接使用物品画像进行构建,放弃了用户自身属性(年龄、性别等)。因为这些属性大部分用户都为空。实际场景中,很多属性是app不能获得的。
    1、关键词:用户操作过的商品的关键词,按照权重加权求和。数据格式为 keyword = [(光滑,1.32),(饱满,2.34),...]
    2、实体词:用户操作过的商品的实体词,按照实体词总数取分数。数据格式为:entity=[("云南省,0.1"),("赣南",0.2),...]
    3、价格:用户的价格画像为每个价格区间的比例:price=[("0-100",0.2),("100-500",0.4),...]
    4、分类:用户分类为操作过的商品的每个分类的比例:category=[(1,0.2),(2,0.1),...]
    5、产地: 用户操作过的商品的产地的分布:area=[("云南省",0.1),("安徽省",0.3),...]

    3、模型输入向量生成

    用户向量+物品向量
    假设有2000个关键词,1000个实体词,10种价格区间,10个分类,50个产地,则最终的用户向量维度为:(2000+1000+10+10+50=3070),物品向量维度为3070。
    ps:这里没有使用物品之外的特征,比如时间信息、app相关信息,行为信息等数据。
    故,最终的模型输入特征向量:input_vector = np.zeros(6140+1,dtype=np.float),然后在对应特征位置赋值。
    生成的numpy数组保存为xxx.npy

    二、torch实现FM

    用于CTR时,模型输出为sigmoid之后的概率值:[0,1]。
    分为几个模块

    1、数据集加载

    import torch
    from torch.utils.data import Dataset
    import numpy as np
    from dataprocess import DataLoad # 自定义的npy数据读取类
    
    class CtrDataset(Dataset):
        """
        Custom dataset class for dataset in order to use efficient 
        dataloader tool provided by PyTorch.
        """
    
        def __init__(self, train=True,split_=0.8):
            """
            Initialize file path and train/test mode.
    
            Inputs:
            - train: bool.是否为训练阶段
            - split_: 训练数据比例。
            """
    
            self.train = train
            train_data,test_data = DataLoad().split_sample(split_)
            if self.train:
                self.train_x = train_data[:, :-1]
                self.train_y = train_data[:, -1]
            else:
                self.test_x = test_data[:,:-1]
                self.test_y = test_data[:,-1]
    
    
        def __getitem__(self, idx):
            '''
            self.train_data的值:[[0,1,...],[],...],y要修改为:[[1],[0],...]的格式。
            '''
            if self.train:
                dataI, targetI = self.train_x[idx, :], self.train_y[idx]
                targetI = np.array(targetI)
                targetI = torch.from_numpy(targetI)
                targetI = torch.unsqueeze(targetI,-1)
                return dataI,targetI
            else:
                dataI, targetI = self.test_x[idx, :], self.test_y[idx]
                targetI = np.array(targetI)
                targetI = torch.from_numpy(targetI)
                targetI = torch.unsqueeze(targetI, -1)
                return dataI, targetI
    
        def __len__(self):
            if self.train:
                return len(self.train_x)
            else:
                return len(self.test_x)
    

    2、 DataLoader加载数据

    train_data = CtrDataset( train=True,split_=split_)
    test_data = CtrDataset( train=True,split_=split_)
    loader_train = DataLoader(train_data, batch_size=50,
                              shuffle=True)
    

    常用操作有:batch_size(每个batch的大小), shuffle(是否进行shuffle操作), num_workers(加载数据的时候使用几个子进程)。

    3、选择使用CPU还是GPU进行训练

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    

    4、FM模型

    class FMLayer(nn.Module):
        def __init__(self, n=10, k=5):
            """
            :param n: 特征维度
            :param k: 隐向量维度
            """
            super(FMLayer, self).__init__()
            self.dtype = torch.float
            self.n = n
            self.k = k
            self.linear = nn.Linear(self.n, 1)   # 前两项线性层
            '''
            torch.nn.Parameter是继承自torch.Tensor的子类,其主要作用是作为nn.Module中的可训练参数使用。它与torch.Tensor的区别就是nn.Parameter会自动被认为是module的可训练参数,即加入到parameter()这个迭代器中去;而module中非nn.Parameter()的普通tensor是不在parameter中的。
    注意到,nn.Parameter的对象的requires_grad属性的默认值是True,即是可被训练的,这与torth.Tensor对象的默认值相反。
    在nn.Module类中,pytorch也是使用nn.Parameter来对每一个module的参数进行初始化的。'''
            self.v = nn.Parameter(torch.randn(self.n, self.k))   # 交互矩阵
            nn.init.uniform_(self.v, -0.1, 0.1)
    
        def fm_layer(self, x):
            # x 属于 R^{batch*n}
            linear_part = self.linear(x)
            #print("linear_part",linear_part.shape)
            # linear_part = torch.unsqueeze(linear_part, 1)
            # print(linear_part.shape)
            # 矩阵相乘 (batch*p) * (p*k)
            inter_part1 = torch.mm(x, self.v)  # out_size = (batch, k) # 矩阵a和b矩阵相乘。 vi,f * xi
            # 矩阵相乘 (batch*p)^2 * (p*k)^2
            inter_part2 = torch.mm(torch.pow(x, 2), torch.pow(self.v, 2))  # out_size = (batch, k)
            # 这里torch求和一定要用sum
            inter = 0.5 * torch.sum(torch.sub(torch.pow(inter_part1, 2), inter_part2),1,keepdim=True)
            #print("inter",inter.shape)
            output = linear_part + inter
            output = torch.sigmoid(output)
            #print(output.shape) # out_size = (batch, 1)
            return output
        def forward(self, x):
            return self.fm_layer(x)
    

    上述为FM公式的torch版本。作为网络模型,还需要定义损失函数和训练过程。
    模型输出已经是经过sigmoid的概率值,直接使用交叉熵作为损失函数。

     def fit(self,data,optimizer,epochs=100):
            """
            Training a model and valid accuracy.
    
            Inputs:
            - loader_train: I
            - optimizer: Abstraction of optimizer used in training process, e.g., "torch.optim.Adam()""torch.optim.SGD()".
            - epochs: Integer, number of epochs.
            """
            criterion = F.binary_cross_entropy
            for epoch in range(epochs):
                for t, (batch_x, batch_y) in enumerate(data):
                    batch_x = batch_x.to(device)
                    batch_y = batch_y.to(device)
                    total = self.forward(batch_x)
                    loss = criterion(total, batch_y)
                    optimizer.zero_grad()
                    loss.backward()
                    optimizer.step()
    
                loader_test = DataLoader(test_data, batch_size=50,
                                         shuffle=True)
    
                r = self.test(loader_test)
                print('Epoch %d , loss = %.4f' % (epoch, r))
    
    
        def test(self,data):
            '''
            测试集测试
            :return:
            '''
            criterion = F.binary_cross_entropy
            all_loss = 0
            i = 0
            for t, (batch_x, batch_y) in enumerate(data):
                batch_x = batch_x.to(device)
                batch_y = batch_y.to(device)
                total = self.forward(batch_x)
                loss = criterion(total, batch_y)
                all_loss += loss.item()
                i += 1
            return all_loss/i
    

    三、模型训练和保存

    使用flask作为web服务框架。
    为了线上部署,使用torchscript进行模型的保存。
    https://pytorch.org/tutorials/beginner/Intro_to_TorchScript_tutorial.html
    https://discuss.pytorch.org/t/infer-torch-model-via-gunicorn-wsgi/60437

    fm = FMLayer(n=features,k=30)
    fm = fm.to(device)
    optimizer = optim.Adam(fm.parameters(), lr=1e-4, weight_decay=0.0)
    fm.fit(loader_train, optimizer, epochs=100)
    fm = fm.to("cpu")
    temp = torch.zeros((1,6140))
    traced_model = torch.jit.trace(fm,temp)
    torch.jit.save(traced_model, 'model.pt') 
    

    使用torch.save(model,path)进行保存的模型,在加载的使用,要求可以找到原始的FMLayer类,直接xx.py没有问题。但是,如果web服务使用gunicorn进行启动,就会报错:

    AttributerError:Can't get attribute 'FMLayer' on <module '__main__' from '/usr/local/bin/gunicorn'
    

    因为:torch.load(model_path)的时候,需要在当前位置有模型类。而使用gunicorn的时候,它会在gunicorn那里寻找模型类。
    使用torch.jit.load(model_path, map_location='cpu')可以不用在当前位置有对应的模型类。

    四、线上部署

    对外提供api接口,接收输入数据:用户id和召回算法得到的物品id,返回排序后的物品id列表。
    使用docker部署注意事项:
    1、完整的requirements.txt
    2、gunicorn 的配置 daemon = "false"
    3、时区改变:RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
    4、主机和容器数据同步,日志和新的模型文件.

     volumes:                       #映射的数据卷
          - ./app:/www/web
          - ./nginx/conf:/etc/nginx
          - ./nginx/logs:/www/web_logs
    

    五、模型更新和线上服务更新

    使用每天的日志训练模型并实时更新线上模型,通过flask_apscheduler模块在web服务中执行定时任务。

  • 相关阅读:
    VS2010导入DLL的总结
    [转]C#事件简单示例
    VS2010中实现TreeView和Panel的动态更新
    【JZOJ1282】打工
    【NOIP2016提高A组五校联考2】tree
    【NOIP2016提高A组五校联考2】running
    【NOIP2016提高A组五校联考2】string
    8月~9月学习总结
    NOIP2016提高A组五校联考2总结
    NOIP2016提高A组五校联考1总结
  • 原文地址:https://www.cnblogs.com/leimu/p/14578392.html
Copyright © 2020-2023  润新知