钉钉杯A赛题是经典的银行卡诈骗预测,在刚看到赛题的时候我就联想到Paddle上的常规赛:MarTech Challenge 点击反欺诈预测。在观察数据之后又联想到NLP的Embedding,由于我懒的再去做特征工程了,就直接结合了Embedding和NN进行模型搭建。在没进行参数调优、网络结构修改、数据均衡等手段的情况下,线下成绩能够直接达到MeanF1-0.9878(各种模型平均水准),可能在经过调优后,模型可以达到更好的效果

1.Embedding介绍

参考怎么形象理解embedding这个概念?中的介绍:

对于颜色,我们可以把它拆成三个特征维度,用这三个维度的组合理论上可以表示任意一种颜色。同理,对于词,我们也可以把它拆成指定数量的特征维度,词表中的每一个词都可以用这些维度组合成的向量来表示,这个就是Word Embedding的含义。

那么在本次比赛中,为什么要使用Embedding?原因其实很简单,数据中的后四列repeat_retailer,used_chip,used_pin_number,online_order是由0和1组成的,直接传入Linear层中,会导致Hidden Layer中很多特征没有使用到,这时候我们很容易就联想到Embedding!

具体做法就是使用Paddle.nn中的Embeeding实现Embedding,非常容易实现!

那么Embedding中的参数怎么选择?如上图中所示,可以看到主要的参数是num_embeddings和embedding_dim,在官方文档中我们可以看到:num_embeddings (int) - 嵌入字典的大小, input中的id必须满足 0 =< id < num_embeddings。在本次比赛的数据集中,后四列数据都是由0和1组成,我们在这里选择num_embeddings为2+1。然后在embedding_dim的选择上就比较主观了,一般按照经验选择后慢慢调整,一般而言,数据的id数量越大,dim就越大,在这里我们选择dim为16

2.NN模型设计

NN网络结构设计就比较玄学了,在网络设计上面参考了MarTech Challenge 点击反欺诈预测的baseline。对后四列数据传入Embedidng之后在过一个Linear层映射到统一的维度,在这里我们选择了一个16这个比较小的维度,那么对于前两列数据,我们就直接过一个linear层+Relu激活函数。随后将两者在维度1上concat起来,在过两个Linear层之后求均值,经过一个分类头就可以得到输出了。结构如下图:

3.开始训练

训练策略就比较简单粗暴了,优化器无脑选择Adamw,Loss函数选择BCEWithLogitsLoss,随机划分一下数据就直接开始训练了。

引入一下包

import paddle
import paddle.nn as nn
import pandas as pd

from paddle.io import Dataset
from sklearn import preprocessing
from paddle.io import random_split
from paddle.io import DataLoader
from paddle.optimizer import AdamW
from tqdm import tqdm
from sklearn.metrics import f1_score, accuracy_score

自定义数据集

# 自定义数据集
class MyDataset(Dataset):
    """
    步骤一:继承paddle.io.Dataset类
    """

    def __init__(self, data_url):
        """
        步骤二:实现构造函数,定义数据集大小
        """
        super(MyDataset, self).__init__()
        self.data_url = data_url
        ann = (pd.read_csv(data_url, encoding="utf-8").to_numpy())
        min_max_scaler = preprocessing.MinMaxScaler()
        self.ann = min_max_scaler.fit_transform(ann)

    def __getitem__(self, index):
        """
        步骤三:实现__getitem__方法,定义指定index时如何获取数据,并返回单条数据(训练数据,对应的标签)
        """
        # 前三列是无需过Embedding的,后四列需要过Embedding就单独返回
        no_emb = self.ann[index, 0:3]
        rep = self.ann[index, 3]
        chip = self.ann[index, 4]
        pin = self.ann[index, 5]
        ol = self.ann[index, 6]

        label = self.ann[index, -1]

        no_emb_tensor = paddle.to_tensor(no_emb, dtype='float32')
        rep = paddle.to_tensor(rep, dtype='int32')
        chip = paddle.to_tensor(chip, dtype='int32')
        pin = paddle.to_tensor(pin, dtype='int32')
        ol = paddle.to_tensor(ol, dtype='int32')
        label_tensor = paddle.zeros([2])
        if label == 0.:
            label_tensor[0] = 1
        else:
            label_tensor[1] = 1

        data = {
            'no_emb': no_emb_tensor,
            'rep': rep,
            'chip': chip,
            'pin': pin,
            'ol': ol,
            'label': label_tensor
        }

        return data

    def __len__(self):
        """
        步骤四:实现__len__方法,返回数据集总数目
        """
        return len(self.ann)

构造模型

# 定义模型
class My_modle(nn.Layer):
    def __init__(self, drop):
        super(My_modle, self).__init__()
        self.emb_rep = EmbLayer(num=2, dim=16, out_dim=16)
        self.emb_chip = EmbLayer(num=2, dim=16, out_dim=16)
        self.emb_pin = EmbLayer(num=2, dim=16, out_dim=16)
        self.emb_ol = EmbLayer(num=2, dim=16, out_dim=16)

        self.dense_fc = FCWithAct(3, 16, drop)

        self.fc1 = FCWithAct(16, 224, drop)
        self.fc2 = FCWithAct(224, 448, drop)
        self.fc3 = FCWithAct(448, 448, drop)

        self.class_head = nn.Linear(448, 2)

    def forward(self, inputs):
        x1 = self.dense_fc(inputs['no_emb'])
        # 这里要unsqueeze一下,将维度对齐
        x1 = paddle.unsqueeze(x1, 1)

        x2 = self.emb_rep(inputs['rep'])
        x3 = self.emb_chip(inputs['chip'])
        x4 = self.emb_pin(inputs['pin'])
        x5 = self.emb_ol(inputs['ol'])

        x = paddle.concat([x1, x2, x3, x4, x5], 1)

        y = self.fc1(x)
        y = self.fc2(y)
        y = paddle.mean(y, 1)
        y = paddle.squeeze(y, 1)
        y = self.fc3(y)

        out = self.class_head(y)
        return out


class EmbLayer(nn.Layer):
    def __init__(self, num, dim, out_dim):
        super(EmbLayer, self).__init__()
        self.emb = nn.Embedding(num, dim)
        self.fc = nn.Linear(dim, out_dim)

    def forward(self, inputs):
        y = self.emb(inputs)
        y = self.fc(y)

        return y


class FCWithAct(nn.Layer):
    def __init__(self, in_dim, out_dim, drop):
        super(FCWithAct, self).__init__()
        self.linear = nn.Linear(in_dim, out_dim)
        self.act = nn.ReLU()
        self.drop = nn.Dropout(drop)

    def forward(self, inputs):
        y = self.linear(inputs)
        y = self.act(y)
        y = self.drop(y)
        return y

定义一下评价指标,这里使用meanF1和acc进行评价,其中meanF1=(microF1+macroF1)/2

def getF1(predictions, labels):
    # prediction and labels are all level-2 class ids

    f1_micro = f1_score(labels, predictions, average='micro')
    f1_macro = f1_score(labels, predictions, average='macro')
    mean_f1 = (f1_micro + f1_macro) / 2.0

    eval_results = {'f1_micro': f1_micro,
                    'f1_macro': f1_macro,
                    'mean_f1': mean_f1}

    return eval_results

定义一下训练函数

# 定义训练函数
def train(model, train_loader, val_loader, loss_fun, opt, epoch):
    for i in range(epoch):

        mean_loss = paddle.zeros([1])

        train_loader = tqdm(train_loader)

        # train
        model.train()
        for step, data in enumerate(train_loader):
            pred = model(data)
            loss = loss_fun(pred, data['label'])

            mean_loss = (mean_loss * step + loss.detach()) / (step + 1)
            # 64, 1
            pred_acc = paddle.argmax(pred, 1)
            label = paddle.topk(data['label'], 1)[1]

            acc = accuracy_score(label.cpu().numpy(), pred_acc.cpu().numpy())

            loss.backward()
            opt.step()
            opt.clear_grad()

            train_loader.desc = "[epoch {}] loss{} mean_loss{} acc{} lr{}".format(i,
                                                                                  round(loss.item(), 2),
                                                                                  round(mean_loss.item(), 2),
                                                                                  round(acc, 2), opt.get_lr())
        # eval
        model.eval()

        predictions = []
        labels = []
        eval_loss = paddle.zeros([1])
        mean_acc = paddle.zeros([1])
        val_loader = tqdm(val_loader)
        for step, data in enumerate(val_loader):
            pred = model(data)
            loss = loss_fun(pred, data['label'])
            eval_loss = (eval_loss * step + loss.detach()) / (step + 1)

            pred_acc = paddle.argmax(pred, 1)
            label = paddle.topk(data['label'], 1)[1]

            acc = accuracy_score(label.cpu().numpy(), pred_acc.cpu().numpy())
            mean_acc = (mean_acc * step + acc) / (step + 1)

            predictions.extend(pred_acc.cpu().numpy())
            labels.extend(label.cpu().numpy())

        F1 = getF1(predictions, labels)

        print(f'mena_loss:{eval_loss.item()}, mean_acc:{mean_acc.item()}, {F1}')

        state_dict = model.state_dict()
        paddle.save(state_dict, f"save/Epoch{i}_f1{F1['mean_f1']}.pdparams")

开始训练,注意数据集直接放在和main.ipynb的同级目录下

# 定义超参数,固定随机种子
paddle.seed(2022)
TRAIN_BATCH_SIZE = 128
VAL_BATCH_SIZE = 256
DROP = 0.2
LR = 7e-4
DECAY_STEP = 7000
GAMMA = 0.5

# 创建数据集、loader

all_dataset = MyDataset('card_transdata.csv')

data_size = len(all_dataset)
train_size = int(data_size * 0.9)
val_size = int(data_size * 0.1)

train_dataset, val_dataset = random_split(all_dataset, [train_size, val_size])

train_loader = DataLoader(train_dataset, batch_size=TRAIN_BATCH_SIZE, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=VAL_BATCH_SIZE)

# 实现模型,优化器, loss
model = My_modle(DROP)

lr_scheduler = paddle.optimizer.lr.StepDecay(
    LR,
    step_size=DECAY_STEP,
    gamma=GAMMA
)

optimizer = AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters()
)

loss_fun = paddle.nn.BCEWithLogitsLoss()

fun = paddle.nn.BCEWithLogitsLoss()

train(model, train_loader, val_loader, loss_fun, optimizer, 20)

最终模型会保存在save文件夹下,训练效果如下:

在这里插入图片描述

可以看到模型收敛还是很快的,最终在训练了两轮之后就F1就开始下降了,最终的线下成绩在0.9878

4.优化方向

  1. 在数据上面可以多做几次实验,某些数据可能不是特别有用
  2. 项目中没有考虑数据清洗和类别不平衡问题,各位可以从Loss上优化或者将数据进行负采样,甚至是交叉验证
  3. 模型的层数,参数可以再多次选择一下
  4. 优化器,学习率,batchsize等等参数都可以再调整

转载自:https://aistudio.baidu.com/aistudio/projectdetail/4369391

Logo

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

更多推荐