人工知能してみる

人工知能の中の人が機械学習とか統計とかAI的なことを書き連ねます

Microsoft Azure のCustom Visionが簡単すぎる

こんにちはGrahamianです。 今日はMicrosoftの提供している機械学習系サービスの一つであるCustom Visionを紹介します。

Custom Vision - Home

要約すると

Custom Visionは素人でもマウスでぽちぽちするだけでもたった5分で画像分類のAPIを作れてしまう!!

f:id:Grahamian:20181103180147p:plain

課題

今回の課題はVTuberの識別です。 理由は画像が集めやすかったからなのと最近になってハマったからです。
さて、今回はそんなVTuberの顔を識別する分類機を作ってみましょう。

データの準備

まずはデータの準備ですね。
今回はキズナアイ輝夜月、電脳少女シロを対象としました。選定理由は単純に好みです。
適当に画像検索して良さそうな画像をそれぞれ15枚くらい準備しました。
普通に画像の分類をやるならば最低でもそれぞれ100枚くらいはないとツライのですが、今回は普通より全然少ない枚数です。
本当は画像のノイズを取り除いたりする必要があるのですが、そこらへんはあえてしないで放り込んでみます。
字幕とか背景とか写ったままですし、体の位置も画像によってバラバラです。

f:id:Grahamian:20181103183505p:plain

Azure Custom Visionの準備

まずはAzureへの登録です。
Custom Vision自体は無料である程度使うことが可能です。確か料金がかかるときは注意してくれるはずです。
Azureのアカウントを作成したらCustom Visionを開きましょう。
Custom Visionを開いたらまずはプロジェクトを作成します。
(画像では既に作成されたプロジェクトが見えてます。シロちゃんマジ天使)

f:id:Grahamian:20181103183715p:plain

名前はなんでも良いですが今回はClassify_VTuberとしておきます。
Project Typeは画像の分類なので “Classification“ を、Classification Typeは今回は3人のVTuberを分類するので "Multilabel" です。
Domainはlandmarkやfoodなどが選べて適したものを選ぶと精度があがります。
今回はどれにも当てはまらないので "general" です。
設定できたらCreate Projectでプロジェクトを作成します。

f:id:Grahamian:20181103180158p:plain

画像アップロード

お次はさっそく画像をアップロードしていきましょう。
Add imagesからアップロードしたい画像を選んでアップロードします。
このとき同じタグの画像だけをアップロードすると一気にタグがつけられるのでタグ付けが楽です。
学習データの準備はこれだけです。
上述したように背景や字幕の削除、顔位置の調整のような前処理はしていません。

f:id:Grahamian:20181103183157p:plain

学習

画像をアップロードしてタグをつけたらあとは学習を回すだけです。
これも右上にあるTrainをポチッと押すだけで開始してくれます。
今回は学習データが数十枚なので2, 3分で終わります。
学習が終わると学習機はもう完成です。
学習データにおける精度(PrecisionとRecall)も同時に表示してくれます。
若干Recallが低いですが学習データが少ない割には高精度といえるでしょう。
たぶん内部では転移学習などを使って少数データでも動作するようになっているのだと思います。 改善するなら学習データを増やして100枚ずつくらいにするのが手っ取り早いでしょう。

f:id:Grahamian:20181103180155p:plain

Quick Test

さっそくできた学習機のテストをしてみましょう。
手元の画像で簡易的にテストができるQuick Testという機能が実装されています。
細かいところに気が利いていて最近のMicrosoftさんマジ良い。
ということで、学習に使っていない画像でサクッとテストしてみます。
ローカルにある画像をアップロードすると自動でテストされてTagごとのProbabilityが表示されます。
labelがsiroのProbabilityが90.9%ということで実際にちゃんと識別できていることがわかります。

f:id:Grahamian:20181103180150p:plain

Prediction URLをクリックすることで学習した結果はそのままAPIとして使用することが可能です。
ここらへんは他の記事に譲りますが、マウスでぽちぽちするだけでAPIにできるのは便利。

まとめると

Azure Custom Visionはマウスでぽちぽちするだけで画像のアップロードからAPI化までできてしまいました。
学習データの前処理も不要で画像処理に詳しくなくても簡単に分類器を作成することができてしまいます。
画像収集の時間を除けばアップロードしてタグつけて学習するだけなので5分もあれば完了です。
アイディアさえあればもっと面白いことができそうです。みなさまもよければ試してみてください。

2つの分布が異なるか調べるKolmogorov–Smirnov検定

Kolmogorov–Smirnov検定って簡単にいうと?

コルモゴロフ-スミルノフ検定(Kolmogorov–Smirnov test)以下KS検定 は任意の2つの分布が異なるか評価する検定です。 例えば用途として、金融では与信スコアリングモデルがデフォルトと非デフォルトを分離できているのか評価するために使うことがあります。

KS検定は順序尺度を用いた検定なのでどんな分布でも検定できる、ノンパラメトリック検定になります。 なのでノンパラメトリックな経験的分布同士の検定も行うことができます。 ちなみに正規分布かどうか調べるならばシャピロ-ウィルク検定[Shapiro-Wilk test]など別の検定を使ったほうが精度が良いとされています。

Kolmogorov–Smirnov検定を使ってみる

Pythonで使うならばscipyにライブラリがあるのでこれを使うのが簡便で良いです。 scipy.stats.ks_2samp — SciPy v0.14.0 Reference Guide

使い方も簡単で2つの分布をnumpy形式で入れてやるだけです。 結果はタプルになっているので必要な方を取得してください。

from scipy import stats
result = stats.ks_2samp(distribution1, distribution1)
result[0]  # 統計検定量
result[1]  # p-value

Kolmogorov–Smirnov検定って何を見てるの?

KS検定が何を評価しているかというと、2つの分布の出現頻度の差を見ています。
横軸にスコアxを0~1、縦軸にyに累積出現割合をおきます。
2つの分布が異なれば片方は0に近い領域で出現数が増え、もう片方は1に近い領域で出現数が増えます。

f:id:Grahamian:20180526011937p:plain

このとき、あるxでのyの差(上図矢印)のうち最大値を統計検定量として検定します。
つまり一番出現割合の差が大きいときを評価します。
この特性のため、KSテストは分布の中央値に影響を受けやすく分布の裾の評価が弱いです。
裾が重要であるような分布(例えばリスク評価)を検定するならばAnderson-Derling検定を使うのが良いとされています。
ただし、2分布間の分布を評価するAnderson-Derling検定はscipyにもありません(1sampleならある)。
Rならばadkというパッケージに入ってるようですがCRANから削除されているのでアーカイブから入れる必要があります。

stats.stackexchange.com

ROC曲線とAUC

機械学習や統計の評価指標は多々ありますが、その中でも基本になるROC曲線とAUCについて説明します。

TL;DR

Q: ROC曲線とは?
A: 横軸に偽陽性、縦軸に真陽性をとったグラフです

Q: AUCとは?
A: ROC曲線の右下側の面積です

ROC曲線とAUC

ある2値分類問題を考えてみます。モデルより得られた確率に対して閾値を設定することでPositive / Negativeを判断できます。このとき、真値のTrue / Falseと予測値のPositive / Negativeによって4つの割合が計算できます。このうちROC曲線では偽陽性と真陽性を使います。

f:id:Grahamian:20180509224551p:plain

閾値を移動させながらPositive / Negativeを判別していくことを考えます。そうすると閾値が0のときは全てNegativeに判断するので偽陽性は0になります。ここから閾値を増加させていくと偽陽性と真陽性が増加していきます。そして閾値が1となったとき全てPositiveと判断するので偽陽性は1になります。モデルの識別能力が高ければ左上に凸のグラフになりますが、識別能力が低ければグラフは対角線上に近づきます。このとき、ROC曲線の右下側の領域の面積がAUC: Area Under Curveとなります。

f:id:Grahamian:20180509230515p:plain

なぜROC曲線やAUCを使うのか?

ROC曲線を使うことでTrue / Falseのデータに偏りがあっても適切に評価することができます。例えばTrue : False = 8:2だとすると全てPositiveとするだけでAccuracy = 0.8となってしまいますが、Falseを識別できていないため偽陽性は0になりAUCは低い値を示します。このように偏りがあっても実際的な判断ができるためサンプルに偏りが発生しやすい医療や金融の分野では定番で使われる指標です。

AUCの計算方法

AUCを計算するだけならばPythonでもscikit-learnで簡単に算出することができます。このとき y_true は真偽値 0 / 1 のnumpy行列で y_scores はスコア(確率)0 ~ 1 のnumpy行列になります。ちなみにROC曲線の描写は面倒です。適当なライブラリも見つからなったので自力で実装するか、データ数が少ないならばスプレッドシートで計算した方が早そうです。

from sklearn.metrics import roc_auc_score
roc_auc_score(y_true, y_scores)

sklearn.metrics.roc_auc_score — scikit-learn 0.19.1 documentation

ジニ係数とAR値、AUCの関係

よく使われる指標としてジニ係数やAR値があるのですが、実はAUCと比例関係にあり 2 × AUC - 1 で示される同じものを見た指標です。金融ではARもAUCもジニ係数も使われますがどれを使ってもよさそうです。 ジニ係数とAUCの関係を説明します。AUCはグラフのうち凸曲線の右側の面積と言いましたが、このとき対角線より右側の面積は常に含まれます。そのため対角線より右側の面積0.5を除くことで曲線と対角線の間にある面積を算出できます。ジニ係数はこの曲線と対角線の間の面積の2倍なのでAUCからジニ係数を導出できます。

AR値とAUCの関係を説明します。ROC曲線に似たグラフとしてCAP曲線というものがあり、この曲線の右下の値をAR値と呼びます。CAP曲線は横軸に確率、縦軸に真値の累計をとります。つまり確率が低い順に並べて真である要素の累積を順番に数えていきプロットするだけです。実はROC曲線とCAP曲線はほぼおなじ形になります。このCAP曲線を使って計算するのがAR値ですが、定義はジニ係数と同じなのでやはりAUCからAR値は導出されます。

補足

上記の説明で分かりにくい場合は、こちらのQiita記事はアニメーションを使って説明していますのでより分かりやすいかもしれません。

https://qiita.com/kenmatsu4/items/550b38f4fa31e9af6f4fqiita.com

SVR: サポートベクター回帰を使ってみる

回帰の問題を解く機会があったので非線形な回帰を解けるSVR: サポートベクター回帰を触ったのでメモします。

SVR: サポートベクター回帰とは?

サポートベクター回帰サポートベクターマシンの回帰版で、カーネルトリックを使って回帰式を解くことで非線形な問題を解決します。
SVMがマージンの最大化と誤推計サンプルを減らすように学習するように、重みの最小化と誤差の最小化で学習します。
誤差の小さい部分を無視することでロバスト性を向上させているようです。

SVRを使ってみた

SVRを使うのは簡単でscikit-learnから呼びだすことができます。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm, cross_validation

# sinカーブにノイズを加えた教師データの作成
x = np.sort(np.random.uniform(-np.pi, np.pi, 1000))
y = np.sin(x) + 0.1 * np.random.normal(size=len(x))
x = x.reshape((len(x), 1))

# 8:2で訓練と評価データに分割
x_train, x_test, y_train, y_test = cross_validation.train_test_split(x, y, test_size=0.2)

# 学習
reg = svm.SVR(kernel='rbf', C=1).fit(x_train, y_train)
y_rbf = reg.fit(x, y).predict(x)

# グラフにプロット
plt.plot(x, y, 'o')
plt.plot(x, y_rbf, color='red', label='SVR(RBF)')
plt.plot(x, np.sin(x), color='yellow', label='sin')
plt.show()

得られたグラフはこんな感じです。赤線がSVRで黄色線が実際のsinカーブです。 f:id:Grahamian:20180321211541p:plain

実際の問題を解く際はパラメータをGrid Searchする必要あるようです。
回帰というと重回帰分析が有名ですが非線形問題を解決する手段のベーシックな方法として使いたいですね。

R言語を使ってcsvデータからARIMAモデルで時系列分析をする

こんにちはGrahamianです。

最近時系列データを扱う機会があって勉強したので基本ですが一連の流れを紹介します。Rに慣れていないので細かいところで引っかかって時間を使ってしまったので実際に分析をするシーンを考えてcsvデータをインポートして使う状態を考えています。

分析の一連の流れについて

前準備

まずはcsvデータを作成します。これは実際の分析だったら既にデータがあるはずなので飛ばしてください。
データはプリセットのAirPassengersを使います。これは月次の航空機の利用客数のデータです。中身は時系列オブジェクトになっており年月に客数がマッピングされています。

> AirPassengers
     Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
1949 112 118 132 129 121 135 148 148 136 119 104 118
1950 115 126 141 135 125 149 170 170 158 133 114 140
1951 145 150 178 163 172 178 199 199 184 162 146 166
1952 171 180 193 181 183 218 230 242 209 191 172 194
1953 196 196 236 235 229 243 264 272 237 211 180 201
1954 204 188 235 227 234 264 302 293 259 229 203 229
1955 242 233 267 269 270 315 364 347 312 274 237 278
1956 284 277 317 313 318 374 413 405 355 306 271 306
1957 315 301 356 348 355 422 465 467 404 347 305 336
1958 340 318 362 348 363 435 491 505 404 359 310 337
1959 360 342 406 396 420 472 548 559 463 407 362 405
1960 417 391 419 461 472 535 622 606 508 461 390 432

これを write.csv(AirPassengers, "airpassengers.csv") という感じで雑にcsvに出力します。中身はこうなります。1カラム目はただのインデックスで、2カラム目が乗客数です。

"","x"
"1",112
"2",118
"3",132
"4",129
"5",121
"6",135
"7",148
"8",148
"9",136

csvを読み取り時系列オブジェクトにする

さて、それではこれを改めて読み取ってみます

ap <- read.csv("airpassengers.csv")
ts <- ts(ap[2], frequency=12, start=c(1949,1))  # ap[1]はインデックスが入っている

これで改めて時系列オブジェクトを作ることができました。
ちなみにfrequencyはどの周期の時系列データなのか決める値で frequency=12 なら月次になります。
startは何月何日からのデータにするのか決める値で、AirPassengersは1949年1月から開始しているデータなので指定しています。このときstartはベクトルで与えるので c(year, month, day) という形で与えます。

普通にARIMAモデルを適用する

ここからは普通にARIMAモデルを使うときと同じで、データをARIMAモデルに突っ込んで処理するだけです。

xt.arima <- auto.arima(ts, max.p=12, max.q=2,stepwise=T,trace=T, seasonal=T)
plot(forecast(xt.arima, range=c(1960, 1), h=24))

結果は下記図のようになります。いい感じですね。 f:id:Grahamian:20180303004025p:plain

まとめ

やっていることは難しくないのですがRに慣れていないと時系列分析のモデルにデータを入力するところでつまずくので一連の流れを書いてみました。これを自動化しようとするとインプットしたデータから開始日付を抽出して変換するなのが必要になりますので面倒になります。

※時系列分析について学ぶにあたり六本木の人の記事をめっちゃ参考にしました。この場を借りて感謝申し上げます。

tjo.hatenablog.com

文字列類似度評価 レーベンシュタイン距離 / ジャロ・ウィンクラー距離

こんにちは。Grahamianです。 文字列の類似度を評価するアルゴリズムであるレーベンシュタイン距離とジャロ・ウィンクラー距離です。

レーベンシュタイン距離

レーベンシュタイン距離は2つの文字列の間の編集距離を示します。編集とは「挿入」と「削除」(と置換)です。距離とは2つの文字列が等しくなるまでの編集回数です。文字列の類似度はこの距離の最短距離で表します。

Pythonであれば python-Levenshtein というライブラリがあるので簡単に導入することができます。ライブラリによってはマルチバイト文字に対応できないケースもあるので日本語に適用する場合は注意が必要です。

使い方はこちらのQiita記事に書いてあるので省略します。 qiita.com

レーベンシュタイン距離は、長さがm, nの文字列間の距離がO(mn)で計算できることが知られています。これは縦横がm+1, n+1の長さの行列を用いて動的計画法で計算を行います。動的計画法については長くなるので別の記事に譲ります。 mathwords.net

計算例

レーベンシュタイン距離について簡単な例を出します。"dog" と "bag" の距離を考えてみましょう。 編集に置換を許すと距離は"2"になります。
d は b に置換 距離+1
o は a に置換 距離+1
g はそのまま 距離+0
=> 合計距離は "2"

置換を許さず挿入と削除の場合は距離は4になります d を削除 距離+1
o を削除 距離+1
b を挿入 距離+1
a を挿入 距離+1
g はそのまま 距離+0
=> 合計距離は"4"

ジャロ・ウィンクラー距離

ジャロ・ウィンクラー距離はレーベンシュタイン距離よりミスタイプの検知に特化したアイディアです。Jaro距離と呼ばれるメトリクスに文字列の先頭数文字の一致度を加算しています。レーベンシュタイン距離が有名かつ分かりやすいのであんまり知られていないかもしれません。このジャロ・ウィンクラー距離も python-Levenshtein から呼び出すことができます。

Jaro距離の定義は下記のとおりです。 f:id:Grahamian:20180223001936p:plain 引用: A Comparison of String Metrics for Matching Names and Records

s, t が元の文字列の長さ、s', t' が一致する文字列の長さ、T_{s,' t'} が置換数です。これで得られたJaro距離を使って下記の用に文字列の先頭数文字における一致度を加えます。

f:id:Grahamian:20180223003309p:plain

sim_jJaro距離でsim_wがジャロ・ウィンクラー距離です。lは先頭から何文字を考慮に入れるかで英語だと最大4文字までを使うようです。これはタイプミスをするときは先頭の文字は正しく打ち込むという調査結果を反映したものらしいです。英語だと複数文字の接頭語があるのでpは長めにとってもうまくいきますが日本語だと長い接頭語は少ないのでl=1, 2が望ましいようです。pは定数で論文だと0.1です。

まとめ

レーベンシュタイン距離はそこそこ理解できているのですがジャロ・ウィンクラー距離はちゃんと理解してないので頑張りたいところ。実際に組んでみるのが一番早いんだろうなあ。
文字列の類似度というと直ぐにembeddedしたがってword2vecを使う人がいるのですが状況によってはレーベンシュタイン距離の方が上手くいくケースは多々あります。word2vecと違い学習もいらず高速に動作するのも良い点です。こういう古典的なアルゴリズムも一個ずつモノにしていきたいですね。

Redshiftで条件ごとに集計する

こんにちは。Grahamianです

↓みたいなデータのときに

category | number
1 | 3
1 | 4
1 | 10
2 | 5
2 | 6
2 | 1
3 | 2
3 | 11

categoryごとにnumberを集計したいときがあると思います。あるはずです。私はありました。

普通にやるとcategoryをgroup byしてsumするので↓みたいな結果になります

category | sum(number)
1 | 17
2 | 21
3 | 14

普段はこれで良いのですが、たまに条件ごとに横持ちにしたいときがあります。

cate1 | cate2 | cate3
17 | 21 | 14

こんなときにどうしたら良いのかいろいろと探し回ってしまったのでメモ。

具体的にはsumとcaseメソッドを使います。caseはif文みたいなやつです。こんな風に書きます。

sum(case when category = 1 then number else 0 end) as cate1
sum(case when category = 2 then number else 0 end) as cate2
sum(case when category = 3 then number else 0 end) as cate3

これは何をしているかというと when category = 1 then number else 0 のように指定してやることで category = 1 のときだけ number の値を得ることができます。他のときは else 0 とすることで返り値を0にしています。この結果をsumするので最終的には category = 1 のときのsumが得られるというわけです。
ただし、categoryそれぞれについて横持ちに計算したい場合はそれぞれのcategoryの数だけカラムを作ってやる必要があるのでselect文が長くなってしまう点は注意かもしれません。最も、そのようになってしまう場合はそもそも抽出元や作りたいデータの構造に問題がある可能性もありそうですが...

SQLは簡単だと思ったことが意外と難しかったりしてツライときもありますが、やはり大規模データを高速に処理するためには必須の技術なので頑張って身につけていきたいですね。