JJJYmmm 发布的文章

基本概念

K折交叉验证用于评估一个模型的泛化性能,那么网格搜索通过调参来提高模型的泛化性能.

简单的网格搜索实现

提前划分好训练集和测试集,简单通过两个for循环寻找最佳参数,评估效果直接使用模型自带的score.

# naive grid search implementation
from sklearn.svm import SVC
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, random_state=0)
print("Size of training set: {}   size of test set: {}".format(
      X_train.shape[0], X_test.shape[0]))

best_score = 0

for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
    for C in [0.001, 0.01, 0.1, 1, 10, 100]:
        # for each combination of parameters, train an SVC
        svm = SVC(gamma=gamma, C=C)
        svm.fit(X_train, y_train)
        # evaluate the SVC on the test set
        score = svm.score(X_test, y_test)
        # if we got a better score, store the score and parameters
        if score > best_score:
            best_score = score
            best_parameters = {'C': C, 'gamma': gamma}

print("Best score: {:.2f}".format(best_score))
print("Best parameters: {}".format(best_parameters))

验证集的引进

上节实现的简单网格搜索存在一个致命问题:将用来调整参数的测试集用于评估模型的好坏!

换句话说,我们是在测试集上选择出来的最佳参数,整个参数不能保证对于新数据也work well.所以需要引入一组新数据.那么数据总共就被分为三份:

  • 用于训练模型的训练集
  • 用于调整超参数的验证集
  • 用于评估模型的测试集

测试集模型训练阶段从未见过,只在评估时使用.

验证集用来衡量训练集训练出来的模型好坏

最后评估时,模型会选择表现最好的超参,用训练集+验证集重新训练,再用测试集进行评估.这一步主要是充分利用验证集数据.

from sklearn.svm import SVC
# split data into train+validation set and test set
X_trainval, X_test, y_trainval, y_test = train_test_split(
    iris.data, iris.target, random_state=0)
# split train+validation set into training and validation sets
X_train, X_valid, y_train, y_valid = train_test_split(
    X_trainval, y_trainval, random_state=1)
print("Size of training set: {}   size of validation set: {}   size of test set:"
      " {}\n".format(X_train.shape[0], X_valid.shape[0], X_test.shape[0]))

best_score = 0

for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
    for C in [0.001, 0.01, 0.1, 1, 10, 100]:
        # for each combination of parameters, train an SVC
        svm = SVC(gamma=gamma, C=C)
        svm.fit(X_train, y_train)
        # evaluate the SVC on the validation set
        score = svm.score(X_valid, y_valid)
        # if we got a better score, store the score and parameters
        if score > best_score:
            best_score = score
            best_parameters = {'C': C, 'gamma': gamma}

# rebuild a model on the combined training and validation set,
# and evaluate it on the test set
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)
test_score = svm.score(X_test, y_test)
print("Best score on validation set: {:.2f}".format(best_score))
print("Best parameters: ", best_parameters)
print("Test set score with best parameters: {:.2f}".format(test_score))

带交叉验证的网格搜索

上一节中人为分出了测试集\训练集\验证集,这钟方法对于数据的划分也比较敏感.因此可以在划分训练集/验证集时使用交叉验证,更好的评估模型的泛化能力.

for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
    for C in [0.001, 0.01, 0.1, 1, 10, 100]:
        # for each combination of parameters,
        # train an SVC
        svm = SVC(gamma=gamma, C=C)
        # perform cross-validation
        scores = cross_val_score(svm, X_trainval, y_trainval, cv=5)
        # compute mean cross-validation accuracy
        score = np.mean(scores)
        # if we got a better score, store the score and parameters
        if score > best_score:
            best_score = score
            best_parameters = {'C': C, 'gamma': gamma}
# rebuild a model on the combined training and validation set
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)

image-20230328113803191

GridSearchCV 热图可视化

from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
              'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
grid_search = GridSearchCV(SVC(), param_grid, cv=5,
                          return_train_score=True)
                          
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, random_state=0)
grid_search.fit(X_train, y_train)

scores = np.array(results.mean_test_score).reshape(6, 6)

# plot the mean cross-validation scores
mglearn.tools.heatmap(scores, xlabel='gamma', xticklabels=param_grid['gamma'],
                      ylabel='C', yticklabels=param_grid['C'], cmap="viridis")

image-20230328114043350

不同的交叉验证策略

嵌套交叉验证

在交叉验证的网格搜索中,我们使用交叉验证来完成训练集/验证集的划分,测试集的划分我们还是手动的.嵌套交叉验证让我们测试集的划分也使用交叉验证.这会使结果更合理,当然需要训练模型的个数也upup~(比如36个模型超参数组合,嵌套交叉验证都是5折,那就需要训练900个模型)

param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
              'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
scores = cross_val_score(GridSearchCV(SVC(), param_grid, cv=5),
                         iris.data, iris.target, cv=5)
print("Cross-validation scores: ", scores)
print("Mean cross-validation score: ", scores.mean())

整体思路

​ RoI-Header共由三部分组成:

  • box_roi_pool:Multi-scale RoIAlign pooling
  • box_head:TwoMLPHead
  • box_predictor:FastRCNNPredictor

MultiScaleRoIAlign

​ 该类与之前所述的RoIPooling不同,RoIAlign的定位能力更强。RoIPooling在计算过程中存在取整操作,从而引入了更多的定位误差,而Align不会进行取整操作。具体以后再展开~

image-20230325223450415

TwoMLPHead

​ TwoMLPHead其实就是RoIPooling之后跟着的两个全连接层(还有一个Flatten层).

image-20230325223606050

FastRCNNPredictor

​ FastRCNNPredictor也就是两个全连接层,分别预测每个proposal的类别和bbox的回归参数。

image-20230325223734155

注意输入的num_classes应该是实际类型+1,因为第0类是background

image-20230325223834839

ROIHeads

init

​ 保存一些需要用到的工具.

  • box_similarity负责计算box_iou
  • proposal_matcher负责正负样本的分配
  • fg_bg_sampler负责正负样本的采样
  • 其他参数就是刚刚提到的类以及一些阈值参数了

image-20230325223924768

forward

​ 训练模式下,首先会对proposal进一步采样,得到proposal样本和对应的label.

​ 其次将proposal和features特征层送入roi_pool得到每个proposal的box_features.box_features的形状应该是[num_proposals,channel,7,7]

​ 随后将box_features送入box_header提取出特征向量

​ 最后将这些向量送入box_predictor得到类别和回归参数预测结果

image-20230325224128474

​ 最后一部分代码如下.如果是训练模式下将通过fastrcnn_loss计算损失;如果是预测模式则会对proposals进行预处理postprocess_detections.最后返回相应的结果

image-20230325231206433

select_training_samples

该函数的功能是将RPN网络提供的Proposal进行采样,并计算这些Proposal的标签和regression参数(分配gtbox并计算,跟之前RPN网络内的操作类似)

​ 如下图所示,源码将gt_boxes也拼到了proposal后面,这里可能考虑到了PRN训练初始无法提供有效的proposal,所以加入gt_boxes来训练FastRCNN网络部分.

image-20230325224923241

​ 接下来将调用assign_targets_to_proposals函数将proposals分配给gt_boxes.这个函数在之前的RPN网络提到过,这里不再赘述.

​ 之后调用了subsample进行采样.得到一定比例的正负样本.

image-20230325225121300

​ 最后一步是遍历每张图片,首先找到正负样本(因为回归参数正负样本都参与计算)对应proposal的类别和proposal分配到的gt_box,再计算gt_box和proposal之间的回归参数(通过box_coder的encode方法,之前在RPN网络中有提到).

注意这里负样本对应的gt_box是第0个gt_box,按道理来说负样本不参与边界回归参数损失的计算.但是为了防止matched_idxs下标越界,所以在计算match_idxs时将-1都置为了0,导致现在"负样本有对应的gt_box,且计算了回归参数",不过这个问题不大,因为label记录了负样本的位置,在计算损失时忽略这部分即可~

image-20230325225957610

subsample

​ 该函数其实只是调用了fg_bg_sampler这个类对象,得到了每张图片里的正负样本索引,随后将每张图片的正负样本索引丢到sampled_inds列表里.

image-20230325225519409

fastrcnn_loss

​ 刚开始将label和regression cat起来是把不同图片的labels和回归参数都摞起来,一起处理.

​ 正负样本都会计算类别损失.

​ 而回归参数损失只计算正样本的,所以这里需要用sampled_pos_inds_subset记录正样本的位置.同时还需要对box_regression进行reshape处理,因为regression参数针对每个类别都会有四个参数. 最后使用smoothL1Loss进行正样本的回归参数损失计算.

image-20230325231409283

postprocess_detections

 在预测模式下,将通过此函数得到最后的预测结果。具体流程见下图;具体操作见源码(带注释)

image-20230326095609394

整体思路

​ 创建DataSet首先需要继承torch.utils.data.Dataset这个类,然后再init函数中完成数据的一些预处理,比如xml文件的解析/类与序号的映射/图片路径的存储等。

​ 接下来需要重载__len____getitem__两个方法,分别返回数据长度和某个序号对应的图片(包括图片本身和标注)

如果用到多GPU训练,按照Pytorch官方的建议,最好再实现get_height_and_wight这个方法,节约内存.(因为这样可以避免pytorch将所有图片读入计算宽高)

源码细节

1. xml解析

​ 在init方法中调用了parse_xml_to_dict方法解析xml文件,获取其中的object信息.(物体的类别/位置/边界框)

image-20230321230712153

​ 而parse_xml_to_dict具体使用递归的方法遍历标签信息,返回字典类型的数据

image-20230321230939717

2.__getitem__方法

​ 首先通过上述的给出的xml解析方法解析图片对应的xml文件,将结果存入data变量.图片也通过Image.open打开

image-20230321231804715

​ 接下来将data中的边界框和类别数据进行读取,丢到boxes和labels列表中.

image-20230321231948677

之后注意将这些数据转换成Tensor类型

​ 最后将信息都整理到target中,作为整体的标签返回.

image-20230321232106426

最后还需要判断是否对图片进行data augmentation

3.Transform

​ transform有很多类型,这里简单介绍一下水平翻转的实现.需要注意的是图片翻转之后,边界框的标注位置也需要翻转.

​ 对于水平翻转: y坐标不需要改变,xmax变为width-xmin,xmin变为width-xmax

image-20230321232427257

4.collate_fn

​ 为了之后实现dataloaer,这里需要实现collate_fn函数.

​ 不同于分类网络中dataset只返回一张图片和一个label(形式比较固定),目标识别网络中需要返回图片加标注,而标注是不等长的,使用默认的stack有可能出现问题.所以需要手动用collate_fn方法进行堆叠.

image-20230321232235799

下图是dataloader的实现,这里传入了collate_fn.不传入这个参数默认使用torch.stack()对__getitem__的每个返回值进行堆叠

image-20230321234927729

基本概念

t-SNE(t-distributed stochastic neighbor embedding)是用于降维的一种机器学习算法,由 Laurens van der Maaten 等在08年提出。

t-SNE 是一种非线性降维算法,非常适用于高维数据降维到2维或者3维,进行可视化。该算法可以将对于较大相似度的点,t分布在低维空间中的距离需要稍小一点;而对于低相似度的点,t分布在低维空间中的距离需要更远。

tips1:TSNE将数据点之间的相似性转换为联合概率,并试图最小化低维嵌入和高维数据的联合概率之间的Kullback-Leibler差异。t-SNE的成本函数不是凸的,即使用不同的初始化,我们可以获得不同的结果。

tips2:如果特征数量非常多,强烈建议使用另一种降维方法(例如,对于密集数据使用PCA或对于稀疏数据使用TruncatedSVD)将尺寸数量减少到合理的数量(例如50个)。这将抑制一些噪声并加快样本之间成对距离的计算。

tips3:sklearn TSNE 源码

tips4:t-SNE 原理及Python实例 - 知乎 (zhihu.com)

优缺点

优点:

  • 对于不相似的点,用一个较小的距离产生较大的梯度来排斥区分。
  • 这种排斥不会无限大(梯度中分母),避免不相似的点距离太远。

缺点:

  • 主要用于可视化,很难用于其他目的。比如测试集合降维,因为他没有显式的预估部分,不能在测试集合直接降维;比如降维到10维,因为t分布偏重长尾,1个自由度的t分布很难保存好局部特征,可能需要设置成更高的自由度。
  • t-SNE倾向于保存局部特征,对于本征维数(intrinsic dimensionality)本身就很高的数据集,是不可能完整的映射到2-3维的空间(映射存在信息损失)
  • t-SNE没有唯一最优解,且没有预估部分。如果想要做预估,可以考虑降维之后,再构建一个回归方程之类的模型去做。但是要注意,t-sne中距离本身是没有意义,都是概率分布问题
  • 训练太慢。有很多基于树的算法在t-sne上做一些改进

算法流程

  • 首先在高维空间,将样本之间的距离转换成概率分布.

$$ P_{j|i}=\frac {exp(-||x_i-x_j||^2/2\sigma^2)}{\sum_{k \neq i} exp(-||x_i-x_k||^2/2\sigma^2)} $$

  • 在低维空间寻找类似的概率分布,并使用KL散度衡量高维空间/低维空间两概率分布的相似度.

image-20230327172920621

  • 使用梯度下降最小化所有数据点上的KL散度总和

    $$ C = \sum_i KL(P_i||Q_i)=\sum_i \sum_j p_{j|i} \log {\frac {p_{j|i}}{q_{j|i}} } $$

  • 对 C 求偏导数(每个样本)获得每次更新的方向

sklearn

  • n_components : tsne最终降维的维度
  • init : 初始化方法,可以使用pca or random
from sklearn.manifold import TSNE
tsne=TSNE(n_components=1,init='pca', random_state=501)
down_X = tsne.fit_transform(X) # train and get embadding

基本概念

  • DBSCAN:Density-Based Spatial Clustering of Applications with Noise (具有噪声的基于密度的聚类方法)
  • 核心对象:若某个点内的密度达到算法设定的阈值则为核心点
  • ∈-邻域的距离阈值:设定的半径r
  • 直接密度可达:若点p在点q的r邻域内,且q是核心点则p-q直接密度可达
  • 密度可达:若有一个点的序列$q_0,q_1,...,q_k$,对任意$q_i,q_{i-1}$直接密度可达,那么称$q_0,q_k$密度可达。即直接密度可达的‘传播’。
  • 密度相连:若从核心点p出发,点q和点k都是密度可达的。那么点q和点k是密度相连的。(不要求q/k必须是核心点)
  • 边界点:属于某个类的非核心点
  • 噪声点:不属于任何一个类簇的点,从任何一个核心点出发都是密度不可达的。

image-20230327144445160

参数选择

  • 半径∈:可以根据K距离设定,即寻找突变点。
  • K距离:给定数据集P = {p(i);i=0,1,...,n},计算点p(i)到集合D的子集S中所有点之间的距离d,按照距离从小到大排序,d(k)即K距离

    例如点s到其他点的距离数组$d_k$={0.1,0.11,0.12,0.3,0.5....}(已排序),发现前三个距离相差不大,第四个距离0.3发生突变,那么半径取d(3)=0.12,MinPts取3
  • MinPts:K-距离中的K值,一般从小开始取

优缺点

优点:

  • 可以处理任意形状的簇
  • 不需要指定簇的个数
  • 擅长找到离群点,用于异常检测任务

缺点:

  • 高维数据处理困难(可以寻找降维方法)
  • 半径r和MinPts难以确定
  • 算法效率慢

算法流程

伪代码如下,类似宽度优先搜索进行簇的传播:

bool visited[n];
memset(visited,false,sizeof visited);
while(some point is unvisited){
    choose p randomly which is unvisited;
    if(p ∈-field has more that MinPts points){
        create a new cluster C;
        N = {q in p ∈-field};
        for(auto &q:N){
            if(visited[q]==false){
                visited[q] = true;
                if(q ∈-field has more that MinPts points){
                    add {k in q ∈-field} to N;
                }
                add q to cluster C;
            }
        }
    }else{
        add p to noise;
    }
}

可视化

Visualizing DBSCAN Clustering (naftaliharris.com)

image-20230327150826761

sklearn

from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.5,min_sample=5)
dbscan.fit(X)    # train
dbscan.label_   # label label=-1 means noise
descan.core_sample_indices_ # 核心样本在原始训练集中的位置