BERTにおけるテキストクレンジングを紹介[BERT]

7月 27, 2020

汎用言語モデルBERTを使用する際に,テキストクレンジングを行う関数を見つけ,読んでみると勉強になったので記事にしてみた.

参考にしたのは,Google Researchの実装である.

github.com

まず,BERTのコード(tokenization.pyのFullTokenizerクラスのtokenize関数の中)で見つけたテキストクレンジングの関数を以下に貼る.

  def _clean_text(self, text):
"""Performs invalid character removal and whitespace cleanup on text."""
output = []
for char in text:
cp = ord(char)
if cp == 0 or cp == 0xfffd or _is_control(char):
continue
if _is_whitespace(char):
output.append(" ")
else:
output.append(char)
return "".join(output)

処理の内容としては,不正な文字と空白文字を除去するシンプルな実装だが,処理の際,ordでASCIIコード*1に変換してから*2,ASCIIコードで条件分岐をさせている.また,6行目の条件分岐のcp==0はNull文字*3を表し,cp==0xfffdは�を表す*4.このcp==0xfffdは文字化けを判定する役目を果たしており,生テキストを扱う際には非常に有用である.さらに,もう1つの条件 _is_controlは,以下の関数によって定義される.

def _is_control(char):
"""Checks whether `chars` is a control character."""
# These are technically control characters but we count them as whitespace
# characters.
if char == "\t" or char == "\n" or char == "\r":
return False
cat = unicodedata.category(char)
if cat in ("Cc", "Cf"):
return True
return False

_is_controlは,制御文字かどうかをチェックする関数である.まずは,"¥t"(タブ区切り),"¥n"(改行),"¥r"(復帰) *5 であるかどうかを判定.次にPythonのunicodedata.categoryによって,大文字か小文字か数字かなどのカテゴリを判定*6.もし,カテゴリが"Cc"(C0, C1 control codes)か"Cf"(format control character)なら制御文字フラグを立てる.

以上3つの条件で文字をチェックすることで,不正な文字を判定している.

さて,_clean_textに戻って,次の処理,_is_whitespaceをみてみよう.

def _is_whitespace(char):
"""Checks whether `chars` is a whitespace character."""
# \t, \n, and \r are technically contorl characters but we treat them
# as whitespace since they are generally considered as such.
if char == " " or char == "\t" or char == "\n" or char == "\r":
return True
cat = unicodedata.category(char)
if cat == "Zs":
return True
return False

_is_whitespaceは,空白文字かどうかを判定するための関数である.5行目の説明は先ほどしたため省略する.7行目について,ここでは,Pythonのunicodedata.categoryで"Zs"(Space character)にカテゴライズされた場合,空白文字フラグを立てる処理になっている.そして,_clean_textでは,_is_whitespaceで空白文字フラグが立つと,半角スペースに置き換わる処理になっているようだ.

以上で_clean_textの説明は終わりだ.一旦,ASCIIコードに変換してから条件分岐を行うところが非常に参考になった.

また,BERTのtokenization.pyでは,句読点を判定する関数も存在する.

def _is_punctuation(char):
"""Checks whether `chars` is a punctuation character."""
cp = ord(char)
# We treat all non-letter/number ASCII as punctuation.
# Characters such as "^", "

#034;, and "`" are not in the Unicode
# Punctuation class but we treat them as punctuation anyways, for
# consistency.
if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or
(cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)):
return True
cat = unicodedata.category(char)
if cat.startswith("P"):
return True
return False
_is_punctuationは,句読点を判定する関数だが,8行目でordで変換したASCIIコードを不等号で条件分岐させている.ASCIIコードに変換する利点は,数値で範囲を指定できるところにあるのかもしれない.また,12行目でカテゴリ"P"(Punctuation)系に分類される文字が現れた場合に句読点フラグを立てている.ASCIIコードとunicodedata.categoryと2つの観点で判定を行なっているようだが,これには何の意味があるのだろうか?もしかすると,ASCIIコードで引っかからない文字もしくはordで変換できない文字が存在するのかもしれない.

また,_is_chinese_charでは,中国語を判定する処理を行なっていた.

  def _is_chinese_char(self, cp):
"""Checks whether CP is the codepoint of a CJK character."""
# This defines a "chinese character" as anything in the CJK Unicode block:
#   https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)
#
# Note that the CJK Unicode block is NOT all Japanese and Korean characters,
# despite its name. The modern Korean Hangul alphabet is a different block,
# as is Japanese Hiragana and Katakana. Those alphabets are used to write
# space-separated words, so they are not treated specially and handled
# like the all of the other languages.
if ((cp >= 0x4E00 and cp <= 0x9FFF) or  #
(cp >= 0x3400 and cp <= 0x4DBF) or  #
(cp >= 0x20000 and cp <= 0x2A6DF) or  #
(cp >= 0x2A700 and cp <= 0x2B73F) or  #
(cp >= 0x2B740 and cp <= 0x2B81F) or  #
(cp >= 0x2B820 and cp <= 0x2CEAF) or
(cp >= 0xF900 and cp <= 0xFAFF) or  #
(cp >= 0x2F800 and cp <= 0x2FA1F)):  #
return True
return False

_is_chinese_charは,中国語を判定する関数である.ここでも,_is_punctuationと同様,ASCIIコードの値域で中国語を定義していた.日本語も同様の判定ができそう*7

Accentsを除去する関数や,テキストをUnicodeに変換する関数もあったが,疲れてしまったので解説は後日追加する予定.

 def _run_strip_accents(self, text):
"""Strips accents from a piece of text."""
text = unicodedata.normalize("NFD", text)
output = []
for char in text:
cat = unicodedata.category(char)
if cat == "Mn":
continue
output.append(char)
return "".join(output)
def convert_to_unicode(text):
"""Converts `text` to Unicode (if it's not already), assuming utf-8 input."""
if six.PY3:
if isinstance(text, str):
return text
elif isinstance(text, bytes):
return text.decode("utf-8", "ignore")
else:
raise ValueError("Unsupported string type: %s" % (type(text)))
elif six.PY2:
if isinstance(text, str):
return text.decode("utf-8", "ignore")
elif isinstance(text, unicode):
return text
else:
raise ValueError("Unsupported string type: %s" % (type(text)))
else:
raise ValueError("Not running on Python2 or Python 3?")

以上でBERTにおけるクレンジング方法の紹介を終える.テキストのクレンジングの際には,やはりforループや,パターンマッチングを多用しまくるコードになってしまうんだな.

Deep Learning

Posted by vastee