0#04 caffe 的使用

1. 前言

    学习机器学习,一般并不会从 caffe 开始启蒙,一般都是使用 pytorch,tensorflow 等偏向于 python 的工具开始,所以讲解 caffe 的使用,默认大家对其他的 python 类开发工具有所了解.
    这次讲解 caffe 的使用,使用的是MNIST 手写数字识别,首先是因为,最容易上手,并且正确率能达到比较高的水平,另外就是 caffe 的example 路径下有 MNIST相关的内容.
需要的预备知识:
    C/C++
    linux shell 脚本
如果没有相关知识,也可以试着读一下.

2. MNIST 数据文件下载

    在路径 "caffe 路径/examples/mnist" 路径下,含有完整的 caffe 的 MNIST 项目.
    在路径 "caffe 路径/build/examples/mnist" 路径下,有对 caffe 编译后,产生的对 caffe 的 MNIST 项目编译的结果.
首先来看 下载 MNIST 的脚本: " caffe 路径/data/mnist/get_mnist.sh "

#!/usr/bin/env sh
# 这个脚本用于下载并解压 mnist 文件
DIR="$( cd "$(dirname "$0")" ; pwd -P )"
cd "$DIR"
# printf
echo "Downloading..."
# for循环 设置要下载文件的名称
for fname in train-images-idx3-ubyte train-labels-idx1-ubyte t10k-images-idx3-ubyte t10k-labels-idx1-ubyte
do
    if [ ! -e $fname ]; then
    # 进行下载文件
        wget --no-check-certificate http://yann.lecun.com/exdb/mnist/${fname}.gz
    # 进行解压文件
        gunzip ${fname}.gz
    fi
done

使用 "./get_mnist.sh" 运行该程序.显示如下:

lee@lee:~/Documents/caffe/data/mnist$ ./get_mnist.sh 
Downloading...
--2019-02-26 13:39:50--  http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Resolving yann.lecun.com (yann.lecun.com)... 216.165.22.6
Connecting to yann.lecun.com (yann.lecun.com)|216.165.22.6|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 9912422 (9.5M) [application/x-gzip]
Saving to: ‘train-images-idx3-ubyte.gz’

train-images-idx3-u 100%[===================>]   9.45M   199KB/s    in 49s     

2019-02-26 13:40:40 (197 KB/s) - ‘train-images-idx3-ubyte.gz’ saved [9912422/9912422]
......
t10k-labels-idx1-ub 100%[===================>]   4.44K  --.-KB/s    in 0s      

2019-02-26 13:40:52 (152 MB/s) - ‘t10k-labels-idx1-ubyte.gz’ saved [4542/4542]

下载的数据是二进制格式,我们可以使用 vim 以二进制格式打开命令

vim -b t10k-images-idx3-ubyte
并在 vim 下方输入 ":%!xxd" 将文本文件强制以十六进制转换

如下


图片.png

注:似乎,打开过文件会被破坏,建议删除重新执行一次.
但是问题出现了,caffe 并不支持 二进制文件,只支持LMDB 或者levelDB,所以我们需要将数据进行转换.进行数据转换的脚本在 " caffe路径/examples/mnist/create_mnist.sh"
进入到 " caffe路径 ",进行执行 "./examples/mnist/create_mnist.sh",显示如下:

lee@lee:~/Documents/caffe$ ./examples/mnist/create_mnist.sh 
Creating lmdb...
I0226 14:39:08.853714  4944 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdb
I0226 14:39:08.853905  4944 convert_mnist_data.cpp:88] A total of 60000 items.
I0226 14:39:08.853924  4944 convert_mnist_data.cpp:89] Rows: 28 Cols: 28
I0226 14:39:13.977677  4944 convert_mnist_data.cpp:108] Processed 60000 files.
F0226 14:39:13.994539  4953 convert_mnist_data.cpp:59] Check failed: magic == 2051 (808464432 vs. 2051) Incorrect image file magic.
*** Check failure stack trace: ***
    @     0x7f71094415cd  google::LogMessage::Fail()
    @     0x7f7109443433  google::LogMessage::SendToLog()
    @     0x7f710944115b  google::LogMessage::Flush()
    @     0x7f7109443e1e  google::LogMessageFatal::~LogMessageFatal()
    @           0x4031d3  convert_dataset()
    @           0x40226a  main
    @     0x7f71087c2b17  (unknown)
    @           0x4022ba  _start
    @              (nil)  (unknown)
Aborted (core dumped)

接下来分析一下 "create_mnist.sh" 文件

#!/usr/bin/env sh
# This script converts the mnist data into lmdb/leveldb format,
# depending on the value assigned to $BACKEND.
set -e
# 定义一些变量
EXAMPLE=examples/mnist
DATA=data/mnist
BUILD=build/examples/mnist

BACKEND="lmdb"
# printf
echo "Creating ${BACKEND}..."

# 下载之前,删除之前下载的内容
rm -rf $EXAMPLE/mnist_train_${BACKEND}
rm -rf $EXAMPLE/mnist_test_${BACKEND}

# 将编译后的可执行文件,加上参数进行运行(主要内容)
$BUILD/convert_mnist_data.bin $DATA/train-images-idx3-ubyte \
  $DATA/train-labels-idx1-ubyte $EXAMPLE/mnist_train_${BACKEND} --backend=${BACKEND}
$BUILD/convert_mnist_data.bin $DATA/t10k-images-idx3-ubyte \
  $DATA/t10k-labels-idx1-ubyte $EXAMPLE/mnist_test_${BACKEND} --backend=${BACKEND}

echo "Done."

很明显最重要的内容就是

$BUILD/convert_mnist_data.bin $DATA/train-images-idx3-ubyte \
  $DATA/train-labels-idx1-ubyte $EXAMPLE/mnist_train_${BACKEND} --backend=${BACKEND}
$BUILD/convert_mnist_data.bin $DATA/t10k-images-idx3-ubyte \
  $DATA/t10k-labels-idx1-ubyte $EXAMPLE/mnist_test_${BACKEND} --backend=${BACKEND}

但是 " convert_mnist_data.bin " 是编译后的二进制文件,并没有任何信息,所以我们应该去了解 " caffe目录/example/mnist/convert_mnist_data.cpp " 这个文件的内容.
C 语言都是从 main 函数开始阅读.

/* 主函数 包含 额外参数(int argc, char** argv)
   使用方法,
   convert_mnist_data.bin 图片文件路径 标签文件路径 生成数据库文件路径 --backend=使用数据库类型
   例如
   convert_mnist_data.bin t10k-images-ubyte t10k-labels-ubyte mnist_test_lmdb --backend=lmdb
*/
int main(int argc, char** argv) {
// 条件编译 实际中我们定义了 #include <gflags/gflags.h>
#ifndef GFLAGS_GFLAGS_H_
  namespace gflags = google;
#endif

  FLAGS_alsologtostderr = 1;
  // SetUsageMessage: 设置命令行帮助信息
  gflags::SetUsageMessage("This script converts the MNIST dataset to\n"
        "the lmdb/leveldb format used by Caffe to load data.\n"
        "Usage:\n"
        "    convert_mnist_data [FLAGS] input_image_file input_label_file "
        "output_db_file\n"
        "The MNIST dataset could be downloaded at\n"
        "    http://yann.lecun.com/exdb/mnist/\n"
        "You should gunzip them after downloading,"
        "or directly use data/mnist/get_mnist.sh\n");
  /* ParseCommandLineFlags: 解析命令行参数,
   * 与上文定义的 
   * DEFINE_string(backend, "lmdb", "The backend for storing the result");
   * 相对应
   * backend 变为 FLAGS_backend 变量
   */
  gflags::ParseCommandLineFlags(&argc, &argv, true);

  const string& db_backend = FLAGS_backend;

  if (argc != 4) {
    // 输出错误信息函数,第一个参数必须是 argv[0]
    gflags::ShowUsageWithFlagsRestrict(argv[0],
        "examples/mnist/convert_mnist_data");
  } else {
    // 初始化 glog
    google::InitGoogleLogging(argv[0]);
    // 进行数据转换(实际操作)
    convert_dataset(argv[1], argv[2], argv[3], db_backend);
  }
  return 0;
}

主函数中用到了大量的 gflags 和 glogs 的函数,都是用来对命令行与 日志相关,具体的真实干活的是 convert_dataset 函数.

// convert_dataset 实际中 将 二进制文件内容 存储到 数据库 中的函数
void convert_dataset(const char* image_filename, const char* label_filename,
        const char* db_path, const string& db_backend) {
  /* 以输入的方式 image_file 和 label_file
   * std::ifstream image_file 指 image_file 为一个类,并指定了文件路径和打开方法
   */
  std::ifstream image_file(image_filename, std::ios::in | std::ios::binary);
  std::ifstream label_file(label_filename, std::ios::in | std::ios::binary);
  // glog 中的 宏函数 如果 状态不正常 则输出后面内容
  CHECK(image_file) << "Unable to open file " << image_filename;
  CHECK(label_file) << "Unable to open file " << label_filename;
  //与二进制文件内容相关
  /* magic        魔数,类似于校验信息
   * num_items    条目(图片)的个数
   * num_labels   标签的个数
   * rows         一行元素中像素点个数
   * cols         一列元素中像素点个数
   */
  uint32_t magic;
  uint32_t num_items;
  uint32_t num_labels;
  uint32_t rows;
  uint32_t cols;
  // 使用 read 方法读取 image_file 前 (4*8)32 位元素的内容,即为魔数
  image_file.read(reinterpret_cast<char*>(&magic), 4);
  magic = swap_endian(magic);
  // 校验魔数是否一致
  CHECK_EQ(magic, 2051) << "Incorrect image file magic.";
  // 同理校验 label_file 的正确性
  label_file.read(reinterpret_cast<char*>(&magic), 4);
  magic = swap_endian(magic);
  CHECK_EQ(magic, 2049) << "Incorrect label file magic.";
  // 读取 条目的个数
  image_file.read(reinterpret_cast<char*>(&num_items), 4);
  num_items = swap_endian(num_items);
  // 读取标签的个数
  label_file.read(reinterpret_cast<char*>(&num_labels), 4);
  num_labels = swap_endian(num_labels);
  CHECK_EQ(num_items, num_labels);
  // 读取 条目(图片)的行像素点个数
  image_file.read(reinterpret_cast<char*>(&rows), 4);
  rows = swap_endian(rows);
  // 读取 条目(图片)的列像素点个数
  image_file.read(reinterpret_cast<char*>(&cols), 4);
  cols = swap_endian(cols);

  /* 来自
   * #include "boost/scoped_ptr.hpp"
   * 不用再分 levelDB 与LMDB
   */
  // 创建新 数据库DB
  scoped_ptr<db::DB> db(db::GetDB(db_backend));
  // 打开数据库,并指定数据库的路径
  db->Open(db_path, db::NEW);
  // 创建对数据库的 操作句柄(txn) Transaction 表示 事务,数据库内容的知识
  scoped_ptr<db::Transaction> txn(db->NewTransaction());

  // Storing to db
  char label;
  char* pixels = new char[rows * cols];
  int count = 0;
  string value;
  // 定义图片 对象
  Datum datum;
  // 图片的通道数
  datum.set_channels(1);
  // 图片的高
  datum.set_height(rows);
  // 图片的宽
  datum.set_width(cols);
  LOG(INFO) << "A total of " << num_items << " items.";
  LOG(INFO) << "Rows: " << rows << " Cols: " << cols;
  // 对 二进制文件中所有的 条目(图片) 进行处理
  for (int item_id = 0; item_id < num_items; ++item_id) {
    // 读取像素点
    image_file.read(pixels, rows * cols);
    // 读取标签
    label_file.read(&label, 1);
    datum.set_data(pixels, rows*cols);
    datum.set_label(label);

    string key_str = caffe::format_int(item_id, 8);
    // 对图片进行字符串序列化
    datum.SerializeToString(&value);
    // 通过事务 将 序列化后的图片信息 和 编号进行加入到数据库中
    txn->Put(key_str, value);
    // 并且 每 1000 个 提交一次
    if (++count % 1000 == 0) {
      txn->Commit();
    }
  }
  // 防止出现不能被1000整除的情况,对最后一批进行处理
  if (count % 1000 != 0) {
      txn->Commit();
  }
  LOG(INFO) << "Processed " << count << " files.";
  //删除 new 的内容
  delete[] pixels;
  // 关闭数据库
  db->Close();
}

convert_dataset 利用对 二进制文件的了解,对二进制文件进行解析,最后将其以数据库的形式保存.其中运用了一些数据库相关的知识,并且将数据库变成了面向对象的编程.
解决最后的问题,数据的大小端问题.

/* MNIST原始数据为大端存储,即数据的高字节保存在地址的低地址中,而数据的低字节保存在内存的高地址中
 * C/C++   数据为小端存储,和MNIST正好相反,
 * 所以需要定义一个函数进行转换
 */
uint32_t swap_endian(uint32_t val) {
    val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF);
    return (val << 16) | (val >> 16);
}

到此 数据准备已经完成了.

3. 训练数据

开始训练生成的数据,训练脚本在"caffe路径/example/mnist/train_lenet.sh"
其中调用了真正执行的语句

#!/usr/bin/env sh
set -e

./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt

"./example/mnist/train_lenet.sh"运行后,可能你会看见这样的显示:

lee@lee:~/Documents/caffe$ ./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt 
I0226 16:01:17.276763 16197 caffe.cpp:204] Using GPUs 0
F0226 16:01:17.277010 16197 common.cpp:66] Cannot use GPU in CPU-only Caffe: check mode.
*** Check failure stack trace: ***
    @     0x7f32a8e065cd  google::LogMessage::Fail()
    @     0x7f32a8e08433  google::LogMessage::SendToLog()
    @     0x7f32a8e0615b  google::LogMessage::Flush()
    @     0x7f32a8e08e1e  google::LogMessageFatal::~LogMessageFatal()
    @     0x7f32a9373780  caffe::Caffe::SetDevice()
    @           0x40a45b  train()
    @           0x406f70  main
    @     0x7f32a7f83b17  (unknown)
    @           0x40779a  _start
    @              (nil)  (unknown)
已放弃 (core dumped)

这是因为默认在 " --solver=examples/mnist/lenet_solver.prototxt " 中写着:

# caffe 求解模式: CPU or GPU
solver_mode: GPU
将其改为
solver_mode: CPU

就能正常运行
运行中显示的内容提示:

lee@lee:~/Documents/caffe$ ./examples/mnist/train_lenet.sh 
I0226 16:32:44.089705 18561 caffe.cpp:197] Use CPU.使用CPU
I0226 16:32:44.089968 18561 solver.cpp:45] 初始化超参数
以下内容来自: "caffe/example/mnist/lenet_solver.prototxt(训练超参数文件) "
test_iter: 100
test_interval: 500
.....
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
solver_mode: CPU
net: "examples/mnist/lenet_train_test.prototxt"
train_state {
  level: 0
  stage: ""
}
I0226 16:32:44.090420 18561 solver.cpp:102] Creating training net from net file: examples/mnist/lenet_train_test.prototxt 
按照  " examples/mnist/lenet_train_test.prototxt " 文件建立网络
I0226 16:32:44.090696 18561 net.cpp:296] The NetState phase (0) differed from the phase (1) specified by a rule in layer mnist
I0226 16:32:44.090768 18561 net.cpp:296] The NetState phase (0) differed from the phase (1) specified by a rule in layer accuracy
I0226 16:32:44.090971 18561 net.cpp:53] Initializing net from parameters: 初始化网络参数
以下内容来自 " examples/mnist/lenet_train_test.prototxt "
name: "LeNet"
state {    # 创建训练网络
  phase: TRAIN
  level: 0
  stage: ""
}
layer {
  name: "mnist"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  transform_param {
    scale: 0.00390625
  }
  data_param {
    source: "examples/mnist/mnist_train_lmdb"  # 使用到的数据库路径
    batch_size: 64
    backend: LMDB
  }
}
......
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
  top: "loss"
}
I0226 16:32:44.097754 18561 layer_factory.hpp:77] Creating layer mnist
I0226 16:32:44.097884 18561 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdb
创建 各种训练 层
I0226 16:32:44.097921 18561 net.cpp:86] Creating Layer mnist
......
# 显示当前内存占有
I0226 16:32:44.098275 18561 net.cpp:139] Memory required for data: 200960
......
进行反馈回路
I0226 16:32:44.104403 18561 layer_factory.hpp:77] Creating layer loss
I0226 16:32:44.104418 18561 net.cpp:86] Creating Layer loss
I0226 16:32:44.104429 18561 net.cpp:408] loss <- ip2
I0226 16:32:44.104440 18561 net.cpp:408] loss <- label
I0226 16:32:44.104460 18561 net.cpp:382] loss -> loss
I0226 16:32:44.104496 18561 layer_factory.hpp:77] Creating layer loss
I0226 16:32:44.104522 18561 net.cpp:124] Setting up loss
I0226 16:32:44.104537 18561 net.cpp:131] Top shape: (1)
I0226 16:32:44.104553 18561 net.cpp:134]     with loss weight 1
I0226 16:32:44.104581 18561 net.cpp:139] Memory required for data: 5169924
I0226 16:32:44.104593 18561 net.cpp:200] loss needs backward computation.
.....
I0226 16:32:44.104709 18561 net.cpp:244] This network produces output loss
I0226 16:32:44.104732 18561 net.cpp:257] Network initialization done.
创建 测试 网络(同上)
......
开始迭代(每100次显示一次,每500次保存一次)
I0226 16:32:47.178556 18561 solver.cpp:239] Iteration 0 (-4.34403e-44 iter/s, 3.065s/100 iters), loss = 2.3588
loss值
I0226 16:32:47.178653 18561 solver.cpp:258]     Train net output #0: loss = 2.3588 (* 1 = 2.3588 loss)
......

上面的指令中 重要的部分是 " --solver=examples/mnist/lenet_solver.prototxt "
examples/mnist/lenet_solver.prototxt 是使用 protobuffer 编写的训练超参数文件.文件中指定的超参数如下:

# 训练与测试 网络的 prototxt 文件所在位置
net: "examples/mnist/lenet_train_test.prototxt"
# test_iter specifies how many forward passes the test should carry out.
# In the case of MNIST, we have test batch size 100 and 100 test iterations,
# covering the full 10,000 testing images.
# 测试截断迭代次数
test_iter: 100
# 训练时每迭代500次进行一次预测
test_interval: 500
# 网络的基础学习速率,冲量,权衰量
base_lr: 0.01
momentum: 0.9
weight_decay: 0.0005
# 学习速率的衰减策略
lr_policy: "inv"
gamma: 0.0001
power: 0.75
# 每100次迭代,屏幕打印一次
display: 100
# 最大的迭代次数
max_iter: 10000
# 每5000次迭代打印一次快照
snapshot: 5000
#快照保存位置
snapshot_prefix: "examples/mnist/lenet"
# 求解模式选择(CPU 或者GPU)
solver_mode: CPU

同样的 在超参数文档中指定了网络结构 net: "examples/mnist/lenet_train_test.prototxt" 也是使用了lenet_train_test.prototxt 也是 prototxt 格式

5. 训练 网络 分析

上文提到了,训练网络位于 "examples/mnist/lenet_train_test.prototxt" 也是使用 protoBuffer 格式.
那这就来看看 " lenet_train_test.prototxt " 中的内容

# 网络名称为 LeNet
name: "LeNet"
layer {
# 层的名称
  name: "mnist"
# 层的类型为:数据层
  type: "Data"
# top 表示输出
# 输出 data 和label
  top: "data"
  top: "label"
  include {
# 该层只有训练阶段有效
    phase: TRAIN
  }
  transform_param {
# 数据变换使用的缩放因子
    scale: 0.00390625
  }
# 数据来源参数
  data_param {
    source: "examples/mnist/mnist_train_lmdb"
    batch_size: 64
    backend: LMDB
  }
}
layer {
  name: "mnist"
  type: "Data"
  top: "data"
  top: "label"
  include {
# 同上,该层只有测试阶段有效
    phase: TEST
  }
  transform_param {
    scale: 0.00390625
  }
  data_param {
    source: "examples/mnist/mnist_test_lmdb"
    batch_size: 100
    backend: LMDB
  }
}
# 卷积层
layer {
  name: "conv1"
# 层的类型为:卷积层
  type: "Convolution"
# bottom 表示输入
  bottom: "data"
# top 表示输出
  top: "conv1"
# 第一个 pram 表示权值学习速率倍乘因子
  param {
    lr_mult: 1
  }
# 第二个 pram 表示bias(偏移量)学习速率倍乘因子
  param {
    lr_mult: 2
  }
# 卷积计算参数
  convolution_param {
# 输出 的 通道数
    num_output: 20
# 卷积核大小
    kernel_size: 5
# 卷积的步长
    stride: 1
# 权值使用 xavier 填充器
    weight_filler {
      type: "xavier"
    }
# 偏移量使用 常数填充器,默认为0
    bias_filler {
      type: "constant"
    }
  }
}

#池化层
layer {
  name: "pool1"
# 层的类型为:池化层
  type: "Pooling"
# 输入输出
  bottom: "conv1"
  top: "pool1"
# 池化层参数
  pooling_param {
# 使用最大池化
    pool: MAX
# 池化核大小
    kernel_size: 2
# 池化核移动步长
    stride: 2
  }
}
......
# 全连接层
layer {
  name: "ip1"
# 层的类型为:全连接层
  type: "InnerProduct"
  bottom: "pool2"
  top: "ip1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
# 全连接层参数
  inner_product_param {
# 输出通道数
    num_output: 500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
# 非线性层
layer {
  name: "relu1"
# 层的类型为:非线性层
  type: "ReLU"
# 输入输出信息
  bottom: "ip1"
  top: "ip1"
}
......
# 准确率计算层
layer {
  name: "accuracy"
# 层的类型为:准确率计算层
  type: "Accuracy"
  bottom: "ip2"
  bottom: "label"
  top: "accuracy"
  include {
# 只有 测试阶段 有效
    phase: TEST
  }
}
# 损失层
layer {
  name: "loss"
# 层的类型为:softmaxloss 损失值计算层
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
  top: "loss"
}

6. caffe 的 一些 技巧

既然已经学习了一些其他的 机器学习框架,只是在另一个框架进行套用,只是一些名称的不同,所以我们还要搞清楚 caffe 支持哪些层.
在 我们安装的 "caffe 路径/docs/tutorial/layers" 这个路径下就有所有层的介绍

lee@lee:~/Documents/caffe/docs/tutorial/layers$ tree .
.
├── absval.md
├── accuracy.md
├── argmax.md
├── batchnorm.md
├── batchreindex.md
├── bias.md
├── bnll.md
├── clip.md
├── concat.md
├── contrastiveloss.md
├── convolution.md
├── crop.md
├── data.md
├── deconvolution.md
├── dropout.md
├── dummydata.md
├── eltwise.md
├── elu.md
├── embed.md
├── euclideanloss.md
├── exp.md
├── filter.md
├── flatten.md
├── hdf5data.md
├── hdf5output.md
├── hingeloss.md
├── im2col.md
├── imagedata.md
├── infogainloss.md
├── innerproduct.md
├── input.md
├── log.md
├── lrn.md
├── lstm.md
├── memorydata.md
├── multinomiallogisticloss.md
├── mvn.md
├── parameter.md
├── pooling.md
├── power.md
├── prelu.md
├── python.md
├── recurrent.md
├── reduction.md
├── relu.md
├── reshape.md
├── rnn.md
├── scale.md
├── sigmoidcrossentropyloss.md
├── sigmoid.md
├── silence.md
├── slice.md
├── softmax.md
├── softmaxwithloss.md
├── split.md
├── spp.md
├── tanh.md
├── threshold.md
├── tile.md
└── windowdata.md

这也局限了 caffe 比如 SSD 算法的输出并不是固定的,对于 SSD 需要专门的 caffe-SSD 的支持.
同样 prototxt 带来了很多的好处,比如可以优秀的可视化工具,连接如下:
http://ethereon.github.io/netscope/#/editor

图片.png

出现一个报错,
解决方案:netscope,Can't infer network data shapes

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

推荐阅读更多精彩内容