点击反欺诈预测Baseline
感觉赛题比较难?或许你应该这样想:点击反欺诈 = 二分类任务(波士顿房价降价预测),有木有感觉到简单呢?目前常规赛事基线撰写模板已经对外开放,欢迎开发者贡献更好的基线作品~
请点击此处查看本环境基本用法.
Please click here for more detailed instructions.
飞桨常规赛:听闻 - 10月第8名方案
( T i p s : 该 部 分 主 要 为 介 绍 思 路 和 基 线 修 改 指 南 , 实 战 部 分 在 后 半 部 分 单 独 列 出 ) \color{red}{(Tips:该部分主要为介绍思路和基线修改指南,实战部分在后半部分单独列出)} (Tips:该部分主要为介绍思路和基线修改指南,实战部分在后半部分单独列出)
赛题介绍
广告欺诈是数字营销需要面临的重要挑战之一,点击会欺诈浪费广告主大量金钱,同时对点击数据会产生误导作用。本次比赛提供了约50万次点击数据。特别注意:我们对数据进行了模拟生成,对某些特征含义进行了隐藏,并进行了脱敏处理。
MarTech技术已经被广泛应用于商业广告分析与挖掘中,在搜索广告,信息流广告,营销预测,反欺诈发现,商品购买预测,智能创意生成中有广泛的应用。
请预测用户的点击行为是否为正常点击,还是作弊行为。点击欺诈预测适用于各种信息流广告投放,banner广告投放,以及 百度网盟平台,帮助商家鉴别点击欺诈,锁定精准真实用户。
点击跳转至赛题页面
(
T
i
p
s
:
机
器
学
习
框
架
方
面
只
允
许
使
用
飞
桨
深
度
学
习
框
架
哦
)
\color{red}{(Tips:机器学习框架方面只允许使用飞桨深度学习框架哦~)}
(Tips:机器学习框架方面只允许使用飞桨深度学习框架哦 )
基线介绍
运行方式
本次基线基于飞桨PaddlePaddle2.0版本,若本地运行则可能需要额外安装pandas模块。
本地运行
下载左侧文件夹中的”本地运行方案“,解压到本地后修改为英文目录后运行run_model.py
即可开始训练,修改该文件的IS_INFER
参数即可生成推理结果,推理结果文件为result.csv
。(推荐在右上角“文件”->“导出Notebook到py”,这样可以保证代码是最新版本)
AI Studio (Notebook)运行
依次运行下方的cell即可,若运行时修改了cell,推荐在右上角重启执行器后再以此运行,避免因内存未清空而产生报错。
Tips:若修改了左侧文件夹中数据,也需要重启执行器后才会加载新文件。
设计思想
执行流程
- 配置预处理数据方案(选手可以自行设计,默认提供用于Embedding的数据转化和归一化两种方案)
- 生成Embedding所需要的index字典(若选手不使用Embedding则可以跳过该步骤)
- 检查数据是否可以正确读取(可省略,若选手自行修改了Reader部分,务必检查能否读取后再进行下一步操作)
- 开始训练
- 执行预测并产生结果文件
技术方案
在本次赛题中,虽然赛题数据与以往的线性回归任务比较相似,但更像是一个二分类任务(判断欺诈、非欺诈)。 接下来将介绍技术方案中的一些细节问题以及新增method流程。
数据处理方案
我们希望选手在做预处理之前首先判断19个字段属性是什么,然后再决定处理方案。
基
线
默
认
提
供
了
两
种
常
见
的
预
处
理
方
案
(
n
o
r
m
、
E
m
b
e
d
d
i
n
g
)
,
这
些
可
在
“
数
据
预
处
理
−
配
置
”
c
e
l
l
中
进
行
配
置
\color{red}{基线默认提供了两种常见的预处理方案(norm、Embedding),这些可在“数据预处理-配置”cell中进行配置 }
基线默认提供了两种常见的预处理方案(norm、Embedding),这些可在“数据预处理−配置”cell中进行配置
Tips:虽然赛题页面有字段说明,但字段类型仍需选手自行判断。
新增预处理方案
比赛数据集字段过多,为了减轻预处理代码的工作量特编写此方案。
新增步骤如下:
- 可以模仿
baseline_tools.py
中的Data2IdNorm
来实现预处理class
以归一化至0~1处理为例:
假设某字段含有1,2,3,4共计4个值,如果想缩放到0~1之间,可以使用(当前值 - 最小值) / (最大值 - 最小值)方式计算出权重大小。
本示例为了方便起见,直接通过(1 / 最大值)来计算权重,此处可能有一定调优空间。
# ===== Data2IdNorm类 ===== 新增预处理方案 =====
class Data2IdNorm:
def __init__(self, norm_weight):
self.norm_weight = norm_weight # 获取当前字段的归一化权重
def transform_data(self, sample, shape=None, d_type="float32"):
sample *= self.norm_weight # sample为当前index下对应字段的数据,该数据与归一化权重相乘即可进行归一化
sample = value2numpy(sample, shape, d_type) # 将该数据转化为numpy的array对象,value2numpy在baseline_tools.py中已经提供
return sample
def get_method(self):
return self.transform_data # 返回预处理方法
新增预处理方案后,可以在Reader类中找到下方代码段,通过添加elif来注册xxx流程。
# ===== Reader类 ===== 预处理方案注册 =====
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] == "xxx":
self.methods[col] = Data2IdNorm(norm_weight=0.0001).get_method() # 此处假设权重为0.0001
else:
raise Exception(str(TAGS) + "是未知的预处理方案,请选手在此位置使用elif注册")
在TAGS中更改对应预处理策略
TAGS = {'android_id': "emb",
'apptype': "emb",
'carrier': "xxx", # 对应elif TAGS[col] == "xxx"中的xxx
...
backbone修改方案
如果在网络部分有特殊需求,可以在SampleNet类中注册网络结构
# ===== 网络结构注册 =====
if tag_method == "emb":
hidden_layer = nn.LayerList([nn.Embedding(size=[size_dict[tag], EMB_SIZE]),
nn.Linear(input_dim=EMB_SIZE, output_dim=EMB_LINEAR_SIZE)])
out_layer_input_size += EMB_LINEAR_SIZE
elif tag_method == "xxx":
# 注册xxx网络结构,也可以多种预处理方案使用同一种网络结构,例如:elif tag_method in ["xxx", "yyy"]:
hidden_layer = nn.LayerList(
[nn.Linear(input_dim=1, output_dim=1, act="tanh") for _ in range(LINEAR_LAYERS_NUM)])
out_layer_input_size += 1
elif tag_method is None:
continue
else:
raise Exception(str(tag_method) + "为未注册的处理方案,请在SampleNet类中进行定义处理流程")
网络结构
通过分析数据集得知,数据集内有较多字段为离散值,并非连续值,若直接放入Linear层可能效果会很差,举例如下:
假设A字段值0 1 2分别代表老王、老李和老张,B字段值0 1 2分别表示0 1 2个苹果的三种价格
我们可以说0个苹果是0元,而2个苹果是2元,Linear层也可以推出1个苹果为1元的结论
但我们能否推出老王是0,老张是2,那么老李一定就是1吗?
显然不能,因为老李与老张和老王并没有直接关系,3也可能是老李,1024也可能是老李。
同理数据集中的设备名字段有“安卓“、”IOS“、”None”三种情况,也不适合当做连续值来处理。
当然我们可以尝试用Embedding操作来做出一组特征向量,输入的虽然是0 1 2这种的索引值,但其会映射出一个shape为N的特征,这个特征可以通过训练来尽可能代表对应人物的关系。
在基线中,我们对若干离散值字段使用了Embedding(Emb)操作来获取其特征向量,随后传入Linear层进行特征提取。当然该方案并不是最优解,选手可以自行判断哪些字段适合做Emb,或者使用比Emb更有效的方案来做处理。
对于连续值,我们则可以用一个Linear层轻松推理出1个苹果的价格,那么对于本次赛题,提升Linear层的size还是其层数由选手自行决定。
同样,网络部分的损失函数(激励函数)也可以由选手自行选择,默认隐藏层为tanh
最终我们将这些特征通concat方法链接在一起,通过softmax激活函数来完成多分类任务。当然选手也可以使用sigmoid激活函数来完成二分类任务,但并不是很推荐这样做。
做softmax时需要注意variable的shape,为了一次性解决该细节问题,基线中使用flatten来转化为2维形式(batch size,feature size),这样就可放心做softmax了。
默认提供的网络组件解析
nn.LayerList
会生成一个layer列表,这里我们以"norm"方法对应的组件为例:
nn.Linear
为创建一个Linear(线性层),其文档可以在飞桨官方网站中搜索得到,此处不提供解析。
... for _ in range(LINEAR_LAYERS_NUM)
表示循环多少次,1次代表循环一层。
该循环等同于:
tmp = []
for i in range(循环次数):
tmp.append(nn.Linear(...))
主要结构为
# ===== 网络结构注册 =====
if tag_method == "emb":
hidden_layer = nn.LayerList([nn.Embedding(size=[size_dict[tag], EMB_SIZE]),
nn.Linear(input_dim=EMB_SIZE, output_dim=EMB_LINEAR_SIZE)])
out_layer_input_size += EMB_LINEAR_SIZE
elif tag_method == "norm":
hidden_layer = nn.LayerList(
[nn.Linear(input_dim=1, output_dim=1, act="tanh") for _ in range(LINEAR_LAYERS_NUM)])
out_layer_input_size += 1
执行预测
为了简化推理流程,只需要选手将训练脚本的IS_INFER
打开,并且设置好CHECK_POINT_ID
(每轮训练都会保存一个“数字.pdparams”的参数文件,可以填写该数字作为ID)
运行完毕后会打印出结果文件的路径,选手可以在赛题的报名页面点击提交结果来上传该文件。
( 以 下 为 实 战 部 分 ) \color{red}{(以下为实战部分)} (以下为实战部分)
环境配置
目前飞桨(PaddlePaddle)正式版仍为1.8.4,以下代码均为2.0RC0测试版本,本地安装需要指定版本号进安装。
数据预处理 - 配置部分
根据赛题说明给出的字段说明,结合数据处理结果的id数量,
判断出哪些可能为干扰因素,哪些为被误删除的有效因素,进行调整并结合数据处理方式即可有效提升准确率\n
作为一个刚刚入门的小白,我的思路是在最容易且有效的地方进行优化,即可产生明显效果。
import os
import pandas as pd
import numpy as np
from paddle.io import Dataset
from baseline_tools import *
DATA_RATIO = 0.9 # 训练集和验证集比例
# None表示不使用,“emb”为Embedding预处理方案,选手可自由选择使用字段以及预处理方案
TAGS = {'android_id': None, # 样本无规律,为干扰因素
'apptype': "emb",
'carrier': "emb",
'dev_height': "emb", # 屏幕宽高和ppi可以有效筛选
'dev_ppi': "emb",
'dev_width': "emb",
'lan': "emb",
'media_id': "emb",
'ntt': "emb",
'os': "emb",
'osv': 'emb',
'package': "emb",
'sid': None,
'timestamp': "norm",
'version': "emb",
'fea_hash': 'emb',
'location': "emb",
'fea1_hash': 'emb',
'cus_type': 'emb'}
# 归一化权重设置
NORM_WEIGHT = {'timestamp': 6.40986e-13}
数据预处理 - 生成Embedding所需数据
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("全部生成完毕")
apptype 字典生成完毕,共 89 个id
carrier 字典生成完毕,共 5 个id
dev_height 字典生成完毕,共 798 个id
dev_ppi 字典生成完毕,共 92 个id
dev_width 字典生成完毕,共 346 个id
lan 字典生成完毕,共 22 个id
media_id 字典生成完毕,共 284 个id
ntt 字典生成完毕,共 8 个id
os 字典生成完毕,共 2 个id
osv 字典生成完毕,共 155 个id
package 字典生成完毕,共 1950 个id
version 字典生成完毕,共 22 个id
fea_hash 字典生成完毕,共 402980 个id
location 字典生成完毕,共 332 个id
fea1_hash 字典生成完毕,共 4959 个id
cus_type 字典生成完毕,共 58 个id
全部生成完毕
数据预处理 - 定义数据读取器以及预处理流程
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)
数据预处理 - 检查数据是否可以正常读取(可选)
默认只检查训练,infer和test可以在val_reader = Reader(此处设置)
中参考刚刚定义的Reader
来配置
# 用于独立测试数据读取是否正常 推荐在本地IDE中下断点进行测试
print("检查数据ing...")
val_reader = Reader()
print(len(val_reader))
for data_id, data in enumerate(val_reader):
for i in range(len(data)):
if data_id == 0:
print("第", i, "个字段 值为:", data[i])
else:
break
if data_id % 1000 == 0:
print("第", data_id, "条数据可正常读取 正在检查中", end="\r")
if data_id == len(val_reader) - 1:
print("数据检查完毕")
break
检查数据ing...
训练&推理配置
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/"
# 部分训练超参数
EPOCHS = 1 # 训练多少个循环
TRAIN_BATCH_SIZE = 1 # mini_batch 大小
EMB_SIZE = 100 # Embedding特征大小
EMB_LINEAR_SIZE = 16 # Embedding后接Linear层神经元数量
LINEAR_LAYERS_NUM = 1 # 归一化方案的Linear层数量
LEARNING_RATE = 0.01 # 学习率
# 配置训练环境
USE_MINI_DATA = True # 默认使用小数据集,此方法可加快模型产出速度,但可能会影响准确率
# 组网
class SampleNet(paddle.nn.Layer):
def __init__(self, tag_dict: dict, size_dict: dict):
# 继承Model
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)
# 定义输出层,因为是二分类任务,激活函数以及损失方案可以由选手自己发挥,此处为sigmoid激活函数
# Tips: 若使用sigmoid激活函数,需要修改output_dim和损失函数,推荐理解原理后再尝试修改
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)
# 获取训练集和测试集数据读取器
train_reader = Reader(use_mini_train=USE_MINI_DATA)
val_reader = Reader(use_mini_train=USE_MINI_DATA, is_val=True)
# 定义优化器
optimizer = paddle.optimizer.SGD(learning_rate=LEARNING_RATE, parameters=model.parameters())
# 模型训练配置
model.prepare(optimizer, paddle.nn.loss.CrossEntropyLoss(), Accuracy())
# 开始训练
model.fit(train_data=train_reader, # 训练集数据
eval_data=val_reader, # 交叉验证集数据
batch_size=TRAIN_BATCH_SIZE, # Batch size大小
epochs=EPOCHS, # 训练轮数
log_freq=1000, # 日志打印间隔
save_dir=SAVE_DIR) # checkpoint保存路径
执行推理
# 推理部分
CHECK_POINT_ID = "final" # 如没有训练完毕,可以挑选SAVE_DIR路径的下的中的checkpoint文件名(不包含拓展名哦),例如"1"
TEST_BATCH_SIZE = 32 # 若因内存/显存发生报错,请优先调整为1
RESULT_FILE = "./result2.csv" # 推理文件保存位置
# 实例化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_df = pd.DataFrame({"sid": np.array(result_df, dtype="int64"), "label": pack})
result_df.to_csv(RESULT_FILE, index=False)
print("结果文件保存至:", RESULT_FILE)
写在最后
本次比赛可调优空间非常大,选手不限于从以下方面来进行调优,如果尝试后发现效果并不理想,可以在基线项目的评论区中和大家一起讨论~
此外,目前飞桨常规系列赛事已经开放基线模板,如您愿意贡献比赛数据集、基线系统,欢迎发送邮件至zhanghongji@baidu.com,我们会将这类贡献计入飞桨开发者技术专家认证(PPDE)积分并给予开发者适当奖励。
数据处理
- 归一化方案 - 直接拉伸是最佳方式吗?
- 离散值与连续值 - 哪种方式更适合处理这些方式?是否有较为通用的方法可以尝试?
- 字段选择 - 字段真的是越多越好吗?
- 数据集划分比例 - 训练集、验证集、测试集应该怎样划分?训练集中包含验证集的Embedding字段是否合理?
首层网络选择
- Embedding还是Linear、Conv?- 如果使用卷积应该怎样处理shape?
- 多字段合并输入还是分开输入?- 分开输入效果一定好吗?哪些字段更适合合并输入?
网络(Backbone)部分搭建
- 隐层大小选择 - 宽度和层数
- 尝试复杂网络构建 - 要不要尝试一下简单的注意力机制?
- 选择更合适的激活函数
- 尝试正则化、dropout等方式避免过拟合
- 尝试非Xavier初始化方案
模型(Model)搭建以及训练相关
- 选择学习率等超参数
- 选择合适的损失函数
- 尝试不同的优化器
- 尝试使用学习率调度器
- 避免脏数据干扰(用深度学习的方式更优更方便)
提交相关
- 测试集表现最好的模型一定是最优秀的吗?
- 用准确率来衡量二分类模型的能力是最好选择吗?
简要概述
- 通过官方基线逐步优化
- 从根源优化,逐步向外拓展
- 小白第一次试水,希望能在比赛中有所收获呀
代码审查
如选手成绩靠前并收到官方邮件通知代码审查,请参考该链接进行项目上传操作
快捷命令:!zip -rP [此处添加审查邮件中的Key值] [邮件中的UID值].zip /home/aistudio/
更多推荐
所有评论(0)