在 MATLAB 中使用嵌套 SVM 调整参数

Tune parameters with nested SVM in MATLAB

我有一个包含 20 个测试对象的数据集,其中包含 50 个变量和一个 10 的结果向量,用于确定它们的状态。我想设置一个嵌套的交叉验证,以便我在内部折叠中执行特征选择以及调整 SVM 的超参数。那么这些参数应该在外折上进行测试。

我以前在使用逻辑回归(使用 sequentialfs)的特征选择方面做过这个,但我不知道如何同时进行特征选择和超参数调整。

一些示例代码是理想的,但是对给定输出的设置和解释的一般解释也会有所帮助,因为我是 SVM 的新手。

特征选择必须向前。如果可能的话,我希望输出是具有参数和所选特征的最佳整体 SVM。当然我也想知道测试集上的错误率。使用的内核是二阶多项式,所以我想只有一个参数需要调整?

此外,我希望它在内部和外部交叉验证上都是 5 倍。

编辑:我找到了一些示例代码,它们应该可以满足我的要求,但我似乎无法让它工作。谁能解释一下如何在 MATLAB 中设置它?

https://stats.stackexchange.com/questions/40906/nested-cross-validation-for-classification-in-matlab

我提出以下答案,其中 allData 包含所有数据。每一行都是一个条目,每一列都是一个特征。名为 targets 的变量将包含 objective 类。有了这个假设,我将逐部分解释下面附加的代码。

变量 featSize 将具有特征数。

featSize = size(allData, 2);

变量kFolds 将包含您想要的折叠数。你说5,但我建议你至少10,如果你有足够的数据。

kFolds = 5;

结构 bestSVM 将是您需要的 "output"。你可以在最后清除所有变量,除了这个。该结构将包含最佳 SVM 及其参数以及将产生最佳性能的特征索引。

bestSVM = struct('SVMModel', NaN, 'C', NaN, 'FeaturesIdx', NaN, 'Score', Inf);     

变量kIdx 将包含基于数据和折叠数的交叉验证指数。它使用matlab的函数crossvalind().

kIdx = crossvalind('Kfold', length(targets), kFolds);

现在,主 outer 循环将 运行 您在 kFolds 中指定的次数,并将准备训练集和测试集( trainDatatestData)及其对应的目标(trainTargtestTarg)如下:

for k = 1:kFolds
    trainData = allData(kIdx~=k, :);
    trainTarg = targets(kIdx~=k);
    testData = allData(kIdx==k, :);
    testTarg = targets(kIdx==k);

现在,我们准备进行特征选择。我们将变量 bestFeatScore 初始化为 Inf 以便稍后我们可以将 SVM 的性能(分数)与该值进行比较(进一步向下可能更有意义)。我们还初始化了一个结构 bestFeatCombo,它将包含所有可能的特征组合中最好的 SVM 及其相应的特征索引 feat 和参数 C.

    bestFeatScore = inf;
    bestFeatCombo = struct('SVM', NaN, 'feat', NaN, 'C', NaN);

特征组合的可能数量是2^featSize - 1。例如,如果您总共有两个特征,则有 2^2 - 1 = 3 个选择:1) 仅选择特征 1,2) 仅选择特征 2,3) 仅选择特征 1 和 2。因此,我们需要一个 for 循环来遍历所有可能的特征组合。

    for b = 1:(2^featSize) - 1

但是,有一个棘手的部分(我相信有更好的解决方案),您需要首先开始选择功能集。我认为这是一个二进制表示问题。假设你总共有三个特征 [f1, f2, f3],那么,我们可以说二元向量 [1, 0, 0] 代表特征的选择 f1 并忽略其余部分。将二进制向量传递给 matlab 会给你一个索引错误。所以,我发现解决这个问题的最好方法是使用 matlab 的 find() function that will find the "indices and values of nonzero elements". Thus, if we do featCombo = find([1, 0, 0]), the variable featCombo would be equal to 1. So, what I did was use the variable b (from the for loop above) that will contain a number indicating the current possible combination of features and convert it to a binary vector of size featSize using matlab's function de2bi()。例如,de2bi(1, 3) 给出 [1, 0, 0],如您所见,它使用左边的数字作为最低位。 de2bi(3, 3) 给出 [1, 1, 0]de2bi(5, 3) 给出 [1, 0, 1],依此类推。然后,如果您在 de2bi() 上使用 find() 函数,将生成您要从中选择的功能的索引。

        featCombo = find(de2bi(b, featSize));

例如,如果featSize = 3,这个:

for b = 1:(2^featSize) - 1; display(de2bi(b, featSize)); end;

会给你这样的东西:

 1     0     0
 0     1     0
 1     1     0
 0     0     1
 1     0     1
 0     1     1
 1     1     1

结合查找如下:

for b = 1:(2^featSize) - 1; display(find(de2bi(b, featSize))); end;

会给你这样的东西:

 1
 2
 1     2
 3
 1     3
 2     3
 1     2     3

适合用作logical indexing。因此,featCombo 将包含要选择的特征集(带有索引的向量)。

下一部分将为 BoxConstraint C 初始化 grid search 的变量,这也称为超参数(取决于 SVM 类型,您可能有其他参数)。 bestCScore 将包含网格搜索期间 SVM 的最佳性能,bestC 将包含最佳 C 参数,而 bestCSVM 将包含搜索期间训练最好的 SVM。变量 gridC 将包含搜索 space,在我的示例中从 2^-52^-32^15。如果您有足够的计算资源,我建议您将 2.^(-5:2:15) 更改为更小的增量,例如 2.^(-5:1:15),甚至更小的 2.^(-5:0.1:15),但要小心,因为它需要一段时间才能完成.同样,如果你的计算能力低(或时间有限),将间隔增加到 2.^(-5:3:15) 甚至 2.^(-5:4:15),知道它会选择不好的超参数。

        bestCScore = inf;
        bestC = NaN;
        bestCSVM = NaN;
        gridC = 2.^(-5:2:15);

接下来,我们将开始基于数组gridC的网格搜索,我们将使用matlab的函数fitcsvm()训练一个SVM。 SVM 使用由 当前折叠 确定的当前 trainData 以及由 featCombo 确定的特定选择特征进行训练。训练目标 trainTarg 也由当前折叠决定。请注意 a) 我正在使用 RBF 内核类型(因为您没有指定它),并且 b) 我让 matlab auto determine 内核规模。如果你想使用另一个内核,这段代码将需要一些修改。

        for C = gridC
            anSVMModel = fitcsvm(trainData(:, featCombo), trainTarg, ...
                'KernelFunction', 'RBF', 'KernelScale', 'auto', ...
                'BoxConstraint', C);

下一步是确定 SVM 对该参数、该组特征、该折叠的执行情况。我们使用 SVM 的函数 loss() 来做到这一点。

            L = loss(anSVMModel,testData(:, featCombo), testTarg);

如果当前SVM(anSVMModel)的表现优于之前的最佳表现,我们会将分数保存到bestCScore,将最佳参数保存到bestC,将最佳SVM保存到bestC bestCSVM

            if L < bestCScore
                bestCScore = L;
                bestC = C;
                bestCSVM = anSVMModel;
            end
        end

inner-most循环结束时,我们应该拥有当前特征集的最佳超参数和SVM。因此,如果这个 SVM 对于任何其他特征集的得分高于任何其他先前训练的 SVM,我们将把那个 SVM、那组特征和那个超参数保存到 bestFeatCombo.SVM 给出的结构中,bestFeatCombo.feat, bestFeatCombo.C` 分别.

        if (bestCScore < bestFeatScore) || ...
                ((bestCScore == bestFeatScore) && ...
                (length(featCombo) < length(bestFeatCombo.feat)))
            bestFeatScore = bestCScore;
            bestFeatCombo.SVM = bestCSVM;
            bestFeatCombo.feat = featCombo;
            bestFeatCombo.C = bestC;
        end
    end

但是请注意,在上面的 if 语句中,我在 or 子句中做了一个特例。我是说,如果当前 SVM 的表现(分数)与 迄今为止最好的 相同,但当前 SVM 具有较小的功能集但提供相同的性能,我选择替换目前为止最好的 SVM 使用较少的特征。

现在,在 中间 循环结束时,我们应该拥有针对任何特征集的最佳 SVM,具有最佳超参数 C(到网格搜索允许的范围)。因此,我们可以将当前最佳(存储在结构bestFeatCombo中)与迄今为止的总体最佳(存储在结构中)进行比较bestSVM).

    if bestFeatScore < bestSVM.Score
        bestSVM.SVMModel = bestFeatCombo.SVM;
        bestSVM.C = bestFeatCombo.C;
        bestSVM.FeaturesIdx = bestFeatCombo.feat;
        bestSVM.Score = bestFeatScore
    end    
end

到此结束。正如我所说,您想要的输出在 bestSVM 中,其中包含 kFolds 中最好的 SVM,用于最好的特征组合,以及最好的超参数 C 作为允许网格搜索。

我希望这是有道理的。下面是一个使用 matlab 的 fisheriris 数据集的工作示例,该数据集具有 100 个样本和 4 个特征。

工作代码:

load fisheriris
inds = ~strcmp(species,'setosa');
allData = meas(inds,:);
targets = species(inds);
featSize = size(allData, 2);
kFolds = 5;     % this is where you specify your number of folds
bestSVM = struct('SVMModel', NaN, ...     % this is to store the best SVM
    'C', NaN, 'FeaturesIdx', NaN, 'Score', Inf);     

kIdx = crossvalind('Kfold', length(targets), kFolds);
for k = 1:kFolds
    trainData = allData(kIdx~=k, :);
    trainTarg = targets(kIdx~=k);
    testData = allData(kIdx==k, :);
    testTarg = targets(kIdx==k);

    % forward feature selection starts
    bestFeatScore = inf;
    bestFeatCombo = struct('SVM', NaN, 'feat', NaN, 'C', NaN);
    for b = 1:(2^featSize) - 1
        % this is to choose the features. e.g. [1 0 0] selects the first
        % feature out of three features.
        featCombo = find(de2bi(b, featSize));

        % this is the grid search for the BoxConstraint
        bestCScore = inf;
        bestC = NaN;
        bestCSVM = NaN;
        gridC = 2.^(-5:2:15);
        for C = gridC
            anSVMModel = fitcsvm(trainData(:, featCombo), trainTarg, ...
                'KernelFunction', 'RBF', 'KernelScale', 'auto', ...
                'BoxConstraint', C);
            L = loss(anSVMModel,testData(:, featCombo), testTarg);
            if L < bestCScore        % saving best SVM on parameter
                bestCScore = L;      % selection
                bestC = C;
                bestCSVM = anSVMModel;
            end
        end

        % saving the best SVM on feature selection
        if (bestCScore < bestFeatScore) || ...
                ((bestCScore == bestFeatScore) && ...
                (length(featCombo) < length(bestFeatCombo.feat)))
            bestFeatScore = bestCScore;
            bestFeatCombo.SVM = bestCSVM;
            bestFeatCombo.feat = featCombo;
            bestFeatCombo.C = bestC;
        end
    end

    % saving the best SVM over all folds
    if bestFeatScore < bestSVM.Score
        bestSVM.SVMModel = bestFeatCombo.SVM;
        bestSVM.C = bestFeatCombo.C;
        bestSVM.FeaturesIdx = bestFeatCombo.feat;
        bestSVM.Score = bestFeatScore
    end
end

编辑

要同时回答您编辑的问题,您希望在其中对参数选择进行另一个 5 折交叉验证,这是您需要做的。

请将 最里面的 循环更改为如下所示:

        % this is the grid search for the BoxConstraint
        bestCScore = inf;
        bestC = NaN;
        gridC = 2.^(-5:2:15);
        for C = gridC
            % cross validation for parameter C
            kIdxC = crossvalind('Kfold', length(trainTarg), kFolds);
            L = zeros(1, kFolds);
            for kC = 1:kFolds
                trainDataC = trainData(kIdxC~=kC, :);
                trainTargC = trainTarg(kIdxC~=kC);
                testDataC = trainData(kIdxC==kC, :);
                testTargC = trainTarg(kIdxC==kC);
                anSVMModel = fitcsvm(trainDataC(:, featCombo), trainTargC, ...
                    'KernelFunction', 'RBF', 'KernelScale', 'auto', ...
                    'BoxConstraint', C);
                L(kC) = loss(anSVMModel,testDataC(:, featCombo), testTargC);
            end
            L = mean(L);
            if L < bestCScore
                bestCScore = L;
                bestC = C;
            end
        end
        % we need to retrain here and save the SVM for the best C
        bestCSVM = fitcsvm(trainData(:, featCombo), trainTarg, ...
            'KernelFunction', 'RBF', 'KernelScale', 'auto', ...
            'BoxConstraint', bestC);
        bestCScore = loss(bestCSVM,testData(:, featCombo), testTarg);

代码使用标准简历。但请注意以下行很重要:

L(kC) = loss(anSVMModel,testDataC(:, featCombo), testTargC);

在这一行中,您将保存所有折叠中的所有表演。但是,请注意,您必须对其进行平均,这就是 CV 的目的。

L = mean(L);

你还需要再训练一次。