ステップ 3: データを準備する

データをモデルに供給する前に、モデルが理解できる形式に変換する必要があります。

まず、収集したデータサンプルは特定の順序の可能性があります。テキストとラベルの関係に影響を与えるために、サンプルの順序付けに関連する情報があってはなりません。たとえば、データセットをクラス別に並べ替え、トレーニング/検証セットに分割した場合、これらのデータセットはデータの全体的な分布を表すものではありません。

モデルがデータ順序の影響を受けないようにするための簡単なベスト プラクティスは、他の作業を行う前に必ずデータをシャッフルすることです。データがすでにトレーニング セットと検証セットに分割されている場合は、トレーニング データを変換するのと同じ方法で検証データを変換してください。個別のトレーニング セットと検証セットがまだない場合は、シャッフル後にサンプルを分割できます。通常は、サンプルの 80% がトレーニングに、20% が検証に使用されます。

次に、機械学習アルゴリズムは入力として数値を取ります。つまり、テキストを数値ベクトルに変換する必要があります。このプロセスは 2 つのステップで行います。

  1. トークン化: テキストを単語または小さなサブテキストに分割します。これにより、テキストとラベルの関係を適切に一般化できます。これにより、データセットの「語彙」(データ内に存在する一意のトークンのセット)が決まります。

  2. ベクトル化: これらのテキストの特徴を表す適切な数値メジャーを定義します。

n グラムベクトルとシーケンス ベクトルの両方に対してこの 2 つの手順を実行する方法と、特徴選択と正規化の手法でベクトル表現を最適化する方法を見ていきましょう。

N-gram ベクトル [オプション A]

以降の段落では、n-gram モデルのトークン化とベクトル化を行う方法について説明します。また、特徴選択と正規化の手法を使用して n グラム表現を最適化する方法についても説明します。

n グラムベクトルでは、テキストは一意の n グラムのコレクションとして表されます。これは、n 個の隣接するトークン(通常は単語)のグループです。テキスト The mouse ran up the clock について考えてみましょう。ここで、ユニグラム(n = 1)という単語は ['the', 'mouse', 'ran', 'up', 'clock']、単語のグラムグラム(n = 2)は ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock'] というようになっています。

トークン化

単語のユニグラムとバイグラムをトークン化することで、計算時間を減らしながら精度を向上させることができます。

ベクトル化

テキスト サンプルを n グラムに分割したら、その n グラムを機械学習モデルが処理できる数値ベクトルに変換する必要があります。次の例は、2 つのテキストに対して生成されたユニグラムとバイグラムに割り当てられたインデックスを示しています。

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'the': 7, 'mouse': 2, 'ran': 4, 'up': 10,
  'clock': 0, 'the mouse': 9, 'mouse ran': 3, 'ran up': 6, 'up the': 11, 'the
clock': 8, 'down': 1, 'ran down': 5}

インデックスを n グラムに割り当てると、通常は次のいずれかのオプションを使用してベクトル化します。

ワンホット エンコード: すべてのサンプル テキストは、テキスト内のトークンの有無を示すベクトルとして表されます。

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]

数エンコード: すべてのサンプル テキストは、テキスト内のトークンの数を示すベクトルとして表されます。ユニグラムを表す 'the'(太字)は、テキストに「the」という単語が 2 回出現するため、2 として表されることに注意してください。

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1]

Tf-idf エンコード: 上記 2 つのアプローチの問題は、すべてのドキュメントで同様の頻度で発生する一般的な単語(データセット内のテキスト サンプルに対して特に一意ではない単語)にペナルティがないことです。たとえば、「a」のような単語は、すべてのテキストで頻繁に発生します。そのため、意味のある単語よりも「the」のトークン数が多くても、あまり意味がありません。

'The mouse ran up the clock' = [0.33, 0, 0.23, 0.23, 0.23, 0, 0.33, 0.47, 0.33,
0.23, 0.33, 0.33] (See Scikit-learn TfidfTransformer)

その他多くのベクトル表現がありますが、上記の 3 つは最もよく使用されています。

tf-idf エンコードは、精度の面で他の 2 つよりわずかに優れており(平均: 0.25 ~ 15% 高い)、n グラムのベクトル化にはこの方法を使用することをおすすめします。ただし、浮動小数点数表記を使用するため、メモリが占有され、計算に時間がかかります(特に大規模なデータセットの場合)。2 倍の時間がかかることもあります)。

特徴選択

データセット内のすべてのテキストを単語 uni+bigram トークンに変換すると、最終的に数万件のトークンになる場合があります。これらのトークン/特徴量がすべてラベルの予測に役立つわけではありません。したがって、データセット全体で極めてまれに発生するトークンなど、特定のトークンを削除できます。また、特徴量の重要度(各トークンがラベルの予測に寄与する程度)を測定し、最も有用なトークンのみを含めることもできます。

特徴とそれに対応するラベルを受け取って、特徴重要度スコアを出力する多くの統計関数があります。よく使用される 2 つの関数は f_classifchi2 です。Google が行ったテストの結果、どちらの機能も同等のパフォーマンスが得られることがわかっています。

さらに重要な点は、多くのデータセットで約 20,000 の特徴で精度がピークに達することです(図 6 を参照)。このしきい値を超える特徴の追加は、ほとんど起こらず、場合によっては過学習やパフォーマンスの低下にもつながります。

上位 K と精度

図 6: 上位 K の特徴と精度データセット全体では、約 2 万の特徴で精度が落ちています。

Normalization

正規化は、すべての特徴/サンプル値を小さな類似値に変換します。これにより、学習アルゴリズムでの勾配降下法の収束が簡単になります。前述のように、データの前処理中の正規化は、テキスト分類の問題に大きな価値をもたらさないようです。この手順はスキップすることをおすすめします。

次のコードは、上記の手順をすべてまとめたものです。

  • テキスト サンプルを単語の uni+bigrams にトークン化する
  • tf-idf エンコードを使用してベクトル化する。
  • トークンのベクトルから上位 20,000 個の特徴のみを選択します。これは、出現回数が 2 回未満のトークンを破棄し、f_classif を使用して特徴の重要度を計算します。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import f_classif

# Vectorization parameters
# Range (inclusive) of n-gram sizes for tokenizing text.
NGRAM_RANGE = (1, 2)

# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Whether text should be split into word or character n-grams.
# One of 'word', 'char'.
TOKEN_MODE = 'word'

# Minimum document/corpus frequency below which a token will be discarded.
MIN_DOCUMENT_FREQUENCY = 2

def ngram_vectorize(train_texts, train_labels, val_texts):
    """Vectorizes texts as n-gram vectors.

    1 text = 1 tf-idf vector the length of vocabulary of unigrams + bigrams.

    # Arguments
        train_texts: list, training text strings.
        train_labels: np.ndarray, training labels.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val: vectorized training and validation texts
    """
    # Create keyword arguments to pass to the 'tf-idf' vectorizer.
    kwargs = {
            'ngram_range': NGRAM_RANGE,  # Use 1-grams + 2-grams.
            'dtype': 'int32',
            'strip_accents': 'unicode',
            'decode_error': 'replace',
            'analyzer': TOKEN_MODE,  # Split text into word tokens.
            'min_df': MIN_DOCUMENT_FREQUENCY,
    }
    vectorizer = TfidfVectorizer(**kwargs)

    # Learn vocabulary from training texts and vectorize training texts.
    x_train = vectorizer.fit_transform(train_texts)

    # Vectorize validation texts.
    x_val = vectorizer.transform(val_texts)

    # Select top 'k' of the vectorized features.
    selector = SelectKBest(f_classif, k=min(TOP_K, x_train.shape[1]))
    selector.fit(x_train, train_labels)
    x_train = selector.transform(x_train).astype('float32')
    x_val = selector.transform(x_val).astype('float32')
    return x_train, x_val

n グラム表現表現では、単語の順序と文法に関する多くの情報を破棄します(最大でも n > 1 のとき、部分的な並べ替え情報を維持できます)。これをバッグ オブ ワード アプローチと呼びます。この表現は、ロジスティック回帰、多層知覚、勾配ブースティング マシン、サポート ベクター マシンなど、順序を考慮していないモデルと組み合わせて使用されます。

シーケンス ベクトル [オプション B]

以降の段落では、シーケンス モデルのトークン化とベクトル化を行う方法について説明します。また、特徴選択と正規化の手法を使用してシーケンス表現を最適化する方法についても説明します。

一部のテキスト サンプルでは、テキストの意味にとって語順が極めて重要です。たとえば、「通勤が嫌いだったので、新しい自転車は完全に改ざんされました」と認識されるのは、順番に読む場合のみです。CNN や RNN などのモデルは、サンプル内の単語の順序から意味を推測できます。これらのモデルでは、テキストを順序付きのトークンのシーケンスとして表します。

トークン化

テキストは、文字のシーケンスまたは単語のシーケンスで表すことができます。単語レベルの表現を使用すると、文字トークンよりもパフォーマンスが向上することが確認されています。これは一般的な業界基準でもあります。文字トークンを使用することは、テキストに入力ミスが多い場合にのみ意味があります(通常、そうではありません)。

ベクトル化

テキスト サンプルを単語のシーケンスに変換したら、これらのシーケンスを数値ベクトルに変換する必要があります。下の例は、2 つのテキストについて生成されたユニグラムに割り当てられたインデックスと、最初のテキストが変換されるトークン インデックスのシーケンスを示しています。

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}.
NOTE: 'the' occurs most frequently, so the index value of 1 is assigned to it.
Some libraries reserve index 0 for unknown tokens, as is the case here.
Sequence of token indexes: 'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]

トークン シーケンスのベクトル化には次の 2 つのオプションがあります。

ワンホット エンコード: シーケンスは n 次元空間における単語ベクトルを使用して表現されます。n = 語彙のサイズです。この表現は、文字としてトークン化しているとうまく機能するため、語彙は小さくなります。単語としてトークン化する場合、語彙には通常数万件のトークンが含まれるため、ワンホット ベクトルは非常にスパースで非効率です。例:

'The mouse ran up the clock' = [
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0]
]

単語の埋め込み: 単語には意味が関連付けられています。その結果、単語のトークンが密集したベクトル空間(約 200 個の実数)で表現され、単語間の位置と距離が意味的に類似していることがわかります(図 7 を参照)。この表現は、単語の埋め込みと呼ばれます。

単語の埋め込み

図 7: 単語の埋め込み

多くの場合、シーケンス モデルでは、最初のレイヤとして埋め込みレイヤがあります。このレイヤは、トレーニング プロセス中に単語インデックスのシーケンスを単語埋め込みベクトルに変換し、各単語インデックスがセマンティック空間での単語の位置を表す実際の値の密なベクトルにマッピングされるように学習します(図 8 を参照)。

埋め込みレイヤ

図 8: 埋め込みレイヤ

特徴選択

データ内のすべての単語がラベルの予測に影響するわけではありません。語彙から珍しい単語や関連性のない単語を破棄することで、学習プロセスを最適化できます。実際には、頻度の高い 20,000 の特徴で十分であることがわかります。これは n グラムモデルにも当てはまります(図 6 を参照)。

上記の手順をすべてシーケンスのベクトル化で実現しましょう。次のコードは、これらのタスクを実行します。

  • テキストを単語にトークン化する
  • 上位 20,000 件のトークンを使用して語彙を作成
  • トークンをシーケンス ベクトルに変換します。
  • 固定されたシーケンス長までシーケンスをパディングします
from tensorflow.python.keras.preprocessing import sequence
from tensorflow.python.keras.preprocessing import text

# Vectorization parameters
# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Limit on the length of text sequences. Sequences longer than this
# will be truncated.
MAX_SEQUENCE_LENGTH = 500

def sequence_vectorize(train_texts, val_texts):
    """Vectorizes texts as sequence vectors.

    1 text = 1 sequence vector with fixed length.

    # Arguments
        train_texts: list, training text strings.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val, word_index: vectorized training and validation
            texts and word index dictionary.
    """
    # Create vocabulary with training texts.
    tokenizer = text.Tokenizer(num_words=TOP_K)
    tokenizer.fit_on_texts(train_texts)

    # Vectorize training and validation texts.
    x_train = tokenizer.texts_to_sequences(train_texts)
    x_val = tokenizer.texts_to_sequences(val_texts)

    # Get max sequence length.
    max_length = len(max(x_train, key=len))
    if max_length > MAX_SEQUENCE_LENGTH:
        max_length = MAX_SEQUENCE_LENGTH

    # Fix sequence length to max value. Sequences shorter than the length are
    # padded in the beginning and sequences longer are truncated
    # at the beginning.
    x_train = sequence.pad_sequences(x_train, maxlen=max_length)
    x_val = sequence.pad_sequences(x_val, maxlen=max_length)
    return x_train, x_val, tokenizer.word_index

ラベルのベクトル化

サンプル テキストデータを数値ベクトルに変換する方法を学びました。ラベルに同様のプロセスを適用する必要があります。ラベルは [0, num_classes - 1] の範囲の値に変換するだけです。たとえば 3 つのクラスがある場合、値 0、1、2 を使用してクラスを表すことができます。内部的には、ネットワークはワンホット ベクトルを使用してこれらの値を表します(ラベル間の誤った関係を推測することを避けるため)。この表現は、損失関数とニューラル ネットワークで使用する最後のレイヤの活性化関数に依存します。次のセクションで詳しく説明します。