[SwiftUI 100天] BetterRest · part3

846 阅读12分钟
译自 Training a model with Create ML
更多内容,欢迎关注公众号 「Swift花园」

用 CreateML 创建模型

在 iOS 11 中,设备上机器学习已从 “极度困难” 变为 “相当可能,而且功能强大”,这要归功于一个苹果框架:Core ML。一年后,Apple 引入了另一个名为 Create ML 的框架,特性清单又多了 “易于执行” 这一点,在这之后次年,Apple 又引入了 Create ML 应用程序,这个应用程序让用户通过拖拽完成主要任务。所有这些工作的成果是,现在连机器学习小白都可以将机器学习添加到他们的应用程序。

Core ML 能够处理各种训练任务,例如识别图像,声音甚至运动,但是在当前实例中,我们将研究表格回归 (tabular regression)。这是一个花哨的名字,在机器学习中很常见,它的含义是我们可以在 Create ML 上喂给模型大量类似电子表格的数据,并要求它找出各种值之间的关系。

机器学习分两个步骤进行:我们训练模型,然后让模型进行预测。训练是计算机查看我们所有数据以弄清我们拥有的所有值之间的关系的过程,而在大型数据集中,这可能需要很长时间,少则几个小时,多的话可能更长。预测是在设备上完成的:我们将训练后的模型提供给它,它将使用以前的结果对新数据进行估算。

让我们开始训练模型:在Mac上打开 “Create ML” 应用。如果你不知道它在哪里,可以通过 Xcode 菜单选择 Open Developer Tool > Create ML 从 Xcode 启动它。

Create ML 应用程序要做的第一件事是让你创建一个项目或打开一个上一个项目——单击“新建文档” 。你会看到有很多模板可供选择,但是如果向下滚动到底部,会看到 Tabular Regressor ,我们选择这个选项,然后点 Next 。对于项目名称,输入 BetterRest ,然后点 Next ,选择桌面,然后 Create。

接下来,在这里 Create ML 似乎有些棘手,因为你会看到一个包含很多选项的界面。不过放心,一旦我逐步引导,就不会那么困难。

第一步是向 Create ML 提供一些训练数据。这是要查看的原始统计数据,在我们的案例中,该统计数据包含四个值:某人想要醒来的时间,他们认为自己需要多少睡眠,每天喝多少咖啡以及他们实际需要多少睡眠。

我已经在 BetterRest.csv 中帮你提供了这份数据,文件位于项目的项目文件中。这是 Create ML 可以使用的由逗号分隔值的数据集,我们的第一项工作是导入这个数据集。

因此,在 Create ML 中,在 “Data Inputs” 下查找,然后在 “Training Data” 标题下选择 “Choose” 。点击 Select File 时,会打开一个文件选择窗口,选择 BetterRest.csv 。

重要提示:这个 CSV 文件包含的数据只能用于这个项目,不要将它用于与健康相关的实际工作。

下一步是确定目标,也就是我们希望计算机学习预测的值,以及特征,也就是计算机用来预测目标值的输入项。比如,如果我们选择某人认为他需要多少睡眠以及他们实际需要多少睡眠作为特征,我们可以训练计算机来预测他们喝了多少咖啡。

在这种实例下,我们将选择 “actualSleep” 作为目标,也就是说,我们希望计算机学习如何预人们实际需要多少睡眠时间。现在,点击 “Select Features”,然后选择所有三个选项:“wake”,“estimatedSleep” 和 “coffee” —— 我们希望计算机在生成预测时将所有这三个因素都考虑在内。

在 “Select Features” 按钮下面是算法的下拉列表,有五个选项:“Automatic”,“Random Forest,”,“ Boosted Tree”,“ Decision Tree” 和 “Linear Regression”。每种方法都采用不同的方法来分析数据,尽管我们这里不是在学习机器学习,但我还是想简要解释一下它们的作用。

线性回归是最容易理解的,因为它几乎完全是我们大脑的工作方式。它尝试通过将变量视为线性函数,如 applyAlgorithm(var1, var2, var3) 的一部分来估计变量之间的关系。线性回归的目标是能够在所有数据点上绘制一条直线,其中直线与每个数据点之间的平均距离应尽可能小。

决策树回归让我们将信息以一系列选择的形式组织,以形成自然的树状结构。尝试想象这就像一个有 20 个问题的游戏:“你是人还是动物?如果你是一个人,你是活着还是死了?如果你还活着,那么你是年轻还是年老?” 依此类推,每次树会根据每个问题的答案而分支,直到最终有了明确的答案。

增强树回归使用一系列决策树来工作,其中每棵树旨在纠正前一棵树中的任何错误。例如,第一棵决策树会尽最大的可能去寻找一个好的预测,但是误差有 20% 。然后,将其传递到第二棵决策树以进行进一步细化,重复该过程,但这一次,误差降至 10% 。进入到第三棵树,误差降低到 8% ,第四棵树,误差降低到 7% 。

随机森林模型和增强树相似,但略有不同:对于增强树,树中的每个决策都可以访问所有可用数据,而对于随机森林中的树,每棵树只能访问数据的一个子集。

这听起来可能很奇怪:你为什么要保留数据?好吧,想象一下,你正面临一个编码问题并试图提出一个解决方案。如果你要求某位同事提供想法,他将根据你所了解的知识为你提供一些想法。如果你要求另外的同事提供想法,这位同事又可能会根据他所知道的知识为你提供不同的想法。如果你向一百位同事征求想法,你将获得一系列解决方案。

每个同事的背景,教育程度和工作经历都与其他同事不同,因此你会得到一系列建议。但是,如果你将每个人的建议平均化(不管大多数人怎么说,不管他们是怎么做出决定的),那么你就很有机会获得正确的解决方案。

这正是随机森林回归的工作方式:每棵决策树都有自己的数据视图,这份数据视图与其他树的不同,将它们的所有预测结合在一起以得出平均值,你就很有可能获得良好的结果。

真希望有一个自动选项可以尝试自动选择最佳算法,它不一定总是正确的,实际上这种选项极大地限制了我们的选择范围。但是对于这个项目来说,已经足够了。

准备就绪后,点击窗口标题栏中的 “Train” 按钮。几秒钟后 —— 我们的数据很小!—— 你会看到一些结果指标。我们关心的值称为“均方根误差”,你应该获得大约 180 的值。这意味着这个模型能够预测准确睡眠时间的误差仅为 180 秒。

更棒的是,你可以在右上角看到一个 MLModel 图标,上面写着 “Output” ,它的文件大小约为 438 字节。 Create ML 占用了 180KB 的数据,并将其压缩为 438 个字节 —— 几乎可以忽略。

我知道 438 个字节听起来很小,但是值得一提的是,几乎所有这些字节都是元数据:作者的名字就在其中,包括 “经过训练用于回归的机器学习模型” 这段默认描述。它甚至对所有字段的名称都进行编码:wake,estimateSleep,coffee 和 actualSleep 。

数据部分占用的实际空间量 —— 即基于我们的三个变量来预测所需的睡眠量的算法 —— 远远低于 100 个字节。这是可能的,因为 Create ML 实际上并不关心值是什么,而只关心关系是什么。因此,它花费了数十亿个 CPU 周期,为每个特征尝试了权重的各种组合,以查看哪些特征组合产生的值与实际目标值最接近,并且一旦知道了最佳算法,便会简单地存储该算法。

现在我们的模型已经训练完毕,你可以把模型文件从 “Create ML” 拖到桌面,以便我们在代码中使用。

提示:如果你想再次尝试训练 —— 也许尝试使用其他可用的各种算法 —— 只要单击窗口右下角的 “Make A Copy ” 就可以创建副本。

创建基本布局

这个 app 会让用户通过一个 date picker 和两个 stepper 来输入信息,这些信息组合起来能够告诉我们他们希望什么时候起床,希望获得多少睡眠,以及他们喝了多少咖啡。

先从添加三个属性开始,这三个属性对应存储三个控件的信息:

@State private var wakeUp = Date()
@State private var sleepAmount = 8.0
@State private var coffeeAmount = 1

body 属性内,我们将放置三组组件,用VStack 再加上 NavigationView套起来。先从起床时间开始。把默认的 “Hello World” 文本视图替换成下面的代码:

NavigationView {
    VStack {
        Text("你想要什么时间起床?")
            .font(.headline)

        DatePicker("请输入一个时间", selection: $wakeUp, displayedComponents: .hourAndMinute)
            .labelsHidden()

        // more to come
    }
}

因为我们是处于 VStack中,所以 date picker 在 iOS 上会以 spinning wheel 的风格展示,在这里是没问题的。我们会用 .hourAndMinute 配置,因为我们只关心起床的时间点,不关心是哪一天。通过 labelsHidden() modifier ,我们把 picker 的标签隐藏 —— 上面的文本说明已经足够了。

接下来我们要添加一个 stepper ,让用户选择他们想要的睡眠时间。通过传给 in 参数一个 4...12 的范围,加上 0.25 的步长,我们可以确保用户输入有意义的时间值。并且我们可以加上 %g 字符串插值限定符,这样就只会看到 “8” 这样的数字,而不是 “8.000000” 。

用下面的代码替换 // more to come注释:

Text("需要的睡觉时长")
    .font(.headline)

Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
    Text("\(sleepAmount, specifier: "%g") hours")
}

最后我们还要添加一个 stepper 和 label ,用来处理他们喝的咖啡数量。这一次我们用 1 到 20 的范围 (一天 20 杯咖啡对于一个人来说足够多了吧?) ,不过我们会同时 stepper 内展示两个标签中的某一个,这两个标签是 “1 cup" 和 " cups" ,根据咖啡数量大于 1 杯来选择。

把下面的代码添加到 VStack,在之前的视图下面:

Text("每天摄入的咖啡")
    .font(.headline)

Stepper(value: $coffeeAmount, in: 1...20) {
    if coffeeAmount == 1 {
        Text("1 杯")
    } else {
        Text("\(coffeeAmount) 杯")
    }
}

我们需要用到的最后一样东西是一个按钮,它用来给用户计算应该上床睡觉的时间。我们可以在 VStack末尾简单添加一个按钮。不过为了调节一下,我想尝试点新东西:我们将把这个按钮直接添加到导航栏上。

首先我们需要写一个供按钮调用的方法,添加下面的calculateBedtime() 空方法:

func calculateBedtime() {
}

接下来我们需要使用 navigationBarItems() modifier 来增加一个 trailing 按钮到导航栏。 “Trailing” 在从左往右读的语言中表示 “在右边”,并且你在这个位置可以放置任何视图 —— 如果你想放几个按钮,你可以用上一个 HStack。既然提高了导航栏按钮,我们也可以介绍一下 navigationBarTitle(),它用来给导航栏设置标题。

把下面这些 modifier 添加到 VStack:

.navigationBarTitle("BetterRest")
.navigationBarItems(trailing:
    // 我们的按钮
)

接下来我们要 实现 “Calculate” 按钮的逻辑。之前我解释过,按钮可以有两种形式:

Button("Hello") {
    print("按钮被点击")
}

Button(action: {
    print("按钮被点击")
}) {
    Text("Hello")
}

我们可以用第一种方式:

Button("计算") {
    self.calculateBedtime()
}

好像可以工作,不过我希望你再考虑一下。上面的方式创建了一个新的闭包,整个闭包的工作就是调用另一个方法。闭包,这里可以暂时简单理解为没有名字的函数 —— 我们把它们直接赋值给某些东西,而不会单独实体的方式声明它们。

这里我们创建了一个调用另一个函数的函数。跳过这个中间层是不是更好呢?

是的。按钮关心的是动作是某种不接收参数并且不返回任何东西的函数 —— 它并不要求这个函数是方法或者闭包,只要满足规则。

因此,我们把 calculateBedtime 直接传给按钮的 action ,就像这样:

Button(action: calculateBedtime) {
    Text("计算")
}

好了,看到这里人们经常会认为我犯了一个编码错误,他们认为代码应该是下面这样的:

Button(action: calculateBedtime()) {
    Text("计算")
}

但是,这个代码无法工作,实际上,它的含义跟我们要表达的大相径庭。如果我们在calculateBedtime 后添加圆括号,它表示 “调用 calculateBedtime() ”,并且它会返回给按钮被点击时要使用正确的函数”。这样一来,Swift 会要求 calculateBedtime() 返回一个闭包。

calculateBedtime 而不是calculateBedtime(),我们是在告诉 Swift ,在按钮点击时才运行这个方法,它并不返回任何东西。

Swift 打破了函数,方法,闭包,甚至操作符 (+-, 等) 的界限,这使得我们以交换的方式使用它们。

所以,整个 modifier 看起来是这样的:

.navigationBarItems(trailing:
    Button(action: calculateBedtime) {
        Text("计算")
    }
)

不过按钮点击时什么都不会发生,因为 calculateBedtime() 还是空的,但是至少我们的 UI 目前已经足够好了。


相关文章

[SwiftUI 100 天] BetterRest - part1

[SwiftUI 100 天] BetterRest - part2


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~