Pythonで学ぶデータラングリング――文字抽出、フィルタリングを試してみよう個人情報の除外など、データ分析でも重要

データ分析や自然言語処理タスクなどでは「データラングリング」が欠かせません。本稿では、なぜデータラングリングが重要なのか、Pythonを利用したテキスト抽出例や特定文字列のフィルタリング、Unicode文字への対応など幾つかのユースケースを交えながら解説します。

» 2022年12月15日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 八楽のNLP(自然言語処理)エンジニアリングチームに所属するGiovanni Gatti De Giacomo(ジョバンニ・ガッティ・デ・ジャコモ)、Vipul Mishra(ビプル・ミシュラ)です。

 八楽はAI(人工知能)の自動翻訳プラットフォーム「ヤラクゼン」の企画、開発、運用を通じて、翻訳業務の効率化と翻訳品質の向上、翻訳チームのナレッジ共有を支援しています。ヤラクゼンはソニーや帝人、コニカミノルタなどの大手企業を含め、延べ1000社以上に導入されています。

 われわれNLPチームでは、自然言語処理の知見を生かしたツールの開発、ツールの裏側で機能するエンジンの開発・改善、最新の研究結果をビジネスに適用する手法を日々検討しています。

 翻訳エンジンなど言語処理の領域では、「データラングリング」(データの前処理)が欠かせません。本稿では、なぜデータラングリングが重要なのか、Pythonを利用したテキスト抽出例や特定文字列のフィルタリングなど実例を交えながら解説していきます。

.docxファイルからテキストを抽出する方法

 まずは、Microsoft WordやGoogle Docsなどで作成された.docxファイルから、Pythonを利用してテキストデータを抽出する方法を紹介します。

インストール

 .docx形式のファイルを解析(パース)するためのパッケージをインストールする必要があります。ドキュメントからテキストを抽出することだけに焦点を当てることとし、.docx形式のファイル構造やパースの方法は割愛します。

 今回使用するパッケージは「python-docx」です。インストールには、Pythonに標準で付属しているパッケージマネジャーのpipを使用します。以下のコマンドを実行すると、パッケージを入手できます。

$ pip install python-docx

パラグラフとタイトル

 必要なパッケージがインストールできたので、テキストを抽出してみましょう。デモ用のドキュメントを作成しました。ここからダウンロードしてみてください。

 では、python-docxライブラリを利用して、.docxファイルからパースしてみましょう。Pythonで以下のコードを実行します。

import docx
doc = docx.Document("test_document.docx")

 ファイルに含まれる表のテキスト以外のテキストを全て抽出したい場合は、以下のPythonコードを実行します。

texts = [p.text for p in doc.paragraphs]

 上記のコードは、タイトル、見出し、通常のテキストを含む、ドキュメント内のテキストほぼ全てを抽出します。タイトルとテキストを別々に管理したい場合は、以下のコードを実行します。

texts = []
titles = []
for p in doc.paragraphs:
    if p.style.name == "Title":
        titles.append(p.text)
    else:
        texts.append(p.text)

 異なるスタイル名を条件とすることで、見出し、タイトル、通常のテキストをさらに区別できます。ちなみに、Paragraphオブジェクトには他にも多くのプロパティがあるので、ドキュメンテーションを参考にご自身のアプリケーションに有用だと思われるフィールドを以下から探してみてください。

 前のセクションで書きましたが、これまで紹介したコード例では、表のテキストは抽出しません。表形式のデータは、別のオブジェクトであるTableに格納されるためです。テーブルの構造は維持せずに全てのテキストを抽出する場合は、以下のコードを実行します。

texts = []
for table in doc.tables:
    for row in table.rows:
        for cell in table.cells:
            texts.append(cell.text)

 このコードは、文書内にある全ての表のセルを調べ、その中に含まれるテキストを抽出します。文書内の全テキストを抽出することが目的なら、最適なコードといえます。

 表の構造を保ったままテーブルを抽出したい場合は、pandas DataFrameに変換する必要があります。まずターミナルで下記のコマンドを実行して、pandasライブラリをインストールします。

$ pip install pandas

 pandasをインストールした後、以下のコードを実行することで、.docxファイルから抽出されたテーブルをDataFrameのリストに変換できます。

import pandas as pd
tables = []
for table in doc.tables:
    table_data = {cell.text: [] for cell in table.rows[0].cells}
    for row in table.rows[1:]:
        for name, cell in zip(table_data.keys(), row.cells):
            table_data[name].append(cell.text)
    tables.append(pd.DataFrame(table_data))

 このコードは、.docxファイルの表を調べて、最初の行を列名として同じ形式のpandas DataFrameを作成します(pandasについてもっと知りたい、pandasで他に何ができるかを知りたいという方は公式ドキュメントを参考にしてみてください)。

データを整形すべき理由

 ここまで、.docxファイルからテキストを抽出する方法を解説しました。抽出されたテキストデータを何らかの形で利用する際に欠かせないのが、データラングリング(データの前処理)です。

 自然言語処理モデルを開発していく際、学習データの質が非常に重要です。自然言語タスクにおけるモデルの性能は、サイズが小さかったとしても高品質のコーパスを用いて学習したモデルが、サイズは大きいが低品質のコーパスを用いて学習したモデルの結果を大幅に上回ることがあります。

 モデル学習におけるコーパスの有効性は、コーパスのサイズ、含まれるデータの多様性、クリーンさなど、さまざまな要因によって決まります。コーパスのサイズと収録範囲を増加させるのは困難ですが、コーパスのクリーニング、前処理をしておくことで、最終的な品質をかなり向上させることができます。

 データクリーニングは、データフィルタリング、データ変更などのプロセスの総称であり、データ内の全体的な情報を失うことなく、品質を向上させることです。コーパスのクリーニングに採用される具体的な手法は、タスクやコーパスの言語などで異なります。

 それでは、文の主語が「python」であるか否かを分類するタスクを考えてみましょう。このタスクの訓練データを以下に示します。

例文 ラベル
Pythonは大好きなプログラミング言語です。 Yes
PYTHONはあまり好きじゃないな.. Yes
私はRustが一番好き。 No

 このタスクでは「PYTHON」「PYTHON」「Python」といったインスタンスを全て「python」に変換する正規化(テキストを標準的な文字や単語に変換すること)が有用だといえます。この前処理をすることで、これらの訓練データで学習したモデルは、形式のみが異なる単語の意味をそれぞれ学習する必要がなくなりました。

 次は別のタスクです。このタスクの目的は、入力文の主語を特定し、生成することとします。

例文 ラベル
Pythonは大好きなプログラミング言語です。 Python
PYTHONはあまり好きじゃないな.. PYTHON
私はRustが一番好き。

 各単語の原形を保つことが重要になるため、上記のタスクで示した訓練データに対する正規化は不適切となります。目的や要件に応じて、前処理の内容も変わってくるというわけです。

 ここからは、テキストの要約や質問応答などの意味解析が重要なタスクを扱う自然言語処理モデルに適したフィルタリングと正規化の具体的な方法を紹介します。

URL、メール、絵文字をテキストからフィルタリングする

 生のコーパスには、学習タスクに関連しない文字列が含まれている場合があります。このような場合は、あらかじめフィルタリングしておきます。よくあるのが、URL、メールアドレス、絵文字です。これらをフィルタリングする方法をご紹介します。

 ほとんどのメールアドレスは単純な構造をしているので、正規表現を書いてマッチ(一致)させ、フィルタリングできます。Pythonの標準ライブラリにある正規表現モジュール、「re」を使用します。

import re
example = "できたらfoo.bar.42@foobar.com宛てに送ってもらってもいいかな"
email_pattern = r"[\w.%+-]{2,63}@[\w.-]{2,250}\.[a-z]{2,4}"
repl_string = "<email>"
re.sub(email_pattern, repl_string, example, flags=re.ASCII)
#できたら<email>宛てに送ってもらってもいいかな

 URLも同様に処理できます。

import re
example = "資料はhttps://www.example.com/docs/からダウンロードお願いします。"
url_pattern = r"((http(s)?):\/\/)?(www\.)[;?\w:%.+~#=-]{2,256}\.[a-z]{2,6}([\w:%_+.~#?&//=-]*)"
repl_string = "<url>"
re.sub(url_pattern, repl_string, example, flags=re.ASCII)
#資料は<url>からダウンロードお願いします。

 上記のいずれの例でも、テキストの文構造が崩れないように、マッチした文字列を削除するのではなく、「<email>」や「<url>」のようなプレースホルダーに置き換えています(※電子メールやURLの正規表現は、IETF〈Internet Engineering Task Force〉で定義されたRFCの規格を簡略化したものです。従って、メールアドレスとURLをほとんど認識できると思われますが、全てを認識できるわけではありません)。

 次に、テキストから絵文字をフィルタリングしてみます。Pythonの「demoji」というライブラリを使用します。先にpipを使ってインストールします。

$ pip install demoji

 入力文字列に対して「replace」または「replace_with_descメソッドを呼び出すだけで、全ての絵文字を置き換えることができます。

import demoji
example = "朝食も昼食も夕食もケーキがいい🍰🎂🧁"
repl_string = "<emoji>"
demoji.replace(example, repl_string)
#朝食も昼食も夕食もケーキがいい<emoji><emoji><emoji>
desc_bound = ":"
demoji.replace_with_desc(example, desc_bound)
#朝食も昼食も夕食もケーキがいい:shortcake::birthday cake::cupcake:

 こうしたフィルタリングをしておく重要な理由はもう1つあります。個人情報(PII)や個人健康情報(PHI)の漏えいを防止することです。

 近年、NLPモデルからの情報漏えいが幾つか報告されています。同様のインシデントを未然に防ぐためには、適切なデータクリーニングを実施する必要があります。対策をしておかなければ、モデルに以下のようなコードを入力するだけで、簡単に機密情報を入手できる可能性が出てきます。

山田太郎の電話番号は<mask>
<mask>のTwitterのユーザー名は@hogehoge2022です。

 データから人名、地理的な位置、組織名などのセンシティブな情報を匿名化する方法も紹介します。まず、固有表現抽出モデル(NERモデル)を用いて情報の実体を検出し、それぞれのカテゴリー名に置き換えます。まず「SpaCy」モジュールをインストールし、「ja_core_news_md」のような事前に学習されたモデルをダウンロードします。

$ pip install spacy
$ python -m spacy download ja_core_news_md
import spacy
to_mask_labels = ['PERSON', 'ORG', 'LOC']
filtered = example = "ダンブルドアさんはホグワーツ魔法魔術学校で校長先生をやっています。"
nlp = spacy.load("ja_core_news_md")
doc = nlp(example)
entities = []
for ent in doc.ents:
    entities.append((ent.text, ent.label_))
to_mask_entities = [(text, f"[{ent}]") for (text, ent) in entities if ent in to_mask_labels]
for ent in to_mask_entities:
    filtered = filtered.replace(ent[0], ent[1])
print(filtered)
#"[PERSON]さんは[ORG]で校長先生をやっています。"

テキストを標準的な文字列の集合に正規化する

 単語や文字の表現のバリエーションを減らすことで、結果的にコーパスの語彙(ごい)サイズが爆発的に増加するのを抑えられます。Webクロールで収集したデータで作成された公開コーパスには、珍しいUnicode文字が含まれていることが多く、学習時に不必要な複雑さが生じてしまいます。こうした理由から正規化を行います。Unicodeの文字「㈤」は、意味を変えることなく簡単に「(五)」に置き換えられます。

 ここで紹介する正規化処理は「mecab-ipadic-neologd」辞書で使用された正規化スクリプトを基にしています。

 まず、Pythonの標準ライブラリにあるモジュール(unicodedataとre)を使用して、全角英数字と半角かな文字をNFKC(Normalization Form Compatibility Composition:正規化形式KC)に基づいて正規化してみましょう。

import unicodedata
import re
def normalize_unicode(char_cls, e):
    p = re.compile("([{0}]+)".format(char_cls))
    def norm(txt):
        return unicodedata.normalize("NFKC", txt) if p.match(txt) else txt
    return "".join(norm(txt_grp) for txt_grp in re.split(p, e))
#全角英文字A→半角英文字A
alnum_cls = '0-9a-zA-Z'
#半角カナ→全角かな
sym_cls = '。-゚'
example = "id:ff4oo2oo"
normalize_unicode(alnum_cls+sym_cls, example)
#id:ff4oo2oo

 続けて、ハイフン、長音符、チルダのような文字を全てPythonのstring.replace()メソッドで正規化します。

e = "わ֊֊ー〰〰いぃぃ!"
e = re.sub("[˗֊‐‑‒–⁃⁻₋−]+", "-", e)
e = re.sub("[﹣−ー—―─━ー]+", "ー", e)
e = re.sub("[~∼∾〜〰〜]+", "〜", e)
print(e)
#わ-ー〜いぃぃ!

 全角の記号は、“「」”や“。”などの例外を除き、半角に変換します。

example = "「人生は虹色」***(公開日決定)***"
punct_cls = "!#$%&()*+,−./:;<>?@[¥]^_`{|}"
e = normalize_unicode(punct_cls, example)
def maketrans(pre, post):
    return {ord(x): ord(y) for x, y in zip(pre, post)}
expect_punct_rule = maketrans("。、・「」’“”", "。、・「」\'\"\"")
e = e.translate(expect_punct_rule)
print(e)
#「人生は虹色」***(公開日決定)***

 不要な空白文字は全て削除します。日本語のみのテキストとラテン語のみのテキストの間に存在するホワイトスペースは削除します。さらに日本語のみのテキスト間のスペースも削除します。この文脈での日本語のみのテキストとラテン語のみのテキストの意味は、以下のコードスニペットのパターン「ja_cls」と「latin_cls」を参照してください。

example = " 生命 、宇宙 、そして 万物に ついて の究極 の疑問    の答え は 42"
def remove_extra_spaces(example):
    #normalizing whitespace chars to half-width space
    e = example.strip()
    e = e.replace('\t', ' ')
    e = re.sub("\u200b", "", e)
    e = re.sub("[  ]+", " ", e)
    ja_cls = "".join(
        (
            "\u4E00-\u9FFF",  # CJK UNIFIED IDEOGRAPHS
            "\u3040-\u309F",  # HIRAGANA
            "\u30A0-\u30FF",  # KATAKANA
            "\u3000-\u303F",  # CJK SYMBOLS AND PUNCTUATION
            "\uFF00-\uFFEF",  # HALFWIDTH AND FULLWIDTH FORMS
        )
    )
    latin_cls = "".join(
        (
            "\u0000-\u007F",  # BASIC LATIN[g]
            "\u0080-\u00FF",  # LATIN-1 SUPPLEMENT[h]
        )
    )
    def remove_space_between(char_cls1, char_cls2, e):
        p = re.compile("([{}]) ([{}])".format(char_cls1, char_cls2))
        while p.search(e):
            e = p.sub(r"\1\2", e)
        return e
    e = remove_space_between(ja_cls, ja_cls, e)
    e = remove_space_between(ja_cls, latin_cls, e)
    e = remove_space_between(latin_cls, ja_cls, e)
    return e
remove_extra_spaces(example)
#生命、宇宙、そして万物についての究極の疑問の答えは42

用途に合わせてテキストをトークンに分割する

 フィルタリングや正規化されたテキストでモデルを学習させたりアルゴリズムを適用したりする前に、ほとんどの場合、テキストをトークン化する必要があります。トークン化とは、基本的にテキストをより小さな単位に分割することです。トークン化をする際の一般的なアプローチである単語レベルとサブワードレベルについて解説します。

単語レベルのトークン化

 単語レベルのトークン化とは、テキストを一連の単語に分解することを指します。英語など一部の言語では、一般的に単語同士がスペースで区切られているため、この処理は簡単です。しかし、日本語の場合、テキストを単語レベルの単位にトークン化することは、より難しく、より曖昧なタスクとなります。「教科書」は1つのトークンと考えるべきでしょうか? それとも、「教科」と「書」に分けた方がいいのでしょうか? 「科学的」は「科学的」と「科学」「的」のどちらが正しいのでしょうか? 残念ながら、曖昧さの問題に対する正しい答えはありません。トークナイザ(トークン化を実行するプログラム)によりそれぞれ異なる処理をすることがほとんどです。

 今回は、単語レベルのトークナイザを複数説明します。お好みのもの、あるいはご自身のアプリケーションに合うものを選んでみてください。まず「KyTea」(読み方:「キューティ」)というトークナイザは、テキストをとても小さな単位に分割します。一方で、spaCy(SudachiPy)はKyTeaに比べるとかなり緩やかなトークナイザだといえるでしょう。

Spacy(SudachiPy)

 最初に紹介する単語レベルのトークナイザは、Pythonの「Spacy package」で提供されています。Spacyパッケージでは複数の言語と異なるサイズで利用可能なトークナイザが提供されています。ちなみに、ここからは全て日本語のトークナイザに焦点を当てた説明になります。

 spaCyでは、日本語で利用できるパイプラインが4つあります。以下の通りです。

  • 「ja_core_news_sm」(小)
  • 「ja_core_news_md」(中)
  • 「ja_core_news_lg」(大)
  • 「ja_core_news_trf」(Transfomer)

 本稿では「ja_core_news_md」パイプラインを使用しますが、公式サイトを参考に他のオプションも参考にしてみてください。

 まずはspaCyパッケージをインストールし、ja_core_news_mdパイプラインをダウンロードします。ターミナルで以下のコマンドを実行しましょう。

$ pip install spacy
$ python -m spacy download ja_core_news_md

 spaCyと前述のパイプラインのインストールが完了したら、Pythonで使用してみます。

import spacy
nlp = spacy.load("ja_core_news_md")

 パイプラインを読み込むと、デフォルトでトークン化以外にも多くのものが含まれていることが分かります。例えば、以下のようなものが含まれます。

 パイプラインのコンポーネントを無効にするには、disableパラメーターを使用します。

import spacy
nlp = spacy.load("ja_core_news_md", disable=["ner"])

 パイプラインを読み込んだら、以下を実行すると、トークンをspaCyパイプラインから簡単に取り出せます。

sents = ["あ〜!!パソコンが壊れた!", "インドの仏教", "もう11時だ。"]
for sent in sents:
    tokens = nlp(sent)
    for token in tokens:
        print(token.idx, token.pos_, token.text)

 上記コードのスニペットは、各文を順番にspaCyパイプラインで処理し、文の各トークンについて文字位置、品詞タグ、生テキストを表示します。しかし、ja_core_news_trfパイプラインの場合、ベストプラクティスではないかもしれません。以下を試してみましょう。

sents = ["あ〜!!パソコンが壊れた!", "インドの仏教", "もう11時だ。"]
for tokens in nlp.pipe(sents, batch_size=4):
    for token in tokens:
        print(token.idx, token.pos_, token.text)

 上記コードでは、文章をバッチで一括処理でき、特にGPU上で動作するモデル(Transformerベースのパイプラインなど)の計算を高速化できるでしょう。

 spaCyパイプラインとトークナイザの詳細は、公式ドキュメンテーション参考にしてみてください。

KyTea

 日本語の単語レベルトークナイザとしては「KyTea」も有名です。インストール方法はspaCyよりも若干複雑になります。以下で全手順を説明します。

 まずは、KyTeaのソフトウェアパッケージをOSにインストールします。Cygwin、Linux、MacOSの場合、以下のコマンドでインストールできます。KyTea公式サイトにも説明があります。

$ curl https://www.phontron.com/kytea/download/kytea-0.4.7.tar.gz --output kytea-0.4.7.tar.gz
$ tar -xzf kytea-0.4.7.tar.gz
$ cd kytea-0.4.7
$ ./configure
$ make
$ sudo make install
$ kytea --help

 「kytea --help」コマンドを実行して、Kyteaのソフトウェアに関する情報が表示されれば、インストールは成功です。

 続いて、トークン化するためのKyTeaモデルをダウンロードします。ダウンロードには3つのオプションがあります。

 よく分からない場合は、精度とスピード、サイズのバランスが良い「Compact SVM」をデフォルトで使用することを推奨します。ダウンロードしたら、以下を実行して解凍してください。

$ gzip -d jp-0.4.7-5.mod.gz

 「jp-0.4.7-5.mod.gz」は、ご自身が選んだモデルで適宜置き換えてください。モデルへのパスを指定する必要があるため、コードで指定しやすい場所に格納しましょう。ここからは説明を簡易にするため、コードはモデルが置かれているのと同じディレクトリで実行されていると仮定します。

 モデルをダウンロードした後、Python用のkyteaパッケージをインストールします。ターミナルで以下のコマンドを実行します。

$ pip install kytea

 KyTeaソフトウェアパッケージとPython用のkyteaパッケージをインストールし、3つのモデルのうち1つをダウンロードすると、以下のコードでKyTeaトークナイザを利用できます。

import Mykytea
tokenizer = Mykytea.Mykytea("-model ./jp-0.4.7-5.mod")
「./jp-0.4.7-5.mod」は以前選択したモデルのパスと名前に変更してください。

 KyTeaが読み込めたら、いよいよ文章のトークン化に進みます。以下のコードスニペットを実行してみてください。

sents = ["あ〜!!パソコンが壊れた!", "インドの仏教", "もう11時だ。"]
for sent in sents:
    tokens = tokenizer.getTags(sent)
    for token in tokens:
        print(token.surface, token.tag[0][0], token.tag[1][0])

 上記コードは各文をトークン化し、トークンを1つずつ、その品詞と発音とともに表示します。

MeCab

 最後に紹介するのは、MeCabという単語レベルのトークナイザです。日本語をサポートする単語レベルのトークナイザとして頻繁に利用されています。機械翻訳結果のBLEUスコアの計算前に日本語文をトークン化する際、広く使用されている「SacreBLEU」にも含まれています。

 始めに、MeCabのソフトウェアパッケージをインストールします。DebianベースのLinuxであれば、以下のコマンドを実行しましょう。

$ sudo apt update
$ sudo apt install -y mecab mecab-utils

 その他、macOSを使用している場合は、Homebrewを使ってインストールできます。

 MeCabがシステムにインストールされたら、コードの中でMeCabを使えるようにするためのPythonパッケージをインストールする必要があります。ターミナルで以下を実行します。

$ pip install mecab-python3

 MeCabのPythonパッケージをインストールしたら、以下のコードを実行します。

import MeCab
tagger = MeCab.Tagger()

 MeCabでは、KyTeaと同様に「Tagger」のコンストラクタでパラメーターを指定可能です。MeCabが使用する辞書(unidic、jumandic、ipadicなど)を設定できます。今回は各辞書やそのインストール方法は割愛します。詳しくは、公式ドキュメンテーションを参照してください。

 MeCabを読み込んだら、次のコードを使って文章を1つずつトークン化してみましょう。

sents = ["あ〜!!パソコンが壊れた!", "インドの仏教", "もう11時だ。"]
for sent in sents:
    current_node = tagger.parseToNode(sent)
    while current_node.next:
        print(current_node.surface, current_node.feature)
        current_node = current_node.next

 このコードは、各文を調べ、MeCabを使用してトークン化します。parseToNodeメソッドを使うと、MeCabはトークンを二重連結リスト構造に格納し、各ノードには前任ノードと後任ノードへのポインタを持たせます。このコードは連結リストを左から右へとたどり、各トークンの表面形と抽出されたMeCab特徴(品詞、発音、見出し語〈レンマ〉など)を表示します。

サブワードレベルのトークン化

 前処理の最終目的は、テキストを数値(=vocab_id)に変換し、NLPモデルで数値計算ができるようにすることです。上記のステップで単語トークン化されたコーパスができ、各トークンが固有のIDを持つ語彙集合を作成できるようになりました。この後、コーパスの各トークンを対応するIDに置き換え、このID列をモデルに渡せばよいのです。

 ただし、単語レベルのトークンで語彙を作るには、幾つかデメリットがあります。

膨大な語彙数

 厳密な正規化に取り組んでも、コーパス内のユニークな単語の数は非常に多くなる場合がほとんどです。要因の1つとしては、コーパス内に誤字がたくさん存在することが挙げられます。語彙数が多くなることは、計算時間やメモリ使用量に悪影響を与えます。

同じ言葉のさまざまな形式

 単語レベルのトークン化では「dog」と「dogs」は語彙の中で別々の項目となり、モデルはこれらの表現を別々に学習しなければなりません。こうした語彙は意味的に類似しているため、一方のベクトル表現を学習することで他方のベクトル表現の学習に役立たせられるのが理想です。

未知の語彙

 データを推論する際、コーパスにない単語、つまり語彙セットにない単語は「unk」トークンにマッピングされます。つまり、訓練データに含まれない語彙には対応できないモデルになってしまうのです。

 このような問題を解決するために、テキストをサブワードレベルまでトークン化してみましょう。サブワードレベルのトークン化は、単語より細かい単位でも、任意の粒度でテキストをチャンク分けできます。

 サブワードレベルの利点は、表現性を犠牲にすることなく、必要な語彙の大きさをあらかじめ指定できることです。サブワードレベルのトークン化のルールは定められているわけではなく、コーパスから学習され、必要な語彙のサイズと採用された特定のアルゴリズムによって決定されます。例えば「アミューズメント」というテキストのトークン化例として以下が挙げられます。

アミューズメント
ア    ミ    ュ    ー    ズ    メ    ン    ト
アミューズ    メント
アミュー    ズメント

 サブワードレベルのトークン化は、ワードレベル型トークン化よりも優れていると考えられています。サブワードレベルのトークン化により、その語彙集合に望ましいサイズを指定でき、語彙サイズが爆発的に大きくなることを防げるためです。また、似ている単語からなるサブワードトークンは重なり合う可能性が高く、一方の表現の学習は片方の表現の学習に役立ちます。さらに、推論する際に出会う新しい単語は、より小さなサブワードトークンに分解して問題なく処理できます。

 ここでは、サブワードレベルのトークン化をするための代表的なライブラリを2つ紹介します。

fastBPE

 fastBPEはBPE(Byte Pair Encoding)アルゴリズムを実装しています。このアルゴリズムは、もともとデータ圧縮のために作られたものですが、トークン化にも採用されました。まずコーパス全体を文字単位に分割し、出現頻度に基づいてペアをマージします。こうして形成された新しいペアは、マージ数の制限に達するまで繰り返しカウントされ、マージされます(BPEアルゴリズムの詳細はこちらをご覧ください)

 g++コンパイラを使用して、fastBPEソフトウェアパッケージをインストールします。このコード例ではホームディレクトリから作業していますが、これはインストールに必須ではありません。

$ cd ~
$ pip install Cython
$ git clone https://github.com/glample/fastBPE.git
$ cd fastBPE
$ g++ -std=c++11 -pthread -O3 fastBPE/main.cc -IfastBPE -o fast

 次に、fastBPE用のPython APIを以下のコマンドでインストールします。

$ python setup.py install

 fastBPEは、コーパスからトークン化ルールを学習するためのPython APIを提供していません。シェルコマンドを使ってルールを学び、Python APIを使って例のテキストをエンコードしていきます。この例では、トークン化ルールを学習するために1文のみを使用します。実際の現場では、訓練データ全体、あるいはそのサブセットを使用することになります。

$ cd ~
$ FASTBPE=~/fastBPE/fast
$ echo "とうきょうとっきょきょかきょくにいく" > train.txt
$ $FASTBPE learnbpe 5 train.txt > codes
$ $FASTBPE applybpe train.tok train.txt codes &
$ $FASTBPE getvocab train.tok > vocab
$ cat train.tok
#とう@@ きょ@@ うとっ@@ きょ@@ きょ@@ かきょ@@ くに@@ い@@ く
$ cat vocab
#きょ@@ 3
#うとっ@@ 1
#かきょ@@ 1
#く 1
#とう@@ 1

 最後に、抽出されたcodesとvocabでテキストをトークン化するためにpython APIを使用します。

import fastBPE
bpe = fastBPE.fastBPE('codes', 'vocab')
bpe.apply(["きょういくきょうか"])
#['きょ@@ う@@ い@@ く@@ きょ@@ う@@ か']

SentencePiece

 SentencePieceは、主にテキスト生成タスクのために開発されたトークン化パッケージです。2つのトークン化アルゴリズムを実装しています。

1. BPEアルゴリズム

2. ユニグラム言語モデルに基づくセグメンテーション(ユニグラムLMアルゴリズム)

 ユニグラム言語モデルは、単語の出現確率の積として文や文節の出現確率を計算するモデルです。ユニグラムLMアルゴリズムを用いて、大規模な語彙サイズを望ましいサイズになるまで繰り返し縮小します。各反復において、ユニグラム言語モデルにより語彙セットから除去しても一定の精度が保たれると判断された項目は除去され、語彙セットのサイズがどんどん小さくなっていきます(アルゴリズムの詳細は、こちらをご覧ください)。

 SentencePieceは、テキスト(空白も含む)をUnicode文字の列として扱うため、言語に依存しません。またテキストの空白情報を保持するため、デトークン化が比較的簡単に行えます。さらに、SentencePieceはNFKC(Normalization Form Compatibility Composition:正規化形式KC)ベースの基本的なテキスト正規化も内部で処理します。

 SentencePieceもpipを通じてインストールできます。

$ pip install sentencepiece

 SentencePiece Python APIでは、トークン化モデルの学習、トークン化、デトークン化を全て実行できます。

import sentencepiece as spm
train_txt = "とうきょうとっきょきょかきょくにいく"
with open ('train.txt', 'w+') as data_file
    data_file.write(train_txt + '\n')
spm.SentencePieceTrainer.train(input="train.txt", model_prefix="sp", vocab_size="14")
sp = spm.SentencePieceProcessor(model_file="sp.model")
example = "きょういくきょうか"
tok = sp.encode([example], out_type=str)[0]
print(tok)
#['▁', 'きょ', 'う', 'い', 'く', 'きょ', 'う', 'か']
detok = sp.decode(tok)
print(detok)
#きょういくきょうか

 トークン化が終わると、次はトークン化されたコーパスをそのモデル用の前処理スクリプトに渡します。モデル用の前処理スクリプトの目的は実装によって若干異なりますが、一般的には入力データのバイナリ化と語彙セットの作成する役割を果たします。最後に学習スクリプトを実行して、バイナリ化されたデータからモデルの学習が行われます。

おわりに

 本稿では、自然言語処理モデルの開発などで欠かせないデータラングリングの説明と実装を紹介しました。厳密には、テキストデータで大規模なモデルを学習する際に必要な前処理の工程に当たります。

 ユースケースやタスクによっては、ストップワードリストの作成やレンマ化など、さらなる前処理が必要になる場合があります。そうしたケースでは、公開されている資料や論文も参照してみてください。

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。