使用 MS CNTK 进行贝塞尔曲线机器学习
使用 CNTK 和 ALGLIB 进行贝塞尔曲线分类训练和验证模型
访问第 1 部分:数据可视化和贝塞尔曲线
访问第 2 部分:贝塞尔曲线机器学习演示
引言
本文是本系列的第 3 部分。第 1 部分演示了如何使用贝塞尔曲线平滑大量数据点波动并提高模式展开的可视性。第 2 部分重点介绍了使用 ALGLIB 机器学习算法训练网络模型,以识别贝塞尔曲线轨迹中反映的学生学业表现模式和趋势。在这里,我们将使用相同的 WinForm 用户界面并扩展演示代码,以检查几种常见的 MS CNTK 数据格式和使用批处理和 mini-batch 方法的 CNTK 分类和评估模型。
演示代码和 UI 反映了用于二元或多标签分类任务的模型构建、训练、验证和测试的众所周知的方法。我们对数据集的随机选择部分进行训练,并对剩余部分进行验证/测试训练模型。
在模型方面,有很多选择,比较结果总是好的。这就是为什么我决定将 ALGLIB 神经网络和决策森林模块以及新的 CTNK 模块保留在演示中。这意味着要运行代码,您需要拥有并包含 *ALGLIB314.dll* 库以及 CNTK for C# 2.5.1 NuGet 包作为项目引用。本系列的第 2 部分解释了如何获取 ALGLIB 免费版源并详细介绍了在 Visual Studio 中构建该 DLL 库的步骤。
背景
此演示以 Microsoft 的 C# 版 CTNK 为特色,它是一款功能强大、动态的 C# 程序员工具,提供许多高级机器学习方法。但是,CNTK 给用户带来了相当大的学习曲线,文档也还没有达到应有的水平。最近,许多特定问题开始在 StackOverFlow 等来源上找到答案,并且 GitHub 上提供了更详细的代码示例。特别是,一位 CodeProject 贡献者 Bahrudin Jrnjica 发布了一系列非常棒的 CNTK 文章,我发现它们非常有用,从这里的 IRIS 数据集示例开始,并在他的技术博客文章 http://bhrnjica.net 中进一步记录。其他参考资料包括 Microsoft 源文档,例如使用 CNTK 和 C#/.NET API。
数据特征
本数据可视化系列的前几篇文章,题为数据可视化和贝塞尔曲线和贝塞尔曲线机器学习演示,是关于使用贝塞尔曲线对数据进行建模的。作为示例,我们查看了学生从 6 年级到 12 年级的学业表现随时间的变化,并使用贝塞尔曲线进行了建模。我们讨论了可以训练以识别各种模式的机器学习分类模型,例如识别表现良好或可能处于危险中的学生的纵向轨迹。
在这里,我们将使用与第 2 部分中使用的相同的核心课程中学期平均成绩 (MPgpa
) 学生表现历史的小型模拟样本 (N=500
)。MPgpa
在评分量表上的范围是 0.00 到 4.00。(请回顾之前的文章以获取数据源的更完整描述。)
我们的小型演示数据集是通过随机选择预分类的学生历史记录构建的,同时确保了代表每个状态组的平衡演示数据。每条曲线都被标记为
- 表示在课程时间线上达到状态评估点(9.0,初中结束)之前相对成功的学业历史
- 可能处于危险之中,因为 MPgpa 模式低于评分量表上的 2.0
- 仍处于危险之中,但 MPgpa 模式呈上升趋势,接近或略高于 2.0
- 严重面临学业失败的风险,模式趋势低于 1.0
使用 CNTK
MS CNTK for C# 提供用于在 CPU 或 GPU 上运行的多线程代码。在这里,我们使用 CPU 作为 CPUOnly 版本的默认设备。MS CNTK for C# 需要 x64 编译。在构建演示项目之前,您应该检查**配置管理器**以确保解决方案中所有项目的 *Debug* 和 *Release* 版本都设置为 x64。(“Any CPU”将不起作用。)
CNTK 进程需要各种格式的数据,并提供根据需要重塑数据的工具。一种特定格式是 |features ... |label ...(热向量样式),用于小批量处理。对于我们的数据,它看起来像这样
|features 2.383 2.624888 2.740712 2.794445 2.814961 2.816335
2.807138 2.793529 2.779638 2.767359 2.756607 2.74636 2.736153
2.727306 2.723097 2.727629 2.744186 2.77428 2.817792 2.873763
2.941018 3.018575 3.105723 3.202 |label 1 0 0 0
|features 1.134 1.090346 1.049686 1.012011 0.9773244 0.9455433
0.9165261 0.8900167 0.8656217 0.8427486 0.8205234 0.7977431 0.7727986
0.7436811 0.7081886 0.6644097 0.611671 0.5517164 0.4896953 0.4342301
0.3965976 0.3895209 0.4262405 0.52 |label 0 0 1 0
…
另一个重要的 CNTK 格式只是一个 1D 数据向量;一个用于特征,一个用于标签;一对用于训练数据集,另一对用于验证/测试数据集。在这个新的演示中,`DataReader` 对象已被修改,以提供这些各种格式的内存中 `dataset`。
Using the Code
在这个用 C# 和 Visual Studio 2017 和 .NET 4.7 编写的“`BezierCurveMachineLearningDemoCNTK`”项目中,我们首先读取一个数据文件,构建学生历史,并随机打乱记录。从每个历史中提取一个子历史。每个子历史都关联一个预分类状态值。接下来,拟合贝塞尔曲线以模拟每个学生子历史。该平滑贝塞尔曲线本身通过在从起点到终点(包括)的相等时间间隔内提取 24 个 MPgpa 值列表来建模。这些成为用于训练和验证各种机器学习分类模型的标签和特征。
下载项目并在 Visual Studio 中打开解决方案文件。在解决方案资源管理器中,右键单击项目引用文件夹并删除对 *ALGLIB314.dll* 库的任何现有引用。然后使用**添加引用**选项找到并添加本系列第 2 部分中创建的 *ALGLIB214.dll* 库的发布版本到项目中。MS CNTK for C# 需要 x64 编译。如上所述,您应该检查**配置管理器**以确保解决方案中所有项目的 *Debug* 和 *Release* 版本都设置为 x64。
然后点击**开始**构建并运行应用程序。该解决方案需要几个包(CNTK for C# 2.5.1 和 `System.ValueTuple` 4.3.1),这些包应该会自动下载并恢复。一个 WinForm 应该会打开,包含一个 `listbox`、一个 `chart`、一个 `datagridview` 和几个按钮。其他详细信息在本系列的第 2 部分中进行了解释。对于此演示,提供了额外的菜单选项以选择使用批处理或小批量数据处理的 CNTK 模型训练。
关注点
这里的理念是,CodeProject“演示”所能获得的大部分好处应该来自于对代码的检查。读者当然应该这样做(任何改进建议都将不胜感激)。大部分代码(如果不是大部分)都是构建表单和演示用户界面的普通代码。然而,最重要的方法是用于创建和操作`dataset`(如上所述)以及调用 CNTK 实现的实际机器学习过程的方法。
**训练模型**:本演示说明了使用 !features …!label … 格式(数据保存为磁盘数据文件)的 CNTK 小批量训练,以及使用 1D 格式(数据存储在内存中)的批量训练。例如,这是演示中用于批量处理的 `CNTKTraining` 神经网络代码块,它定义并实现了一个 (24:12:6:4) 神经网络,该网络已被证明足以对表示学业表现的贝塞尔曲线进行分类。(此类别中的大部分代码已改编自 Bahrudin Jrnjica 在上述参考文献中提供的示例源代码。)
class CNTKTraining
{
public static CNTK.Function CNTK_ffnn_model;
private static CNTK.Trainer _trainer;
private static double _accuracy;
public static void RunExample3() // CNTK Neural Network demo using Batch mode
{
// Network definition
int inputDim = Form1.rdr.NofFeatures;
int numOutputClasses = Form1.rdr.NofLabels;
int[] hiddenLayerDim = new int[2] { 12, 6 }; // The Bezier Curve NN model used in
// this demo
int numHiddenLayers = hiddenLayerDim.Count();
uint batchSize = (uint)Form1.rdr.NofTrainingCases; // use all data for each batch
ModelInfoSummary_B(hiddenLayerDim); // sends output to the UI
// Define input and output variables (features and label)
var xValues = Value.CreateBatch<float>(new NDShape(1, inputDim),
Form1.rdr.GetCNTK_Train_1DFeatures, Form1.device);
var yValues = Value.CreateBatch<float>(new NDShape(1, numOutputClasses),
Form1.rdr.GetCNTK_Train_1DLabels, Form1.device);
var features = Variable.InputVariable(new NDShape(1, inputDim), DataType.Float);
var labels = Variable.InputVariable(new NDShape(1, numOutputClasses), DataType.Float);
// Combine variables and data into a Dictionary for the training
Dictionary<Variable, Value> dic = new Dictionary<Variable, Value>
{
{ features, xValues },
{ labels, yValues }
};
//Build simple Feed Forward Neural Network model
CNTK_ffnn_model = CreateFFNN(features, numHiddenLayers, hiddenLayerDim,
numOutputClasses, ACTIVATION.Tanh, "DemoData", Form1.device);
// Loss and error functions definition
var trainingLoss = CNTKLib.CrossEntropyWithSoftmax(new Variable(CNTK_ffnn_model),
labels, "lossFunction");
var classError = CNTKLib.ClassificationError( new Variable(CNTK_ffnn_model),
labels, "classificationError");
// LEARNING RATE
PairSizeTDouble p1 = new PairSizeTDouble( 1, 0.05);
PairSizeTDouble p2 = new PairSizeTDouble( 2, 0.03);
PairSizeTDouble p3 = new PairSizeTDouble( 2, 0.02);
var vlr = new VectorPairSizeTDouble() { p1, p2, p3};
var learningRatePerEpoch =
new CNTK.TrainingParameterScheduleDouble(vlr, batchSize); // variable
// learning
// schedule
//var learningRatePerEpoch = new TrainingParameterScheduleDouble(0.02); // fixed
// learning
// schedule
//MOMENTUM
PairSizeTDouble m1 = new PairSizeTDouble(1, 0.005);
PairSizeTDouble m2 = new PairSizeTDouble(2, 0.001);
PairSizeTDouble m3 = new PairSizeTDouble(2, 0.0005);
var vm = new VectorPairSizeTDouble() { m1, m2, m3 };
var momentumSchedulePerSample =
new CNTK.TrainingParameterScheduleDouble
(vm, (uint)Form1.rdr.NofTrainingCases); // variable
// schedule
//var momentumSchedulePerSample =
CNTKLib.MomentumAsTimeConstantSchedule(256); // time constant
// schedule
//var momentumSchedulePerSample =
new TrainingParameterScheduleDouble(.001); // fixed schedule
//L1 and L2 REGULARIZATION
//var addParam = new AdditionalLearningOptions()
//{
// l1RegularizationWeight = 0.01,
// l2RegularizationWeight = 0.05
//};
var vp = new ParameterVector();
foreach (Parameter p in CNTK_ffnn_model.Parameters()) vp.Add(p);
// DEFINE LEARNERS FOR THE NN MODEL
//var myLearner = Learner.SGDLearner(CNTK_ffnn_model.Parameters(), learningRatePerEpoch);
var myLearner = Learner.MomentumSGDLearner(CNTK_ffnn_model.Parameters(),
learningRatePerEpoch, momentumSchedulePerSample, true);
//var myLearner = CNTK.CNTKLib.AdamLearner(vp,
// learningRatePerEpoch, momentumSchedulePerSample, true);
// DEFINE A TRAINER
_trainer = Trainer.CreateTrainer(CNTK_ffnn_model, trainingLoss,
classError, new Learner[] { myLearner });
// IF WE WANTED TO CONTINUE TRAINING A PARTICULAR MODEL
//_trainer.RestoreFromCheckpoint(ApplicationPath + "CNTK_Trained_Network(CKP)");
//Preparation for the iterative learning process
int epoch = 1;
int epochmax = 4000;
int reportcycle = 200;
double prevCE = 0.0;
double currCE = 0.0;
double losscritierion = 0.00001; // About as good as we are going to get
Dictionary<int, double> traindic = new Dictionary<int, double>();
while (epoch <= epochmax)
{
_trainer.TrainMinibatch(dic, true, Form1.device); // whole batch is used each epoch
// print progress
currCE = _trainer.PreviousMinibatchLossAverage();
if (Math.Abs(prevCE - currCE) < losscritierion) break; // stop interations
prevCE = currCE;
_accuracy = 1.0 - _trainer.PreviousMinibatchEvaluationAverage();
traindic.Add(epoch, _accuracy);
PrintTrainingProgress(_trainer, epoch, reportcycle); // UI ouput info
Charts.ChartAddaPoint(epoch, _accuracy);
epoch++;
}
TrainingSummary_B(); // UI output info
…
}
… // a bunch of private support methods for this RunExample
}
如您所见,首先是进一步的数据处理和重塑。大多数情况下,这还会涉及一个规范化步骤。然而,在我们的例子中,贝塞尔曲线特征数据已经在 0.0 到 4.0 的 MPgpa 值范围内,并且模型似乎在该范围内的数据下工作良好。然后定义一个网络,在本例中由 24 个节点输入层组成,用于描述贝塞尔曲线的 24 个点,两个隐藏层——第一个有 12 个节点,第二个有 6 个节点,以及一个输出层,根据三元或二元分类有 4 个或 2 个节点。接下来,可以设置各种参数(例如,学习率和/或动量调度)。这些用于定义一个学习器(例如,内置的 `SGDLearner` 或 `MomentumSGDLearner`,或者,作为另一个例子,`CNTK.CNTKLib` 中提供的 `AdamLearner`)。给定学习器,创建一个训练器。每个 epoch,批量 `dataset` 被呈现给训练器,并生成“逐步”中间输出。这在这里用于更新 `listbox` 信息表和训练进度图可视化。
模型训练完成后,将其保存为磁盘文件并重新加载到不同的程序中以对新数据记录进行分类是简单的。CNTK 为此目的提供了函数,例如
public static CNTK.Function CNTK_ffnn_model;
public static CNTK.Trainer _trainer;
…
/// <summary>
/// Write the trained CNTK neural network and checkpoint as files
/// </summary>
/// <param name="pathname"></param>
public static void SaveTrainedCNTKModel(string pathname)
{
CNTK_ffnn_model.Save(pathname); // saves trained model for production
// or evaluation
_trainer.SaveCheckpoint(pathname + "(CKP)"); // in case we want to continue
// training the model
}
和
/// <summary>
/// Load a saved trained CNTK neural network
/// </summary>
/// <param name="pathname"></param>
public static void LoadTrainedCNTKModel(string networkPathName)
{
if (!File.Exists(networkPathName))
throw new FileNotFoundException("CNTK_NN Classifier file not found.");
CNTK_ffnn_model = Function.Load(networkPathName, Form1.device, ModelFormat.CNTKv2);
}
**注意**:在此演示中,一个有趣的要点是所有数据文件都被复制或保存到项目的运行时“*应用程序文件夹*”(例如,*x64 debug* 或 *release* 文件夹)。
**验证与测试**:训练完成后,您需要将训练好的模型应用于验证测试数据。CNTK 模型包含可以提取的定义信息。通常,您会看到类似于此 `EvaluateModel_Batch` 例程的代码,它利用内存中的数据、1D 格式样式和 CNTK 数据重塑。该演示还包括一个更复杂的 CNTK `Evaluate_MiniBatch` 方法以及一个用于生产运行的相对简单的 `MLModel IClassifier` 对象。(需要注意的是,经过训练的 CBTK 模型不是线程安全的。但是,`IClassifier` 对象是线程安全的,并将在本系列的下一篇文章中详细描述。)
class CNTKTesting
{
private static double _accuracy;
/// <summary>/// Evaluates using a trained model and test data
/// </summary>
/// <param name="ffnn_model"></param>
/// <param name="device"></param>
/// <param name="rdr"></param>
public static void EvaluateModel_Batch(Function ffnn_model, bool validationFlag = false)
{
// extract features and labels from the model
var features = (Variable)ffnn_model.Arguments[0];
var labels = ffnn_model.Outputs[0];
// get dimensions
int inputDim = features.Shape.TotalSize;
int numOutputClasses = labels.Shape.TotalSize;
// define input and output variable
Value xValues, yValues;
if (validationFlag == true)
{
xValues = Value.CreateBatch<float>(new NDShape(1, inputDim),
Form1.rdr.GetCNTK_Validate_1DFeatures, Form1.device);
yValues = Value.CreateBatch<float>(new NDShape(1, numOutputClasses),
Form1.rdr.GetCNTK_Validate_1DLabels, Form1.device);
}
else
{
xValues = Value.CreateBatch<float>(new NDShape(1, inputDim),
Form1.rdr.GetCNTK_Train_1DFeatures, Form1.device);
yValues = Value.CreateBatch<float>(new NDShape(1, numOutputClasses),
Form1.rdr.GetCNTK_Train_1DLabels, Form1.device);
}
var inputDataMap = new Dictionary<Variable, Value>() { { features, xValues} };
var expectedDataMap = new Dictionary<Variable, Value>() { { labels, yValues } };
var outputDataMap = new Dictionary<Variable, Value>() { { labels, null } };
var expectedData = expectedDataMap[labels].GetDenseData<float>(labels);
var expectedLabels = expectedData.Select(l => l.IndexOf(l.Max())).ToList();
ffnn_model.Evaluate(inputDataMap, outputDataMap, Form1.device);
var outputData = outputDataMap[labels].GetDenseData<float>(labels); // get
// probabilities
var actualLabels = outputData.Select(l => l.IndexOf(l.Max())).ToList(); // get
// labels
if (validationFlag == true)
{
int NofMatches = actualLabels.Zip(expectedLabels,
(a, b) => a.Equals(b) ? 1 : 0).Sum();
int totalCount = yValues.Data.Shape.Dimensions[2]; // gets NofCases
_accuracy = (double)NofMatches / totalCount;
ValidationSummary_B();
}
// Start by converting the 0-based labels to 1-based labels
List<int> predict = new List<int>();
List<int> status = new List<int>();
for (int k = 0; k < actualLabels.Count; k++)
{
predict.Add(actualLabels[k] + 1);
status.Add(expectedLabels[k] + 1);
}
Support.RunCrosstabs(predict, status);
}
…
}
crosstabs.cs 模块包含用于在 WinForm 的 `datagridview` 控件中以“混淆”矩阵格式显示实际和预测分类结果的方法,以及各种精确度、召回率和准确性统计数据。有兴趣将该例程用于自己目的的读者可以参考我很久以前写的另一篇 CodeProject 文章,题为**AI 分类项目的交叉表/混淆矩阵**。
在以前的研究中,使用更大的数据集 (N>14,000) 和 60%/40% 的训练/验证比例,这些分类模型通常可以实现贝塞尔曲线 >.98 的稳定验证准确性。以下是一个示例(使用 Python),一个 (24:12:6:2) 二元神经网络与其他分类模型(逻辑回归、线性判别分析、K 近邻、CART - 决策森林和支持向量机)在该数据集上的比较。
结论
本系列第一个演示得出的主要结论是,贝塞尔曲线对于在不同时间或 X 轴点收集的噪声纵向数据来说是非常有用的模型。第二个推论是,如果模型很好,那么关于这些模型的推论也可能很好。能够以各种方式分类性能轨迹的机器学习模型无疑将是数据驱动决策的有利工具。
学校和学区不断使用商业学生信息系统 (SIS) 收集数据。但这种“原始”信息很少可访问。一旦训练好的模型在手,仪表板和其他可视化方法就可以访问这些数据,对其进行分析,并向教育工作者提供用于实时个人或聚合队列分析的决策工具。这些也许是本系列未来文章的主题。如果您对使用本项目系列中描述的方法进行的一些实际研究感兴趣,您可能会对最近的 AERA 演讲《公共教育中的数据可视化》感兴趣。
除此之外,本项目还提供了各种有用的技术和方法,用于处理贝塞尔曲线以及训练机器学习模型来识别此类曲线所反映的模式和趋势,这些技术和方法可以适用于其他类似项目。
历史
- 2018 年 9 月 3 日:版本 1.0