《Arduino家居安全系统构建实战》——1.3 我们的第一个模型(C#版本)

简介:

本节书摘来异步社区《机器学习项目开发实战》一书中的第1章,第1.3节,作者:【美】Mathias Brandewinder(马蒂亚斯·布兰德温德尔),更多章节内容可以访问云栖社区“异步社区”公众号查看

1.3 我们的第一个模型(C#版本)

我们从C#实现开始热身(这应该是读者熟悉的领域),并在Visual Studio中创建一个C#控制台应用程序。我将自己的解决方案称作DigitsRecognizer(数字识别器),C#控制台应用名为CSharp——尽管取些比我更有创意的名称!

1.3.1 数据集组织

显然,首先我们需要数据。我们从http://1drv.ms/1sDThtz下载trainingsample.csv数据集,保存在机器上的某个位置。同一个位置还有第二个文件—— validationsample.csv,这个文件以后才会用到,但是我们现在就获取它。这两个文件采用CSV格式(逗号分隔值),结构如图1-5所示。第一行是文件头,后面的每行代表一个图像。第一列(“label”)表示图像代表的数字,接下来的784列(“pixel0”,“pixel11”)代表原始图像的每个像素,按照灰度编码(0~255,0表示纯黑,255表示纯白,中间的任何值是灰度级)。

9ef4881dc79fde0f62e58dcef2b6672b7bcf0c2e

例如,第一行的数据表示数字1,如果我们打算从这行数据重建真实的图像,可以将行分成28个“切片”,每个切片代表图像中的一行:像素(Pixel)0,像素1…像素27编码图像的第一行,像素28,像素29…像素55代表第二行,依次类推。我们最终一共有785列数据:一列用于标签,其他784列代表28行×28列=784个像素。图1-6所示用简化的4×4图像描述了编码机制:

真实的图像是1(第1列),然后是代表每个像素灰度的16列数据。

bded90b1c72d91437accc48559020bf543d78b63

■ 注意:

如果认真观察,就会注意到trainingsample.csv文件只有5000行,而不是前面提到的50000行。创建较小的文件是为了方便,只保留了原始数据最开始的一部分。50000行这个数字并不算大,但是足以让我们的进展变得很慢,这令人不快,此时在较大的数据集上工作也没有什么价值。
1.3.2 读取数据
我们将用典型的C#风格,围绕表示问题领域的几个类和接口构造代码。我们将在Observation类中保存每个图像的数据,用IClassifier接口表示算法,这样就可以在以后创建模型的变种。

第一步,我们需要从CSV文件读取数据,放入一组观测值中。下面我们进入解决方案,并在CSharp控制台项目中添加一个类,以保存观测值。

程序清单1-1 在Observation类中保存数据

public class Observation
{
    public Observation(string label, int[] pixels)
    {
        this.Label = label;
        this.Pixels = pixels;
    }

    public string Label { get; private set; }
    public int[] Pixels { get; private set; }
}```
接下来,我们添加一个DataReader类,用于从数据文件中读取观测值。这里实际上是执行两个不同的任务:从文本文件中提出每个相关的行,将每行转换为观测值类型。我们将这些任务分别放在两个方法中。

程序清单1-2 用DataReader类从文件读取

public class DataReader
{

private static Observation ObservationFactory(string data)
{
    var commaSeparated = data.Split(',');
    var label = commaSeparated[0];
    var pixels =
        commaSeparated
        .Skip(1)
        .Select(x => Convert.ToInt32(x))
        .ToArray();

    return new Observation(label, pixels);
}

public static Observation[] ReadObservations(string dataPath)
{
    var data =
        File.ReadAllLines(dataPath)
        .Skip(1)
        .Select(ObservationFactory)
        .ToArray();

    return data;
}

}`
注意,我们的代码主要是LINQ表达式!面向表达式的代码(如LINQ或者后面将会看到的F#)有助于编写非常清晰的代码,以直接的方式表达意图,通常比过程式编码有效得多。这种代码读起来更像自然语言:“读取所有行,跳过文件头,根据逗号拆分各行,解析为整数,为我提供新的观测值”。如果我和同行交谈,这就是描述意图的方法,而这个意图很清晰地反映在代码中。这种代码特别适合于数据操纵任务,因为它提供了描述数据转换工作流的自然手段,是机器学习的基础。毕竟,这就是LINQ的设计目的——“语言集成查询”!

我们已经有了数据、阅读器和保存它们的结构——下面在控制台应用中将其组合起来,用本地机器上实际数据文件的路径代替trainingPath中的PATH-ON-YOUR-MACHINE。

程序清单1-3 控制台应用程序

class Program
{
    static void Main(string[] args)
    {
        var trainingPath = @"PATH-ON-YOUR-MACHINE\trainingsample.csv";
        var training = DataReader.ReadObservations(trainingPath);

        Console.ReadLine();
    }
}```
如果你在代码块的最后放置一个断点,然后以调试模式运行,应该看到training变量是一个包含5000个观测值的数组。很好——一切似乎都很正常。

下一个任务是编写一个分类器,获得一个图像时,分类器将其与数据集中的每个Observation比较,找出最类似的,返回其标签。为此,需要两个元素:Distance(距离)和Classifier(分类器)。

####1.3.3 计算图像之间的距离
我们从距离开始,所需的是一个方法,用以取得两个像素数组并返回描述它们的差异的数字。距离是算法中的易变领域,我们很可能想要试验不同的图像比较方法,找出最合适的方法。因此采用一种设计,使我们能够轻松地替换不同的距离定义且不需要做很多的代码更改,是最为可取的。接口提供了一种方便的机制,可以使用它避免紧密耦合,确保在以后想要更改距离代码时,不需要遇到令人烦恼的重构问题。所以,我们从一开始就提取一个接口。

程序清单1-4 IDistance接口

public interface IDistance
{

double Between(int[] pixels1, int[] pixels2);

}`
有了接口之后,需要一个实现。同样,我们现在将采用可能有效的最简单的方法。例如,如果我们想要的是计量两个图像的差异,为什么不逐个像素比较,计算每个像素的差值,然后加总其绝对值?完全相同的图像距离为0,两个像素相差越远,两个图像的距离就越大。这种距离的名称为“曼哈顿距离”,实现起来相当简单,如程序清单1-5所示。

程序清单1-5 计算图像之间的曼哈顿距离

public class ManhattanDistance : IDistance
{
    public double Between(int[] pixels1, int[] pixels2)
    {
        if (pixels1.Length != pixels2.Length)
        {
                throw new ArgumentException("Inconsistent image sizes.");
        }

        var length = pixels1.Length;

        var distance = 0;

        for (int i = 0; i < length; i++)
        {
            distance += Math.Abs(pixels1[i] - pixels2[i]);
        }

        return distance;
    }
}```
有趣的事实:曼哈顿距离


前面我已经提到过,距离可以用多种方法计算。我们在这里使用的具体公式被称作“曼哈顿距离”。这个名称来自于这样一个事实:如果你是纽约市的出租车司机,这就是计算两点之间驾车距离的方法。因为所有街道都被组织为一个完善的矩形网格,可以计算东/西位置和南/北位置之间的绝对距离,这就是我们的代码中所完成的工作。这种方法还有一种不那么有诗意的名称——L1距离。
我们取两个图像并逐个像素比较,计算差值并返回总数,代表两个图像的距离。注意,这里使用的代码采用了过程式风格,完全没有使用LINQ。实际上,我最初使用LINQ编写了这一段代码,但是老实说不喜欢结果的显示方式。在我看来,在某种程度(或者对于某些运算来说)上,以C#语言编写的LINQ代码看起来有点过于复杂,这主要是因为C#很冗长,尤其是其函数构造(Func<A,B,C>)。这也是比较两种风格的有趣示例。在此,要理解代码所做的是什么,需要逐行阅读并将其翻译为“人类的描述”。这段代码还使用了突变(Mutation)——这是一种需要注意的风格。

MATH.ABS( )


你可能觉得奇怪,为什么我们要使用绝对值?为什么不简单地计算差值?为了理解这成为问题的原因,考虑如下的例子:

<div style="text-align: center"><img src="https://yqfile.alicdn.com/7f59e33503eb3fce6e1fd3bf6ab20fac4a6377c9.png" width="250" height="">
</div>

如果我们使用像素颜色的“简单”差值,就会遇到一个微妙的问题。计算第一个和第二个图像的差值将得到−255+255−255+255=0,和第一个图像与自身的距离相同。这明显是不正确的。第一个图像明显和自身完全相同,而按照该指标,不同的图像1和2在外观上可能是相同的!使用绝对值的理由就是:如果不这么做,方向相反的差异就会相互抵消,结果是完全不同的图像可能会产生很高的相似度。绝对值保证不会有这种问题:任何差异都根据其量级进行处置,而不考虑其符号。
####1.3.4 编写分类器
我们已经有了比较图像的方法,现在可以从一个通用接口开始,编写分类器了。在每种情况下,我们都预期使用一个两步的过程。提供一个已知观测值的训练集对分类器进行训练,一旦训练完成,我们就应该能够预测某个图像的标签了,见程序清单1-6。

程序清单1-6 IClassifier接口

public interface IClassifier
{

void Train(IEnumerable<Observation> trainingSet);
string Predict(int[] pixels);

}`
下面是实现前述算法的多种方法之一。

程序清单1-7 基本分类器实现

public class BasicClassifier : IClassifier
{
    private IEnumerable<Observation> data;

    private readonly IDistance distance;

    public BasicClassifier(IDistance distance)
    {
        this.distance = distance;
    }

    public void Train(IEnumerable<Observation> trainingSet)
    {
         this.data = trainingSet;
    }

    public string Predict(int[] pixels)
    {
        Observation currentBest = null;
        var shortest = Double.MaxValue;

        foreach (Observation obs in this.data)
        {

            var dist = this.distance.Between(obs.Pixels, pixels);
            if (dist < shortest)
            {
                shortest = dist;
                currentBest = obs;
            }
        }

        return currentBest.Label;
    }
}```
相关文章
|
C# C++
创建目标类型对象在C#7.3中不可用,请使用9.0或更高的语言版本
创建目标类型对象在C#7.3中不可用,请使用9.0或更高的语言版本
1766 0
创建目标类型对象在C#7.3中不可用,请使用9.0或更高的语言版本
|
PyTorch API C#
【Python+C#】手把手搭建基于Hugging Face模型的离线翻译系统,并通过C#代码进行访问
目前翻译都是在线的,要在C#开发的程序上做一个可以实时翻译的功能,好像不是那么好做。而且大多数处于局域网内,所以访问在线的api也显得比较尴尬。于是,就有了以下这篇文章,自己搭建一套简单的离线翻译系统。以下内容采用python提供基础翻译服务+ C#访问服务的功能,欢迎围观。
1116 0
【Python+C#】手把手搭建基于Hugging Face模型的离线翻译系统,并通过C#代码进行访问
|
4月前
|
API C# 数据库
SemanticKernel/C#:实现接口,接入本地嵌入模型
SemanticKernel/C#:实现接口,接入本地嵌入模型
92 1
|
4月前
|
C# 索引
C#各大版本特性
C#各大版本特性
78 0
|
4月前
|
API C#
SemanticKernel/C#:使用Ollama中的对话模型与嵌入模型用于本地离线场景
SemanticKernel/C#:使用Ollama中的对话模型与嵌入模型用于本地离线场景
111 0
|
6月前
|
C#
C# 版本的 计时器类 精确到微秒 秒后保留一位小数 支持年月日时分秒带单位的输出
这篇2010年的文章是从别处搬运过来的,主要包含一个C#类`TimeCount`,该类有多个方法用于处理时间相关的计算。例如,`GetMaxYearCount`计算以毫秒为单位的最大年数,`GetCurrentTimeByMiliSec`将当前时间转换为毫秒,还有`SecondsToYYMMDDhhmmss`将秒数转换为年月日时分秒的字符串。此外,类中还包括一些辅助方法,如处理小数点后保留一位数字的`RemainOneFigureAfterDot`。
|
编解码 自动驾驶 算法
Baumer工业相机堡盟工业相机如何使用新版本NEOAPI SDK控制相机数据流的开启和关闭(C#)
Baumer工业相机堡盟工业相机如何使用新版本NEOAPI SDK控制相机数据流的开启和关闭(C#)
150 0
|
测试技术 C#
委托与事件(一)——C#版本
委托与事件(一)——C#版本
82 0
|
C# C++ 网络架构
《c# 实现p2p文件分享与传输系统》 一、 模型
《c# 实现p2p文件分享与传输系统》 一、 模型
272 0
|
算法 C# 决策智能
运筹优化学习16:【电招问题】Dial-a-Ride问题模型及C#实现的VNS程序
运筹优化学习16:【电招问题】Dial-a-Ride问题模型及C#实现的VNS程序
运筹优化学习16:【电招问题】Dial-a-Ride问题模型及C#实现的VNS程序
下一篇
DataWorks