深度学习 - iOS使用Metal Performance Shaders加速TensorFlow

前言

在上一篇深度学习 - Tensorflow on iOS 入门 + MNIST中,我们使用了TensorFlow训练了一个模型,并且编译了iOS的库在项目工程中引用,用起来整个过程还是比较麻烦的,而且包的大小因为引用了TensorFlow的库所以比较大,并且目前TensorFlow在iOS上还不支持GPU。

本文章翻译了Speeding Up TensorFlow with Metal Performance Shaders里面的主要内容,并实现一个demoGithub(MNISToniOSWithoutTFlib)。

MNIST on iOS

为什么要用Metal Performance Shaders

  1. Metal在iOS9中已经提供,主要是用Apple的API的写Kernel把图像运算丢给GPU获得更好的性能。在iOS10中Apple提供了Metal Performance Shaders,是一个上层的API,专门用于用GPU加速深度学习中卷积,池化等运算,相比CPU会快很多。

  2. 使用Metal可以不用引用TensorFlow的lib,减少包的大小和复杂的工程设置。

基于以上两点可以说基本上能用Metal是一定要用的。

主要过程

使用Metal构建网络,传入训练好的参数 ->输入数据,经过训练好的网络 ->获得输出

1. 使用Metal定义网络,加载训练好的参数

这一步主要是用Metal重写我们之前训练用过的网络train_metal.py,包括定义输入,卷积层,池化,全连接和输出。

这里有一点需要注意的是Metal和TensorFlow使用的数据格式有所区别,在Tensorflow中的格式为:

[{source/kernel}Height][{source/kernel}Width][inputChannels][outputChannels]
[0, 1, 2, 3]

在Metal中的格式为:

[outputChannels][{source/kernel}Height][{source/kernel}Width][inputChannels]
[3, 0, 1, 2]

所以在我们训练模型时需要使用TensorFlow调整一下模型参数的格式:

with open('W_conv1', 'w') as f:
  W_conv1_p = tf.transpose(W_conv1, perm=[3, 0, 1, 2])
  f.write(session.run(W_conv1_p).tobytes())

同理我们调整剩余的参数,包括b_conv1,W_conv2等等...具体可以见train_metal.py

搞定格式之后,我们使用Metal来重写网络:

我们训练的网络结构为:
输入->卷积1->池化1->卷积2->池化2->全连接1->全连接2->softmax
在使用TensorFlow训练的时候:

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
// 卷积1
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.nn.softmax(tf.matmul(h_fc1, W_fc2) + b_fc2, name="softmax")

对应到Metal里面,我们首先要构建相同的网络。为了之后做出预测,传入我们已经训练好的参数:

-(void) initMetal:(id) nDevice {
    float *conv1weights = loadTensor(@"W_conv1", 5 * 5 * 1 * 32);
    float *conv1biases = loadTensor(@"b_conv1", 32);
    float *conv2weights = loadTensor(@"W_conv2", 5 * 5 * 32 * 64);
    float *conv2biases = loadTensor(@"b_conv2", 64);
    float *fc1weights = loadTensor(@"W_fc1", 7 * 7 * 64 * 1024);
    float *fc1biases = loadTensor(@"b_fc1", 1024);
    float *fc2weights = loadTensor(@"W_fc2", 1024 * 10);
    float *fc2biases = loadTensor(@"b_fc2", 10);
    
    id<MTLDevice> device = nDevice;

    const MPSCNNNeuronReLU *reluUnit = [[MPSCNNNeuronReLU alloc] initWithDevice:device a:0];
    
    self.conv1descriptor = [MPSCNNConvolutionDescriptor cnnConvolutionDescriptorWithKernelWidth:5 kernelHeight:5 inputFeatureChannels:1 outputFeatureChannels:32 neuronFilter:reluUnit];
    self.conv1layer = [[MPSCNNConvolution alloc] initWithDevice:device convolutionDescriptor:self.conv1descriptor kernelWeights:conv1weights biasTerms:conv1biases flags:MPSCNNConvolutionFlagsNone];
    self.conv1outdescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:kImageSide height:kImageSide featureChannels:32];
    
    self.pool1layer = [[MPSCNNPoolingMax alloc]initWithDevice:device kernelWidth:2 kernelHeight:2 strideInPixelsX:2 strideInPixelsY:2];
    self.pool1layer.offset = (MPSOffset){1, 1, 0};
    self.pool1layer.edgeMode = MPSImageEdgeModeClamp;
    self.pool1outdescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:kImageSide2 height:kImageSide2 featureChannels:32];
    
    self.conv2descriptor = [MPSCNNConvolutionDescriptor cnnConvolutionDescriptorWithKernelWidth:5 kernelHeight:5 inputFeatureChannels:32 outputFeatureChannels:64 neuronFilter:reluUnit];
    self.conv2layer = [[MPSCNNConvolution alloc] initWithDevice:device convolutionDescriptor:self.conv2descriptor kernelWeights:conv2weights biasTerms:conv2biases flags:MPSCNNConvolutionFlagsNone];
    self.conv2outdescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:kImageSide2 height:kImageSide2 featureChannels:64];
    self.pool2layer = [[MPSCNNPoolingMax alloc] initWithDevice:device kernelWidth:2 kernelHeight:2 strideInPixelsX:2 strideInPixelsY:2];
    self.pool2layer.offset = (MPSOffset){1, 1, 0};
    self.pool2layer.edgeMode = MPSImageEdgeModeClamp;
    self.pool2outdescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:kImageSide4 height:kImageSide4 featureChannels:64];
    
    self.fc1descriptor = [MPSCNNConvolutionDescriptor cnnConvolutionDescriptorWithKernelWidth:kImageSide4 kernelHeight:kImageSide4 inputFeatureChannels:64 outputFeatureChannels:1024 neuronFilter:reluUnit];
    self.fc1layer = [[MPSCNNFullyConnected alloc] initWithDevice:device convolutionDescriptor:self.fc1descriptor kernelWeights:fc1weights biasTerms:fc1biases flags:MPSCNNConvolutionFlagsNone];
    self.fc1outdescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:1 height:1 featureChannels:1024];
    
    self.fc2descriptor = [MPSCNNConvolutionDescriptor cnnConvolutionDescriptorWithKernelWidth:1 kernelHeight:1 inputFeatureChannels:1024 outputFeatureChannels:kOutputs neuronFilter:nil];
    self.fc2layer = [[MPSCNNFullyConnected alloc] initWithDevice:device convolutionDescriptor:self.fc2descriptor kernelWeights:fc2weights biasTerms:fc2biases flags:MPSCNNConvolutionFlagsNone];
    self.fc2outdescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:1 height:1 featureChannels:kOutputs];
    self.softmaxOutput = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat16 width:1 height:1 featureChannels:kOutputs];
    self.softmaxLayer = [[MPSCNNSoftMax alloc]initWithDevice:device];
    self.inputDescriptor = [MPSImageDescriptor imageDescriptorWithChannelFormat:MPSImageFeatureChannelFormatFloat32 width:kImageSide height:kImageSide featureChannels:1];
    
    self.pendingBuffers = [[NSMutableArray alloc] init];
    self.results = [[NSMutableArray alloc] init];
    
}

2. 将图像丢到网络中运算(预测)

在1中构建网络之后,我们就可以输入数据了,这里的数据就是我们画板中手写的数字,做成一个uiimage,转为28*28的灰度图然后输入我们的网络。
处理输入数据:

 UIImage *scaledImage = [self scaleImage:drawedImage];
    UIImage *image = [self convertImageToGrayScale:scaledImage];

    float *data = [self getGrayPixelFromImage:image atX:0 andY:0 count:kInputLength];
    id<MTLDevice> device = MTLCreateSystemDefaultDevice();
    if (device == nil) {
        NSLog(@"no metal support");
        return;
    }

跑网络,这里我们把之前定义好的网络描述首尾相接,并将数据输入:

    id<MTLCommandQueue> queue = [device newCommandQueue];
    id<MTLCommandBuffer> buffer = [queue commandBuffer];
    MPSImage *inputImage = [[MPSImage alloc] initWithDevice:device imageDescriptor:self.inputDescriptor];
    [inputImage.texture replaceRegion:MTLRegionMake2D(0, 0, kImageSide, kImageSide) mipmapLevel:0 withBytes:data bytesPerRow:sizeof(float) * kImageSide];
    [MPSTemporaryImage prefetchStorageWithCommandBuffer:buffer imageDescriptorList:@[self.conv1outdescriptor, self.pool1outdescriptor, self.conv2outdescriptor, self.pool2outdescriptor, self.fc1outdescriptor, self.fc2outdescriptor]];
    
    MPSTemporaryImage *c1o = [MPSTemporaryImage temporaryImageWithCommandBuffer:buffer imageDescriptor:self.conv1outdescriptor];
    [self.conv1layer encodeToCommandBuffer:buffer sourceImage:inputImage destinationImage:c1o];
    
    MPSTemporaryImage *p1o = [MPSTemporaryImage temporaryImageWithCommandBuffer:buffer imageDescriptor:self.pool1outdescriptor];
    [self.pool1layer encodeToCommandBuffer:buffer sourceImage:c1o destinationImage:p1o];
    
    MPSTemporaryImage *c2o = [MPSTemporaryImage temporaryImageWithCommandBuffer:buffer imageDescriptor:self.conv2outdescriptor];
    [self.conv2layer encodeToCommandBuffer:buffer sourceImage:p1o destinationImage:c2o];
    
    MPSTemporaryImage *p2o = [MPSTemporaryImage temporaryImageWithCommandBuffer:buffer imageDescriptor:self.pool2outdescriptor];
    [self.pool2layer encodeToCommandBuffer:buffer sourceImage:c2o destinationImage:p2o];
    
    MPSTemporaryImage *fc1tdi = [MPSTemporaryImage temporaryImageWithCommandBuffer:buffer imageDescriptor:self.fc1outdescriptor];
    [self.fc1layer encodeToCommandBuffer:buffer sourceImage:p2o destinationImage:fc1tdi];
    
    MPSTemporaryImage *fc2tdi = [MPSTemporaryImage temporaryImageWithCommandBuffer:buffer imageDescriptor:self.fc2outdescriptor];
    [self.fc2layer encodeToCommandBuffer:buffer sourceImage:fc1tdi destinationImage:fc2tdi];
    
    __block MPSImage *resultImage = [[MPSImage alloc] initWithDevice:device imageDescriptor:self.softmaxOutput];
    [self.softmaxLayer encodeToCommandBuffer:buffer sourceImage:fc2tdi destinationImage:resultImage];
    
    [buffer commit];
    [buffer waitUntilCompleted];

3. 获得结果

    const size_t numSlices = (resultImage.featureChannels + 3)/4;
    float16_t halfs[numSlices * 4];
    NSLog(@"size of float16_t %lu",sizeof(float16_t));
    for (size_t i = 0; i < numSlices; i += 1) {
        [resultImage.texture getBytes:&halfs[i * 4] bytesPerRow:8 bytesPerImage:8 fromRegion:MTLRegionMake3D(0, 0, 0, 1, 1, 1) mipmapLevel:0 slice:i];
        for (size_t j = i * 4; j < i * 4 + 4; j++) {
            NSLog(@"half %zu %f", j, halfs[j]);
        }
    }
   
    int bestIndex = -1;
    float bestProbability = 0;
    for (auto i = 0; i < kOutputs; i++) {
        const auto probability = halfs[i];
        if (probability > bestProbability) {
            bestProbability = probability;
            bestIndex = i;
        }
    }

最后

在程序中我使用了Speeding Up TensorFlow with Metal Performance Shaders里训练好的参数,我发现6和8和9的识别很不准啊,大家可以试试1,3,5这种数字,直接下载demo,不用任何库就能跑起来,需要ios10以上。

我在ip6s上,试着用这个demo循环10000次预测,时间为8.76855秒。在上一个使用了TensorFlow的库并跑在CPU的demo中,循环10000次的时间是26.4693秒。

Speeding Up TensorFlow with Metal Performance Shaders原作者使用MNIST的测试数据集跑了一遍:
On my iPad Pro, this took 3.29s, down from 5.4s (a 40% improvement).

不知道是不是我循环的方式有问题...

Have Fun :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容