CCF BDCI 返乡发展人群预测

机缘

在网上找数据集的时候无意中看到了有这么个比赛,伴随着好奇的心里以及尝试的心态就参加试一试。这次比赛出现了一些让我意想不到的结果特此记录一下。

赛题内容

基于中国联通的大数据能力,通过使用对联通的信令数据、通话数据、互联网行为等数据进行建模,对个人是否会返乡工作进行判断。以上为官网的背景介绍。以论文的角度看应该是要参考一些人口流动预测的场景。
数据集介绍:

  1. train.csv:包含全量数据集的70%(dataNoLabel是训练集的一部分,选手可以自己决定是否使用)
  2. 位置类特特征:基于联通基站产生的用户信令数据;
  3. 互联网类特征:基于联通用户上网产生的上网行为数据;
  4. 通话类特征:基于联通用户日常通话、短信产生的数据;
  5. 评价指标:该问题作为一个二分类问题使用ROC曲线下面积AUC(Area Under Curve)作为评价指标,AUC越大,预测越准确。
  6. 赛题链接:返乡人群预测

Baseline

这个赛题已经有大佬在网上开源了baseline方案。大佬的解决方案的大致思路是:

  1. 先对原始特征进行暴力穷举:
train_data['f47'] = train_data['f1'] * 10 + train_data['f2']
test_data['f47'] = test_data['f1'] * 10 + test_data['f2']
loc_f = ['f1', 'f2', 'f4', 'f5', 'f6']
for df in [train_data, test_data]:
    for i in range(len(loc_f)):
        for j in range(i + 1, len(loc_f)):
            df[f'{loc_f[i]}+{loc_f[j]}'] = df[loc_f[i]] + df[loc_f[j]]
            df[f'{loc_f[i]}-{loc_f[j]}'] = df[loc_f[i]] - df[loc_f[j]]
            df[f'{loc_f[i]}*{loc_f[j]}'] = df[loc_f[i]] * df[loc_f[j]]
            df[f'{loc_f[i]}/{loc_f[j]}'] = df[loc_f[i]] / (df[loc_f[j]]+1)

com_f = ['f43', 'f44', 'f45', 'f46']
for df in [train_data, test_data]:
    for i in range(len(com_f)):
        for j in range(i + 1, len(com_f)):
            df[f'{com_f[i]}+{com_f[j]}'] = df[com_f[i]] + df[com_f[j]]
            df[f'{com_f[i]}-{com_f[j]}'] = df[com_f[i]] - df[com_f[j]]
            df[f'{com_f[i]}*{com_f[j]}'] = df[com_f[i]] * df[com_f[j]]
            df[f'{com_f[i]}/{com_f[j]}'] = df[com_f[i]] / (df[com_f[j]]+1)
cat_columns = ['f3']
data = pd.concat([train_data, test_data])

for col in cat_columns:
    lb = LabelEncoder()
    lb.fit(data[col])
    train_data[col] = lb.transform(train_data[col])
    test_data[col] = lb.transform(test_data[col])
num_columns = [ col for col in train_data.columns if col not in ['id', 'label', 'f3']]
feature_columns = num_columns + cat_columns
target = 'label'

train = train_data[feature_columns]
label = train_data[target]
test = test_data[feature_columns]
print(train.shape)
print(train)
  1. 采用了lgb的树模型和5折交叉验证来进行训练
features = [i for i in train.columns if i not in ['label',  'id']]
y = train['label']
KF = StratifiedKFold(n_splits=5, random_state=2021, shuffle=True)
feat_imp_df = pd.DataFrame({'feat': features, 'imp': 0})
params = {
    'objective': 'binary',
    'boosting_type': 'gbdt',
    'metric': 'auc',
    'n_jobs': 30,
    'learning_rate': 0.05,
    'num_leaves': 2 ** 6,
    'max_depth': 8,
    'tree_learner': 'serial',
    'colsample_bytree': 0.8,
    'subsample_freq': 1,
    'subsample': 0.8,
    'num_boost_round': 5000,
    'max_bin': 255,
    'verbose': -1,
    'seed': 2021,
    'bagging_seed': 2021,
    'feature_fraction_seed': 2021,
    'early_stopping_rounds': 100,

}

oof_lgb = np.zeros(len(train))
predictions_lgb = np.zeros((len(test)))
# 模型训练
for fold_, (trn_idx, val_idx) in enumerate(KF.split(train.values, y.values)):
    print("fold n°{}".format(fold_))
    trn_data = lgb.Dataset(train.iloc[trn_idx][features], label=y.iloc[trn_idx])
    val_data = lgb.Dataset(train.iloc[val_idx][features], label=y.iloc[val_idx])
    num_round = 3000
    clf = lgb.train(
        params,
        trn_data,
        num_round,
        valid_sets=[trn_data, val_data],
        verbose_eval=100,
        early_stopping_rounds=50,
    )

    oof_lgb[val_idx] = clf.predict(train.iloc[val_idx][features], num_iteration=clf.best_iteration)
    predictions_lgb[:] += clf.predict(test[features], num_iteration=clf.best_iteration) / 5
    feat_imp_df['imp'] += clf.feature_importance() / 5

print("AUC score: {}".format(roc_auc_score(y, oof_lgb)))
print("F1 score: {}".format(f1_score(y, [1 if i >= 0.5 else 0 for i in oof_lgb])))
print("Precision score: {}".format(precision_score(y, [1 if i >= 0.5 else 0 for i in oof_lgb])))
print("Recall score: {}".format(recall_score(y, [1 if i >= 0.5 else 0 for i in oof_lgb])))

这里就不放完整的大佬代码了,有需要的可以去赛题的评论区里面自行获取。


自己的理解

以上是大佬们开源的解决方案,就我个人感觉方案存在一些小问题(虽然我的排名非常垃圾可是我依旧想这样说一下我的看法):

  1. 大佬们的解决方案没有使用那个未标注的数据集。
  2. 可以看到大佬们的特征构造是对原始特征的几个字段进行暴力穷举得到的新特征并没有使用全部的字段,那就有个问题为什么其他字段的特征就没有用呢?能不能有一种这样的方法可以自动发掘和构造这样的特征以及可以自主判断特征对于目标问题的重要程度呢?
  3. 而且大佬们采用了lgb模型没有考虑样本之间的关系。为什么我说样本之间可能是存在关系的呢?这里的意思并不是说会默认人的行为活动是相关的,就是说A的返乡行为一定会影响B。比如,如果A和B都是一个学校的学生,那么他俩会有影响。而是存在某种意义度量下样本之间存在一定的联系,具体为比如毕业季,不在同一个学校的两个互不认识的学生应该具有相同或者相似的行为比如毕业旅行、找工作、聚餐等等。这样就算两个人不认识,我觉得也可以根据特征上的相似度来构建样本之间的拓扑结构。从而聚合特征来消除类内之间样本的差异性和噪声得到比如返乡学生的一般性特征。这是我的想法,我觉得是有道理的。

解决方案

以上是我对场景的理解。也就是说我认为解决预测问题的需要一种可以自适应的挖掘出特征之间对目标的重要程度,并且可以使用未标注数据集,而且还可以自动挖掘出样本之间的相关性的模型。

  1. 针对第一个可以自动挖掘特征对目标的重要程度,换种说法也就是可以对特征进行自适应的加权。自然可以想到多头注意力机制。
  2. 而可以使用未标注数据集,而且还可以自动挖掘出样本之间的相关性这两个问题我们而然可以想到图模型。如果接触过图学习的可能知道图数据中有一个trainmask和testmask的参数。只要我们让那些未标注的数据的这两个参数全部等于false那也就是让这部分数据即不参与训练也不参与预测,只进行特征聚合。至于样本之间的联系我们知道图本身可以表示数据之间的联系。
  3. 经以上想法我们将样本中每个人的特征看成是图上的节点,将原始问题转化为图上的节点的二分类问题。通过多头注意力机制加图学习来进行预测。
  4. 但是以上想法存在问题,就是我们没有图上边的信息,我们该如何在没有先验知识的情况下构建图的邻接矩阵呢?我们采用KDD22上一篇癫痫病预测论文里面的方法:通过计算样本之间向量在映射到高维空间当中的余弦相似度,采用topk机制,选取前k个最大的值对应的样本作为该样本的一阶邻居这种方法让模型来自适应的学习出图的邻接矩阵。(论文名字我忘记了,我只记得是KDD22)

代码

引入库文件

import os
import numpy as np
import cv2
import matplotlib.pyplot as plt
import torch
from torch.utils.data import Dataset,DataLoader
import torch_geometric
import pandas as pd
import networkx as ntx
import torch
import numpy as np
import torch.nn as nn
import math
import torch.nn.functional as F
import torch.nn as nn
import torch.nn.functional as F
from torchmetrics import Accuracy
from torchmetrics import AUROC
#from torchmetrics.classification import BinaryAccuracy
import torch
import numpy as np
import torch.nn as nn
import math
import torch.nn.functional as F

引入数据集

batchsize=1024##batchsize直接和图的邻接矩阵的规模相关不能太小
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
class MyDataset(torch.utils.data.Dataset):
    def __init__(self, root,datatype,num=49858):
        name1= os.path.join(root,'dataTrain.csv')
        name2= os.path.join(root,'dataNoLabel.csv')
        name3 =os.path.join(root,'dataA.csv')  
        name=''
        if(datatype=='train'):
            name=name1
        else:
            name=name3
        df=pd.read_csv(name)
        df1=pd.read_csv(name1)
        df2=pd.read_csv(name2)
        df3=pd.read_csv(name3)
        self.data2=df2.insert(loc=df2.shape[1], column='label', value=2)
        self.data3=df3.insert(loc=df3.shape[1], column='label', value=3)
        data = pd.DataFrame(df,columns=['f3'])
        dummies = pd.get_dummies(data)
        for index,row in df.iteritems():
            if(index not in ['id','label','f3']):
                a=np.min(np.array(df1[[index]]))
                b=np.max(np.array(df1[[index]]))
                df[[index]]=df[[index]].apply(lambda x : (x-a)/(b-a))
        for index, row in dummies.iteritems():
        # print(index) 
        # print(dummies[index])
        # print(row)
            df.insert(loc=3, column=index, value=row.tolist())
        df=df.drop(columns='f3',inplace=False)##在里面f3字段是一个离散特征,我们将它转换为onehot编码
        print(df.shape)
        # self.traindata=df.sample(n=num, frac=None, replace=False, weights=None, random_state=None, axis=0)
        # print(self.traindata.shape)
        # self.testdata=df[~df.index.isin(self.traindata.index)]
        # print(self.testdata.shape)
        self.data=df
        self.datatype=datatype

    def getdata(self,index,df):
        a=df.iloc[index,1:49].values.tolist()
        b=df.iloc[index,49:].values.tolist()
        a = [float(i) for i in a]
        b = [float(i) for i in b]
        X=torch.tensor(a,dtype=torch.float32)
        # X=X.unsqueeze(-1)
        Y=torch.tensor(b,dtype=torch.float32)
        return X,Y

    def __getitem__(self, index):    
        samples, labels=self.getdata(index,self.data) 
        sample=[samples,labels]
        return sample

    def __len__(self):
        return self.data.shape[0]

traindata=MyDataset(root='./data/person/',datatype='train')
print(len(traindata))
testdata=MyDataset(root='./data/person/',datatype='test')
print(len(testdata))
train_size = int(len(traindata) * 0.9)
test_size = len(traindata) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(traindata, [train_size, test_size])
train_loader = DataLoader(train_dataset, batch_size=batchsize, shuffle=False)
print(len(train_dataset))
print(len(test_dataset))
print(len(testdata))
#for step, (input,label) in enumerate(train_loader):
    #print(input.shape)
    #print(label.shape)
print('+++++++++++++++++++++test++++++++++++++++++++')
test_loader = DataLoader(test_dataset, batch_size=batchsize, shuffle=False)
#for step, (input,label) in enumerate(test_loader):
    #print(input.shape)
    #print(label.shape)

多头注意力机制

class selfAttention(nn.Module) :
    def __init__(self, num_attention_heads, input_size, hidden_size):
        super(selfAttention, self).__init__()
        if hidden_size % num_attention_heads != 0 :
            raise ValueError(
                "the hidden size %d is not a multiple of the number of attention heads"
                "%d" % (hidden_size, num_attention_heads)
            )

        self.num_attention_heads = num_attention_heads
        self.attention_head_size = int(hidden_size / num_attention_heads)
        self.all_head_size = hidden_size

        self.key_layer = nn.Linear(input_size, hidden_size)
        self.query_layer = nn.Linear(input_size, hidden_size)
        self.value_layer = nn.Linear(input_size, hidden_size)

    def trans_to_multiple_heads(self, x):
        new_size = x.size()[ : -1] + (self.num_attention_heads, self.attention_head_size)
        x = x.view(new_size)
        return x.permute(0, 2, 1, 3)

    def forward(self, x):
        key = self.key_layer(x)
        query = self.query_layer(x)
        value = self.value_layer(x)#kqv

        key_heads = self.trans_to_multiple_heads(key)
        query_heads = self.trans_to_multiple_heads(query)
        value_heads = self.trans_to_multiple_heads(value)

        attention_scores = torch.matmul(query_heads, key_heads.permute(0, 1, 3, 2))
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)

        attention_probs = F.softmax(attention_scores, dim = -1)

        context = torch.matmul(attention_probs, value_heads)
        context = context.permute(0, 2, 1, 3).contiguous()
        new_size = context.size()[ : -2] + (self.all_head_size , )
        context = context.view(*new_size)
        return context

图学习层

class attgraNet(nn.Module):
    def __init__(self,inputsize,batchsize,k):
        super(attgraNet, self).__init__()
        self.k=k
        self.batchsize=batchsize
        self.inputsize=inputsize
        self.fc1 = nn.Linear(inputsize,inputsize*10)
        self.att = selfAttention(4,10,48)
        #self.para = torch.nn.Parameter(torch.ones([2,batchsize]), requires_grad=True)
        #四层GAT
        self.gat1=torch_geometric.nn.GATConv(48*48,16,16,dropout=0.6)
        self.act1=nn.LeakyReLU(0.1)
        self.gat2=torch_geometric.nn.GATConv(256,8,8,dropout=0.6)
        self.act2=nn.LeakyReLU(0.1)
        self.gat3=torch_geometric.nn.GATConv(64,8,8,dropout=0.6)
        self.act3=nn.LeakyReLU(0.1)
        self.gat4=torch_geometric.nn.GATConv(64,16,16,dropout=0.6)
        self.act4=nn.LeakyReLU(0.1)
        self.fc2 = nn.Sequential(nn.Linear(16*16, 84),nn.LeakyReLU(0.1),nn.BatchNorm1d(84))
        self.fc3 = nn.Linear(84, 2)

    def forward(self, x):
        #print(x.size())
        print(x.device)
        x = self.fc1(x)
        x = x.unsqueeze(-1)
        x = x.reshape(-1,self.inputsize,10)
        #print(x.size())
        x = self.att(x)
        x = x.reshape(x.shape[0],-1,1)
        #print(x.size())
        a=x
        dim0, dim1,dim2 = a.shape
        #print(dim0)
        #print(dim1)
        #print(dim2)
        para = torch.ones([2,a.shape[0]],dtype=torch.long).to(device)
        #print(para.shape)
        for i in range(dim0):
            score=torch.zeros(dim0)
            for j in range(dim0):
                if(i!=j):
                    #print(a[i].shape)
                    score[j]=torch.abs(torch.cosine_similarity(a[i], a[j], dim=0))
            #print(score)
            #print(torch.argmax(score, dim=0))
            for j in range(self.k):
                idx=torch.argmax(score, dim=0)
                para.data[0][i]=i
                para.data[1][i]=idx
                score[idx]=0
        #构造邻接矩阵
        x = x.reshape(dim0,-1)
        data = torch_geometric.data.Data(x=x, edge_index=para.long()).to(device)
        #print(data.x.shape)
        #print(data.edge_index.shape)
        #print(data.x.is_cuda)
        #print(data.edge_index.is_cuda)
        x = self.gat1(data.x,data.edge_index)
        x = self.act1(x)
        #print(x.shape)
        #print(data.edge_index.shape)
        x = self.gat2(x,data.edge_index)
        #print(x.shape)
        #print(data.edge_index.shape)
        x = self.act2(x)
        x = self.gat3(x,data.edge_index)
        x = self.act3(x)
        x = self.gat4(x,data.edge_index)
        x = self.act4(x)
        x = self.fc2(x)
        x = F.dropout(x, training=self.training)
        x = self.fc3(x)
        return F.log_softmax(x,dim=1)

训练

net = attgraNet(48,batchsize,5)
print(net)
net.to(device)
print(next(net.parameters()).device)
criterion = torch.nn.NLLLoss()
optimizer = torch.optim.Adam(net.parameters())
print(torch.cuda.is_available())
def train(epoch,model):        
    model.train()        
    running_loss = 0.0    
    for i,(X, y) in enumerate(train_loader):
        input = X.to(device)                     
        y=y.to(device)
        output = model.forward(input)          
        #print(output.shape)
        #print(y.squeeze(dim=1).shape)
        loss = criterion(output , y.squeeze(dim=1).long())
        print("[{}, {}] loss %{}':".format(epoch,i,loss))
        running_loss += loss.item()
 
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
    epoch_loss_train = running_loss / (len(train_dataset)/batchsize)        
    print(epoch_loss_train)
    return epoch_loss_train

测试

def val(model):
    model.eval()        
    running_loss = 0.0 
    n=0
    result=0
    resauroc=0
    metric = Accuracy(top_k=1)    
    #auroc = AUROC(num_classes=2)
    #应该以AUC来评判,但是我如果使用torchmetric中的AUC汇报GPU资源不够的错误    
    with torch.no_grad():   
        for i, (data,y) in enumerate(test_loader):
            input = data.to(device)         
            y=y.to(device)
            optimizer.zero_grad()               
            output = model.forward(input)
            #print(y.squeeze(dim=1).long().shape)
            #print(output.shape)
            loss= criterion(output,y.squeeze(dim=1).long())
            print("[{}] loss %{}':".format(i,loss))
            n=n+1
            res=metric(output.cpu(),y.squeeze(dim=1).long().cpu())
            #res1=AUROC(output.cpu(),y.squeeze(dim=1).long().cpu())
            print("[{}] ACC %{}':".format(i,res))
            #print("[{}] AUROC %{}':".format(i,res1))
            result=res+result
            #resauroc=res1+resauroc
            running_loss += loss.item()
 
    epoch_loss_val = running_loss / (len(test_dataset)/batchsize)    
    print(epoch_loss_val)
    print(result/n)
    #print(resauroc/n)
    return epoch_loss_val
    
val(net)

其余代码

def main(model):
    min_loss = 100000000.0    
    loss_train = []        
    loss_val = []          
    epochs=200
    since = time.time()           
    for epoch in range(epochs):
        epoch_loss_train = train(epoch,model) 
        loss_train.append(epoch_loss_train)
        epoch_loss_val = val(model) 
        loss_val.append(epoch_loss_val)
 
        if epoch_loss_val < min_loss:
            min_loss = epoch_loss_val               
            best_model_wts = model.state_dict()
            #torch.save(best_model_wts,os.path.join(parameter_address, experiment_name + '.pkl'))  
            torch.save(model.state_dict(),'bestsaveBIG.pt')
        model_wts = model.state_dict()    
        #torch.save(model_wts,os.path.join(parameter_address, experiment_name + "_" + str(epoch) + '.pkl'))  
        time_elapsed = time.time() - since   
        #torch.save(model,str(epoch)+'.pt')
    torch.save(model.state_dict(),'lastsaveBIG.pt')
if __name__ == "__main__":
    main(net)
    print('train finish')

Tips

  1. 以上是我的想法,但是理想很丰满现实很骨干,精度在A榜上只有0.89,B榜上只有0.88.
  2. 大家应该看到我也没有使用未标注的nolabel数据,其实理论上使用起来非常简单,把那个nolabel数据变成tensor和每个batch的数据拼接在一起进行计算余弦相似度,然后设置一个与拼接在一起以后batch长度相等的布尔型的的tensor,让未标注的数据的batch对应的索引为false其他为true就可以了。但是我为什么说是理论上是很简单呢,因为我们在计算余弦相似度的时候算法的时间复杂度是batchsize的平方量级的,而那个未标注数据里面大概有四万多数据。如果拼接之前的话我们需要计算1024*1024时间复杂度,拼接后的时间复杂度就是四万的平方。我拼接过后发现这个程序到比赛结束连一个batch都没有跑完(我是10月25号发现这个比赛的)所以就果断放弃了。
  3. 哎没想到啊,我本来以为我的想法是很有道理的,同时也是十分相信GNN的模型预测能力。没想到是这种结果连开源的baseline的精度都没有超过。无法接受,我个人认为GNN的模型预测能力是毋庸置疑的,可能是因为本人能力不够同时对GNN理解程度才导致这样的结果。还是需要巩固基础,加强GNN调参的技巧。同时我也会参加更多这样的比赛。希望有一天我可以使用GNN得到一个不错的名次。

你可能感兴趣的:(图神经网络,人工智能,机器学习,python,人工智能,开发语言)