Google Search Console の キーワードを python sklearn LogisticRegression でクラス分類してカテゴリ分けする


Google Search Console の キーワードを python で カテゴリ 分けしてみた結果を記載します。
今回は、sklearn LogisticRegression ロジスティック回帰 を使ってみます。


前提

Querylabel_name
sonarqubesonarqube
no module namedpython
(1_8.w001) the standalone template_* settings were deprecated in django 1.8django
//nosonarsonarqube
404 エラー404
404エラー404
404エラーページ404

ロジスティック回帰とは

仕事の合間に! 3分間データマイニング入門第3回 マーケティングに活用できるデータマイニング手法 | ネットイヤーグループ株式会社 からの抜粋です。

YESとNOを明確に定義できるものの予測に向いている手法が、発生確率を予測するロジスティック回帰分析です。 商品購入を促すダイレクトメールの送付を例にとってみましょう。DMがきっかけで「購入する」を1(100%)、「購入しない」をゼロ(0%)と定義すると、DM送付者1人単位の購入の確率を算出することができます。確率が高い順番にDMを送付すれば、ランダムに送付するとき以上の効率が上げられます。 ロジスティック回帰分析によく似た解析手法に重回帰分析があります。重回帰分析は、「体重(A)から血圧(B)を予測する」場合など、AもBも連続する数値であることが前提です。 これに対してロジスティック回帰分析は、「体重(A)から高血圧になる確率(B)を予測する」場合など、Bを0と1の間、つまり確率で表示できることが前提となります。

上記を読む限り、2クラス分類はできるということがわかりました。 多クラス分類ができるかというとできるようで、以下のサイトで記載されています。
ロジスティック回帰 - 人工知能に関する断創録

skeatlearn の 例ではなかったので、ないか確認したところ以下サイトが見つかりました。
Lec80-81_多クラス分類
上記が今回参考になりそうなので、参考にして実装していきます。

実装

実装は以下になります。
過去の実装だと、python スクリプトを貼り付けていましたが、jupyter note book を覚えたので、そちらで、実装していきます。
この記事は、jupyter note book で作成したノートを markdown export したものです。

使用する function を定義する

以下、メイン処理で使用する function を定義します。

# ---------------------------------------------------------------------
# 文字列の処理に必要なfunctionの定義
# ------------------------------
from sets import Set
stop_words = Set(['name', 'not', 'the', 'usr', 'you', 'version', 'this'])

# stop word のチェック
# 2文字以下の文字列、クラスタリングした結果、
# ラベルとして、出力されたあまり意味のわからない単語を除外
def __check_stop_word(word):
    if word in stop_words:
        return False
    if len(word) <= 2:
        return False
    return True


# キーワードを区切る
def split_keyword(text):
    keywords = text.split(" ")
    return [keyword for keyword in keywords if __check_stop_word(keyword)]


# ストップワードを除外する
def exclude_stop_words(text):
    return " ".join(split_keyword(text))
/Library/Python/2.7/site-packages/ipykernel_launcher.py:4: DeprecationWarning: the sets module is deprecated
  after removing the cwd from sys.path.

以下、入力TSVファイルの読み込み処理です。 検索キーワードと、学習データの入力フォーマットはファイルの一部抜粋をgistにUPしましたので、ご確認ください。
python sklearn LogisticRegression の入力データとした 学習データと、Search Console の検索キーワードファイルのフォーマット

#-------------------------------------------------------------------
# TSVのカラム値を抜き出すfunction
#------------------------------------------------------
# learning_tsv parseする
def read_from_learning_tsv(index):
    lines = []
    row_count = 1
    for line in open('LearningData.tsv', 'r'):
        if row_count != 1:
            arr = line.split("\t")
            # キーワードカラムを取り出す
            lines.append(arr[index])
        row_count += 1
    return lines

# report tsv を parse する
def parse_report_tsv():
    lines = []
    row_count = 1
    for line in open('ReportData.tsv', 'r'):
        if row_count != 1:
            arr = line.split("\t")
            # キーワードカラムを取り出す
            lines.append(arr[1])
        row_count += 1
    return lines

分類ラベリング処理

処理順序は以下の通りです。

  1. 学習データと、サーチコンソールからの取得結果のキーワードをマージ、キーワード内の単語の出現頻度を数えて、結果を素性ベクトル化する。
  2. LogisticRegression で学習
  3. スコア算出&ラベリング

順に処理を行います。

1.学習データと、サーチコンソールからの取得結果のキーワードをマージ、キーワード内の単語の出現頻度を数えて、結果を素性ベクトル化する。

# ----------------------------------------------------------------------------
# 1. 学習データと、サーチコンソールからの取得結果のキーワードをマージ、キーワード内の単語の出現頻度を数えて、結果を素性ベクトル化する。
# ------------------------------------------------------
# LogisticRegression を import
from sklearn.linear_model import LogisticRegression
logreg = LogisticRegression()

keywords = []
# 学習データからキーワードを取得
for line in read_from_learning_tsv(0):
    keywords.append(exclude_stop_words(line))

#keywordsの出力
keywords
['404',
 'api',
 'bootstrap',
 'brew',
 'centos',
 'cron',
 'django',
 'docker',
 'dropwizard',
 'easybatch',
 'eclipse',
 'eclipselink',
 'elasticsearch',
 'findbugs',
 'flyway',
 'git',
 'google analytics api',
 'gradle',
 'infer',
 'intellij',
 'jackson',
 'java',
 'jetty',
 'lambok',
 'linux',
 'lombok',
 'maven',
 'memcached',
 'mezzanine',
 'mongodb',
 'nvd3',
 'openscap',
 'owasp',
 'pmd',
 'postgres',
 'pycharm',
 'pylint',
 'python',
 'querydsl',
 'redpen',
 'rundeck',
 'sonarqube',
 'spring-boot',
 ...,
 'brew link',
 ...,
 '\xef\xbd\x82\xef\xbd\x92\xef\xbd\x85\xef\xbd\x97',
 'could find executable `gauge_home`, `path` `gauge_root`',
 'want have current date default, use `django.utils.timezone.now`']
#  実データからキーワードを取得
for line in parse_report_tsv():
    keywords.append(exclude_stop_words(line))
keywords
['404',
 'api',
 'bootstrap',
 'brew',
 'centos',
 'cron',
 'django',
 'docker',
 'dropwizard',
 'easybatch',
 'eclipse',
 'eclipselink',
 'elasticsearch',
 'findbugs',
 'flyway',
 'git',
 'google analytics api',
 'gradle',
 'infer',
 'intellij',
 'jackson',
 'java',
 'jetty',
 'lambok',
 'linux',
 'lombok',
 'maven',
 'memcached',
 'mezzanine',
 'mongodb',
 'nvd3',
 'openscap',
 'owasp',
 'pmd',
 'postgres',
 'pycharm',
 'pylint',
 'python',
 ...,
 'wsgirequest\n',
 '\xe3\x82\xaf\xe3\x82\xa8\xe3\x83\xaa\xe3\x82\xb9\xe3\x83\x88\xe3\x83\xaa\xe3\x83\xb3\xe3\x82\xb0\n',
 ...]

キーワードを読み込んだ後に、素性ベクトル化します。
ベクトル化には、CountVectorizer を使用します。

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np

# テキスト内の単語の出現頻度を数えて、結果を素性ベクトル化する(Bag of words)
count_vectorizer = CountVectorizer()
# csr_matrix(疎行列)が返る
feature_vectors = count_vectorizer.fit_transform(keywords)
# 学習したデータのみ切り出し
learning_vectors = feature_vectors[:len(read_from_learning_tsv(0))]
# データに対応したラベルを取得
learning_labels = np.array(read_from_learning_tsv(1))

learning_vectors
<940x2303 sparse matrix of type '<type 'numpy.int64'>'
    with 2650 stored elements in Compressed Sparse Row format>
learning_labels
array(['404\n', 'api\n', 'bootstrap\n', 'brew\n', 'centos\n', 'cron\n',
       'django\n', 'docker\n', 'dropwizard\n', 'easybatch\n', 'eclipse\n',
       'eclipselink\n', 'elasticsearch\n', 'findbugs\n', 'flyway\n',
       'git\n', 'google analytics api\n', 'gradle\n', 'infer\n',
       'intellij\n', 'jackson\n', 'java\n', 'jetty\n', 'lambok\n',
       'linux\n', 'lombok\n', 'maven\n', 'memcached\n', 'mezzanine\n',
       'mongodb\n', 'nvd3\n', 'openscap\n', 'owasp\n', 'pmd\n',
       'postgres\n', 'pycharm\n', 'pylint\n', 'python\n', 'querydsl\n',
       'redpen\n', 'rundeck\n', 'sonarqube\n', 'spring-boot\n',
       'VIP\xe7\x9b\xae\xe9\xbb\x92\n', 'webfonts\n', 'wecker\n',
       'wicket\n',
       '\xe6\x8a\x80\xe8\xa1\x93\xe7\x9a\x84\xe8\xb2\xa0\xe5\x82\xb5\n',
       '\xe6\xad\xaf\xe7\x9f\xb3\n',
       '\xe7\x84\xa1\xe6\xb0\xb4\xe9\x8d\x8b\n', 'python\n', 'django\n',
       ...,
       '\xe6\x8a\x80\xe8\xa1\x93\xe7\x9a\x84\xe8\xb2\xa0\xe5\x82\xb5\n',
       'brew\n', 'intellij\n', 'django'],
      dtype='|S21')

LogisticRegression で学習

sklearn で ロジスティック回帰を行う場合は、LogisticRegression を使用します。
学習自体は、fit で行えます。

from sklearn.linear_model import LogisticRegression

# 訓練
clf = LogisticRegression()
# 第一引数が、行列で、第二引数がラベル
clf.fit(learning_vectors, learning_labels)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)
clf.score(learning_vectors,learning_labels)
0.95851063829787231

スコアは1.0 にはならず。学習データの数900近くあり、多すぎるのかもしれません。
完全に分類できているというのが少し怪しく思いましたが、学習データなので、このまま続けます。

scores = clf.predict_proba(feature_vectors[len(read_from_learning_tsv(0)):])
scores
array([[ 0.0072083 ,  0.00733393,  0.01281034, ...,  0.00729888,
         0.00853825,  0.00572039],
       [ 0.0108284 ,  0.01104879,  0.02168211, ...,  0.01098717,
         0.01321534,  0.00829743],
       [ 0.00546877,  0.00554397,  0.00793214, ...,  0.00552303,
         0.00624621,  0.0045466 ],
       ..., 
       [ 0.00923513,  0.00939803,  0.01667396, ...,  0.0093525 ,
         0.0109664 ,  0.00730852],
       [ 0.01520893,  0.01553761,  0.03245924, ...,  0.01544563,
         0.01881058,  0.01149057],
       [ 0.01565687,  0.0159937 ,  0.0330284 , ...,  0.01589945,
         0.01934464,  0.01184186]])
clf.classes_
array(['404\n', 'VIP\xe7\x9b\xae\xe9\xbb\x92\n', 'api\n', 'bootstrap\n',
       'brew\n', 'centos\n', 'cron\n', 'django', 'django\n', 'docker\n',
       'dropwizard\n', 'easybatch\n', 'eclipse\n', 'eclipselink\n',
       'elasticsearch\n', 'findbugs\n', 'flyway\n', 'git\n',
       'google analytics api\n', 'gradle\n', 'infer\n', 'intellij\n',
       'jackson\n', 'java\n', 'jetty\n', 'lambok\n', 'linux\n', 'lombok\n',
       'maven\n', 'memcached\n', 'mezzanine\n', 'mongodb\n', 'nvd3\n',
       'openscap\n', 'owasp\n', 'pmd\n', 'postgres\n', 'pycharm\n',
       'pylint\n', 'python\n', 'querydsl\n', 'redpen\n', 'rundeck\n',
       'sonarqube\n', 'spring-boot\n', 'webfonts\n', 'wecker\n',
       'wicket\n',
       '\xe6\x8a\x80\xe8\xa1\x93\xe7\x9a\x84\xe8\xb2\xa0\xe5\x82\xb5\n',
       '\xe6\xad\xaf\xe7\x9f\xb3\n',
       '\xe7\x84\xa1\xe6\xb0\xb4\xe9\x8d\x8b\n'],
      dtype='|S21')

分類

predict_proba で ラベル属する確率が取得できます。
ドキュメントを読む限りだと、LogisticRegression は、決定境界からの距離を返す decision_function関数も使えます。

predict_proba で確率からラベルを決定する

def __get_max_label(array, score):
    index = 0
    max_value = 0
    for i in range(len(array)):
        item = array[i]
        if max_value < item:
            max_value = item
            index = i
    return {score[index]: max_value}

scores = clf.predict_proba(feature_vectors[len(read_from_learning_tsv(0)):])
labels = []
classes = clf.classes_
for score in scores:
    max_dict = __get_max_label(score, classes)
    for k, v in max_dict.items():
        if v > 0.50:
            # 0.50 より大きい場合、ラベルを設定
            labels.append(k)
        else:
            # 上記以外の場合は、"unknown"
            labels.append("unknown")
labels
['pycharm\n',
 'unknown',
 'python\n',
 'unknown',
 'unknown',
 'python\n',
 ....,
 'pycharm\n',
 ...]
max_scores
[0.5972620299284529,
 0.3383567214750145,
 0.68749652202323097,
 0.2940551988600052,
 0.064129770355348184,
 0.62262596966624917,
 0.68749652202323097,
 0.39244242700503712,
 0.5972620299284529,
 ...,
 0.5972620299284529,
 ...]
from collections import Counter
counter = Counter(labels)
counter
Counter({'api\n': 327,
         'bootstrap\n': 210,
         'brew\n': 576,
         'django': 13,
         'django\n': 1969,
         'dropwizard\n': 7,
         'eclipselink\n': 105,
         'elasticsearch\n': 312,
         'findbugs\n': 31,
         'google analytics api\n': 9,
         'gradle\n': 46,
         'intellij\n': 2,
         'jackson\n': 634,
         'java\n': 30,
         'linux\n': 93,
         'maven\n': 41,
         'mezzanine\n': 74,
         'mongodb\n': 198,
         'pmd\n': 94,
         'postgres\n': 40,
         'pycharm\n': 460,
         'python\n': 889,
         'querydsl\n': 77,
         'rundeck\n': 11,
         'sonarqube\n': 652,
         'unknown': 9002,
         'wicket\n': 682})

半数は、unknown に分類されています。

decision_function 関数を実行してみる

clf.decision_function(feature_vectors[len(read_from_learning_tsv(0)):])
array([[-4.60120198, -4.58374887, -4.01834957, ..., -4.58858833,
        -4.43002503, -4.83446962],
       [-4.41044913, -4.39005257, -3.70388085, ..., -4.39571457,
        -4.20856081, -4.67951099],
       [-4.93822167, -4.9244657 , -4.56311987, ..., -4.92827896,
        -4.80428225, -5.12410495],
       ..., 
       [-4.58909269, -4.57142767, -3.99003962, ..., -4.57633428,
        -4.4153638 , -4.8251841 ],
       [-4.34944285, -4.3277831 , -3.57658474, ..., -4.33379832,
        -4.13384321, -4.63295136],
       [-4.35313569, -4.33157371, -3.59229901, ..., -4.33756156,
        -4.13859456, -4.63553464]])

値が返ってきます。個人的には確率のほうがわかりやすいので、predict_proba が使えるならそちらを使おうかなという気持ちです。

LogisticRegression で学習 (Cを変更する)

LogisticRegression にパラメータ C を与えると、分類精度が変わるようなので、試しに実行してみます。
python sklearn でロジスティック回帰。fit して predict、引数 C で正則化 | コード7区

from sklearn.linear_model import LogisticRegression

# 訓練
clf = LogisticRegression(C=100)
# 第一引数が、行列で、第二引数がラベル
clf.fit(learning_vectors, learning_labels)
LogisticRegression(C=100, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)
scores = clf.predict_proba(feature_vectors[len(read_from_learning_tsv(0)):])
scores
array([[  3.92285260e-04,   4.30773733e-04,   1.03282309e-03, ...,
          3.97506780e-04,   4.15491513e-04,   2.20233853e-04],
       [  6.92312831e-04,   7.86450652e-04,   2.58540527e-03, ...,
          7.04069206e-04,   7.48335417e-04,   3.19176940e-04],
       [  2.02331944e-04,   2.17160118e-04,   3.35685910e-04, ...,
          2.04163691e-04,   2.11255775e-04,   1.31034168e-04],
       ..., 
       [  4.12031300e-04,   4.52898646e-04,   1.05029520e-03, ...,
          4.17132918e-04,   4.36528567e-04,   2.26749311e-04],
       [  1.32888692e-02,   1.59165270e-02,   1.79309604e-01, ...,
          1.36033269e-02,   1.48212636e-02,   4.98320204e-03],
       [  1.83712824e-02,   2.19693381e-02,   2.09757293e-01, ...,
          1.88044747e-02,   2.04704374e-02,   6.95727218e-03]])
def __get_max_label(array, score):
    index = 0
    max_value = 0
    for i in range(len(array)):
        item = array[i]
        if max_value < item:
            max_value = item
            index = i
    return {score[index]: max_value}

scores = clf.predict_proba(feature_vectors[len(read_from_learning_tsv(0)):])
labels = []
for score in scores:
    max_dict = __get_max_label(score, classes)
    for k, v in max_dict.items():
        if v > 0.50:
            # 0.50 より大きい場合、ラベルを設定
            labels.append(k)
        else:
            # 上記以外の場合は、"unknown"
            labels.append("unknown")
labels
['pycharm\n',
 'memcached\n',
 'python\n',
 ...,
 'pycharm\n',
 ...]
from collections import Counter
counter = Counter(labels)
counter
Counter({'404\n': 78,
         'VIP\xe7\x9b\xae\xe9\xbb\x92\n': 51,
         'api\n': 593,
         'bootstrap\n': 244,
         'brew\n': 652,
         'centos\n': 21,
         'cron\n': 4,
         'django': 16,
         'django\n': 2249,
         'docker\n': 3,
         'dropwizard\n': 92,
         'easybatch\n': 35,
         'eclipse\n': 10,
         'eclipselink\n': 150,
         'elasticsearch\n': 340,
         'findbugs\n': 160,
         'flyway\n': 24,
         'git\n': 14,
         'google analytics api\n': 129,
         'gradle\n': 213,
         'infer\n': 12,
         'intellij\n': 129,
         'jackson\n': 964,
         'java\n': 698,
         'jetty\n': 27,
         'lambok\n': 2,
         'linux\n': 161,
         'lombok\n': 29,
         'maven\n': 121,
         'memcached\n': 133,
         'mezzanine\n': 113,
         'mongodb\n': 304,
         'nvd3\n': 24,
         'openscap\n': 7,
         'owasp\n': 8,
         'pmd\n': 143,
         'postgres\n': 230,
         'pycharm\n': 500,
         'pylint\n': 5,
         'python\n': 1212,
         'querydsl\n': 151,
         'redpen\n': 6,
         'rundeck\n': 81,
         'sonarqube\n': 744,
         'spring-boot\n': 44,
         'unknown': 4583,
         'webfonts\n': 28,
         'wecker\n': 4,
         'wicket\n': 936,
         '\xe6\x8a\x80\xe8\xa1\x93\xe7\x9a\x84\xe8\xb2\xa0\xe5\x82\xb5\n': 32,
         '\xe6\xad\xaf\xe7\x9f\xb3\n': 52,
         '\xe7\x84\xa1\xe6\xb0\xb4\xe9\x8d\x8b\n': 23})

C = 1.0 の場合に比べて、ばらつきがでるようになりました。
unknown に分類されているデータが減っています。


まとめ

Google Search Console から取得したキーワードを、sklearn LogisticRegression ロジスティック回帰 でラベリングをしてみました。
以下、まとめます。

  • Cの値を大きくすると、正規化が弱くなり、ラベリングにばらつきがでた。 C=1.0 > 正規化が強い。 C=100 > 正規化が弱い。
  • C以外にも指定できるパラメータはあるので、チューニングの余地は途方もなくなる。
  • ぱっとみの分類精度は、ナイーブベイブで分類したときのほうが良さげ。

以上です。

コメント