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这是根据标签进行索引的,后者是根据位置进行索引的。
.loc:当你需要根据标签来选择数据时,例如选择特定的行或列,或者行和列的组合。.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': [1, 3, 5, 7]})
# 通过搜索找到的最好参数值
grid_search.best_estimator_
KNeighborsClassifier(n_neighbors=7)
grid_search.best_params_
讨论了那么多,都在围绕机器学习,随着2006年的深度学习的兴起,人工智能的在众多领域达到了突破,而两者关联在于,深度学习可以归为机器学习的一个子集,主要通过神经网络学习数据的特征和分布。深度学习的一个重要进化是不再需要繁琐的特征工程,让神经网络自己从里面学习特征,但前提是提供的数据中有足够多的明显学习特征。
递归神经网络
在本次的训练目标下,GNN可能是更好的选择,但本次我们先了解RNN的搭建,然后逐步了解GNN。 回归这次的主题,在第一篇章中我们称化学是自然的语言,联想到自然语言领域常用的模型,RNN,
简单概括就是在监督下,训练一个包含有序数据全部内容导向的结果的模型,存在累乘效应,就相当于会遗忘之前的内容。
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的数学原理。首先明确一些词汇具体指代(对象,属性,类,方法):
- 对象指的是一个具体的实体,不用于指代一个抽象的群体。(实例化)
- 属性就是类属性也有实例属性
- 类是对一组具有相同属性和行为的对象的抽象。
- 方法,它是对属性的操作,包括读取操作和修改操作。
以上就是面向对象开发的基础。我们基本理解了类的存在就是为优化代码,其中有一点需要具体讲解。
类属性和实例属性
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__()表示调用父类的构造方法,这一部分在上文以及举例说明了。
现在我们回归到这个神经网络本身,