Announcing the TensorFlow Dev Summit 2020 Learn more

Unicode 文字列

View on TensorFlow.org Run in Google Colab View source on GitHub Download notebook

はじめに

自然言語モデルは、しばしば異なる文字セットを使った異なる言語を扱います。 Unicodeは、ほぼすべての言語で文字表示に使われている標準的なエンコードの仕組みです。各文字は、0 から0x10FFFFまでの一意の整数の コードポイント(符号位置) を使ってエンコードされます。1つの Unicode文字列は、ゼロ個以上のコードポイントのシーケンスです。

このチュートリアルでは、TensorFlow での Unicode文字列の表現方法と、どうやって Unicode で標準的な文字列操作と同様の操作を行うかについて示します。また、スクリプト検出にもとづいて Unicode 文字列をトークンに分解します。

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf

tf.string データ型

標準的な TensorFlow のtf.string型は、バイト列のテンソルを作ります。また、Unicode文字列はデフォルトでは utf-8 でエンコードされます。

tf.constant(u"Thanks 😊")
<tf.Tensor: shape=(), dtype=string, numpy=b'Thanks \xf0\x9f\x98\x8a'>

バイト列が最小限の単位として扱われるため、tf.string 型のテンソルは可変長のバイト文字列を保持できます。また、文字列長はテンソルの次元には含まれません。

tf.constant([u"You're", u"welcome!"]).shape
TensorShape([2])

注 : Pythonを使って文字列を構成するとき、v2.x系とv3.x系では Unicode の扱いが異なります。v2.x系では、Unicode文字列は上記のようにプレフィックス "u" で明示します。v3.x系では、デフォルトで Unicode としてエンコードされます。

Unicode 表現

TensorFlow での Unicode文字列表現は、2つの標準的な方法があります:

  • string スカラー — コードポイントのシーケンスは既知の 文字符合化方式 でエンコードされる
  • int32 ベクトル — 各文字には単一のコードポイントが入る

たとえば、以下3つはすべて Unicode文字列 "语言処理 "(中国語で「言語処理」を意味します)を表します。

# Unicode文字列。UTF-8にエンコードされた文字列スカラーとして表される
text_utf8 = tf.constant(u"语言处理")
text_utf8
<tf.Tensor: shape=(), dtype=string, numpy=b'\xe8\xaf\xad\xe8\xa8\x80\xe5\xa4\x84\xe7\x90\x86'>
# Unicode文字列。UTF-16-BEにエンコードされた文字列スカラーとして表される
text_utf16be = tf.constant(u"语言处理".encode("UTF-16-BE"))
text_utf16be
<tf.Tensor: shape=(), dtype=string, numpy=b'\x8b\xed\x8a\x00Y\x04t\x06'>
# Unicode文字列。Unicodeコードポイントのベクトルとして表される
text_chars = tf.constant([ord(char) for char in u"语言处理"])
text_chars
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([35821, 35328, 22788, 29702], dtype=int32)>

Unicode 表現間の変換

TensorFlowでは、これらの異なる Unicode 表現間で変換する方法を用意しています。

  • tf.strings.unicode_decode:エンコードされた文字列スカラーを、コードポイントのベクトルに変換します。
  • tf.strings.unicode_encode:コードポイントのベクトルを、エンコードされた文字列スカラーに変換します。
  • tf.strings.unicode_transcode:エンコードされた文字列スカラーを、別の文字コードに再エンコードします。
tf.strings.unicode_decode(text_utf8,
                          input_encoding='UTF-8')
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([35821, 35328, 22788, 29702], dtype=int32)>
tf.strings.unicode_encode(text_chars,
                          output_encoding='UTF-8')
<tf.Tensor: 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: shape=(), dtype=string, numpy=b'\x8b\xed\x8a\x00Y\x04t\x06'>

バッチの次元

複数の文字列をデコードする場合、各文字列の文字数が等しくない場合があります。返される結果はtf.RaggedTensorであり、最も内側の次元の長さは各文字列の文字数によって異なります。:

# Unicode文字列のバッチ。それぞれが、UTF8にエンコードされた文字列として表される
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.RaggedTensor.to_tensor メソッドを使ってパディングを追加した密な tf.Tensor に変換するか、あるいは tf.RaggedTensor.to_sparse メソッドを使って tf.SparseTensor に変換することもできます。

batch_chars_padded = batch_chars_ragged.to_tensor(default_value=-1)
print(batch_chars_padded.numpy())
[[   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: 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: 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)>

パディングされた、あるいはスパースな複数の文字列を含むテンソルがある場合は、unicode_encode を呼び出す前に tf.RaggedTensor に変換します。

tf.strings.unicode_encode(
    tf.RaggedTensor.from_sparse(batch_chars_sparse),
    output_encoding='UTF-8')
<tf.Tensor: 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: 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)>

Unicode 操作

文字列長

tf.strings.length は、文字列長をどう計算するかを示す unit パラメーターが使えます。unit のデフォルトは "BYTE" ですが、"UTF8_CHAR""UTF16_CHAR" など他の値に設定して、エンコードされた string 文字列のUnicodeコードポイントの数を決めることができます。

# 最後の絵文字は、UTF8で4バイトを占めることに注意する
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 の場合、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'

Unicode文字列を分割する

tf.strings.unicode_split は、Unicode文字列を個々の文字に分割します。

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 に似ていますが、各文字の開始オフセットを含む2番目のテンソルを返す点が異なります。

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

Unicode スクリプト

各Unicodeコードポイントは、スクリプト として知られる単一のコードポイント集合に属しています。文字スクリプトは、その文字がどの言語なのかを判断するのに役立ちます。たとえば、「Б」がキリル文字であることがわかれば、その文字を含むテキストはロシア語やウクライナ語などのスラブ言語である可能性が高いことがわかります。

TensorFlowは、あるコードポイントがどのスクリプトかを返す tf.strings.unicode_script を提供しています。戻り値のスクリプトコードは、International Components for Unicode (ICU) の UScriptCode に対応する int32 値になります。

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

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

tf.strings.unicode_script は、多次元のコードポイントの tf.Tensorstf.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]]>

例:シンプルなセグメンテーション

セグメンテーションは、テキストを単語のような粒度に分割するタスクです。これは、スペース文字を使用して単語を区切れる場合には簡単に行えますが、一部の言語(中国語や日本語など)はスペースを使いませんし、また、一部の言語(ドイツ語など)には、意味を解析するために分ける必要がある、単語を結合した長い複合語があります。Webテキストでは、「NY株価」(ニューヨーク株価)のように、異なる言語とスクリプトがしばしば混在しています。

単語の境界を推定してスクリプトを変更することにより、(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] は、i番目の文のn番目の文字のコードポイント
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] は、i番目の文のn番目の文字のスクリプトコード
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] は、i番目の文のn番目の文字が単語の始まりである場合にTrue
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] は、i番目の単語のn番目の文字のコードポイント
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番目の文の単語数
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] は、i番目の文のn番目の単語のk番目の文字のコードポイント
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']]