大多数浏览器和
Developer App 均支持流媒体播放。
-
了解 Create ML 组件
Create ML 使为图像分类、物体检测、声音分类、手势分类、动作分类、表格数据回归等构建自定义机器学习模型变得更加简单。借助 Create ML 组件框架,您可以进一步自定义底层任务并优化您的模型。我们将探索组成此类任务的特征提取器、转换器和估算器,并向您介绍如何将其与其他组件和预处理步骤组合起来,以便为图像回归等概念构建自定义任务。 要了解有关创建复杂可自定任务的更多信息,我们建议您观看 WWDC22 的“利用 Create ML 组件构建高级模型”。
资源
相关视频
WWDC23
WWDC22
WWDC21
Tech Talks
WWDC20
WWDC19
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 您好 我是 Alejandro 我是 CreateML 团队的工程师 今天我要介绍一个 基于组件构建 机器学习模型的全新 API Create ML 提供一种 简单的 API 来训练机器学习的模型 它基于一组支持的任务 比如图像分类 声音分类等等 在 WWDC 2021 我们就 Create ML 框架 进行了两场精彩的演讲 如果您还没看过 请务必查看这些内容 但现在我想谈谈 如何超越预定义任务 如果您想自定义 Create ML 解决方案 以外的任务 该怎么办? 又或者 如果您想构建 其他类型的任务 该怎么办? 使用组件 您就可以 用新的创造性方式 进行任务编写 让我们进一步探讨 我将从分解机器学习任务 并解释每个组件的作用开始 然后讨论应该如何将组件拼合起来 接下来是自定义图像任务的示例 之后 我将讨论表列任务 最后以部署策略结束今天的讲座 就让我从探讨一项机器学习任务的 内部细节入手 以便让您了解 它的内容和工作原理 这样一来 当我们开始构建自定义任务时 您就会更容易理解我在说什么 我将以图像分类器为例 图像分类器使用标记图像列表 对模型进行训练 在这个例子当中 我有一系列猫和狗的图像 各自带有标签 但让我们先来研究一下 图像在每一步是如何转换的 为此 我将展开图像分类任务 看看里面有些什么 从概念上看 图像分类器非常简单 它由特征提取器和分类器组成 重要的一点 是 Create ML 组件 让您可以独立访问这些组件 您可以通过添加 移除 或切换组件来编写新任务 我将用方框来代表组件 用箭头代表数据流 让我们关注图像分类器的第一步 特征提取 通常 特征提取器 会对输入进行降维处理 只保留有趣的关键部分 即“特征” 处理图像时 特征提取器从图像中寻找模式 Create ML 使用 Vision Feature Print 它是由 Vision 框架提供的 出色的图像特征提取器 现在 让我们再来谈谈第二部分 分类器 分类器使用一组示例 来学习分类 常见的实现方式包括逻辑回归算法 提升树算法和神经网络算法 也就是说 图像分类器的训练 从带注释的图像开始 接下来是带注释的特征 并以分类器结束 那我们为什么要对它进行拆解呢? 因为我们想扩展可能性 也许您想通过增加对比度 对图像做一些预处理 也许您想对所有图像进行归一化处理 以确保提取特征前 它们具有统一的亮度 也许您想尝试不同的特征提取器 也许您想尝试不同的分类器 可能性永无止境 这些只是其中的几个选项 因此我们在 macOS iOS iPadOS 和 tvOS 中 添加了对 ML 组件的支持 我们希望您能创建出新的模型 使用我们提供的部分组件 与您自己的组件配合 甚至是和社群中其他人 创建的组件一同使用 您可以在我们所有的平台上使用它 以下是 Create ML Components 中 内置的一些组件 但让我先回过头来介绍一些概念 组件分为两种类型 转换器和估算器 转换器仅仅是一种 能够执行部分转换的类型 它定义输入类型和输出类型 例如 图像特征提取器获取输入图像 并产出特征整形数组 而估算器则需要从数据中学习 它获取输入示例 进行一些处理后 生成转换器 我们称这个过程为“拟合” 很好 解决了这些概念 接下来我会说明 Create ML 组件 如何通过合成各个组件 创建处图像分类器 这是一个使用组件的图像分类器 它将 ImageFeaturePrint 作为特征提取器 将 LogisticRegressionClassifier 作为分类器 无论一个组件 是转换器还是估算器 都可以用 appending 方法进行组合 这就是为什么说 组件可以带来无限的可能性 您可以使用全连接神经网络 作为分类器 而不是简单更改逻辑回归算法 您也可以在 CoreML 模型中 使用自定义特征提取器 例如 可以在模型库中找到的 无头 ResNet-50 模型 合成两个组件时 第一个组件的输出类型 必须和第二个组件的输入类型匹配 就我们的图像分类器来说 特征提取器的输出是一个 来自 CoreML 框架的 Shaped 数组 这也是逻辑回归分类器的输入 如果在使用附加法时 遇到编译器错误 首先要检查这里 确保类型匹配 但我要澄清一个 跟拟合有关的重要观点 我之前说过 拟合是 从估算器到转换器的过程 让我们从组合估算器的角度 来看一看 当组合估算器中 既有转换器又有估算器时 例如在图像分类器的案例中 仅对估算器的部分进行拟合 但转换器是该过程的重要组成部分 因为它们被用于向估算器的拟合方法 馈送正确的特征 这是代码 图像分类器 需要一个带注释的特征集合 其中特征是图像 注释是字符串 我们将在进入演示时 讨论特征的加载 一旦有了数据 就可以调用拟合法 它将返回经过训练的模型 即一个转换器 要重点注意的是 拟合时使用的类型 与生成转换器的类型 相互关联 但并不相同 尤其是拟合法中使用的类型 始终是集合 在使用监督估算器的情况下 特征必须包括注释 Create ML Components 使用 AnnotatedFeature 类型 来表述特征及其注释 一旦有了模型 就可以进行预测 无论是用我刚刚拟合的模型 还是从硬盘加载的参数 两者采用的 API 都是相同的 由于我训练的是分类器 结果就是分类分布 该分布包含了每个标签的概率 在这种情况下 我只为图像打印 可能性最高的标签 拟合法还提供了一种机制 用于观察训练事件 包括验证指标 在这个例子中 我传送验证数据 并打印验证准确性 请注意 只有监督估算器 才提供验证指标 一旦完成了模型的训练 您可以保存学习的参数 既可以在将来重新使用 也可以部署到 App 中 您可以使用 write 方法 进行这项操作 之后可以通过 read 方法 进行数据读取 这就是合成 从这里开始就变得有趣了 先来谈谈新任务的编写吧 这是 Create ML 刚刚开始支持的功能
假使您想训练模型 对图像进行评分该怎么做? 假设您有一些水果的照片 但不是要对水果进行分类 而是要评价它 根据它的成熟程度进行评分 为此 您需要使用回归 而不是分类 所以我来写一个图像回归器 根据成熟度为香蕉图像打分 我会给每张图片一个 介于 1 到 10 之间的成熟度值 图像回归器与图像分类器很相似 唯一的区别是估算器 将成为回归器而不是分类器 您可能已经猜到了 这将会很容易 帮您回忆一下 这是我们的图像分类器 而这是一个图像回归器 我用线性回归器替换了 逻辑回归分类器 这一简单的改变也使拟合法的 预期输入发生了变化 之前 它预期的是图像和标签 现在 它预期的是图像和分数 概念说到这里就足够了 让我用实际的代码来演示一下
让我向您展示如何 编写自定义图像回归器 我将从定义 ImageRegressor 结构 以封装代码开始
我有一个包含不同成熟度的 香蕉图像的文件夹 我将首先定义这个 URL
下一步是添加 train 方法 在这里您可以使用 训练数据来生成模型 我将在返回类型上 使用“some”关键字 这样一来 当我在组合估算器中 添加或修改步骤时 返回类型就不会改变 现在 我要定义估算器 它只是简单地在特征提取器上 附加了线性回归器 现在 我需要加载训练用的图像 以及它们的分数 我可以使用 AnnotatedFiles 它是一个 包含 URL 和字符串标签的 AnnotatedFeatures 集合 它提供了一个满足我需求的 方便的初始设定式 我的文件都有一个名称 后面跟一个破折号 后面跟着成熟度值 所以我要指定分隔符为破折号 并且指定注释位于 文件名组件的 index: 1 我还准备通过 使用类型参数 仅请求图像文件 现在有了 URL 我就需要加载图像 这可以通过 mapFeatures 方法 和 ImageReader 来实现 我还需要将分数 从字符串转换为浮点值 这可以通过 mapAnnotations 方法来实现
这样一来 我就获得了训练数据 但我想把其中一些拿出来用于验证 这可以使用 randomSplit 方法来实现 我将保留 80% 的数据用于训练 并使用剩余的进行验证 现在 我准备好进行拟合了
我还要保存训练好的参数 以便部署到我的 App 中 我要选择一个保存的位置
我将调用 write 方法
最后 我将返回转换器
这是使用组件定义和训练模型的 本质所在 我定义了组合估算器 加载了训练数据 调用了拟合方法 用 write 方法来保存参数 但还有些地方是可以改进的 首先 我正在传递验证数据集 但并没有观察验证错误 所以我会这样做 拟合法需要一个事件处理程序 可以用于收集指标
现在 我将只打印训练 和验证的最大误差值 我还想要最终模型的平均绝对误差
我的计算方法是将拟合转换器 用于验证特征 然后将它与实际分数一起传递给 meanAbsoluteError 函数 运行之后 我并没有 获得一个出色的模型 错误率很高 这是因为我没有足够多的香蕉图像 我应该去获取更多图像 但在这样做之前 我可以尝试增强数据集 我可以对图像进行旋转和缩放 以获得更多示例 为此 我将编写一个新方法 使用带注释的图像并对其进行增强 它会返回一个带注释的图像数组
我要实现的第一种增强是旋转
我会在 -pi 和 pi 之间 随机选取一个角度 并以这个角度旋转图像 我还设置了随机缩放
之后会返回三个图像 原始的 旋转的和缩放的
现在有了增强函数 我将使用它来借助 flatMap 增强我的训练图像
我的数据集中 每个元素都将转换为一个数组 FlatMap 将该数组集合 平铺为单个数组 这是拟合法所需要的 请注意 增强仅适用于拟合 而不适用于预测 好的 这样我的准确性就提高了 让我来谈谈另一项 可以进一步改善我的模型的措施 我想使用 Vision 框架 将图像裁剪到只剩突出对象 这是我的训练数据中的一张图片 一个人拿着香蕉 背景中有其他水果 模型可能会混淆照片中的其他物体 使用 Vision 框架 API 可以实现自动裁剪图像 仅突出最显著的对象 查看 WWDC 2019 上 关于 Vision 的演讲以了解 如果写一个自定义转换器 就可以轻松地将 这种转换应用于所有图像 在拟合和预测时均可使用 让我告诉您该怎么做 为了符合转换器协议 我唯一需要做的 就是实现应用的方法 在这个案例里 我想要它获取一张图片 并返回一张图片 我就不详细解释这段代码了 但还是提一句 如果得不到显著的对象 只需要返回原始图像 现在有了自定义转换器 我要将它添加到我的图像回归器中
我只需要在特征提取之前 使用自定义转换器即可
现在显著性成了 我任务定义的一部分 它将用于裁剪每个训练图像 进行推理时也会用到它 这是将任务定义 同时分配给训练和推理的 优势之一 在继续下一个任务之前 我要强调几个重点事项 使用组件 我可以创建自定义任务 我通过使用 appending 方法 来实现这一点 我使用 AnnotatedFiles 来加载 带有注释名称的文件 也可以加载由目录注释的文件 我使用 ImageReader 将 URL 映射到图像 并将注释从字符串映射到值 我使用 randomSplit 留出一个验证数据集 还保存了训练好的参数供以后使用 然后我添加了增强功能 并定义了一个自定义转换器 来改进我的模型 但这不仅仅适用于图像 我要换个方向 谈谈另一种类型的任务 表列任务 它们是使用表列数据的任务 表列数据的特点是拥有 不同类型的多种特征 它既包含了数值数据 也包含了分类数据 房价数据就是一个受欢迎的例子 既涉及面积和房龄之类的数据 也包含了街区位置 建筑类型等信息 现在您想学习预测一个值 比如说 销售价格 2021 年 我们引入了 TabularData 框架 现在您可以使用 TabularData 框架 配合 Create ML 组件 来创建和训练表列分类器和回归器 我还推荐您查看 关于 TabularData 的 Tech Talk 它对数据探索进行了精彩的介绍 您在构建表列任务时可能会需要 让我们了解一下吧 在处理表列数据时 表列的每一列 都包含不同类型的特征 您可能希望基于每一列 包含的信息类型 比如分布 数值范围 和其他因素分别进行处理 Create ML Components 提供了 ColumnSelector 来执行此操作 来看看这个例子 我刚才提到了房价 但房价太离谱了 我将改用鳄梨价格来举例 这里有一张鳄梨价格表 我想创建一个表列回归器 基于表列数据预测鳄梨价格 表列里既有包含数值数据的列 例如袋数 年份和总量 也有包含类型和 区域等分类数据的列 有些回归器受益于 对这些值更好的表述 比如说 这是数据集中总量值的分布 它接近正态分布 但较大的值大多 在 15,000 附近 我认为这是数据集可以 受益于归一化的绝佳范例 所以我要做的第一件事 就是将这些值归一化 为此 我可以将希望归一化的列名 传递给 ColumnSelector 然后使用标准缩放器 这是代码 首先 我创建一个列选取器 然后传递我想要缩放的列名 所有列必须包含相同类型的元素 在个例子中是 Double 然后我解析可选类型 之所以这样做 是因为我知道没有缺失值 但也可以使用 imputer 来替换缺失值 然后 我在解析代码上 附加 StandardScaler 所以 一开始的这张表列中 袋数达到好几万 总量达到几十万 在对这几列进行缩放后 最终得到的值规模接近于 1 这可以提升模型的性能 更具体来说 这些值现在的平均数为 0 标准差为 1 这是一个类似的例子 但在这个例子中 我选择了类型和区域列 它们是字符串类型并执行独热编码 独热编码是指对分类数据进行编码 使用数组来指示类别 这个例子中有三个类别 铜 银和金 每个类别在数组中 都拥有唯一的位置 由处于该位置的 1 表示 另一种方法是使用序数编码器 为每个类别分配一个连续的数字 仅有几个类别时 更适合使用独热编码器 类别较多时 则应当使用序数编码器 现在让我把这些结合起来 创建一个表列回归器
和之前一样 我将以创建结构开始 并定义数据 URL 和参数 URL
我还想定义一个列 ID 用于我要预测的列 即价格
我将单独定义我的任务 以便 对它应用 train 和 predict 两种方法
就像之前提到的那样 我要把总量归一化
然后 我将使用增强树回归器 来预测价格 它先获取注释列的名称 这也就是结果预测列 然后获取所有三个特征列的名称 我就从这三列开始 然后我会使用 appending 方法组合这些段落 并返回任务
现在有了任务定义 我就像之前一样 添加 train 方法
和之前一样 我想确保返回类型 不取决于我模型的细节 第一步是将 CSV 文件 加载到dataFrame中 我使用 TabularData 框架来执行此操作 和之前一样 我想拆分一些数据 用于验证
我将训练和验证数据集传递给 拟合法
我也会像之前一样报告验证错误 并保存训练好的参数供以后使用
最后 我会返回转换器
有了训练好的转换器 就可以用它来基于 dataFrame进行价格预测 我需要写一个 predict 方法来实现预测
首先 我将从任务定义中加载模型 和参数 URL
我需要确保用于预测的dataFrame 包含我用作特征的列 类型 区域和总量 预测值将出现在价格列中 我将使用我在顶部定义的列 ID
我的表列回归器这样就完成了 我写了一个 train 方法 只需要调用一次 就可以生成训练好的参数 还有一个 predict 方法 可以基于类型 地区以及 鳄梨的总量 返回鳄梨价格的预测 这就是我需要在 App 中使用的全部内容 在处理表列任务时 还有几点需要牢记 您可以使用 ColumnSelector 操作 来处理特定的列 值得注意的是 树形分类器和回归器 都是表列类的 但您也可以 使用非表列估算器 例如线性回归器 来处理使用 AnnotatedFeatureProvider 的表列任务 请参阅相关文档 在进行预测时 请创建一个包含所需列的dataFrame 确保使用的类型正确 现在您已经知道应该 如何创建自定义任务了 那就让我们来谈谈部署 到目前为止 我都在使用相同的 API 进行训练和推理 我想指出的是 在使用 Create ML Components 时 您的模型就是代码 您需要任务定义 即使只是从文件加载训练好的参数 这在部分情况下很有用 但有时您也许希望 使用 Core ML 进行部署 使用 Core ML 时 就必须将代码抛在脑后 模型完全由模型文件表述 如果您已经 准备好使用 Core ML 那么这一工作流可能会很顺畅 还具备优化张量操作的优势 但还有一些注意事项 您应该记住 Core ML 并不支持所有操作 具体来说 它不支持自定义转换器 和估算器 而且 Core ML 只支持几种类型 比如图像和整形数组 如果您使用自定义类型 您可能需要 在使用 Core ML 模型时 在 App 中进行转换 这就是将转换器导出为 Core ML 模型的方法 如果转换器中包含不受支持的操作 将会引发错误 如果您更希望将任务定义 连同训练参数一同进行部署 应当考虑将它们 捆绑在一个 Swift 软件包中 这样一来 您就可以提供 简单的方法来加载参数 和执行预测 有关 Swift 软件包资源的 更多信息 请查看 WWDC 2020 关于 Swift 软件包的演讲 我想说的就这么多 要记住的重点是 您现在可以通过合成 创建自定义任务 可能性永无止境 我期待看到您的创造 如想了解更高阶的技巧 包括音频和视频任务 请查看 “使用 Create ML Components 创建高阶模型”讲座 我的同事 David 将展示 更高阶的自定义任务 谢谢 希望您享受 WWDC 2022 的其他内容 ♪
-
-
8:59 - Image regressor
import CoreImage import CreateMLComponents struct ImageRegressor { static let trainingDataURL = URL(fileURLWithPath: "~/Desktop/bananas") static let parametersURL = URL(fileURLWithPath: "~/Desktop/parameters") static func train() async throws -> some Transformer<CIImage, Float> { let estimator = ImageFeaturePrint() .appending(LinearRegressor()) // File name example: banana-5.jpg let data = try AnnotatedFiles(labeledByNamesAt: trainingDataURL, separator: "-", index: 1, type: .image) .mapFeatures(ImageReader.read) .mapAnnotations({ Float($0)! }) let (training, validation) = data.randomSplit(by: 0.8) let transformer = try await estimator.fitted(to: training, validateOn: validation) try estimator.write(transformer, to: parametersURL) return transformer } }
-
12:18 - Image regressor with metrics and augmentations
import CoreImage import CreateMLComponents struct ImageRegressor { static let trainingDataURL = URL(fileURLWithPath: "~/Desktop/bananas") static let parametersURL = URL(fileURLWithPath: "~/Desktop/parameters") static func train() async throws -> some Transformer<CIImage, Float> { let estimator = SaliencyCropper() .appending(ImageFeaturePrint()) .appending(LinearRegressor()) // File name example: banana-5.jpg let data = try AnnotatedFiles(labeledByNamesAt: trainingDataURL, separator: "-", index: 1, type: .image) .mapFeatures(ImageReader.read) .mapAnnotations({ Float($0)! }) .flatMap(augment) let (training, validation) = data.randomSplit(by: 0.8) let transformer = try await estimator.fitted(to: training, validateOn: validation) { event in guard let trainingMaxError = event.metrics[.trainingMaximumError] else { return } guard let validationMaxError = event.metrics[.validationMaximumError] else { return } print("Training max error: \(trainingMaxError), Validation max error: \(validationMaxError)") } let validationError = try await meanAbsoluteError( transformer.applied(to: validation.map(\.feature)), validation.map(\.annotation) ) print("Mean absolute error: \(validationError)") try estimator.write(transformer, to: parametersURL) return transformer } static func augment(_ original: AnnotatedFeature<CIImage, Float>) -> [AnnotatedFeature<CIImage, Float>] { let angle = CGFloat.random(in: -.pi ... .pi) let rotated = original.feature.transformed(by: .init(rotationAngle: angle)) let scale = CGFloat.random(in: 0.8 ... 1.2) let scaled = original.feature.transformed(by: .init(scaleX: scale, y: scale)) return [ original, AnnotatedFeature(feature: rotated, annotation: original.annotation), AnnotatedFeature(feature: scaled, annotation: original.annotation), ] } }
-
20:23 - Tabular regressor
import CreateMLComponents import Foundation import TabularData struct TabularRegressor { static let dataURL = URL(fileURLWithPath: "~/Downloads/avocado.csv") static let parametersURL = URL(fileURLWithPath: "~/Downloads/parameters.pkg") static let priceColumnID = ColumnID("price", Double.self) static var task: some SupervisedTabularEstimator { let numeric = ColumnSelector( columns: ["volume"], estimator: OptionalUnwrapper() .appending(StandardScaler<Double>()) ) let regression = BoostedTreeRegressor<String>( annotationColumnName: priceColumnID.name, featureColumnNames: ["type", "region", "volume"] ) return numeric.appending(regression) } static func train() async throws -> some TabularTransformer { let dataFrame = try DataFrame(contentsOfCSVFile: dataURL) let (training, validation) = dataFrame.randomSplit(by: 0.8) let transformer = try await task.fitted(to: DataFrame(training), validateOn: DataFrame(validation)) { event in guard let validationError = event.metrics[.validationError] as? Double else { return } print("Validation error: \(validationError)") } try task.write(transformer, to: parametersURL) return transformer } static func predict( type: String, region: String, volume: Double ) async throws -> Double { let model = try task.read(from: parametersURL) let dataFrame: DataFrame = [ "type": [type], "region": [region], "volume": [volume] ] let result = try await model(dataFrame) return result[priceColumnID][0]! } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。