1 赛题背景

广告欺诈是数字营销需要面临的重要挑战之一,点击会欺诈浪费广告主大量金钱,同时对点击数据会产生误导作用。本次比赛提供了约50万次点击数据。特别注意:我们对数据进行了模拟生成,对某些特征含义进行了隐藏,并进行了脱敏处理。

请预测用户的点击行为是否为正常点击,还是作弊行为。点击欺诈预测适用于各种信息流广告投放,banner广告投放,以及百度网盟平台,帮助商家鉴别点击欺诈,锁定精准真实用户。

2 任务分析

根据赛题设置,本任务为二分类任务;
根据数据挖掘与机器学习的一般任务流程,将按照下述流程完成该任务:

  1. 数据分析
  2. 特征工程
  3. 模型构建
  4. 模型训练
  5. 模型调优
  6. 模型推理

其中,模型调优部分同时涉及数据的再处理(比如选用新的特征工程方案)以及模型结构、优化方法、损失函数等方面的优化

3 解决方案

基于比赛提供的baseline实现,按照以下思路进行:

  1. 对于结构化数据中的各字段,进行稠密向量嵌入式表示;并将嵌入维度作为超参数进行调优;
  2. 维持baseline模型的基本结构不变,调整优化器及其学习率等训练超参数;

4 数据分析

在本赛题中,由于数据形式为结构化数据,为了对数据进行更好的表示以便神经网络模型能够充分挖掘数据背后的模式与规律,参照自然语言处理(NLP)中的字词方法,对数据的各字段进行稠密向量的嵌入式表示,即embedding方法。

简单而言,Embedding方法就是用一个低维的向量表示一个物体,可以是一个词,一个商品,或是一部电影等。这个向量能使距离相近的向量对应的物体有相近的含义,比如“复仇者联盟”对应的向量和“钢铁侠”对应的向量之间的距离(欧几里得距离,汉明距离等)就会很小,但 “复仇者联盟”对应的向量和“乱世佳人”对应的向量之间的距离就会大一些。
此外,Embedding形式的表示使得其背后所代表的“物体”具有数学运算关系,比如:Embedding(马德里)-Embedding(西班牙)+Embedding(法国)≈Embedding(巴黎)。

Embedding能够用低维向量对物体进行编码还能保留其含义的特点非常适合深度学习。在传统机器学习模型构建过程中,经常使用onehot编码对离散特征、特别是id类特征进行编码,但由于onehot编码的维度等于物体的类别总数,这样的编码方式对于类别型变量来说是极端稀疏的,而深度学习的特点使其不利于对稀疏特征向量的处理。

因此,在本任务中,先对数据集中的类别型变量进行嵌入式表示(连续性变量进行标准化),将处理之后的字段输入到神经网络模型中用以分类。

5 模型分析

本任务中,模型结构维持baseline提供的模型结构基本不变,其主要由embedding层、concat层以及dense层组成,embedding层用于获取每个输入字段值的嵌入式表示向量,concat层用于拼接所有字段的表示向量成为一个总的样本特征向量,dense层用于转换数据大小,其中模型最后一层的输出维度为2(类别数目),激活函数使用softmax函数。

关于模型的优化器与损失函数,优化器使用RMSProp,损失函数使用分类任务常设的交叉熵损失函数。

6 总结改进

根据赛题重点,合理有效地处理数据集的各类特征是完成分类任务的关键之处。 本项目只是使用较为初级的多层感知机网络执行分类任务,项目可改进的地方包含但不限于:

进一步细化特征处理办法,深化特征工程有关工作,Embedding处理只是其中一个方法;
改进或换用预测模型结构,可以尝试使用现代深度学习框架内更为先进的神经网络模型;
更换任务思路,采用传统机器学习项目中的相关思路与模型解决该问题,如适用于结构化数据的TabNet网络。

7 飞桨使用

在使用paddlepaddle进行深度学习时,注重理论课程与实践应用的合理结合; 一方面,强调通过资料与视频课程领会框架的基本使用; 另一方面,需要结合具体应用(如参加飞桨的各类竞赛)熟练掌握数据预处理、模型构建、模型训练、模型调优与应用等深度学习各阶段操作

8 参考资料

  1. 本次竞赛的baseline代码
  2. 深入浅出Word2Vec原理解析
  3. Embedding从入门到专家必读的十篇论文

feature_process

import os
import pandas as pd
import numpy as np
from paddle.io import Dataset
from baseline_tools import *

DATA_RATIO = 0.9  # 训练集和验证集比例

TAGS = {'android_id': None,
        'apptype': "emb",
        'carrier': "emb",
        'dev_height': "emb",
        'dev_ppi': "emb",
        'dev_width': "emb",
        'lan': "emb",
        'media_id': "emb",
        'ntt': "emb",
        'os': "emb",
        'osv': None,
        'package': "emb",
        'sid': None,
        'timestamp': "norm",
        'version': "emb",
        'fea_hash': None,
        'location': "emb",
        'fea1_hash': None,
        'cus_type': None}

# 归一化权重设置
NORM_WEIGHT = {'timestamp': 6.40986e-12}
TRAIN_PATH = "train.csv"
SAVE_PATH = "emb_dicts"
df = pd.read_csv(TRAIN_PATH, index_col=0)

pack = dict()
for tag, tag_method in TAGS.items():
    if tag_method != "emb":
        continue
    data = df.loc[:, tag]
    dict_size = make_dict_file(data, SAVE_PATH, dict_name=tag)
    pack[tag] = dict_size + 1  # +1是为了增加字典中不存在的情况,提供一个默认值

with open(os.path.join(SAVE_PATH, "size.dict"), "w", encoding="utf-8") as f:
    f.write(str(pack))

print("全部生成完毕")

data_loading

def get_size_dict(dict_path="./emb_dicts/size.dict"):
    """
    获取Embedding推荐大小
    :param dict_path: 由run_make_emb_dict.py生成的size.dict
    :return: 推荐大小字典{key: num}
    """
    with open(dict_path, "r", encoding="utf-8") as f:
        try:
            size_dict = eval(f.read())
        except Exception as e:
            print("size_dict打开失败,请检查", dict_path, "文件是否正常,报错信息如下:\n", e)
        return size_dict


class Reader(Dataset):
    def __init__(self,
                 is_infer: bool = False,
                 is_val: bool = False,
                 use_mini_train: bool = False,
                 emb_dict_path="./emb_dicts"):

        """
        数据读取类
        :param is_infer: 是否为预测Reader
        :param is_val: 是否为验证Reader
        :param use_mini_train:使用Mini数据集
        :param emb_dict_path: emb字典路径
        """
        super().__init__()
        # 选择文件名
        train_name = "mini_train" if use_mini_train else "train"
        file_name = "test" if is_infer else train_name
        # 根据文件名读取对应csv文件
        df = pd.read_csv(file_name + ".csv")
        # 划分数据集
        if is_infer:
            self.df = df.reset_index()
        else:
            start_index = 0 if not is_val else int(len(df) * DATA_RATIO)
            end_index = int(len(df) * DATA_RATIO) if not is_val else len(df)
            self.df = df.loc[start_index:end_index].reset_index()
        # 数据预处理
        self.cols = [tag for tag, tag_method in TAGS.items() if tag_method is not None]
        self.methods = dict()
        for col in self.cols:
            # ===== 预处理方法注册 =====
            if TAGS[col] == "emb":
                self.methods[col] = Data2IdEmb(dict_path=emb_dict_path, dict_name=col).get_method()
            elif TAGS[col] == "norm":
                self.methods[col] = Data2IdNorm(norm_weight=NORM_WEIGHT[col]).get_method()
            else:
                raise Exception(str(TAGS) + "是未知的预处理方案,请选手在此位置使用elif注册")

        # 设置FLAG负责控制__getitem__的pack是否包含label
        self.add_label = not is_infer
        # 设置FLAG负责控制数据集划分情况
        self.is_val = is_val

    def __getitem__(self, index):
        """
        获取sample
        :param index: sample_id
        :return: sample
        """
        # 因为本次数据集的字段非常多,这里就使用一个列表来"收纳"这些数据
        pack = []
        # 遍历指定数量的字段
        for col in self.cols:
            sample = self.df.loc[index, col]
            sample = self.methods[col](sample)
            pack.append(sample)

        # 如果不是预测,则添加标签数据
        if self.add_label:
            tag_data = self.df.loc[index, "label"]
            tag_data = np.array(tag_data).astype("int64")
            pack.append(tag_data)
            return pack
        else:
            return pack

    def __len__(self):
        return len(self.df)

# 获取训练集和测试集数据读取器
USE_MINI_DATA = False
train_reader = Reader(use_mini_train=USE_MINI_DATA)
val_reader = Reader(use_mini_train=USE_MINI_DATA, is_val=True)

model_define

import os
import numpy as np
import pandas as pd
import paddle
import paddle.nn as nn
import paddle.tensor as tensor
from paddle.static import InputSpec
from paddle.metric import Accuracy

# 模型保存与加载文件夹
SAVE_DIR = "./output/"

# 部分训练超参数
EMB_SIZE = 256  # Embedding特征大小
EMB_LINEAR_SIZE = 32  # Embedding后接Linear层神经元数量
LINEAR_LAYERS_NUM = 2  # 归一化方案的Linear层数量


# 组网
class SampleNet(paddle.nn.Layer):
    def __init__(self, tag_dict: dict, size_dict: dict):
        super().__init__()

        # 新建一个隐藏层列表,用于存储各字段隐藏层对象
        self.hidden_layers_list = []

        # 定义一个用于记录输出层的输入大小变量,经过一个emb的网络结构就增加该结构的output_dim,以此类推
        out_layer_input_size = 0

        # 遍历每个字段以及其处理方式
        for tag, tag_method in tag_dict.items():
            # Embedding方法注册
            if tag_method == "emb":
                hidden_layer = nn.LayerList([nn.Embedding(num_embeddings=size_dict[tag],
                                                          embedding_dim=EMB_SIZE),
                                             nn.Linear(in_features=EMB_SIZE, out_features=EMB_LINEAR_SIZE)])
                out_layer_input_size += EMB_LINEAR_SIZE
            # 归一化方法注册
            elif tag_method == "norm":
                hidden_layer = nn.LayerList(
                    [nn.Linear(in_features=1, out_features=1) for _ in range(LINEAR_LAYERS_NUM)])
                out_layer_input_size += 1
            # 如果对应方法为None,那么跳过该字段
            elif tag_method is None:
                continue
            # 若出现没有注册的方法,提示报错
            else:
                raise Exception(str(tag_method) + "为未知的处理方案,请在SampleNet类中用elif注册处理流程")
            self.hidden_layers_list.append(hidden_layer)
        
        self.out_layers = nn.Linear(in_features=out_layer_input_size,out_features=2)

    # 前向推理部分 `*input_data`的`*`表示传入任一数量的变量
    def forward(self, *input_data):
        layer_list = []  # 用于存储各字段特征结果
        for sample_data, hidden_layers in zip(input_data, self.hidden_layers_list):
            tmp = sample_data
            for hidden_layer in hidden_layers:
                tmp = hidden_layer(tmp)
            layer_list.append(tensor.flatten(tmp, start_axis=1))  # flatten是因为原始shape为[batch size, 1 , *n], 需要变换为[bs, n]
        # 对所有字段的特征合并
        layers = tensor.concat(layer_list, axis=1)

        # 把特征放入用于输出层的网络
        result = self.out_layers(layers)
        result = paddle.nn.functional.softmax(result)
        
        # 返回分类结果
        return result
# 定义网络输入
inputs = []
for tag_name, tag_m in TAGS.items():
    d_type = "float32"
    if tag_m == "emb":
        d_type = "int64"
    if tag_m is None:
        continue
    inputs.append(InputSpec(shape=[-1, 1], dtype=d_type, name=tag_name))

# 定义Label
labels = [InputSpec([-1, 1], 'int64', name='label')]

# 实例化SampleNet
model = paddle.Model(SampleNet(TAGS, get_size_dict()), inputs=inputs, labels=labels)

model_config

from paddle.optimizer import RMSProp

# 定义优化器
optimizer = RMSProp(learning_rate=0.01, parameters=model.parameters())

# 模型训练配置
model.prepare(optimizer, paddle.nn.loss.CrossEntropyLoss(), Accuracy())

model_training

# 开始训练
model.fit(train_data=train_reader,  # 训练集数据
            eval_data=val_reader,  # 验证集数据
            batch_size=128,  # Batch size大小
            epochs=10,  # 训练轮数
            log_freq=1000,  # 日志打印间隔
            save_dir=SAVE_DIR)  # checkpoint保存路径

model_reasoning

# 推理部分
CHECK_POINT_ID = "final"  
TEST_BATCH_SIZE = 128  

# 实例化SampleNet
model = paddle.Model(SampleNet(TAGS, get_size_dict()), inputs=inputs)
# 获取推理Reader并读取参数进行推理
infer_reader = Reader(is_infer=True)
model.load(os.path.join(SAVE_DIR, CHECK_POINT_ID))
# 开始推理
model.prepare()
infer_output = model.predict(infer_reader, TEST_BATCH_SIZE)

# 获取原始表中的字段并添加推理结果
result_df = infer_reader.df.loc[:, "sid"]
pack = []
for batch_out in infer_output[0]:
    for sample in batch_out:
        pack.append(np.argmax(sample))

# 保存csv文件
RESULT_FILE = "./result1.csv"  
result_df = pd.DataFrame({"sid": np.array(result_df, dtype="int64"), "label": pack})
result_df.to_csv(RESULT_FILE, index=False)
print("结果文件保存至:", RESULT_FILE)

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐