一, 问题与解决方案
通过多元分类算法进行手写数字识别, 手写数字的图片分辨率为 8*8 的灰度图片, 已经预先进行过处理, 读取了各像素点的灰度值, 并进行了标记.
其中第 0 列是序号(不参与运算),1-64 列是像素值, 65 列是结果.
我们以 64 位像素值为特征进行多元分类, 算法采用 SDCA 最大熵分类算法.
二, 源码
先贴出全部代码:
- namespace MulticlassClassification_Mnist
- {
- class Program
- {
- static readonly string TrainDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "optdigits-full.csv");
- static readonly string ModelPath = Path.Combine(Environment.CurrentDirectory, "Data", "SDCA-Model.zip");
- static void Main(string[] args)
- {
- MLContext mlContext = new MLContext(seed: 1);
- TrainAndSaveModel(mlContext);
- TestSomePredictions(mlContext);
- Console.WriteLine("Hit any key to finish the app");
- Console.ReadKey();
- }
- public static void TrainAndSaveModel(MLContext mlContext)
- {
- // STEP 1: 准备数据
- var fulldata = mlContext.Data.LoadFromTextFile(path: TrainDataPath,
- columns: new[]
- {
- new TextLoader.Column("Serial", DataKind.Single, 0),
- new TextLoader.Column("PixelValues", DataKind.Single, 1, 64),
- new TextLoader.Column("Number", DataKind.Single, 65)
- },
- hasHeader: true,
- separatorChar: ','
- );
- var trainTestData = mlContext.Data.TrainTestSplit(fulldata, testFraction: 0.2);
- var trainData = trainTestData.TrainSet;
- var testData = trainTestData.TestSet;
- // STEP 2: 配置数据处理管道
- var dataProcessPipeline = mlContext.Transforms.Conversion.MapValueToKey("Label", "Number", keyOrdinality: ValueToKeyMappingEstimator.KeyOrdinality.ByValue);
- // STEP 3: 配置训练算法
- var trainer = mlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy(labelColumnName: "Label", featureColumnName: "PixelValues");
- var trainingPipeline = dataProcessPipeline.Append(trainer)
- .Append(mlContext.Transforms.Conversion.MapKeyToValue("Number", "Label"));
- // STEP 4: 训练模型使其与数据集拟合
- Console.WriteLine("=============== Train the model fitting to the DataSet ===============");
- ITransformer trainedModel = trainingPipeline.Fit(trainData);
- // STEP 5: 评估模型的准确性
- Console.WriteLine("===== Evaluating Model's accuracy with Test data =====");
- var predictions = trainedModel.Transform(testData);
- var metrics = mlContext.MulticlassClassification.Evaluate(data: predictions, labelColumnName: "Number", scoreColumnName: "Score");
- PrintMultiClassClassificationMetrics(trainer.ToString(), metrics);
- // STEP 6: 保存模型
- mlContext.ComponentCatalog.RegisterAssembly(typeof(DebugConversion).Assembly);
- mlContext.Model.Save(trainedModel, trainData.Schema, ModelPath);
- Console.WriteLine("The model is saved to {0}", ModelPath);
- }
- private static void TestSomePredictions(MLContext mlContext)
- {
- // Load Model
- ITransformer trainedModel = mlContext.Model.Load(ModelPath, out var modelInputSchema);
- // Create prediction engine
- var predEngine = mlContext.Model.CreatePredictionEngine<InputData, OutPutData>(trainedModel);
- //num 1
- InputData MNIST1 = new InputData()
- {
- PixelValues = new float[] { 0, 0, 0, 0, 14, 13, 1, 0, 0, 0, 0, 5, 16, 16, 2, 0, 0, 0, 0, 14, 16, 12, 0, 0, 0, 1, 10, 16, 16, 12, 0, 0, 0, 3, 12, 14, 16, 9, 0, 0, 0, 0, 0, 5, 16, 15, 0, 0, 0, 0, 0, 4, 16, 14, 0, 0, 0, 0, 0, 1, 13, 16, 1, 0 }
- };
- var resultprediction1 = predEngine.Predict(MNIST1);
- resultprediction1.PrintToConsole();
- }
- }
- class InputData
- {
- public float Serial;
- [VectorType(64)]
- public float[] PixelValues;
- public float Number;
- }
- class OutPutData : InputData
- {
- public float[] Score;
- }
- }
- View Code
三, 分析
整体流程和二元分类没有什么区别, 下面解释一下有差异的两个地方.
1, 加载数据
- // STEP 1: 准备数据
- var fulldata = mlContext.Data.LoadFromTextFile(path: TrainDataPath,
- columns: new[]
- {
- new TextLoader.Column("Serial", DataKind.Single, 0),
- new TextLoader.Column("PixelValues", DataKind.Single, 1, 64),
- new TextLoader.Column("Number", DataKind.Single, 65)
- },
- hasHeader: true,
- separatorChar: ','
- );
这次我们不是通过实体对象来加载数据, 而是通过列信息来进行加载, 其中 PixelValues 是特征值, Number 是标签值.
2, 训练通道
- // STEP 2: 配置数据处理管道
- var dataProcessPipeline = mlContext.Transforms.Conversion.MapValueToKey("Label", "Number", keyOrdinality: ValueToKeyMappingEstimator.KeyOrdinality.ByValue)
- // STEP 3: 配置训练算法
- var trainer = mlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy(labelColumnName: "Label", featureColumnName: "PixelValues");
- var trainingPipeline = dataProcessPipeline.Append(trainer)
- .Append(mlContext.Transforms.Conversion.MapKeyToValue("Number", "Label"));
- // STEP 4: 训练模型使其与数据集拟合
- ITransformer trainedModel = trainingPipeline.Fit(trainData);
首先通过 MapValueToKey 方法将 Number 值转换为 Key 类型, 多元分类算法要求标签值必须是这种类型(类似枚举类型, 二元分类要求标签为 BOOL 类型). 关于这个转换的原因及编码方式, 下面详细介绍.
四, 键值类型编码与独热编码
MapValueToKey 功能是将 (字符串) 值类型转换为 KeyTpye 类型.
有时候某些输入字段用来表示类型(类别特征), 但本身并没有特别的含义, 比如编号, 电话号码, 行政区域名称或编码等, 这里需要把这些类型转换为 1 到一个整数如 1-300 来进行重新编号.
举个简单的例子, 我们进行图片识别的时候, 目标结果可能是 "猫咪","小狗","人物" 这些分类, 需要把这些分类转换为 1,2,3 这样的整数. 但本文的标签值本身就是 1,2,3, 为什么还要转换呢? 因为我们这里的一二三其实不是数学意义上的数字, 而是一种标志, 可以理解为壹, 贰, 叁, 所以要进行编码.
MapKeyToValue 和 MapValueToKey 相反, 它把将键类型转换回其原始值(字符串). 就是说标签是文本格式, 在运算前已经被转换为数字枚举类型了, 此时预测结果为数字, 通过 MapKeyToValue 将其结果转换为对应文本.
MapValueToKey 一般是对标签值进行编码, 一般不用于特征值, 如果是特征值为字符串类型的, 建议采用独热编码. 独热编码即 One-Hot 编码, 又称一位有效编码, 其方法是使用 N 位状态寄存器来对 N 个状态进行编码, 每个状态都由他独立的寄存器位, 并且在任意时候, 其中只有一位有效. 例如:
自然状态码为: 0,1,2,3,4,5
独热编码为: 000001,000010,000100,001000,010000,100000
怎么理解这个事情呢? 举个例子, 假如我们要进行人的身材的分析, 但我们希望加入地域特征, 比如:"黑龙江","山东","湖南","广东" 这种特征, 但这种字符串机器学习是不认识的, 必须转换为浮点数, 刚才提到 MapKeyToValue 可以把字符串转换为数字, 为什么这里要采用独热编码呢? 简单来说, 假设把地域名称转换为 1 到 10 几个数字, 在欧氏几何中 1 到 3 的欧拉距离和 1 到 9 的欧拉距离是不等的, 但经过独热编码后, 任意两点间的欧拉距离都是相等的, 而我们这里的地域特征仅仅是想表达分类关系, 彼此之间没有其他逻辑关系, 所以应该采用独热编码.
五, 进度调试
一般机器算法的数据拟合过程时间都比较长, 有时程序跑了两个小时还没结束, 也不知道还需要多长时间, 着实让人着急, 所以及时了解学习进度, 是很有必要的.
由于机器学习算法一般都有 "递归直到收敛" 这种操作, 所以我们是没有办法预先知道最终运算次数的, 能做到的只能打印一些过程信息, 看到程序在动, 心里也有点底, 当系统跑过一次之后, 基本就大致知道需要多少次拟合了, 后面再调试就可以大致了解进度了. 补充一句, 可不可以在测试阶段先减少样本数据进行快速调试, 调试通过后再切换到全样本进行训练? 其实不行, 有时候样本数量小, 可能会引起指标震荡, 时间反而长了.
之前在 Githube 上看到有人通过 MLContext.LOG 事件来打印调试信息, 我试了一下, 发现没法控制筛选内容, 不太方便, 后来想到一个方法, 就是新增一个自定义数据处理通道, 这个通道不做具体事情, 就打印调试信息.
类定义:
- namespace MulticlassClassification_Mnist
- {
- public class DebugConversionInput
- {
- public float Serial { get; set; }
- }
- public class DebugConversionOutput
- {
- public float DebugFeature { get; set; }
- }
- [CustomMappingFactoryAttribute("DebugConversionAction")]
- public class DebugConversion : CustomMappingFactory<DebugConversionInput, DebugConversionOutput>
- { static long TotalCount = 0;
- public void CustomAction(DebugConversionInput input, DebugConversionOutput output)
- {
- output.DebugFeature = 1.0f;
- TotalCount++;
- Console.WriteLine($"DebugConversion.CustomAction's debug info.TotalCount={TotalCount} ");
- }
- public override Action<DebugConversionInput, DebugConversionOutput> GetMapping()
- => CustomAction;
- }
- }
使用方法:
- var dataProcessPipeline = mlContext.Transforms.CustomMapping(new DebugConversion().GetMapping(), contractName: "DebugConversionAction")
- .Append(...)
- .Append(mlContext.Transforms.Concatenate("Features", new string[] { "RealFeatures", "DebugFeature" }));
通过 CustomMapping 加载我们自定义的数据处理通道, 由于数据集是懒加载 (Lazy) 的, 所以必须把我们自定义数据处理通道的输出加入为特征值, 才能参与运算, 然后算法在操作每一条数据时都会调用到 CustomAction 方法, 这样就可以打印进度信息了. 为了不影响运算结果, 我们把这个数据处理通道的输出值固定为 1.0f .
六, 资源获取
源码下载地址: https://github.com/seabluescn/Study_ML.NET
工程名称: MulticlassClassification_Mnist
点击查看机器学习框架 ML.NET 学习笔记系列文章目录
来源: https://www.cnblogs.com/seabluescn/p/10941580.html