scikit-learnでテキストをBoWやtfidfに変換する時に一文字の単語も学習対象に含める

本当に初めて自然言語処理をやった頃のメモから記事化。

テキストを分かち書きしたあと、BoW(Bag of Words) や tfidfに変換するとき、
scikit-learnを使うと便利です。
sklearn.feature_extraction.text に次の二つのクラスが定義されていて、
それぞれ語彙の学習と BoW /tfidfへの変換を行ってくれます。

CountVectorizer
TfidfVectorizer

ただ、これらのクラスはデフォルトパラメーターに少し癖があり注意していないと一文字の単語を拾ってくれません。
TfidfVectorizer の方を例にやってみましょう。


from sklearn.feature_extraction.text import TfidfVectorizer

model = TfidfVectorizer()
text_list = [
    "すもも も もも も もも の うち",
    "隣 の 客 は よく 柿 食う 客 だ",
]
model.fit(text_list)
print(model.vocabulary_)

これの出力結果が、下記になります。
これらが学習した単語です。
{‘すもも’: 1, ‘もも’: 2, ‘うち’: 0, ‘よく’: 3, ‘食う’: 4}

“も” や “の” が入ってないのはまだ許容範囲としても、
“隣”や”客”がvocabulary_に含まれないのは困ります。

これらは TfidfVectorizer のインスタンスをデフォルトのパラメーターで作ったことに起因します。

modelの内容を表示してみましょう。(jupyterで model とだけ入力して実行した結果)


>>> model
TfidfVectorizer(analyzer='word', binary=False, decode_error='strict',
        dtype=, encoding='utf-8', input='content',
        lowercase=True, max_df=1.0, max_features=None, min_df=1,
        ngram_range=(1, 1), norm='l2', preprocessor=None, smooth_idf=True,
        stop_words=None, strip_accents=None, sublinear_tf=False,
        token_pattern='(?u)\\b\\w\\w+\\b', tokenizer=None, use_idf=True,
        vocabulary=None)

注目するべきはここです。
token_pattern='(?u)\\b\\w\\w+\\b’
\\ は \をエスケープしたものなので、以下の説明中では\\ は \と書きます。
正規表現で、\b は単語の境界 、 \wは、単語構成文字、\w+ は1文字以上の単語構成文字の連続 を意味します。
そのため、 \w\w+ は2文字以上の単語構成文字の連続を意味し、
\b\w\w+\b は “単語の境界、2文字以上の単語構成文字、単語の境界” と続く文字列を単語のパターンとして採用するという指定になります。
そのため、 一文字の単語は学習対象から抜けています。

英語なら a や I が抜けるだけですが日本語では多くの漢字が抜けてしまうので、これは困りますね。

ということで、最初に TfidfVectorizer のオブジェクトを作る時には token_pattern を指定しましょう。
1文字以上を含めたいだけなので、 ‘(?u)\\b\\w+\\b’ にすれば大丈夫です。


from sklearn.feature_extraction.text import TfidfVectorizer

model = TfidfVectorizer(token_pattern='(?u)\\b\\w+\\b')
text_list = [
    "すもも も もも も もも の うち",
    "隣 の 客 は よく 柿 食う 客 だ",
]
model.fit(text_list)
print(model.vocabulary_)

これで出力は下記のようりなり、”隣”も”客”も含まれます。
{‘すもも’: 1, ‘も’: 5, ‘もも’: 6, ‘の’: 3, ‘うち’: 0, ‘隣’: 10, ‘客’: 8, ‘は’: 4, ‘よく’: 7, ‘柿’: 9, ‘食う’: 11, ‘だ’: 2}

これ以外にも “-” (ハイフン) などが単語の境界として設定されていて想定外のところで切られたり、
デフォルトでアルファベットを小文字に統一する設定になっていたり(lowercase=True)と、注意する時に気をつけないといけないことが、結構あります。

norm,smooth_idf,sublinear_tf などの影響でよくある自然言語処理の教科書に載っている数式と実装が違うのも注意ですね。
この辺はまた別の機会にまとめようと思います。

scikit-learnのtrain_test_splitで、訓練データとテストデータのラベルの割合を揃える

自分の場合なのですが、普段の業務で機械学習を行う場合不均衡データを扱うことが非常に多くあります。
ラベルづけされたデータを train_test_split で訓練データとテストデータに分けるとき、
運が悪いと訓練データとテストデータで、ラベルの割合がずいぶん変わってしまうことがありました。


#  全データのラベルの割合は 99:1
df['label'].value_counts()
'''
0    9900
1     100
'''
# データの2割りをテストデータにする
df_train, df_test = train_test_split(df, test_size=0.2)

# 訓練データでは ラベル1 は 0.9625 %
df_train.label.value_counts() / len(df_train)
```
0    0.990375
1    0.009625
```
# テストデータでは ラベル1 は 1.15%
df_test.label.value_counts() / len(df_test)
```
0    0.9885
1    0.0115
```

この例ではまだ許容範囲かなという気もしますが運が悪いとかなりの差が開きます。

そこで、かつてはデータフレームをラベルごとに分けてから個別に訓練用とテスト用に分けて、
それをマージして訓練データとテストデータを作ると言った面倒なことをやっていたことがあります。

その後、 train_test_split のマニュアルを読んでいたら非常に便利な引数があることがわかりました。

stratify に、割合を揃えたい列を指定してあげると、訓練データとテストデータで同じ割合になるように分けてくれます。


#  全データのラベルの割合は 99:1
df['label'].value_counts()
'''
0    9900
1     100
'''
# データの2割をテストデータにする
df_train, df_test = train_test_split(df, test_size=0.2, stratify=df.label)

df_train.label.value_counts() / len(df_train)
```
0    0.99
1    0.01
```
df_test.label.value_counts() / len(df_test)
```
0    0.99
1    0.01
```

綺麗に分かれました。

scikit-learn でグリッドサーチ

機械学習のハイパーパラメーターを決定するとき、グリッドサーチという手法を使うことがあります。
よほど学習時にかかるケース以外では、ほぼ確実に行なっています。

そのとき、scikit-learn の GridSearchCV というクラスを使うことが多いのでその使い方をメモしておきます。
今回は題材として、 digits という手書き数字のデータセットを利用します。


from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

最初にデータを準備します。
# データの読み込み
digits = load_digits()
X = digits.data
y = digits.target
# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

次にサーチするパラメーターを指定します。


# グリッドサーチするパラメーターを指定。変数名と値のリストの辞書 、それが複数ある場合はそれらの配列。
param_grid = [
    {
        'C': [1, 10, 100, 1000],
        'kernel': ['linear']
    },
    {
        'C': [0.1, 1, 10, 100, 1000],
        'kernel': ['rbf'],
        'gamma': [0.001, 0.0001, 'auto']
    },
    {
        'C': [0.1, 1, 10, 100, 1000],
        'kernel': ['poly'], 'degree': [2, 3, 4],
        'gamma': [0.001, 0.0001, 'auto']
    },
    {
        'C': [0.1, 1, 10, 100, 1000],
        'kernel':['sigmoid'],
        'gamma': [0.001, 0.0001, 'auto']
    }
]

モデルを作って、グリッドサーチの実行


from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC

# モデルの準備
model = GridSearchCV(
    SVC(),  # 予測機
    param_grid,  # サーチ対象のパラメーター
    cv=5,  # 交差検証の数
    # このほか、評価指標(scoring) や、パラレル実行するJob数なども指定可能(n_jobs)
)
# グリッドサーチの実行
model.fit(X_train, y_train)

最良のパラメーターを確認する


print(model.best_params_)

# 出力
{'C': 1, 'gamma': 0.001, 'kernel': 'rbf'}

最後に、テスト用に取っておいたデータで、出来上がったモデルを評価します。


from sklearn.metrics import classification_report

# 学習したモデルで予測
y_predict = model.predict(X_test)
# 作成したモデルの評価
print(classification_report(y_test, y_predict))

# 出力
             precision    recall  f1-score   support

          0       1.00      1.00      1.00        38
          1       0.97      1.00      0.99        34
          2       1.00      1.00      1.00        38
          3       0.97      1.00      0.99        34
          4       1.00      1.00      1.00        36
          5       1.00      0.97      0.99        35
          6       0.97      0.97      0.97        39
          7       0.97      1.00      0.98        31
          8       0.97      0.97      0.97        38
          9       1.00      0.95      0.97        37

avg / total       0.99      0.99      0.99       360

なかなか良い正解率ですね。