以前、scikit-learnのテキスト系の前処理モデルである、CountVectorizer や
TfidfVectorizer において、1文字の単語を学習結果に含める方法の記事を書きました。
参照: scikit-learnでテキストをBoWやtfidfに変換する時に一文字の単語も学習対象に含める
この記事の最後の方で、以下のようなことを書いてました。
これ以外にも “-” (ハイフン) などが単語の境界として設定されていて想定外のところで切られたり、デフォルトでアルファベットを小文字に統一する設定になっていたり(lowercase=True)と、注意する時に気をつけないといけないことが、結構あります。
このうち、ハイフンなどの空白以外のところで単語を切ってしまう問題はワードクラウドで可視化したり単語の出現頻度変化を調べたりする用途の時に非常に厄介に思っていました。機械学習の特徴量を作る時などは最終的な精度にあまり影響しないことが多いので良いのですが。
これを回避するスマートな方法を探していたのですが、それがようやく分かったので紹介します。
前の記事でも行ったように、token_patternの指定で対応を目指したわけですが、以下のコード中の\\\\b の部分をどう調整したら良いのかがわからず苦戦していました。
このbは、\\\\wと\\\\Wの境にマッチするという非常に特殊な正規表現ですが、これをどうすれば空白と\\\\wの間にマッチさせらるのかが分からなかったのです。
# 1文字の単語も学習する設定
bow_model = CountVectorizer(token_pattern='(?u)\\b\\w+\\b')
それがあるとき気づいたのですが、この\\\\b とついでに(?u)は無くても動作変わらないんですよ。
先にそれを紹介しておきます。以前作ったニュースコーパスのデータでやってみます。
参考: livedoorニュースコーパスのファイルをデータフレームにまとめる
import MeCab
import subprocess
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
# データの読み込み
df = pd.read_csv("./livedoor_news_corpus.csv")
# ユニコード正規化とアルファベットの小文字統一
df.text = df.text.str.normalize("NFKC").str.lower()
# 改行コードを取り除く
df.text = df.text.str.replace("\n", " ")
# 辞書のパス取得
dicdir = subprocess.run(["mecab-config", "--dicdir"], capture_output=True, text=True).stdout.strip()
# 今回は品詞による絞り込みも原型への変換も行わないので -Owakati で実行する。
tagger = MeCab.Tagger(f"-Owakati -d {dicdir}/ipadic")
# 分かち書きした結果を返す。
def mecab_tokenizer(text):
# 末尾に改行コードがつくのでstrip()で取り除く
return tagger.parse(text).strip()
# 動作確認
print(mecab_tokenizer("すもももももももものうち"))
# すもも も もも も もも の うち
# 分かち書き
df["tokens"] = df.text.apply(mecab_tokenizer)
# 普段指定している token_pattern で学習
bow_model_1 = CountVectorizer(token_pattern='(?u)\\b\\w+\\b', min_df=10)
bow_model_1.fit(df["tokens"])
# 学習した語彙数
print(len(bow_model_1.vocabulary_))
# 15332
# # token_pattern を \\w+ だけにしたもの
bow_model_2 = CountVectorizer(token_pattern='\\w+', min_df=10)
bow_model_2.fit(df["tokens"])
# 学習した語彙数
print(len(bow_model_2.vocabulary_))
# 15332
語彙数が同じなだけでなく、学習した単語の中身も全く同じです。
# 学習した単語は一致する
set(bow_model_1.get_feature_names()) == set(bow_model_2.get_feature_names())
# True
さて、本題に戻ります。\\\\w+ でこれにマッチする単語が抜き出せるとなれば、単純にスペース以外にマッチする正規表現を書いてあげれればそれで解決です。
“[^ ]+” (ハット”^”と閉じ大括弧”]”の間にスペースを忘れないでください)や、\\\\S+ などを使えばOKです。
それぞれ試しておきます。
bow_model_3 = CountVectorizer(token_pattern='\\S+', min_df=10)
bow_model_3.fit(df["tokens"])
print(len(bow_model_3.vocabulary_))
# 15479
bow_model_4 = CountVectorizer(token_pattern="[^ ]+", min_df=10)
bow_model_4.fit(df["tokens"])
print(len(bow_model_4.vocabulary_))
# 15479
学習した語彙数が増えましたね。
「セ・リーグ」とか「ウォルト・ディズニー」が「・」で区切られずに学習されているのがわかりますよ。
print("セ・リーグ" in bow_model_1.get_feature_names()) # \\w+のモデルでは学習されていない。
# False
print("セ・リーグ" in bow_model_3.get_feature_names()) # \\S+のモデルでは学習されている
# True
print("ウォルト・ディズニー" in bow_model_1.get_feature_names()) # \\w+のモデルでは学習されていない。
# False
print("ウォルト・ディズニー" in bow_model_3.get_feature_names()) # \\S+のモデルでは学習されている
# True
これで、変なところで区切られずにMeCabで切った通りの単語で学習ができました。
ただし、この方法でもデメリットがないわけではありません。
\\\\w+ にはマッチしないが、\\\\S+にはマッチする文字がたくさん存在するのです。
要するに\\\\Wにマッチする文字たちのことです。
「?」や「!」などの感嘆符や「◆」のようなこれまで語彙に含まれなかった文字やそれを含む単語も含まれるようになります。これらが不要だという場合は、分かち書きする前か後に消しておいた方が良いでしょう。
もしくは逆に、不要に切ってほしくない文字は「- (ハイフン)」と「・(中点)」だけなんだ、みたいに特定できているのであれば、token_pattern=”[\\w\-・]+” みたいに指定するのも良いと思います。(ハイフンは正規表現の[]内で使うときは文字の範囲指定を意味する特殊文字なのでエスケープ必須なことに気をつけてください。)
いずれにせよ、結果を慎重に検証しながら使った方が良さそうです。