u++の備忘録

勾配ブースティング決定木を用いたマーケティング施策の選定

はじめに

今回は、勾配ブースティング決定木(Gradient Boosting Decision Tree, GBDT)を用いて、マーケティング施策を選定する枠組みについて解説します。具体的には、説明変数 X=[x_0, x_1, ..., x_{n-1}]から目的変数 yを予測するモデルを構築し、各説明変数の重要度を算出することで、どの説明変数がマーケティング施策の対象になり得るかを検討します。

例えば Xとして製品のステータス、 yを製品の打ち上げとすると、製品のステータスのうち、どの要素が売上に貢献しているか示唆する情報が得られます。この情報を利用することで「どの要素に注力して売り出すか」「どの要素に注力して改善を目指すか」など、適切な施策の選定につながります。

勾配ブースティング決定木とは

勾配ブースティング決定木は、単純な「決定木」というモデルを拡張した、高精度かつ高速な予測モデルです。

理論の全体像については、以下のブログ記事がとても良くまとまっていました。本記事では、マーケティング施策の選定に活かすという観点で必要な部分のみを概観します。
http://hiyoko9t.hatenadiary.jp/entry/2017/12/03/204333hiyoko9t.hatenadiary.jp

決定木とは

決定木とは、 Xのとある要素に対して次々と分岐点を見つけていくことで yを分類しようとするモデルです。視覚的にも結果が理解しやすいという利点があります。

f:id:upura:20180526211510p:plain

原田達也: 画像認識 (機械学習プロフェッショナルシリーズ), 講談社, p.149, 2017.

アンサンブルとは

アンサンブルとは、決定木のような単純なモデルを複数組み合わせ、複雑で非線形なモデルを構築する手法です。個々のモデルは単純なため高速に実行できるにもかかわらず、「3人よれば文殊の知恵」ということで高い精度も実現できる枠組みとされています。

代表的な手法としては、以下の二つが挙げられます。

  • バギング
  • ブースティング

決定木を「バギング」で拡張したモデルの一つに「ランダムフォレスト」があります。そして「ブースティング」で拡張したモデルが、勾配ブースティング決定木です。

バギング

バギングは、訓練データから重複を許して何度もデータセットを作成し、それぞれで学習した複数のモデルの結果を利用する手法です。「訓練データから重複を許して何度もデータセットを作成」するのは「ブートストラップ法」と呼ばれています。

f:id:upura:20180526212458p:plain

ブースティング

バギングはデータセットを複数個作り、それぞれ独立で学習させています。そのため、せっかく何回も学習しているのに、過去の間違いを「反省」して次のモデル構築に活かせていません。

ブースティングは、先に学習したモデルで分類に失敗した訓練データを積極的に分類できるよう、後段のモデルを修正していく手法です。逐次的にモデルを作成し、最終的には個々のモデルの精度を用いた重み付き多数決を実施します。

f:id:upura:20180526212951p:plain

ここまで説明したように「勾配ブースティング」は「決定木」という「 Xのとある要素に対して次々と分岐点を見つけていくことで yを分類しようとするモデル」を、過去の失敗を活かすように複数作成していくモデルです。

決定木がベースになっているので、最終的には Xのどの要素での分岐が yの予測に寄与しているか、つまりは各説明変数の重要度を算出できるモデルとなっています。

Pythonでの実装例

最後に以下では、有名なirisのデータセットを用いたPython実装例を示します。 Xを花の情報、 yを花の種類としています。

実装例は、GitHubでも公開しています。
github.com

データの準備

import pandas as pd
import numpy as np

from sklearn.datasets import load_iris
iris_dataset = load_iris()
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris_dataset['data'], iris_dataset['target'], test_size=0.1,  random_state=0)

データの可視化

X_trainの次元を削減して、花の種類で色分けしてみます。現実のデータだと、ここまで綺麗に分かれていることは無いかと思います。

%matplotlib inline
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

X_reduced = TSNE(n_components=2, random_state=0).fit_transform(X_train)
plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=y_train)

f:id:upura:20180526210011p:plain

モデルの構築(クロスバリデーション)

from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import GradientBoostingClassifier

learning_rate = [0.05, 0.1, 0.02]
max_depth = [2, 3, 4]
min_samples_leaf =  [5, 9, 17]
max_features = [1.0, 0.3, 0.1]

hyperparams = {'learning_rate': learning_rate, 'max_depth': max_depth, 'min_samples_leaf': min_samples_leaf, 'max_features': max_features}
gd = GridSearchCV(estimator = GradientBoostingClassifier(n_estimators=30), param_grid = hyperparams, verbose=True, cv=10, scoring = "accuracy", n_jobs=10)
gd.fit(X_train, y_train)

print(gd.best_score_)
print(gd.best_estimator_)

クロスバリデーションの結果、以下の結果が得られました。

0.9703703703703703
GradientBoostingClassifier(criterion='friedman_mse', init=None,
              learning_rate=0.05, loss='deviance', max_depth=4,
              max_features=0.1, max_leaf_nodes=None,
              min_impurity_decrease=0.0, min_impurity_split=None,
              min_samples_leaf=17, min_samples_split=2,
              min_weight_fraction_leaf=0.0, n_estimators=30,
              presort='auto', random_state=None, subsample=1.0, verbose=0,
              warm_start=False)

テストデータに適用

構築した予測モデルをテストデータに適用したところ、全て的中しました。

from sklearn.metrics import confusion_matrix
clf = gd.best_estimator_
clf.fit(X_train, y_train)
confusion_matrix(y_test, clf.predict(X_test))
array([[3, 0, 0],
       [0, 8, 0],
       [0, 0, 4]], dtype=int64)

説明変数の重要度の算出

説明変数の重要度を可視化した結果を、以下に示します。petal lengthが一番重要で、sepal widthが一番重要でないと分かります。

今回の場合は説明変数が四つしかないこともあり「だから何?」という印象も受けますが、説明変数が膨大な場合などでも重要な要素を機械的に選定できる点で価値がある手法です。

feature_importance = clf.feature_importances_
feature_importance = 100.0 * (feature_importance / feature_importance.max())

label = iris_dataset.feature_names

plt.xlabel('feature importance')
plt.barh(label,feature_importance, tick_label=label, align="center")

f:id:upura:20180526210547p:plain

二部グラフの最大マッチング問題|Python実装

参考にした実装

ina17.hatenablog.jp

改良点

setのエラーの回避

setでは .items() が使えないので回避した

条件分岐の追加

パッケージの都合で(3, 9)の組み合わせが(9,3)の順で出力される場合が考慮されていなかったので修正した

Python実装

import networkx as nx
from networkx.algorithms import bipartite
import numpy as np
import itertools
import matplotlib.pyplot as plt

group1 = range(5)
group2 = range(5,10)

node_color = ["b"] * 5
node_color.extend(["r"] * 5)

g = nx.Graph()
g.add_nodes_from(group1, bipartite=1)
g.add_nodes_from(group2, bipartite=0)

for (i,j) in itertools.product(group1, group2):
    val = np.random.randint(1, 10)
    g.add_edge(i, j, weight=val)

A,B = bipartite.sets(g)
pos = dict()
pos.update((n,(1,i)) for i,n in enumerate(A))
pos.update((n,(2,i)) for i,n in enumerate(B))

edge_width = [ d['weight']*0.3 for (u,v,d) in g.edges(data=True)]

nx.draw_networkx(g, pos, node_color=node_color)
nx.draw_networkx_edges(g, pos, width=edge_width)
plt.axis("off")
plt.show()

f:id:upura:20180523150357p:plain

d = nx.max_weight_matching(g)

for (i, j) in list(g.edges()):
    if (i, j) not in d:
        if (j, i) not in d:
            g.remove_edge(i,j)

edge_width = [ d['weight']*0.3 for (u,v,d) in g.edges(data=True)]

nx.draw_networkx(g, pos, node_color=node_color)
nx.draw_networkx_edges(g, pos, width=edge_width)
plt.axis("off")
plt.show()

f:id:upura:20180523150423p:plain

安定結婚問題|ゲール-シャプレイ (Gale-Shapley) アルゴリズムのPython実装

参考にした実装

cielan.hateblo.jp

改良点

アルゴリズム修正

とある男がプロポーズする際に、好みの女からプロポーズするように修正した

引数の追加

男4人・女3人など、男女の数が同一でないパターンにも対応できるようにした

挙動

入力

python3 gale_shapley.py 3 4

出力

[[3. 1. 0. 2.]
 [1. 2. 3. 0.]
 [1. 3. 2. 0.]]
[[0. 1. 2.]
 [1. 0. 2.]
 [2. 1. 0.]
 [0. 1. 2.]]
Engaged  (0, 2)
Engaged  (1, 3)
Already engaged  (1, 3)
Engaged  (2, 0)
[[0 2]
 [1 3]
 [2 0]]

Python実装

# coding: utf-8

import sys
import numpy as np
"""
Original:
http://cielan.hateblo.jp/entry/2016/12/25/193618
"""

def create_priority(n, m):
    """
    優先順位が行ごとにシャッフルされたn次正方行列を生成する
    :param n: 自グループの次数
    :param m: 相手グループの次数
    :return: 優先順位行列
    """
    data = np.empty([0, m])
    priority = np.arange(m)
    for i in range(n):
        np.random.shuffle(priority)
        data = np.vstack([data, priority])
    return data


def gale_shapley(a, b):
    """
    Gale-Shapley アルゴリズムを用いて安定結婚問題を解く
    :param a: 優先順位行列
    :param b: 優先順位行列
    :return: 結果行列
    """
    single_as = {i for i in range(len(a))}  # set型の独身集合A
    single_bs = {i for i in range(len(b))}  # set型の独身集合B
    engaged = np.zeros((len(a), len(b)), dtype=bool)  # 婚約行列

    while len(single_as) != 0:
        single_a = single_as.pop()
        # 好みのリストを順に走査
        for target_b in np.argsort(a[single_a, :]):
            if target_b in single_bs:
                # まだ婚約していなかったらめでたく婚約成立
                engaged[single_a, target_b] = True
                single_bs.remove(target_b)
                print('Engaged ', (single_a, target_b))
                break
            else:
                # 既に婚約していたら婚約者を探し出してどっちがより好まれているか調べる
                engaged_a = np.where(engaged[:, target_b])[0][0]
                print('Already engaged ', (engaged_a, target_b))
                target_a_list = b[target_b, :]
                if np.where(target_a_list == single_a)[0][0] < np.where(target_a_list == engaged_a)[0][0]:
                    # 好みの優先順位を調べて今回のほうが好まれていたら元の婚約者との婚約を破棄
                    engaged[engaged_a, target_b] = False
                    single_as.add(engaged_a)
                    print('Discard ', (engaged_a, target_b))
                    # 改めて婚約
                    engaged[single_a, target_b] = True
                    print('Engaged ', (single_a, target_b))
                    break
    return engaged


if __name__ == '__main__':
    MEN_NUM = int(sys.argv[1])  # 人数はコマンドライン引数から取得
    WOMEN_NUM = int(sys.argv[2])  # 人数はコマンドライン引数から取得
    
    men = create_priority(MEN_NUM, WOMEN_NUM)  # 男性の優先順位生成
    print(men)
    women = create_priority(WOMEN_NUM, MEN_NUM)  # 女性の優先順位生成
    print(women)
    result = gale_shapley(men, women)  # 安定結婚問題を解く
    print(np.transpose(np.where(result)))  # 結果表示

データ分析からの新規施策提案|SF Bay Area Bike Share, Kaggle


はじめに

某イベントにて、「データ分析からの新規施策提案」をテーマに資料を作成したのでブログでも共有します。

Oculus Goが届いた(感想)

f:id:upura:20180517201018p:plain

昨晩に届いて2時間くらい一気に試して、今日は「もう別に触らなくて良いかな」という気分まで燃え尽きた。

試したアプリ

Epic Roller Coaster

速攻で酔った。自分がVR酔いしやすいと判明し、萎えた。

ブラウザ

DAZN日経新聞紙面ビューアーを見た。DAZNは巨大ディスプレイで臨場感があった。紙面ビューアーはOculus Goでの使用が当然想定されてないため、操作性が壊滅的に悪かった。

ニュースアプリ(CNNやBBC

  • ジャーナリズムの新しい可能性を感じた。スマホを見るような手軽さで、VRを通じて現場を体感できる。

DMM VR

普通にすごかった(語彙力不足)。

終わりに

  • 勝手に燃え尽きたのは、まだまだ魅力的なコンテンツを知らないからな気もするので調べたい。

【Pandas】欠損値を、欠損していない値からランダム抽出して補完する

あまりこういう欠損値補完はしない気もするけど、業務にて要望があった。調べても、これくらいしか情報が出てこなかったので、自分用メモ。もっと良い書き方がある気がする。

stackoverflow.com

target_column_name = list(df.columns[df.isnull().any(0)])

for tcn in target_column_name:
     df[tcn] = df[tcn].apply(lambda x: df[tcn].dropna().sample().values[0] if x != x else x)

自然言語処理×ジャーナリズムな研究まとめ ~ 言語処理学会(NLP2018)より ~

今年のGWも終わりますね。僕は若者らしく、今年3月の言語処理学会の論文を読み漁っていました*1

個人的に興味のあるジャーナリズム絡みの論文を中心に総計12本読んだので、下記の記事からタイトルを拝借する形で、一つの記事にまとめておきます。

data.gunosy.io

新聞記事における政治家の発言の引用記述と議会会議録との対応関係の調査 ―フェイクニュース検出に向けて―

概要
フェイクニュース検出に向けた調査の研究。「新聞記事に掲載された政治家の発言の引用」と「地方議会会議録」を逐一比較していき、約95%はBoWなどの語句レベルの一致で推定できるとまとめている。

所感
BoWという単純な仕組みで、結構な高割合が評価できるというのは意外。ただよく考えると、新聞記事でそこまで凝った文章加工はしないので妥当な数字な気も。

upura.hatenablog.com

関連記事判定のためのニュース記事キーフレーズ抽出

概要
ニュースサイトで良くある「関連記事」を自動で導出するタスク。「キーフレーズ共有性」という新たな評価尺度と、その概念に基づく抽出法を提案している。

所感
計算量の節約のためにRNNによる近似を導入したら、性能も上がったという報告が興味深い(本論文の考察でも理由は十分に掘り下げられていない)。

upura.hatenablog.com

経済記事からの不祥事報道検知

概要
経済記事を「不祥事」に関するものか否かで二値分類するシステムの開発。アルゴリズムロジスティクス回帰とN-gram。精度を追い求めるだけでなく、解釈性・頑健性などを深く議論している。

所感
機械学習を実システムに導入する際のTipsのような論文。

upura.hatenablog.com

決算短信からの事業セグメント情報抽出

概要
決算短信特有の言語的な特徴を考慮した「事業セグメント情報抽出手法」を提案し、その有用性について実データを用いて評価。

所感
ドメイン知識をフル活用して、実直にタスクに取り組んでいる。

upura.hatenablog.com

ブートストラップ法による科学ニュース記事からの雑誌名抽出

概要
雑誌名が特定の文脈に出現しやすいという仮定を立て、雑誌名の両側から学習した文脈をパターンとして利用しブートストラップ法で雑誌名を抽出

所感
論文の第一文のこの問題の解消が根本的な解決策ではないかと思ったり。。。

日本語の科学ニュース記事では,研究成果がわかりやすく述べられるが,出典となる文献情報は明記されない傾向にある.

upura.hatenablog.com

検索エンジンによる上位検索ページを情報源とするフェイクニュース自動検出のためのデータセット作成

概要
フェイクニュース検出に関して、人間と同じやり方(検索エンジンによる上位検索ページを情報源として判断)をコンピュータで再現しようとした論文。

所感
うまくいかなかった例を分析した結果「検索された結果ページにフェイクニュースの記事のほうが多く存在してしまう場合」が挙げられていて、そうだよなあと思った。このアプローチだとどうやっても人間を大きく超える性能は出せないだろうが、目的は「データセット作成」に置いているので悪くもない気もする。
upura.hatenablog.com

ニュースからのトピックに関するストーリーラインの生成

概要
ニュースコーパスからトピック (知りたい事柄) に関連するテーマを抽出し,そのテーマに関連する文が時系列順に並んだ文集合 (ストーリーライン) を出力するシステムを提案。

所感
「ストーリーラインの生成」というタスクを、細かいタスクに分解している。他の研究成果を動員して実現する応用研究のような立ち位置。

upura.hatenablog.com

複数エンコーダを用いたヤフートピックス見出し候補生成

概要
「記事タイトル」と「記事リード文」を入力とし、エンコーダ・デコーダの枠組みからトピックス見出しを生成する手法を提案。

所感
単に先行研究をサービスに適用するだけでなく、技術的な修正もしているのが良い。

upura.hatenablog.com

会話によるニュース記事伝達のための間の調整

概要
会話によるニュース記事伝達において、割り込みを許容しながら快適なリズムで会話を進行させるための間の調整について検討。テクノロジー系のニュース記事 100 個を人手で要約・口語化し、実際に声優に話してもらいコーパスを作成。双方向 LSTMやBayesianRidgeモデルで学習させた。

所感
スマートスピーカーが流行っている中、ある意味新しいジャーナリズムの形を模索する論文と言っても良い気がする。

upura.hatenablog.com

プレイデータからのサッカーの速報テキスト生成

概要
www.nikkei.com

「選手名やチーム名を汎化タグに変換」「単語bigramを1つの単語として結合」の工夫で、encoder-decoder[6]モデルの性能が向上。

所感
直感的にも性能改善しそうな前処理をすることで、実際に性能が改善しており、腑に落ちやすかった。

upura.hatenablog.com

ファクトチェックを必要とするニュース記事の探索の支援

概要
ファクトチェックの必要性を示唆する情報(=「端緒情報」)の探索を自動化し,人手による要検証記事探索作業を技術的に支援する仕組みを構築。

所感
うまくいかなかった例を見ていると、人間の発言をコンピュータに解釈させることの難しさを改めて実感する。

upura.hatenablog.com

Experiment on Using Topic Sentence for Neural News Headline Generation

概要
encoder-decoderモデルを用いたニュースの見出し生成タスクで、第一文ではなく「トピックセンテンス」を使った場合の影響を調べる。第一文に加えてトピックセンテンスも利用した方が性能が上がった。

所感
多様な情報を利用した方が精度は出そうなので、最後はどこかで計算コストと性能のトレード・オフみたいな話に帰着しそう。

upura.hatenablog.com