CV・NLPハマりどころメモ

画像認識と自然言語処理を研究する中でうまくいかなかったことと、その対策をまとめる自分用メモが中心。

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

汎用言語モデル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 "^", "$", 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ループや,パターンマッチングを多用しまくるコードになってしまうんだな.