學(xué)習(xí)筆記TF066:TensorFlow移動(dòng)端應(yīng)用磺芭,iOS、Android系統(tǒng)實(shí)踐

TensorFlow對(duì)Android醉箕、iOS徘跪、樹(shù)莓派都提供移動(dòng)端支持。

移動(dòng)端應(yīng)用原理琅攘。移動(dòng)端、嵌入式設(shè)備應(yīng)用深度學(xué)習(xí)方式松邪,一模型運(yùn)行在云端服務(wù)器坞琴,向服務(wù)器發(fā)送請(qǐng)求,接收服務(wù)器響應(yīng)逗抑;二在本地運(yùn)行模型剧辐,PC訓(xùn)練模型,放到移動(dòng)端預(yù)測(cè)邮府。向服務(wù)端請(qǐng)求數(shù)據(jù)可行性差荧关,移動(dòng)端資源稀缺。本地運(yùn)行實(shí)時(shí)性更好褂傀。加速計(jì)算忍啤,內(nèi)存空間和速度優(yōu)化。精簡(jiǎn)模型仙辟,節(jié)省內(nèi)存空間同波,加快計(jì)算速度。加快框架執(zhí)行速度叠国,優(yōu)化模型復(fù)雜度和每步計(jì)算速度未檩。
精簡(jiǎn)模型,用更低權(quán)得精度粟焊,量化(quantization)冤狡、權(quán)重剪枝(weight pruning,剪小權(quán)重連接项棠,把所有權(quán)值連接低于閾值的從網(wǎng)絡(luò)移除)悲雳。加速框架執(zhí)行,優(yōu)化矩陣通用乘法(GEMM)運(yùn)算香追,影響卷積層(先數(shù)據(jù)im2col運(yùn)行怜奖,再GEMM運(yùn)算)和全連接層。im2col翅阵,索引圖像塊重排列為矩陣列歪玲。先將大矩陣重疊劃分多個(gè)子矩陣迁央,每個(gè)子矩陣序列化成向量,得到另一個(gè)矩陣滥崩。

量化(quantitative)岖圈。《How to Quantize Neural Networks with TensorFlow》https://www.tensorflow.org/performance/quantization 钙皮。離散化蜂科。用比32位浮點(diǎn)數(shù)更少空間存儲(chǔ)、運(yùn)行模型短条,TensorFlow量化實(shí)現(xiàn)屏蔽存儲(chǔ)导匣、運(yùn)行細(xì)節(jié)。神經(jīng)網(wǎng)絡(luò)預(yù)測(cè)茸时,浮點(diǎn)影響速度贡定,量化加快速度,保持較高精度可都。減小模型文件大小缓待。存儲(chǔ)模型用8位整數(shù),加載模型運(yùn)算轉(zhuǎn)換回32位浮點(diǎn)數(shù)渠牲。降低預(yù)測(cè)過(guò)程計(jì)算資源旋炒。神經(jīng)網(wǎng)絡(luò)噪聲健壯笥強(qiáng),量化精度損失不會(huì)危害整體準(zhǔn)確度签杈。訓(xùn)練瘫镇,反向傳播需要計(jì)算梯度,不能用低精度格式直接訓(xùn)練答姥。PC訓(xùn)練浮點(diǎn)數(shù)模型汇四,轉(zhuǎn)8位,移動(dòng)端用8位模型預(yù)測(cè)踢涌。
量化示例通孽。GoogleNet模型轉(zhuǎn)8位模型例子。下載訓(xùn)練好GoogleNet模型睁壁,http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz 背苦。

bazel build tensorflow/tools/quantization:quantization_graph
bazel-bin/tensorflow/tools/quantization/quantization_graph \
--input=/tmp/classify_image_graph_def.pb \
--output_node_names="softmax" --output=/tmp/quantized_graph.pb \
--mode=eightbit

生成量化后模型大小只有原來(lái)的1/4。執(zhí)行:

bazel build tensorflow/examples/label_image:label_image
bazel-bin/tensorflow/examples/label_image/label_image \
--image=/tmp/cropped_panda.jpg \
--graph=/tmp/quantized_graph.pb \
--labels=/tmp/imagenet_synset_to_human_label_map.txt \
--input_width=299 \
--input_height=299 \
--input_mean=128 \
--input_std=128 \
--input_layer="Mul:0" \
--output_layer="softmax:0"

量化過(guò)程實(shí)現(xiàn)潘明。預(yù)測(cè)操作轉(zhuǎn)換成等價(jià)8位版本操作實(shí)現(xiàn)行剂。原始Relu操作,輸入钳降、輸出浮點(diǎn)數(shù)厚宰。量化Relu操作,根據(jù)輸入浮點(diǎn)數(shù)計(jì)算最大值、最小值铲觉,進(jìn)入量化(Quantize)操作輸入數(shù)據(jù)轉(zhuǎn)換8位澈蝙。保證輸出層輸入數(shù)據(jù)準(zhǔn)確性,需要反量化(Dequantize)操作撵幽,權(quán)重轉(zhuǎn)回32位精度灯荧,保證預(yù)測(cè)準(zhǔn)確性。整個(gè)模型前向傳播用8位整數(shù)支行盐杂,最后一層加反量化層逗载,8位轉(zhuǎn)回32位輸出層輸入。每個(gè)量化操作后執(zhí)行反量化操作链烈。

量化數(shù)據(jù)表示厉斟。浮點(diǎn)數(shù)轉(zhuǎn)8位表示,是壓縮問(wèn)題强衡。權(quán)重擦秽、經(jīng)過(guò)激活函數(shù)處理上層輸出,是分布在一個(gè)范圍內(nèi)的值食侮。量化過(guò)程,找出最大值目胡、最小值锯七,將浮點(diǎn)數(shù)線(xiàn)性分布,做線(xiàn)性擴(kuò)展誉己。

優(yōu)化矩陣乘法運(yùn)算眉尸。谷歌開(kāi)源小型獨(dú)立低精度通用矩陣乘法(General Matrix to Matrix Multiplication,GEMM)庫(kù) gemmlowp。https://github.com/google/gemmlowp 巨双。

iOS系統(tǒng)實(shí)踐噪猾。

環(huán)境準(zhǔn)備。操作系統(tǒng)Mac OS X筑累,集成開(kāi)發(fā)工具Xcode 7.3以上版本袱蜡。編譯TensorFlow核心靜態(tài)庫(kù)。tensorflow/contrib/makefiles/download_depencies.sh 慢宗。依賴(lài)庫(kù)下載到tensorflow/contrib/makefile/downloads目錄坪蚁。eigen #C++開(kāi)源矩陣計(jì)算工具。gemmlowp #小型獨(dú)立低精度通用矩陣乘法(GEMM)庫(kù)镜沽。googletest #谷歌開(kāi)源C++測(cè)試框架敏晤。protobuf #谷歌開(kāi)源數(shù)據(jù)交換格式協(xié)議。re2 #谷歌開(kāi)源正則表達(dá)式庫(kù)缅茉。

編譯演示程度嘴脾,運(yùn)行。tensorflow/contrib/makefile/build_all_iso.sh蔬墩。編譯生成靜態(tài)庫(kù)译打,tensorflow/contrib/makefile/gen/lib:ios_ARM64耗拓、ios_ARMV7、ios_ARMV7S扶平、ios_I386帆离、ios_X86_64、libtensorflow-core.a结澄。Xcode模擬器或iOS設(shè)備運(yùn)行APP預(yù)測(cè)示例哥谷。TensorFlow iOS示例。https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/ios/ 麻献。3個(gè)目錄们妥。benchmark目錄是預(yù)測(cè)基準(zhǔn)示例。simple目錄是圖片預(yù)測(cè)示例勉吻。camera目錄是視頻流實(shí)時(shí)預(yù)測(cè)示例监婶。下載Inception V1模型,能識(shí)別1000類(lèi)圖片齿桃,https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip 惑惶。解壓模型,復(fù)制到benchmark短纵、simple带污、camera的data目錄。運(yùn)行目錄下xcodeproj文件香到。選擇iPhone 7 Plus模擬器鱼冀,點(diǎn)擊運(yùn)行標(biāo)志,編譯完成點(diǎn)擊Run Model按鈕悠就。預(yù)測(cè)結(jié)果見(jiàn)Xcode 控制臺(tái)千绪。

自定義模型編譯、運(yùn)行梗脾。https://github.com/tensorflow/tensorflow/blob/15b1cf025da5c6ac2bcf4d4878ee222fca3aec4a/tensorflow/docs_src/tutorials/image_retraining.md 荸型。下載花卉數(shù)據(jù) http://download.tensorflow.org/example_images/flower_photos.tgz 。郁金香(tulips)炸茧、玫瑰(roses)帆疟、浦公英(dandelion)、向日葵(sunflowers)宇立、雛菊(daisy)5種花卉文件目錄踪宠,各800張圖片。
訓(xùn)練原始模型妈嘹。下載預(yù)訓(xùn)練Inception V3模型 http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz 柳琢。

python tensorflow/examples/image_retraining/retrain.py \
--bottlenectk_dir=/tmp/bottlenecks/ \
--how_many_training_steps 10 \
--model_dir=/tmp/inception \
--output_graph=/tmp/retrained_graph.pb \
--output_labels=/tmp/retrained_labels.txt \
--image_dir /tmp/flower_photos

訓(xùn)練完成,/tmp目錄有模型文件retrained_graph.pb、標(biāo)簽文件上retrained_labels.txt柬脸∷ィ“瓶頸”(bottlenecks)文件,描述實(shí)際分類(lèi)最終輸出層前一層(倒數(shù)第二層)倒堕。倒數(shù)第二層訓(xùn)練很好灾测,瓶頸值是有意義緊湊圖像摘要,包含足夠信息使分類(lèi)選擇垦巴。第一次訓(xùn)練媳搪,retrain.py文件代碼先分析所有圖片,計(jì)算每張圖片瓶頸值存儲(chǔ)下來(lái)骤宣。每張圖片被使用多次秦爆,不必重復(fù)計(jì)算。

編譯iOS支持模型憔披。https://petewarden.com/2016/09/27/tensorflow-for-mobile-poets/ 等限。原始模型到iOS模型,先去掉iOS系統(tǒng)不支持操作芬膝,優(yōu)化模型望门,再將模型量化,權(quán)重變8位常數(shù)锰霜,縮小模型筹误,最后模型內(nèi)存映射。
去掉iOS系統(tǒng)不支持操作锈遥,優(yōu)化模型纫事。iOS版本TensorFlow僅支持預(yù)測(cè)階段常見(jiàn)沒(méi)有大外部依賴(lài)關(guān)系操作勘畔。支持操作列表:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/makefile/tf_op_files.txt 所灸。DecodeJpeg不支持,JPEG格式圖片解碼炫七,依賴(lài)libjpeg爬立。從攝像頭實(shí)時(shí)識(shí)別花卉種類(lèi),直接處理相機(jī)圖像緩沖區(qū)万哪,不存JPEG文件再解碼侠驯。預(yù)訓(xùn)練模型Inception V3 從圖片數(shù)據(jù)集訓(xùn)練,包含DecodeJpeg操作奕巍。輸入數(shù)據(jù)直接提供(feed)Decode后Mul操作吟策,繞過(guò)Decode操作。優(yōu)化加速預(yù)測(cè)的止,顯式批處理規(guī)范化(explicit batch normalization)操作合并到卷積權(quán)重檩坚,減少計(jì)算次數(shù)。

bazel build tensorflow/python/tools:optimize_for_inference
bazel-bin/tensorflow/python/tools/optimize_for_inference \
--input=/tmp/retrained_graph.pb \
--output=/tmp/optimized_graph.pb \
--input_names=Mul \
--output_names=final_result \

label_image命令預(yù)測(cè):

bazel-bin/tensorflow/examples/label_image/label_image \
--output_layer=final_result \
--labels=/tmp/output_labels.txt \
--image=/tmp/flower_photos/daisy/5547758_eea9edfd54_n.jpg
--graph=/tmp/output_graph.pb \
--input_layer=Mul \
--input_mean=128 \
--input_std=128 \

量化模型。蘋(píng)果系統(tǒng)在.ipa包分發(fā)應(yīng)用程度匾委,所有應(yīng)用程度資源都用zip壓縮拖叙。模型權(quán)重從浮點(diǎn)數(shù)轉(zhuǎn)整數(shù)(范圍0~255),損失準(zhǔn)確度赂乐,小于1%薯鳍。

bazel build tensorflow/tools/quantization:quantization_graph
bazel-bin/tensorflow/tools/quantization/quantization_graph \
--input=/tmp/optimized_graph.pb \
--output=/tmp/rounded_graph.pb \
--output_node_names=final_result \
--mode=weights_rounded

內(nèi)存映射 memory mapping。物理內(nèi)存映射到進(jìn)程地址空間內(nèi)挨措,應(yīng)用程序直接用輸入/輸出地址空間挖滤,提高讀寫(xiě)效率。模型全部一次性加載到內(nèi)存緩沖區(qū)运嗜,會(huì)對(duì)iOS RAM施加過(guò)大壓力壶辜,操作系統(tǒng)會(huì)殺死內(nèi)存占用過(guò)多程序。模型權(quán)值緩沖區(qū)只讀担租,可映射到內(nèi)存砸民。重新排列模型,權(quán)重分部分逐塊從主GraphDef加載到內(nèi)存奋救。

bazel build tensorflow/contrib/util:convert_graphdef_memmapped_format
bazel-bin/tensorflow/contrib/util/convert_graphdef_memmapped_format \
--in_graph=/tmp/rounded_graph.pb \
--out_graph=/tmp/mmapped_graph.pb

生成iOS工程文件運(yùn)行岭参。視頻流實(shí)進(jìn)預(yù)測(cè)演示程序例子。https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/ios/camera 尝艘。模型文件演侯、標(biāo)記文件復(fù)制到data目錄。修改CameraExampleViewController.mm背亥,更改加載模型文件名稱(chēng)秒际、輸入圖片尺寸、操作節(jié)點(diǎn)名字狡汉、縮放像素大小娄徊。

#import <AssertMacros.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <CoreImage/CoreImage.h>
#import <ImageIO/ImageIO.h>
#import "CameraExampleViewController.h"
#include <sys/time.h>
#include "tensorflow_utils.h"
// If you have your own model, modify this to the file name, and make sure
// you've added the file to your app resources too.
static NSString* model_file_name = @"tensorflow_inception_graph";
static NSString* model_file_type = @"pb";
// This controls whether we'll be loading a plain GraphDef proto, or a
// file created by the convert_graphdef_memmapped_format utility that wraps a
// GraphDef and parameter file that can be mapped into memory from file to
// reduce overall memory usage.
const bool model_uses_memory_mapping = false;
// If you have your own model, point this to the labels file.
static NSString* labels_file_name = @"imagenet_comp_graph_label_strings";
static NSString* labels_file_type = @"txt";
// These dimensions need to match those the model was trained with.
// 以下尺寸需要和模型訓(xùn)練時(shí)相匹配
const int wanted_input_width =299;// 224;
const int wanted_input_height = 299;//224;
const int wanted_input_channels = 3;
const float input_mean = 128.0f;//117.0f;
const float input_std = 128.0f;//1.0f;
const std::string input_layer_name = "Mul";//"input";
const std::string output_layer_name = "final_result";//"softmax1";
static void *AVCaptureStillImageIsCapturingStillImageContext =
    &AVCaptureStillImageIsCapturingStillImageContext;
@interface CameraExampleViewController (InternalMethods)
- (void)setupAVCapture;
- (void)teardownAVCapture;
@end
@implementation CameraExampleViewController
- (void)setupAVCapture {
  NSError *error = nil;
  session = [AVCaptureSession new];
  if ([[UIDevice currentDevice] userInterfaceIdiom] ==
      UIUserInterfaceIdiomPhone)
    [session setSessionPreset:AVCaptureSessionPreset640x480];
  else
    [session setSessionPreset:AVCaptureSessionPresetPhoto];
  AVCaptureDevice *device =
      [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
  AVCaptureDeviceInput *deviceInput =
      [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
  assert(error == nil);
  isUsingFrontFacingCamera = NO;
  if ([session canAddInput:deviceInput]) [session addInput:deviceInput];
  stillImageOutput = [AVCaptureStillImageOutput new];
  [stillImageOutput
      addObserver:self
       forKeyPath:@"capturingStillImage"
          options:NSKeyValueObservingOptionNew
          context:(void *)(AVCaptureStillImageIsCapturingStillImageContext)];
  if ([session canAddOutput:stillImageOutput])
    [session addOutput:stillImageOutput];
  videoDataOutput = [AVCaptureVideoDataOutput new];
  NSDictionary *rgbOutputSettings = [NSDictionary
      dictionaryWithObject:[NSNumber numberWithInt:kCMPixelFormat_32BGRA]
                    forKey:(id)kCVPixelBufferPixelFormatTypeKey];
  [videoDataOutput setVideoSettings:rgbOutputSettings];
  [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];
  videoDataOutputQueue =
      dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL);
  [videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue];
  if ([session canAddOutput:videoDataOutput])
    [session addOutput:videoDataOutput];
  [[videoDataOutput connectionWithMediaType:AVMediaTypeVideo] setEnabled:YES];
  previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
  [previewLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
  [previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
  CALayer *rootLayer = [previewView layer];
  [rootLayer setMasksToBounds:YES];
  [previewLayer setFrame:[rootLayer bounds]];
  [rootLayer addSublayer:previewLayer];
  [session startRunning];
  if (error) {
    NSString *title = [NSString stringWithFormat:@"Failed with error %d", (int)[error code]];
    UIAlertController *alertController =
        [UIAlertController alertControllerWithTitle:title
                                            message:[error localizedDescription]
                                     preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *dismiss =
        [UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleDefault handler:nil];
    [alertController addAction:dismiss];
    [self presentViewController:alertController animated:YES completion:nil];
    [self teardownAVCapture];
  }
}
- (void)teardownAVCapture {
  [stillImageOutput removeObserver:self forKeyPath:@"isCapturingStillImage"];
  [previewLayer removeFromSuperlayer];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
  if (context == AVCaptureStillImageIsCapturingStillImageContext) {
    BOOL isCapturingStillImage =
        [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
    if (isCapturingStillImage) {
      // do flash bulb like animation
      flashView = [[UIView alloc] initWithFrame:[previewView frame]];
      [flashView setBackgroundColor:[UIColor whiteColor]];
      [flashView setAlpha:0.f];
      [[[self view] window] addSubview:flashView];
      [UIView animateWithDuration:.4f
                       animations:^{
                         [flashView setAlpha:1.f];
                       }];
    } else {
      [UIView animateWithDuration:.4f
          animations:^{
            [flashView setAlpha:0.f];
          }
          completion:^(BOOL finished) {
           [flashView removeFromSuperview];
            flashView = nil;
          }];
    }
  }
}
- (AVCaptureVideoOrientation)avOrientationForDeviceOrientation:
    (UIDeviceOrientation)deviceOrientation {
  AVCaptureVideoOrientation result =
      (AVCaptureVideoOrientation)(deviceOrientation);
  if (deviceOrientation == UIDeviceOrientationLandscapeLeft)
    result = AVCaptureVideoOrientationLandscapeRight;
  else if (deviceOrientation == UIDeviceOrientationLandscapeRight)
    result = AVCaptureVideoOrientationLandscapeLeft;
  return result;
}
- (IBAction)takePicture:(id)sender {
  if ([session isRunning]) {
    [session stopRunning];
    [sender setTitle:@"Continue" forState:UIControlStateNormal];
    flashView = [[UIView alloc] initWithFrame:[previewView frame]];
    [flashView setBackgroundColor:[UIColor whiteColor]];
    [flashView setAlpha:0.f];
    [[[self view] window] addSubview:flashView];
    [UIView animateWithDuration:.2f
        animations:^{
          [flashView setAlpha:1.f];
        }
        completion:^(BOOL finished) {
          [UIView animateWithDuration:.2f
              animations:^{
                [flashView setAlpha:0.f];
              }
              completion:^(BOOL finished) {
                [flashView removeFromSuperview];
                flashView = nil;
              }];
        }];
  } else {
    [session startRunning];
    [sender setTitle:@"Freeze Frame" forState:UIControlStateNormal];
  }
}
+ (CGRect)videoPreviewBoxForGravity:(NSString *)gravity
                          frameSize:(CGSize)frameSize
                       apertureSize:(CGSize)apertureSize {
  CGFloat apertureRatio = apertureSize.height / apertureSize.width;
  CGFloat viewRatio = frameSize.width / frameSize.height;
  CGSize size = CGSizeZero;
  if ([gravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) {
    if (viewRatio > apertureRatio) {
      size.width = frameSize.width;
      size.height =
          apertureSize.width * (frameSize.width / apertureSize.height);
    } else {
      size.width =
          apertureSize.height * (frameSize.height / apertureSize.width);
      size.height = frameSize.height;
   }
  } else if ([gravity isEqualToString:AVLayerVideoGravityResizeAspect]) {
    if (viewRatio > apertureRatio) {
      size.width =
          apertureSize.height * (frameSize.height / apertureSize.width);
      size.height = frameSize.height;
    } else {
      size.width = frameSize.width;
      size.height =
          apertureSize.width * (frameSize.width / apertureSize.height);
    }
  } else if ([gravity isEqualToString:AVLayerVideoGravityResize]) {
    size.width = frameSize.width;
    size.height = frameSize.height;
  }
  CGRect videoBox;
  videoBox.size = size;
  if (size.width < frameSize.width)
    videoBox.origin.x = (frameSize.width - size.width) / 2;
  else
    videoBox.origin.x = (size.width - frameSize.width) / 2;
  if (size.height < frameSize.height)
    videoBox.origin.y = (frameSize.height - size.height) / 2;
  else
    videoBox.origin.y = (size.height - frameSize.height) / 2;
  return videoBox;
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  CFRetain(pixelBuffer);
  [self runCNNOnFrame:pixelBuffer];
  CFRelease(pixelBuffer);
}
- (void)runCNNOnFrame:(CVPixelBufferRef)pixelBuffer {
  assert(pixelBuffer != NULL);
  OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
  int doReverseChannels;
  if (kCVPixelFormatType_32ARGB == sourcePixelFormat) {
    doReverseChannels = 1;
  } else if (kCVPixelFormatType_32BGRA == sourcePixelFormat) {
    doReverseChannels = 0;
  } else {
    assert(false);  // Unknown source format
  }
  const int sourceRowBytes = (int)CVPixelBufferGetBytesPerRow(pixelBuffer);
  const int image_width = (int)CVPixelBufferGetWidth(pixelBuffer);
  const int fullHeight = (int)CVPixelBufferGetHeight(pixelBuffer);
  CVPixelBufferLockFlags unlockFlags = kNilOptions;
  CVPixelBufferLockBaseAddress(pixelBuffer, unlockFlags);
  unsigned char *sourceBaseAddr =
      (unsigned char *)(CVPixelBufferGetBaseAddress(pixelBuffer));
  int image_height;
  unsigned char *sourceStartAddr;
  if (fullHeight <= image_width) {
    image_height = fullHeight;
    sourceStartAddr = sourceBaseAddr;
  } else {
    image_height = image_width;
    const int marginY = ((fullHeight - image_width) / 2);
    sourceStartAddr = (sourceBaseAddr + (marginY * sourceRowBytes));
  }
  const int image_channels = 4;
  assert(image_channels >= wanted_input_channels);
  tensorflow::Tensor image_tensor(
      tensorflow::DT_FLOAT,
      tensorflow::TensorShape(
          {1, wanted_input_height, wanted_input_width, wanted_input_channels}));
  auto image_tensor_mapped = image_tensor.tensor<float, 4>();
  tensorflow::uint8 *in = sourceStartAddr;
  float *out = image_tensor_mapped.data();
  for (int y = 0; y < wanted_input_height; ++y) {
    float *out_row = out + (y * wanted_input_width * wanted_input_channels);
    for (int x = 0; x < wanted_input_width; ++x) {
      const int in_x = (y * image_width) / wanted_input_width;
      const int in_y = (x * image_height) / wanted_input_height;
      tensorflow::uint8 *in_pixel =
          in + (in_y * image_width * image_channels) + (in_x * image_channels);
      float *out_pixel = out_row + (x * wanted_input_channels);
      for (int c = 0; c < wanted_input_channels; ++c) {
        out_pixel[c] = (in_pixel[c] - input_mean) / input_std;
      }
    }
  }
  CVPixelBufferUnlockBaseAddress(pixelBuffer, unlockFlags);
  if (tf_session.get()) {
    std::vector<tensorflow::Tensor> outputs;
    tensorflow::Status run_status = tf_session->Run(
        {{input_layer_name, image_tensor}}, {output_layer_name}, {}, &outputs);
    if (!run_status.ok()) {
      LOG(ERROR) << "Running model failed:" << run_status;
    } else {
      tensorflow::Tensor *output = &outputs[0];
      auto predictions = output->flat<float>();
      NSMutableDictionary *newValues = [NSMutableDictionary dictionary];
      for (int index = 0; index < predictions.size(); index += 1) {
        const float predictionValue = predictions(index);
        if (predictionValue > 0.05f) {
          std::string label = labels[index % predictions.size()];
          NSString *labelObject = [NSString stringWithUTF8String:label.c_str()];
          NSNumber *valueObject = [NSNumber numberWithFloat:predictionValue];
          [newValues setObject:valueObject forKey:labelObject];
        }
      }
      dispatch_async(dispatch_get_main_queue(), ^(void) {
        [self setPredictionValues:newValues];
      });
    }
  }
  CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
}
- (void)dealloc {
  [self teardownAVCapture];
}
// use front/back camera
- (IBAction)switchCameras:(id)sender {
  AVCaptureDevicePosition desiredPosition;
  if (isUsingFrontFacingCamera)
    desiredPosition = AVCaptureDevicePositionBack;
  else
    desiredPosition = AVCaptureDevicePositionFront;
  for (AVCaptureDevice *d in
       [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
    if ([d position] == desiredPosition) {
      [[previewLayer session] beginConfiguration];
      AVCaptureDeviceInput *input =
          [AVCaptureDeviceInput deviceInputWithDevice:d error:nil];
      for (AVCaptureInput *oldInput in [[previewLayer session] inputs]) {
        [[previewLayer session] removeInput:oldInput];
      }
      [[previewLayer session] addInput:input];
      [[previewLayer session] commitConfiguration];
      break;
    }
  }
  isUsingFrontFacingCamera = !isUsingFrontFacingCamera;
}
- (void)didReceiveMemoryWarning {
  [super didReceiveMemoryWarning];
}
- (void)viewDidLoad {
  [super viewDidLoad];
  square = [UIImage imageNamed:@"squarePNG"];
  synth = [[AVSpeechSynthesizer alloc] init];
  labelLayers = [[NSMutableArray alloc] init];
  oldPredictionValues = [[NSMutableDictionary alloc] init];
  tensorflow::Status load_status;
  if (model_uses_memory_mapping) {
    load_status = LoadMemoryMappedModel(
        model_file_name, model_file_type, &tf_session, &tf_memmapped_env);
  } else {
    load_status = LoadModel(model_file_name, model_file_type, &tf_session);
  }
  if (!load_status.ok()) {
    LOG(FATAL) << "Couldn't load model: " << load_status;
  }
  tensorflow::Status labels_status =
      LoadLabels(labels_file_name, labels_file_type, &labels);
  if (!labels_status.ok()) {
    LOG(FATAL) << "Couldn't load labels: " << labels_status;
  }
  [self setupAVCapture];
}
- (void)viewDidUnload {
  [super viewDidUnload];
}
- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated {
  [super viewDidDisappear:animated];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:
    (UIInterfaceOrientation)interfaceOrientation {
  return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
- (BOOL)prefersStatusBarHidden {
  return YES;
}
- (void)setPredictionValues:(NSDictionary *)newValues {
  const float decayValue = 0.75f;
  const float updateValue = 0.25f;
  const float minimumThreshold = 0.01f;
  NSMutableDictionary *decayedPredictionValues =
      [[NSMutableDictionary alloc] init];
  for (NSString *label in oldPredictionValues) {
    NSNumber *oldPredictionValueObject =
        [oldPredictionValues objectForKey:label];
    const float oldPredictionValue = [oldPredictionValueObject floatValue];
    const float decayedPredictionValue = (oldPredictionValue * decayValue);
    if (decayedPredictionValue > minimumThreshold) {
      NSNumber *decayedPredictionValueObject =
          [NSNumber numberWithFloat:decayedPredictionValue];
      [decayedPredictionValues setObject:decayedPredictionValueObject
                                  forKey:label];
    }
  }
  oldPredictionValues = decayedPredictionValues;
  for (NSString *label in newValues) {
    NSNumber *newPredictionValueObject = [newValues objectForKey:label];
    NSNumber *oldPredictionValueObject =
        [oldPredictionValues objectForKey:label];
    if (!oldPredictionValueObject) {
      oldPredictionValueObject = [NSNumber numberWithFloat:0.0f];
    }
    const float newPredictionValue = [newPredictionValueObject floatValue];
    const float oldPredictionValue = [oldPredictionValueObject floatValue];
    const float updatedPredictionValue =
        (oldPredictionValue + (newPredictionValue * updateValue));
    NSNumber *updatedPredictionValueObject =
        [NSNumber numberWithFloat:updatedPredictionValue];
    [oldPredictionValues setObject:updatedPredictionValueObject forKey:label];
  }
  NSArray *candidateLabels = [NSMutableArray array];
  for (NSString *label in oldPredictionValues) {
    NSNumber *oldPredictionValueObject =
        [oldPredictionValues objectForKey:label];
    const float oldPredictionValue = [oldPredictionValueObject floatValue];
    if (oldPredictionValue > 0.05f) {
      NSDictionary *entry = @{
        @"label" : label,
        @"value" : oldPredictionValueObject
      };
      candidateLabels = [candidateLabels arrayByAddingObject:entry];
    }
  }
  NSSortDescriptor *sort =
      [NSSortDescriptor sortDescriptorWithKey:@"value" ascending:NO];
  NSArray *sortedLabels = [candidateLabels
      sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]];
  const float leftMargin = 10.0f;
  const float topMargin = 10.0f;
  const float valueWidth = 48.0f;
  const float valueHeight = 26.0f;
  const float labelWidth = 246.0f;
  const float labelHeight = 26.0f;
  const float labelMarginX = 5.0f;
  const float labelMarginY = 5.0f;
  [self removeAllLabelLayers];
  int labelCount = 0;
  for (NSDictionary *entry in sortedLabels) {
    NSString *label = [entry objectForKey:@"label"];
    NSNumber *valueObject = [entry objectForKey:@"value"];
    const float value = [valueObject floatValue];
    const float originY =
        (topMargin + ((labelHeight + labelMarginY) * labelCount));
    const int valuePercentage = (int)roundf(value * 100.0f);
    const float valueOriginX = leftMargin;
    NSString *valueText = [NSString stringWithFormat:@"%d%%", valuePercentage];
    [self addLabelLayerWithText:valueText
                        originX:valueOriginX
                        originY:originY
                          width:valueWidth
                         height:valueHeight
                      alignment:kCAAlignmentRight];
    const float labelOriginX = (leftMargin + valueWidth + labelMarginX);
    [self addLabelLayerWithText:[label capitalizedString]
                        originX:labelOriginX
                        originY:originY
                          width:labelWidth
                         height:labelHeight
                      alignment:kCAAlignmentLeft];
    if ((labelCount == 0) && (value > 0.5f)) {
      [self speak:[label capitalizedString]];
    }
    labelCount += 1;
    if (labelCount > 4) {
      break;
    }
  }
}
- (void)removeAllLabelLayers {
  for (CATextLayer *layer in labelLayers) {
    [layer removeFromSuperlayer];
  }
  [labelLayers removeAllObjects];
}
- (void)addLabelLayerWithText:(NSString *)text
                      originX:(float)originX
                      originY:(float)originY
                        width:(float)width
                       height:(float)height
                    alignment:(NSString *)alignment {
  CFTypeRef font = (CFTypeRef) @"Menlo-Regular";
  const float fontSize = 20.0f;
  const float marginSizeX = 5.0f;
  const float marginSizeY = 2.0f;
  const CGRect backgroundBounds = CGRectMake(originX, originY, width, height);
  const CGRect textBounds =
      CGRectMake((originX + marginSizeX), (originY + marginSizeY),
                 (width - (marginSizeX * 2)), (height - (marginSizeY * 2)));
  CATextLayer *background = [CATextLayer layer];
  [background setBackgroundColor:[UIColor blackColor].CGColor];
  [background setOpacity:0.5f];
  [background setFrame:backgroundBounds];
  background.cornerRadius = 5.0f;
  [[self.view layer] addSublayer:background];
  [labelLayers addObject:background];
  CATextLayer *layer = [CATextLayer layer];
  [layer setForegroundColor:[UIColor whiteColor].CGColor];
  [layer setFrame:textBounds];
  [layer setAlignmentMode:alignment];
  [layer setWrapped:YES];
  [layer setFont:font];
  [layer setFontSize:fontSize];
  layer.contentsScale = [[UIScreen mainScreen] scale];
  [layer setString:text];
  [[self.view layer] addSublayer:layer];
  [labelLayers addObject:layer];
}
- (void)setPredictionText:(NSString *)text withDuration:(float)duration {
  if (duration > 0.0) {
    CABasicAnimation *colorAnimation =
        [CABasicAnimation animationWithKeyPath:@"foregroundColor"];
    colorAnimation.duration = duration;
    colorAnimation.fillMode = kCAFillModeForwards;
    colorAnimation.removedOnCompletion = NO;
    colorAnimation.fromValue = (id)[UIColor darkGrayColor].CGColor;
    colorAnimation.toValue = (id)[UIColor whiteColor].CGColor;
    colorAnimation.timingFunction =
        [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    [self.predictionTextLayer addAnimation:colorAnimation
                                    forKey:@"colorAnimation"];
  } else {
    self.predictionTextLayer.foregroundColor = [UIColor whiteColor].CGColor;
  }
  [self.predictionTextLayer removeFromSuperlayer];
  [[self.view layer] addSublayer:self.predictionTextLayer];
  [self.predictionTextLayer setString:text];
}
- (void)speak:(NSString *)words {
  if ([synth isSpeaking]) {
    return;
  }
  AVSpeechUtterance *utterance =
      [AVSpeechUtterance speechUtteranceWithString:words];
  utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"en-US"];
  utterance.rate = 0.75 * AVSpeechUtteranceDefaultSpeechRate;
  [synth speakUtterance:utterance];
}
@end

連上iPhone手機(jī),雙擊tensorflow/contrib/ios_examples/camera/camera_example.xcodeproj編譯運(yùn)行盾戴。手機(jī)安裝好APP寄锐,打開(kāi)APP,找到玫瑰花識(shí)別尖啡。訓(xùn)練迭代次數(shù)10000次后橄仆,識(shí)別率99%以上。模擬器打包衅斩,生成打包工程文件位于/Users/libinggen/Library/Developer/Xcode/DeriveData/camera_example-dhfdsdfesfmrwtfb1fpfkfjsdfhdskf/Build/Products/Debug-iphoneos盆顾。打開(kāi)CameraExample.app您宪,有可執(zhí)行文件CameraExample惫搏、資源文件模型文件mmapped_graph.pb筐赔、標(biāo)記文件retrained_labels.txt。

Android系統(tǒng)實(shí)踐茴丰。

環(huán)境準(zhǔn)備天吓。MacBook Pro。Oracle官網(wǎng)下載JDK1.8版本龄寞。http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 汰规。jdk-8u111-macosx-x64.dmg。雙擊安裝物邑。設(shè)置Java環(huán)境變量:

JAVA_HOME='/usr/libexec/java_home'
export JAVA_HOME

搭建Android SDK環(huán)境溜哮。Android官網(wǎng)下載Android SDK,https://developer.android.com 色解。25.0.2版本茂嗓。android-sdk_r25.0.2-macosx.zip。解壓到~/Library/Android/sdk目錄科阎。build-tools述吸、extras、patcher锣笨、platform-tools #各版本SDK 根據(jù)API Level劃分SDK版本蝌矛、platforms、sources错英、system-images入撒、temp #臨時(shí)文件夾 在SDK更新安裝時(shí)用到、tools #各版本通用SDK工具 有adb走趋、aapt衅金、aidl噪伊、dx文件簿煌。
搭建Android NDK環(huán)境。Android官網(wǎng)下載Android NDK Mac OS X版本鉴吹,https://developer.android.com/ndk/downloads/index.html 姨伟。android-ndk-r13b-darwin-x86_64.zip文件。解壓豆励,CHANGELOG.md夺荒、build、ndk-build伍玖、ndk-depends窍箍、ndk-gdb椰棘、ndk-stack邪狞、ndk-which帆卓、platforms鳞疲、prebuilt尚洽、python-packages腺毫、shader-tools潮酒、simpleperf急黎、source.properties勃教、sources故源、toolchains汞贸。搭建Bazel。brew安裝bazel:

brew install bazel

更新bazel:

brew upgrade bazel

編譯演示程序運(yùn)行射赛。修改tensorflow-1.1.0根目錄WORKSPACE文件咒劲。android_sdk_repository腐魂、android_ndk_repository配置改為用戶(hù)自己安裝目錄蛔屹、版本兔毒。

android_sdk_repository(
    name = "androidsdk",
    api_level = 25,
    build_tools_version = "25.0.2",
    # Replace with path to Android SDK on your system
    path = "~/Library/Android/sdk"
)
android_ndk_repository(
    name = "androidndk",
    api_level = 23,
    path = "~/Downloads/android-ndk-r13b"
)

在根目錄用bazel構(gòu)建:

bazel build // tensorflow/examples/android:tensorflow_demo

編譯成功育叁,默認(rèn)在tensorflow-1.1.0/bazel-bin/tensorflow/examples/android目錄生成TensorFlow演示程序豪嗽。
運(yùn)行龟梦。生成apk文件傳輸?shù)绞謾C(jī)计贰,手機(jī)攝像頭看效果躁倒。Android 6.0.1秧秉。開(kāi)啟“開(kāi)發(fā)者模式”福贞。手機(jī)用數(shù)據(jù)線(xiàn)與計(jì)算機(jī)相連停士,進(jìn)入SDK所在目錄恋技,進(jìn)入platform-tools文件夾蜻底,找到adb命令薄辅,執(zhí)行:

./adb install tensorflow-0.12/bazel-bin/tensorflow/examples/android/tensorflow_demo.apk

tensorflow_demo.apk自動(dòng)安裝到手機(jī)站楚。打開(kāi)TF Detec App窿春。App 調(diào)起手機(jī)攝像頭旧乞,攝像頭返回?cái)?shù)據(jù)流實(shí)時(shí)監(jiān)測(cè)尺栖。

自定義模型編譯運(yùn)行延赌。訓(xùn)練原始模型皮胡、編譯Android系統(tǒng)支持模型屡贺、生成Android apk文件運(yùn)行。
訓(xùn)練原始模型泻仙、編譯Android系統(tǒng)支持模型玉转。用項(xiàng)目根目錄tensorflow/python/tools/optimize_for_inference.py究抓、tensorflow/tools/quantization/quantize_graph.py刺下、tensorflow/contrib/util/convert_graphdef_memmapped_format.cc對(duì)模型優(yōu)化橘茉。將第一步生成原始模型文件retrained_graph.pb畅卓、標(biāo)記文件retrained_labels.txt放在tensorflow/examples/android/assets目錄翁潘。修改tensorflow/examples/android/src/org/tensorflow/demo/TensorFlowImageClassifier.java要加載模型文件名稱(chēng)唐础,輸入圖片尺寸一膨、操作節(jié)點(diǎn)名字豹绪、縮放像素大小瞒津。

package org.tensorflow.demo;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.os.Trace;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Vector;
import org.tensorflow.Operation;
import org.tensorflow.contrib.android.TensorFlowInferenceInterface;
/** A classifier specialized to label images using TensorFlow. */
public class TensorFlowImageClassifier implements Classifier {
  private static final String TAG = "TensorFlowImageClassifier";
  // Only return this many results with at least this confidence.
  private static final int MAX_RESULTS = 3;
  private static final float THRESHOLD = 0.1f;
  // Config values.
  private String inputName;
  private String outputName;
  private int inputSize;
  private int imageMean;
  private float imageStd;
  // Pre-allocated buffers.
  private Vector<String> labels = new Vector<String>();
  private int[] intValues;
  private float[] floatValues;
  private float[] outputs;
  private String[] outputNames;
  private boolean logStats = false;
  private TensorFlowInferenceInterface inferenceInterface;
  private TensorFlowImageClassifier() {}
  /**
   * Initializes a native TensorFlow session for classifying images.
   *
   * @param assetManager The asset manager to be used to load assets.
   * @param modelFilename The filepath of the model GraphDef protocol buffer.
   * @param labelFilename The filepath of label file for classes.
   * @param inputSize The input size. A square image of inputSize x inputSize is assumed.
   * @param imageMean The assumed mean of the image values.
   * @param imageStd The assumed std of the image values.
   * @param inputName The label of the image input node.
   * @param outputName The label of the output node.
   * @throws IOException
   */
  public static Classifier create(
      AssetManager assetManager,
      String modelFilename,
      String labelFilename,
      int inputSize,
      int imageMean,
      float imageStd,
      String inputName,
      String outputName) {
    TensorFlowImageClassifier c = new TensorFlowImageClassifier();
    c.inputName = inputName;
    c.outputName = outputName;
    // Read the label names into memory.
    // TODO(andrewharp): make this handle non-assets.
    String actualFilename = labelFilename.split("file:///android_asset/")[1];
    Log.i(TAG, "Reading labels from: " + actualFilename);
    BufferedReader br = null;
    try {
      br = new BufferedReader(new InputStreamReader(assetManager.open(actualFilename)));
      String line;
      while ((line = br.readLine()) != null) {
        c.labels.add(line);
      }
      br.close();
    } catch (IOException e) {
      throw new RuntimeException("Problem reading label file!" , e);
    }
    c.inferenceInterface = new TensorFlowInferenceInterface(assetManager, modelFilename);
    // The shape of the output is [N, NUM_CLASSES], where N is the batch size.
    final Operation operation = c.inferenceInterface.graphOperation(outputName);
    final int numClasses = (int) operation.output(0).shape().size(1);
    Log.i(TAG, "Read " + c.labels.size() + " labels, output layer size is " + numClasses);
    // Ideally, inputSize could have been retrieved from the shape of the input operation.  Alas,
    // the placeholder node for input in the graphdef typically used does not specify a shape, so it
    // must be passed in as a parameter.
    c.inputSize = inputSize;
    c.imageMean = imageMean;
    c.imageStd = imageStd;
    // Pre-allocate buffers.
    c.outputNames = new String[] {outputName};
    c.intValues = new int[inputSize * inputSize];
    c.floatValues = new float[inputSize * inputSize * 3];
    c.outputs = new float[numClasses];
    return c;
  }
  @Override
  public List<Recognition> recognizeImage(final Bitmap bitmap) {
    // Log this method so that it can be analyzed with systrace.
    Trace.beginSection("recognizeImage");
    Trace.beginSection("preprocessBitmap");
    // Preprocess the image data from 0-255 int to normalized float based
    // on the provided parameters.
    bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
    for (int i = 0; i < intValues.length; ++i) {
      final int val = intValues[i];
      floatValues[i * 3 + 0] = (((val >> 16) & 0xFF) - imageMean) / imageStd;
      floatValues[i * 3 + 1] = (((val >> 8) & 0xFF) - imageMean) / imageStd;
      floatValues[i * 3 + 2] = ((val & 0xFF) - imageMean) / imageStd;
    }
    Trace.endSection();
    // Copy the input data into TensorFlow.
    Trace.beginSection("feed");
    inferenceInterface.feed(inputName, floatValues, 1, inputSize, inputSize, 3);
    Trace.endSection();
    // Run the inference call.
    Trace.beginSection("run");
    inferenceInterface.run(outputNames, logStats);
    Trace.endSection();
    // Copy the output Tensor back into the output array.
    Trace.beginSection("fetch");
    inferenceInterface.fetch(outputName, outputs);
    Trace.endSection();
    // Find the best classifications.
    PriorityQueue<Recognition> pq =
        new PriorityQueue<Recognition>(
            3,
            new Comparator<Recognition>() {
              @Override
              public int compare(Recognition lhs, Recognition rhs) {
                // Intentionally reversed to put high confidence at the head of the queue.
                return Float.compare(rhs.getConfidence(), lhs.getConfidence());
              }
            });
    for (int i = 0; i < outputs.length; ++i) {
      if (outputs[i] > THRESHOLD) {
        pq.add(
            new Recognition(
                "" + i, labels.size() > i ? labels.get(i) : "unknown", outputs[i], null));
      }
    }
    final ArrayList<Recognition> recognitions = new ArrayList<Recognition>();
    int recognitionsSize = Math.min(pq.size(), MAX_RESULTS);
    for (int i = 0; i < recognitionsSize; ++i) {
      recognitions.add(pq.poll());
    }
    Trace.endSection(); // "recognizeImage"
    return recognitions;
  }
  @Override
  public void enableStatLogging(boolean logStats) {
    this.logStats = logStats;
  }
  @Override
  public String getStatString() {
    return inferenceInterface.getStatString();
  }
  @Override
  public void close() {
    inferenceInterface.close();
  }
}

重新編譯apk啦膜,連接Android手機(jī)僧家,安裝apk:

bazel buld //tensorflow/examples/android:tensorflow_demo
adb install -r -g bazel-bin/tensorflow/examples/android/tensorflow_demo.apk

樹(shù)莓派實(shí)踐八拱。

Tensorflow可以在樹(shù)莓派(Raspberry Pi)運(yùn)行清蚀。樹(shù)莓派轧铁,只有信用卡大小微型電腦旦棉,系統(tǒng)基于Linux绑洛,有音頻真屯、視頻功能绑蔫。應(yīng)用配深,輸入1萬(wàn)張自己的面部圖片篓叶,在樹(shù)莓派訓(xùn)練人臉識(shí)別模型羞秤,教會(huì)它認(rèn)識(shí)你瘾蛋,你進(jìn)入家門(mén)后哺哼,幫你開(kāi)燈幸斥、播放音樂(lè)各種功能甲葬。樹(shù)莓派編譯方法和直接在Linux環(huán)境上用相似。

參考資料:
《TensorFlow技術(shù)解析與實(shí)戰(zhàn)》

歡迎推薦上海機(jī)器學(xué)習(xí)工作機(jī)會(huì)梭灿,我的微信:qingxingfengzi

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末堡妒,一起剝皮案震驚了整個(gè)濱河市皮迟,隨后出現(xiàn)的幾起案子伏尼,更是在濱河造成了極大的恐慌爆阶,老刑警劉巖辨图,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件故河,死亡現(xiàn)場(chǎng)離奇詭異忧勿,居然都是意外死亡鸳吸,警方通過(guò)查閱死者的電腦和手機(jī)晌砾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)养匈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)呕乎,“玉大人猬仁,你說(shuō)我怎么就攤上這事湿刽≌┕耄” “怎么了雅镊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵漓穿,是天一觀(guān)的道長(zhǎng)晃危。 經(jīng)常有香客問(wèn)我僚饭,道長(zhǎng)丈钙,這世上最難降的妖魔是什么雇锡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任摩钙,我火速辦了婚禮贪薪,結(jié)果婚禮上画切,老公的妹妹穿的比我還像新娘霍弹。我一直安慰自己典格,他們只是感情好耍缴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布债沮。 她就那樣靜靜地躺著疫衩,像睡著了一般荣德。 火紅的嫁衣襯著肌膚如雪涮瞻。 梳的紋絲不亂的頭發(fā)上署咽,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天窒升,我揣著相機(jī)與錄音慕匠,去河邊找鬼台谊。 笑死锅铅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的号杠。 我是一名探鬼主播姨蟋,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼眼溶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼堂飞!你這毒婦竟也來(lái)了绰筛?” 一聲冷哼從身側(cè)響起铝噩,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤骏庸,失蹤者是張志新(化名)和其女友劉穎具被,沒(méi)想到半個(gè)月后一姿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辈挂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜂林。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片噪叙。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡睁蕾,死狀恐怖子眶,靈堂內(nèi)的尸體忽然破棺而出臭杰,到底是詐尸還是另有隱情渴杆,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布囊拜,位于F島的核電站艾疟,受9級(jí)特大地震影響蔽莱,放射性物質(zhì)發(fā)生泄漏戚长。R本人自食惡果不足惜同廉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一锅劝、第九天 我趴在偏房一處隱蔽的房頂上張望蟆湖。 院中可真熱鬧隅津,春花似錦伦仍、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)确买。三九已至湾趾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铛楣,已是汗流浹背簸州。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工岸浑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矢洲,地道東北人读虏。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓盖桥,卻偏偏與公主長(zhǎng)得像揩徊,于是被迫代替她去往敵國(guó)和親藐握。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猾普,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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