美文网首页
Core ML中的自定义层(译)

Core ML中的自定义层(译)

作者: 梁间 | 来源:发表于2018-12-24 20:45 被阅读0次

    原文:Custom Layers in Core ML

    译者注:这篇文章从如何在Keras中建立自定义层,讲到如何建立、训练Keras模型,如何转换为Core ML模型,以及如何在app中使用自定义层,如何使用Accelerate加速代码,如何使用GPU加速代码。内容非常全面,学习Core ML自定义层不可错过的优秀文章,译者笔力有限,英文水平过得去的可以看英文原文。

    苹果新的Core ML框架使得在iOS app中添加机器学习模型变得很容易。但有一个很大的局限是Core ML只支持有限的神经网络层类型。更糟糕的是,作为应用程序开发人员,不可能扩展Core ML的功能。

    好消息:从iOS 11.2开始,Core ML现在支持定制层!在我看来,这使Coew ML更加有用。

    在本文中,我将展示如何将具有自定义层的Keras模型转换为Core ML。

    步骤如下:

    1. 创建具有自定义层的Keras模型
    2. 使用coremltools将Keras转换为mlmodel
    3. 为自定义层实现Swift类
    4. 将Core ML模型放到iOS应用程序中并运行它
    5. 利润!

    像往常一样,您可以在GitHub上找到源代码。运行环境为Python 2、TensorFlow、Keras、coremltools和Xcode 9。

    注意:我选择Keras作为这个博客帖子,因为它易于使用和解释,但是使定制层以相同的方式工作,而不管您使用什么工具来训练模型。

    Swish!

    让我们实现一个名为Swish的激活函数(activation function),演示如何创建自定义层。

    “等等…”,你可能会说,“我以为这篇文章是关于自定义层的,而不是定制激活函数?”哦,这要看你怎么看待事物。

    您可以认为激活函数是非线性的应用于层的输出,但是您也可以将激活函数视为它们自己的层。在许多深度学习软件包中,包括Keras,激活功能实际上被看作独立的层。

    Core ML只支持一组固定的激活函数,比如标准的ReLU和sigmoid激活。(完整的列表在NeuralNetwork.proto中,它是mlmodel规范的一部分。)

    但时常有人发明一种奇特的新激活函数,如果你想在Core ML模型中使用它,那你只能编写自己的自定义层。这就是我们要做的。

    我们将实现Swish激活函数。公式是:

    swish(x) = x * sigmoid(beta * x)
    

    其中sigmoid是著名的logistic sigmoid函数1/(1+exp(-x))。因此,Swish的完整定义是:

    swish(x) = x / (1 + exp(-beta * x))
    

    这里,x是输入值,beta可以是常数或可训练的参数。不同的beta会改变Swish函数的曲线。

    用beta=1.0进行刷新看起来是这样的:


    是不是很像无处不在的ReLU激活函数,不同的是Swish在左手边是平滑的,而不是在x=0处进行突然改变(这给Swish提供了一个不错的、干净的导数)。

    beta值越大,Swish看起来越像ReLU。beta越接近0,Swish看起来越像直线。(如果你好奇,试试看。)

    显然,这种Swish激活使您的神经网络比ReLU更容易学习,并且也给出了更好的结果。您可以在“Searching for Activation Functions”一文中阅读更多关于Swish的信息。

    为了简化示例,最初我们将使用beta=1,但是稍后我们将使用beta作为一个可学习的参数。

    Keras模型

    撰写本文时,Swish还不够流行,没有进入Keras。所以我们还要编写一个定制的Keras层。它很容易实现:

    from keras import backend as K
    
    def swish(x):
        return K.sigmoid(x) * x
    

    这里,x是一个张量,我们简单地把它和K.sigmoid函数的结果相乘。K是对Keras后端的引用,后者通常是TensorFlow。现在我将beta排除在代码之外(这与beta=1相同)。

    为了在Keras模型中使用这个自定义激活函数,我们可以编写以下代码:

    import keras
    from keras.models import *
    from keras.layers import *
    
    def create_model():
        inp = Input(shape=(256, 256, 3))
        x = Conv2D(6, (3, 3), padding="same")(inp)
        x = Lambda(swish)(x)                       # look here!
        x = GlobalAveragePooling2D()(x)
        x = Dense(10, activation="softmax")(x)
        return Model(inp, x)
    

    这只是一个带有一些基本层类型的简单模型。重要的部分是x=Lambda(swish)(x)。这在前一层的输出上调用新的swish函数,该层在本例中是卷积层。

    Lambda层是一个特殊的Keras类,它非常适合于只使用函数或lambda表达式(类似于Swift中的闭包)编写快速但不完善的层。Lambda对于没有状态的层很有用,在Keras模型中通常用于进行基本计算。

    注意:您还可以通过创建Layer子类在Keras中创建更高级的自定义层,稍后我们将看到一个示例。

    激活呢?

    如果您是Keras用户,那么您可能习惯于为这样的层指定激活函数:

    x = Conv2D(..., activation="swish")(x)
    

    或者像这样

    x = Conv2D(6, (3, 3), padding="same")(inp)
    x = Activation(swish)(x)
    

    在Keras中我们通常使用Activation层,而不是使用Lambda作为激活函数。

    不幸的是,0.7版的coremltools不能转换自定义激活,只能转换自定义层。如果试图转换使用Activation(...),而它不是Keras内置激活函数之一,coremltools将给出错误消息:

    RuntimeError: Unsupported option activation=swish in layer Activation
    

    解决方法是使用Lambda层替代Activation。

    特别指出来,因为这是一个稍微令人讨厌的限制。我们可以使用自定义层来实现不支持的激活函数,但是模型编码中不能使用Activation(func)或activation="func"。在使用coremltools Keras转换器之前,必须先用Lambda层替换它们。

    注意:或者,您可以使用coremltools的NeuralNetworkBuilder类从头创建模型。这样,您不受Keras转换器理解的限制,但是也不太方便。

    在我们将这个模型转换为Core ML之前,应该先给它一些权重。

    “训练”模型

    在这篇文章的源代码中,我创建了Keras模型,它写在转换脚本_lambda.py之中。在实践中,您可能有不同的用于训练和转换的脚本,但是对于这个示例,我们不会烦恼训练。(不管怎么说,这是个粗糙的模型。)

    首先,我们使用您刚才看到的create_model()函数创建模型的实例:

    model = create_model()
    model.compile(loss="categorical_crossentropy", optimizer="Adam", 
                  metrics=["accuracy"])
    model.summary()
    

    我们不训练模型,而是给它随机加权:

    import numpy as np
    
    W = model.get_weights()
    np.random.seed(12345)
    for i in range(len(W)):
        W[i] = np.random.randn(*(W[i].shape)) * 2 - 1
    model.set_weights(W)
    

    通常训练过程会填补这些权重,但是为了这个博客的目的,我们只是假装。

    为了获得一些输出,我们在输入图像上测试模型:


    这是一个256×256像素的RGB图像。你可以使用任何你想要的图像,但是我的猫自愿做这份工作。以下是加载图像、将其加入神经网络并输出结果的代码:

    from keras.preprocessing.image import load_img, img_to_array
    
    img = load_img("floortje.png", target_size=(256, 256))
    img = np.expand_dims(img_to_array(img), 0)
    pred = model.predict(img)
    
    print("Predicted output:")
    print(pred)
    

    预测输出是:

    [[  2.24579312e-02   6.99496120e-02   7.55519234e-03   1.38940173e-03
        5.51432837e-03   8.00364137e-01   1.42883752e-02   3.57461395e-04
        5.40433871e-03   7.27192238e-02]]
    

    这些数字没有任何意义……毕竟,这只是一个非常基本的模型,我们没有对其进行训练。没关系,在这个阶段,我们只是想得到一些有关输入图像的输出。

    在将模型转换为Core ML之后,我们希望iOS应用程序为相同的输入图像提供完全相同的输出。如果做到了,可以证明转换是正确的,我们的自定义层可以正常工作。

    注:有可能你的电脑会有不同的输出。不用担心,只要每次运行脚本时得到相同的数字就好。

    转换模型

    现在让我们将这个非常基本的模型转换为Core ML mlmodel文件。如果一切顺利,生成的mlmodel文件将不仅包含标准Keras层,而且还包含我们的自定义lambda层。然后,我们将编写这个层的Swift实现,以便可以在iOS上运行模型。

    注意:我使用coremltools 0.7版本进行转换。随着软件的不断改进,在您阅读本文时,它的行为可能会稍有不同。有关使用和安装说明,请查看文档。

    将Keras模型转换为Core ML非常简单,只需调用coremltools.converters.keras..():

    import coremltools
    
    coreml_model = coremltools.converters.keras.convert(
        model,
        input_names="image",
        image_input_names="image",
        output_names="output",
        add_custom_layers=True,
        custom_conversion_functions={ "Lambda": convert_lambda })
    

    这引用了我们刚刚创建的模型,以及模型的输入和输出的名称。

    对于我们的目的来说特别重要的是add_custom_layers=True,它告诉转换器检测自定义层。但是转换器还需要知道一旦找到这样的层该做什么——这就是custom_conversion_functions的用途。

    custom_conversion_functions参数接受一个字典,该字典将层类型的名称映射为所谓的“转换函数”。我们还需要编写这个函数:

    from coremltools.proto import NeuralNetwork_pb2
    
    def convert_lambda(layer):
        # Only convert this Lambda layer if it is for our swish function.
        if layer.function == swish:
            params = NeuralNetwork_pb2.CustomLayerParams()
    
            # The name of the Swift or Obj-C class that implements this layer.
            params.className = "Swish"
    
            # The desciption is shown in Xcode's mlmodel viewer.
            params.description = "A fancy new activation function"
    
            return params
        else:
            return None
    

    此函数接收Keras层对象,并应返回CustomLayerParams对象。CustomLayerParams对象告诉Core ML如何处理这个层。

    CustomLayerParams在NeuralNetwork.proto中定义。它具有以下字段:

    • className
    • description
    • parameters
    • weights

    至少你应该填写className字段。这是在iOS上实现这一层的Swift或Objective-C类的名称。我选择简单地将这个类命名为Swish。

    如果不填写className,Xcode将显示以下错误,并且不能使用模型:


    其他字段是可选的。description显示在Xcode的mlmodel查看器中,parameters是一个带有附加定制选项的字典,weights包含层的学习参数(如果有的话)。

    现在我们有了转换函数,我们可以使用coremltools.converters.keras.convert() 运行Keras转换器,它将为模型中遇到的任何Lambda层调用convert_lambda()。

    注意:convert_lambda()函数将针对网络中的每个Lambda层调用,因此如果具有具有不同函数的多个Lambda层,则需要在它们之间消除歧义。这就是为什么我们首先执行layer.function == swish的原因。

    转换过程中的最后一步是填充模型的元数据并保存mlmodel文件:

    coreml_model.author = "AuthorMcAuthorName"
    coreml_model.license = "Public Domain"
    coreml_model.short_description = "Playing with custom Core ML layers"
    
    coreml_model.input_description["image"] = "Input image"
    coreml_model.output_description["output"] = "The predictions"
    
    coreml_model.save("NeuralMcNeuralNet.mlmodel")
    

    当您运行转换脚本时,coremltools将打印出它所找到的所有层并转换:

    0 : input_1, <keras.engine.topology.InputLayer object at 0x1169995d0>
    1 : conv2d_1, <keras.layers.convolutional.Conv2D object at 0x10a50ae10>
    2 : lambda_1, <keras.layers.core.Lambda object at 0x1169b0650>
    3 : global_average_pooling2d_1, <keras.layers.pooling.GlobalAveragePooling2D object at 0x1169d7110>
    4 : dense_1, <keras.layers.core.Dense object at 0x116657f50>
    5 : dense_1__activation__, <keras.layers.core.Activation object at 0x116b56350>
    

    名为lambda_1的层是具有swish激活功能的层。转换没有给出任何错误,这意味着我们已经准备好将.mlmodel文件放入应用程序中!

    注意:您不是必须使用转换函数。另一种填写自定义层详细信息的方法是传递custom_conversion_functions={}。(省略它就会出错,但是空字典也可以。)然后调用coremltools.converters.keras.convert()。这将在模型中包括您的自定义层,但不会给它任何属性。然后,执行以下操作:

    layer = coreml_model._spec.neuralNetwork.layers[1]
    layer.custom.className = "Swish"
    

    这将获取层并直接更改其属性。无论哪种方式都可以,只要在保存mlmodel文件时已经填充了className。

    将模型放入app

    在应用程序中添加Core ML模型非常简单:只需将mlmodel文件拖放到Xcode项目中即可。

    Xcode mlmodel查看器展示转换后的模型如下所示:


    它像往常一样显示输入和输出,并且在新的Dependencies部分列出自定义层以及哪些类实现它们。

    我已经创建了一个演示应用程序,它使用Vision框架运行模型,并与Python脚本使用的相同图片。它将预测数字打印到Xcode输出窗格。回想一下,这个模型实际上没有计算任何有意义的内容——因为我们没有训练它——但是它应该给出与Python相同的结果。

    在将mlmodel文件添加到应用程序之后,您需要提供一个实现自定义层的Swift或Objective-C类。如果没有,那么一旦尝试实例化MLModel对象,您将得到以下错误:

    [coreml] A Core ML custom neural network layer requires an implementation 
    named 'Swish' which was not found in the global namespace.
    [coreml] Error creating Core ML custom layer implementation from factory 
    for layer "Swish".
    [coreml] Error in adding network -1.
    [coreml] MLModelAsset: load failed with error Error Domain=com.apple.CoreML 
    Code=0 "Error in declaring network."
    

    Core ML试图实例化一个名为Swish的类,因为我们告诉转换脚本类名是这个,但是它找不到这个类。所以我们需要在Swish.swift中实现它:

    import Foundation
    import CoreML
    import Accelerate
    
    @objc(Swish) class Swish: NSObject, MLCustomLayer {
      required init(parameters: [String : Any]) throws {
        print(#function, parameters)
        super.init()
      }
    
      func setWeightData(_ weights: [Data]) throws {
        print(#function, weights)
      }
    
      func outputShapes(forInputShapes inputShapes: [[NSNumber]]) throws 
           -> [[NSNumber]] {
        print(#function, inputShapes)
        return inputShapes
      }
    
      func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
        print(#function, inputs.count, outputs.count)
      }
    }
    

    这是你需要做的最低限度的工作。该类需要扩展NSObject,使用@objc()修饰符使其对Objective-C运行时可见,并实现MLCustomLayer协议。该协议由四个必需的方法和一个可选的方法组成:

    • init(parameters) 构造函数。参数是一个字典,它为该层提供了附加的配置选项(稍后将详细介绍)。
    • setWeightData() 为具有可训练权重的层赋值(稍后将详细介绍)。
    • outputShapes(forInputShapes) 这决定了层如何修改输入数据的大小。我们的Swish激活函数不会改变层的大小,因此我们只是返回输入形状。
    • evaluate(inputs, outputs) 执行实际的计算-这是魔术发生的地方!此方法是必需的,当模型在CPU上运行时将调用此方法。
    • encode(commandBuffer, inputs, outputs) 此方法是可选的。它也实现了在GPU上的计算。

    所以有两种不同的函数提供层的实现:一个用于CPU,一个用于GPU。CPU方法是必需的——您必须始终至少提供层的CPU版本。GPU方法是可选的,但是推荐使用。

    目前,Swish类没有做任何事情,但它足以在设备上(或在模拟器中)实际运行模型。给定256×256像素输入图像,Swish.swift打印中的打印语句输出如下:

    init(parameters:) ["engineName": Swish]
    
    setWeightData []
    
    outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
    outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
    outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
    outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
    outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
    
    evaluate(inputs:outputs:) 1 1
    

    显然,首先调用init(parameters),它的参数字典包含一个项目“engineName”,其值是Swish。很快我将向您展示如何将自己的参数添加到这个字典中。

    其次调用setWeightData(),这将得到一个空数组。那是因为我们没有在这个层中加入任何可学习的权重(稍后我们将讨论)。

    然后一行多次调用outputShapes(forInputShapes:)。我不确定为什么它被如此频繁地调用,但是没什么大不了的,因为无论如何我们没有用那种方法做很多工作。

    注意,这些形状是以五个维度给出的。这使用了以下约定:

    [ sequence, batch, channel, height, width ]
    

    我们的Swish层接收一个6个通道的256×256像素的图像。(为什么有6个频道?回想一下模型定义,这个Swish层应用于Conv2D层的输出,而卷积层有6个滤波器。)

    最后,调用evaluate(inputs, outputs)来执行该层的计算。它接受一个MLMultiArray对象数组作为输入,并生成一个新MLMultiArray对象数组作为输出(这些输出对象已经被分配,所以很方便——我们只需要填充它们)。

    它获得MLMultiArray对象数组的原因是某些类型的层可以接受多个输入或产生多个输出。在上面的调试输出中可以看到,我们只得到了其中的一个,因为我们的模型非常简单。

    好的,让我们真正实现这个Swish激活函数:

    func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
      for i in 0..<inputs.count {
        let input = inputs[I]
        let output = outputs[I]
    
        assert(input.dataType == .float32)
        assert(output.dataType == .float32)
        assert(input.shape == output.shape)
    
        for j in 0..<input.count {
          let x = input[j].floatValue
          let y = x / (1 + exp(-x))        // look familiar?
          output[j] = NSNumber(value: y)
        }
      }  
    }
    

    与大多数激活函数一样,Swish是按元素进行的操作,因此它循环遍历输入数组中的所有值,计算x/(1+exp(-x))并将结果写入输出数组。

    重点:MLMultiArray支持不同的数据类型。在这种情况下,我们假设数据类型是.float32,即单精度浮点数,这对我们的模型是正确的。但是,MLMultiArray也可以支持int32和double,因此需要确保层类能够处理Core ML抛出的任何数据类型。(这里我使用了一个简单的断言来使应用程序崩溃,但是最好抛出一个错误并让Core ML进行适当的清理。)

    如果我们现在运行应用程序,预测的输出是:

    [0.02245793305337429, 0.06994961202144623, 0.007555192802101374, 
     0.00138940173201263, 0.005514328368008137, 0.8003641366958618,
     0.01428837608546019, 0.0003574613947421312, 0.005404338706284761,
     0.07271922379732132]
    

    这与Keras输出完全匹配!

    那么现在我们完成了吗?是的,如果你不介意代码变慢的话。我们可以加快一点(实际上很多)。

    使用Accelerate加速代码

    evaluate(inputs, outputs)函数是在CPU上执行的,我们使用一个简单的for循环。这对于实现和调试层算法的第一个版本很有用,但是它运行速度不快。

    更糟糕的是,当我们以这种方式使用MLMultiArray时,我们访问的每个值都会得到NSNumber对象。直接访问MLMultiArray内存中的浮点值要快得多。

    我们将使用向量化的CPU函数代替for循环。幸运的是,Accelerate框架使此操作变得简单——但是我们必须使用指针,这使得代码的可读性稍微降低。

    func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
      for i in 0..<inputs.count {
        let input = inputs[i]
        let output = outputs[i]
    
        let count = input.count
        let iptr = UnsafeMutablePointer<Float>(OpaquePointer(input.dataPointer))
        let optr = UnsafeMutablePointer<Float>(OpaquePointer(output.dataPointer))
    
        // output = -input
        vDSP_vneg(iptr, 1, optr, 1, vDSP_Length(count))
    
        // output = exp(-input)
        var countAsInt32 = Int32(count)
        vvexpf(optr, optr, &countAsInt32)
    
        // output = 1 + exp(-input)
        var one: Float = 1
        vDSP_vsadd(optr, 1, &one, optr, 1, vDSP_Length(count))
    
        // output = x / (1 + exp(-input))
        vvdivf(optr, iptr, optr, &countAsInt32)
      }
    }
    

    对于for循环,我们将公式output=input/(1+exp(-input))应用于每个数组值。但是在这里,我们将这个公式分成单独的步骤,并且同时将每个步骤应用于所有数组值。

    首先,我们使用vDSP_vneg()一次性计算输入数组中所有值的-input。中间结果被写入输出数组。然后,使用vvexpf()一次性对数组中的每个值进行指数化。我们使用vDSP_vsadd()对每个值添加1,最后执行vvdivf()给出最终结果的除法。

    结果和前面完全一样,但是它是通过利用CPU的SIMD指令集以更有效的方式完成的。如果您要编写自己的自定义层,我建议您尽可能多地使用Accelerate框架(这也是Core ML内部为其自己的层使用的)。

    即使启用了优化,for循环版本在iPhone 7上也花费了0.18秒。加速版本花费了0.0012秒。快150倍!

    您可以在repo中的CPU only文件夹中找到此代码。您可以在设备上或在模拟器中运行此应用程序。试试看!

    更快的速度:在GPU上运行

    与其他机器学习框架相比,使用Core ML的优势在于,Core ML可以在CPU上或是在GPU上运行模型,而不需要您做任何额外的工作。对于大型神经网络,它通常尝试使用GPU,但是在没有非常强大的GPU的较老设备上,它将回到使用CPU。

    事实证明,Core ML也可以混合匹配。如果您的自定义层只有一个CPU实现(就像我们刚刚做的那样),那么它仍然会在GPU上运行其他层,切换到用于自定义层的CPU,然后切换回GPU用于神经网络的其余部分。

    因此,在自定义层中只使用CPU实现不会降低模型的其余部分的性能。然而,为什么不充分利用GPU呢?

    对于Swish激活功能,GPU实现非常简单。这是Metal shader代码:

    #include <metal_stdlib>
    using namespace metal;
    
    kernel void swish(
      texture2d_array<half, access::read> inTexture [[texture(0)]],
      texture2d_array<half, access::write> outTexture [[texture(1)]],
      ushort3 gid [[thread_position_in_grid]])
    {
      if (gid.x >= outTexture.get_width() || 
          gid.y >= outTexture.get_height()) {
        return;
      }
    
      const float4 x = float4(inTexture.read(gid.xy, gid.z));
      const float4 y = x / (1.0f + exp(-x));                  // recognize this?
      outTexture.write(half4(y), gid.xy, gid.z);
    }
    

    我们将对输入数组中的每个数据元素调用这个计算内核一次。因为Swish是按元素进行的操作,所以我们可以在这里简单地编写熟悉的公式x/(1.0f+exp(-x))。

    与以前使用MLMultiArray不同,这里的数据放在Metal纹理对象中。MLMultiArray的数据类型是32位浮点数,但这里我们实际处理的是16位浮点数或者half。请注意,即使纹理类型是half,我们也要使用浮点值进行实际计算,否则会损失太多的精度,而答案将是完全错误的。

    回想一下,数据有6个通道深。这就是为什么计算内核使用texture_array,它是由多个“片”组成的Metal纹理。在我们的演示应用程序中,纹理数组只包含2个切片(总共8个通道,所以最后两个通道被忽略),但是上面的计算内核将处理任意数量的切片/通道。

    要使用这个GPU计算内核,我们必须向Swift类添加一些代码:

    @objc(Swish) class Swish: NSObject, MLCustomLayer {
      let swishPipeline: MTLComputePipelineState
    
      required init(parameters: [String : Any]) throws {
        // Create the Metal compute kernels.
        let device = MTLCreateSystemDefaultDevice()!
        let library = device.makeDefaultLibrary()!
        let swishFunction = library.makeFunction(name: "swish")!
        swishPipeline = try! device.makeComputePipelineState(
                                        function: swishFunction)
        super.init()
      }
    

    这是将Metal swish内核函数加载到MTLComputePipelineState对象中的样式化代码。我们还需要添加以下方法:

    func encode(commandBuffer: MTLCommandBuffer, 
                inputs: [MTLTexture], outputs: [MTLTexture]) throws {
      if let encoder = commandBuffer.makeComputeCommandEncoder() {
        for i in 0..<inputs.count {
          encoder.setTexture(inputs[i], index: 0)
          encoder.setTexture(outputs[i], index: 1)
          encoder.dispatch(pipeline: swishPipeline, texture: inputs[I])
          encoder.endEncoding()
        }
      }
    }
    

    如果MLCustomLayer类中存在此方法,那么该层将在GPU上运行。在这个方法中,您将“compute pipeline state”编码为MTLCommandBuffer。多半又是样式化代码。encoder.dispatch()方法确保对输入纹理中的每个通道中的每个像素调用一次计算内核。有关详细信息,请参阅源代码

    现在,当您运行应用程序(在一个相当新的设备上)时,encode(commandBuffer, inputs, outputs) 函数被调用,而不是evaluate(inputs, outputs),GPU有幸计算swish激活函数。

    您应该得到与以前相同的输出。这很有意义——您希望自定义层的CPU和GPU版本计算完全相同的答案!

    注意:您不能在模拟器上运行Metal应用程序,所以这个版本的应用程序只能在真机上运行。一部iPhone 6或者更好的就行了。如果设备太旧,Core ML仍将使用CPU而不是GPU运行模型。

    进一步:如果您以前使用过MPSCNN,那么请注意,Core ML使用GPU有一些不同。对于MPSCNN,您处理的是MPSImage对象,但是Core ML为您提供了MTLTexture。像素格式似乎是.rgba16Float,这与MPSImage的.float16通道格式相对应。

    使用MPSCNN,具有4个通道或更少通道的图像使用type2D纹理,超过4个通道的图像使用type2DArray纹理。这意味着,对于MPSCNN,您可能必须编写两个版本的计算内核:一个采用texture对象,另一个采用texture_array对象。据我所知,对于Core ML,纹理总是type2DArray,即使有4个通道或更少,因此只需要编写一个版本的计算内核。

    参数和权重

    现在我们有了一个带有相应的Swift实现的自定义层。不错,但这只是一个非常简单的层。

    我们还可以向该层添加参数和权重。“参数”在此上下文中表示可配置设置,例如卷积层的内核大小和在该层周围添加的填充量。

    在我们的例子中,我们可以将beta设置为一个参数。还记得beta吗?beta的值决定了Swish函数有多陡峭。到目前为止,我们已经实现的Swish版本是:

    swish(x) = x * sigmoid(x)
    

    但是记住完整的定义是这样

    swish(x) = x * sigmoid(beta * x)
    

    beta是一个数字。到目前为止,我们假设beta总是1.0,但是我们可以把它配置为一个参数,或者甚至让模型在训练时学习beta的值,在这种情况下,我们将它看作一个权重。

    要向自定义层添加参数或权重,请按以下方式更改转换函数:

    def convert_lambda(layer):
        if layer.function == swish:
            params = NeuralNetwork_pb2.CustomLayerParams()
    
            . . .
    
            # Set configuration parameters
            params.parameters["someNumber"].intValue = 100
            params.parameters["someString"].stringValue = "Hello, world!"
            
            # Add some random weights
            my_weights = params.weights.add()
            my_weights.floatValue.extend(np.random.randn(10).astype(float))
    
            return params
        else:
            return None
    

    现在,当您运行该应用程序时,Swish类将在init(parameters)方法中接收带有这些整数和字符串值的参数字典,通过setWeightData()中的Data对象接收权重。

    让我们添加beta作为参数。为此,我们应该远离Lambda层,并将Swish激活函数转换为适当的Keras层对象。Lambda层非常适合于简单的计算,但是现在我们希望给Swish层一些状态(beta的值),创建一个Layer子类是更好的方法。

    在Python脚本 convert_subclass.py中,我们现在将Swish函数定义为Layer的子类:

    from keras.engine.topology import Layer
    
    class Swish(Layer):
        def __init__(self, beta=1., **kwargs):
            super(Swish, self).__init__(**kwargs)
            self.beta = beta
    
        def build(self, input_shape):
            super(Swish, self).build(input_shape)
    
        def call(self, x):
            return K.sigmoid(self.beta * x) * x
    
        def compute_output_shape(self, input_shape):
            return input_shape
    

    注意,这如何在构造函数中采用beta值。Keras中的call()函数等价于swift中的evaluate(inputs, outputs)。在call()函数中,我们计算Swish公式——这次包含beta。

    新的模型定义如下所示:

    def create_model():
        inp = Input(shape=(256, 256, 3))
        x = Conv2D(6, (3, 3), padding="same")(inp)
        x = Swish(beta=0.01)(x)                     # look here!
        x = GlobalAveragePooling2D()(x)
        x = Dense(10, activation="softmax")(x)
        return Model(inp, x)
    

    beta的值是一个超参数,它是在模型构建时定义的。这里我选择使用beta=0.01,这样我们就会得到与以前不同的预测。

    顺便说一下,这里是Swish在beta 0.01中的样子,它几乎是一条直线:


    为了将这个层转换为Core ML,我们需要为它建一个转换函数:

    def convert_swish(layer):
        params = NeuralNetwork_pb2.CustomLayerParams()
        params.className = "Swish"
        params.description = "A fancy new activation function"
    
        # Add the hyperparameter to the dictionary
        params.parameters["beta"].doubleValue = layer.beta
    
        return params
    

    这与以前非常相似,只是现在我们从层(这是我们刚刚创建的新Swish类的实例)读取beta属性,并将其粘贴到CustomLayerParams的参数字典中。注意,这个字典不支持32位浮点,只支持64位双精度浮点(以及整数和布尔值),所以我们使用.doubleValue。

    当我们调用Keras转换器时,我们必须告诉它这个新的转换函数:

    coreml_model = coremltools.converters.keras.convert(
        model,
        input_names="image",
        image_input_names="image",
        output_names="output",
        add_custom_layers=True,
        custom_conversion_functions={ "Swish": convert_swish })
    

    这一切都非常类似于我们之前所做的,除了现在Swish不是包装在Lambda对象中的基本Python函数,而是从Keras Layer基类派生的一个成熟的类。

    在iOS方面,我们需要调整Swish.swift以从参数字典中读出这个“beta”值并将其应用于计算。

    @objc(Swish) class Swish: NSObject, MLCustomLayer {
      let beta: Float
    
      required init(parameters: [String : Any]) throws {
        if let beta = parameters["beta"] as? Float {
          self.beta = beta
        } else {
          self.beta = 1
        }
        ...
      }
    

    在evaluate(inputs, outputs) 时,我们现在用self.beta乘以输入。

    同样,对于Metal compute shader,在encode(commandBuffer, inputs, outputs)中,我们可以将self.beta传递到计算内核中,如下所示:

    var beta = self.beta
    encoder.setBytes(&beta, length: MemoryLayout<Float>.size, index: 0)
    

    然后在Metak内核中:

    kernel void swish(
      texture2d_array<half, access::read> inTexture [[texture(0)]],
      texture2d_array<half, access::write> outTexture [[texture(1)]],
      constant float& beta [[buffer(0)]],
      ushort3 gid [[thread_position_in_grid]])
    {
      ...
      const float4 y = x / (1.0f + exp(-x * beta));
      ...
    }
    

    请参阅源代码以获得完整的更改。我希望解释的足够清楚,使您能很容易的配置参数添加到定制层中。

    注意:当我运行这个新版本的iOS应用程序时,预测结果与Keras的结果并不100%匹配。当Core ML使用GPU时,这种不匹配的情况很常见。卷积层在GPU上运行,带有16位浮点数,这降低了精度,而Keras对一切都使用32位浮点数。所以您一定会看到来自iOS模型和来自原始Keras模型的预测之间的差异。只要差别很小(大约1e-3或更小)就可以接受。

    可学习权重

    最后一件事我想告诉你。机器学习的全部意义在于学习东西,所以对于许多定制层,您希望能够赋予它们可学习的权重。因此,让我们再一次改变Swish层的实现以使beta可以学习。这让模型学习激活函数的最佳形状是什么。

    Swish层仍然是Layer的一个子类,但是这次我们通过add_weight()函数来赋予它一个可学习权重:

    class LearnableSwish(Layer):
        def __init__(self, **kwargs):
            super(LearnableSwish, self).__init__(**kwargs)
    
        def build(self, input_shape):
            self.beta = self.add_weight(
                    name="beta", 
                    shape=(input_shape[3], ),
                    initializer=keras.initializers.Constant(value=1),
                    trainable=True)
            super(LearnableSwish, self).build(input_shape)
    
        def call(self, x):
            return K.sigmoid(self.beta * x) * x
    
        def compute_output_shape(self, input_shape):
            return input_shape
    

    我们将为输入数据中的每个通道创建可学习的权重,而不是单个beta值,这就是为什么我们使用shape=(input_shape[3], )。在该示例中,因为来自前一个Conv2D层的输出有6个通道,所以该层将学习6个不同的beta值。beta的初始值为1,这似乎是一个合理的缺省值。

    现在,当您调用model.fit(...)来训练模型时,它将学习每个通道的最佳beta值。

    在转换函数中,我们必须执行以下操作以将这些学习到的权重放入mlmodel文件中:

    def convert_learnable_swish(layer):
        params = NeuralNetwork_pb2.CustomLayerParams()
        . . .
        
        beta_weights = params.weights.add()
        beta_weights.floatValue.extend(layer.get_weights()[0].astype(float))
        
        return params
    

    以上是Keras中所需要做的所有工作。

    在运行iOS应用程序时,您将注意到setWeightData() 现在接收一个包含24字节的Data对象。就是6通道乘以每个浮点数的4字节。

    使用Swish.swift层代码从这个权重数组读取beta并在计算中使用它,这相当简单。与以前的主要区别是,我们知道,在数据中有许多不同的beta值。我将把这个留给读者作为练习。

    相关文章

      网友评论

          本文标题:Core ML中的自定义层(译)

          本文链接:https://www.haomeiwen.com/subject/ytqtlqtx.html