Wspólne interfejsy API SavedModel dla zadań tekstowych

Na tej stronie opisano, w jaki sposób TF2 SavedModels do zadań związanych z tekstem powinien implementować interfejs API SavedModel wielokrotnego użytku . (Zastępuje to i rozszerza wspólne podpisy tekstowe dla obecnie przestarzałego formatu TF1 Hub .)

Przegląd

Istnieje kilka interfejsów API do obliczania osadzania tekstu (znanych również jako gęste reprezentacje tekstu lub wektory cech tekstu).

  • Interfejs API do osadzania tekstu z danych wejściowych jest implementowany przez SavedModel, który mapuje partię ciągów na partię wektorów osadzania. Jest to bardzo łatwe w użyciu i wiele modeli w TF Hub je zaimplementowało. Nie pozwala to jednak na dostrojenie modelu na TPU.

  • Interfejs API do osadzania tekstu z wstępnie przetworzonymi danymi wejściowymi rozwiązuje to samo zadanie, ale jest implementowany przez dwa osobne modele SavedModels:

    • preprocesor , który może działać wewnątrz potoku wejściowego tf.data i konwertuje ciągi znaków i inne dane o zmiennej długości na tensory numeryczne,
    • koder , który akceptuje wyniki preprocesora i wykonuje możliwą do wyszkolenia część obliczeń osadzania.

    Podział ten umożliwia asynchroniczne wstępne przetwarzanie danych wejściowych przed wprowadzeniem ich do pętli szkoleniowej. W szczególności umożliwia budowanie koderów, które można uruchamiać i dostrajać na TPU .

  • Interfejs API do osadzania tekstu w koderach Transformer rozszerza interfejs API do osadzania tekstu z wstępnie przetworzonych danych wejściowych na konkretny przypadek BERT i innych koderów Transformer.

    • Preprocesor został rozszerzony w celu tworzenia danych wejściowych kodera z więcej niż jednego segmentu tekstu wejściowego.
    • Koder Transformer udostępnia kontekstowe osadzanie poszczególnych tokenów.

W każdym przypadku tekstem wejściowym są ciągi znaków zakodowane w formacie UTF-8, zazwyczaj zwykły tekst, chyba że dokumentacja modelu stanowi inaczej.

Niezależnie od interfejsu API różne modele zostały wstępnie przeszkolone na tekstach z różnych języków i domen oraz z myślą o różnych zadaniach. Dlatego nie każdy model osadzania tekstu jest odpowiedni dla każdego problemu.

Osadzanie tekstu z wprowadzonego tekstu

SavedModel do osadzania tekstu z wejść tekstowych akceptuje partię danych wejściowych w tensorze ciągu o kształcie [batch_size] i odwzorowuje je na tensor float32 o kształcie [batch_size, dim] z gęstymi reprezentacjami (wektorami cech) wejść.

Podsumowanie użycia

obj = hub.load("path/to/model")
text_input = ["A long sentence.",
              "single-word",
              "http://example.com"]
embeddings = obj(text_input)

Przypomnij sobie z interfejsu API SavedModel wielokrotnego użytku , że uruchomienie modelu w trybie uczenia (np. w przypadku porzucenia) może wymagać argumentu słowa kluczowego obj(..., training=True) i że obj udostępnia atrybuty .variables , .trainable_variables i .regularization_losses , stosownie do przypadku .

W Keras tym wszystkim zajmuje się

embeddings = hub.KerasLayer("path/to/model", trainable=...)(text_input)

Szkolenia rozproszone

Jeśli osadzanie tekstu jest używane jako część modelu szkolonego w ramach strategii dystrybucji, wywołanie metody hub.load("path/to/model") lub hub.KerasLayer("path/to/model", ...) , odpowiednio, musi nastąpić w zakresie DistributionStrategy, aby utworzyć zmienne modelu w sposób rozproszony. Na przykład

  with strategy.scope():
    ...
    model = hub.load("path/to/model")
    ...

Przykłady

Osadzanie tekstu z wstępnie przetworzonymi danymi wejściowymi

Osadzanie tekstu z wstępnie przetworzonymi danymi wejściowymi jest realizowane przez dwa oddzielne SavedModels:

  • preprocesor , który odwzorowuje tensor ciągu o kształcie [batch_size] na dyktando tensorów numerycznych,
  • koder , który akceptuje dykt tensorów zwrócony przez preprocesor, wykonuje możliwą do wyszkolenia część obliczeń osadzania i zwraca dyktando wyników. Dane wyjściowe pod kluczem "default" to tensor float32 o kształcie [batch_size, dim] .

Umożliwia to uruchomienie preprocesora w potoku wejściowym, ale dostrojenie osadzania obliczonego przez koder w ramach większego modelu. W szczególności pozwala budować kodery, które można uruchamiać i dostrajać na TPU .

Jest to szczegół implementacji, które Tensory są zawarte na wyjściu preprocesora i które (jeśli w ogóle) dodatkowe Tensory poza "default" są zawarte na wyjściu kodera.

Dokumentacja kodera musi określać, jakiego preprocesora z nim używać. Zazwyczaj istnieje dokładnie jeden prawidłowy wybór.

Podsumowanie użycia

text_input = tf.constant(["A long sentence.",
                          "single-word",
                          "http://example.com"])
preprocessor = hub.load("path/to/preprocessor")  # Must match `encoder`.
encoder_inputs = preprocessor(text_input)

encoder = hub.load("path/to/encoder")
encoder_outputs = encoder(encoder_inputs)
embeddings = encoder_outputs["default"]

Przypomnij sobie z interfejsu API SavedModel wielokrotnego użytku , że uruchomienie kodera w trybie uczenia (np. w przypadku porzucenia) może wymagać argumentu słowa kluczowego encoder(..., training=True) i że encoder udostępnia atrybuty .variables , .trainable_variables i .regularization_losses , stosownie do przypadku .

Model preprocessor może mieć .variables , ale nie jest przeznaczony do dalszego uczenia. Przetwarzanie wstępne nie jest zależne od trybu: jeśli preprocessor() w ogóle ma argument training=... , nie ma to żadnego efektu.

W Keras tym wszystkim zajmuje się

encoder_inputs = hub.KerasLayer("path/to/preprocessor")(text_input)
encoder_outputs = hub.KerasLayer("path/to/encoder", trainable=True)(encoder_inputs)
embeddings = encoder_outputs["default"]

Szkolenia rozproszone

Jeśli koder jest używany jako część modelu szkolonego w ramach strategii dystrybucji, wywołanie metody hub.load("path/to/encoder") lub hub.KerasLayer("path/to/encoder", ...) odpowiednio, musi wydarzyć się wewnątrz

  with strategy.scope():
    ...

w celu odtworzenia zmiennych enkodera w sposób rozproszony.

Podobnie, jeśli preprocesor jest częścią wyszkolonego modelu (jak w prostym przykładzie powyżej), należy go również załadować w zakresie strategii dystrybucji. Jeśli jednak preprocesor jest używany w potoku wejściowym (np. w wywołaniu przekazywanym do tf.data.Dataset.map() ), jego ładowanie musi odbywać się poza zakresem strategii dystrybucji, aby umieścić jego zmienne (jeśli takie istnieją) ) na procesorze hosta.

Przykłady

Osadzanie tekstu za pomocą enkoderów Transformer

Transformatorowe kodery tekstu działają na partii sekwencji wejściowych, przy czym każda sekwencja zawiera n ≥ 1 segmentów tekstu tokenizowanego, w ramach pewnego specyficznego dla modelu ograniczenia na n . W przypadku BERT i wielu jego rozszerzeń granica ta wynosi 2, więc akceptowane są pojedyncze segmenty i pary segmentów.

Interfejs API do osadzania tekstu w koderach Transformer rozszerza interfejs API do osadzania tekstu o wstępnie przetworzone dane wejściowe do tego ustawienia.

Preprocesor

Preprocesor SavedModel do osadzania tekstu w koderach Transformer implementuje interfejs API preprocesora SavedModel do osadzania tekstu z wstępnie przetworzonymi danymi wejściowymi (patrz wyżej), który umożliwia mapowanie jednosegmentowych wejść tekstowych bezpośrednio na wejścia kodera.

Ponadto preprocesor SavedModel zapewnia wywoływalne podobiekty tokenize do tokenizacji (oddzielnie na segment) i bert_pack_inputs do pakowania n tokenizowanych segmentów w jedną sekwencję wejściową dla kodera. Każdy podobiekt jest zgodny z interfejsem API SavedModel wielokrotnego użytku .

Podsumowanie użycia

Jako konkretny przykład dwóch segmentów tekstu przyjrzyjmy się zadaniu polegającemu na wyciąganiu zdań, które zadaje pytanie, czy przesłanka (pierwszy segment) implikuje hipotezę, czy też nie (drugi segment).

preprocessor = hub.load("path/to/preprocessor")

# Tokenize batches of both text inputs.
text_premises = tf.constant(["The quick brown fox jumped over the lazy dog.",
                             "Good day."])
tokenized_premises = preprocessor.tokenize(text_premises)
text_hypotheses = tf.constant(["The dog was lazy.",  # Implied.
                               "Axe handle!"])       # Not implied.
tokenized_hypotheses = preprocessor.tokenize(text_hypotheses)

# Pack input sequences for the Transformer encoder.
seq_length = 128
encoder_inputs = preprocessor.bert_pack_inputs(
    [tokenized_premises, tokenized_hypotheses],
    seq_length=seq_length)  # Optional argument.

W Kerasie obliczenie to można wyrazić jako

tokenize = hub.KerasLayer(preprocessor.tokenize)
tokenized_hypotheses = tokenize(text_hypotheses)
tokenized_premises = tokenize(text_premises)

bert_pack_inputs = hub.KerasLayer(
    preprocessor.bert_pack_inputs,
    arguments=dict(seq_length=seq_length))  # Optional argument.
encoder_inputs = bert_pack_inputs([tokenized_premises, tokenized_hypotheses])

Szczegóły tokenize

Wywołanie preprocessor.tokenize() akceptuje Tensor ciągu znaków o kształcie [batch_size] i zwraca Tensor RaggedTensor o kształcie [batch_size, ...] , którego wartościami są identyfikatory tokenów int32 reprezentujące ciągi wejściowe. Po batch_size może występować r ≥ 1 nierównych wymiarów, ale nie może być żadnego innego jednolitego wymiaru.

  • Jeśli r =1, kształt to [batch_size, (tokens)] , a każde dane wejściowe są po prostu tokenizowane w płaską sekwencję tokenów.
  • Jeżeli r >1, istnieją r -1 dodatkowe poziomy grupowania. Na przykład tensorflow_text.BertTokenizer używa r =2 do grupowania tokenów według słów i daje kształt [batch_size, (words), (tokens_per_word)] . To, ile takich dodatkowych poziomów istnieje, jeśli w ogóle, i jakie grupy reprezentują, zależy od danego modelu.

Użytkownik może (ale nie musi) modyfikować tokenizowanych wejść, np. aby dostosować się do limitu seq_length, który będzie wymuszany przy pakowaniu wejść kodera. Dodatkowe wymiary w wynikach tokenizera mogą być tutaj pomocne (np. przestrzeganie granic słów), ale w następnym kroku staną się bez znaczenia.

Jeśli chodzi o interfejs API SavedModel wielokrotnego użytku , obiekt preprocessor.tokenize może mieć .variables , ale nie jest przeznaczony do dalszego szkolenia. Tokenizacja nie jest zależna od trybu: jeśli preprocessor.tokenize() w ogóle ma argument training=... , nie ma to żadnego efektu.

Szczegóły bert_pack_inputs

Wywołanie funkcji preprocessor.bert_pack_inputs() akceptuje listę tokenizowanych danych wejściowych w języku Python (pogrupowanych oddzielnie dla każdego segmentu wejściowego) i zwraca dykt Tensorów reprezentujący partię sekwencji wejściowych o stałej długości dla modelu kodera Transformera.

Każde tokenizowane wejście jest int32 RaggedTensor kształtu [batch_size, ...] , gdzie liczba r nierównych wymiarów po Batch_size wynosi 1 lub jest taka sama jak na wyjściu preprocessor.tokenize(). (To ostatnie służy wyłącznie wygodzie; dodatkowe wymiary są spłaszczane przed pakowaniem.)

Pakowanie dodaje specjalne tokeny wokół segmentów wejściowych zgodnie z oczekiwaniami kodera. Wywołanie bert_pack_inputs() implementuje dokładnie schemat pakowania używany w oryginalnych modelach BERT i wielu ich rozszerzeniach: spakowana sekwencja zaczyna się od jednego tokena początku sekwencji, po którym następują tokenizowane segmenty, każdy zakończony jednym końcem segmentu znak. Pozostałe pozycje do seq_length, jeśli istnieją, są wypełniane tokenami dopełniającymi.

Jeśli spakowana sekwencja przekroczyłaby długość seq_length, bert_pack_inputs() obcina jej segmenty do przedrostków o w przybliżeniu równych rozmiarach, tak aby spakowana sekwencja mieściła się dokładnie w obrębie seq_length.

Pakowanie nie jest zależne od trybu: jeśli preprocessor.bert_pack_inputs() w ogóle ma argument training=... , nie ma to żadnego efektu. Ponadto nie oczekuje się, że preprocessor.bert_pack_inputs będzie zawierał zmienne ani wspierał dostrajania.

Koder

Koder jest wywoływany na podstawie dyktatu encoder_inputs w taki sam sposób, jak w API do osadzania tekstu z wstępnie przetworzonymi danymi wejściowymi (patrz wyżej), uwzględniając postanowienia z API Reusable SavedModel .

Podsumowanie użycia

encoder = hub.load("path/to/encoder")
encoder_outputs = encoder(encoder_inputs)

lub równoważnie w Keras:

encoder = hub.KerasLayer("path/to/encoder", trainable=True)
encoder_outputs = encoder(encoder_inputs)

Detale

encoder_outputs są dyktowane przez Tensory z następującymi kluczami.

  • "sequence_output" : tensor typu float32 o kształcie [batch_size, seq_length, dim] z kontekstowym osadzaniem każdego tokena każdej spakowanej sekwencji wejściowej.
  • "pooled_output" : tensor typu float32 o kształcie [batch_size, dim] z osadzeniem każdej sekwencji wejściowej jako całości, wyprowadzony z sekwencji_wyjściowej w jakiś możliwy do wytrenowania sposób.
  • "default" , zgodnie z wymaganiami API dla osadzania tekstu z wstępnie przetworzonymi danymi wejściowymi: tensor float32 o kształcie [batch_size, dim] z osadzeniem każdej sekwencji wejściowej. (Może to być po prostu alias Pooled_output.)

Zawartość wejść encoder_inputs nie jest ściśle wymagana przez tę definicję API. Jednakże w przypadku koderów korzystających z wejść w stylu BERT zaleca się użycie następujących nazw (z zestawu narzędzi do modelowania NLP w TensorFlow Model Garden ), aby zminimalizować tarcia podczas wymiany koderów i ponownego wykorzystania modeli preprocesora:

  • "input_word_ids" : tensor int32 kształtu [batch_size, seq_length] z identyfikatorami tokenów spakowanej sekwencji wejściowej (tj. włączając token początku sekwencji, tokeny końca segmentu i dopełnienie).
  • "input_mask" : tensor int32 o kształcie [batch_size, seq_length] o wartości 1 w pozycji wszystkich tokenów wejściowych obecnych przed dopełnieniem i wartością 0 dla tokenów dopełnienia.
  • "input_type_ids" : tensor int32 o kształcie [batch_size, seq_length] z indeksem segmentu wejściowego, który dał początek tokenowi wejściowemu w odpowiedniej pozycji. Pierwszy segment wejściowy (indeks 0) zawiera żeton początku sekwencji i jego żeton końca segmentu. Drugi i kolejne segmenty (jeśli są obecne) zawierają odpowiedni token końca segmentu. Tokeny dopełniające ponownie uzyskują indeks 0.

Szkolenia rozproszone

W przypadku ładowania obiektów preprocesora i kodera do zakresu strategii dystrybucji lub poza nią obowiązują te same zasady, co w interfejsie API w przypadku osadzania tekstu z wstępnie przetworzonymi danymi wejściowymi (patrz wyżej).

Przykłady