Юникод-строки

Смотрите на TensorFlow.org Запустите в Google Colab Изучайте код на GitHub Скачайте ноутбук

Введение

Модели обрабатывающие естественные языки, часто имеют дело с разными языками и разными наборами символов. * Unicode * - это стандартная система кодирования, которая используется для представления символов практически всех языков. Каждый символ кодируется с использованием уникального целого числа кодовой точки между 0 и0x10FFFF. Юникод-строка - это последовательность из нуля или более таких кодовых точек.

Это руководство показывает как представлять юникод-строки в Tensorflow и манипулировать ими используя юникодовские эквиваленты стандартной строковой. Она выделяет юникод-строки в токены на основе обнаружения скрипта.

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf

Тип данных tf.string

Базовый TensorFlow tf.string dtype позволяет вам строить тензоры байт-строк. Юникод-строки по умолчанию в кодировке utf-8.

tf.constant(u"Спасибо 😊")
<tf.Tensor: id=0, shape=(), dtype=string, numpy=b'\xd0\xa1\xd0\xbf\xd0\xb0\xd1\x81\xd0\xb8\xd0\xb1\xd0\xbe \xf0\x9f\x98\x8a'>

Тензор tf.string может содержать байт-строки различной длины поскольку байт-строки обрабатываются как отдельные единицы. Длина строки не включена в размерность тензора.

tf.constant([u"Добро", u"пожаловать!"]).shape
TensorShape([2])

Замечание: При использовании python при конструировании строк обработка юникода отличается между v2 и v3. В v2, юникод-строки отмечены префиксом "u", как и выше. В v3, строки закодированы в юникоде по умолчанию.

Представление Юникода

Есть два стандартных способа представления юникод-строк в TensorFlow:

  • string скаляр — где последовательность кодовых точек закодирована с использованием набора символов.
  • int32 вектор — где каждая позиция содержит единственную кодовую точку.

Например, следующие три значения все представляют юникод-строку "语言处理" (что значит "обработка языка" на китайском):

# Юникод-строки, представленные как UTF-8 закодированные строки скаляры.
text_utf8 = tf.constant(u"语言处理")
text_utf8
<tf.Tensor: id=2, shape=(), dtype=string, numpy=b'\xe8\xaf\xad\xe8\xa8\x80\xe5\xa4\x84\xe7\x90\x86'>
# Юникод-строки представленные как UTF-16-BE закодированные строки скаляры.
text_utf16be = tf.constant(u"语言处理".encode("UTF-16-BE"))
text_utf16be
<tf.Tensor: id=3, shape=(), dtype=string, numpy=b'\x8b\xed\x8a\x00Y\x04t\x06'>
# Юникод строки представленные как векторы юникодовских кодовых точек.
text_chars = tf.constant([ord(char) for char in u"语言处理"])
text_chars
<tf.Tensor: id=4, shape=(4,), dtype=int32, numpy=array([35821, 35328, 22788, 29702], dtype=int32)>

Конвертация между представлениями

TensorFlow предоставляет операции для конвертации между этими различными представлениями:

  • tf.strings.unicode_decode: Конвертирует закодированную строку скаляр в вектор кодовых точек.
  • tf.strings.unicode_encode: Конвертирует вектор кодовых точек в закодированную строку скаляр.
  • tf.strings.unicode_transcode: Конвертирует строку скаляр в другую кодировку.
tf.strings.unicode_decode(text_utf8,
                          input_encoding='UTF-8')
<tf.Tensor: id=8, shape=(4,), dtype=int32, numpy=array([35821, 35328, 22788, 29702], dtype=int32)>
tf.strings.unicode_encode(text_chars,
                          output_encoding='UTF-8')
<tf.Tensor: id=18, shape=(), dtype=string, numpy=b'\xe8\xaf\xad\xe8\xa8\x80\xe5\xa4\x84\xe7\x90\x86'>
tf.strings.unicode_transcode(text_utf8,
                             input_encoding='UTF8',
                             output_encoding='UTF-16-BE')
<tf.Tensor: id=19, shape=(), dtype=string, numpy=b'\x8b\xed\x8a\x00Y\x04t\x06'>

Размерности пакета

При декодировании нескольких строк размер символов в каждой строке может не совпадать, Возвращаемый результат это tf.RaggedTensor, где длина самого внутреннего измерения меняется в зависимости от количества символов в каждой строке:

# Пакет юнокод-строк каждая из которых представлена в виде строки в юникод-кодировке.
batch_utf8 = [s.encode('UTF-8') for s in
              [u'hÃllo',  u'What is the weather tomorrow',  u'Göödnight', u'😊']]
batch_chars_ragged = tf.strings.unicode_decode(batch_utf8,
                                               input_encoding='UTF-8')
for sentence_chars in batch_chars_ragged.to_list():
  print(sentence_chars)
[104, 195, 108, 108, 111]
[87, 104, 97, 116, 32, 105, 115, 32, 116, 104, 101, 32, 119, 101, 97, 116, 104, 101, 114, 32, 116, 111, 109, 111, 114, 114, 111, 119]
[71, 246, 246, 100, 110, 105, 103, 104, 116]
[128522]

Вы можете использовать tf.RaggedTensor напрямую, или конвертировать его в плотный tf.Tensor с паддингом или в tf.SparseTensor используя методы tf.RaggedTensor.to_tensor и tf.RaggedTensor.to_sparse.

batch_chars_padded = batch_chars_ragged.to_tensor(default_value=-1)
print(batch_chars_padded.numpy())
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.5/site-packages/tensorflow_core/python/ops/ragged/ragged_tensor.py:1586: where (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.where in 2.0, which has the same broadcast rule as np.where
[[   104    195    108    108    111     -1     -1     -1     -1     -1
      -1     -1     -1     -1     -1     -1     -1     -1     -1     -1
      -1     -1     -1     -1     -1     -1     -1     -1]
 [    87    104     97    116     32    105    115     32    116    104
     101     32    119    101     97    116    104    101    114     32
     116    111    109    111    114    114    111    119]
 [    71    246    246    100    110    105    103    104    116     -1
      -1     -1     -1     -1     -1     -1     -1     -1     -1     -1
      -1     -1     -1     -1     -1     -1     -1     -1]
 [128522     -1     -1     -1     -1     -1     -1     -1     -1     -1
      -1     -1     -1     -1     -1     -1     -1     -1     -1     -1
      -1     -1     -1     -1     -1     -1     -1     -1]]
batch_chars_sparse = batch_chars_ragged.to_sparse()

При кодировании нескольких строк одинаковой длины tf.Tensor может быть использован в качестве входных данных:

tf.strings.unicode_encode([[99, 97, 116], [100, 111, 103], [ 99, 111, 119]],
                          output_encoding='UTF-8')
<tf.Tensor: id=119, shape=(3,), dtype=string, numpy=array([b'cat', b'dog', b'cow'], dtype=object)>

При кодировании нескольких строк различной длины нужно использовать tf.RaggedTensor в качестве входных данных:

tf.strings.unicode_encode(batch_chars_ragged, output_encoding='UTF-8')
<tf.Tensor: id=120, shape=(4,), dtype=string, numpy=
array([b'h\xc3\x83llo', b'What is the weather tomorrow',
       b'G\xc3\xb6\xc3\xb6dnight', b'\xf0\x9f\x98\x8a'], dtype=object)>

Если у вас тензор с несколькими строками с паддингом или в разреженном формате, то ковертируйте его в tf.RaggedTensor перед вызовом unicode_encode:

tf.strings.unicode_encode(
    tf.RaggedTensor.from_sparse(batch_chars_sparse),
    output_encoding='UTF-8')
<tf.Tensor: id=199, shape=(4,), dtype=string, numpy=
array([b'h\xc3\x83llo', b'What is the weather tomorrow',
       b'G\xc3\xb6\xc3\xb6dnight', b'\xf0\x9f\x98\x8a'], dtype=object)>
tf.strings.unicode_encode(
    tf.RaggedTensor.from_tensor(batch_chars_padded, padding=-1),
    output_encoding='UTF-8')
<tf.Tensor: id=272, shape=(4,), dtype=string, numpy=
array([b'h\xc3\x83llo', b'What is the weather tomorrow',
       b'G\xc3\xb6\xc3\xb6dnight', b'\xf0\x9f\x98\x8a'], dtype=object)>

Операции юникода

Длина символа

Операция tf.strings.length имеет параметр unit, который показывает как должна быть посчитана длина. По умолчанию размер unit равен "BYTE", но он может быть установлен с другим значением, таким как "UTF8_CHAR" или "UTF16_CHAR", чтобы определить число кодовых точек в каждой закодированой string.

# Заметьте что последний символ занимает до 4 байтов в UTF8.
thanks = u'Thanks 😊'.encode('UTF-8')
num_bytes = tf.strings.length(thanks).numpy()
num_chars = tf.strings.length(thanks, unit='UTF8_CHAR').numpy()
print('{} bytes; {} UTF-8 characters'.format(num_bytes, num_chars))
11 bytes; 8 UTF-8 characters

Подстроки символов

Аналогично у операции tf.strings.substr есть параметр "unit", который используется для определения смещений параметров "pos" и "len".

# по умолчанию: unit='BYTE'. С len=1, мы возвращаем один байт.
tf.strings.substr(thanks, pos=7, len=1).numpy()
b'\xf0'
# Установив unit='UTF8_CHAR', мы возвратим один символ, размер которого в этом случае
#  4 байта.
print(tf.strings.substr(thanks, pos=7, len=1, unit='UTF8_CHAR').numpy())
b'\xf0\x9f\x98\x8a'

Разбиение юникод-строки

Операция tf.strings.unicode_split разбивает юникод-строки в подстроки отдельных символов:

tf.strings.unicode_split(thanks, 'UTF-8').numpy()
array([b'T', b'h', b'a', b'n', b'k', b's', b' ', b'\xf0\x9f\x98\x8a'],
      dtype=object)

Смещения байтов для символов

Чтобы выровнять тензор символа порожденный tf.strings.unicode_decode с оригинальной строкой, полезно знать смещение начала каждого символа. Метод tf.strings.unicode_decode_with_offsets аналогичен unicode_decode, за исключением того, что он возвращает второй тензор содержащий размер отступа от начала для каждого символа.

codepoints, offsets = tf.strings.unicode_decode_with_offsets(u"🎈🎉🎊", 'UTF-8')

for (codepoint, offset) in zip(codepoints.numpy(), offsets.numpy()):
  print("At byte offset {}: codepoint {}".format(offset, codepoint))
At byte offset 0: codepoint 127880
At byte offset 4: codepoint 127881
At byte offset 8: codepoint 127882

Юникод скрипты

Каждая кодовая точка принадлежит коллекции символов известной как система письма . Система письма полезна для определения того, какому языку может принадлежать символ. Например, зная что 'Б' из кириллицы указывает на то, что современный текст содержащий этот символ скорее всего из славянского языка, такого как русский или украинский.

В TensorFlow есть операция tf.strings.unicode_script для определения какой системе письма принадлежит данная кодовая точка. Коды систем письма это int32 числа соответствующие Международным компонентам для юникода (ICU) UScriptCode значения.

uscript = tf.strings.unicode_script([33464, 1041])  # ['芸', 'Б']

print(uscript.numpy())  # [17, 8] == [USCRIPT_HAN, USCRIPT_CYRILLIC]
[17  8]

Операция tf.strings.unicode_script может быть также применена к многомерному tf.Tensors или кодовым точкам tf.RaggedTensor:

print(tf.strings.unicode_script(batch_chars_ragged))
<tf.RaggedTensor [[25, 25, 25, 25, 25], [25, 25, 25, 25, 0, 25, 25, 0, 25, 25, 25, 0, 25, 25, 25, 25, 25, 25, 25, 0, 25, 25, 25, 25, 25, 25, 25, 25], [25, 25, 25, 25, 25, 25, 25, 25, 25], [0]]>

Пример: Простая сегментация

Сегментация это задача разбиения текста на словоподобные юниты. Часто это легко когда испольуются символы пробела для отделения слов, но некоторые языки (например китайский и японский) не используют пробелы, а некоторые языки (например немецкий) содержат длинные соединения, которые должны быть разделены для анализа их значений. В веб текстах, различные языки и скрипты часто перемешаны между собой , как например в "NY株価" (New York Stock Exchange).

Мы можем выполнить грубую сегментацию (без реализации каких-либо моделей ML), используя изменения систем письма для приблизительного определения границ слов. Это будет работать для строк наподобие вышеприведенного примера "NY株価". Это также будет работать для всех языков, которые используют пробелы, так как символы пробела в различных системах письма все классифицируются как USCRIPT_COMMON, специальный код, который отличается от кода любого актуального текста.

# dtype: string; shape: [num_sentences]
#
# Предложения для обработки.  Поменяйте эту строку чтобы попробовать разные входные данные!
sentence_texts = [u'Hello, world.', u'世界こんにちは']

Сперва мы декодируем предложения в кодовые точки, и определим идентификатор системы письма для каждого символа.

# dtype: int32; shape: [num_sentences, (num_chars_per_sentence)]
#
# sentence_char_codepoint[i, j] кусок кода для j-го символа
# в i-м предложении.
sentence_char_codepoint = tf.strings.unicode_decode(sentence_texts, 'UTF-8')
print(sentence_char_codepoint)

# dtype: int32; shape: [num_sentences, (num_chars_per_sentence)]
#
# sentence_char_scripts[i, j] код системы письма для j-го символа в
# i-м предложении.
sentence_char_script = tf.strings.unicode_script(sentence_char_codepoint)
print(sentence_char_script)
<tf.RaggedTensor [[72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 46], [19990, 30028, 12371, 12435, 12395, 12385, 12399]]>
<tf.RaggedTensor [[25, 25, 25, 25, 25, 0, 0, 25, 25, 25, 25, 25, 0], [17, 17, 20, 20, 20, 20, 20]]>

Далее мы используем эти идентификаторы систем письма чтобы определить куда должны быть добавлены границы слов. Мы добавим границу слова в начало каждого предложения и для каждого символа чья система письма отличается от предыдущего символа:

# dtype: bool; shape: [num_sentences, (num_chars_per_sentence)]
#
# sentence_char_starts_word[i, j] является True если j'th символ в i'th
# предложении является началом слова.
sentence_char_starts_word = tf.concat(
    [tf.fill([sentence_char_script.nrows(), 1], True),
     tf.not_equal(sentence_char_script[:, 1:], sentence_char_script[:, :-1])],
    axis=1)

# dtype: int64; shape: [num_words]
#
# word_starts[i] это индекс символа начинающего i-е слово (в
# выпрямленном списке символов всех предложений).
word_starts = tf.squeeze(tf.where(sentence_char_starts_word.values), axis=1)
print(word_starts)
tf.Tensor([ 0  5  7 12 13 15], shape=(6,), dtype=int64)

Затем мы можем использовать эти сдвиги от начала для построения RaggedTensor содержащего список слов из всех пакетов:

# dtype: int32; shape: [num_words, (num_chars_per_word)]
#
# word_char_codepoint[i, j] is the кодовая точка для j-го символа в
# i-м слове.
word_char_codepoint = tf.RaggedTensor.from_row_starts(
    values=sentence_char_codepoint.values,
    row_starts=word_starts)
print(word_char_codepoint)
<tf.RaggedTensor [[72, 101, 108, 108, 111], [44, 32], [119, 111, 114, 108, 100], [46], [19990, 30028], [12371, 12435, 12395, 12385, 12399]]>

И наконец, мы можем сегментировать кодовые точки слов RaggedTensor обратно в предложения:

# dtype: int64; shape: [num_sentences]
#
# sentence_num_words[i] число слов в i'th предложении.
sentence_num_words = tf.reduce_sum(
    tf.cast(sentence_char_starts_word, tf.int64),
    axis=1)

# dtype: int32; shape: [num_sentences, (num_words_per_sentence), (num_chars_per_word)]
#
# sentence_word_char_codepoint[i, j, k] это кодовая точка для k-го символа
# в j-м слове i-го предложения.
sentence_word_char_codepoint = tf.RaggedTensor.from_row_lengths(
    values=word_char_codepoint,
    row_lengths=sentence_num_words)
print(sentence_word_char_codepoint)
<tf.RaggedTensor [[[72, 101, 108, 108, 111], [44, 32], [119, 111, 114, 108, 100], [46]], [[19990, 30028], [12371, 12435, 12395, 12385, 12399]]]>

Чтобы сделать итоговый результат проще для чтения, мы можем закодировать его обратно в UTF-8 строки:

tf.strings.unicode_encode(sentence_word_char_codepoint, 'UTF-8').to_list()
[[b'Hello', b', ', b'world', b'.'],
 [b'\xe4\xb8\x96\xe7\x95\x8c',
  b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf']]