Utilizzo del formato SavedModel

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub Scarica il taccuino

Un SavedModel contiene un programma TensorFlow completo, inclusi i parametri addestrati (ad esempio, tf.Variable s) e il calcolo. Non richiede l'esecuzione del codice di creazione del modello originale, il che lo rende utile per la condivisione o la distribuzione con TFLite , TensorFlow.js , TensorFlow Serving o TensorFlow Hub .

Puoi salvare e caricare un modello nel formato SavedModel utilizzando le seguenti API:

Creazione di un modello salvato da Keras

Per una rapida introduzione, questa sezione esporta un modello Keras pre-addestrato e serve con esso le richieste di classificazione delle immagini. Il resto della guida riempirà i dettagli e discuterà altri modi per creare modelli salvati.

import os
import tempfile

from matplotlib import pyplot as plt
import numpy as np
import tensorflow as tf

tmpdir = tempfile.mkdtemp()
physical_devices = tf.config.list_physical_devices('GPU')
for device in physical_devices:
  tf.config.experimental.set_memory_growth(device, True)
file = tf.keras.utils.get_file(
    "grace_hopper.jpg",
    "https://storage.googleapis.com/download.tensorflow.org/example_images/grace_hopper.jpg")
img = tf.keras.preprocessing.image.load_img(file, target_size=[224, 224])
plt.imshow(img)
plt.axis('off')
x = tf.keras.preprocessing.image.img_to_array(img)
x = tf.keras.applications.mobilenet.preprocess_input(
    x[tf.newaxis,...])
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/grace_hopper.jpg
65536/61306 [================================] - 0s 0us/step

png

Utilizzerai un'immagine di Grace Hopper come esempio in esecuzione e un modello di classificazione delle immagini pre-addestrato da Keras poiché è facile da usare. Anche i modelli personalizzati funzionano e sono trattati in dettaglio in seguito.

labels_path = tf.keras.utils.get_file(
    'ImageNetLabels.txt',
    'https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt')
imagenet_labels = np.array(open(labels_path).read().splitlines())
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt
16384/10484 [==============================================] - 0s 0us/step
pretrained_model = tf.keras.applications.MobileNet()
result_before_save = pretrained_model(x)

decoded = imagenet_labels[np.argsort(result_before_save)[0,::-1][:5]+1]

print("Result before saving:\n", decoded)
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/mobilenet/mobilenet_1_0_224_tf.h5
17227776/17225924 [==============================] - 0s 0us/step
Result before saving:
 ['military uniform' 'bow tie' 'suit' 'bearskin' 'pickelhaube']

La migliore previsione per questa immagine è "uniforme militare".

mobilenet_save_path = os.path.join(tmpdir, "mobilenet/1/")
tf.saved_model.save(pretrained_model, mobilenet_save_path)
INFO:tensorflow:Assets written to: /tmp/tmpfcgkddlh/mobilenet/1/assets

Il percorso di salvataggio segue una convenzione utilizzata da TensorFlow Serving in cui l'ultimo componente del percorso ( 1/ qui) è un numero di versione per il modello: consente a strumenti come Tensorflow Serving di ragionare sulla relativa freschezza.

Puoi caricare nuovamente il SavedModel in Python con tf.saved_model.load e vedere come è classificata l'immagine dell'ammiraglio Hopper.

loaded = tf.saved_model.load(mobilenet_save_path)
print(list(loaded.signatures.keys()))  # ["serving_default"]
['serving_default']

Le firme importate restituiscono sempre dizionari. Per personalizzare i nomi delle firme e le chiavi del dizionario di output, vedere Specifica delle firme durante l'esportazione .

infer = loaded.signatures["serving_default"]
print(infer.structured_outputs)
{'predictions': TensorSpec(shape=(None, 1000), dtype=tf.float32, name='predictions')}

L'esecuzione dell'inferenza da SavedModel fornisce lo stesso risultato del modello originale.

labeling = infer(tf.constant(x))[pretrained_model.output_names[0]]

decoded = imagenet_labels[np.argsort(labeling)[0,::-1][:5]+1]

print("Result after saving and loading:\n", decoded)
Result after saving and loading:
 ['military uniform' 'bow tie' 'suit' 'bearskin' 'pickelhaube']

Esecuzione di un modello salvato in TensorFlow Serving

I modelli salvati sono utilizzabili da Python (maggiori informazioni di seguito), ma gli ambienti di produzione in genere utilizzano un servizio dedicato per l'inferenza senza eseguire il codice Python. Questo è facile da configurare da un modello salvato utilizzando TensorFlow Serving.

Vedere l' esercitazione TensorFlow Serving REST per un esempio end-to-end di utilizzo di tensorflow.

Il formato SavedModel su disco

Un SavedModel è una directory contenente le firme serializzate e lo stato necessario per eseguirle, inclusi i valori delle variabili e i vocabolari.

ls {mobilenet_save_path}
assets  saved_model.pb  variables

Il file saved_model.pb memorizza il programma o modello TensorFlow effettivo e un insieme di firme denominate, ciascuna identificante una funzione che accetta input di tensore e produce output di tensore.

SavedModels possono contenere molteplici varianti del modello (multiple v1.MetaGraphDefs , identificati con il --tag_set bandiera saved_model_cli ), ma questo è raro. Le API che creano più varianti di un modello includono tf.Estimator.experimental_export_all_saved_models e in TensorFlow 1.x tf.saved_model.Builder .

saved_model_cli show --dir {mobilenet_save_path} --tag_set serve
2021-02-11 02:25:22.757135: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcudart.so.11.0
The given SavedModel MetaGraphDef contains SignatureDefs with the following keys:
SignatureDef key: "__saved_model_init_op"
SignatureDef key: "serving_default"

La directory delle variables contiene un checkpoint di addestramento standard (vedi la guida ai checkpoint di addestramento ).

ls {mobilenet_save_path}/variables
variables.data-00000-of-00001  variables.index

La directory delle assets contiene i file utilizzati dal grafico TensorFlow, ad esempio i file di testo utilizzati per inizializzare le tabelle di vocabolario. Non è utilizzato in questo esempio.

SavedModels può avere una directory assets.extra per tutti i file non utilizzati dal grafico TensorFlow, ad esempio informazioni per i consumatori su cosa fare con SavedModel. Lo stesso TensorFlow non usa questa directory.

Salvataggio di un modello personalizzato

tf.saved_model.save supporta il salvataggio di oggetti tf.Module e delle sue sottoclassi, come tf.keras.Layer e tf.keras.Model .

Diamo un'occhiata a un esempio di salvataggio e ripristino di un tf.Module .

class CustomModule(tf.Module):

  def __init__(self):
    super(CustomModule, self).__init__()
    self.v = tf.Variable(1.)

  @tf.function
  def __call__(self, x):
    print('Tracing with', x)
    return x * self.v

  @tf.function(input_signature=[tf.TensorSpec([], tf.float32)])
  def mutate(self, new_v):
    self.v.assign(new_v)

module = CustomModule()

Quando si salva un tf.Module , eventuali tf.Variable attributi, tf.function -decorated metodi, e tf.Module s trovato tramite attraversamento ricorsivo sono salvati. (Vedi il tutorial Checkpoint per ulteriori informazioni su questo attraversamento ricorsivo.) Tuttavia, tutti gli attributi, le funzioni e i dati di Python vengono persi. Ciò significa che quando viene salvata una tf.function , non viene salvato alcun codice Python.

Se non viene salvato alcun codice Python, come fa SavedModel a sapere come ripristinare la funzione?

In breve, tf.function funziona tracciando il codice Python per generare una ConcreteFunction (un wrapper richiamabile attorno a tf.Graph ). Quando salvi un tf.function , stai davvero salvando la cache di ConcreteFunctions di tf.function .

Per saperne di più sulla relazione tra tf.function e ConcreteFunctions, consulta la guida tf.function .

module_no_signatures_path = os.path.join(tmpdir, 'module_no_signatures')
module(tf.constant(0.))
print('Saving model...')
tf.saved_model.save(module, module_no_signatures_path)
Tracing with Tensor("x:0", shape=(), dtype=float32)
Saving model...
Tracing with Tensor("x:0", shape=(), dtype=float32)
INFO:tensorflow:Assets written to: /tmp/tmpfcgkddlh/module_no_signatures/assets

Caricamento e utilizzo di un modello personalizzato

Quando carichi un SavedModel in Python, tutti tf.Variable attributi tf.Variable , i metodi tf.function -decorated e i tf.Module vengono ripristinati nella stessa struttura di oggetti del tf.Module salvato originale.

imported = tf.saved_model.load(module_no_signatures_path)
assert imported(tf.constant(3.)).numpy() == 3
imported.mutate(tf.constant(2.))
assert imported(tf.constant(3.)).numpy() == 6

Poiché non viene salvato alcun codice Python, la chiamata a tf.function con una nuova firma di input avrà esito negativo:

imported(tf.constant([3.]))
ValueError: Could not find matching function to call for canonicalized inputs ((,), {}). Only existing signatures are [((TensorSpec(shape=(), dtype=tf.float32, name=u'x'),), {})].

Messa a punto di base

Sono disponibili oggetti variabili ed è possibile eseguire il backprop tramite funzioni importate. Questo è sufficiente per mettere a punto (cioè riqualificare) un SavedModel in casi semplici.

optimizer = tf.optimizers.SGD(0.05)

def train_step():
  with tf.GradientTape() as tape:
    loss = (10. - imported(tf.constant(2.))) ** 2
  variables = tape.watched_variables()
  grads = tape.gradient(loss, variables)
  optimizer.apply_gradients(zip(grads, variables))
  return loss
for _ in range(10):
  # "v" approaches 5, "loss" approaches 0
  print("loss={:.2f} v={:.2f}".format(train_step(), imported.v.numpy()))
loss=36.00 v=3.20
loss=12.96 v=3.92
loss=4.67 v=4.35
loss=1.68 v=4.61
loss=0.60 v=4.77
loss=0.22 v=4.86
loss=0.08 v=4.92
loss=0.03 v=4.95
loss=0.01 v=4.97
loss=0.00 v=4.98

Messa a punto generale

Un SavedModel di Keras fornisce più dettagli di una semplice __call__ per affrontare casi più avanzati di messa a punto. TensorFlow Hub consiglia di fornire quanto segue, se applicabile, nei modelli salvati condivisi ai fini della messa a punto:

  • Se il modello utilizza il dropout o un'altra tecnica in cui il passaggio in avanti differisce tra addestramento e inferenza (come la normalizzazione batch), il metodo __call__ accetta un argomento training= facoltativo con valore Python che per impostazione predefinita è False ma può essere impostato su True .
  • Accanto all'attributo __call__ , ci sono .trainable_variable attributi .variable e .trainable_variable con i corrispondenti elenchi di variabili. Una variabile originariamente addestrabile ma destinata a essere congelata durante l'ottimizzazione viene omessa da .trainable_variables .
  • Per motivi di framework come Keras che rappresentano i regolarizzatori di peso come attributi di livelli o sottomodelli, può esserci anche un attributo .regularization_losses . Contiene un elenco di funzioni senza argomenti i cui valori sono pensati per l'aggiunta alla perdita totale.

Tornando all'esempio iniziale di MobileNet, puoi vederne alcuni in azione:

loaded = tf.saved_model.load(mobilenet_save_path)
print("MobileNet has {} trainable variables: {}, ...".format(
          len(loaded.trainable_variables),
          ", ".join([v.name for v in loaded.trainable_variables[:5]])))
MobileNet has 83 trainable variables: conv1/kernel:0, conv1_bn/gamma:0, conv1_bn/beta:0, conv_dw_1/depthwise_kernel:0, conv_dw_1_bn/gamma:0, ...
trainable_variable_ids = {id(v) for v in loaded.trainable_variables}
non_trainable_variables = [v for v in loaded.variables
                           if id(v) not in trainable_variable_ids]
print("MobileNet also has {} non-trainable variables: {}, ...".format(
          len(non_trainable_variables),
          ", ".join([v.name for v in non_trainable_variables[:3]])))
MobileNet also has 54 non-trainable variables: conv1_bn/moving_mean:0, conv1_bn/moving_variance:0, conv_dw_1_bn/moving_mean:0, ...

Specificare le firme durante l'esportazione

Strumenti come TensorFlow Serving e saved_model_cli possono interagire con SavedModels. Per aiutare questi strumenti a determinare quali ConcreteFunctions utilizzare, è necessario specificare le firme di pubblicazione. tf.keras.Model s specifica automaticamente le firme di pubblicazione, ma dovrai dichiarare esplicitamente una firma di pubblicazione per i nostri moduli personalizzati.

Per impostazione predefinita, nessuna firma viene dichiarata in un tf.Module personalizzato.

assert len(imported.signatures) == 0

Per dichiarare una firma di servizio, specificare una ConcreteFunction utilizzando le signatures kwarg. Quando si specifica una singola firma, la sua chiave di firma sarà 'serving_default' , che viene salvata come costante tf.saved_model.DEFAULT_SERVING_SIGNATURE_DEF_KEY .

module_with_signature_path = os.path.join(tmpdir, 'module_with_signature')
call = module.__call__.get_concrete_function(tf.TensorSpec(None, tf.float32))
tf.saved_model.save(module, module_with_signature_path, signatures=call)
Tracing with Tensor("x:0", dtype=float32)
Tracing with Tensor("x:0", dtype=float32)
INFO:tensorflow:Assets written to: /tmp/tmpfcgkddlh/module_with_signature/assets
imported_with_signatures = tf.saved_model.load(module_with_signature_path)
list(imported_with_signatures.signatures.keys())
['serving_default']

Per esportare più firme, passa un dizionario di chiavi di firma a ConcreteFunctions. Ogni chiave di firma corrisponde a una ConcreteFunction.

module_multiple_signatures_path = os.path.join(tmpdir, 'module_with_multiple_signatures')
signatures = {"serving_default": call,
              "array_input": module.__call__.get_concrete_function(tf.TensorSpec([None], tf.float32))}

tf.saved_model.save(module, module_multiple_signatures_path, signatures=signatures)
Tracing with Tensor("x:0", shape=(None,), dtype=float32)
Tracing with Tensor("x:0", shape=(None,), dtype=float32)
INFO:tensorflow:Assets written to: /tmp/tmpfcgkddlh/module_with_multiple_signatures/assets
imported_with_multiple_signatures = tf.saved_model.load(module_multiple_signatures_path)
list(imported_with_multiple_signatures.signatures.keys())
['serving_default', 'array_input']

Per impostazione predefinita, i nomi del tensore di output sono abbastanza generici, come output_0 . Per controllare i nomi degli output, modifica il tuo tf.function per restituire un dizionario che tf.function i nomi degli output agli output. I nomi degli input sono derivati ​​dai nomi degli argomenti della funzione Python.

class CustomModuleWithOutputName(tf.Module):
  def __init__(self):
    super(CustomModuleWithOutputName, self).__init__()
    self.v = tf.Variable(1.)

  @tf.function(input_signature=[tf.TensorSpec([], tf.float32)])
  def __call__(self, x):
    return {'custom_output_name': x * self.v}

module_output = CustomModuleWithOutputName()
call_output = module_output.__call__.get_concrete_function(tf.TensorSpec(None, tf.float32))
module_output_path = os.path.join(tmpdir, 'module_with_output_name')
tf.saved_model.save(module_output, module_output_path,
                    signatures={'serving_default': call_output})
INFO:tensorflow:Assets written to: /tmp/tmpfcgkddlh/module_with_output_name/assets
imported_with_output_name = tf.saved_model.load(module_output_path)
imported_with_output_name.signatures['serving_default'].structured_outputs
{'custom_output_name': TensorSpec(shape=(), dtype=tf.float32, name='custom_output_name')}

Carica un modello salvato in C++

La versione C++ del caricatore SavedModel fornisce un'API per caricare un SavedModel da un percorso, consentendo SessionOptions e RunOptions. Devi specificare i tag associati al grafico da caricare. La versione caricata di SavedModel è denominata SavedModelBundle e contiene MetaGraphDef e la sessione all'interno della quale è caricata.

const string export_dir = ...
SavedModelBundle bundle;
...
LoadSavedModel(session_options, run_options, export_dir, {kSavedModelTagTrain},
               &bundle);

Dettagli dell'interfaccia della riga di comando SavedModel

È possibile utilizzare l'interfaccia della riga di comando (CLI) SavedModel per ispezionare ed eseguire un SavedModel. Ad esempio, è possibile utilizzare la CLI per esaminare i SignatureDef del modello. La CLI consente di confermare rapidamente che il dtype e la forma del tensore di input corrispondano al modello. Inoltre, se desideri testare il tuo modello, puoi utilizzare la CLI per eseguire un controllo di integrità passando input di esempio in vari formati (ad esempio, espressioni Python) e quindi recuperare l'output.

Installa il SavedModel CLI

In generale, puoi installare TensorFlow in uno dei seguenti due modi:

  • Installando un binario TensorFlow precostruito.
  • Compilando TensorFlow dal codice sorgente.

Se hai installato TensorFlow tramite un binario TensorFlow precostruito, la CLI SavedModel è già installata sul tuo sistema in percorso bin/saved_model_cli .

Se hai creato TensorFlow dal codice sorgente, devi eseguire il seguente comando aggiuntivo per creare saved_model_cli :

$ bazel build tensorflow/python/tools:saved_model_cli

Panoramica dei comandi

La CLI SavedModel supporta i seguenti due comandi su un SavedModel:

  • show , che mostra i calcoli disponibili da un SavedModel.
  • run , che esegue un calcolo da un SavedModel.

show comando

Un SavedModel contiene una o più varianti di modello (tecnicamente, v1.MetaGraphDef s), identificate dai loro tag-set. Per servire un modello, potresti chiederti che tipo di SignatureDef sono in ogni variante di modello e quali sono i loro input e output. Il comando show consente di esaminare il contenuto del SavedModel in ordine gerarchico. Ecco la sintassi:

usage: saved_model_cli show [-h] --dir DIR [--all]
[--tag_set TAG_SET] [--signature_def SIGNATURE_DEF_KEY]

Ad esempio, il comando seguente mostra tutti i tag-set disponibili nel SavedModel:

$ saved_model_cli show --dir /tmp/saved_model_dir
The given SavedModel contains the following tag-sets:
serve
serve, gpu

Il comando seguente mostra tutte le chiavi SignatureDef disponibili per un set di tag:

$ saved_model_cli show --dir /tmp/saved_model_dir --tag_set serve
The given SavedModel `MetaGraphDef` contains `SignatureDefs` with the
following keys:
SignatureDef key: "classify_x2_to_y3"
SignatureDef key: "classify_x_to_y"
SignatureDef key: "regress_x2_to_y3"
SignatureDef key: "regress_x_to_y"
SignatureDef key: "regress_x_to_y2"
SignatureDef key: "serving_default"

Se sono presenti più tag nel set di tag, è necessario specificare tutti i tag, ciascuno separato da una virgola. Per esempio:

$ saved_model_cli show --dir /tmp/saved_model_dir --tag_set serve,gpu

Per mostrare tutti gli input e gli output TensorInfo per una SignatureDef specifica, passare la chiave SignatureDef all'opzione signature_def . Questo è molto utile quando si desidera conoscere il valore della chiave del tensore, il dtype e la forma dei tensori di input per eseguire successivamente il grafico di calcolo. Per esempio:

$ saved_model_cli show --dir \
/tmp/saved_model_dir --tag_set serve --signature_def serving_default
The given SavedModel SignatureDef contains the following input(s):
  inputs['x'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 1)
      name: x:0
The given SavedModel SignatureDef contains the following output(s):
  outputs['y'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 1)
      name: y:0
Method name is: tensorflow/serving/predict

Per mostrare tutte le informazioni disponibili in SavedModel, usa l'opzione --all . Per esempio:

$ saved_model_cli show --dir /tmp/saved_model_dir --all
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['classify_x2_to_y3']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['inputs'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1)
        name: x2:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['scores'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1)
        name: y3:0
  Method name is: tensorflow/serving/classify

...

signature_def['serving_default']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['x'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1)
        name: x:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['y'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1)
        name: y:0
  Method name is: tensorflow/serving/predict

run comando

Invocare il comando run per eseguire un calcolo grafico, passando gli input e quindi visualizzando (e facoltativamente salvando) gli output. Ecco la sintassi:

usage: saved_model_cli run [-h] --dir DIR --tag_set TAG_SET --signature_def
                           SIGNATURE_DEF_KEY [--inputs INPUTS]
                           [--input_exprs INPUT_EXPRS]
                           [--input_examples INPUT_EXAMPLES] [--outdir OUTDIR]
                           [--overwrite] [--tf_debug]

Il comando run fornisce i tre modi seguenti per passare gli input al modello:

  • --inputs opzione --inputs ti consente di passare numpy ndarray nei file.
  • --input_exprs opzione --input_exprs ti consente di passare espressioni Python.
  • --input_examples opzione --input_examples ti consente di passare tf.train.Example .

--inputs

Per passare i dati di input nei file, specifica l'opzione --inputs , che assume il seguente formato generale:

--inputs <INPUTS>

dove INPUTS è uno dei seguenti formati:

  • <input_key>=<filename>
  • <input_key>=<filename>[<variable_name>]

Puoi passare più INPUT . Se passi più input, usa un punto e virgola per separare ciascuno degli INPUT .

saved_model_cli usa numpy.load per caricare il nome del file . Il nome del file può essere in uno dei seguenti formati:

  • .npy
  • .npz
  • formato sottaceto

Un file .npy contiene sempre un numpy ndarray. Pertanto, durante il caricamento da un file .npy , il contenuto verrà assegnato direttamente al tensore di input specificato. Se si specifica un nome_variabile con quella .npy file, il nome_variabile verrà ignorato e verrà emesso un avviso.

Al termine del caricamento da un .npz del file (zip), si può opzionalmente specificare un nome_variabile per identificare la variabile all'interno del file zip da caricare per la chiave di ingresso tensore. Se non si specifica un nome_variabile, il SavedModel CLI controllerà che solo un file è incluso nel file zip e caricarlo per la chiave di ingresso tensore specificato.

Al termine del caricamento da un file salamoia, se non variable_name è specificato nelle parentesi quadre, qualunque essa sia all'interno del file salamoia verrà passato alla chiave di ingresso tensore specificato. Altrimenti, il SavedModel CLI assumerà un dizionario è memorizzato nel file salamoia e verrà utilizzato il valore corrispondente alla variable_name.

--input_exprs

Per passare gli input attraverso le espressioni Python, specifica l'opzione --input_exprs . Questo può essere utile quando non si dispone di file di dati in giro, ma si desidera comunque eseguire un controllo di integrità del modello con alcuni semplici input che corrispondano al dtype e alla forma dei SignatureDef del modello. Per esempio:

`<input_key>=[[1],[2],[3]]`

Oltre alle espressioni Python, puoi anche passare funzioni numpy. Per esempio:

`<input_key>=np.ones((32,32,3))`

(Nota che il modulo numpy è già disponibile come np .)

--input_examples

Per passare tf.train.Example come input, specifica l'opzione --input_examples . Per ogni chiave di input, è necessario un elenco di dizionari, dove ogni dizionario è un'istanza di tf.train.Example . Le chiavi del dizionario sono le caratteristiche ei valori sono le liste di valori per ciascuna caratteristica. Per esempio:

`<input_key>=[{"age":[22,24],"education":["BS","MS"]}]`

Salva output

Per impostazione predefinita, la CLI SavedModel scrive l'output su stdout. Se una directory viene passata all'opzione --outdir , gli output verranno salvati come file .npy denominati dopo le chiavi del tensore di output nella directory data.

Usa --overwrite per sovrascrivere i file di output esistenti.