Caffe2 - Python API
A deep learning, cross platform ML framework
layers.py
1 # Copyright (c) 2016-present, Facebook, Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 ##############################################################################
15 
16 ## @package layers
17 # Module caffe2.python.layers.layers
18 from __future__ import absolute_import
19 from __future__ import division
20 from __future__ import print_function
21 from __future__ import unicode_literals
22 
23 import logging
24 from caffe2.python import core, schema, scope, workspace
25 from caffe2.python.layers.tags import TagContext
26 from caffe2.proto import caffe2_pb2
27 
28 from collections import namedtuple
29 import numpy as np
30 logger = logging.getLogger(__name__)
31 logger.setLevel(logging.INFO)
32 
33 # Some types to simplify descriptions of things traveling between ops
34 IdList = schema.List(np.int64)
35 IdScoreList = schema.Map(np.int64, np.float32)
36 
37 
38 def get_key(record):
39  if schema.equal_schemas(record, IdList):
40  key = 'values'
41  elif schema.equal_schemas(record, IdScoreList, check_field_types=False):
42  key = 'values:keys'
43  else:
44  raise NotImplementedError('Not implemented for {}'.format(record))
45  assert record[key].metadata is not None, (
46  "Blob {} doesn't have metadata".format(str(record[key]())))
47  return record[key]
48 
49 
50 def get_categorical_limit(record):
51  key = get_key(record)
52  return key.metadata.categorical_limit
53 
54 
55 def get_avg_length(record):
56  return record['lengths'].metadata.expected_value
57 
58 
59 def set_request_only(field):
60  for f in field.all_scalars():
61  categorical_limit, expected_value = None, None
62  if not f.metadata:
63  feature_specs = schema.FeatureSpec(
64  feature_is_request_only=True,
65  )
66  elif not f.metadata.feature_specs:
67  categorical_limit = f.metadata.categorical_limit
68  expected_value = f.metadata.expected_value
69  feature_specs = schema.FeatureSpec(
70  feature_is_request_only=True,
71  )
72  else:
73  categorical_limit = f.metadata.categorical_limit
74  expected_value = f.metadata.expected_value
75  feature_specs = schema.FeatureSpec(
76  feature_type=f.metadata.feature_specs.feature_type,
77  feature_names=f.metadata.feature_specs.feature_names,
78  feature_ids=f.metadata.feature_specs.feature_ids,
79  feature_is_request_only=True,
80  desired_hash_size=f.metadata.feature_specs.desired_hash_size,
81  )
82 
83  # make sure not to set categorical_limit for a non-integer field
84  if not np.issubdtype(f.field_type(), np.integer):
85  assert categorical_limit is None, \
86  "categorical_limit shouldn't be set for no-integer field"
87 
88  f.set_metadata(
89  schema.Metadata(
90  categorical_limit=categorical_limit,
91  expected_value=expected_value,
92  feature_specs=feature_specs,
93  )
94  )
95 
96 
97 class InstantiationContext(object):
98  """
99  List of contexts where layer could be instantitated
100  """
101  # The layers support this context will accumulate predictions, labels,
102  # weights. The accumulated data can later be used to compute
103  # calibration or for other
104  # purpose.
105  ACCUMULATE_PRED = 'accumulate_pred'
106  EVAL = 'eval'
107  PREDICTION = 'prediction'
108  TRAINING = 'training'
109 
110 
111 _LAYER_REGISTRY = {}
112 
113 
114 def register_layer(name, layer):
115  assert name not in _LAYER_REGISTRY, "{0} already exists".format(name)
116  _LAYER_REGISTRY[name] = layer
117 
118 
119 def layer_exists(name):
120  return name in _LAYER_REGISTRY
121 
122 
123 def get_layer_class(name):
124  return _LAYER_REGISTRY[name]
125 
126 
127 def create_layer(layer_name, *args, **kwargs):
128  return _LAYER_REGISTRY[layer_name](*args, **kwargs)
129 
130 
131 LayerPsParam = namedtuple('LayerPsParam', ['sparse_key', 'average_length'])
132 
133 
134 class LayerParameter(object):
135 
136  def __init__(self, parameter=None, optimizer=None, initializer=None,
137  ps_param=None, regularizer=None):
138  assert isinstance(parameter, core.BlobReference), \
139  "expect {0} to be a blob reference".format(str(parameter))
140  # need to put the following line (shape) before initialier
141  # shape will be updated once initializer is (re)set
142  self._shape = None
143  self.parameter = parameter
144  self.optimizer = optimizer
145  self.initializer = initializer
146  self.ps_param = ps_param
147  self.regularizer = regularizer
148 
149  @property
150  def initializer(self):
151  return self._initializer
152 
153  @initializer.setter
154  def initializer(self, op):
155  assert op is None or core.IsOperator(getattr(op, 'type', None)), \
156  "initializer expects an operator, got type: {}".format(type(op))
157  self._initializer = op
158  if op is not None:
160 
161  @property
162  def shape(self):
163  return self._shape
164 
165  @shape.setter
166  def shape(self, shape):
167  assert self.shape is None or self.shape == shape, \
168  "inconsistent shape for layer parameter:"\
169  " {}, expect: {}, but got {}".format(self, self.shape, shape)
170  self._shape = shape
171 
172  def _infer_shape_from_initializer(self):
173  for arg in self.initializer.arg:
174  if arg.name == 'shape':
175  return list(arg.ints)
176  with workspace.WorkspaceGuard("model_init_by_loading_params"):
177  try:
178  net = core.Net("shape_checker")
179  net._net.op.extend([self.initializer])
180  shape_blob = net.NextScopedBlob(self.parameter + "_shape")
181  net.Shape([self.parameter], shape_blob)
182  workspace.RunNetOnce(net)
183  shape = workspace.FetchBlob(shape_blob).tolist()
184  # ResetWorkspace to save memory
185  workspace.ResetWorkspace()
186  return shape
187  except RuntimeError as exp:
188  logger.warning(
189  "Cannot infer the shape of blob {} from operator {}: {}".format(
190  self.parameter, self.initializer.type, exp)
191  )
192  workspace.ResetWorkspace()
193  return None
194 
195  def __str__(self):
196  return str(self.parameter)
197 
198 
199 def is_request_only_scalar(scalar):
200  if len(scalar.field_metadata()) == 0:
201  return False
202  for metadata in scalar.field_metadata():
203  if not (metadata and metadata.feature_specs and getattr(
204  metadata.feature_specs, 'feature_is_request_only', False)):
205  return False
206  return True
207 
208 
209 class ModelLayer(object):
210 
211  def __init__(self, model, prefix, input_record,
212  predict_input_record_fields=None, tags=None, **kwargs):
213  """
214  Base class for model layers. Layer is an abstraction that allows to
215  provide model description in terms of meta-operators, where each of the
216  meta-operators can have different implementations for training,
217  evaluation and prediction, that are instantiated later. As an example
218  SampledSoftmax can do something related to sampling depending on
219  supervision during the training and just apply softmax if it's used for
220  prediction/evaluation.
221 
222  All inputs/outputs from layers are represented as a record (instance of
223  schema bounded to blobs) and are accessible through input_record and
224  output_schema. If Layer needs to have only a subset of inputs/provides
225  subset of outputs during the inference - it should provide
226  predict_input_record and predict_output_schema correspondingly (those
227  records are expected to be a subset of input_record/output_schema).
228 
229  Each layer has a list of Tags associated with it, that depends on
230  current context and arguments. It's possible to use those tags during
231  the instantiation time.
232 
233  """
234  self.name = model.next_layer_name(prefix)
235  self.model = model
236  self.kwargs = kwargs
237  self._input_record = input_record
238  if predict_input_record_fields:
239  if not isinstance(predict_input_record_fields, list):
240  predict_input_record_fields = [predict_input_record_fields]
242  predict_input_record_fields]
243  else:
244  self._predict_input_record = None
245 
246  self.request_only = True
247  if len(input_record.all_scalars()) == 0:
248  self.request_only = False
249  for scalar in input_record.all_scalars():
250  if not is_request_only_scalar(scalar):
251  self.request_only = False
252  break
253 
254  self._output_schema = None
255  self._predict_output_schema = None
256  self.eval_output_schema = None
257  self.tags = set(tags or [])
258  self.tags.update(TagContext.current().tags)
259  self.params = []
260  self._export_output_for_metrics = False
261  self._export_params_for_metrics = False
262 
263  def get_type(self):
264  return self.__class__.__name__
265 
266  def _check_output_schema(self):
267  assert self._output_schema is not None, "Schema is not initialized"
268  assert (self._predict_output_schema is None or
269  schema.is_schema_subset(self._predict_output_schema,
270  self._output_schema)), (
271  "predict_output_schema is not a subset of the output_schema")
272 
273  @property
274  def predict_input_record(self):
275  return self._predict_input_record or self._input_record
276 
277  @property
278  def input_record(self):
279  return self._input_record
280 
281  @property
282  def predict_output_schema(self):
283  self._check_output_schema()
284  return self._predict_output_schema or self._output_schema
285 
286  @predict_output_schema.setter
287  def predict_output_schema(self, output_schema):
288  assert self._predict_output_schema is None
289  self._predict_output_schema = output_schema
290 
291  @property
292  def output_schema(self):
293  if self.request_only:
294  set_request_only(self._output_schema)
295  self._check_output_schema()
296  return self._output_schema
297 
298  @output_schema.setter
299  def output_schema(self, output_schema):
300  assert self._output_schema is None
301  self._output_schema = output_schema
302 
303  def get_parameters(self):
304  return self.params
305 
307  """Return a subset of parameters which can be converted to fp16"""
308  return []
309 
310  def get_memory_usage(self):
311  return 0
312 
313  def add_init_params(self, init_net):
314  '''
315  Adds layer initialization operators to passed net.
316  '''
317  for param in self.params:
318  # TODO(amalevich): Either return back to lambdas, that add
319  # all params (looks a bit safer and breaking less
320  # abstractions) or extend Net interface to this type of
321  # operations better
322  # TODO(xlwang) init_net._net.op has type google.protobuf.\
323  # internal.containers.RepeatedCompositeFieldContainer, but
324  # the version of protobuf in fbcode does not support append
325  # so extend is used
326  init_op = param.initializer
327  current_device_scope = scope.CurrentDeviceScope()
328  if init_op:
329  if not init_op.HasField('device_option') and\
330  current_device_scope:
331  init_op = caffe2_pb2.OperatorDef()
332  init_op.CopyFrom(param.initializer)
333  init_op.device_option.CopyFrom(current_device_scope)
334  init_net._net.op.extend([init_op])
335 
336  def create_param(self, param_name, shape, initializer, optimizer,
337  ps_param=None, regularizer=None):
338  with scope.NameScope(self.name, reset=True):
339  param = self.model.create_param(param_name=param_name,
340  shape=shape,
341  initializer=initializer,
342  optimizer=optimizer,
343  ps_param=ps_param,
344  regularizer=regularizer)
345 
346  # make sure we don't share parameters in the same layer
347  assert all(param.parameter != p.parameter for p in self.params)
348 
349  self.params.append(param)
350  return param.parameter
351 
352  def get_next_blob_reference(self, name):
353  with scope.NameScope(self.name, reset=True):
354  return self.model.net.NextScopedBlob(name)
355 
356  def add_operators(self, net, init_net=None,
357  context=InstantiationContext.TRAINING):
358  '''
359  Adds layer trainig or initialization operators to the passed in net.
360  init_net can be None and can be called independently from add_init_params
361  '''
362  # Namescope below should warranty that all intermediate blobs will be
363  # assiciated with the layer that produces them
364  with scope.NameScope(self.name):
365  if context not in {InstantiationContext.PREDICTION,
366  InstantiationContext.EVAL,
367  InstantiationContext.ACCUMULATE_PRED}:
368  assert init_net, (
369  "Only prediction and eval context don't need init_net")
370  if init_net:
371  self.add_init_params(init_net)
372  if context == InstantiationContext.TRAINING:
373  self.add_train_ops(net)
374  elif context == InstantiationContext.EVAL:
375  self.add_eval_ops(net)
376  elif context == InstantiationContext.ACCUMULATE_PRED:
378  else:
379  self.add_ops(net)
380 
381  if context in {InstantiationContext.TRAINING,
382  InstantiationContext.EVAL} \
384  self.add_param_copy_operators(net)
385 
386  def add_ops(self, net):
387  # Predict layer implementation.
388  raise NotImplementedError
389 
390  def add_eval_ops(self, net):
391  # Default eval layer implementation is completely matching
392  # predict layer implementation.
393  self.add_ops(net)
394 
395  def add_train_ops(self, net):
396  # Default train layer implementation is completely matching
397  # eval layer implementation.
398  self.add_eval_ops(net)
399 
400  def add_ops_to_accumulate_pred(self, net):
401  # This adds operators to accumulate predictions/labels/weights. The
402  # accumulated data can later be used to compute calibration or for other
403  # purpose. Default layer implementation is completely matching eval
404  # layer implementation.
405  self.add_eval_ops(net)
406 
407  def add_param_copy_operators(self, net):
408  for param in self.params:
409  param_copy_ref = self.model.metrics_schema[str(param.parameter)]
410  net.Copy([param.parameter], param_copy_ref.field_blobs())
411 
412  def export_output_for_metrics(self):
413  self._export_output_for_metrics = True
414 
415  # Export output of the layer directly
416  export_name = self.name + "/output"
417  self.model.add_metric_field(export_name, self.output_schema)
418 
419  def export_params_for_metrics(self):
420  self._export_params_for_metrics = True
421 
422  # Export copies of parameters
423  for param in self.params:
424  param_copy_ref = self.get_next_blob_reference(
425  str(param).split("/")[-1] + "_copy")
426  self.model.add_metric_field(str(param.parameter), param_copy_ref)
Module caffe2.python.layers.split.
def add_operators(self, net, init_net=None, context=InstantiationContext.TRAINING)
Definition: layers.py:357
def __init__(self, model, prefix, input_record, predict_input_record_fields=None, tags=None, kwargs)
Definition: layers.py:212
def get_next_blob_reference(self, name)
Definition: layers.py:352
def add_param_copy_operators(self, net)
Definition: layers.py:407
def add_ops_to_accumulate_pred(self, net)
Definition: layers.py:400
def add_init_params(self, init_net)
Definition: layers.py:313