Zobacz na TensorFlow.org | Uruchom w Google Colab | Wyświetl źródło na GitHub | Pobierz notatnik |
Ustawiać
!pip install -q tf_nightly
import tensorflow as tf
import numpy as np
from typing import Tuple, List, Mapping, Union, Optional
import tempfile
Rodzaje rozszerzeń
Typy zdefiniowane przez użytkownika mogą sprawić, że projekty będą bardziej czytelne, modułowe i łatwe w utrzymaniu. Jednak większość interfejsów API TensorFlow ma bardzo ograniczoną obsługę typów Pythona zdefiniowanych przez użytkownika. Obejmuje to zarówno interfejsy API wysokiego poziomu (takie jak Keras , tf.function , tf.SavedModel ) jak i interfejsy API niższego poziomu (takie jak tf.while_loop
i tf.concat
). Typy rozszerzeń TensorFlow mogą być używane do tworzenia zdefiniowanych przez użytkownika typów obiektowych, które bezproblemowo współpracują z interfejsami API TensorFlow. Aby utworzyć typ rozszerzenia, po prostu zdefiniuj klasę Pythona z tf.experimental.ExtensionType
jako jej podstawą i użyj adnotacji typu , aby określić typ dla każdego pola.
class TensorGraph(tf.experimental.ExtensionType):
"""A collection of labeled nodes connected by weighted edges."""
edge_weights: tf.Tensor # shape=[num_nodes, num_nodes]
node_labels: Mapping[str, tf.Tensor] # shape=[num_nodes]; dtype=any
class MaskedTensor(tf.experimental.ExtensionType):
"""A tensor paired with a boolean mask, indicating which values are valid."""
values: tf.Tensor
mask: tf.Tensor # shape=values.shape; false for missing/invalid values.
class CSRSparseMatrix(tf.experimental.ExtensionType):
"""Compressed sparse row matrix (https://en.wikipedia.org/wiki/Sparse_matrix)."""
values: tf.Tensor # shape=[num_nonzero]; dtype=any
col_index: tf.Tensor # shape=[num_nonzero]; dtype=int64
row_index: tf.Tensor # shape=[num_rows+1]; dtype=int64
Klasa bazowa tf.experimental.ExtensionType
działa podobnie do typing.NamedTuple
i @dataclasses.dataclass
ze standardowej biblioteki Pythona. W szczególności automatycznie dodaje konstruktor i metody specjalne (takie jak __repr__
i __eq__
) na podstawie adnotacji typu pola.
Zazwyczaj typy rozszerzeń należą do jednej z dwóch kategorii:
Struktury danych , które grupują zbiór powiązanych wartości i mogą udostępniać przydatne operacje oparte na tych wartościach. Struktury danych mogą być dość ogólne (takie jak powyższy przykład
TensorGraph
); lub mogą być wysoce dostosowane do konkretnego modelu.Typy tensorowe , które specjalizują się lub rozszerzają pojęcie „Tensor”. Typy w tej kategorii mają
rank
,shape
i zwykledtype
; i sensowne jest ich używanie z operacjami Tensor (takimi jaktf.stack
,tf.add
lubtf.matmul
).MaskedTensor
iCSRSparseMatrix
to przykłady typów podobnych do tensora.
Obsługiwane interfejsy API
Typy rozszerzeń są obsługiwane przez następujące interfejsy API TensorFlow:
- Keras : Typy rozszerzeń mogą być używane jako dane wejściowe i wyjściowe dla
Models
iLayers
Keras. - tf.data.Dataset : typy rozszerzeń mogą być zawarte w zestawach danych i zwracane przez
Iterators
Datasets
danych . - Tensorflow hub : typy rozszerzeń mogą być używane jako wejścia i wyjścia dla modułów
tf.hub
. - SavedModel : Typy rozszerzeń mogą być używane jako wejścia i wyjścia dla funkcji
SavedModel
. - tf.function : typy rozszerzeń mogą być używane jako argumenty i wartości zwracane dla funkcji opakowanych w dekorator
@tf.function
. - pętle while : typy rozszerzeń mogą być używane jako zmienne pętli w
tf.while_loop
i mogą być używane jako argumenty i wartości zwracane dla treści pętli while. - warunkowe : Typy rozszerzeń można warunkowo wybierać za pomocą
tf.cond
itf.case
. - py_function : typy rozszerzeń mogą być używane jako argumenty i zwracane wartości dla argumentu
func
dotf.py_function
. - Tensor ops : typy rozszerzeń można rozszerzyć, aby obsługiwały większość operacji TensorFlow, które akceptują dane wejściowe Tensor (np.
tf.matmul
,tf.gather
itf.reduce_sum
). Zobacz sekcję „ Wysyłka ” poniżej, aby uzyskać więcej informacji. - strategia dystrybucji : typy rozszerzeń mogą być używane jako wartości na replikę.
Aby uzyskać więcej informacji, zobacz sekcję „Interfejsy API TensorFlow obsługujące typy rozszerzeń” poniżej.
Wymagania
Rodzaje pól
Wszystkie pola (inaczej zmienne instancji) muszą być zadeklarowane, a dla każdego pola należy podać adnotację typu. Obsługiwane są następujące adnotacje typu:
Rodzaj | Przykład |
---|---|
Liczby całkowite w Pythonie | i: int |
Python unosi się na wodzie | f: float |
Łańcuchy Pythona | s: str |
Wartości logiczne Pythona | b: bool |
Python Brak | n: None |
Kształty tensorów | shape: tf.TensorShape |
Typy tensorów | dtype: tf.DType |
Tensory | t: tf.Tensor |
Rodzaje rozszerzeń | mt: MyMaskedTensor |
Poszarpane Tensory | rt: tf.RaggedTensor |
Rzadkie Tensory | st: tf.SparseTensor |
Zindeksowane plastry | s: tf.IndexedSlices |
Opcjonalne Tensory | o: tf.experimental.Optional |
Wpisz związki | int_or_float: typing.Union[int, float] |
Krotki | params: typing.Tuple[int, float, tf.Tensor, int] |
Krotki o długości var | lengths: typing.Tuple[int, ...] |
Mapowania | tags: typing.Mapping[str, tf.Tensor] |
Wartości opcjonalne | weight: typing.Optional[tf.Tensor] |
Zmienność
Typy rozszerzeń muszą być niezmienne. Gwarantuje to, że mogą być odpowiednio śledzone przez mechanizmy śledzenia wykresów TensorFlow. Jeśli chcesz zmutować wartość typu rozszerzenia, rozważ zamiast tego zdefiniowanie metod, które przekształcają wartości. Na przykład, zamiast definiować metodę set_mask
do mutacji MaskedTensor
, można zdefiniować metodę replace_mask
, która zwraca nowy MaskedTensor
:
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
def replace_mask(self, new_mask):
self.values.shape.assert_is_compatible_with(new_mask.shape)
return MaskedTensor(self.values, new_mask)
Funkcjonalność dodana przez ExtensionType
Klasa bazowa ExtensionType
zapewnia następującą funkcjonalność:
- Konstruktor (
__init__
). - Drukowalna metoda reprezentacji (
__repr__
). - Operatory równości i nierówności (
__eq__
). - Metoda weryfikacji (
__validate__
). - Wymuszona niezmienność.
- Zagnieżdżony
TypeSpec
. - Obsługa wysyłki Tensor API.
Zobacz sekcję „Dostosowywanie typów rozszerzeń” poniżej, aby uzyskać więcej informacji na temat dostosowywania tej funkcji.
Konstruktor
Konstruktor dodany przez ExtensionType
przyjmuje każde pole jako nazwany argument (w kolejności, w jakiej zostały wymienione w definicji klasy). Ten konstruktor sprawdzi każdy parametr i przekonwertuje go w razie potrzeby. W szczególności pola Tensor
są konwertowane za pomocą tf.convert_to_tensor
; Pola Tuple
są konwertowane na tuple
; Pola Mapping
są konwertowane na niezmienne wersety.
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
# Constructor takes one parameter for each field.
mt = MaskedTensor(values=[[1, 2, 3], [4, 5, 6]],
mask=[[True, True, False], [True, False, True]])
# Fields are type-checked and converted to the declared types.
# E.g., mt.values is converted to a Tensor.
print(mt.values)
tf.Tensor( [[1 2 3] [4 5 6]], shape=(2, 3), dtype=int32)
Konstruktor TypeError
, jeśli wartości pola nie można przekonwertować na zadeklarowany typ:
try:
MaskedTensor([1, 2, 3], None)
except TypeError as e:
print(f"Got expected TypeError: {e}")
Got expected TypeError: mask: expected a Tensor, got None
Domyślną wartość pola można określić, ustawiając jego wartość na poziomie klasy:
class Pencil(tf.experimental.ExtensionType):
color: str = "black"
has_erasor: bool = True
length: tf.Tensor = 1.0
Pencil()
Pencil(color='black', has_erasor=True, length=<tf.Tensor: shape=(), dtype=float32, numpy=1.0>)
Pencil(length=0.5, color="blue")
Pencil(color='blue', has_erasor=True, length=<tf.Tensor: shape=(), dtype=float32, numpy=0.5>)
Reprezentacja do druku
ExtensionType
dodaje domyślną, drukowalną metodę reprezentacji ( __repr__
), która zawiera nazwę klasy i wartość każdego pola:
print(MaskedTensor(values=[1, 2, 3], mask=[True, True, False]))
MaskedTensor(values=<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 2, 3], dtype=int32)>, mask=<tf.Tensor: shape=(3,), dtype=bool, numpy=array([ True, True, False])>)
Operatorzy równości
ExtensionType
dodaje domyślne operatory równości ( __eq__
i __ne__
), które uznają dwie wartości za równe, jeśli mają ten sam typ i wszystkie ich pola są równe. Pola tensorowe są uważane za równe, jeśli mają ten sam kształt i są równe elementowo dla wszystkich elementów.
a = MaskedTensor([1, 2], [True, False])
b = MaskedTensor([[3, 4], [5, 6]], [[False, True], [True, True]])
print(f"a == a: {a==a}")
print(f"a == b: {a==b}")
print(f"a == a.values: {a==a.values}")
a == a: True a == b: False a == a.values: False
Metoda walidacji
ExtensionType
dodaje metodę __validate__
, którą można nadpisać w celu sprawdzenia poprawności pól. Jest uruchamiany po wywołaniu konstruktora i po sprawdzeniu typu pól i przekonwertowaniu ich na zadeklarowane typy, dzięki czemu można założyć, że wszystkie pola mają zadeklarowane typy.
Poniższy przykład aktualizuje MaskedTensor
, aby sprawdzić poprawność shape
s i dtype
s jego pól:
class MaskedTensor(tf.experimental.ExtensionType):
"""A tensor paired with a boolean mask, indicating which values are valid."""
values: tf.Tensor
mask: tf.Tensor
def __validate__(self):
self.values.shape.assert_is_compatible_with(self.mask.shape)
assert self.mask.dtype.is_bool, 'mask.dtype must be bool'
try:
MaskedTensor([1, 2, 3], [0, 1, 0]) # wrong dtype for mask.
except AssertionError as e:
print(f"Got expected AssertionError: {e}")
Got expected AssertionError: mask.dtype must be bool
try:
MaskedTensor([1, 2, 3], [True, False]) # shapes don't match.
except ValueError as e:
print(f"Got expected ValueError: {e}")
Got expected ValueError: Shapes (3,) and (2,) are incompatible
Wymuszona niezmienność
ExtensionType
zastępuje metody __setattr__
i __delattr__
, aby zapobiec mutacji, zapewniając, że wartości typu rozszerzenia są niezmienne.
mt = MaskedTensor([1, 2, 3], [True, False, True])
try:
mt.mask = [True, True, True]
except AttributeError as e:
print(f"Got expected AttributeError: {e}")
Got expected AttributeError: Cannot mutate attribute `mask` outside the custom constructor of ExtensionType.
try:
mt.mask[0] = False
except TypeError as e:
print(f"Got expected TypeError: {e}")
Got expected TypeError: 'tensorflow.python.framework.ops.EagerTensor' object does not support item assignment
try:
del mt.mask
except AttributeError as e:
print(f"Got expected AttributeError: {e}")
Got expected AttributeError: Cannot mutate attribute `mask` outside the custom constructor of ExtensionType.
Specyfikacja typu zagnieżdżonego
Każda klasa ExtensionType
ma odpowiadającą jej klasę TypeSpec
, która jest tworzona automatycznie i przechowywana jako <extension_type_name>.Spec
.
Ta klasa przechwytuje wszystkie informacje z wartości z wyjątkiem wartości dowolnych zagnieżdżonych tensorów. W szczególności TypeSpec
dla wartości jest tworzony przez zastąpienie dowolnego zagnieżdżonego Tensor, ExtensionType lub CompositeTensor jego TypeSpec
.
class Player(tf.experimental.ExtensionType):
name: tf.Tensor
attributes: Mapping[str, tf.Tensor]
anne = Player("Anne", {"height": 8.3, "speed": 28.1})
anne_spec = tf.type_spec_from_value(anne)
print(anne_spec.name) # Records dtype and shape, but not the string value.
print(anne_spec.attributes) # Records keys and TensorSpecs for values.
WARNING:tensorflow:Mapping types may not work well with tf.nest. Prefer using MutableMapping for <class 'tensorflow.python.framework.immutable_dict.ImmutableDict'> TensorSpec(shape=(), dtype=tf.string, name=None) ImmutableDict({'height': TensorSpec(shape=(), dtype=tf.float32, name=None), 'speed': TensorSpec(shape=(), dtype=tf.float32, name=None)})
Wartości TypeSpec
mogą być konstruowane jawnie lub mogą być budowane z wartości ExtensionType
przy użyciu tf.type_spec_from_value
:
spec1 = Player.Spec(name=tf.TensorSpec([], tf.float32), attributes={})
spec2 = tf.type_spec_from_value(anne)
TypeSpec
są używane przez TensorFlow do dzielenia wartości na składnik statyczny i składnik dynamiczny :
- Składnik statyczny (który jest ustalany w czasie tworzenia wykresu) jest kodowany za pomocą
tf.TypeSpec
. - Komponent dynamiczny (który może się zmieniać przy każdym uruchomieniu wykresu) jest zakodowany jako lista
tf.Tensor
s.
Na przykład tf.function
odtwarza swoją opakowaną funkcję za każdym razem, gdy argument ma wcześniej niewidoczną TypeSpec
:
@tf.function
def anonymize_player(player):
print("<<TRACING>>")
return Player("<anonymous>", player.attributes)
# Function gets traced (first time the function has been called):
anonymize_player(Player("Anne", {"height": 8.3, "speed": 28.1}))
WARNING:tensorflow:Mapping types may not work well with tf.nest. Prefer using MutableMapping for <class 'tensorflow.python.framework.immutable_dict.ImmutableDict'> WARNING:tensorflow:Mapping types may not work well with tf.nest. Prefer using MutableMapping for <class 'tensorflow.python.framework.immutable_dict.ImmutableDict'> <<TRACING>> Player(name=<tf.Tensor: shape=(), dtype=string, numpy=b'<anonymous>'>, attributes=ImmutableDict({'height': <tf.Tensor: shape=(), dtype=float32, numpy=8.3>, 'speed': <tf.Tensor: shape=(), dtype=float32, numpy=28.1>}))
# Function does NOT get traced (same TypeSpec: just tensor values changed)
anonymize_player(Player("Bart", {"height": 8.1, "speed": 25.3}))
Player(name=<tf.Tensor: shape=(), dtype=string, numpy=b'<anonymous>'>, attributes=ImmutableDict({'height': <tf.Tensor: shape=(), dtype=float32, numpy=8.1>, 'speed': <tf.Tensor: shape=(), dtype=float32, numpy=25.3>}))
# Function gets traced (new TypeSpec: keys for attributes changed):
anonymize_player(Player("Chuck", {"height": 11.0, "jump": 5.3}))
<<TRACING>> Player(name=<tf.Tensor: shape=(), dtype=string, numpy=b'<anonymous>'>, attributes=ImmutableDict({'height': <tf.Tensor: shape=(), dtype=float32, numpy=11.0>, 'jump': <tf.Tensor: shape=(), dtype=float32, numpy=5.3>}))
Więcej informacji można znaleźć w przewodniku tf.function Guide .
Dostosowywanie typów rozszerzeń
Oprócz prostego deklarowania pól i ich typów, typy rozszerzeń mogą:
- Zastąp domyślną reprezentację drukowalną (
__repr__
). - Zdefiniuj metody.
- Zdefiniuj metody klasowe i statyczne.
- Zdefiniuj właściwości.
- Zastąp domyślny konstruktor (
__init__
). - Zastąp domyślny operator równości (
__eq__
). - Zdefiniuj operatory (takie jak
__add__
i__lt__
). - Zadeklaruj wartości domyślne dla pól.
- Zdefiniuj podklasy.
Zastępowanie domyślnej reprezentacji do druku
Możesz zastąpić ten domyślny operator konwersji ciągów dla typów rozszerzeń. Poniższy przykład aktualizuje klasę MaskedTensor
, aby wygenerować bardziej czytelną reprezentację ciągu, gdy wartości są drukowane w trybie Eager.
class MaskedTensor(tf.experimental.ExtensionType):
"""A tensor paired with a boolean mask, indicating which values are valid."""
values: tf.Tensor
mask: tf.Tensor # shape=values.shape; false for invalid values.
def __repr__(self):
return masked_tensor_str(self.values, self.mask)
def masked_tensor_str(values, mask):
if isinstance(values, tf.Tensor):
if hasattr(values, 'numpy') and hasattr(mask, 'numpy'):
return f'<MaskedTensor {masked_tensor_str(values.numpy(), mask.numpy())}>'
else:
return f'MaskedTensor(values={values}, mask={mask})'
if len(values.shape) == 1:
items = [repr(v) if m else '_' for (v, m) in zip(values, mask)]
else:
items = [masked_tensor_str(v, m) for (v, m) in zip(values, mask)]
return '[%s]' % ', '.join(items)
mt = MaskedTensor(values=[[1, 2, 3], [4, 5, 6]],
mask=[[True, True, False], [True, False, True]])
print(mt)
<MaskedTensor [[1, 2, _], [4, _, 6]]>
Definiowanie metod
Typy rozszerzeń mogą definiować metody, tak jak każda normalna klasa Pythona. Na przykład typ MaskedTensor
może zdefiniować metodę with_default
, która zwraca kopię self
z zamaskowanymi wartościami zastąpionymi podaną wartością default
. Metody mogą opcjonalnie być opatrzone adnotacjami za pomocą dekoratora @tf.function
.
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
def with_default(self, default):
return tf.where(self.mask, self.values, default)
MaskedTensor([1, 2, 3], [True, False, True]).with_default(0)
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 0, 3], dtype=int32)>
Definiowanie metod klasowych i statycznych
Typy rozszerzeń mogą definiować metody przy użyciu dekoratorów @classmethod
i @staticmethod
. Na przykład typ MaskedTensor
mógłby zdefiniować metodę fabryki, która maskuje dowolny element o podanej wartości:
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
def __repr__(self):
return masked_tensor_str(self.values, self.mask)
@staticmethod
def from_tensor_and_value_to_mask(values, value_to_mask):
return MaskedTensor(values, values == value_to_mask)
x = tf.constant([[1, 0, 2], [3, 0, 0]])
MaskedTensor.from_tensor_and_value_to_mask(x, 0)
<MaskedTensor [[_, 0, _], [_, 0, 0]]>
Definiowanie właściwości
Typy rozszerzeń mogą definiować właściwości za pomocą dekoratora @property
, tak jak każda normalna klasa Pythona. Na przykład typ MaskedTensor
może definiować właściwość dtype
, która jest skrótem dla dtype wartości:
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
@property
def dtype(self):
return self.values.dtype
MaskedTensor([1, 2, 3], [True, False, True]).dtype
tf.int32
Zastępowanie domyślnego konstruktora
Możesz zastąpić domyślny konstruktor dla typów rozszerzeń. Konstruktory niestandardowe muszą ustawić wartość dla każdego zadeklarowanego pola; a po powrocie konstruktora niestandardowego wszystkie pola zostaną sprawdzone pod względem typu, a wartości zostaną przekonwertowane zgodnie z powyższym opisem.
class Toy(tf.experimental.ExtensionType):
name: str
price: tf.Tensor
def __init__(self, name, price, discount=0):
self.name = name
self.price = price * (1 - discount)
print(Toy("ball", 5.0, discount=0.2)) # On sale -- 20% off!
Toy(name='ball', price=<tf.Tensor: shape=(), dtype=float32, numpy=4.0>)
Alternatywnie możesz rozważyć pozostawienie domyślnego konstruktora bez zmian, ale dodanie jednej lub więcej metod fabrycznych. Np:
class Toy(tf.experimental.ExtensionType):
name: str
price: tf.Tensor
@staticmethod
def new_toy_with_discount(name, price, discount):
return Toy(name, price * (1 - discount))
print(Toy.new_toy_with_discount("ball", 5.0, discount=0.2))
Toy(name='ball', price=<tf.Tensor: shape=(), dtype=float32, numpy=4.0>)
Zastępowanie domyślnego operatora równości ( __eq__
)
Możesz zastąpić domyślny operator __eq__
dla typów rozszerzeń. Poniższy przykład aktualizuje MaskedTensor
, aby ignorować zamaskowane elementy podczas porównywania pod kątem równości.
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
def __repr__(self):
return masked_tensor_str(self.values, self.mask)
def __eq__(self, other):
result = tf.math.equal(self.values, other.values)
result = result | ~(self.mask & other.mask)
return tf.reduce_all(result)
x = MaskedTensor([1, 2, 3, 4], [True, True, False, True])
y = MaskedTensor([5, 2, 0, 4], [False, True, False, True])
print(x == y)
tf.Tensor(True, shape=(), dtype=bool)
Korzystanie z odwołań do przodu
Jeśli typ pola nie został jeszcze zdefiniowany, możesz zamiast niego użyć ciągu zawierającego nazwę typu. W poniższym przykładzie ciąg "Node"
jest używany do children
pola child, ponieważ typ Node
nie został jeszcze (w pełni) zdefiniowany.
class Node(tf.experimental.ExtensionType):
value: tf.Tensor
children: Tuple["Node", ...] = ()
Node(3, [Node(5), Node(2)])
Node(value=<tf.Tensor: shape=(), dtype=int32, numpy=3>, children=(Node(value=<tf.Tensor: shape=(), dtype=int32, numpy=5>, children=()), Node(value=<tf.Tensor: shape=(), dtype=int32, numpy=2>, children=())))
Definiowanie podklas
Typy rozszerzeń mogą być podzielone na podklasy przy użyciu standardowej składni Pythona. Podklasy typu rozszerzenia mogą dodawać nowe pola, metody i właściwości; i może przesłonić konstruktor, reprezentację drukowalną i operator równości. Poniższy przykład definiuje podstawową klasę TensorGraph
, która używa trzech pól Tensor
do kodowania zestawu krawędzi między węzłami. Następnie definiuje podklasę, która dodaje pole Tensor
, aby zarejestrować „wartość cechy” dla każdego węzła. Podklasa definiuje również metodę propagowania wartości cech wzdłuż krawędzi.
class TensorGraph(tf.experimental.ExtensionType):
num_nodes: tf.Tensor
edge_src: tf.Tensor # edge_src[e] = index of src node for edge e.
edge_dst: tf.Tensor # edge_dst[e] = index of dst node for edge e.
class TensorGraphWithNodeFeature(TensorGraph):
node_features: tf.Tensor # node_features[n] = feature value for node n.
def propagate_features(self, weight=1.0) -> 'TensorGraphWithNodeFeature':
updates = tf.gather(self.node_features, self.edge_src) * weight
new_node_features = tf.tensor_scatter_nd_add(
self.node_features, tf.expand_dims(self.edge_dst, 1), updates)
return TensorGraphWithNodeFeature(
self.num_nodes, self.edge_src, self.edge_dst, new_node_features)
g = TensorGraphWithNodeFeature( # Edges: 0->1, 4->3, 2->2, 2->1
num_nodes=5, edge_src=[0, 4, 2, 2], edge_dst=[1, 3, 2, 1],
node_features=[10.0, 0.0, 2.0, 5.0, -1.0, 0.0])
print("Original features:", g.node_features)
print("After propagating:", g.propagate_features().node_features)
Original features: tf.Tensor([10. 0. 2. 5. -1. 0.], shape=(6,), dtype=float32) After propagating: tf.Tensor([10. 12. 4. 4. -1. 0.], shape=(6,), dtype=float32)
Definiowanie pól prywatnych
Pola typu rozszerzenia można oznaczyć jako prywatne, poprzedzając je podkreśleniem (zgodnie ze standardowymi konwencjami Pythona). Nie ma to wpływu na sposób, w jaki TensorFlow traktuje pola w jakikolwiek sposób; ale po prostu służy jako sygnał dla wszystkich użytkowników typu rozszerzenia, że te pola są prywatne.
Dostosowywanie specyfikacji TypeSpec
rozszerzenia ExtensionType
Każda klasa ExtensionType
ma odpowiadającą jej klasę TypeSpec
, która jest tworzona automatycznie i przechowywana jako <extension_type_name>.Spec
. Aby uzyskać więcej informacji, zobacz sekcję „Zagnieżdżona specyfikacja typów” powyżej.
Aby dostosować TypeSpec
, po prostu zdefiniuj własną zagnieżdżoną klasę o nazwie Spec
, a ExtensionType
użyje jej jako podstawy dla automatycznie skonstruowanej TypeSpec
. Możesz dostosować klasę Spec
poprzez:
- Zastępowanie domyślnej reprezentacji do druku.
- Zastępowanie domyślnego konstruktora.
- Definiowanie metod, metod klasowych, metod statycznych i właściwości.
Poniższy przykład dostosowuje klasę MaskedTensor.Spec
, aby ułatwić jej użycie:
class MaskedTensor(tf.experimental.ExtensionType):
values: tf.Tensor
mask: tf.Tensor
shape = property(lambda self: self.values.shape)
dtype = property(lambda self: self.values.dtype)
def __repr__(self):
return masked_tensor_str(self.values, self.mask)
def with_values(self, new_values):
return MaskedTensor(new_values, self.mask)
class Spec:
def __init__(self, shape, dtype=tf.float32):
self.values = tf.TensorSpec(shape, dtype)
self.mask = tf.TensorSpec(shape, tf.bool)
def __repr__(self):
return f"MaskedTensor.Spec(shape={self.shape}, dtype={self.dtype})"
shape = property(lambda self: self.values.shape)
dtype = property(lambda self: self.values.dtype)
Wysyłka API Tensora
Typy rozszerzeń mogą być „podobne do tensora”, w tym sensie, że specjalizują się lub rozszerzają interfejs zdefiniowany przez typ tf.Tensor
. Przykłady typów rozszerzeń podobnych do tensora obejmują RaggedTensor
, SparseTensor
i MaskedTensor
. Dekoratory wysyłania mogą służyć do zastępowania domyślnego zachowania operacji TensorFlow w przypadku zastosowania do typów rozszerzeń podobnych do tensorów. TensorFlow obecnie definiuje trzy dekoratory wysyłek:
-
@tf.experimental.dispatch_for_api(tf_api)
-
@tf.experimental.dispatch_for_unary_elementwise_api(x_type)
-
@tf.experimental.dispatch_for_binary_elementwise_apis(x_type, y_type)
Wysyłka dla jednego API
Dekorator tf.experimental.dispatch_for_api
zastępuje domyślne zachowanie określonej operacji TensorFlow, gdy jest wywoływana z określoną sygnaturą. Na przykład możesz użyć tego dekoratora, aby określić, jak tf.stack
powinien przetwarzać wartości MaskedTensor
:
@tf.experimental.dispatch_for_api(tf.stack)
def masked_stack(values: List[MaskedTensor], axis = 0):
return MaskedTensor(tf.stack([v.values for v in values], axis),
tf.stack([v.mask for v in values], axis))
Zastępuje to domyślną implementację dla tf.stack
za każdym razem, gdy jest wywoływana z listą wartości MaskedTensor
(ponieważ argument values
jest oznaczony typing.List[MaskedTensor]
):
x = MaskedTensor([1, 2, 3], [True, True, False])
y = MaskedTensor([4, 5, 6], [False, True, True])
tf.stack([x, y])
<MaskedTensor [[1, 2, _], [_, 5, 6]]>
Aby umożliwić tf.stack
obsługę list mieszanych wartości MaskedTensor
i Tensor
, można udoskonalić adnotację typu dla parametru values
i odpowiednio zaktualizować treść funkcji:
tf.experimental.unregister_dispatch_for(masked_stack)
def convert_to_masked_tensor(x):
if isinstance(x, MaskedTensor):
return x
else:
return MaskedTensor(x, tf.ones_like(x, tf.bool))
@tf.experimental.dispatch_for_api(tf.stack)
def masked_stack_v2(values: List[Union[MaskedTensor, tf.Tensor]], axis = 0):
values = [convert_to_masked_tensor(v) for v in values]
return MaskedTensor(tf.stack([v.values for v in values], axis),
tf.stack([v.mask for v in values], axis))
x = MaskedTensor([1, 2, 3], [True, True, False])
y = tf.constant([4, 5, 6])
tf.stack([x, y, x])
<MaskedTensor [[1, 2, _], [4, 5, 6], [1, 2, _]]>
Listę interfejsów API, które można zastąpić, można znaleźć w dokumentacji interfejsu API dla tf.experimental.dispatch_for_api
.
Wysyłka dla wszystkich jednoargumentowych interfejsów API elementwise
Dekorator tf.experimental.dispatch_for_unary_elementwise_apis
przesłania domyślne zachowanie wszystkich operacji jednoargumentowych elementwise (takich jak tf.math.cos
), gdy wartość pierwszego argumentu (zazwyczaj o nazwie x
) pasuje do adnotacji typu x_type
. Zdobiona funkcja powinna mieć dwa argumenty:
-
api_func
: Funkcja, która przyjmuje pojedynczy parametr i wykonuje operację elementwise (np.tf.abs
). -
x
: pierwszy argument operacji elementarnej.
Poniższy przykład aktualizuje wszystkie jednoargumentowe operacje elementwise w celu obsługi typu MaskedTensor
:
@tf.experimental.dispatch_for_unary_elementwise_apis(MaskedTensor)
def masked_tensor_unary_elementwise_api_handler(api_func, x):
return MaskedTensor(api_func(x.values), x.mask)
Ta funkcja będzie teraz używana za każdym razem, gdy jednoargumentowa operacja elementowa jest wywoływana na MaskedTensor
.
x = MaskedTensor([1, -2, -3], [True, False, True])
print(tf.abs(x))
<MaskedTensor [1, _, 3]>
print(tf.ones_like(x, dtype=tf.float32))
<MaskedTensor [1.0, _, 1.0]>
Wyślij binarne wszystkie API elementwise
Podobnie, tf.experimental.dispatch_for_binary_elementwise_apis
może służyć do aktualizacji wszystkich binarnych operacji elementwise do obsługi typu MaskedTensor
:
@tf.experimental.dispatch_for_binary_elementwise_apis(MaskedTensor, MaskedTensor)
def masked_tensor_binary_elementwise_api_handler(api_func, x, y):
return MaskedTensor(api_func(x.values, y.values), x.mask & y.mask)
x = MaskedTensor([1, -2, -3], [True, False, True])
y = MaskedTensor([[4], [5]], [[True], [False]])
tf.math.add(x, y)
<MaskedTensor [[5, _, 1], [_, _, _]]>
Listę interfejsów API elementwise, które są zastępowane, można znaleźć w dokumentacji interfejsu API dla tf.experimental.dispatch_for_unary_elementwise_apis
i tf.experimental.dispatch_for_binary_elementwise_apis
.
Typy rozszerzeń wsadowych
ExtensionType
można przetwarzać wsadowo , jeśli pojedyncze wystąpienie może służyć do reprezentowania partii wartości. Zazwyczaj osiąga się to poprzez dodanie wymiarów wsadowych do wszystkich zagnieżdżonych Tensor
. Następujące interfejsy API TensorFlow wymagają, aby wszelkie dane wejściowe typu rozszerzenia były grupowe:
-
tf.data.Dataset
(batch
,unbatch
,from_tensor_slices
) -
tf.Keras
(fit
,evaluate
,predict
) -
tf.map_fn
Domyślnie BatchableExtensionType
tworzy wartości wsadowe, grupując dowolne zagnieżdżone Tensor
s, CompositeTensor
s i ExtensionType
s. Jeśli nie jest to odpowiednie dla Twojej klasy, będziesz musiał użyć tf.experimental.ExtensionTypeBatchEncoder
, aby zastąpić to domyślne zachowanie. Na przykład nie byłoby właściwe tworzenie partii wartości tf.SparseTensor
przez proste zestawienie poszczególnych values
tensorów sparse , indices
i pól dense_shape
— w większości przypadków nie można układać tych tensorów w stos, ponieważ mają one niezgodne kształty ; a nawet gdybyś mógł, wynik nie byłby prawidłowym SparseTensor
.
Przykład BatchableExtensionType: Sieć
Jako przykład rozważmy prostą klasę Network
używaną do równoważenia obciążenia, która śledzi, ile pracy pozostało do wykonania w każdym węźle i jaka przepustowość jest dostępna do przenoszenia pracy między węzłami:
class Network(tf.experimental.ExtensionType): # This version is not batchable.
work: tf.Tensor # work[n] = work left to do at node n
bandwidth: tf.Tensor # bandwidth[n1, n2] = bandwidth from n1->n2
net1 = Network([5., 3, 8], [[0., 2, 0], [2, 0, 3], [0, 3, 0]])
net2 = Network([3., 4, 2], [[0., 2, 2], [2, 0, 2], [2, 2, 0]])
Aby uczynić ten typ partiami, zmień typ podstawowy na BatchableExtensionType
i dostosuj kształt każdego pola, aby uwzględnić opcjonalne wymiary partii. Poniższy przykład dodaje również pole shape
, aby śledzić kształt partii. To pole shape
nie jest wymagane przez tf.data.Dataset
ani tf.map_fn
, ale jest wymagane przez tf.Keras
.
class Network(tf.experimental.BatchableExtensionType):
shape: tf.TensorShape # batch shape. A single network has shape=[].
work: tf.Tensor # work[*shape, n] = work left to do at node n
bandwidth: tf.Tensor # bandwidth[*shape, n1, n2] = bandwidth from n1->n2
def __init__(self, work, bandwidth):
self.work = tf.convert_to_tensor(work)
self.bandwidth = tf.convert_to_tensor(bandwidth)
work_batch_shape = self.work.shape[:-1]
bandwidth_batch_shape = self.bandwidth.shape[:-2]
self.shape = work_batch_shape.merge_with(bandwidth_batch_shape)
def __repr__(self):
return network_repr(self)
def network_repr(network):
work = network.work
bandwidth = network.bandwidth
if hasattr(work, 'numpy'):
work = ' '.join(str(work.numpy()).split())
if hasattr(bandwidth, 'numpy'):
bandwidth = ' '.join(str(bandwidth.numpy()).split())
return (f"<Network shape={network.shape} work={work} bandwidth={bandwidth}>")
net1 = Network([5., 3, 8], [[0., 2, 0], [2, 0, 3], [0, 3, 0]])
net2 = Network([3., 4, 2], [[0., 2, 2], [2, 0, 2], [2, 2, 0]])
batch_of_networks = Network(
work=tf.stack([net1.work, net2.work]),
bandwidth=tf.stack([net1.bandwidth, net2.bandwidth]))
print(f"net1={net1}")
print(f"net2={net2}")
print(f"batch={batch_of_networks}")
net1=<Network shape=() work=[5. 3. 8.] bandwidth=[[0. 2. 0.] [2. 0. 3.] [0. 3. 0.]]> net2=<Network shape=() work=[3. 4. 2.] bandwidth=[[0. 2. 2.] [2. 0. 2.] [2. 2. 0.]]> batch=<Network shape=(2,) work=[[5. 3. 8.] [3. 4. 2.]] bandwidth=[[[0. 2. 0.] [2. 0. 3.] [0. 3. 0.]] [[0. 2. 2.] [2. 0. 2.] [2. 2. 0.]]]>
Następnie możesz użyć tf.data.Dataset
, aby przejść przez partię sieci:
dataset = tf.data.Dataset.from_tensor_slices(batch_of_networks)
for i, network in enumerate(dataset):
print(f"Batch element {i}: {network}")
Batch element 0: <Network shape=() work=[5. 3. 8.] bandwidth=[[0. 2. 0.] [2. 0. 3.] [0. 3. 0.]]> Batch element 1: <Network shape=() work=[3. 4. 2.] bandwidth=[[0. 2. 2.] [2. 0. 2.] [2. 2. 0.]]>
Możesz także użyć map_fn
, aby zastosować funkcję do każdego elementu wsadowego:
def balance_work_greedy(network):
delta = (tf.expand_dims(network.work, -1) - tf.expand_dims(network.work, -2))
delta /= 4
delta = tf.maximum(tf.minimum(delta, network.bandwidth), -network.bandwidth)
new_work = network.work + tf.reduce_sum(delta, -1)
return Network(new_work, network.bandwidth)
tf.map_fn(balance_work_greedy, batch_of_networks)
<Network shape=(2,) work=[[5.5 1.25 9.25] [3. 4.75 1.25]] bandwidth=[[[0. 2. 0.] [2. 0. 3.] [0. 3. 0.]] [[0. 2. 2.] [2. 0. 2.] [2. 2. 0.]]]>
Interfejsy API TensorFlow obsługujące typy rozszerzeń
@tf.funkcja
tf.function to dekorator, który wstępnie oblicza wykresy TensorFlow dla funkcji Pythona, co może znacznie poprawić wydajność Twojego kodu TensorFlow. Wartości typu rozszerzenia mogą być używane w sposób przezroczysty z @tf.function
.
class Pastry(tf.experimental.ExtensionType):
sweetness: tf.Tensor # 2d embedding that encodes sweetness
chewiness: tf.Tensor # 2d embedding that encodes chewiness
@tf.function
def combine_pastry_features(x: Pastry):
return (x.sweetness + x.chewiness) / 2
cookie = Pastry(sweetness=[1.2, 0.4], chewiness=[0.8, 0.2])
combine_pastry_features(cookie)
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([1. , 0.3], dtype=float32)>
Jeśli chcesz jawnie określić input_signature
dla tf.function
, możesz to zrobić za pomocą TypeSpec
typu rozszerzenia.
pastry_spec = Pastry.Spec(tf.TensorSpec([2]), tf.TensorSpec(2))
@tf.function(input_signature=[pastry_spec])
def increase_sweetness(x: Pastry, delta=1.0):
return Pastry(x.sweetness + delta, x.chewiness)
increase_sweetness(cookie)
Pastry(sweetness=<tf.Tensor: shape=(2,), dtype=float32, numpy=array([2.2, 1.4], dtype=float32)>, chewiness=<tf.Tensor: shape=(2,), dtype=float32, numpy=array([0.8, 0.2], dtype=float32)>)
Konkretne funkcje
Konkretne funkcje hermetyzują poszczególne grafy śledzone, które są budowane przez tf.function
. Typy rozszerzeń mogą być używane w sposób przezroczysty z konkretnymi funkcjami.
cf = combine_pastry_features.get_concrete_function(pastry_spec)
cf(cookie)
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([1. , 0.3], dtype=float32)>
Kontroluj operacje przepływu
Typy rozszerzeń są obsługiwane przez operacje przepływu sterowania TensorFlow:
# Example: using tf.cond to select between two MaskedTensors. Note that the
# two MaskedTensors don't need to have the same shape.
a = MaskedTensor([1., 2, 3], [True, False, True])
b = MaskedTensor([22., 33, 108, 55], [True, True, True, False])
condition = tf.constant(True)
print(tf.cond(condition, lambda: a, lambda: b))
<MaskedTensor [1.0, _, 3.0]>
# Example: using tf.while_loop with MaskedTensor.
cond = lambda i, _: i < 10
def body(i, mt):
return i + 1, mt.with_values(mt.values + 3 / 7)
print(tf.while_loop(cond, body, [0, b])[1])
<MaskedTensor [26.285717, 37.285698, 112.285736, _]>
Kontrola przebiegu autografu
Typy rozszerzeń są również obsługiwane przez instrukcje przepływu sterowania w tf.function (przy użyciu autografu). W poniższym przykładzie instrukcje if
i instrukcje for
są automatycznie konwertowane na tf.cond
i tf.while_loop
, które obsługują typy rozszerzeń.
@tf.function
def fn(x, b):
if b:
x = MaskedTensor(x, tf.less(x, 0))
else:
x = MaskedTensor(x, tf.greater(x, 0))
for i in tf.range(5 if b else 7):
x = x.with_values(x.values + 1 / 2)
return x
print(fn(tf.constant([1., -2, 3]), tf.constant(True)))
print(fn(tf.constant([1., -2, 3]), tf.constant(False)))
<MaskedTensor [_, 0.5, _]> <MaskedTensor [4.5, _, 6.5]>
Keras
tf.keras to wysokopoziomowy interfejs API firmy TensorFlow do tworzenia i trenowania modeli uczenia głębokiego. Typy rozszerzeń mogą być przekazywane jako dane wejściowe do modelu Keras, przekazywane między warstwami Keras i zwracane przez modele Keras. Keras stawia obecnie dwa wymagania dotyczące typów rozszerzeń:
- Muszą nadawać się do przetwarzania wsadowego (patrz „Batchable ExtensionTypes” powyżej).
- Musi mieć pole lub właściwość o nazwie
shape
. Przyjmuje się, żeshape[0]
jest wymiarem partii.
Poniższe dwie podsekcje zawierają przykłady pokazujące, w jaki sposób typy rozszerzeń mogą być używane z Keras.
Przykład Kerasa: Network
W pierwszym przykładzie rozważmy klasę Network
zdefiniowaną w sekcji „Batchable ExtensionTypes” powyżej, która może służyć do równoważenia obciążenia między węzłami. Jego definicja jest tutaj powtórzona:
class Network(tf.experimental.BatchableExtensionType):
shape: tf.TensorShape # batch shape. A single network has shape=[].
work: tf.Tensor # work[*shape, n] = work left to do at node n
bandwidth: tf.Tensor # bandwidth[*shape, n1, n2] = bandwidth from n1->n2
def __init__(self, work, bandwidth):
self.work = tf.convert_to_tensor(work)
self.bandwidth = tf.convert_to_tensor(bandwidth)
work_batch_shape = self.work.shape[:-1]
bandwidth_batch_shape = self.bandwidth.shape[:-2]
self.shape = work_batch_shape.merge_with(bandwidth_batch_shape)
def __repr__(self):
return network_repr(self)
single_network = Network( # A single network w/ 4 nodes.
work=[8.0, 5, 12, 2],
bandwidth=[[0.0, 1, 2, 2], [1, 0, 0, 2], [2, 0, 0, 1], [2, 2, 1, 0]])
batch_of_networks = Network( # Batch of 2 networks, each w/ 2 nodes.
work=[[8.0, 5], [3, 2]],
bandwidth=[[[0.0, 1], [1, 0]], [[0, 2], [2, 0]]])
Możesz zdefiniować nową warstwę Keras, która przetwarza Network
.
class BalanceNetworkLayer(tf.keras.layers.Layer):
"""Layer that balances work between nodes in a network.
Shifts work from more busy nodes to less busy nodes, constrained by bandwidth.
"""
def call(self, inputs):
# This function is defined above, in "Batchable ExtensionTypes" section.
return balance_work_greedy(inputs)
Następnie możesz użyć tych warstw do stworzenia prostego modelu. Aby wprowadzić ExtensionType
do modelu, można użyć warstwy tf.keras.layer.Input
z type_spec
ustawionym na TypeSpec
typu rozszerzenia. Jeśli model Keras będzie używany do przetwarzania partii, wówczas parametr type_spec
musi zawierać wymiar partii.
input_spec = Network.Spec(shape=None,
work=tf.TensorSpec(None, tf.float32),
bandwidth=tf.TensorSpec(None, tf.float32))
model = tf.keras.Sequential([
tf.keras.layers.Input(type_spec=input_spec),
BalanceNetworkLayer(),
])
Na koniec możesz zastosować model do pojedynczej sieci i do partii sieci.
model(single_network)
<Network shape=() work=[ 9.25 5. 14. -1.25] bandwidth=[[0. 1. 2. 2.] [1. 0. 0. 2.] [2. 0. 0. 1.] [2. 2. 1. 0.]]>
model(batch_of_networks)
<Network shape=(2,) work=[[8.75 4.25] [3.25 1.75]] bandwidth=[[[0. 1.] [1. 0.]] [[0. 2.] [2. 0.]]]>
Przykład Keras: MaskedTensor
W tym przykładzie MaskedTensor
jest rozszerzony o obsługę Keras
. shape
jest definiowany jako właściwość obliczana z pola values
. Keras wymaga dodania tej właściwości zarówno do typu rozszerzenia, jak i do jego TypeSpec
. MaskedTensor
definiuje również zmienną __name__
, która będzie wymagana do serializacji SavedModel
(poniżej).
class MaskedTensor(tf.experimental.BatchableExtensionType):
# __name__ is required for serialization in SavedModel; see below for details.
__name__ = 'extension_type_colab.MaskedTensor'
values: tf.Tensor
mask: tf.Tensor
shape = property(lambda self: self.values.shape)
dtype = property(lambda self: self.values.dtype)
def with_default(self, default):
return tf.where(self.mask, self.values, default)
def __repr__(self):
return masked_tensor_str(self.values, self.mask)
class Spec:
def __init__(self, shape, dtype=tf.float32):
self.values = tf.TensorSpec(shape, dtype)
self.mask = tf.TensorSpec(shape, tf.bool)
shape = property(lambda self: self.values.shape)
dtype = property(lambda self: self.values.dtype)
def with_shape(self):
return MaskedTensor.Spec(tf.TensorSpec(shape, self.values.dtype),
tf.TensorSpec(shape, self.mask.dtype))
Następnie dekoratory wysyłania są używane do zastąpienia domyślnego zachowania kilku interfejsów API TensorFlow. Ponieważ te interfejsy API są używane przez standardowe warstwy Keras (takie jak warstwa Dense
), zastąpienie ich pozwoli nam używać tych warstw z MaskedTensor
. Na potrzeby tego przykładu, matmul
dla zamaskowanych tensorów jest zdefiniowany w celu traktowania zamaskowanych wartości jako zer (tj. aby nie uwzględniać ich w produkcie).
@tf.experimental.dispatch_for_unary_elementwise_apis(MaskedTensor)
def unary_elementwise_op_handler(op, x):
return MaskedTensor(op(x.values), x.mask)
@tf.experimental.dispatch_for_binary_elementwise_apis(
Union[MaskedTensor, tf.Tensor],
Union[MaskedTensor, tf.Tensor])
def binary_elementwise_op_handler(op, x, y):
x = convert_to_masked_tensor(x)
y = convert_to_masked_tensor(y)
return MaskedTensor(op(x.values, y.values), x.mask & y.mask)
@tf.experimental.dispatch_for_api(tf.matmul)
def masked_matmul(a: MaskedTensor, b,
transpose_a=False, transpose_b=False,
adjoint_a=False, adjoint_b=False,
a_is_sparse=False, b_is_sparse=False,
output_type=None):
if isinstance(a, MaskedTensor):
a = a.with_default(0)
if isinstance(b, MaskedTensor):
b = b.with_default(0)
return tf.matmul(a, b, transpose_a, transpose_b, adjoint_a,
adjoint_b, a_is_sparse, b_is_sparse, output_type)
Następnie można skonstruować model Keras, który akceptuje dane wejściowe MaskedTensor
, używając standardowych warstw Keras:
input_spec = MaskedTensor.Spec([None, 2], tf.float32)
masked_tensor_model = tf.keras.Sequential([
tf.keras.layers.Input(type_spec=input_spec),
tf.keras.layers.Dense(16, activation="relu"),
tf.keras.layers.Dense(1)])
masked_tensor_model.compile(loss='binary_crossentropy', optimizer='rmsprop')
a = MaskedTensor([[1., 2], [3, 4], [5, 6]],
[[True, False], [False, True], [True, True]])
masked_tensor_model.fit(a, tf.constant([[1], [0], [1]]), epochs=3)
print(masked_tensor_model(a))
Epoch 1/3 1/1 [==============================] - 1s 955ms/step - loss: 10.2833 Epoch 2/3 1/1 [==============================] - 0s 5ms/step - loss: 10.2833 Epoch 3/3 1/1 [==============================] - 0s 5ms/step - loss: 10.2833 tf.Tensor( [[-0.09944128] [-0.7225147 ] [-1.3020657 ]], shape=(3, 1), dtype=float32)
Zapisany model
SavedModel to serializowany program TensorFlow, obejmujący zarówno wagi, jak i obliczenia. Może być zbudowany z modelu Keras lub z modelu niestandardowego. W obu przypadkach typy rozszerzeń mogą być używane w sposób przezroczysty z funkcjami i metodami zdefiniowanymi przez SavedModel.
SavedModel może zapisywać modele, warstwy i funkcje, które przetwarzają typy rozszerzeń, o ile typy rozszerzeń mają pole __name__
. Ta nazwa służy do rejestrowania typu rozszerzenia, dzięki czemu można ją zlokalizować, gdy model jest ładowany.
Przykład: zapisywanie modelu Keras
Modele Keras, które używają typów rozszerzeń, można zapisać za pomocą SavedModel
.
masked_tensor_model_path = tempfile.mkdtemp()
tf.saved_model.save(masked_tensor_model, masked_tensor_model_path)
imported_model = tf.saved_model.load(masked_tensor_model_path)
imported_model(a)
2021-11-06 01:25:14.285250: W tensorflow/python/util/util.cc:368] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them. WARNING:absl:Function `_wrapped_model` contains input name(s) args_0 with unsupported characters which will be renamed to args_0_1 in the SavedModel. INFO:tensorflow:Assets written to: /tmp/tmp3ceuupv9/assets INFO:tensorflow:Assets written to: /tmp/tmp3ceuupv9/assets <tf.Tensor: shape=(3, 1), dtype=float32, numpy= array([[-0.09944128], [-0.7225147 ], [-1.3020657 ]], dtype=float32)>
Przykład: zapisywanie niestandardowego modelu
SavedModel może być również używany do zapisywania niestandardowych podklas tf.Module
z funkcjami przetwarzającymi typy rozszerzeń.
class CustomModule(tf.Module):
def __init__(self, variable_value):
super().__init__()
self.v = tf.Variable(variable_value)
@tf.function
def grow(self, x: MaskedTensor):
"""Increase values in `x` by multiplying them by `self.v`."""
return MaskedTensor(x.values * self.v, x.mask)
module = CustomModule(100.0)
module.grow.get_concrete_function(MaskedTensor.Spec(shape=None,
dtype=tf.float32))
custom_module_path = tempfile.mkdtemp()
tf.saved_model.save(module, custom_module_path)
imported_model = tf.saved_model.load(custom_module_path)
imported_model.grow(MaskedTensor([1., 2, 3], [False, True, False]))
INFO:tensorflow:Assets written to: /tmp/tmp2x8zq5kb/assets INFO:tensorflow:Assets written to: /tmp/tmp2x8zq5kb/assets <MaskedTensor [_, 200.0, _]>
Ładowanie SavedModel, gdy ExtensionType jest niedostępny
Jeśli załadujesz SavedModel
, który używa ExtensionType
, ale ten ExtensionType
nie jest dostępny (tj. nie został zaimportowany), zobaczysz ostrzeżenie, a TensorFlow wróci do używania obiektu „anonimowego typu rozszerzenia”. Ten obiekt będzie miał te same pola, co oryginalny typ, ale nie będzie w nim żadnych dalszych dostosowań dodanych dla typu, takich jak niestandardowe metody lub właściwości.
Korzystanie z typów rozszerzeń z obsługą TensorFlow
Obecnie obsługa TensorFlow (i inni użytkownicy słownika „sygnatur” SavedModel) wymagają, aby wszystkie dane wejściowe i wyjściowe były surowymi tensorami. Jeśli chcesz korzystać z udostępniania TensorFlow z modelem, który używa typów rozszerzeń, możesz dodać metody opakowujące, które komponują lub rozkładają wartości typów rozszerzeń z tensorów. Np:
class CustomModuleWrapper(tf.Module):
def __init__(self, variable_value):
super().__init__()
self.v = tf.Variable(variable_value)
@tf.function
def var_weighted_mean(self, x: MaskedTensor):
"""Mean value of unmasked values in x, weighted by self.v."""
x = MaskedTensor(x.values * self.v, x.mask)
return (tf.reduce_sum(x.with_default(0)) /
tf.reduce_sum(tf.cast(x.mask, x.dtype)))
@tf.function()
def var_weighted_mean_wrapper(self, x_values, x_mask):
"""Raw tensor wrapper for var_weighted_mean."""
return self.var_weighted_mean(MaskedTensor(x_values, x_mask))
module = CustomModuleWrapper([3., 2., 8., 5.])
module.var_weighted_mean_wrapper.get_concrete_function(
tf.TensorSpec(None, tf.float32), tf.TensorSpec(None, tf.bool))
custom_module_path = tempfile.mkdtemp()
tf.saved_model.save(module, custom_module_path)
imported_model = tf.saved_model.load(custom_module_path)
x = MaskedTensor([1., 2., 3., 4.], [False, True, False, True])
imported_model.var_weighted_mean_wrapper(x.values, x.mask)
INFO:tensorflow:Assets written to: /tmp/tmpxhh4zh0i/assets INFO:tensorflow:Assets written to: /tmp/tmpxhh4zh0i/assets <tf.Tensor: shape=(), dtype=float32, numpy=12.0>
Zbiory danych
tf.data to interfejs API, który umożliwia budowanie złożonych potoków wejściowych z prostych elementów wielokrotnego użytku. Jego podstawową strukturą danych jest tf.data.Dataset
, która reprezentuje sekwencję elementów, w której każdy element składa się z jednego lub więcej składników.
Budowanie zbiorów danych z typami rozszerzeń
Zestawy danych można budować z wartości typu rozszerzenia za pomocą Dataset.from_tensors
, Dataset.from_tensor_slices
lub Dataset.from_generator
:
ds = tf.data.Dataset.from_tensors(Pastry(5, 5))
iter(ds).next()
Pastry(sweetness=<tf.Tensor: shape=(), dtype=int32, numpy=5>, chewiness=<tf.Tensor: shape=(), dtype=int32, numpy=5>)
mt = MaskedTensor(tf.reshape(range(20), [5, 4]), tf.ones([5, 4]))
ds = tf.data.Dataset.from_tensor_slices(mt)
for value in ds:
print(value)
<MaskedTensor [0, 1, 2, 3]> <MaskedTensor [4, 5, 6, 7]> <MaskedTensor [8, 9, 10, 11]> <MaskedTensor [12, 13, 14, 15]> <MaskedTensor [16, 17, 18, 19]>
def value_gen():
for i in range(2, 7):
yield MaskedTensor(range(10), [j%i != 0 for j in range(10)])
ds = tf.data.Dataset.from_generator(
value_gen, output_signature=MaskedTensor.Spec(shape=[10], dtype=tf.int32))
for value in ds:
print(value)
<MaskedTensor [_, 1, _, 3, _, 5, _, 7, _, 9]> <MaskedTensor [_, 1, 2, _, 4, 5, _, 7, 8, _]> <MaskedTensor [_, 1, 2, 3, _, 5, 6, 7, _, 9]> <MaskedTensor [_, 1, 2, 3, 4, _, 6, 7, 8, 9]> <MaskedTensor [_, 1, 2, 3, 4, 5, _, 7, 8, 9]>
Grupowanie i rozdzielanie zbiorów danych z typami rozszerzeń
Zestawy danych z typami rozszerzeń mogą być wsadowe i niewsadowe przy użyciu Dataset.batch
adn Dataset.unbatch
.
batched_ds = ds.batch(2)
for value in batched_ds:
print(value)
<MaskedTensor [[_, 1, _, 3, _, 5, _, 7, _, 9], [_, 1, 2, _, 4, 5, _, 7, 8, _]]> <MaskedTensor [[_, 1, 2, 3, _, 5, 6, 7, _, 9], [_, 1, 2, 3, 4, _, 6, 7, 8, 9]]> <MaskedTensor [[_, 1, 2, 3, 4, 5, _, 7, 8, 9]]>
unbatched_ds = batched_ds.unbatch()
for value in unbatched_ds:
print(value)
<MaskedTensor [_, 1, _, 3, _, 5, _, 7, _, 9]> <MaskedTensor [_, 1, 2, _, 4, 5, _, 7, 8, _]> <MaskedTensor [_, 1, 2, 3, _, 5, 6, 7, _, 9]> <MaskedTensor [_, 1, 2, 3, 4, _, 6, 7, 8, 9]> <MaskedTensor [_, 1, 2, 3, 4, 5, _, 7, 8, 9]>