Datawhale AI夏令营-交叉验证,神经网络

167 阅读7分钟

k折交叉验证

对于多数的机器学习,调节参数对结果有很大的影响,同时要考虑一个模型的优势,就要考察模型的泛化和拟合程度,由此也可以看出数据之间的差异。交叉验证是调节参数,评估模型的较好途径,让我们在提交之前有更多的测试机会。交叉验证主要有k折和留一两种类型,其核心思想就是在训练集中分离验证集,采用不同统计方法。

def cv_model(clf, train_x, train_y, test_x, clf_name, seed=2022):
    kf = KFold(n_splits=5, shuffle=True, random_state=seed)
 
    train_preds = np.zeros(train_x.shape[0])
    test_preds = np.zeros(test_x.shape[0]) #取和原数组形状一致的0矩阵
 
    cv_scores = []
 
    for i, (train_index, valid_index) in enumerate(kf.split(train_x)):
        print(f'Fold {i+1}/{kf.n_splits}')
 
        trn_x, trn_y = train_x.iloc[train_index], train_y.iloc[train_index]
        val_x, val_y = train_x.iloc[valid_index], train_y.iloc[valid_index]
 
        model = clf(iterations=20000, learning_rate=0.1, depth=6, l2_leaf_reg=10, bootstrap_type='Bernoulli',
                    random_seed=seed, od_type='Iter', od_wait=100, allow_writing_files=False, task_type='CPU',
                    eval_metric='AUC')
 
        model.fit(trn_x, trn_y, eval_set=(val_x, val_y), verbose=100, use_best_model=True)
 
        val_pred = model.predict_proba(val_x)[:, 1]
        test_pred = model.predict_proba(test_x)[:, 1]
 
        train_preds[valid_index] = val_pred
        test_preds += test_pred / kf.n_splits
 
        cv_scores.append(f1_score(val_y, np.where(val_pred > 0.5, 1, 0)))
    print(f"{clf_name} score list:", cv_scores)
    print(f"{clf_name} mean score:", np.mean(cv_scores))
    print(f"{clf_name} std score:", np.std(cv_scores))
 
    return train_preds, test_preds

这是一个使用5折交叉验证模型的函数,其中kf = KFold(n_splits=5, shuffle=True, random_state=seed)就是k折交叉验证的划分器, for i, (train_index, valid_index) in enumerate(kf.split(train_x)):会返回索引列表,用于验证集和训练集的构建,值得注意enumerate( sequence, [start=0])函数,该函数会同时返回这个数组和序列的组合数组。 除此以外,值得学习的还有:iloc,loc,这都panda库中进行行列搜索的工具,但一般一直使用loc这是根据标签进行索引的,后者是根据位置进行索引的。

  1. .loc:当你需要根据标签来选择数据时,例如选择特定的行或列,或者行和列的组合。
  2. .iloc:当你需要根据位置来选择数据时,例如选择第n行和第m列的数据。

上段代码中提及val_pred = model.predict_proba(val_x)[:, 1],这与通常采用的model.predict不同,事实上,在scikit-learn库中,有三种方法来预测,这里介绍两种,一般在分类问题中,model.predict,model.predict_proba,前者直接给出了类别,后者给出了类别概率的数组,顺序是按照model.classes_中,如果是[0,1],所以文中截取了第二列。 这就是交叉验证的基础介绍,上述函数同时给出对test的预测,值得注意的,最后的预测是每折模型预测结果的平均,这让我们联想的到集成学习,事实上,不仅rf内部的集成学习,我们也可以自行写模型融合。

讨论完交叉验证,值得继续探讨网格搜索去优化超参数。这远比自己手动调参高效的多。但这也仅限于小数据集

from sklearn.model_selection import GridSearchCV
# 搜索的参数
knn_paras = {"n_neighbors":[1,3,5,7]}
# 默认的模型
knn_grid = KNeighborsClassifier()
# 网格搜索的实例化对象
grid_search = GridSearchCV(
 knn_grid, 
 knn_paras, 
 cv=10  # 10折交叉验证
grid_search.fit(X_train, y_train)
GridSearchCV(cv=10, estimator=KNeighborsClassifier(),param_grid={'n_neighbors': [1357]})
# 通过搜索找到的最好参数值
grid_search.best_estimator_ 
KNeighborsClassifier(n_neighbors=7)
grid_search.best_params_

讨论了那么多,都在围绕机器学习,随着2006年的深度学习的兴起,人工智能的在众多领域达到了突破,而两者关联在于,深度学习可以归为机器学习的一个子集,主要通过神经网络学习数据的特征和分布。深度学习的一个重要进化是不再需要繁琐的特征工程,让神经网络自己从里面学习特征,但前提是提供的数据中有足够多的明显学习特征。

递归神经网络

在本次的训练目标下,GNN可能是更好的选择,但本次我们先了解RNN的搭建,然后逐步了解GNN。 回归这次的主题,在第一篇章中我们称化学是自然的语言,联想到自然语言领域常用的模型,RNN,

image.png 简单概括就是在监督下,训练一个包含有序数据全部内容导向的结果的模型,存在累乘效应,就相当于会遗忘之前的内容。

class RNNModel(nn.Module):
    def __init__(self, num_embed, input_size, hidden_size, output_size, num_layers, dropout, device):
        super(RNNModel, self).__init__()
        self.embed = nn.Embedding(num_embed, input_size)
        self.rnn = nn.RNN(input_size, hidden_size, num_layers=num_layers, 
                          batch_first=True, dropout=dropout, bidirectional=True)
        self.fc = nn.Sequential(nn.Linear(2 * num_layers * hidden_size, output_size),
                                nn.Sigmoid(),
                                nn.Linear(output_size, 1),
                                nn.Sigmoid())

    def forward(self, x):
        # x : [bs, seq_len]
        x = self.embed(x)
        # x : [bs, seq_len, input_size]
        _, hn = self.rnn(x) # hn : [2*num_layers, bs, h_dim]
        hn = hn.transpose(0,1)
        z = hn.reshape(hn.shape[0], -1) # z shape: [bs, 2*num_layers*h_dim]
        output = self.fc(z).squeeze(-1) # output shape: [bs, 1]
        return output

在编程语言中有些概念还是十分有趣的,在这里的使用了python的继承的方法。 继承能实现什么功能?

  • 类解决对象与对象之间代码冗余的问题,子类可以遗传父类的属性
  • 继承解决的是类与类之间代码冗余的问题
  • object类丰富了代码的功能

这样说的很笼统,很多精妙的点无法立即体会,class RNNModel(nn.Module):,这里class我们通常称创建了一个类,一个继承nn.Module的RNNModle,但其实也可以说是一种是数据类型,对于动态语言的python所有的类都继承自object,只要类型是继承object并且有调用的函数,就可以直接套用对nn.Module的调用搭建的程序,这不仅是动态语言的特点,也是继承的特点多态。 这就是设计模式的原则之一:开闭原则。

近一步,我们发现__init__这样的表示,定义了方法,这是python中的私有对象

为详细而具体的理解这层代码,以及搭建网络的逻辑,先从python的语言特性学习,然后再介绍RNN的数学原理。首先明确一些词汇具体指代(对象,属性,类,方法):

  1. 对象指的是一个具体的实体,不用于指代一个抽象的群体。(实例化)
  2. 属性就是类属性也有实例属性
  3. 类是对一组具有相同属性和行为的对象的抽象
  4. 方法,它是对属性的操作,包括读取操作和修改操作。

以上就是面向对象开发的基础。我们基本理解了类的存在就是为优化代码,其中有一点需要具体讲解。

类属性和实例属性

 def __init__(self, num_embed, input_size, hidden_size, output_size, num_layers, dropout, device):
        super(RNNModel, self).__init__()
        self.embed = nn.Embedding(num_embed, input_size)
        self.rnn = nn.RNN(input_size, hidden_size, num_layers=num_layers, 
                          batch_first=True, dropout=dropout, bidirectional=True)
        self.fc = nn.Sequential(nn.Linear(2 * num_layers * hidden_size, output_size),
                                nn.Sigmoid(),
                                nn.Linear(output_size, 1),
                                nn.Sigmoid())

我们可以看到这里的__init__这是私有成员,是构造函数,就是将类构造成对象,其中self是约定的表示,也可以更改,但这表征的是对象属性。不需要使用self而直接使用类加属性就是类属性。这里也有但直接继承下来了。 与构造函数对应的就是析构函数 __del__,意味着将不再需要这个对象时,还会采取的操作,比如可以将更改过的类属性恢复。

私有属性和私有方法

关注到上面所谈论的构造函数和析构函数,可以发现这是特定的表示方式。上面说析构函数可以恢复类属性,其实大多数的类属性不需要更改,也不准更改,此时私有属性的作用就格外重要。 要想读取或者更改只能在类的定义中进行操作,实例中就不行,总的来说,在类的外部通过 get/set 方法访问私有属性。 同样的定义,私有方法也是类中进行操作,也只需在名称前添加前缀 __。

多态和继承

super(RNNModel, self).__init__()表示调用父类的构造方法,这一部分在上文以及举例说明了。

现在我们回归到这个神经网络本身,