/
functions.py
1941 lines (1668 loc) · 90.5 KB
/
functions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
CNTK function constructs. This is the core abstraction of all primitive operators in the CNTK computational graph.
"""
from os import path
from enum import Enum, unique
import sys
import warnings
import collections
import cntk
from cntk import cntk_py, Value
from cntk.device import DeviceDescriptor, cpu
from cntk.internal import map_if_possible, typemap, sanitize_var_map,\
sanitize_batch, sanitize_dtype_cntk, _as_tuple,\
sanitize_variable_value_dict,\
sanitize_Function_attributes,\
sanitize_variables_or_functions,\
_value_as_sequence_or_array
from cntk.internal.utils import get_python_function_arguments, \
map_function_arguments, _py_dict_to_cntk_dict, \
_to_cntk_dict_value
from cntk.internal import _UDFDeserializeCallbackWrapper, _serialize
from cntk.internal.sanitize import is_byte_buffer
from ..variables import Record, Variable
@unique
class ModelFormat(Enum):
'''
Describes the supported disk format for CNTK model.
'''
CNTKv2 = cntk_py.ModelFormat_CNTKv2
'''
Default CNTK version 2 format, it supports all CNTK functionalities.
'''
ONNX = cntk_py.ModelFormat_ONNX
'''
Open Neural Network Exchange format from https://github.com/onnx/onnx, ONNX currently support
subset of CNTK functionalities.
'''
@unique
class CloneMethod(Enum):
'''
Describes different ways how :func:`~cntk.ops.functions.Function.clone`
works.
'''
share = 'share'
'''
Parameters are shared between the Function being cloned and the new clone
'''
clone = 'clone'
'''
New learnable parameters are created and initialized with the current values of the
corresponding parameters of the Function being cloned
'''
freeze = 'freeze'
'''
Parameters are cloned and made immutable; i.e. Constants in the new clone
(e.g. for use as a fixed feature extractor)
'''
class Function(cntk_py.Function):
'''
Base class of all primitive tensor operators.
If it has only one output, one can invoke Variable methods on it, which it
will relay to its only output.
`Function` objects can also be constructed directly from a Python lambda,
by means of the `@Function` decorator.
The `Function`'s input signature is defined by the lambda.
Example:
>>> @Function
... def f(x):
... return x * x
>>> print(f) # inspect the Function's type
ElementTimes(x: Sequence[tensor]) -> Sequence[tensor]
The above form creates a CNTK Function whose arguments are placeholder variables.
Such a function can only be combined with other symbolic functions.
To train a Function or pass data to it, you need to declare the types
of the arguments. In this case, the @Function decorator creates a CNTK Function
whose arguments are input variables.
If you use Python 3, Functions with types are declared using Python annotation syntax, e.g.::
@Function
def f(x:Tensor[13]):
return x * x
If you are working with Python 2.7, use CNTK's :class:`@Signature <cntk.layers.typing.Signature>` decorator instead::
>>> from cntk.layers.typing import *
>>> @Function
... @Signature(Tensor[13])
... def f(x):
... return x * x
>>> print(f)
ElementTimes(x: Tensor[13]) -> Tensor[13]
``make_block=True`` is an internal parameter used to implement :func:`@BlockFunction <cntk.ops.functions.BlockFunction>`.
If `BlockFunction()` passes `True`, then the result will be wrapped
in :func:`~cntk.ops.as_block()`, using the supplied ``op_name`` and ``name`` parameters, which are otherwise ignored.
'''
_udf_callback_map = {}
_deserializer = _UDFDeserializeCallbackWrapper(_udf_callback_map)
cntk_py._register_udf_deserialize_callback(_deserializer)
# We override the constructors to implement an overload that constructs
# a CNTK Functions from a Python function (@Function).
def __new__(cls, *args, **kwargs):
if len(args) > 0 and hasattr(args[0], '__call__') and not isinstance(args[0], Function): # overload
return Function._to_Function(*args, **kwargs)
return super(Function, cls).__new__(cls) # for some reason, passing *args, **kwargs fails with "object() takes no args
def __init__(self, *args, **kwargs):
if len(args) > 0 and hasattr(args[0], '__call__') and not isinstance(args[0], Function): # overload
return
super(Function, self).__init__(*args, **kwargs)
# TODO: bring this back once we have a design for name-accessible .outputs etc.
#class NamedOutput:
# def __init__(self, **kwargs):
# for kw in kwargs: # TODO: only allow one arg
# self.name = kw
# self.arg = kwargs[kw]
_placeholders_under_construction = set()
@staticmethod
def _to_Function_unchecked(f, make_block=False, op_name=None, name=None):
'''implements @Function decorator; see :class:`~cntk.layers.functions.Function`'''
f_name = f.__name__ # (only used for debugging and error messages)
# helper to create a CNTK placeholder or input for a given name
# An input is created if the parameter is annotated with a Tensor(...) type.
# In this case, CNTK will immediately trigger type inference.
# Unannotated parameters will yield placeholder variables instead.
from .. import placeholder
def make_arg_variable(name, annotations):
from ..variables import Variable
var_type = annotations.get(name, None)
var_type = Variable._Type._sanitize(var_type)
if isinstance(var_type, Variable._Type):
return cntk.input_variable(name=name, **var_type)
else:
return placeholder(name=name)
from ..default_options import default_options
# Parameter() creation inside code of a Function def is forbidden. Setting 'pure' blocks it in Parameter().
with default_options(pure=True):
# get the parameter list through inspection
arg_names, annotations = get_python_function_arguments(f)
# The Python function is converted to a CNTK Function by executing it once
# passing placeholders as inputs. This createss a piece of graph.
# During execution, the Placeholders of this function are hidden from signatures of any
# further Functions that may be defined inside this invocation.
# This is required when @Function definitions are nested, and expression from
# the outer @Function block is used in an inner block, which would introduce
# additional Placeholders that will show up as .arguments.
# This is prevented by (1) maintaining a "invisible placeholders" list,
# and always filtering .arguments against that list. This is done by the property .signature;
# i.e. in all of this, do not use .arguments; use .signature instead.
from .. import combine, alias, as_block
args = [make_arg_variable(arg_name, annotations) for arg_name in arg_names]
# helpers
def force_order_args(fun_args):
block_args = [placeholder(name=fun_arg.name) for fun_arg in fun_args] # placeholders inside the BlockFunction
combined_block_args = combine(block_args) # the content of the BlockFunction
arg_map = list(zip(block_args, fun_args)) # after wrapping, the block_args map to args
return as_block(composite=combined_block_args, block_arguments_map=arg_map, block_op_name='Tuple').outputs
def invoke(fun_args):
try:
# hide Placeholders of this function from .signature() of any function defined inside
for arg in args:
Function._placeholders_under_construction.add(arg)
out = f(*fun_args)
if out is None:
raise TypeError("CNTK Function '{}' must return a value".format(f_name))
finally:
# unhide Placeholders of this function again
for arg in args:
Function._placeholders_under_construction.remove(arg)
# resolve tuples and NamedOutputs --TODO: check for duplicates
def resolve_named(output):
#if isinstance(output, Function.NamedOutput): # a tuple member is wrapped in a NamedOutput class, we got a name for it
# output = alias(output.arg, name=output.name)
# ^^ TODO: Complete the design for name-accessible .outputs, then bring this back.
if isinstance(output, cntk_py.Variable):
output = combine([output]) # workaround: wrap in another combine() call
# TODO: ^^ is this still necessary? Or is this a sanitize() call we need here?
return output
if isinstance(out, tuple): # multi-valued function, returned as a tuple
out = [resolve_named(output) for output in out]
# BUGBUG: combine() does not allow duplicates, so we wrap them in alias()
out_seen = set()
for i, out_i in enumerate(out):
if out_i in out_seen:
out[i] = alias(out_i)
else:
out_seen.add(out_i)
out = combine(out) # --> turn into a combine()
else:
out = resolve_named(out)
return out
# if called from BlockFunction() then wrap into a block
if make_block: # if we make a block then run off a separate set
block_args = [make_arg_variable(arg.name, annotations) for arg in args] # placeholders inside the BlockFunction
out = invoke(block_args)
out = as_block(composite=out, block_arguments_map=list(zip(block_args, args)), block_op_name=op_name, block_instance_name=name)
# not a block: ensure parameter ordering
else:
fun_args = args
#if len(fun_args) > 1:
# fun_args = force_order_args(fun_args)
# BUGBUG: Python interpreter crashes sometimes with this enabled, so for now fix it after the fact only if needed
# now invoke the Python function
out = invoke(fun_args)
# BUGBUG workaround: fix it after the fact with an inefficient solution only if we got it wrong
out_arg_names = [arg.name for arg in out.signature]
if set(out_arg_names) == set(arg_names) and out_arg_names != arg_names: # order came out wrong
fun_args = force_order_args(fun_args)
out = invoke(fun_args)
return out, args
@staticmethod
def _sanitize_check_Function(f_out, f_args, f):
arg_names, annotations = get_python_function_arguments(f)
#verify the argument length first
if len(f_out.signature) != len(f_args):
f_name = f.__name__
unfulfilled_args = set(f_out.signature) - set(f_args)
if unfulfilled_args:
unfulfilled_arg_names = [arg.name for arg in unfulfilled_args]
raise TypeError(
"CNTK Function '{}' has {} missing arguments ({}), which is currently not supported".format(f_name,
len(
unfulfilled_arg_names),
", ".join(
unfulfilled_arg_names)))
else:
unused_args = set(f_args) - set(f_out.signature)
unused_arg_names = [arg.name for arg in unused_args]
raise TypeError(
"CNTK Function '{}' has {} unused arguments ({}), which is currently not supported".format(f_name,
len(
unused_arg_names),
", ".join(
unused_arg_names)))
#then verify that we got the parameter order right
out_arg_names = [arg.name for arg in f_out.signature]
assert out_arg_names == arg_names, (out_arg_names, arg_names)
return f_out
@staticmethod
def _to_Function(f, make_block=False, op_name=None, name=None):
out, args = Function._to_Function_unchecked(f, make_block, op_name, name)
return Function._sanitize_check_Function(out, args, f)
@property
def signature(self):
'''
Returns the signature of a Function.
This is the .arguments[] list without placeholders that belong to an outer, not yet completed @Function def.
'''
sig = [arg for arg in self.arguments if arg not in Function._placeholders_under_construction]
return tuple(sig)
def argument_map(self, *args, **kwargs):
'''
Determines the {placeholder: variable} map for use with various call operations
Returns a dictionary from this function's placeholders to whatever arguments are passed.
Accepted are both positional and keyword arguments.
This mimics Python's argument interpretation, except that keyword arguments are not optional
(there is no concept of default value).
This does not require the arguments to be Variables or Functions. It is also called by train_minibatch().
'''
params = self.signature # function parameters
if len(args) + len(kwargs) != len(params):
raise TypeError("CNTK Function expected {} arguments, got {}".format(len(params), len(args) + len(kwargs)))
params_dict = { arg.name: arg for arg in params }
return map_function_arguments(params, params_dict, *args, **kwargs)
@staticmethod
def _replace_args_type_check(arg_map): # type: (Dict[param: Variable, arg: Variable]), param meant to be substituted by arg
'''
Performs a type-compatibility check for arguments to replace_placeholders() and clone(),
in order to output an actionable error message in case of an error.
'''
for i, arg_map_item in enumerate(arg_map.items()):
param = arg_map_item[0] # parameter = what gets substituted
arg = arg_map_item[1] # argument = what it gets substituted with
#print('checking param', param.name, 'against arg', arg.name)
param_type = param._type
arg_type = arg._type if isinstance(arg, cntk_py.Variable) else arg.output._type if isinstance(arg, Function) else None
def param_name(): # helper to get a descriptive name for param
if param.name:
return "argument %s" % param.name
else:
return 'positional argument %d' % i
if not arg_type:
raise TypeError(param_name() + " was passed an object that is not a Variable or Function")
# parameter shape is not yet known, any input is acceptable
if not param_type.shape_is_known or param.is_placeholder:
# Note: if a Function with nown inputs gets cloned while replacing the inputs
# with placeholders, those placeholders retain their shapes for some reason.
# But in this case, it should be allowed to replace them with mismatching dimensions,
# hence we do not test placeholders, only inputs.
# TODO: Should clone-replacing inputs with placeholders reset the shapes to unknown?
continue
if not arg_type.shape_is_known:
raise TypeError(param_name() + ' has a known shape, and cannot be passed a Variable of unknown shape')
# TODO: add tests for this complex condition
if len(arg_type.shape) < len(param_type.shape) or \
arg_type.shape[-len(param_type.shape):] != param_type.shape or \
(arg_type.dynamic_axes and arg_type.dynamic_axes != param_type.dynamic_axes) or \
arg_type.dtype != param_type.dtype or \
arg_type.is_sparse != param_type.is_sparse:
raise TypeError(param_name() + "'s type " + str(param_type) + " is incompatible with the type " + str(arg_type) + " of the passed Variable")
def update_signature(self, *arg_types, **kwarg_types):
'''
Defines input shapes, in-place
e.g.
model.update_signature(42)
pass a list of objects that define the dimensions etc. of the placeholders
Currently you can pass an int, a tuple, an Input, or a dict created with Type()
'''
arg_map = self.argument_map(*arg_types, **kwarg_types) # map type specs to Function parameters
def to_input(arg_type, name):
#from cntk import input
from ..variables import Variable
if isinstance(arg_type, (int, tuple)): # just passed a shape
return cntk.input_variable(shape=_as_tuple(arg_type), name=name)
arg_type = Variable._Type._sanitize(arg_type)
if isinstance(arg_type, Variable._Type): # full type given as Tensor[...] etc.
return cntk.input_variable(name=name, **arg_type)
raise TypeError("update_signature() expects arguments of type int, tuple of int, or Type.Variable")
# map the given types:
# - create an Input with the given Type or shape
# - keep the name property of the Function parameter
# - skip argument types passed as None
arg_map = { param: to_input(arg_type, name=param.name) for param, arg_type in arg_map.items() if arg_type is not None }
Function._replace_args_type_check(arg_map)
self.replace_placeholders(arg_map)
def declare_args(self, *arg_types):
'''
Back-compat wrapper for update_signature() (beta12 and before).
'''
warnings.warn('This will be removed in future versions. Please use '
'update_signature(...) instead', DeprecationWarning)
placeholders = self.placeholders # the unbound parameters to fill in
if len(arg_types) != len(placeholders):
raise TypeError("CNTK Function.declare_args() expected {} arguments, got {}".format(len(placeholders), len(arg_types)))
def to_input(arg):
if isinstance(arg, cntk_py.Variable):
return arg
else:
#from cntk import input
return cntk.input_variable(arg)
args = [to_input(arg) for arg in arg_types]
arg_map = dict(zip(placeholders, args))
Function._replace_args_type_check(arg_map)
self.replace_placeholders(arg_map)
def __call__(self, *args, **kwargs):
'''
Call a Function, either on symbolic or numeric inputs.
* If at least one input is a CNTK Function or Variable, then
result is a CNTK Function object, with inputs bound to the arguments.
This is a short-hand for `f.clone(share, argument_map(*args, **kwargs))`.
* Otherwise, all arguments must be numbers, numpy arrays, or a :class:`~cntk.io.MinibatchData` instance.
Then perform the actual computation and return the numeric result.
This is a short-hand for `f.eval(argument_map(*args, **kwargs))`,
except that there is no `device` parameter. If you need that, use `eval()` directly.
Args:
*args, **kwargs: The arguments to pass to the Function.
Returns:
In case of symbolic inputs, returns another CNTK Function object with inputs bound to the arguments.
Otherwise returns a tuple of numpy arrays for tuple-valued Functions, and a single numpy array otherwise.
'''
# parse argument list and map to the function's input
arg_map = self.argument_map(*args, **kwargs)
# if placeholders were excluded due to being under construction,
# we must include them in the argmap, otherwise they will be cloned
for arg in self.arguments:
if arg not in arg_map:
arg_map[arg] = arg
# determine whether this is eval() or clone()
is_symbolic = any(isinstance(arg, (cntk_py.Function, cntk_py.Variable)) for arg in arg_map.values())
# symbolic: return a cloned Function
# applying the function means to inline its piece of graph
if is_symbolic:
Function._replace_args_type_check(arg_map)
return self.clone(CloneMethod.share, arg_map)
# numeric: evaluate
outputs = self.outputs
_, output_map = self.forward(arg_map, outputs)
assert len(output_map) == len(outputs), (output_map, outputs)
if len(output_map) > 1: # tuple-valued: return tuple
return tuple(output_map[output] for output in outputs)
else: # single value: return numpy array and that's it
return list(output_map.values())[0]
# TODO: remove the parallel application; instead
# - function tuples always operate on all inputs, just as if they were a single function
# - parallel application would be done by nested Sequential or >> expressions
# - we also need to rethink Sequential() for the case that the first function passed to
# it accepts multiple arguments. That should just become the returned composite's signature.
# It naturally would if we just passed it on to Function, but in case of a tuple, we'd need
# to create intermediate placeholders so that all functions in the tuple get to share the inputs.
def __rshift__(self, other):
'''
Forward function composition (G o F), same as Sequential([F, G]).
Unlike __call__(), __rshift__() accepts tuples:
* `G` can be a tuple of Functions. They are applied in parallel, yielding a tuple result.
If `F` is a single-valued Function, it will be fed to all items.
* if `F` is a tuple-valued Function piped and `G` is a single Function, the tuple
values will be used as the arguments to `G`.
* if both are tuples, they are applied 1:1
E.g. `Embedding(500) >> (Recurrence(500), Recurrence(500, go_backwards=True)) >> splice >> Dense`
'''
inputs = self.outputs
input_is_tuple = len(inputs) > 1
# if piping into a tuple of Functions, apply item-wise
if isinstance(other, tuple):
from cntk import combine
return combine([other[i](inputs[i if input_is_tuple else 0]) for i in range(len(other))])
# if applying a single function to a tuple-valued Function, pass the items as the args
elif input_is_tuple:
return other(*inputs)
# regular case: one input, one Function
else:
return other(self)
def __lshift__(self, other):
'''
Backward function composition (self o other)
'''
return self(other)
def __getattr__(self, name):
'''
Access a member inside this object.
Members of ``Function`` can be accessed directly.
In addition, members of the Function's output, if only one, are accessed here.
Lastly, this also gives access to Functions and Variables inside this Function's
graph by their user-specified name, e.g. ``model.embed.E``, as long as those names are not also
member names of Function or Variable.
'''
# If name is not a member of Function or Variable, first look for
# a user-named item in the graph.
# (Known member names cannot be overridden by user-named items,
# to ensure that the API functions.)
if not hasattr(Variable, name) and not hasattr(Function, name) \
and not name.startswith('_') and name not in ['outputs', 'output', 'this']:
# lookup of a named object inside the graph
# When 'self' is a BlockFunction (e.g. a named layer), then we only search in there,
# while when 'self' is a regular node (e.g. a named output using Label),
# we search the composite, which may return multiple hits with the same name.
# In case of multiple matches, we fail.
# BUGBUG: That is a problem if, e.g., someone used a layer (=BlockFunction) twice
# and then looks it up by name, as that will fail although both instances are identical.
from cntk.logging.graph import find_by_name
root = self.block_root if self.is_block else self
item = typemap(find_by_name)(root, name, depth=1)
if item:
return item
# If something is not found in Function, look it up in its output
# variable, if it has only one.
if name.startswith('_') or name in ['outputs', 'output', 'this']:
# These should not be looked up in self's output.
# 'outputs' and 'output' are required to fetch the attribute for
# in the Variable.
# 'this' is required for Swig and needs to be thrown if the
# object is created the first time.
raise AttributeError("neither Function nor its output variable"
" has '%s'"%name)
# access an API member of 'output', such as .shape()
outputs = self.__getattribute__('outputs')
if len(outputs) != 1:
raise AttributeError("Function does not have '%s' and it cannot "
"be looked up in its outputs because it does not have "
"exactly one"%name)
return getattr(outputs[0], name)
@property
def type(self):
'''
Get type of a Function's output.
'''
return self.output.type
@property
@typemap
def arguments(self):
'''
List of all input variables of the Function that are not of type Parameter or Constant.
Note that due to the different matrix storage format in C++(column major) and Python(row major),
the argument order for some ops(Times, TransposeTimes, and Gemm) in C++ and Python are not the same.
In previous CNTK versions, the default for this api was to return arguments in C++ order.
Now we are changing the default for this api to python order, such that it will return arguments in the same order as they are fed into ops.
If you wish to still get arguments in C++ order, you can simple override the global option.
Example:
>>> import cntk as C
>>> a = C.input_variable((3,4), name='a')
>>> b = C.input_variable((4,5), name='b')
>>> c = C.times(a, b)
>>> c.arguments # python order
(Input('a', [#], [3 x 4]), Input('b', [#], [4 x 5]))
>>> from cntk.default_options import set_global_option
>>> set_global_option('python_operand_order', False)
>>> c.arguments # C++ order
(Input('b', [#], [4 x 5]), Input('a', [#], [3 x 4]))
'''
from ..default_options import get_global_option
python_operand_order = get_global_option('python_operand_order', True)
return super(Function, self).arguments(python_operand_order)
@property
@typemap
def attributes(self):
'''
List of the attributes of the function
'''
return sanitize_Function_attributes(super(Function, self).attributes())
def set_attribute(self, name, value):
'''
Allows to change a function attribute.
Args:
name (string): one of
* 'dropoutRate': modifies the dropout rate of a dropout function
(can only be invoked on a function instance returned either from
:func:`~cntk.ops.dropout` or :func:`find_by_name`).
* 'rngSeed': modifies the seed of a stateful function (can only be
invoked on function instance returned from :func:`~cntk.ops.dropout`,
:func:`~cntk.ops.random_sample`,
:func:`~cntk.ops.random_sample_inclusion_frequency` or :func:`find_by_name`)
value (float in case of 'dropoutRate', int for 'rngSeed'): the new value
of the corresponding attribute.
'''
value = _to_cntk_dict_value(value)
return super(Function, self).set_attribute(name, value)
def _get_or_reset_custom_attributes(self, reset):
'''
Internal non-property version of custom attribute
Note that composite function does not have custom attributes, so the property returns its root_function's custom_attributes.
Args:
reset (bool): whether to reset the dictionary
'''
if self.is_composite:
return self.root_function._get_or_reset_custom_attributes(reset)
else:
if reset:
super(Function, self).reset_custom_attributes()
return super(Function, self).get_custom_attributes()
@property
def custom_attributes(self):
'''
Get function custom attributes in cntk_py.Dictionary for both read and write.
'''
return self._get_or_reset_custom_attributes(reset=False)
@custom_attributes.setter
def custom_attributes(self, values):
'''
Set function custom attributes in a batch, and drops old attributes
Args:
values (dict): a dictionary of new custom attributes
'''
values = values or {}
if not isinstance(values, dict):
raise TypeError("values must be a dictionary")
custom_attr = self._get_or_reset_custom_attributes(reset=True)
for key in values.keys():
custom_attr[key] = values[key]
@typemap
def clone(self, method, substitutions=None):
'''
Clones the function. The parameters of the Function are either cloned,
shared or frozen as specified by the method argument and any variable
substitutions requested are applied in the cloned Function instance.
Args:
method (:class:`CloneMethod`): one of
* 'clone': the returned function gets its own copy of parameters (default)
* 'share': the returned function shares its parameters with this function
* 'freeze': parameters are cloned and made immutable (constant).
substitutions (dict): a dictionary mapping variables in this
function to variables in the cloned function
Returns:
:class:`~cntk.ops.functions.Function`: the cloned Function
'''
# C++ clone() can only clone composites. If we are not a composite, make it one using combine()
if not self.is_composite:
from cntk import combine
return combine([self]).clone(method, substitutions)
method = getattr(cntk_py,
'ParameterCloningMethod_' + CloneMethod(method).name.capitalize())
substitutions = substitutions or {}
if not isinstance(substitutions, dict):
raise TypeError("Variable substitution map must be a dictionary")
for prev_node, new_node in substitutions.items():
if not new_node or not prev_node:
raise AttributeError("Cannot replace node: " + str(prev_node) + " with node: " + str(new_node) + ". Neither node can be None.")
return super(Function, self).clone(method, substitutions)
@property
@typemap
def constants(self):
'''
List of all `Constant` variables of this :class:`~cntk.ops.functions.Function`
'''
return super(Function, self).constants()
def eval(self, arguments=None, outputs=None, device=None, as_numpy=True):
'''
Evaluate the Function's outputs using the specified ``arguments`` as input.
Args:
arguments: maps variables to their input data. The interpretation depends on
the input type:
* dict: keys are input variable or names, and values are the input data.
See :meth:`~cntk.ops.functions.Function.forward` for details on passing
input data.
* any other type: if node has a unique input, arguments is
mapped to this input.
For nodes with more than one input, only dict is allowed.
In both cases, every sample in the data will be interpreted
as a new sequence.
Sequences can be marked as continuations of the same sequence in
the previous minibatch (that is the sequence in the same slot).
There are two possibilities for this:
* specifying arguments as a `tuple` where the first element is
used as arguments and the second one will be used as a list
of bools, denoting whether a sequence is a new one (`True`) or a
continuation of the sequence in the same slot of the previous
minibatch (`False`). This will be applied to all batches.
* specifying arguments as a dictionary of variables to tuples
where the first element is used as arguments and the second
one will be used as a list of bools, denoting whether a sequence
is a new one (`True`) or a continuation of the sequence in the
same slot of the previous minibatch (`False`). This will be
applied to all batches.
Data should be either NumPy arrays or a
:class:`~cntk.io.MinibatchData` instance.
outputs (iterable, optional): outputs to fetch values for. If not
set, all outputs of the function will be fetched.
device (:class:`~cntk.device.DeviceDescriptor`): the device descriptor that
contains the type and id of the device on which the computation is
to be performed.
as_numpy (bool): whether to return the result as a NumPy array. Default True.
Specifying this as False returns a CNTK Value which avoids a
costly conversion but returns a somewhat opaque object. Also, the Value objects
are temporary and only guaranteed to be valid until the next forward/eval/backward/grad call.
You must explicitly clone the temporay Value objects if they need to be accessed later.
Note:
See :meth:`~cntk.ops.functions.Function.forward` for examples on
passing input data.
Returns:
dict or NumPy Array: Dict with keys of output variable names and values of
output variable. A single NumPy array if there is only one output value.
'''
if outputs is None:
outputs = self.outputs
_, output_map = self.forward(arguments, outputs, device=device, as_numpy=as_numpy)
return sanitize_variable_value_dict(output_map)
@typemap
def forward(self, arguments, outputs=None, keep_for_backward=None, device=None, as_numpy=True):
'''
Computes the values of speficied variables in ``outputs``, using values
provided in ``arguments`` that correspond to each input `Variable` of
the function (i.e. those that have ``is_input = True``).
Example:
>>> # Example of passing dense data
>>> v = C.input_variable(shape=(3,))
>>> f = C.reciprocal(v)
>>> _, fv = f.forward({v:[[1, 2, 4]]})
>>> list(fv.values())[0]
array([[ 1. , 0.5 , 0.25]], dtype=float32)
Example:
>>> # Passing sparse values as one-hot with a vocabulary size of 5
>>> vocab_size = 5
>>> v = C.sequence.input_variable(shape=(vocab_size,), is_sparse=True)
>>> f = C.times(v, np.eye(vocab_size))
>>> # Passing a batch of two sequences:
>>> # 1st sequence: word 1
>>> # 2nd sequence: words 2 and 4
>>> batch = [[1],[2,4]]
>>> sparse_batch = C.Value.one_hot(batch, vocab_size)
>>> _, fv = f.forward({v:sparse_batch})
>>> list(fv.values())[0]
[array([[ 0., 1., 0., 0., 0.]], dtype=float32),
array([[ 0., 0., 1., 0., 0.], [ 0., 0., 0., 0., 1.]], dtype=float32)]
Example:
>>> # Doing the same, but with a CSR matrix from scipy.sparse
>>> vocab_size = 5
>>> from scipy.sparse import csr_matrix
>>> v = C.sequence.input_variable(shape=(vocab_size,), is_sparse=True)
>>> f = C.times(v, np.eye(vocab_size))
>>> # Note that csr_matrix automatically uses a sparse representation underneath.
>>> sparse_batch = [csr_matrix([[0,1,0,0,0]]), csr_matrix([[0,0,1,0,0], [0,0,0,0,1]])]
>>> _, fv = f.forward({v:sparse_batch})
>>> list(fv.values())[0]
[array([[ 0., 1., 0., 0., 0.]], dtype=float32),
array([[ 0., 0., 1., 0., 0.], [ 0., 0., 0., 0., 1.]], dtype=float32)]
<BLANKLINE>
>>> # Much more efficient, however, is to incrementally create CSR arrays.
>>> # See https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html
>>> # for more information.
>>> def seq_to_csr_matrix(seq, vocab_size):
... indptr = [0]
... indices = []
... data = []
... for term_idx in seq:
... indices.append(term_idx)
... data.append(1)
... indptr.append(len(indices))
... return csr_matrix((data, indices, indptr), shape=(len(seq), vocab_size))
>>> sparse_batch = [seq_to_csr_matrix(seq, vocab_size) for seq in batch]
>>> _, fv = f.forward({v:sparse_batch})
>>> list(fv.values())[0]
[array([[ 0., 1., 0., 0., 0.]], dtype=float32),
array([[ 0., 0., 1., 0., 0.], [ 0., 0., 0., 0., 1.]], dtype=float32)]
Args:
arguments: maps variables to their input data. The interpretation depends on
the input type:
* dict: keys are input variable or names, and values are the
input data. To specify a minibatch, provide a list of arrays.
The shape of each array must be compatible with the shape of
the dictionary key. If the array denotes a sequence then the
elements of the sequence are grouped along axis 0.
* any other type: if node has a unique input, arguments is
mapped to this input.
For nodes with more than one input, only dict is allowed.
In both cases, every sample in the data will be interpreted
as a new sequence.
Sequences can be marked as continuations of the same sequence in
the previous minibatch (that is the sequence in the same slot).
There are two possibilities for this:
* specifying arguments as a `tuple` where the first element is
used as arguments and the second one will be used as a list
of bools, denoting whether a sequence is a new one (`True`) or a
continuation of the sequence in the same slot of the previous
minibatch (`False`). This will be applied to all batches.
* specifying arguments as a dictionary of variables to tuples
where the first element is used as arguments and the second
one will be used as a list of bools, denoting whether a sequence
is a new one (`True`) or a continuation of the sequence in the
same slot of the previous minibatch (`False`). This will be
applied to all batches.
Data should be either NumPy arrays or a
:class:`~cntk.io.MinibatchData` instance.
outputs (iterable, optional): outputs to fetch values for. If not
set, all outputs of the function will be fetched.
keep_for_backward (set, default `None`): the subset of the
Function's output variables for which gradients shall be calculated
in a subsequent backward call. If `None`, the returned state will
be `None` and a subsequent call to :func:`backward` will not be
possible.
device (:class:`~cntk.device.DeviceDescriptor`, default `None`): the device
descriptor that contains the type and id of the device on which the
computation is. If `None`, the default device is used.
as_numpy (bool): whether to return the result as a NumPy array. Default True.
Specifying this as False returns a CNTK Value which avoids a
costly conversion but returns a somewhat opaque object. Also, the Value objects
are temporary and only guaranteed to be valid until the next forward/eval/backward/grad call.
You must explicitly clone the temporay Value objects if they need to be accessed later.
Returns:
A tuple (BackPropState, map of outputs to NumPy arrays). The
BackPropState is a handle taken by :func:`backward`.
'''
if device is None:
device = DeviceDescriptor.use_default_device()
in_var_map = sanitize_var_map(self.arguments, arguments,
None, device)
if outputs is None:
outputs = self.outputs
else:
outputs = sanitize_variables_or_functions(outputs)
output_map = {v: None for v in outputs}
keep_for_backward = set(keep_for_backward or {})
state = super(Function, self)._forward(in_var_map, output_map, device,
keep_for_backward)
if as_numpy:
for k, v in output_map.items():
output_map[k] = _value_as_sequence_or_array(v, k)
return state, output_map
@typemap
def backward(self, state, root_gradients, variables, as_numpy=True):
'''
Backpropagates supplied ``root_gradients`` for one or more of the output
variables of the Function, to calculate gradients with respect to
``variables``. Formally, multiplies the values of ``root_gradients`` by
the Jacobian of the Function and returns the subset of the output that
corresponds to ``variables``.
Example:
>>> # compute the value and the derivative of the sigmoid at 0
>>> v = C.input_variable(shape=(1,), needs_gradient=True)
>>> f = C.sigmoid(v)
>>> df, fv = f.forward({v:[[0]]}, [f.output], set([f.output]))
>>> value = list(fv.values())[0]
>>> grad = f.backward(df, {f.output: np.ones_like(value)}, set([v]))
>>> value
array([[ 0.5]], dtype=float32)
>>> list(grad.values())[0]
array([[ 0.25]], dtype=float32)
Args:
state (BackPropState): state obtained from a previous call to the
func:`cntk.ops.Function.forward` method on this Function for the
computation that this gradient backpropagation corresponds to.
root_gradients (dict): the gradients that will be backpropagated
variables (set): a list of input variables with respect to which
the gradients have to be computed.
as_numpy (bool): whether to return the gradients as a NumPy array. Default True.
Specifying this as False returns a CNTK Value which avoids a
costly conversion but returns a somewhat opaque object. Also, the Value objects
are temporary and only guaranteed to be valid until the next forward/eval/backward/grad call.
You must explicitly clone the temporay Value objects if they need to be accessed later.
Note:
See :meth:`~cntk.ops.functions.Function.forward` for more examples
on passing input data.
Returns:
dict: mapping of ``variables`` to NumPy arrays
'''
if state is None:
raise ValueError('You are attempting to backpropagate on a '
'minibatch for which the corresponding forward operation did not '
'keep any intermediate results, Please set keep_for_backward in '
'forward to the variables in root_gradients.keys()')
device = state.device()
root_gradients = sanitize_var_map(self.outputs, root_gradients,
None, device)
var_gradients = {var: None for var in variables}
self._backward(state, root_gradients, var_gradients)
if as_numpy:
for var, value in var_gradients.items():
var_gradients[var] = _value_as_sequence_or_array(value, var)
return var_gradients
@typemap
def grad(self, at, wrt=None, outputs=None, device=None, as_numpy=True, grad_root=None):
'''
Computes the gradient of this Function at location ``at`` with respect to ``wrt``.
The Function must have a single output.
Example:
>>> x = C.input_variable(shape=(1,), needs_gradient=True)
>>> y = C.sqrt(x)
>>> a = np.asarray([1,4,16],dtype=np.float32).reshape(3,1)
>>> y.grad({x:a})
array([[ 0.5 ],
<BLANKLINE>
[ 0.25 ],
<BLANKLINE>
[ 0.125]], dtype=float32)
Args:
at (dict) : mapping of the Function's arguments to values
wrt (list, default `None`): list of Variables with respect to which the
gradient will be computed. If omitted, the gradients with
respect to all arguments of this Function that need gradient will be computed.
outputs (iterable, optional): outputs (including intermediate outputs in the graph)
to fetch values for. If not specified, values for none of the outputs are fetched.
device (:class:`~cntk.device.DeviceDescriptor`, default `None`): the device
descriptor that contains the type and id of the device on which the
computation is performed. If `None`, the default device is used.
as_numpy (bool, default `True`): whether to return the gradients as a NumPy array. Default True.
Specifying this as False returns a CNTK Value which avoids a
costly conversion but returns a somewhat opaque object. Also, the Value objects
are temporary and only guaranteed to be valid until the next forward/eval/backward/grad call.
You must explicitly clone the temporay Value objects if they need to be accessed later.
grad_root (:class:`~cntk.variables.Variable`, optional): specify the root of gradients calculation.
If not specified, the output of this function will be used as gradient root.
Returns:
dict or NumPy Array or a tuple of these: Dict with keys of ``wrt`` variables and gradient values of
``wrt`` variables. A single NumPy array if there is only one gradient value.
If ``outputs`` were specified (to fetch values for), this method returns a tuple where the 2nd element
of the tuple is the ``outputs`` values; a dict with keys of specified ``outputs`` variables and
values of computed ``outputs``, or a single NumPy array if there is only one output value.
Each element has the same shape as the ``wrt`` or ``outputs`` variables including dynamic axes
(such as the batch axis).
'''
if device is None:
device = DeviceDescriptor.use_default_device()
in_var_map = sanitize_var_map(self.arguments, at, None, device)
if outputs is None:
outputs = []
if wrt is None:
wrt = [arg for arg in self.arguments if arg.needs_gradient]
if len(wrt) == 0:
raise ValueError("None of the Function '%s' arguments have 'needs_gradient == True'" % str(self))
output_map = {v: None for v in outputs}
wrt_map = {v: None for v in wrt}
if grad_root is None:
super(Function, self).gradients(in_var_map, wrt_map, output_map, device)
else:
super(Function, self).gradients(in_var_map, grad_root, wrt_map, output_map, device)
if as_numpy:
for k in output_map: