常规赛:个贷违约预测 - 使用Paddle极简构造个贷违约预测器

赛题介绍

利用已有的与目标客群稍有差异的另一批信贷数据,辅助目标业务风控模型的创建,两者数据集之间存在大量相同的字段和极少的共同用户。此处希望大家可以利用迁移学习捕捉不同业务中用户基本信息与违约行为之间的关联,帮助实现对新业务的用户违约预测。

数据介绍

训练集包括10000条数据,属性分别为

‘loan_id’, ‘user_id’, ‘total_loan’, ‘year_of_loan’, ‘interest’,
‘monthly_payment’, ‘class’, ‘employer_type’, ‘industry’, ‘work_year’,
‘house_exist’, ‘censor_status’, ‘issue_date’, ‘use’, ‘post_code’,
‘region’, ‘debt_loan_ratio’, ‘del_in_18month’, ‘scoring_low’,
‘scoring_high’, ‘known_outstanding_loan’, ‘known_dero’,
‘pub_dero_bankrup’, ‘recircle_b’, ‘recircle_u’, ‘initial_list_status’,
‘app_type’, ‘earlies_credit_mon’, ‘title’, ‘policy_code’, ‘f0’, ‘f1’,
‘f2’, ‘f3’, ‘f4’, ‘early_return’, ‘early_return_amount’,
‘early_return_amount_3mon’, ‘isDefault’

其中’isDefault’为目标分类值,近1/3为分类变量,近2/3为数值型变量,剩余变量包含时间信息等。

并且10000条数据中仅1683个样本为正样本,剩余为负样本,即样本不平衡。

方案介绍

  1. 对于样本不平衡的问题,修改损失函数的权重,将负样本的权重设为0.2,正样本为1.0
  2. 对于连续数值型变量,对他们进行 均值-方差归一化
  3. 对于分类变量,在网络中进行重编码(即增加一个全连接层,用于模拟embedding)
  4. 本方案直接弃用了地区编码,时间等信息。

代码

# 查看样本信息
import pandas as pd
train_df=pd.read_csv('data/data130186/train_public.csv')
# 展示一下train_df
# train_df.columns # 展示列名
train_df

构造dataset

通过paddle的io.dataset构造数据读取可以不用手动写入shuffle和drop_last判断,但是本质效果和直接写一个loader进行yeild是一样的

import paddle
import numpy as np
import paddle.vision.transforms as T
from PIL import Image
import pandas as pd

class MyDateset(paddle.io.Dataset):
    # csv_dir对应要读取的数据地址,standard_csv_dir用于生成均值和方差信息对数据进行归一化的文件地址
    def __init__(self,csv_dir,standard_csv_dir='data/data130186/train_public.csv',mode = 'train'):
        super(MyDateset, self).__init__()

        # 读取数据
        self.df = pd.read_csv(csv_dir)
        
        # 构造各个变量的均值和方差
        st_df = pd.read_csv(standard_csv_dir)
        self.mean_df = st_df.mean()
        self.std_df = st_df.std()

        # 分别指定数值型变量/分类变量/不使用的变量
        self.num_item = ['total_loan', 'year_of_loan', 'interest','monthly_payment',
        'debt_loan_ratio', 'del_in_18month', 'scoring_low','scoring_high', 'known_outstanding_loan', 'known_dero','pub_dero_bankrup', 'recircle_b', 'recircle_u', 
        'f0', 'f1','f2', 'f3', 'f4', 'early_return', 'early_return_amount','early_return_amount_3mon']
        self.un_num_item = ['class','employer_type','industry','work_year','house_exist', 'censor_status',
        'use',
        'initial_list_status','app_type',
        'policy_code']
        self.un_use_item = ['loan_id', 'user_id',
        'issue_date', 
        'post_code', 'region',
        'earlies_credit_mon','title']

        # 构造一个映射表,将分类变量/分类字符串映射到对应数值上
        un_num_item_list = {}
        for item in self.un_num_item:
            un_num_item_list[item]=list(set(st_df[item].values))
        self.un_num_item_list = un_num_item_list

        self.mode = mode

    def __getitem__(self, index):
        data=[]

        # 进行归一化,如果这个数值缺省了直接设置为0
        for item in self.num_item:
            if np.isnan(self.df[item][index]):
                data.append((0-self.mean_df[item])/self.std_df[item])
            else:
                data.append((self.df[item][index]-self.mean_df[item])/self.std_df[item])
        
        emb_data = []

        # 将分类变量映射到对应数值上
        for item in self.un_num_item:
            try:
                if self.df[item][index] not in self.un_num_item_list[item]:
                    emb_data.append(-1)
                else:
                    emb_data.append(self.un_num_item_list[item].index(self.df[item][index]))
            except:
                emb_data.append(-1)

        data = paddle.to_tensor(data).astype('float32')
        emb_data = paddle.to_tensor(emb_data).astype('float32')

        # 如果当前模式不为train,则返回对应的loan_id,用于锁定样本条目
        if self.mode == 'train':
            label = self.df['isDefault'][index]
        else:
            label = self.df['loan_id'][index]

        label = np.array(label).astype('int64')
        return data,emb_data,label

    def __len__(self):
        return len(self.df)
dataset=MyDateset('data/data130186/train_public.csv')
[data,emb_data,label] = dataset[0]
print(data.shape)
print(emb_data.shape)
print(label)
W0304 08:24:17.984318   274 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W0304 08:24:17.989488   274 device_context.cc:465] device: 0, cuDNN Version: 7.6.


[21]
[10]
0

构造网络

对于分类变量使用两层全连接成生成embedding

class MyNet(paddle.nn.Layer):
    def __init__(self):
        super(MyNet,self).__init__()
        self.fc = paddle.nn.Linear(in_features=21, out_features=512)

        self.emb1 = paddle.nn.Linear(in_features=10,out_features=2048)
        self.emb2 = paddle.nn.Linear(in_features=2048,out_features=512)

        self.out = paddle.nn.Linear(in_features=1024,out_features=2)

    def forward(self,data,emb_data):
        x = self.fc(data)

        emb = self.emb1(emb_data)
        emb = self.emb2(emb)

        x = paddle.concat([x,emb],axis=-1)

        x = self.out(x)
        
        x = paddle.nn.functional.sigmoid(x)
        return x

训练

# 构造读取器
train_dataset=MyDateset('data/data130186/train_public.csv')

train_dataloader = paddle.io.DataLoader(
    train_dataset,
    batch_size=1000,
    shuffle=True,
    drop_last=False)
# 构造模型
model = MyNet()

# model_dict = paddle.load('model.pdparams')
# model.set_dict(model_dict)

model.train()

max_epoch=10
opt = paddle.optimizer.SGD(learning_rate=0.1, parameters=model.parameters())

# 训练
now_step=0
for epoch in range(max_epoch):
    for step, data in enumerate(train_dataloader):
        now_step+=1

        data,emb_data, label = data
        pre = model(data,emb_data)
        loss = paddle.nn.functional.cross_entropy(pre,label,weight=paddle.to_tensor([0.2,1.0]),reduction='mean')
        # loss = paddle.nn.functional.square_error_cost(pre,label.reshape([-1,1]).astype('float32'))
        # loss = paddle.mean(loss)
        loss.backward()
        opt.step()
        opt.clear_gradients()
        if now_step%1==0:
            print("epoch: {}, batch: {}, loss is: {}".format(epoch, step, loss.mean().numpy()))

# 保存模型到model.pdparams
paddle.save(model.state_dict(), 'model.pdparams')

预测

这里直接读取保存好的得分为0.85984的模型,如需测试自己的模型请替换对应的模型读取路径

最后直接提交生成result.csv即可

# 读取模型和构造读取器
model = MyNet()

# 如果想要替换自己的训练结果请替换load的pdparams文件路径,如model.pdarams
model_dict = paddle.load('model_85.pdparams')
# model_dict = paddle.load('model.pdparams')

model.set_dict(model_dict)

model.eval()

test_dataset=MyDateset('data/data130187/test_public.csv',mode = 'test')

test_dataloader = paddle.io.DataLoader(
    test_dataset,
    batch_size=1,
    shuffle=False,
    drop_last=False)
# 将结果保存在result.csv中
result = []
for step, data in enumerate(test_dataloader):
    data ,emb_data, loan_id = data
    pre = model(data,emb_data)
    result.append([loan_id.numpy()[0], pre[:,1].numpy()[0]])
    # result.append([loan_id.numpy()[0], np.argmax(pre.numpy())])

pd.DataFrame(result,columns=['id','isDefault']).to_csv('result.csv',index=None)

结语

本项目通过非常简朴的方式构造了一个分类器,AUC为0.85984,查看了原比赛链接,榜单上大量选手得分为0.9,因此本模型仍有改进空间。

可以考虑从以下几点进行改进:

  1. 更替网络结构:当前的网络仅包含4个全连接层和一个Sigmoid函数,较为简朴,可以增加层数和激活函数促使模型收敛到更高精度。
  2. 对时间信息等加以使用。
  3. 使用dropout等策略。

请点击此处查看本环境基本用法.

Please click here for more detailed instructions.

此文章为搬运
原项目链接

更多推荐