caffe源码学习——用python定义网络时,源代码生成prototxt文件的原理(1)
概述
一般地,我们大都是使用作者定义好的网络结构来使用网络。即直接使用train.prototxt文件来使用网络,有时候为了方便修改网络参数,使用python代码来定义网络可能就便于管理。因此,该文章主要讲述了源代码中生成prototxt文件的原理。源代码主要在caffe-master/python/caffe/net_spec.py文件中。
由于此处部分的源码较多,因此,此篇文章主要测试部分功能(layer的name,type,bottom,top变量如何写入到prototxt文件中),后续的功能将继续完善。此篇文章的主要功能如下:
定义网络:
输出网络结果:
一、作者创建layer来构建网络时的思路
(一)Layer的创建过程
一般地,使用python代码定义网络时格式如下。按照一般的想法,不同type的layer需要定义不同的python类。即存在ImageData和Convolution等python类。
data,label=L.ImageData(source=img_list,batch_size=batch_size,ntop=2,root_folder=root+'inputdata/',transform_param=dict(scale=0.00390625))
conv1=L.Convolution(data,kernel_size=5,stride=1,num_output=20, pad=0, weight_filler=dict(type='xavier'))
pool1=L.Pooling(conv1,pool=P.Pooling.MAX,kernel_size=2,stride=2)
但是,作者对代码的管理更为巧妙。作者通过一个类Layer来管理所有的类型。Layer类的定义如下:
class Layer(object):
''' __getattr__:如果该class不包含成员name,那么在使用成员name时,通过调用此函数来实现。这里的name便是ImageData和Convolution等'''
def __getattr__(self, name):
def layer_fn(*args, **kwargs):
''' *args表示任何多个无名参数,它是一个tuple
**kwargs表示关键字参数,它是一个dict'''
# each layer own a member,Function,
fn = Function(name,args,kwargs)
if fn.ntop == 0:
return fn
elif fn.ntop == 1:
return fn.tops[0]
else:
return fn.tops
return layer_fn
由此可见:当我们创建一个layer时,函数返回的实际上是Function.tops的类型。因此,我们再来看看类Function的定义(为了方便,这里只看看创建Function类时所需要的代码):
class Function(object):
def __init__(self,type_name,inputs,params): #name args kwargs
"""
:param type_name: the name of the Layer.
:param inputs: the name of the input Layer
:param params: the parameter of the Layer. A dict
"""
self.type_name = type_name
self.inputs = inputs
self.params = params
#初始化成员变量ntop,如果传入了该值则赋值,如果没有,则赋值为1
self.ntop = self.params.get('ntop',1)
if 'ntop' in self.params:
del self.params['ntop']
self.in_place = self.params.get('in_place', False)
if 'in_place' in self.params:
del self.params['in_place']
self.tops = tuple(Top(self,n) for n in range(self.ntop))
因此,Function类含有的成员为:
type_name:管理layer的类型
inputs :管理该layer的输入层
params :管理layer的参数
ntop :表明layer构建时的输出有几个
in_place :。。。。。
tops :管理layer构建时的输出(Top类的元组)
根据Function类的全部定义可知,Function类主要是为layer提供一些函数来管理如何将layer的参数转化为prototxt的参数。反观Layer的定义,每次创建一个Layer的时候。便返回Function的tops成员,因此,Top类是枢纽。来看看Top类的定义:
class Top(object):
def __init__(self,fn,n):
self.fn = fn
self.n = n
def to_proto(self):
return to_proto(self)
def _to_proto(self,layers,names,autonames):
return self.fn._to_proto(layers,names,autonames)
因此,Top类主要掌管了该Layer类的Function成员,以及用n来区分同一个Layer含有多个Top的情况。
(二)总结
因此,Layer创建时,每一个Layer获取一个Top元组来管理该Layer。在获取的该Top元组中,每个Top都含有一个Function类来管理创建Layer时所传入的参数。又因为有的Layer创建时可能产生多个Top,因此,用参数n来区分同一个Layer所创建的多个Top,用于在写入文件时知道某个Layer的输入。
二、将构建的网络转化为字符串写入文本
(一)转化为字符串
一般地,使用python代码构建网络并且写入文本的流程如下:
data = Layer().Hello(ntop=1)
layer = Layer().World(data, ntop=1)
layer1 = Layer().World(layer,ntop=1)
print str(to_proto(layer1))
由代码可知,只需将网络的最上层的Top传入函数便可将整个网络转化为字符串。因此,我们来看看to_proto(*tops)函数的定义:
def to_proto(*tops):
layers = OrderedDict() #create a dict of layers, null first
autonames = Counter() #to count the times of a top layer in a layer.A dict
for top in tops:
print 'to_proto',top.fn.type_name
top.fn._to_proto(layers, {}, autonames)
#以下的代码暂且不分析
net = caffe_pb2.NetParameter()
net.layer.extend(layers.values())
return net
可知,to_proto(*tops)函数实际调用了Function._to_proto()
class Function(object):
""""""
def _to_proto(self, layers, names, autonames):
if self in layers:
return
bottom_names = []
for inp in self.inputs:
#(1)注意这里调用了Top的_to_proto方法,实际也是调用Function的此方法,因此,直到最底层(该层没有输入)循环退出
inp._to_proto(layers, names, autonames)
#(3)最底层的for循环的外部执行完(也就是上层的(2)),此时的layers[inp.fn]也就是上一层的layer即caffe_pb2.LayerParameter()的对象。执行完后便退出该层的for循环而执行该层的(2)。然后再是(3)........
bottom_names.append(layers[inp.fn].top[inp.n])
#(2)当到达最底层没有输入层时,执行下面代码,此时self表示最底层
layer = caffe_pb2.LayerParameter()
layer.type = self.type_name
layer.bottom.extend(bottom_names)#最底层时,bottom_names为空
if self.in_place:
layer.top.extend(layer.bottom)
else:
for top in self.tops:
layer.top.append(self._get_top_name(top, names, autonames))
layer.name = self._get_name(names, autonames)
#以下代码暂且不看
for k, v in six.iteritems(self.params):
# special case to handle generic *params
if k.endswith('param'):
assign_proto(layer, k, v)
else:
try:
assign_proto(getattr(layer,
_param_names[self.type_name] + '_param'), k, v)
except (AttributeError, KeyError):
assign_proto(layer, k, v)
layers[self] = layer
layer.top.extend(layer.bottom)的源码实际为c++代码(在protobuf的源代码中,路径为protobuf-master/python/google/protobuf/pyext/repeated_scalar_container.cc),由protobuf的编译器引申出接口(在文件),如下:
注意此过程中的递归过程的处理。此方法的使用很值得学习。然后再来看看一个layer的name变量是如何获取的。即Function._get_name()和Function._get_top_name()方法,定义如下:
class Function(object):
"""A Function specifies a layer, its parameters, and its inputs (which
are Tops from other layers)."""
#autonames记录每个类型的layer的出现的次数,name=type + count
def _get_name(self, names, autonames):
if self not in names and self.ntop > 0:
names[self] = self._get_top_name(self.tops[0], names, autonames)
elif self not in names:
autonames[self.type_name] += 1
names[self] = self.type_name + str(autonames[self.type_name])
return names[self]
def _get_top_name(self, top, names, autonames):
if top not in names:
autonames[top.fn.type_name] += 1
names[top] = top.fn.type_name + str(autonames[top.fn.type_name])
return names[top]
(二)总结
因此,在转为字符串的过程中,先是由最上层一直搜索到最下层,然后再从最下层依次往上创建layer=caffe_pb2.LayerParameter()对象,然后再往layer对象中填充字符串。注意,在暂时的层的命名方法中,名字都是根据层的类型名和出现次数来命名,后续会有灵活命名。
三、总结
由上面的网络的创建和写入过程可知:网络的创建是自下而上的,即由最下层往最上层创建,此时,上层对于下层来说是不可见的,表现在给top命名时,top的值是该层的name,因为此时并不知道上层信息;网络的写入先是自上而下,再是自下而上的,在自上而下的过程中,为每个层创建了空的bottom_name列表,搜索到最底层后,自下而上地将bottom_name列表填满,并且同时创建layer=caffe_pb2.LayerParameter()对象,然后填写layer对象。
注意此自下而上,然后自上而下的思想,对于层结构,模块化的结构的项目,此编程思想和技术很值得借鉴。完成了以最小的代码量解决在构建网络时,下层结构看不到上层结构的问题。
四、代码
#!/usr/bin/env python
import h5py
import numpy as np
import sys
import random
caffe_root = '/workspace/wanghao/git-repository/caffe-CN/'
sys.path.insert(0, caffe_root + 'python')
sys.path.insert(0, caffe_root+ 'python/caffe/')
import caffe
import proto
from caffe import layers as L,params as P,proto,to_proto
from proto import caffe_pb2
from collections import OrderedDict, Counter
import six
def to_proto(*tops):
layers = OrderedDict() #create a dict of layers, null first
autonames = Counter() #to count the times of a top layer in a layer.A dict
for top in tops:
print 'to_proto',top.fn.type_name
top.fn._to_proto(layers, {}, autonames)
net = caffe_pb2.NetParameter()
net.layer.extend(layers.values())
return net
class Top(object):
def __init__(self,fn,n):
self.fn = fn
self.n = n
#print fn ,n
def to_proto(self):
return to_proto(self)
def _to_proto(self,layers,names,autonames):
return self.fn._to_proto(layers,names,autonames)
# a class to charge of some functios ,
class Function(object):
def __init__(self,type_name,inputs,params): #name args kwargs
"""
:param type_name: the name of the Layer.
:param inputs: the name of the input Layer
:param params: the parameter of the Layer. A dict
"""
self.type_name = type_name
self.inputs = inputs
self.params = params
self.ntop = self.params.get('ntop',1) #get ntop in dict 'params',if not extist, return 1
if 'ntop' in self.params:
del self.params['ntop']
self.in_place = self.params.get('in_place', False)
if 'in_place' in self.params:
del self.params['in_place']
self.tops = tuple(Top(self,n) for n in range(self.ntop))
# get name of the Layer
def _get_name(self,names,autonames):
if self not in names and self.ntop > 0:
names[self] = self._get_top_name(self.tops[0],names,autonames)
elif self not in names:
autonames[self.type_name] += 1
names[self] = self.type_name + str(autonames[self.type_name])
return names[self]
# get name of the Layer's top layer
def _get_top_name(self, top, names, autonames):
if top not in names:
autonames[top.fn.type_name] += 1
# top name is not the true top name,
names[top] = top.fn.type_name + str(autonames[top.fn.type_name])
return names[top]
def _to_proto(self,layers, names, autonames):
if self in layers:
return
bottom_names = []
# iterator the layer's inputs.from Top to Bottom
# if a Layer has Bottom Layer,it will go to(not add the Bottom-Layer to OrderedDict) the layer's Bottom-Layer,
# and check the new Layer has a Bottom-Layer or not.if has,go to the Bottom-Layer.Loop this
#until a Layer is out of Bottom-Layer,it will end the for loop of the Layer(other topper Layer's for loop is going on)
for inp in self.inputs:
print inp.fn.type_name
inp._to_proto(layers, names, autonames)
# this will be done untill a layer without input
#at the moment,the layer is the toppest layer,so layer[inp.fn].top=inp.fn.type_name + n
print layers[inp.fn]
print 'for bottom_name',layers[inp.fn].top[inp.n]
bottom_names.append(layers[inp.fn].top[inp.n])
print self.type_name,'out of for'
# first, self is the most-bottom layer.fn. from Bottom to Top .
# then,self is the output.fn of the most-bottom layer, .....
layer = caffe_pb2.LayerParameter()
layer.type = self.type_name
layer.bottom.extend(bottom_names) #Appends all the elements in the input iterator to the container.
if self.in_place:
layer.top.extend(layer.bottom)
else:
for top in self.tops:
print self._get_top_name(top,names,autonames)
layer.top.append(self._get_top_name(top,names,autonames))
layer.name = self._get_name(names,autonames)
# deal with parameter of a layer
for k, v in six.iteritems(self.params):
if k.endswitch('param'):
assign_proto(layer, k, v)
else:
try:
assign_proto(getattr(layer,
_param_names[self.type_name] + '_param'), k, v)
except (ArithmeticError, KeyError):
assign_proto(layer, k, v)
layers[self] = layer
print 'layers end',layers
# when create a object of class Layer, we get a tuple of class Top
class Layer(object):
''' __getattr__:
if class Layer does not have the attribute of name, we will call the function __getattr__
when we use the attribute of name'''
def __getattr__(self, name):
def layer_fn(*args, **kwargs):
# each layer own a member,Function,
fn = Function(name,args,kwargs)
if fn.ntop == 0:
return fn
elif fn.ntop == 1:
return fn.tops[0]
else:
return fn.tops
return layer_fn
def VGG():
data = L.Input(shape=dict(dim=[1,3,244,224]))
if __name__ == '__main__':
#Layer()
data = Layer().Hello(ntop=1) # the return value is a type of Top
layer = Layer().World(data, ntop=1)
layer1 = Layer().World(layer,ntop=1)
#layer2 = Layer().World(layer1, ntop=1)
print str(to_proto(layer1))