65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 CNTK 和 C# 通过 MinibatchSource 训练 Iris 数据

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017 年 11 月 13 日

CPOL

4分钟阅读

viewsIcon

11696

通过这篇博文,我们将实现完整的 C# 程序来训练 Iris 数据。

到目前为止(文章 1文章 2文章 3),我们已经了解了什么是 CNTK,如何将其与 Python 一起使用,以及如何创建一个简单的 C# .NET 应用程序并调用基本的 CNTK 方法。在这篇博文中,我们将实现一个完整的 C# 程序来训练 Iris 数据。

使用 CNTK 的第一步是如何获取数据并将其输入到训练器。在之前的文章中,我们准备了 CNTK 格式的 Iris 数据,该格式适用于使用 MinibatchSource 的情况。为了使用 MinibatchSource,我们需要创建两个流

  • 一个用于特征,以及
  • 一个用于标签

此外,特征和标签变量也必须使用流创建,以便在使用变量访问数据时,训练器知道数据来自文件。

数据准备

如上所述,我们将使用 CNTK MinibatchSource 加载 Iris 数据。为本次演示准备了两个文件

var dataFolder = "Data";
var dataPath = Path.Combine(dataFolder, "trainIris_cntk.txt");
var trainPath = Path.Combine(dataFolder, "testIris_cntk.txt");

var featureStreamName = "features";
var labelsStreamName = "label";

一个文件路径包含用于训练的 Iris 数据,第二个路径包含用于测试的数据,这将在以后的文章中使用。创建用于训练和验证的 minibatchSource 时,这两个文件将作为参数。

从文件中获取数据的第一步是定义具有正确信息的流配置。当从文件中提取数据时,将使用该信息。通过提供文件中特征的数量和标签的 one-hot 向量分量的数量,以及特征和标签的名称,完成配置。在本博文的末尾,数据被附加,以便读者可以看到如何为 minibatchSource 准备数据。

以下代码定义了 Iris 数据集的流配置。

//stream configuration to distinct features and labels in the file
var streamConfig = new StreamConfiguration[]
  {
    new StreamConfiguration(featureStreamName, inputDim),
    new StreamConfiguration(labelsStreamName, numOutputClasses)
  };

此外,必须通过提供上述流名称来创建特征和标签变量。

//define input and output variable and connecting to the stream configuration
var feature = Variable.InputVariable(new  NDShape(1,inputDim), DataType.Float, featureStreamName);
var label = Variable.InputVariable(new NDShape(1, numOutputClasses), DataType.Float, labelsStreamName);

现在,输入和输出变量与文件中的数据连接,并且 minibachSource 可以处理它们。

创建前馈神经网络模型

一旦我们定义了流和变量,我们就可以定义网络模型。CNTK 的实现方式使您可以定义任意数量的隐藏层以及任何激活函数。对于本次演示,我们将创建一个具有一个隐藏层的简单前馈神经网络。下图显示了 NN 模型。

为了实现上述 NN 模型,我们需要实现三个方法

  • static Function applyActivationFunction(Function layer, NNActivation actFun)
  • static Function simpleLayer(Function input, int outputDim, DeviceDescriptor device)
  • static Function createFFNN(Function input, int hiddenLayerCount, 
    	int hiddenDim, int outputDim, NNActivation activation, string modelName, 
        DeviceDescriptor device)

第一个方法仅将指定的激活函数应用于传递的层。该方法非常简单,应如下所示

static Function applyActivationFunction(Function layer, Activation actFun)
{
    switch (actFun)
    {
        default:
        case Activation.None:
            return layer;
        case Activation.ReLU:
            return CNTKLib.ReLU(layer);
        case Activation.Sigmoid:
            return CNTKLib.Sigmoid(layer);
        case Activation.Tanh:
            return CNTKLib.Tanh(layer);
    }
}
public enum Activation
{
    None,
    ReLU,
    Sigmoid,
    Tanh
}

该方法将层作为参数,并返回应用了激活函数的层。

下一个方法是创建具有 n 个权重和一个偏差的简单层。该方法显示在以下列表中

static Function simpleLayer(Function input, int outputDim, DeviceDescriptor device)
{
    //prepare default parameters values
    var glorotInit = CNTKLib.GlorotUniformInitializer(
            CNTKLib.DefaultParamInitScale,
            CNTKLib.SentinelValueForInferParamInitRank,
            CNTKLib.SentinelValueForInferParamInitRank, 1);

    //create weight and bias vectors
    var var = (Variable)input;
    var shape = new int[] { outputDim, var.Shape[0] };
    var weightParam = new Parameter(shape, DataType.Float, glorotInit, device, "w");
    var biasParam = new Parameter(new NDShape(1,outputDim), 0, device, "b");

    //construct W * X + b matrix
    return CNTKLib.Times(weightParam, input) + biasParam;
}

初始化参数后,使用输出组件的数量以及前一层或输入变量创建 Function 对象。这在 NN 层创建中称为链式规则。通过这种策略,用户可以创建非常复杂的 NN 模型。

最后一个方法执行图层的创建。它从 main 方法调用,并且可以通过提供参数来创建任意前馈神经网络。

static Function createFFNN(Variable input, int hiddenLayerCount, 
int hiddenDim, int outputDim, Activation activation, string modelName, DeviceDescriptor device)
{
    //First the parameters initialization must be performed
    var glorotInit = CNTKLib.GlorotUniformInitializer(
            CNTKLib.DefaultParamInitScale,
            CNTKLib.SentinelValueForInferParamInitRank,
            CNTKLib.SentinelValueForInferParamInitRank, 1);

    //hidden layers creation
    //first hidden layer
    Function h = simpleLayer(input, hiddenDim, device);
    h = ApplyActivationFunction(h, activation);
    //2,3, ... hidden layers
    for (int i = 1; i < hiddenLayerCount; i++)
    {
        h = simpleLayer(h, hiddenDim, device);
        h = ApplyActivationFunction(h, activation);
    }
    //the last action is creation of the output layer
    var r  = simpleLayer(h, outputDim, device);
    r.SetName(modelName);
    return r;
}

现在我们已经实现了用于 NN 模型创建的方法,下一步将是训练实现。

训练过程是迭代的,其中 minibachSource 在每次迭代中为训练器提供数据。
在每次迭代中计算 Lossevaluation 函数,并在迭代进度中显示。迭代进度由一个单独的方法定义,该方法如下面的代码清单所示

private static void printTrainingProgress
(Trainer trainer, int minibatchIdx, int outputFrequencyInMinibatches)
{
    if ((minibatchIdx % outputFrequencyInMinibatches) == 0 && 
                      trainer.PreviousMinibatchSampleCount() != 0)
    {
        float trainLossValue = (float)trainer.PreviousMinibatchLossAverage();
        float evaluationValue = (float)trainer.PreviousMinibatchEvaluationAverage();
        Console.WriteLine($"Minibatch: {minibatchIdx} CrossEntropyLoss = 
                           {trainLossValue}, EvaluationCriterion = {evaluationValue}");
    }
}

在迭代过程中,Loss 函数不断降低其值,表明该模型变得越来越好。迭代过程完成后,将在训练数据的准确性上下文中显示模型。

完整程序实现

以下清单显示了使用 CNTK 进行 Iris 数据集训练的完整源代码实现。首先,定义几个变量,以便定义 NN 模型的结构:输入和输出变量的数量。此外,main 方法实现了迭代过程,其中 minibatchSource 通过将相关数据传递给训练器来处理数据。有关更多信息将在单独的博文中介绍。迭代过程完成后,将显示模型结果,程序终止。

public static void TrainIris(DeviceDescriptor device)
{
    var dataFolder = "";//files must be on the same folder as program
    var dataPath = Path.Combine(dataFolder, "iris_with_hot_vector.csv");
    var trainPath = Path.Combine(dataFolder, "iris_with_hot_vector_test.csv");

    var featureStreamName = "features";
    var labelsStreamName = "labels";

    //Network definition
    int inputDim = 4;
    int numOutputClasses = 3;
    int numHiddenLayers = 1;
    int hidenLayerDim = 6;
    uint sampleSize = 130;

    //stream configuration to distinct features and labels in the file
    var streamConfig = new StreamConfiguration[]
        {
            new StreamConfiguration(featureStreamName, inputDim),
            new StreamConfiguration(labelsStreamName, numOutputClasses)
        };

    // build a NN model
    //define input and output variable and connecting to the stream configuration
    var feature = Variable.InputVariable(new NDShape(1, inputDim), DataType.Float, featureStreamName);
    var label = Variable.InputVariable
                (new NDShape(1, numOutputClasses), DataType.Float, labelsStreamName);


    //Build simple Feed Froward Neural Network model
    // var ffnn_model = CreateMLPClassifier(device, numOutputClasses, 
    //                  hidenLayerDim, feature, classifierName);
    var ffnn_model = CreateFFNN(feature, numHiddenLayers, 
                     hidenLayerDim, numOutputClasses, Activation.Tanh, "IrisNNModel", device);

    //Loss and error functions definition
    var trainingLoss = CNTKLib.CrossEntropyWithSoftmax(new Variable(ffnn_model), label, "lossFunction");
    var classError = CNTKLib.ClassificationError(new Variable(ffnn_model), label, "classificationError");

    // prepare the training data
    var minibatchSource = MinibatchSource.TextFormatMinibatchSource(
        dataPath, streamConfig, MinibatchSource.InfinitelyRepeat, true);
    var featureStreamInfo = minibatchSource.StreamInfo(featureStreamName);
    var labelStreamInfo = minibatchSource.StreamInfo(labelsStreamName);

    // set learning rate for the network
    var learningRatePerSample = new TrainingParameterScheduleDouble(0.001125, 1);

    //define learners for the NN model
    var ll = Learner.SGDLearner(ffnn_model.Parameters(), learningRatePerSample);

    //define trainer based on ffnn_model, loss and error functions , and SGD learner 
    var trainer = Trainer.CreateTrainer(ffnn_model, trainingLoss, classError, new Learner[] { ll });

    //Preparation for the iterative learning process
    //used 800 epochs/iterations. Batch size will be the same as sample size since the data set is small
    int epochs = 800;
    int i = 0;
    while (epochs > -1)
    {
        var minibatchData = minibatchSource.GetNextMinibatch(sampleSize, device);
        //pass to the trainer the current batch separated by the features and label.
        var arguments = new Dictionary<Variable, MinibatchData>
        {
            { feature, minibatchData[featureStreamInfo] },
            { label, minibatchData[labelStreamInfo] }
        };

        trainer.TrainMinibatch(arguments, device);

        Helper.PrintTrainingProgress(trainer, i++, 50);

        // MinibatchSource is created with MinibatchSource.InfinitelyRepeat.
        // Batching will not end. Each time minibatchSource completes an sweep (epoch),
        // the last minibatch data will be marked as end of a sweep. We use this flag
        // to count number of epochs.
        if (Helper.MiniBatchDataIsSweepEnd(minibatchData.Values))
        {
            epochs--;
        }
    }
    //Summary of training
    double acc = Math.Round((1.0 - trainer.PreviousMinibatchEvaluationAverage()) * 100, 2);
    Console.WriteLine($"------TRAINING SUMMARY--------");
    Console.WriteLine($"The model trained with the accuracy {acc}%");

    //// validate the model
    // this will be posted as separate blog post
}

可以在此处找到带有格式化的 Iris 数据集用于训练的完整源代码。

© . All rights reserved.