유니코드 문자열

TensorFlow.org에서 보기 구글 코랩(Colab)에서 실행하기 깃허브(GitHub) 소스 보기 Download notebook

소개

자연어 처리 모델은 종종 다른 문자 집합을 갖는 다양한 언어를 다루게 됩니다. 유니코드(unicode)는 거의 모든 언어의 문자를 표현할 수 있는 표준 인코딩 시스템입니다. 각 문자는 0부터 0x10FFFF 사이의 고유한 정수 코드 포인트(code point)를 사용해서 인코딩됩니다. 유니코드 문자열은 0개 또는 그 이상의 코드 포인트로 이루어진 시퀀스(sequence)입니다.

이 튜토리얼에서는 텐서플로(Tensorflow)에서 유니코드 문자열을 표현하고, 표준 문자열 연산의 유니코드 버전을 사용해서 유니코드 문자열을 조작하는 방법에 대해서 소개합니다. 또한 스크립트 감지(script detection)를 활용하여 유니코드 문자열을 토큰으로 분리해 보겠습니다.

import tensorflow as tf
2022-12-14 21:00:16.670782: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 21:00:16.670879: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 21:00:16.670889: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.

tf.string 데이터 타입

텐서플로의 기본 tf.string dtype은 바이트 문자열로 이루어진 텐서를 만듭니다. 유니코드 문자열은 기본적으로 utf-8로 인코딩 됩니다.

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

tf.string 텐서는 바이트 문자열을 최소 단위로 다루기 때문에 다양한 길이의 바이트 문자열을 다룰 수 있습니다. 문자열 길이는 텐서 차원(dimensions)에 포함되지 않습니다.

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

노트: 파이썬을 사용해 문자열을 만들 때 버전 2와 버전 3에서 유니코드를 다루는 방식이 다릅니다. 버전 2에서는 위와 같이 "u" 접두사를 사용하여 유니코드 문자열을 나타냅니다. 버전 3에서는 유니코드 인코딩된 문자열이 기본값입니다.

유니코드 표현

텐서플로에서 유니코드 문자열을 표현하기 위한 두 가지 방법이 있습니다:

  • string 스칼라 — 코드 포인트의 시퀀스가 알려진 문자 인코딩을 사용해 인코딩됩니다.
  • int32 벡터 — 위치마다 개별 코드 포인트를 포함합니다.

예를 들어, 아래의 세 가지 값이 모두 유니코드 문자열 "语言处理"(중국어로 "언어 처리"를 의미함)를 표현합니다.

# UTF-8로 인코딩된 string 스칼라로 표현한 유니코드 문자열입니다.
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'>
# UTF-16-BE로 인코딩된 string 스칼라로 표현한 유니코드 문자열입니다.
text_utf16be = tf.constant(u"语言处理".encode("UTF-16-BE"))
text_utf16be
<tf.Tensor: 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: shape=(4,), dtype=int32, numpy=array([35821, 35328, 22788, 29702], dtype=int32)>

표현 간의 변환

텐서플로는 다른 표현으로 변환하기 위한 연산을 제공합니다.

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'>

배치(batch) 차원

여러 개의 문자열을 디코딩 할 때 문자열마다 포함된 문자의 개수는 동일하지 않습니다. 반환되는 값은 tf.RaggedTensor로 가장 안쪽 차원의 크기가 문자열에 포함된 문자의 개수에 따라 결정됩니다.

# UTF-8 인코딩된 문자열로 표현한 유니코드 문자열의 배치입니다. 
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를 바로 사용하거나, 패딩(padding)을 사용해 tf.Tensor로 변환하거나, tf.RaggedTensor.to_tensortf.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)>

패딩된 텐서나 희소(sparse) 텐서는 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)>

유니코드 연산

길이

tf.strings.length 연산은 계산해야 할 길이를 나타내는 unit 인자를 가집니다. unit의 기본 단위는 "BYTE"이지만 인코딩된 string에 포함된 유니코드 코드 포인트의 수를 파악하기 위해 "UTF8_CHAR""UTF16_CHAR"같이 다른 값을 설정할 수 있습니다.

# 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('{} 바이트; {}개의 UTF-8 문자'.format(num_bytes, num_chars))
11 바이트; 8개의 UTF-8 문자

부분 문자열

이와 유사하게 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로 만든 문자 텐서를 원본 문자열과 위치를 맞추려면 각 문자의 시작 위치의 오프셋(offset)을 알아야 합니다. tf.strings.unicode_decode_with_offsetsunicode_decode와 비슷하지만 각 문자의 시작 오프셋을 포함한 두 번째 텐서를 반환합니다.

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

for (codepoint, offset) in zip(codepoints.numpy(), offsets.numpy()):
    print("바이트 오프셋 {}: 코드 포인트 {}".format(offset, codepoint))
바이트 오프셋 0: 코드 포인트 127880
바이트 오프셋 4: 코드 포인트 127881
바이트 오프셋 8: 코드 포인트 127882

유니코드 스크립트

각 유니코드 코드 포인트는 스크립트(script)라 부르는 하나의 코드 포인트의 집합(collection)에 속합니다. 문자의 스크립트는 문자가 어떤 언어인지 결정하는 데 도움이 됩니다. 예를 들어, 'Б'가 키릴(Cyrillic) 스크립트라는 것을 알고 있으면 이 문자가 포함된 텍스트는 아마도 (러시아어나 우크라이나어 같은) 슬라브 언어라는 것을 알 수 있습니다.

텐서플로는 주어진 코드 포인트가 어떤 스크립트를 사용하는지 판별하기 위해 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.Tensortf.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]]>

예제: 간단한 분할

분할(segmentation)은 텍스트를 단어와 같은 단위로 나누는 작업입니다. 공백 문자가 단어를 나누는 구분자로 사용되는 경우는 쉽지만, (중국어나 일본어 같이) 공백을 사용하지 않는 언어나 (독일어 같이) 단어를 길게 조합하는 언어는 의미를 분석하기 위한 분할 과정이 꼭 필요합니다. 웹 텍스트에는 "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]는
# i번째 문장 안에 있는 j번째 문자에 대한 코드 포인트 입니다.
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_codepoint[i, j]는 
# i번째 문장 안에 있는 j번째 문자의 유니코드 스크립트 입니다.
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번째 문장 안에 있는 j번째 문자가 단어의 시작이면 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번째 단어 안에 있는 j번째 문자에 대한 코드 포인트 입니다.
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번째 문장 안에 있는
# j번째 단어 안의 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']]
# MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.