BERTにおけるテキストクレンジングを紹介[BERT]
汎用言語モデルBERTを使用する際に,テキストクレンジングを行う関数を見つけ,読んでみると勉強になったので記事にしてみた.
参考にしたのは,Google Researchの実装である.
まず,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ループや,パターンマッチングを多用しまくるコードになってしまうんだな.
*1:ASCIIコード表 http://www3.nit.ac.jp/~tamura/ex2/ascii.html
*2:ord後にchrを実行すれば,元に戻せる https://python.civic-apps.com/char-ord/
*3:Null文字ってなんのためにあるの? http://www.altima.jp/column/fpga_edison/null.html
*4:Replacement Character https://www.fileformat.info/info/unicode/char/fffd/index.htm
*5:LFとCR https://marusunrise2.blogspot.com/2014/06/lfcrcrlf.html
*6:General Category Value http://www.unicode.org/reports/tr44/#General_Category_Values
ディスカッション
コメント一覧
まだ、コメントがありません