Core ML中的自定義層(譯)

原文:Custom Layers in Core ML

譯者注:這篇文章從如何在Keras中建立自定義層,講到如何建立督弓、訓(xùn)練Keras模型愚隧,如何轉(zhuǎn)換為Core ML模型,以及如何在app中使用自定義層睹耐,如何使用Accelerate加速代碼硝训,如何使用GPU加速代碼窖梁。內(nèi)容非常全面,學(xué)習(xí)Core ML自定義層不可錯(cuò)過(guò)的優(yōu)秀文章假哎,譯者筆力有限舵抹,英文水平過(guò)得去的可以看英文原文扇救。

蘋(píng)果新的Core ML框架使得在iOS app中添加機(jī)器學(xué)習(xí)模型變得很容易迅腔。但有一個(gè)很大的局限是Core ML只支持有限的神經(jīng)網(wǎng)絡(luò)層類(lèi)型。更糟糕的是掺出,作為應(yīng)用程序開(kāi)發(fā)人員汤锨,不可能擴(kuò)展Core ML的功能。

好消息:從iOS 11.2開(kāi)始柬泽,Core ML現(xiàn)在支持定制層锨并!在我看來(lái),這使Coew ML更加有用包警。

在本文中害晦,我將展示如何將具有自定義層的Keras模型轉(zhuǎn)換為Core ML苟呐。

步驟如下:

  1. 創(chuàng)建具有自定義層的Keras模型
  2. 使用coremltools將Keras轉(zhuǎn)換為mlmodel
  3. 為自定義層實(shí)現(xiàn)Swift類(lèi)
  4. 將Core ML模型放到iOS應(yīng)用程序中并運(yùn)行它
  5. 利潤(rùn)!

像往常一樣,您可以在GitHub上找到源代碼粱挡。運(yùn)行環(huán)境為Python 2询筏、TensorFlow、Keras踱讨、coremltools和Xcode 9痹筛。

注意:我選擇Keras作為這個(gè)博客帖子,因?yàn)樗子谑褂煤徒忉專(zhuān)鞘苟ㄖ茖右韵嗤姆绞焦ぷ髯淘纾还苣褂檬裁垂ぞ邅?lái)訓(xùn)練模型。

Swish!

讓我們實(shí)現(xiàn)一個(gè)名為Swish的激活函數(shù)(activation function)夕土,演示如何創(chuàng)建自定義層馆衔。

“等等…”,你可能會(huì)說(shuō)怨绣,“我以為這篇文章是關(guān)于自定義層的角溃,而不是定制激活函數(shù)篮撑?”哦减细,這要看你怎么看待事物。

您可以認(rèn)為激活函數(shù)是非線性的應(yīng)用于層的輸出赢笨,但是您也可以將激活函數(shù)視為它們自己的層未蝌。在許多深度學(xué)習(xí)軟件包中驮吱,包括Keras,激活功能實(shí)際上被看作獨(dú)立的層萧吠。

Core ML只支持一組固定的激活函數(shù)左冬,比如標(biāo)準(zhǔn)的ReLU和sigmoid激活。(完整的列表在NeuralNetwork.proto中纸型,它是mlmodel規(guī)范的一部分拇砰。)

但時(shí)常有人發(fā)明一種奇特的新激活函數(shù),如果你想在Core ML模型中使用它狰腌,那你只能編寫(xiě)自己的自定義層除破。這就是我們要做的。

我們將實(shí)現(xiàn)Swish激活函數(shù)琼腔。公式是:

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

其中sigmoid是著名的logistic sigmoid函數(shù)1/(1+exp(-x))瑰枫。因此,Swish的完整定義是:

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

這里丹莲,x是輸入值光坝,beta可以是常數(shù)或可訓(xùn)練的參數(shù)。不同的beta會(huì)改變Swish函數(shù)的曲線圾笨。

用beta=1.0進(jìn)行刷新看起來(lái)是這樣的:


是不是很像無(wú)處不在的ReLU激活函數(shù)教馆,不同的是Swish在左手邊是平滑的,而不是在x=0處進(jìn)行突然改變(這給Swish提供了一個(gè)不錯(cuò)的擂达、干凈的導(dǎo)數(shù))土铺。

beta值越大,Swish看起來(lái)越像ReLU板鬓。beta越接近0悲敷,Swish看起來(lái)越像直線。(如果你好奇俭令,試試看后德。)

顯然,這種Swish激活使您的神經(jīng)網(wǎng)絡(luò)比ReLU更容易學(xué)習(xí)抄腔,并且也給出了更好的結(jié)果瓢湃。您可以在“Searching for Activation Functions”一文中閱讀更多關(guān)于Swish的信息。

為了簡(jiǎn)化示例赫蛇,最初我們將使用beta=1绵患,但是稍后我們將使用beta作為一個(gè)可學(xué)習(xí)的參數(shù)。

Keras模型

撰寫(xiě)本文時(shí)悟耘,Swish還不夠流行落蝙,沒(méi)有進(jìn)入Keras。所以我們還要編寫(xiě)一個(gè)定制的Keras層。它很容易實(shí)現(xiàn):

from keras import backend as K

def swish(x):
    return K.sigmoid(x) * x

這里筏勒,x是一個(gè)張量移迫,我們簡(jiǎn)單地把它和K.sigmoid函數(shù)的結(jié)果相乘。K是對(duì)Keras后端的引用管行,后者通常是TensorFlow〕瘢現(xiàn)在我將beta排除在代碼之外(這與beta=1相同)。

為了在Keras模型中使用這個(gè)自定義激活函數(shù)病瞳,我們可以編寫(xiě)以下代碼:

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)

這只是一個(gè)帶有一些基本層類(lèi)型的簡(jiǎn)單模型揽咕。重要的部分是x=Lambda(swish)(x)。這在前一層的輸出上調(diào)用新的swish函數(shù)套菜,該層在本例中是卷積層。

Lambda層是一個(gè)特殊的Keras類(lèi)设易,它非常適合于只使用函數(shù)或lambda表達(dá)式(類(lèi)似于Swift中的閉包)編寫(xiě)快速但不完善的層逗柴。Lambda對(duì)于沒(méi)有狀態(tài)的層很有用,在Keras模型中通常用于進(jìn)行基本計(jì)算顿肺。

注意:您還可以通過(guò)創(chuàng)建Layer子類(lèi)在Keras中創(chuàng)建更高級(jí)的自定義層戏溺,稍后我們將看到一個(gè)示例。

激活呢屠尊?

如果您是Keras用戶(hù)旷祸,那么您可能習(xí)慣于為這樣的層指定激活函數(shù):

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

或者像這樣

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

在Keras中我們通常使用Activation層,而不是使用Lambda作為激活函數(shù)讼昆。

不幸的是托享,0.7版的coremltools不能轉(zhuǎn)換自定義激活,只能轉(zhuǎn)換自定義層浸赫。如果試圖轉(zhuǎn)換使用Activation(...)闰围,而它不是Keras內(nèi)置激活函數(shù)之一,coremltools將給出錯(cuò)誤消息:

RuntimeError: Unsupported option activation=swish in layer Activation

解決方法是使用Lambda層替代Activation既峡。

特別指出來(lái)羡榴,因?yàn)檫@是一個(gè)稍微令人討厭的限制。我們可以使用自定義層來(lái)實(shí)現(xiàn)不支持的激活函數(shù)运敢,但是模型編碼中不能使用Activation(func)或activation="func"校仑。在使用coremltools Keras轉(zhuǎn)換器之前,必須先用Lambda層替換它們传惠。

注意:或者迄沫,您可以使用coremltools的NeuralNetworkBuilder類(lèi)從頭創(chuàng)建模型。這樣涉枫,您不受Keras轉(zhuǎn)換器理解的限制邢滑,但是也不太方便。

在我們將這個(gè)模型轉(zhuǎn)換為Core ML之前,應(yīng)該先給它一些權(quán)重困后。

“訓(xùn)練”模型

在這篇文章的源代碼中乐纸,我創(chuàng)建了Keras模型,它寫(xiě)在轉(zhuǎn)換腳本_lambda.py之中摇予。在實(shí)踐中汽绢,您可能有不同的用于訓(xùn)練和轉(zhuǎn)換的腳本,但是對(duì)于這個(gè)示例侧戴,我們不會(huì)煩惱訓(xùn)練爽雄。(不管怎么說(shuō),這是個(gè)粗糙的模型塑猖。)

首先品姓,我們使用您剛才看到的create_model()函數(shù)創(chuàng)建模型的實(shí)例:

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

我們不訓(xùn)練模型,而是給它隨機(jī)加權(quán):

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)

通常訓(xùn)練過(guò)程會(huì)填補(bǔ)這些權(quán)重蜕猫,但是為了這個(gè)博客的目的寂曹,我們只是假裝。

為了獲得一些輸出回右,我們?cè)谳斎雸D像上測(cè)試模型:


這是一個(gè)256×256像素的RGB圖像隆圆。你可以使用任何你想要的圖像,但是我的貓自愿做這份工作翔烁。以下是加載圖像渺氧、將其加入神經(jīng)網(wǎng)絡(luò)并輸出結(jié)果的代碼:

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)

預(yù)測(cè)輸出是:

[[  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]]

這些數(shù)字沒(méi)有任何意義……畢竟,這只是一個(gè)非车乓伲基本的模型侣背,我們沒(méi)有對(duì)其進(jìn)行訓(xùn)練。沒(méi)關(guān)系哩治,在這個(gè)階段秃踩,我們只是想得到一些有關(guān)輸入圖像的輸出。

在將模型轉(zhuǎn)換為Core ML之后业筏,我們希望iOS應(yīng)用程序?yàn)橄嗤妮斎雸D像提供完全相同的輸出憔杨。如果做到了,可以證明轉(zhuǎn)換是正確的蒜胖,我們的自定義層可以正常工作消别。

注:有可能你的電腦會(huì)有不同的輸出。不用擔(dān)心台谢,只要每次運(yùn)行腳本時(shí)得到相同的數(shù)字就好寻狂。

轉(zhuǎn)換模型

現(xiàn)在讓我們將這個(gè)非常基本的模型轉(zhuǎn)換為Core ML mlmodel文件朋沮。如果一切順利蛇券,生成的mlmodel文件將不僅包含標(biāo)準(zhǔn)Keras層,而且還包含我們的自定義lambda層。然后纠亚,我們將編寫(xiě)這個(gè)層的Swift實(shí)現(xiàn)塘慕,以便可以在iOS上運(yùn)行模型。

注意:我使用coremltools 0.7版本進(jìn)行轉(zhuǎn)換蒂胞。隨著軟件的不斷改進(jìn)图呢,在您閱讀本文時(shí),它的行為可能會(huì)稍有不同骗随。有關(guān)使用和安裝說(shuō)明蛤织,請(qǐng)查看文檔。

將Keras模型轉(zhuǎn)換為Core ML非常簡(jiǎn)單鸿染,只需調(diào)用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 })

這引用了我們剛剛創(chuàng)建的模型指蚜,以及模型的輸入和輸出的名稱(chēng)。

對(duì)于我們的目的來(lái)說(shuō)特別重要的是add_custom_layers=True牡昆,它告訴轉(zhuǎn)換器檢測(cè)自定義層姚炕。但是轉(zhuǎn)換器還需要知道一旦找到這樣的層該做什么——這就是custom_conversion_functions的用途。

custom_conversion_functions參數(shù)接受一個(gè)字典丢烘,該字典將層類(lèi)型的名稱(chēng)映射為所謂的“轉(zhuǎn)換函數(shù)”。我們還需要編寫(xiě)這個(gè)函數(shù):

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

此函數(shù)接收Keras層對(duì)象些椒,并應(yīng)返回CustomLayerParams對(duì)象播瞳。CustomLayerParams對(duì)象告訴Core ML如何處理這個(gè)層。

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

  • className
  • description
  • parameters
  • weights

至少你應(yīng)該填寫(xiě)className字段赢乓。這是在iOS上實(shí)現(xiàn)這一層的Swift或Objective-C類(lèi)的名稱(chēng)。我選擇簡(jiǎn)單地將這個(gè)類(lèi)命名為Swish石窑。

如果不填寫(xiě)className牌芋,Xcode將顯示以下錯(cuò)誤,并且不能使用模型:


其他字段是可選的松逊。description顯示在Xcode的mlmodel查看器中躺屁,parameters是一個(gè)帶有附加定制選項(xiàng)的字典,weights包含層的學(xué)習(xí)參數(shù)(如果有的話)经宏。

現(xiàn)在我們有了轉(zhuǎn)換函數(shù)犀暑,我們可以使用coremltools.converters.keras.convert() 運(yùn)行Keras轉(zhuǎn)換器,它將為模型中遇到的任何Lambda層調(diào)用convert_lambda()烁兰。

注意:convert_lambda()函數(shù)將針對(duì)網(wǎng)絡(luò)中的每個(gè)Lambda層調(diào)用耐亏,因此如果具有具有不同函數(shù)的多個(gè)Lambda層,則需要在它們之間消除歧義沪斟。這就是為什么我們首先執(zhí)行l(wèi)ayer.function == swish的原因广辰。

轉(zhuǎn)換過(guò)程中的最后一步是填充模型的元數(shù)據(jù)并保存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")

當(dāng)您運(yùn)行轉(zhuǎn)換腳本時(shí),coremltools將打印出它所找到的所有層并轉(zhuǎn)換:

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激活功能的層。轉(zhuǎn)換沒(méi)有給出任何錯(cuò)誤择吊,這意味著我們已經(jīng)準(zhǔn)備好將.mlmodel文件放入應(yīng)用程序中李根!

注意:您不是必須使用轉(zhuǎn)換函數(shù)。另一種填寫(xiě)自定義層詳細(xì)信息的方法是傳遞custom_conversion_functions={}干发。(省略它就會(huì)出錯(cuò)朱巨,但是空字典也可以。)然后調(diào)用coremltools.converters.keras.convert()枉长。這將在模型中包括您的自定義層冀续,但不會(huì)給它任何屬性。然后必峰,執(zhí)行以下操作:

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

這將獲取層并直接更改其屬性洪唐。無(wú)論哪種方式都可以,只要在保存mlmodel文件時(shí)已經(jīng)填充了className吼蚁。

將模型放入app

在應(yīng)用程序中添加Core ML模型非常簡(jiǎn)單:只需將mlmodel文件拖放到Xcode項(xiàng)目中即可凭需。

Xcode mlmodel查看器展示轉(zhuǎn)換后的模型如下所示:


它像往常一樣顯示輸入和輸出,并且在新的Dependencies部分列出自定義層以及哪些類(lèi)實(shí)現(xiàn)它們肝匆。

我已經(jīng)創(chuàng)建了一個(gè)演示應(yīng)用程序粒蜈,它使用Vision框架運(yùn)行模型,并與Python腳本使用的相同圖片旗国。它將預(yù)測(cè)數(shù)字打印到Xcode輸出窗格枯怖。回想一下能曾,這個(gè)模型實(shí)際上沒(méi)有計(jì)算任何有意義的內(nèi)容——因?yàn)槲覀儧](méi)有訓(xùn)練它——但是它應(yīng)該給出與Python相同的結(jié)果度硝。

在將mlmodel文件添加到應(yīng)用程序之后,您需要提供一個(gè)實(shí)現(xiàn)自定義層的Swift或Objective-C類(lèi)寿冕。如果沒(méi)有蕊程,那么一旦嘗試實(shí)例化MLModel對(duì)象,您將得到以下錯(cuò)誤:

[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試圖實(shí)例化一個(gè)名為Swish的類(lèi)驼唱,因?yàn)槲覀兏嬖V轉(zhuǎn)換腳本類(lèi)名是這個(gè)藻茂,但是它找不到這個(gè)類(lèi)。所以我們需要在Swish.swift中實(shí)現(xiàn)它:

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)
  }
}

這是你需要做的最低限度的工作曙蒸。該類(lèi)需要擴(kuò)展NSObject捌治,使用@objc()修飾符使其對(duì)Objective-C運(yùn)行時(shí)可見(jiàn),并實(shí)現(xiàn)MLCustomLayer協(xié)議纽窟。該協(xié)議由四個(gè)必需的方法和一個(gè)可選的方法組成:

  • init(parameters) 構(gòu)造函數(shù)肖油。參數(shù)是一個(gè)字典,它為該層提供了附加的配置選項(xiàng)(稍后將詳細(xì)介紹)臂港。
  • setWeightData() 為具有可訓(xùn)練權(quán)重的層賦值(稍后將詳細(xì)介紹)森枪。
  • outputShapes(forInputShapes) 這決定了層如何修改輸入數(shù)據(jù)的大小视搏。我們的Swish激活函數(shù)不會(huì)改變層的大小,因此我們只是返回輸入形狀县袱。
  • evaluate(inputs, outputs) 執(zhí)行實(shí)際的計(jì)算-這是魔術(shù)發(fā)生的地方浑娜!此方法是必需的,當(dāng)模型在CPU上運(yùn)行時(shí)將調(diào)用此方法式散。
  • encode(commandBuffer, inputs, outputs) 此方法是可選的筋遭。它也實(shí)現(xiàn)了在GPU上的計(jì)算。

所以有兩種不同的函數(shù)提供層的實(shí)現(xiàn):一個(gè)用于CPU暴拄,一個(gè)用于GPU漓滔。CPU方法是必需的——您必須始終至少提供層的CPU版本。GPU方法是可選的乖篷,但是推薦使用响驴。

目前,Swish類(lèi)沒(méi)有做任何事情撕蔼,但它足以在設(shè)備上(或在模擬器中)實(shí)際運(yùn)行模型豁鲤。給定256×256像素輸入圖像,Swish.swift打印中的打印語(yǔ)句輸出如下:

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

顯然鲸沮,首先調(diào)用init(parameters)琳骡,它的參數(shù)字典包含一個(gè)項(xiàng)目“engineName”,其值是Swish讼溺。很快我將向您展示如何將自己的參數(shù)添加到這個(gè)字典中日熬。

其次調(diào)用setWeightData(),這將得到一個(gè)空數(shù)組肾胯。那是因?yàn)槲覀儧](méi)有在這個(gè)層中加入任何可學(xué)習(xí)的權(quán)重(稍后我們將討論)。

然后一行多次調(diào)用outputShapes(forInputShapes:)耘纱。我不確定為什么它被如此頻繁地調(diào)用敬肚,但是沒(méi)什么大不了的,因?yàn)闊o(wú)論如何我們沒(méi)有用那種方法做很多工作艳馒。

注意员寇,這些形狀是以五個(gè)維度給出的。這使用了以下約定:

[ sequence, batch, channel, height, width ]

我們的Swish層接收一個(gè)6個(gè)通道的256×256像素的圖像陆爽。(為什么有6個(gè)頻道扳缕?回想一下模型定義别威,這個(gè)Swish層應(yīng)用于Conv2D層的輸出省古,而卷積層有6個(gè)濾波器丧失。)

最后,調(diào)用evaluate(inputs, outputs)來(lái)執(zhí)行該層的計(jì)算琳拭。它接受一個(gè)MLMultiArray對(duì)象數(shù)組作為輸入炒事,并生成一個(gè)新MLMultiArray對(duì)象數(shù)組作為輸出(這些輸出對(duì)象已經(jīng)被分配臀栈,所以很方便——我們只需要填充它們)。

它獲得MLMultiArray對(duì)象數(shù)組的原因是某些類(lèi)型的層可以接受多個(gè)輸入或產(chǎn)生多個(gè)輸出挠乳。在上面的調(diào)試輸出中可以看到权薯,我們只得到了其中的一個(gè),因?yàn)槲覀兊哪P头浅:?jiǎn)單睡扬。

好的盟蚣,讓我們真正實(shí)現(xiàn)這個(gè)Swish激活函數(shù):

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)
    }
  }  
}

與大多數(shù)激活函數(shù)一樣,Swish是按元素進(jìn)行的操作卖怜,因此它循環(huán)遍歷輸入數(shù)組中的所有值屎开,計(jì)算x/(1+exp(-x))并將結(jié)果寫(xiě)入輸出數(shù)組。

重點(diǎn):MLMultiArray支持不同的數(shù)據(jù)類(lèi)型马靠。在這種情況下奄抽,我們假設(shè)數(shù)據(jù)類(lèi)型是.float32,即單精度浮點(diǎn)數(shù)甩鳄,這對(duì)我們的模型是正確的逞度。但是,MLMultiArray也可以支持int32和double妙啃,因此需要確保層類(lèi)能夠處理Core ML拋出的任何數(shù)據(jù)類(lèi)型档泽。(這里我使用了一個(gè)簡(jiǎn)單的斷言來(lái)使應(yīng)用程序崩潰,但是最好拋出一個(gè)錯(cuò)誤并讓Core ML進(jìn)行適當(dāng)?shù)那謇硪靖啊#?/p>

如果我們現(xiàn)在運(yùn)行應(yīng)用程序馆匿,預(yù)測(cè)的輸出是:

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

這與Keras輸出完全匹配!

那么現(xiàn)在我們完成了嗎燥滑?是的渐北,如果你不介意代碼變慢的話突倍。我們可以加快一點(diǎn)(實(shí)際上很多)。

使用Accelerate加速代碼

evaluate(inputs, outputs)函數(shù)是在CPU上執(zhí)行的淡喜,我們使用一個(gè)簡(jiǎn)單的for循環(huán)。這對(duì)于實(shí)現(xiàn)和調(diào)試層算法的第一個(gè)版本很有用瘟芝,但是它運(yùn)行速度不快锌俱。

更糟糕的是,當(dāng)我們以這種方式使用MLMultiArray時(shí)吭练,我們?cè)L問(wèn)的每個(gè)值都會(huì)得到NSNumber對(duì)象。直接訪問(wèn)MLMultiArray內(nèi)存中的浮點(diǎn)值要快得多分尸。

我們將使用向量化的CPU函數(shù)代替for循環(huán)寓落。幸運(yùn)的是史飞,Accelerate框架使此操作變得簡(jiǎn)單——但是我們必須使用指針抽诉,這使得代碼的可讀性稍微降低迹淌。

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)
  }
}

對(duì)于for循環(huán)耙饰,我們將公式output=input/(1+exp(-input))應(yīng)用于每個(gè)數(shù)組值。但是在這里件已,我們將這個(gè)公式分成單獨(dú)的步驟,并且同時(shí)將每個(gè)步驟應(yīng)用于所有數(shù)組值鉴未。

首先歼狼,我們使用vDSP_vneg()一次性計(jì)算輸入數(shù)組中所有值的-input。中間結(jié)果被寫(xiě)入輸出數(shù)組梅屉。然后坯汤,使用vvexpf()一次性對(duì)數(shù)組中的每個(gè)值進(jìn)行指數(shù)化。我們使用vDSP_vsadd()對(duì)每個(gè)值添加1搓幌,最后執(zhí)行vvdivf()給出最終結(jié)果的除法。

結(jié)果和前面完全一樣拐揭,但是它是通過(guò)利用CPU的SIMD指令集以更有效的方式完成的堂污。如果您要編寫(xiě)自己的自定義層息楔,我建議您盡可能多地使用Accelerate框架(這也是Core ML內(nèi)部為其自己的層使用的)值依。

即使啟用了優(yōu)化,for循環(huán)版本在iPhone 7上也花費(fèi)了0.18秒辆亏。加速版本花費(fèi)了0.0012秒〕勾牛快150倍衷蜓!

您可以在repo中的CPU only文件夾中找到此代碼。您可以在設(shè)備上或在模擬器中運(yùn)行此應(yīng)用程序置吓。試試看交洗!

更快的速度:在GPU上運(yùn)行

與其他機(jī)器學(xué)習(xí)框架相比咆爽,使用Core ML的優(yōu)勢(shì)在于符糊,Core ML可以在CPU上或是在GPU上運(yùn)行模型,而不需要您做任何額外的工作建瘫。對(duì)于大型神經(jīng)網(wǎng)絡(luò)啰脚,它通常嘗試使用GPU,但是在沒(méi)有非常強(qiáng)大的GPU的較老設(shè)備上亮航,它將回到使用CPU准给。

事實(shí)證明圆存,Core ML也可以混合匹配。如果您的自定義層只有一個(gè)CPU實(shí)現(xiàn)(就像我們剛剛做的那樣)油讯,那么它仍然會(huì)在GPU上運(yùn)行其他層陌兑,切換到用于自定義層的CPU,然后切換回GPU用于神經(jīng)網(wǎng)絡(luò)的其余部分软驰。

因此锭亏,在自定義層中只使用CPU實(shí)現(xiàn)不會(huì)降低模型的其余部分的性能戴已。然而糖儡,為什么不充分利用GPU呢?

對(duì)于Swish激活功能拴疤,GPU實(shí)現(xiàn)非常簡(jiǎn)單呐矾。這是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);
}

我們將對(duì)輸入數(shù)組中的每個(gè)數(shù)據(jù)元素調(diào)用這個(gè)計(jì)算內(nèi)核一次荞膘。因?yàn)镾wish是按元素進(jìn)行的操作羽资,所以我們可以在這里簡(jiǎn)單地編寫(xiě)熟悉的公式x/(1.0f+exp(-x))潮改。

與以前使用MLMultiArray不同汇在,這里的數(shù)據(jù)放在Metal紋理對(duì)象中亩鬼。MLMultiArray的數(shù)據(jù)類(lèi)型是32位浮點(diǎn)數(shù)辛孵,但這里我們實(shí)際處理的是16位浮點(diǎn)數(shù)或者h(yuǎn)alf。請(qǐng)注意,即使紋理類(lèi)型是half咆瘟,我們也要使用浮點(diǎn)值進(jìn)行實(shí)際計(jì)算袒餐,否則會(huì)損失太多的精度,而答案將是完全錯(cuò)誤的焰宣。

回想一下,數(shù)據(jù)有6個(gè)通道深闪唆。這就是為什么計(jì)算內(nèi)核使用texture_array,它是由多個(gè)“片”組成的Metal紋理笼吟。在我們的演示應(yīng)用程序中,紋理數(shù)組只包含2個(gè)切片(總共8個(gè)通道撵枢,所以最后兩個(gè)通道被忽略)潜必,但是上面的計(jì)算內(nèi)核將處理任意數(shù)量的切片/通道。

要使用這個(gè)GPU計(jì)算內(nèi)核垂攘,我們必須向Swift類(lèi)添加一些代碼:

@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內(nèi)核函數(shù)加載到MTLComputePipelineState對(duì)象中的樣式化代碼晒他。我們還需要添加以下方法:

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類(lèi)中存在此方法灼伤,那么該層將在GPU上運(yùn)行饺蔑。在這個(gè)方法中隆敢,您將“compute pipeline state”編碼為MTLCommandBuffer穴墅。多半又是樣式化代碼。encoder.dispatch()方法確保對(duì)輸入紋理中的每個(gè)通道中的每個(gè)像素調(diào)用一次計(jì)算內(nèi)核温自。有關(guān)詳細(xì)信息玄货,請(qǐng)參閱源代碼

現(xiàn)在悼泌,當(dāng)您運(yùn)行應(yīng)用程序(在一個(gè)相當(dāng)新的設(shè)備上)時(shí)松捉,encode(commandBuffer, inputs, outputs) 函數(shù)被調(diào)用,而不是evaluate(inputs, outputs)馆里,GPU有幸計(jì)算swish激活函數(shù)隘世。

您應(yīng)該得到與以前相同的輸出械媒。這很有意義——您希望自定義層的CPU和GPU版本計(jì)算完全相同的答案!

注意:您不能在模擬器上運(yùn)行Metal應(yīng)用程序臀玄,所以這個(gè)版本的應(yīng)用程序只能在真機(jī)上運(yùn)行。一部iPhone 6或者更好的就行了示损。如果設(shè)備太舊,Core ML仍將使用CPU而不是GPU運(yùn)行模型。

進(jìn)一步:如果您以前使用過(guò)MPSCNN葫盼,那么請(qǐng)注意闺金,Core ML使用GPU有一些不同。對(duì)于MPSCNN抬驴,您處理的是MPSImage對(duì)象,但是Core ML為您提供了MTLTexture推捐。像素格式似乎是.rgba16Float,這與MPSImage的.float16通道格式相對(duì)應(yīng)舌狗。

使用MPSCNN,具有4個(gè)通道或更少通道的圖像使用type2D紋理筷屡,超過(guò)4個(gè)通道的圖像使用type2DArray紋理。這意味著诡曙,對(duì)于MPSCNN,您可能必須編寫(xiě)兩個(gè)版本的計(jì)算內(nèi)核:一個(gè)采用texture對(duì)象,另一個(gè)采用texture_array對(duì)象。據(jù)我所知资铡,對(duì)于Core ML珊擂,紋理總是type2DArray,即使有4個(gè)通道或更少槽华,因此只需要編寫(xiě)一個(gè)版本的計(jì)算內(nèi)核傀蚌。

參數(shù)和權(quán)重

現(xiàn)在我們有了一個(gè)帶有相應(yīng)的Swift實(shí)現(xiàn)的自定義層。不錯(cuò)聊记,但這只是一個(gè)非常簡(jiǎn)單的層湿弦。

我們還可以向該層添加參數(shù)和權(quán)重∶笊冢“參數(shù)”在此上下文中表示可配置設(shè)置,例如卷積層的內(nèi)核大小和在該層周?chē)砑拥奶畛淞俊?/p>

在我們的例子中虏缸,我們可以將beta設(shè)置為一個(gè)參數(shù)。還記得beta嗎嫩实?beta的值決定了Swish函數(shù)有多陡峭刽辙。到目前為止,我們已經(jīng)實(shí)現(xiàn)的Swish版本是:

swish(x) = x * sigmoid(x)

但是記住完整的定義是這樣

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

beta是一個(gè)數(shù)字甲献。到目前為止宰缤,我們假設(shè)beta總是1.0,但是我們可以把它配置為一個(gè)參數(shù)竟纳,或者甚至讓模型在訓(xùn)練時(shí)學(xué)習(xí)beta的值撵溃,在這種情況下疚鲤,我們將它看作一個(gè)權(quán)重锥累。

要向自定義層添加參數(shù)或權(quán)重,請(qǐng)按以下方式更改轉(zhuǎn)換函數(shù):

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

現(xiàn)在集歇,當(dāng)您運(yùn)行該應(yīng)用程序時(shí)桶略,Swish類(lèi)將在init(parameters)方法中接收帶有這些整數(shù)和字符串值的參數(shù)字典,通過(guò)setWeightData()中的Data對(duì)象接收權(quán)重。

讓我們添加beta作為參數(shù)际歼。為此惶翻,我們應(yīng)該遠(yuǎn)離Lambda層,并將Swish激活函數(shù)轉(zhuǎn)換為適當(dāng)?shù)腒eras層對(duì)象鹅心。Lambda層非常適合于簡(jiǎn)單的計(jì)算吕粗,但是現(xiàn)在我們希望給Swish層一些狀態(tài)(beta的值),創(chuàng)建一個(gè)Layer子類(lèi)是更好的方法旭愧。

在Python腳本 convert_subclass.py中颅筋,我們現(xiàn)在將Swish函數(shù)定義為L(zhǎng)ayer的子類(lèi):

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

注意,這如何在構(gòu)造函數(shù)中采用beta值输枯。Keras中的call()函數(shù)等價(jià)于swift中的evaluate(inputs, outputs)议泵。在call()函數(shù)中,我們計(jì)算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的值是一個(gè)超參數(shù)先口,它是在模型構(gòu)建時(shí)定義的。這里我選擇使用beta=0.01瞳收,這樣我們就會(huì)得到與以前不同的預(yù)測(cè)碉京。

順便說(shuō)一下,這里是Swish在beta 0.01中的樣子缎讼,它幾乎是一條直線:


為了將這個(gè)層轉(zhuǎn)換為Core ML收夸,我們需要為它建一個(gè)轉(zhuǎn)換函數(shù):

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

這與以前非常相似,只是現(xiàn)在我們從層(這是我們剛剛創(chuàng)建的新Swish類(lèi)的實(shí)例)讀取beta屬性血崭,并將其粘貼到CustomLayerParams的參數(shù)字典中卧惜。注意,這個(gè)字典不支持32位浮點(diǎn)夹纫,只支持64位雙精度浮點(diǎn)(以及整數(shù)和布爾值)咽瓷,所以我們使用.doubleValue。

當(dāng)我們調(diào)用Keras轉(zhuǎn)換器時(shí)舰讹,我們必須告訴它這個(gè)新的轉(zhuǎn)換函數(shù):

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 })

這一切都非常類(lèi)似于我們之前所做的茅姜,除了現(xiàn)在Swish不是包裝在Lambda對(duì)象中的基本Python函數(shù),而是從Keras Layer基類(lèi)派生的一個(gè)成熟的類(lèi)月匣。

在iOS方面钻洒,我們需要調(diào)整Swish.swift以從參數(shù)字典中讀出這個(gè)“beta”值并將其應(yīng)用于計(jì)算。

@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) 時(shí)锄开,我們現(xiàn)在用self.beta乘以輸入素标。

同樣,對(duì)于Metal compute shader,在encode(commandBuffer, inputs, outputs)中萍悴,我們可以將self.beta傳遞到計(jì)算內(nèi)核中头遭,如下所示:

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

然后在Metak內(nèi)核中:

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));
  ...
}

請(qǐng)參閱源代碼以獲得完整的更改寓免。我希望解釋的足夠清楚,使您能很容易的配置參數(shù)添加到定制層中计维。

注意:當(dāng)我運(yùn)行這個(gè)新版本的iOS應(yīng)用程序時(shí)袜香,預(yù)測(cè)結(jié)果與Keras的結(jié)果并不100%匹配。當(dāng)Core ML使用GPU時(shí)鲫惶,這種不匹配的情況很常見(jiàn)蜈首。卷積層在GPU上運(yùn)行,帶有16位浮點(diǎn)數(shù)欠母,這降低了精度疾就,而Keras對(duì)一切都使用32位浮點(diǎn)數(shù)。所以您一定會(huì)看到來(lái)自iOS模型和來(lái)自原始Keras模型的預(yù)測(cè)之間的差異艺蝴。只要差別很锈(大約1e-3或更小)就可以接受猜敢。

可學(xué)習(xí)權(quán)重

最后一件事我想告訴你姑荷。機(jī)器學(xué)習(xí)的全部意義在于學(xué)習(xí)東西,所以對(duì)于許多定制層缩擂,您希望能夠賦予它們可學(xué)習(xí)的權(quán)重鼠冕。因此,讓我們?cè)僖淮胃淖僑wish層的實(shí)現(xiàn)以使beta可以學(xué)習(xí)胯盯。這讓模型學(xué)習(xí)激活函數(shù)的最佳形狀是什么懈费。

Swish層仍然是Layer的一個(gè)子類(lèi),但是這次我們通過(guò)add_weight()函數(shù)來(lái)賦予它一個(gè)可學(xué)習(xí)權(quán)重:

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

我們將為輸入數(shù)據(jù)中的每個(gè)通道創(chuàng)建可學(xué)習(xí)的權(quán)重博脑,而不是單個(gè)beta值憎乙,這就是為什么我們使用shape=(input_shape[3], )。在該示例中叉趣,因?yàn)閬?lái)自前一個(gè)Conv2D層的輸出有6個(gè)通道泞边,所以該層將學(xué)習(xí)6個(gè)不同的beta值。beta的初始值為1疗杉,這似乎是一個(gè)合理的缺省值阵谚。

現(xiàn)在,當(dāng)您調(diào)用model.fit(...)來(lái)訓(xùn)練模型時(shí)烟具,它將學(xué)習(xí)每個(gè)通道的最佳beta值梢什。

在轉(zhuǎn)換函數(shù)中,我們必須執(zhí)行以下操作以將這些學(xué)習(xí)到的權(quán)重放入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中所需要做的所有工作朝聋。

在運(yùn)行iOS應(yīng)用程序時(shí)嗡午,您將注意到setWeightData() 現(xiàn)在接收一個(gè)包含24字節(jié)的Data對(duì)象。就是6通道乘以每個(gè)浮點(diǎn)數(shù)的4字節(jié)玖翅。

使用Swish.swift層代碼從這個(gè)權(quán)重?cái)?shù)組讀取beta并在計(jì)算中使用它翼馆,這相當(dāng)簡(jiǎn)單。與以前的主要區(qū)別是金度,我們知道应媚,在數(shù)據(jù)中有許多不同的beta值。我將把這個(gè)留給讀者作為練習(xí)猜极。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末中姜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子跟伏,更是在濱河造成了極大的恐慌丢胚,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件受扳,死亡現(xiàn)場(chǎng)離奇詭異携龟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)勘高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)峡蟋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人华望,你說(shuō)我怎么就攤上這事蕊蝗。” “怎么了赖舟?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蓬戚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我宾抓,道長(zhǎng)子漩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任石洗,我火速辦了婚禮痛单,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘劲腿。我一直安慰自己旭绒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布焦人。 她就那樣靜靜地躺著挥吵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪花椭。 梳的紋絲不亂的頭發(fā)上忽匈,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音矿辽,去河邊找鬼丹允。 笑死郭厌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雕蔽。 我是一名探鬼主播折柠,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼批狐!你這毒婦竟也來(lái)了扇售?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嚣艇,失蹤者是張志新(化名)和其女友劉穎承冰,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體食零,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡困乒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贰谣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顶燕。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖冈爹,靈堂內(nèi)的尸體忽然破棺而出涌攻,到底是詐尸還是另有隱情,我是刑警寧澤频伤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布恳谎,位于F島的核電站,受9級(jí)特大地震影響憋肖,放射性物質(zhì)發(fā)生泄漏因痛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一岸更、第九天 我趴在偏房一處隱蔽的房頂上張望鸵膏。 院中可真熱鬧,春花似錦怎炊、人聲如沸谭企。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)债查。三九已至,卻和暖如春瓜挽,著一層夾襖步出監(jiān)牢的瞬間盹廷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工久橙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俄占,地道東北人管怠。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缸榄,于是被迫代替她去往敵國(guó)和親渤弛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容