From e7d896c9a6dcc4689f75f70ce56244e44442f280 Mon Sep 17 00:00:00 2001 From: "TF.Text Team" Date: Tue, 17 Dec 2024 13:55:32 -0800 Subject: [PATCH] Handle the punctuation definition mismatch between different Unicode versions. PiperOrigin-RevId: 707239296 --- tensorflow_text/core/kernels/BUILD | 3 + .../core/kernels/fast_wordpiece_tokenizer.cc | 20 ++++-- .../kernels/fast_wordpiece_tokenizer_test.cc | 67 ++++++++++++++++++ ...fast_wordpiece_tokenizer_model_ver_15_1.fb | Bin 0 -> 354252 bytes ...fast_wordpiece_tokenizer_model_ver_16_0.fb | Bin 0 -> 354304 bytes 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tensorflow_text/python/ops/test_data/fast_wordpiece_tokenizer_model_ver_15_1.fb create mode 100644 tensorflow_text/python/ops/test_data/fast_wordpiece_tokenizer_model_ver_16_0.fb diff --git a/tensorflow_text/core/kernels/BUILD b/tensorflow_text/core/kernels/BUILD index 3fb90294f..dc113c56d 100644 --- a/tensorflow_text/core/kernels/BUILD +++ b/tensorflow_text/core/kernels/BUILD @@ -403,12 +403,15 @@ cc_test( srcs = ["fast_wordpiece_tokenizer_test.cc"], data = [ "//tensorflow_text:python/ops/test_data/fast_wordpiece_tokenizer_model.fb", + "//tensorflow_text:python/ops/test_data/fast_wordpiece_tokenizer_model_ver_15_1.fb", + "//tensorflow_text:python/ops/test_data/fast_wordpiece_tokenizer_model_ver_16_0.fb", ], deps = [ ":fast_wordpiece_tokenizer", ":fast_wordpiece_tokenizer_model_builder", "@com_google_googletest//:gtest_main", "@com_google_absl//absl/flags:flag", + "//third_party/icu:headers", # tf:lib tensorflow dep, ], ) diff --git a/tensorflow_text/core/kernels/fast_wordpiece_tokenizer.cc b/tensorflow_text/core/kernels/fast_wordpiece_tokenizer.cc index 8268566ea..c55aa9265 100644 --- a/tensorflow_text/core/kernels/fast_wordpiece_tokenizer.cc +++ b/tensorflow_text/core/kernels/fast_wordpiece_tokenizer.cc @@ -278,14 +278,24 @@ void FastWordpieceTokenizer::TokenizeTextImpl( prev_unicode_char))) { // If the current Unicode character is a valid word boundary, collect the // remaining tokens stored on a path on the trie. + absl::string_view cur_str = absl::string_view( + input_substr.data(), cur_pos - input_word_offset_in_text); HandleTheRemainingStringOnTriePath( - absl::string_view(input_substr.data(), - cur_pos - input_word_offset_in_text), - input_word_offset_in_text, cur_node, original_num_tokens, + cur_str, input_word_offset_in_text, cur_node, original_num_tokens, cur_offset_in_input_word, output_pieces, output_ids, output_start_offsets, output_end_offsets); - // Skip the whitespace. - if (is_white_space) cur_pos = next_pos; + if (is_white_space) { + // Skip the whitespace. + cur_pos = next_pos; + } else if (cur_str.empty()) { + // If the remaining tokens are empty, it means we encountered an + // unmappable separator, so output an unknown token and continue. + cur_pos = next_pos; + ResetOutputAppendUnknownToken( + input_word_offset_in_text, (cur_pos - input_word_offset_in_text), + original_num_tokens, output_pieces, output_ids, + output_start_offsets, output_end_offsets); + } // Continue in the outer while loop to process the remaining input. continue; } diff --git a/tensorflow_text/core/kernels/fast_wordpiece_tokenizer_test.cc b/tensorflow_text/core/kernels/fast_wordpiece_tokenizer_test.cc index 8bfbb12e1..49a721eb8 100644 --- a/tensorflow_text/core/kernels/fast_wordpiece_tokenizer_test.cc +++ b/tensorflow_text/core/kernels/fast_wordpiece_tokenizer_test.cc @@ -17,6 +17,7 @@ #include #include #include "absl/flags/flag.h" +#include "icu4c/source/common/unicode/uchar.h" #include "tensorflow/core/platform/env.h" #include "tensorflow_text/core/kernels/fast_wordpiece_tokenizer_model_builder.h" @@ -24,6 +25,7 @@ namespace tensorflow { namespace text { namespace { +using ::testing::AnyOf; using ::testing::ElementsAre; constexpr char kTestConfigPath[] = @@ -58,6 +60,71 @@ TEST(FastWordpieceTokenizerTest, LoadAndTokenize) { EXPECT_THAT(output_end_offsets, ElementsAre(3, 5, 6, 9)); } +using TestPunctuationVersionMismatch = testing::TestWithParam; + +TEST_P(TestPunctuationVersionMismatch, Test) { + // The config_flatbuffer used here is built from the following config: + // * vocab = {"a", "abc", "abcdefghi", "##de", "##defgxy", "##deh", "##f", + // "##ghz", ""} + // * unk_token = "" + // * suffix_indicator = "##" + // * max_bytes_per_token = 100 + // * end_to_end = True + + const std::string kTestConfigUnicodePath = GetParam(); + + // We test the new punctuation symbol: \341\255\277, which was available in + // Unicode 16: https://www.fileformat.info/info/unicode/char//1b7f/index.htm, + // but not in 15.1. + // We also test an existing punctuation symbol ">". + std::string input = "abc>abc\341\255\277abc"; + + std::string config_flatbuffer; + auto status = tensorflow::ReadFileToString( + tensorflow::Env::Default(), kTestConfigUnicodePath, &config_flatbuffer); + ASSERT_TRUE(status.ok()); + + ASSERT_OK_AND_ASSIGN( + auto tokenizer, FastWordpieceTokenizer::Create(config_flatbuffer.data())); + + std::vector output_tokens; + std::vector output_ids; + std::vector output_start_offsets; + std::vector output_end_offsets; + tokenizer.Tokenize(input, &output_tokens, &output_ids, &output_start_offsets, + &output_end_offsets); + + // If the runtime environment has unicode <=15.1, "\341\255\277" is not a + // punctuation, so "abc\341\255\277abc" is one token. + // If the runtime environment has unicode >=16.0, "\341\255\277" is a + // punctuation, so tokens are "abc", "", "abc" + EXPECT_THAT(output_tokens.size(), AnyOf(3, 5)); + if (!u_ispunct(0x1b7f)) { + // We have a runtime environment of unicode <= 15.1. + EXPECT_THAT(output_tokens, ElementsAre("abc", "", "")); + EXPECT_THAT(output_ids, ElementsAre(1, 8, 8)); + EXPECT_THAT(output_start_offsets, ElementsAre(0, 3, 4)); + EXPECT_THAT(output_end_offsets, ElementsAre(3, 4, 13)); + } else { + // We have a runtime environment of unicode >= 16.0. + EXPECT_THAT(output_tokens, + ElementsAre("abc", "", "abc", "", "abc")); + EXPECT_THAT(output_ids, ElementsAre(1, 8, 1, 8, 1)); + EXPECT_THAT(output_start_offsets, ElementsAre(0, 3, 4, 7, 10)); + EXPECT_THAT(output_end_offsets, ElementsAre(3, 4, 7, 10, 13)); + } +} + +INSTANTIATE_TEST_SUITE_P(FastWordpieceTokenizerPunctuationTest, + TestPunctuationVersionMismatch, + testing::Values( + // Unicode v 15.1 config + "third_party/tensorflow_text/python/ops/test_data/" + "fast_wordpiece_tokenizer_model_ver_15_1.fb", + // Unicode v 16.0 config + "third_party/tensorflow_text/python/ops/test_data/" + "fast_wordpiece_tokenizer_model_ver_16_0.fb")); + template std::string ListToString(const std::vector& list) { return absl::StrCat("[", absl::StrJoin(list, ", "), "]"); diff --git a/tensorflow_text/python/ops/test_data/fast_wordpiece_tokenizer_model_ver_15_1.fb b/tensorflow_text/python/ops/test_data/fast_wordpiece_tokenizer_model_ver_15_1.fb new file mode 100644 index 0000000000000000000000000000000000000000..72b0a1019a9823182b54c61663ff093cffc8cecd GIT binary patch literal 354252 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

PKHOHVc4?b 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 literal 0 HcmV?d00001 diff --git a/tensorflow_text/python/ops/test_data/fast_wordpiece_tokenizer_model_ver_16_0.fb b/tensorflow_text/python/ops/test_data/fast_wordpiece_tokenizer_model_ver_16_0.fb new file mode 100644 index 0000000000000000000000000000000000000000..770fb0152c47a837ebbce80b635e675c0b3a2259 GIT binary patch literal 354304 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 Z5r0Cwg1C(MGvY6ZR}ohbe?|Na@!zmI?&1Id literal 0 HcmV?d00001