u++の備忘録

RBFカーネルのハイパーパラメータは何物か?

はじめに

今回の記事は、下記の質問に答えるものです。

f:id:upura:20180121220647p:plain

RBFカーネルとは?

RBFカーネル(Radial basis function kernel)は下記のように定義される関数のことです。

 K(x, x')=\exp(-\gamma||x-x'||^2)
ただし ||x-x'||^2ユークリッド空間上の距離の2乗、 \gamma=\frac{1}{2\sigma^2}です。

RBFカーネルカーネル関数の一つで、機械学習の文脈では、サポートベクターマシン(SVM)など内積のみを扱う線形のアルゴリズム非線形化する際に登場します*1

RBFカーネルを用いたSVM

RBFカーネルのハイパーパラメータが何物かを知るには、実際にRBFカーネルを用いたSVMが(RBFカーネルの)ハイパーパラメータの違いでどのような挙動を示すかを見るのが分かりやすいかと思います。

そしてscikit-learnの公式ページに、ズバリそのものの項目があるので、該当部分を引用します。

RBF SVM parameters — scikit-learn 0.22 documentation

Intuitively, the gamma parameter defines how far the influence of a single training example reaches, with low values meaning ‘far’ and high values meaning ‘close’. The gamma parameters can be seen as the inverse of the radius of influence of samples selected by the model as support vectors.

日本語訳:
直感的には、ハイパーパラメータ \gammaは「一つの訓練データが与える影響の範囲」を意味します。小さいほど「遠く」、大きいほど「近く」まで影響します。ハイパーパラメータ \gammaはサポートベクトルモデルとして選ばれた訓練データの影響範囲の半径の逆数(インバース)としても見なすことができます。

つまり、ハイパーパラメータ \gammaが小さいほど個々の訓練データが「存在を主張」し、大きいほど「おとなしく」なります。そのためハイパーパラメータ \gammaが小さい場合には青・赤の訓練データがお互いに存在を大きく主張するので、互いの境界で識別曲線が引かれます。一方で大きい場合には青・赤の訓練データがお互いに自分の周りしか存在を主張しないため、歪な形の識別曲線が引かれるといった具合です。

f:id:upura:20180121230034p:plain

上の画像を生成したPythonコード

SVM(RBFカーネル)のハイパーパラメータを変えると何が起こるの? - Qiitaを参考に作成しました。

# -*- coding: utf-8 -*-
import numpy as np
from sklearn import svm, datasets
import matplotlib.pyplot as plt
from itertools import product

if __name__ == '__main__':
    iris = datasets.load_iris()
    #特徴量は最初の2つ, クラスラベルも最初の2つを使う
    X = iris.data[:100, :2]
    #特徴量にノイズを加える
    E = np.random.uniform(0, 1.0, size=np.shape(X))
    X += E
    y = iris.target[:100]
    #meshのステップサイズ
    h = 0.02
    #コストパラメータ
    Cs = [2 ** -5, 2 ** 15]
    #RBFカーネルのパラメータ
    gammas = [2 ** -9, 2 ** -6, 2** -3, 
              2 ** 3, 2 ** 6, 2 ** 9]

    svms = [svm.SVC(C=C, gamma=gamma).fit(X, y) for C, gamma in product(Cs, gammas)]
    titles = ["C: small, gamma: 2**-3", "C: small, gamma: 2**-2",
              "C: small, gamma: 2**-1", "C: small, gamma: 2**1",
              "C: small, gamma: 2**2", "C: small, gamma: 2**3",
              "C: large, gamma: 2**-3", "C: large, gamma: 2**-2",
              "C: large, gamma: 2**-1", "C: large, gamma: 2**1",
              "C: large, gamma: 2**2", "C: large, gamma: 2**3"]
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))

    for i, clf in enumerate(svms):
        plt.subplot(4, 3, i + 1)
        plt.subplots_adjust(wspace=0.4, hspace=1)
        Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)
        plt.contourf(xx, yy, Z, cmap=plt.cm.Paired, alpha=0.8)
        plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Paired)
        plt.xlabel("Sepal length", fontsize=8)
        plt.ylabel("Sepal width", fontsize=8)
        plt.xlim(xx.min(), xx.max())
        plt.ylim(yy.min(), yy.max())
        plt.xticks(())
        plt.yticks(())
        plt.title(titles[i], fontsize=8)
    plt.show()

おわりに

質問に立ち返ると、RBFカーネルのハイパーパラメータ \gammaは「個々の訓練データをどれだけ重要視するか」の設定と捉えられそうです。訓練データからRBFカーネルを用いて分布を推定するという文脈では「(RBFカーネル)関数の広がり(やすさ)を制御する」というような解釈になるかと思います。

【Python&遊戯王】文章類似度の計算手法”Doc2vec”は「コンマイ語」にも通用するのか

はじめに

要するにやること

本記事では

  • 遊戯王カードの効果テキストを基に
  • Doc2vecという文章単位の類似度を計算するアルゴリズムを使って
  • 類似したカードを探すコードを実装してみます

遊戯王カードの効果テキストは、あまりの複雑さや特殊な解釈のために「コンマイ語」と揶揄されることもあるのですが、今回の記事はそんなコンマイ語に対してもDoc2vecが通用するのかの検証の意味合いも持っています。

動機

下記の記事を発見し、次のような記載があったので、やってみることにしました。
qiita.com

余談ですが遊戯王のカードでもやってみたのですが、自分が遊戯王に詳しくないので似ているかどうかピンと来なかったのでやめました。
Githubにソースとモデル一式をUPしておくので、興味がある方がいましたらやってみてください。
https://github.com/GuiltyMorishita/card2vec

ソースコードの修正

↑のGithubからデータを取得し、Windowsならではの"UnicodeEncodeError"を回避するためにコードを修正しました。修正の過程などはGithubでご確認ください。

github.com

gensimのDoc2vecは以前に下記の記事で触れていたこともあり、自分のやりやすい書き方にザックリと変更している箇所もあります。

upura.hatenablog.com

実行

card2vec.py(先駆者のこの命名センス、素晴らしいと思います)を実行すると、下記のような形式のデータを保持します。"names"はカード名、"texts"は効果テキストをそれぞれテキスト型で持っています。

f:id:upura:20180121181249p:plain

類似カードの出力

学習が終了したら、いよいよ類似カードを出力してみます。

    # 類似カードを求めたいカード名
    TARGET_CARD_NAME = names[random.randint(0, len(names))]
    # 直接指定も可能
    # TARGET_CARD_NAME = "xxxxxxxxxxxxxx"
    card_index = names.index(TARGET_CARD_NAME)

    # 類似カードと類似度のタプル(類似度上位10件)のリストを受け取る
    similar_docs = model.docvecs.most_similar(card_index)
    print(names[card_index])
    print(texts[card_index])
    print("--------------------is similar to--------------------\n")
    for similar_doc in similar_docs:
        print(names[similar_doc[0]] + " " + str(similar_doc[1]))
        print(texts[similar_doc[0]], "\n")

例えば「ソーラー・エクスチェンジ」に関する類似カードは、以下のような結果になりました。きちんと、ライトロード関連のカードが出力されています。

ソーラー・エクスチェンジ
手札から「ライトロード」と名のついたモンスターカード1枚を捨てて発動する。自分のデッキからカードを2枚ドローし、その後デッキの上からカードを2枚墓地に送る。
--------------------is similar to--------------------

閃光のイリュージョン 0.7759238481521606
自分の墓地から「ライトロード」と名のついたモンスター1体を選択し、攻撃表示で特殊召喚する。自分のエンドフェイズ毎に、デッキの上からカードを2枚墓地に送る。このカードがフィールド上から離れた時、そのモンスターを破壊する。そのモンスターがフィールド上から離れた時このカードを破壊する。 

ライトロード・ドルイド オルクス 0.7378130555152893
このカードがフィールド上に表側表示で存在する限り、「ライトロード」と名のついたモンスターを魔法・罠・効果モンスターの効果の対象にする事はできない。このカードが自分フィールド上に表側表示で存在する限り、自分のエンドフェイズ毎に、自分のデッキの上からカードを2枚墓地に送る。 

ライトロード・スピリット シャイア 0.7361984252929688
このカードの攻撃力は、自分の墓地に存在する「ライトロード」と名のついたモンスターの種類×300ポイントアップする。このカードが自分フィールド上に表側表示で存在する限り、自分のエンドフェイズ毎に、自分のデッキの上からカードを2枚墓地へ送る。 

ライトロード・ドラゴン グラゴニス 0.7135570049285889
このカードの攻撃力と守備力は、自分の墓地に存在する「ライトロード」と名のついたモンスターカードの種類×300ポイントアップする。このカードが守備表示モンスターを攻撃した時、その守備力を攻撃力が超えていれば、その数値だけ相手ライフに戦闘ダメージを与える。このカードが自分フィールド上に表側表示で存在する場合、自分のエンドフェイズ毎に、デッキの上からカードを3枚墓地に送る。 

ライト・バニッシュ 0.7092658877372742
自分フィールド上に存在する「ライトロード」と名のついたモンスター1体をリリースして発動する。モンスターの召喚・反転召喚・特殊召喚を無効にし破壊する。 

ライトロード・エンジェル ケルビム 0.703083872795105
このカードが「ライトロード」と名のついたモンスターを生け贄にして生け贄召喚に成功した時、デッキの上からカードを4枚墓地に送る事で相手フィールド上のカードを2枚まで破壊する。 

光の援軍 0.6913601756095886
自分のデッキの上からカードを3枚墓地へ送って発動する。自分のデッキからレベル4以下の「ライトロード」と名のついたモンスター1体を手札に加える。 

マイン・ゴーレム 0.6905478835105896
このカードが戦闘によって墓地に送られた時、相手ライフに500ポイントダメージを与える。 

ライトロード・レイピア 0.6747479438781738
「ライトロード」と名のついたモンスターにのみ装備可能。装備モンスターの攻撃力は700ポイントアップする。このカードがデッキから墓地に送られた時、このカードを自分フィールド上に存在する「ライトロード」と名のついたモンスター1体に装備する事ができる。 

魔法再生 0.6690211296081543
手札の魔法カードを2枚墓地に送る。自分の墓地から魔法カードを1枚選択し、手札に加える。 

おわりに

無事にコンマイ語でもDoc2vecが機能していそうだと分かりました。定量的に精査するならば、遊戯王Wikiなどのラベルを用いてP@kなどを算出するのが良い気がします。うまい検証方法を思いついたら、発表資料の形式にまとめられるレベルまで深めてみたいと思っています。自然言語処理では「英語でうまくいった手法を日本語で使ってみたら~だった」みたいな論文を見かける気もするので、僕もJSAI辺りへの投稿を目指したいと思います(←舐めんな)。

【論文メモ】Aggregated knowledge from a small number of debates outperforms the wisdom of large crowds

論文名

Aggregated knowledge from a small number of debates outperforms the wisdom of large crowds
Joaquin Navajas et. al.
Nature Human Behaviour, 2018
https://www.nature.com/articles/s41562-017-0273-4

概要

Abstractの要約は上記ツイート。正しくは5,180人っぽい。話し合いによって、明らかな間違いが是正されていくということか。

お台場の水陸両用バス「TOKYO NO KABA(KABA3)」に乗ってきた

土日は基本予定がなく意識的に外出しないと引きこもりになるので、お台場の水陸両用バス「TOKYO NO KABA(KABA3)」に乗ってきました。

www.kaba-bus.com

予約

上記のホームページから予約します。大人3,500円です。完了するとメールが送られてきて、記載されたURLからチケット確認用のQRコードが取得できます。当日でも空いていれば予約可能らしいです(その場合の予約方法は電話?)。

搭乗

当日は発車時刻の15分前までに、アクアシティお台場の1階にある集合場所に行き、上記QRコードを提示して搭乗口に並びます。早めに手続を済ませ待っていたところ、発車時刻の15分前ごろに、ひと目でそれと分かる大型バスがやってきました。

f:id:upura:20180120193403j:plain

「どこから乗るのかな?」と思っていたら、男心くすぐる変形!

f:id:upura:20180120193538j:plain
f:id:upura:20180120193529j:plain

このような柄の座席で、可愛らしいです。定員38人ですが、昨年11月27日に運行開始したばかりで認知度がまだまだ低いのか、乗客は5組20人弱でした。

f:id:upura:20180120193636j:plain

若干気になる注意書きも……。

f:id:upura:20180120193919j:plain

運転席には当然のことながらハンドルと舵の両方がありました。

f:id:upura:20180120193943j:plain

運行

f:id:upura:20180120194221p:plain
公式サイト(http://www.kaba-bus.com/tokyo/)より引用。

運行時間は約45分。最初の15分は地上でお台場の観光地を巡ります。バスツアーのごとく、ガイドのお姉さんが名所を紹介してくれました。

地上のコースを終えた後は、いよいよ東京湾へ突入です。

f:id:upura:20180120194448j:plain

乗客全員で「3、2、1、カバ~」と掛け声を掛けながら、バスが東京湾へ進んでいきます。

※この瞬間の動画を撮ったのですが、ぜひご自身で体験していただきたいので掲載しないことにしました

f:id:upura:20180120194642j:plainf:id:upura:20180120194650j:plain

水中パートでは、窓を開けて解放感あるクルージングを体験できます。レインボーブリッジを下から望むこともでき、少々寒かったですが非常に良い時間を過ごしました。

おわりに

貴重な体験ができ、とても満足です。最初は大人1人3,500円は若干高い気もしましたが、お台場をガイドさん付きで観光できるオプションもありますし、悪くない額だと思います。また運転手さん(キャプテン)は、大型バスの運転免許と船舶の免許を両方持っている稀有な人材なので、人件費がかかるのではないかと。ひさしぶりに充実した休日を過ごした気がしました。

【Python, sklearn】モデル名の表示

documentation読んでも上手く見つけられず、他人のコードを読み漁って発見したのでメモ。

from sklearn.linear_model import LogisticRegression
# ロジスティック回帰
model = LogisticRegression()
clf = model.fit(X_train,y_train)
print(clf.__class__.__name__)

文字列型で"LogisticRegression"が返る。

Google Homeで「室内留学」のすゝめ

はじめに

今更ながら、下記の記事を見て無性にGoogle Homeが欲しくなったので、昨日に購入しました。

note.mu

f:id:upura:20180117091042j:plain

用途:「室内留学」

僕の場合の用途は、自宅(※独り暮らし)で英会話ができる「室内留学」環境の構築です。

Google Homeを利用する利点は、以下のようなものが挙げられます。

音声操作のため、半強制的かつ手軽に英語を話す/聞く環境下を構築できる

  • 例えば目覚まし時計のアラームを止めるためにも、英語を話す必要が生じます
  • 朝には身支度をしながら「Tell me 'MyDay'」と言うだけで、今日の天気・最新のニュースを英語で流してくれます
    • "How can I get to the office"と聞くと、何時の電車に乗れそうかを教えてくれます
  • 何か調べたいときにも、わざわざスマホやPCを立ち上げることなく音声で処理しようとして英語を発するようになります

英会話教室や実際の留学と違って相手が「忖度」してくれない

  • Google Homeさんは発音が悪いと全く相手にしてくれません
  • 向こうから助け舟を出してくれることも、ほとんどありません
    • 目的語が足りないなど一定の条件下で聞き返してくれることも
  • 一方で生身の人間と違い、何回も話しかけても嫌な顔をしないという利点もあります

取りあえず一日稼働させただけですが、結構な量の英語を話す/聞くことができたので、今後も続けていきたいと思っています。

Google Home」か「Google Home Mini」か

店頭で店員に確認したところ

機能的に「できること」の違いはない

マイクとスピーカーの性能が違う

  • Miniはマイクが1個で、通常版は2個。通常版は高さがある分、聞き取る可能性が高くなる
  • スピーカーとしての音質/音域が違う

とのことでした。

スピーカーについては想定用途にも依りますが、店頭で確認すべきかと思います。個人的には結構違いました。

今回の用途はGoogle Homeとのやり取りが大切になるため、コミュニケーションの障害になりうる要素は極力排除したいと思い、倍の値段にはなりますが通常版を選択しました。

諸設定(参考までに)

  1. Google Homeのデフォルト初期設定
    • マニュアルに沿って設定します。この初期設定の終了時点では言語設定が日本語になっています。もちろん英語で話しかけても理解してくれません。
  2. 言語設定の変更
  3. ニュースの設定
    • スマホGoogle Homeアプリから、英語のニュースを追加し、(必要に応じて日本語のニュースを外し)流す順番を整備します
  4. 音楽の設定

PythonでRFM分析(任意のクラスタ数にK-meansクラスタリング)

やったこと

  1. RFMの3特徴量で各ユーザのデータを取得
  2. F, Mについては分布を考慮しlog10を取る
  3. データの可視化

サンプル

想定データ構造

UserID Recency Frequency Monetary (K-meansResult)
000000 123 456 789 0
000001 378 764 924 0
000002 578 267 532 1

Pythonコード

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pandas as pd
df = pd.read_csv('df.csv')

import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

df.iloc[:,2] = np.log10(df.iloc[:,2]+1)
df.iloc[:,3] = np.log10(df.iloc[:,3]+1)

CLUSTERS_NUM = 5
pred = KMeans(n_clusters=CLUSTERS_NUM).fit_predict(df.iloc[:,1:4])
df = pd.concat([df, pd.DataFrame(pred)], axis=1)
fig = plt.figure()
ax = Axes3D(fig)
ax.set_xlabel("Recency")
ax.set_ylabel("Log10(Frequency)")
ax.set_zlabel("Log10(Monetary)")
d = []
for cluster_i in range(CLUSTERS_NUM):
    d.append(df[df.iloc[:,4]==cluster_i])
    c = cm.hot(float(cluster_i) / CLUSTERS_NUM)
    ax.plot(d[cluster_i].iloc[:,1], d[cluster_i].iloc[:,2], d[cluster_i].iloc[:,3], "o", color=c, ms=2, mew=0.5)
plt.show()