オットセイの経営日誌

データサイエンス系ベンチャーを経営してます。経営のこと、趣味のことつぶやきます。

フレームワークに則って昨年の振り返りをしてみた。

皆様、あけましておめでとうございます。

本年もよろしくお願い申し上げます。

年末年始は、妻と自分の実家廻りをしておりました。

行き先は長野と富山ということで、暖かく迎えていただきながら、中部・北陸の美味しい酒・魚と、普段全く見ない分だけより面白い気がするTV番組に現を抜かしておりまして、昨年の振り返りをするのを後回しにしておりました。

しかし、ただ振り返りをするのも能がない。あと、振り返りに出遅れた中、普通に振り返りをするのもなんかダサい。

仕事のできる人はビジネスパーソンだろうがデータサイエンティストだろうが皆パターン化を行なっているのである

ということで、フレームワークに則った振り返りを行なってみることにしました。

尚、フレームワークの出所は妻のブログです。決して、決して、ブログやフレームワークの宣伝をしろと脅されたわけではございません。

概要

フレームワークですが、決して複雑なものではなく、↓のようなものです。

f:id:mhiro216:20200102213947p:plain

振り返りはそういう時間を持つことに意味があるので、チェックリスト的に多方面から振り返ることができるフレームであれば十分だと思います。

このツールを使う時のポイントは、関係性のあまり深くない人同士で発表し合う(その方が後腐れなく本音を言い合える)ことのようですが、生憎ボッチの自分にはそのような機会がないので、この記事で公開する形で発表してしまいます。

採点結果

まずは採点結果をまとめたレーダーチャートを示します。matplotlibで書いたので最初の図とは見た目が違うこと、ご承知おきください。

f:id:mhiro216:20200102215110p:plain

以下、詳細。

仕事 今年1年、仕事に満足できたか 7点

10年弱過ごした大企業生活から、一転して起業家生活に移行し、悪戦苦闘した1年でした。

やりたいことの100点満点が大きく変わったので、とても10点はつけられません。

が、とにかく目標を目掛けて努力し続けることはできた、という意味では5点以上はつけられるかなとは思います。

家族 今年1年、家族やパートナーとの関係は良好だったか 8点

高い点数をつけましたが、精神的に浮き沈みが激しくなりがちな仕事をしていた中で家族関係が良好であり続けられたのは、自分の力というより妻の理解に依るところが大きいです。

そんな良き妻をGETした過去の自分を褒め称えつつも、調子に乗り過ぎず、他力本願ではなく自分も余裕を持って家庭と仕事の両立という命題に向き合っていければと思い、10点満点から少し減点をしておきます。

健康 今年1年、健康でいられたか 5点

運良く大病はせずに1年を過ごすことができましたが、心身ともに重圧が少なくない中で、特に後半は低空飛行を続けてなんとか着陸したという健康状態でした。脳の働きに対して身体がついていけていない感が強くありました。

どうしてもやりたいこと、やらねばならないことに注力してしまいがちで、PCの前にいることが多い1年でしたが、今年はさすがにPCの前を離れて体を鍛えることをルーチン化したいと思います。

お金 今年1年、家計は良好だったか 5点

昨年はお仕事のご依頼にも恵まれ、現状で家計のキャッシュフローに特段影響はないものの、起業家の宿命で、今後も安泰と言い切れることはあり得ないのが実情です。

会社のポートフォリオ、あるいは自分のポートフォリオとしても、家計に負担をかけない体制を早めに構築したいところです。

人間関係 今年1年、人間関係は良好だったか 3点

低い点数をつけましたが、特に誰かと犬猿の仲になったというわけでもないのですが、やることの多さにかまけてひたすら飲み会を断り・こちらからも誘わない体制を築き上げ過ぎてしまったのが反省点です。

時間がなかなか捻出し難いのも事実ですが、時間に囚われすぎると枠にハマったつまらない発想しか出てこないのも事実なので、良い加減にテキトーになってフラフラ遊びにいきたいと思います。

学び 今年1年で自分のための学びができたか 5点

あまり高くない点数をつけましたが、学びの絶対量としては確実に以前よりも増えました。データサイエンスに関することも、そうでないことも含めて。学ばざるを得ない状況でもありました。

しかし、今の自分の立場である、会社の経営に責任を負い、かつ会社の技術的指針を示す役割を十分に全うするには、まだまだ学びが足りないなと感じています。

一方で、学びは本来的に自分の世界を広げてくれる意味で楽しいもののはずなので、義務感に囚われないようにしながら、もっと多くの時間を学びに費やしていきたいと思います。

ここで好きな言葉を1つ。

Live as if you were to die tomorrow. Learn as if you were to live forever.

遊び 今年1年、プライベートの時間は充実していたか 5点?

採点に?をつけたのは、起業したというのは、ある程度公私を分けずに生きていく判断をしたということだと考えているので、プライベート単体で採点をつけるのはあまり意味がないと思うためです。

仕事は充実しているのでその意味ではプライベートの点数も高くなりますが、ただ、仕事と全く関係のないことに時間を使う、余白のような時間を持てているかどうかという意味では厳しい点数になる1年でした。

とはいえ、新婚旅行でイタリアに行って食・自然・歴史を堪能したり、

f:id:mhiro216:20200102220629j:plain

味覚の境界線を攻めてくるようなうまい酒と肴を楽しめたり、

f:id:mhiro216:20200102220644j:plain

したのですが、それ以上を求めたい体になっているのです。

やはり、データサイエンスを絡めた活動で、プライベートを充実させていきたいと思います。一応温めているアイデアはあるので、実行に移そうと思います。

環境 今年1年、住んでいる環境に満足できたか 8点

日本の中では、やはり東京はとても楽しい街で、特に飲食やエンタメにおいて素晴らしいコンテンツが揃っていると思うし、今私が住んでいる人形町も、三越前人形町という平成と昭和の匂いが強く感じられるエリアへのアクセスが良くて(あと、食べログ3点台後半のお店が選り取り見取りで)、住環境には大満足しています。

ただ居住地としての日本は、所得税の上昇のように高齢化が進む中で避けられないトレンドもあり、住みづらくなってきている要素もないではないので、10点とはつけづらいですね。

あとは、住環境については、家族が増えるようなことがあれば全く違う景色が見えてくるのだろうなとも思います。

まとめ

こういうのは、レーダーの面積の大きさは性格に依って変わるものだと思うので、形というか平均からの偏差を見るべきなんでしょうね。

自分の場合、家族や環境、仕事への満足度は高いけれども、その分人間関係を犠牲にしているなーというのが改めて分かりました。

1年後の採点がどう変わるかを楽しみに、今年1年も頑張って行こうと思います。

レーダーチャート作成のソース

最後に、ご参考までにレーダーチャート作成のソースコードを貼っておきます。一応コーディングブログと銘打っているので。。

こちらを元に、DataFrameへのdataの持たせ方を多少改変しただけです。

# Libraries
import matplotlib.pyplot as plt
import pandas as pd
from math import pi
 
# Set data
df = pd.DataFrame(data = [['self',7,8,5,5,3,5,5,8]], columns = ['who','仕事','家族','健康','お金','人間関係','学び','遊び','環境'])

# number of variable
categories=list(df)[1:]
N = len(categories)
 
# We are going to plot the first line of the data frame.
# But we need to repeat the first value to close the circular graph:
values=df.loc[0].drop('who').values.flatten().tolist()
values += values[:1]
 
# What will be the angle of each axis in the plot? (we divide the plot / number of variable)
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]

# Initialise the spider plot
ax = plt.subplot(111, polar=True)

# Draw one axe per variable + add labels labels yet
plt.xticks(angles[:-1], categories, color='grey', size=12)
 
# Draw ylabels
ax.set_rlabel_position(0)
plt.yticks([2,4,6,8], ["2","4","6","8"], color="grey", size=12)
plt.ylim(0,10)
 
# Plot data
ax.plot(angles, values, linewidth=1, linestyle='solid')
 
# Fill area
ax.fill(angles, values, 'b', alpha=0.1)

# Save figure
plt.savefig('radarchart.png')

LeetCode / Ransom Note

リハビリその2。

https://leetcode.com/problems/ransom-note/

Given an arbitrary ransom note string and another string containing letters from all the magazines, write a function that will return true if the ransom note can be constructed from the magazines ; otherwise, it will return false.

Each letter in the magazine string can only be used once in your ransom note.

Note:
You may assume that both strings contain only lowercase letters.

canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true

Ransom Noteは身代金を要求する手紙のことのようですが、なぜこの問題がRansome Note?と思ったら、昔のそういった手紙は新聞や雑誌の切り抜きで文章を作っていたため、ということですね。
変数名がmagazineというところで気がつきました。

難易度はとても低いです。問題タイトルと内容の対応を読み解くところがこの問題のハイライトな気がします。

解答・解説

解法1

文字列のままループを回し、変数magazineからransomNoteを構成する文字を剥ぎ取っていきます。まさに脅迫状作成プロセスを逆回ししている感じです。

replaceの第三引数は、第一引数の値が存在した場合いくつ置換するかを表します。あまり普段の言語処理で使うことがない引数なので、ググってしまいました。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        for i in ransomNote:
            if i in magazine:
                magazine = magazine.replace(i, '', 1)
            else:
                return False
        return True
解法2

リストに変換してもほとんど同様に解けますが、リスト化の分計算コストが高い。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        l_ran = [s for s in ransomNote]
        l_mag = [s for s in magazine]
        for i in l_ran:
            if i in l_mag:
                l_mag.remove(i)
            else:
                return False
        return True

LeetCode / Guess Number Higher or Lower

今年も残すところ後1週間程度ですね。

ここ数ヶ月は怒涛のように過ぎ去り、ちょっと自分の人としての器を超えた容量が押し寄せている感じで、リハビリの必要性を感じてます。

ということで、リハビリがてらプレーンな二分探索問題を解きます。

https://leetcode.com/problems/guess-number-higher-or-lower/

We are playing the Guess Game. The game is as follows:

I pick a number from 1 to n. You have to guess which number I picked.

Every time you guess wrong, I'll tell you whether the number is higher or lower.

You call a pre-defined API guess(int num) which returns 3 possible results (-1, 1, or 0):

-1 : My number is lower
1 : My number is higher
0 : Congrats! You got it!
Example :

Input: n = 10, pick = 6
Output: 6

解答・解説

解法1

過去記事でも取り扱った二分探索で解きます。

# The guess API is already defined for you.
# @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
# def guess(num: int) -> int:

class Solution:
    def guessNumber(self, n: int) -> int:
        low = 1
        high = n
        while low <= high:
            mid = (low+high)//2
            if guess(mid) == 0:
                return mid
            elif guess(mid) == 1:
                low = mid+1
            else:
                high = mid-1

データサイエンスコンペプラットフォームNishika、リリースしました。

起業して早数ヶ月。

おそらく通常のベンチャーがあまり経験していないであろう苦難が幾度かありましたが、本日漸く、サービスリリースにこぎつけました。

Nishika

やりたいことの30%もできていない状態ではありますが、兎にも角にも自分たちの目指すものを形にして世に出せたこと、嬉しく思います。

さて、このNishikaというサービスを一言で言うと、
データサイエンティストと、データサイエンスを活用する人を繋ぐコミュニティプラットフォーム
です。

データサイエンスとビジネスを繋ぐサービスとして、形の異なる3つの柱を考えていますが、まずは
データサイエンスコンペティション を主に展開していきます。

データサイエンスコンペのプラットフォームとしては、世界で圧倒的な知名度・権威を誇るKaggle、日本の先人であるSIGNATEが存在しますが、

そんな中で我々Nishikaは、2つの点を大事にしながら、コンペを世に生み出していきます。

目指すのは、データサイエンスコンペが"社会の当たり前"になること

データサイエンスコンペ、特にKaggleは、データサイエンティストや一部エンジニアにとってみれば知らない人はいないというほどの存在ですが、

当然ながら、コンペには課題を解く側だけでなく課題を提供する側も必要です。

ここ数ヶ月、企業を中心に営業に回り、データサイエンスコンペの存在と、その中で享受できるメリットについてお話しして回ってきましたが、直面した現実は

一般のビジネスパーソンで、データサイエンスコンペという存在を知っている人はほとんど皆無

ということでした。

私自身データサイエンティストとして日々アンテナをはっていると、Kaggle、SIGNATEをはじめデータサイエンスコンペの話を聞かない日はないのですが、

それが機械学習による課題解決のソリューションの1つとして当たり前の存在になっているかというと、程遠いというのが実感です。

そこで我々は、

データサイエンスコンペによる課題解決のユースケースを多く生み出し、コンペを社会の当たり前にすること

を目指していきます。

エッジの効いたコンペももちろんやっていきますが、それ以上に大事にしていきたいのは、所謂テーブルコンペや、レコメンドや異常検知といった課題など、技術的には多少古いものが使われるかもしれないが、社会での適用例が多いユースケースにもしっかり目を向け、コンペ化していきます。

さらに、特定の課題や特定の業界にこだわるのでなく、様々な業界の方にとってコンペが自分事になるように、多種多様なコンペを生み出していきます。

そんな当たり前のことを、と思われるかもしれませんが、我々が先人がいる中でこの事業に取り組もうと思ったのも、まだまだ世の中には掘り出しきれていないコンペのニーズがあるのではないか、と考えたからです。

コンペの集合知の力をもっと世に知らしめていくこと

もう1点、大事にしていきたいことですが、これも営業の中で様々な方とお話しする中で、改めて感じたことになります。

よく言われることですが、コンペに対して課題やデータを提供する側にとっては、主にプライバシー保護や競合へのナレッジ流出のリスクを事由として、データを公開することへの不安感・拒否感があります。

その壁を乗り越えるのも簡単ではないのですが、それを超えた後にも壁があります。

データサイエンスコンペのプラットフォームにはディスカッションフォーラムというものが開設されることがあり、むしろKaggleではこれがデファクトとなっていますが、お題に対して参加者同士がコードを共有したり、データが示す規則性の発見などについてディスカッションできるようになっています。

三人よれば文殊の知恵、ならぬ千人いれば文殊の知恵で、最終アウトプットとなる機械学習モデルの性能をさらに高めることに繋がるわけですが、これについても、コンペのホスト側は必ずしも好ましく思わないケースがあります。

フォーラムが開設されないコンペが世の中にあるのは、このようなホスト側の事情に配慮したものではないかと思っています。(他のプラットフォームの話なので、推測に過ぎませんが)

正直なところ、私も大企業2社を経験してきていることもあり、ホスト側にこういった反応があるのはよく理解できます。

一方で、データサイエンティスト/コンペ参加者としての立場に立てば、ディスカッションフォーラムがあるコンペとないコンペではコンペ参加へのモチベーション・学びの度合いが全く異なり、実際により良い性能のモデルがアウトプットとなるメリットの方が大きいだろうと思うのですが、

2つの相反する気持ちが自分の中にあります。

結局、データサイエンティストにとってモチベーション高く参加できるコンペを増やすには、コンペにおける集合知の力が如何に優れたアウトプットを生み出すのかを、もっと世に知らしめていくしかない、と感じています。

我々Nishikaとしては、積極的にディスカッションフォーラム・集合知の価値を伝えていき、力の及ぶ限り、より魅力的なコンペを打ち出していこうと考えています。

今後

今日(19/12/4)時点では、Nishika自社開催のAIは芥川龍之介を見分けられるか?というコンペを開催しています。

難易度は決して高くなく、正直なところ、我々が面白くない?と思ったものをコンペにしてしまった部分が大きいですが(笑)、是非参加してみてください。

一部機能について、最終の動作確認中でアクティブにしていないものもありますが、お使いいただきやすく魅力的なプラットフォームになるよう、順次アップデートしていきます。

そして来週、クライアントがホストとなる1件目のコンペを開始する予定です。

内容は我々にとって身近で興味深い、一方で難易度としては(個人的には)歯応えのある、そんなコンペになっていると思います。

ご期待ください!

AI・機械学習に限らない異常検知の手法

仕事をバリバリやりながらブログ記事を投稿するって、なかなかきつい。

しっかり定期的に更新できる方は、どういう時間術を駆使しているんだろう。


そんな悩みを抱えていた中、仕事で表題の件についてまとめる機会があり、ちょうど記事化できそうな内容なので書き留めます。

異常検知は古くて新しい課題で、古くはルールベース・統計ベースの検出方法があり、直近は機械学習、特にオートエンコーダやGANを用いた教師なし学習による検出手法も発表され、現在進行形で進化が続いている領域です。

Web上にも古い記事から新しい記事まで、参考となる情報が大量に見つかります(実際、本記事もそれらをかなり参考にさせていただいています)が、異常検知素人にも分かるレベルでまとめられている記事はあまりないようだったので、そんな視点を意識してまとめます。

従って、異常検知を業務でバリバリ実装されているデータサイエンティスト向けというより、ビジネスパーソン向けの導入・(少し)発展編、ということになろうかと思います。

そもそも異常検知とはどういう問題を指すか

f:id:mhiro216:20191124164544p:plain

「異常検知ってどんな問題だと思いますか?」と聞くと、外れ値を検出する問題を想像する人が最も多いように思いますが、そのような静的なデータに対する検知だけでなく、動的な変化について変化点を検知したり、各時点の状態の正常/異常を判定する問題も、異常検知に含まれます。

どの問題を解くかによって適用すべき手法も異なりますが、共通に使われる手法もあり、本記事ではあまり厳格な切り分けをせず、どのような手法があるか例示します。

異常検知問題を解く手法にはどのようなものがあるか

f:id:mhiro216:20191124165234p:plain

異常検知問題を解く手法の説明に入る前に、機械学習全般についてのおさらいをしておきました。

その中で、異常検知問題で特にありがちな状況として、異常データが極端に少なく、教師あり学習を行うことが難しいケースが多いことが挙げられます。そのため「教師なし学習により「正常」とは何かを学習させ、そこから外れるものを「異常」とする」という考え方に基づく手法が種々研究されています。

尚、後段では3σ法やホテリング理論など、教師なし学習というより統計ベースの手法と言った方が適切では?という印象を受ける手法も出てきますが、「与えられたデータからパターンを学習する」のが機械学習であるという定義に従って、教師なし学習に一括りにして説明を続けます。

f:id:mhiro216:20191124165704p:plain

これも機械学習全般に成り立つ説明ですが、そもそも機械学習で問題を解くにはデータが必要であり、特に異常検知では異常データが十分に確保できるかどうかによって、適用できる手法が明確に変わる点に留意する必要があります。

f:id:mhiro216:20191124165951p:plain

ちょっと挑戦的なスライドを起こしてみました。

AI・機械学習がブームになってから数年経ち、意外にもブームは長続きしている状況ですが、一方で、手法レベルでの技術的革新が継続している領域と、一段落した領域に色が分かれてきていると思います。

優秀なデータサイエンティストの方も世の中にどんどん増えている中で、技術的革新が一段落した領域で不要に高い投資をしてしまうのは、もったいない。所謂「枯れた技術」を使いこなせることは技術者・ベンダーにとって強みではありますが、差別化要素になるほどではない。

枯れたといっても、技術を自在に使いこなせる人は限られているので、投資にブレーキを踏むべきという意味ではないですが、一方でそのような領域で自社の技術が如何に優れているかアピールし高い投資を要求するような輩がもしいたら、十分に注意していただきたいという気持ちを込めてこのスライドを作りました。

手法レベルではそのような状況ですが、一方で、弊社Nishikaが運営するデータサイエンスコンペティションは、有用な特徴量や手法の組み合わせを発見する恰好の機会となりモデルのさらなる性能向上の切欠となるので、技術的革新の進捗とは別に必要性を判断し、役立てていただきたいというのがポジショントークでした。

f:id:mhiro216:20191124171108p:plain

異常検知を行う手法を調べるとその多様さには驚きますが、一方でたくさんある中でどれを選ぼうかという無邪気な話でもなく、ユースケースにおけるデータの特徴によって適用すべき手法が決まってくるというのがポイントです。

以下、いくつかの手法を例に、適用条件について列記します。

異常検知を行う手法と適用条件

3σ法

f:id:mhiro216:20191124171312p:plain

皆様ご存知、3σ法です。品質管理の分野では最も著名な手法ではないでしょうか。

しかしこの3σ法、スライド記載の通り適用条件にはかなり制限があります。

以下、3σ法の制限をクリアする手法を各々述べていきます。

ホテリング理論

f:id:mhiro216:20191124171427p:plain

3σ法の制限の1つである、多変数に対する異常検知では実質的に使えない点をクリアする手法が、これもまた古典的な手法であるホテリング理論です。

「出現確率がX%未満の高い異常度が観測されたら、異常とみなす」という考え方は、精度が良いかは別として、直感的に決めやすく、上への説明はしやすいですね。

k近傍法

f:id:mhiro216:20191124171624p:plain

3σ法やホテリング理論はひと山の分布に対する手法でしたが、複数の山からなる分布に対して異常検知を行う手法として、k近傍法があります。

k近傍法は、Pythonライブラリは存在するのですが意外に理論から実装した例が公開されておらず、右図の異常度の描画に際してゼロから実装してみましたが、シンプルな考え方に基づく手法なだけあって非常に実装も簡単でした。

ただ、分布の密集度によらず同じ基準で異常度を算出してしまう弱点があり、それを解決するLocal Outlier Factor (LOF) という手法が考案されています。

k近傍法(時系列データに対して)

f:id:mhiro216:20191124171934p:plain

ここまでは測定値がお互いに独立な場合の例で、測定値同士に関係性があるデータ、典型的には時系列データにおいては、異なる手法を用いる必要があります。

但し、先ほど紹介したk近傍法は、スライド窓により波形パターンを抽出することで、時系列データにも適用することができます。

長期的に上昇/下降するようなトレンドがあるとそのままでは適用できませんが、例えば工場の異常検知で長期トレンドが観測されるようなケースはなかなかレアな気がします。

その場合も、長期トレンドを前処理で除いた上でk近傍法を適用すれば良いのではないか、と推察しています(素人考えですが)。

オートエンコーダ

f:id:mhiro216:20191124172224p:plain

最後に、非構造化データに対する異常検知の例として、オートエンコーダによる画像に対する異常検知を示します。

オートエンコーダの、自分自身を入力かつ出力にすることでニューラルネットワークに自分自身の特徴を"ざっくり"把握させ、異常検知に活用するという発想は、理論の詳細は分からずとも存在を知っているだけで、たくさんのユースケースが着想できるのではないかと思います。

まとめ

f:id:mhiro216:20191124172546p:plain

繰り返しになりますが、異常検知はルールベース・統計ベースの古い手法から、ニューラルネットワークを活用した教師なし学習のような新規な手法まであり、自分自身のユースケースにどの手法を適用すべきなのか、一度勉強・整理しないと分かりにくい領域だと感じました。

それだけに、問題を解決する我々データサイエンティスト側からすると、適切な手法の選択や学会発表したての新規な技術の適用といった点で非常にやりがいのある領域なのですが、

解決してほしい問題を提供するビジネス側の皆様におかれては、まずはユースケースの置かれた状況(より具体的には、データの特徴)を整理するところから始めて、なんでもない技術を不要に高く売りつけるベンダーに騙されないようにしていただきたい、ということも思いました。

参考文献・URL

追記予定

ホテリング理論と、k近傍法による異常度算出の実装について、後日Qiitaに公開します。

Kaggleコンペ初心者が命削りながらなんとかメダル圏内に滑り込んだ話 (IEEE-CIS Fraud Detection)

前回のブログ記事投稿から約1ヶ月。この1ヶ月はKaggleのIEEE-CIS Fraud Detectionに人生を捧げると決めてブログを休んでいましたが、10/4にコンペが終了しました。

結果は、6381の参加チーム中、532位でした。上位10%に入ることができ、初Kaggle本気参戦で銅メダルを獲得することができました。

しかし、2週間ほど前からあらゆる試行錯誤を繰り返してもPublic LBが上がらず、所謂「このKaggleコンペ何もわからない」状態に陥り、非常に苦しい思いをした記憶が強いです。

ということで、本記事はKaggleで初メダル圏内を目指そう、という方を読者に想定して、自分のやったことを書きます。

メダルを既に獲得されている方、ましてKaggle Expert以上の方で万が一本記事にたどり着かれた場合は、さくっと離脱いただくか、笑って眺めていただければと思います。

1. 目的を明らかにし、自分の能力や環境を客観的に評価して、目的に到達するための戦略を立てる

いきなり自己啓発本みたいな言葉を並べましたが、Kaggleも一つの競技である以上、何を目的としてやっていて、それに到達するための自分の能力や環境はどうで、だからこんな戦略をとるべきだ、ということを考えないで戦いに臨むのは愚かだと考えました。

自分の場合、目的は以下2点でした。

  • データサイエンスコンペティションを開こうとしている者として、Kagglerのお気持ちを理解するために全力で戦う
  • 自分の全力を引き出せるように、無謀ではないが高い目標を設定する。具体的には銀メダルの獲得

データを眺めて法則を見出すのが好きな自分としては、本来はEDAをフラフラやって楽しみたいなという気持ちもあったのですが、上記がより重要度の高い目的としてあったので、本音は封印して、結果を出すことを重視しました。

一方で、自分の能力や環境は以下の通り捉えました。

  • Positiveな面
    • 大学で学ぶレベルの統計・機械学習の知識はそこそこある
    • 大学院や仕事で英語に触れていたのでそこそこ読める
    • コンサル業界にいたことがあるので調査力(ググり力)はそこそこある
  • Negativeな面
    • Kaggle初本気参戦なので、全体として何をやれば良いか、その中で何に力を入れるのがスコアupのために重要か、何も分かっていない
    • プログラムを書き始めて歴が浅いので、何でもない処理を書くのにも他の参加者に比べてもたつく可能性
    • スタートアップを経営しながらの参戦なので、(多分)他の参加者に比べて潤沢な時間を投入できるわけではない

半ば必然的に導かれた戦略は、
変に自分の能力に期待せず、徹底的に先人の知恵を調べ尽くし取り入れる。さらに時間が限られているので、スコアを上げるために重要な作業に集中的に時間を投入する
という身も蓋もないものとなりました。

結果から言えばこの戦略は間違っていて、先人の知恵ばかりに期待して自ら有効な特徴量のアイデアを探索する姿勢が弱いようでは、銀メダル圏内に到達できるほどKaggleは甘いものではなかったです。

しかし、自分の目的と能力・環境を照らして戦略を立てたこと自体は間違っていなかったと思います。

2. 全体として何をやれば良いか、その中で何に力を入れるのが重要か、特定する

大方針が決まったので、次に具体的にやるべき作業を理解する段に移ります。

まず、全体として何をやれば良いか把握するところからですが、偉大な先人が素晴らしいまとめをしてくださっています!

qiita.com

Kaggle界隈ではとても有名なupuraさんの記事ですね。この情報がなければ当て所なくKaggleに臨んでいたと思うので、ゾッとします。

さらに、主にTwitterやslack(kaggle-ja)で調べを進めた結果、自分なりの解釈ですが、Kaggleでは全体として以下をやれば良いのだと理解しました。

f:id:mhiro216:20191006125412p:plain

そして、もし投入できる時間が限られているのであれば、力を入れるべき工程は「特徴量エンジニアリング」「Discussion & Notebook 読み込み」だと理解しました。

情報を集める中で、スコアを上げるために特に重要なのは有効な特徴量を発見する特徴量エンジニアリングである、ということが多く言われていました。

一方でハイパラチューニングなどは、議論はそこかしこでなされていますが、実際のところ大きくスコアupに寄与することは少ない、と言われていた気がします。

さらに、Kaggleの良いところは、Discussion & Notebookがあることで、他の参加者と発見した知見を共有しながら戦えることです。

他の参加者の知見だけでなく、今回のようにデータの秘匿性が深く、カラム名がマスキングされているコンペでは、コンペ主催者から開示できる範囲のデータの補足説明がなされたりします。

実際今回のコンペでは、コンペ主催者からの説明はスコアupのための大きなヒントが含まれていました。

前述の通り先人の知恵に学ぶと決めたので、Discussion & Notebookは毎日チェックすることにしました。

あまりにDiscussionの内容を参考にしすぎたので、危うく落とし穴にハマりかけたこともありましたが。。。

以降、各プロセスでやったことを記しますが、明らかに特徴量エンジニアリングに重きを置いた内容となっており、他の工程がスッカスカなのはご了承ください。

3. 実験環境構築

早速データ分析に移りたいところですが、特徴量を追加・削除してモデルを作りスコアの変動を見るというプロセスを何百・何千回と繰り返すことになるので、その作業を行う環境を固定し、できるだけ効率的に実験を回せるように準備しておくことが長い目で見ると重要です。

実験のプロセスですが、大きく分けると以下の2つから成ります。

  1. 新たな仮説に基づく特徴量を追加してみて、スコアupに寄与するかどうか、全データを使わずにクイックに検証
  2. 検証結果が積み上がってきたら、全データを使ってCross Validation (CV)しながらモデルを作り、出力をsubmitしてLeader Board (LB)の変化を確認

一方で実験に使える環境は、一般的には以下の選択肢があると思います。

  • 個人のローカル環境
  • Kaggle NotebookやGoogle Colaboratoryなど無償のコンピュータリソース
  • GCPAWSなど有償のコンピュータリソース

結論から言えば、私は
1のクイックな検証には個人のローカル環境+Kaggle Notebookを、
2のモデル構築にはGCPのGCEを、
使いました。

理由は単純で、個人のローカル環境やKaggle Notebookは高々十数GBのメモリしかないので、特徴量を何千と作り探索しようとしていた私のやり方では、簡単にMemory Errorとなってしまうためです。

GCPを使う最大のデメリットは有償であることで、今回私は3万円ほど使いました(他の方のお話を聞いていると、決して高くないと思います)が、学生さんなどはなかなか難しいかと思います。

有償のリソースを使えない場合、メモリに配慮しながらKaggle Notebookなどを使い回していくことになりますが、それでも金メダルを獲得されている方はいらっしゃるので、テーブルデータであれば十分可能と思います(画像データではなかなか難しいと聞きます)。

で、GCPを使った実験環境の作り方ですが、これも偉大な先人であるtkmさんが手順を残してくださっています。

www.youtube.com

動画の手順と若干異なるのは、私はvimに慣れていないので、ローカルのPyCharm上でスクリプトを修正してgithubにcommitし、GCEにSSHで接続した後githubから修正したスクリプトをpullして実行する、という手順をとっていました。

いずれにせよ、tkmさんは他の動画でBigQueryでKaggleのデータを分析する手順も残してくださっていたり、非常に有益なコンテンツが多いので、食事中にテレビ番組を見るくらいならこちらの動画を流した方がinf倍有益かと思います。

4. EDA

ここからデータ分析に入るわけですが、コンペ参戦を決めたのがコンペ中盤だったこともあり、既に先人が多くのEDAを投稿してくださっていました。

そこで、ざっとEDA系のNotebookを確認した後は、先に特徴量エンジニアリングに着手してしまうこととし、特徴量のアイデアに行き詰まったら自分でEDAをしてみよう、という手順をとることにしました。

5. モデル構築手法選択

テーブルデータにおけるモデル構築手法は、大きく2つの選択肢があります。

テーブルデータでは、多くの場合勾配ブースティング系の手法の方がスコアが高くでるようです。

また、後に出てくるハイパーパラメータチューニングの方法はもちろんのこと、一部の特徴量の作り方についてもGBDTとNNで異なるので、投入できる時間が限られる今回は複数の手法に手を出すのは得策ではないと判断しました。

そこで今回は、LightGBM一本に絞って取り組むことにしました。

この戦略が良かったかどうかですが、最後に行うアンサンブルで、モデルの予測値を複数組み合わせて最終的な予測とするということをしますが、類似のモデルではなく全く独立に作られたモデルを混ぜた方がスコアが良くなることが一般的に知られています。

従って、使う手法を一本に絞ったことはアンサンブルで不利になりますが、一方でLightGBM一本でも上位Solutionでは銀メダル圏内に到達する相当のスコアが出ていたので、やはり次の特徴量エンジニアリングが勝負の分かれ目だったと思います。

6. 特徴量エンジニアリング(Feature Engineering)

戦いのスタートにあたり、良さげなNotebookを見つけ、Forkします(こういうことができるのがスターターに優しくて良いですね)。

私はKonstantin YakovlevさんのNotebookを元にしました。

選んだ理由は、スコアが良いこともそうですが、コードがシンプルでわかりやすく、今後コードを改変して特徴量を追加・削除していくときに混乱しづらいと思ったためでした。

ちなみにこのKonstantin Yakovlevさん、大量のDiscussion & Notebookで参加者に知見を振りまきながら、2位以下を突き放して圧勝されています。

スコアでも教育目的でも多大な貢献をされていて、プラットフォーマーのKaggle的にはこれ以上の貢献はないでしょうね。いつかこの域に達したいものです。

次に、Discussionを眺めながら良さげな特徴量をピックアップし、追加・削除しながらスコアupに寄与するか検証していきました。

以下、私が試行錯誤した特徴量を書き連ねます。本コンペのDiscussionを元に試したものが多いですが、過去コンペで有効だった特徴量も調べて投入しています。

具体的な実装については、長くなるので別記事にまとめることとして、ここでは頭出しだけします。

主に参考にしたDiscussion:

行った特徴量エンジニアリング:

  • 既存のfeatureに対する処理
    • NANをfeatureの最小値より小さい値(-999など)で置換
      • LightGBMはNANがあるとnon-NANだけを分割し最後に残りのNANをいずれかのnodeに振り分ける。過学習につながるリスクがあるためNANは置換しておくのが一般的
    • binning
      • 量的変数を任意の境界値で区切りカテゴリ分け
      • 境界値は1000, 2000, 3000,,,などと一定間隔の値で決める方法もあれば出現頻度で区切る方法もある(histogramの作り方と同じ)
    • label encoding
      • string, category, object型の変数をint型に変換
    • outlier removal
      • 出現頻度が非常に低い値は-9999など(NANの置換に使った値とは違う値)で置換
      • 今回は異常値を見つける問題なので注意して使用すべき
    • TransactionAmtのlog1pをとる
  • 既存のfeatureをgroup単位でaggregation(例えば、card1のcategoryごとに平均をとって新たなfeature "card1_mean"とするなど)
    • sum, mean, std, min, max
    • frequency encoding
    • normalization
      • mean, stdを使ったnormalization、min, maxを使ったnormalization
    • target encoding
      • train dataのターゲット変数の値をgroup単位でmeanなどのaggregationを行うこと
      • やりすぎると過学習になる(未知のデータに対する精度が低くなる)ので初心者にはリスキー。私見ではこれを使わないで精度が上がる方法をまず探すべき
  • group自体を新たに生成
    • cardやaddrを結合してuser groupを生成し、aggregation
    • 今回は時系列データなので、TransactionDTを月/週/日/時間などに変換し、各々の単位でaggregation
    • 今回は時系列データなので、TransactionDTの前後X個のレコードについてaggregation (rolling window)
    • NANの数が同じ変数でグルーピング -> PCAに利用(実装の参考)
  • aggregationではない方法でfeatureを新たに生成
    • 量的変数の和・積・差・商
    • TransactionAmtの小数点以下(cent)のみを取り出す
    • lag/lead
      • 例えば、card1を使ったtransactionが1つ前に発生した時点までのTransactionDTの差(lag)、1つ後に発生した時点までの差(lead)
      • 実装の参考
    • pseudo-labeling
      • 一度予測を行い、予測値が0ないしは1に非常に近いtest dataのtargetに0ないしは1をlabelしてしまい、train dataに新たに追加
  • featureを縮約して新たなfeatureで代替(元のfeatureを残しておいても良いが、縮約したいというニーズがある以上普通は元のfeatureは削除する)
    • PCA
      • 量的変数をn次元に縮約して新たなfeatureとする
      • これが有効な理由は、決定木系アルゴリズムが軸に斜めの表現を不得手なため(参考
      • 実装の参考
    • LDA
      • LDAは文章をベクトル化するとき、出現する単語の出現回数で表現すると次元が極度に大きくなってしまうので、次元削減するときに使う手法
      • この発想をテーブルデータにも適用して、カテゴリ変数間の共起行列を作り、それに対してLDAを行い新たなfeatureとする
      • 参考

様々試した中、最も有効だった特徴量は、

cardやaddrを結合してuser groupを生成し、count, mean, stdなどでaggregation

でした。

アンサンブルする前のモデル単体で、Public LBは0.951+でした。

1st solutionでも同様にuser groupを生成してaggregationした特徴量を有効に使っていて、私との違いは、私はuser groupを特定する程度で止まっていたところを、1st solutionではuser個人を特定する程度までcardやaddrを結合した上で様々なaggregationを行なっていました。

user個人を特定することが重要だというのは以下のDiscussionを見てなんとなく気づいていたのですが、実験するところまで動けなかったのが敗因で、悔やまれます。

"The logic of our labeling is define reported chargeback on the card as fraud transaction (isFraud=1) and transactions posterior to it with either user account, email address or billing address directly linked to these attributes as fraud too. If none of above is reported and found beyond 120 days, then we define as legit transaction (isFraud=0).

ちなみに、大きくスコアをboostするfeatureをmagic featureと呼ぶそうです。コンペ中盤にKonstantin Yakovlevさんが「magic featureが3つあるよ」とおっしゃっているのを見て以降、ひたすらmagic featureを探していたのですが。。。

他にも「TransactionAmtの小数点以下(cent)のみを取り出し、aggregation」などもスコアupに寄与しましたが、user個人を特定した特徴量に比べると効果は小さく、本当にここで勝負が決まってしまったなという感じです。

7. 特徴量選択(Feature Selection)

特徴量を作成した後は、それが有効かどうか検証し、取り込むか否かを決める必要があります。

私は1変数追加するごとに一部データを使ってLightGBMでモデルを構築し、CVの結果をみて有効か判定していました。

LightGBMで検証するとそこそこ時間がかかるので、高速に検証を回したい場合はRidge回帰で代替する手法も一般的なようです。

また今回は採用しませんでしたが、LightGBMではfeature importanceを返してくれる関数が用意されているので、とりあえず大量に思いつく限りの特徴量を作って投入し、feature importanceが0より大きいものは全て採用する、という手法もあると思います(逆にfeature importanceが0より大きいものは、モデル単体の精度向上には寄与しているので、いくら小さくても除くべきではない。敢えてそれらを除いたモデルを作り、後でアンサンブルしたい場合は別)。

今回は大量にfeatureを作ればスコアが上がるというより、特定のmagic featureを見つけることが重要な気がして1変数ごとに検証する手法を採りましたが、これが良かったかどうかは分かりません。

ちなみに世の中にはboruta, NullImportancesなどのfeature selectionの手法もあるそうですが、なかなか扱いは難しいようです。

8. クロスバリデーション(Cross Validation)

Cross Validation (CV)が何かはここでは詳説しませんが、簡単に言えばTrain dataをさらにTrain, Validate dataに分割し、Test dataによる予測を行う前に精度を予測する手法です。

これがなぜ重要かというと、Public LBが計算される対象はTest dataの一部を切り出したもので、Test dataの中で分布に偏りがある部分を切り取っている可能性があったり、そもそも少ないデータ量が切り出されていたりで、最終的に順位決定に使われるPrivate LBとは必ずしも結果が相関しないことがあるからです。

尚、Public LBとPrivate LBが激しく変動することをShake up/downというようです。あまりにこれが大きいコンペでは参加者に混乱を巻き起こしますが、しかし毎回安定して勝っている方もいるので、CVをしっかり設計すれば安定して勝てるはずとも言えます。

それくらいCVは重要で、たとえPublic LBが多少下がっても"Trust your CV"してCVが良くなるモデルを採用していくのが望ましいとされています。

逆に言えば、"Trust your CV"できるCV Strategyを立てなければいけません。

CV StrategyってKFold以外に何があるの?と私も以前は思っていましたが、今回は以下の3つを検討しました。

  • KFold
    • Train dataをK個に分割。デフォルトでは連続するレコードのまとまりで分割するし、shuffle=Trueとすることでランダムな分割も可能
  • GroupKFold
    • 特定カラムのgroupごとにTrain dataを分割。今回のデータであればTransactionDTから算出したmonth別に分割するなど
  • TimeSeriesSplit

どのCVが良いのかですが、明確な判断基準はないものの、

CVのTrain dataとValidate dataの関係が、最後のPredictionでTrainするdataとTestするdataの関係と同様になるようにCVを設計する

のが重要なようです。

今回で言えば、Test dataはTrain dataより先の月のデータとなっていることがわかっていたため、TransactionDTから算出したmonth別に分割するGroupKFoldを採用することとしました。

9. ハイパーパラメータチューニング

LightGBM公式Documentに従ったチューニングを行うつもりでしたが、結局最後まで特徴量探索に時間をかけていたので、最初にForkしたNotebookの設定値を最後まで使っていました。

一応Optunaを使ってみたりもしたのですが、探索範囲が悪かったのか、広すぎたのか、CVを改善するパラメタは発見できず。

次回コンペの宿題ですね。

10. アンサンブル

アンサンブルは異なるモデルを複数作り、予測値の平均をとって最終的な予測とする手法です。

異なるモデルを複数作る方法は色々あって、

  • モデル構築手法を変える(LightGBM, XGBoost, CatBoost, NNで作った各モデルを混ぜるなど)
  • 特徴量を変える
  • ハイパーパラメータを変える
  • 乱数seedを変える
  • (邪道ですが)upされているNotebookの予測値と自分の予測値を混ぜる

などとありますが、今回はモデル構築をLightGBM一本でやっていて、かつ特徴量探索にほとんどの時間を振ったため、乱数seedを変えるseed averagingと、upされているNotebookの予測値と自分の予測値をsimple averaging / rank averagingする程度のことしかしませんでした。

ちなみにアンサンブルの各手法はこちらにまとまっています。

簡単なアンサンブルの結果、Public LBは0.952+まで到達しましたが、0.953の壁を破ることができず、Public LBでもPrivate LBでも銅メダル圏内に終わりました。

この0.953の壁が非常に高く、コンペ最後の1週間ほどは、努力が報われない辛さと不規則な生活によるダブルパンチでなかなかシンドイ日々を送っていました。。。

余談:毒入りKernel

余談ですが、コンペ終盤にPublic LBが0.9532のNotebookがupされました。これは当時銀メダル相当のNotebookで、一瞬色めき立ったものです。

しかし自分の予測値との相関をとってみると、初めの10万行は0.99+ですが、最後の10万行は0.09ほどしかないではありませんか! (今回のTest Datasetは初めの20%のレコードがPublic LB算出に使われ、残りの80%がPrivate LB算出に使われることはDiscussionから分かっていました)

つまりこのNotebookはPublic LBに過学習したもので、これをアンサンブルに取り入れてしまったりすると大きくPrivate LBを落としてしまうわけです。

TwitterではこのNotebookのことを「毒入りKernel」と呼んでいる方がいて、言い得て妙だなと思いました。

勝負事らしい騙し合いも一部では繰り広げられていましたという余談です。

感想

次回コンペに向けて

多くの時間を極端に特徴量エンジニアリングに振る戦略をとりましたが、それ自体は間違っていなかったと思っており、

Discussionを読み込む中で気になったものはすぐに実装に落として検証する、という姿勢を貫くことが次回コンペに向けては重要だと思いました。

後は、なんだかんだで投入した時間は平日夜は毎日2時間ほど、土日計16時間ほどに達していて、相当家庭に負担をかけてしまったなと思っており、

今回作成した特徴量作成のスクリプトなどを再利用できる形にして、効率的に戦うことも重視せねばと思いました。

匿名コンペについて雑感

今回のコンペはuserを特定し特徴量作成することが重要だったわけですが、当然ながらコンペのデータ提供者はuserデータを保持しており、匿名化されていたことになります。

つまり既に分かっている情報を解き明かすことに多くのKagglerの力が注がれていて、それって無駄じゃない?という考えも当然あります。

私も基本的には同意で、ただ難しいのは履歴データはデータが多ければ多いほど匿名性が落ちるので、userを仮名化していても、プライバシー保護しきれないリスクをデータ提供側が憂慮する気持ちも分かります。

加えて今回はFraudというsensitiveな情報が含まれるデータなので、k-匿名性を担保できない状態では、user情報を削除せざるを得なかったと想像します。

データサイエンスコンペは、仮に成果物が実務で役に立たなくとも参加者のスキルアップに繋がるし、何より1つの競技として面白く価値は高いと思うのですが、 一方で実務で役に立たないものに人生の多くを捧げている人もいることを思うと、資源が無駄使いされているようにも思います。

悩ましさを感じつつも、まずは不満の出ないコンペを開催することよりも議論を呼ぶコンペを開催することが正だと信じて、突き進もうと思います。

DAG(有向非循環グラフ)に対する最長経路問題(AtCoder Beginner Contest 139より)

最近はKaggleを優先していてなかなか競プロの時間を取れないのですが、AtCoderのABCには参加しています。

で、1週間遅れになってしまいましたが、先日のABC139でDAGの問題が出ました。
DAGは今後も重要そうな気がして、YouTube公式解説のC++のコードを読み解き、Python化しました。

問題内容・解法

E - League

atcoder.jp

解法の考え方については以下の公式解説が非常に分かりやすいです。

www.youtube.com

簡単に紹介しておくと、問題で与えられたデータは、リーグ戦をイメージして、各選手について対戦したい選手の順番が決まっているというものですが、これを試合idに読み替えます。

f:id:mhiro216:20190908141016p:plain

すると、これはDAG(有向非循環グラフ)に対する最長経路問題であると読み替えることができます。

f:id:mhiro216:20190908141439p:plain

上の例では、頂点Aから頂点Eへの移動が最長経路となり、値は4、つまり試合日程を全てこなすのに必要な最小の日数は4日である、ということになります。

DAGということはループが存在しないわけですが、この問題でなぜそう読み替えられるかというと、
「選手1はまず選手2と対戦したくて、選手2はまず選手3と対戦したくて、選手3はまず選手1と対戦したい」などというデータがあった場合、これを満たす試合日程を組むことはできないわけですが、これはグラフ上ではループとして検出できます。
問題文では条件を満たすように試合日程を組めなければ'-1'を返せと言っているので、ループが検出されたら'-1'を返すように実装することになります。

さて、以下がPythonによる実装になります。

import sys
sys.setrecursionlimit(10**6)
readline = sys.stdin.readline

MAXN = 1005
MAXV = MAXN*(MAXN-1)//2
to = [[] for _ in range(MAXV)] # 頂点間の辺の情報
id = [[[] for _ in range(MAXN)] for _ in range(MAXN)] # 試合のid=DAGの頂点番号
def toId(i,j): # 選手idから試合idを返す関数
    if i > j: # i,jが逆になっても試合idは同じ
        i,j = j,i
    return id[i][j]

visited = [False]*MAXV
calculated = [False]*MAXV
dp = [1]*MAXV # Vからスタートしたときの最長経路。頂点の個数ベースで経路の長さを数えるので、初期値は1

def dfs(v):
    if visited[v]:
        if not calculated[v]:
            return -1 # 計算が終わっていない頂点を2度訪れるのはループがあるということ
        return dp[v]
    visited[v] = True
    for u in to[v]: # 全ての辺をなめる
        res = dfs(u)
        if res == -1: return -1 # ループがあれば-1を返す
        dp[v] = max(dp[v], res+1)
    calculated[v] = True
    return dp[v]

def main():
    n = int(input())
    a = [[int(i) for i in readline().split()] for _ in range(n)]
    for i in range(n):
        for j in range(n-1):
            a[i][j] -= 1 # 選手idを0始まりに変換
    V = 0
    for i in range(n):
        for j in range(n):
            if i < j:
                id[i][j] = V
                V += 1 # 0から順に各試合にidを割り振る
    for i in range(n):
        for j in range(n-1):
            a[i][j] = toId(i,a[i][j]) # 選手idを試合id(頂点番号)に置き換える
        for j in range(n-2): # 頂点間の依存関係はn-2個
            to[a[i][j+1]].append(a[i][j])
    ans = 0
    for i in range(V):
        res = dfs(i)
        if res == -1:
            print('-1') # ループがあれば-1を返す(問題文の指示)
            return
        ans = max(ans, res)
    print(ans)
    return

if __name__ == '__main__':
    main()

ここで(大)問題は、Pythonでsubmitすると一部のテストケースでTLEになってしまいます(泣)。
リスト内包表記を入れるとかしてチマチマ高速化しようか悩んでいたところ、Pypyでsubmitしてみるという手があることを知りました。

[https://juppy.hatenablog.com/entry/2019/06/14/Python%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E9%AB%98%E9%80%9F%E5%8C%96tips%28Python%E3%81%A7Atcoder%E3%82%92%E3%82%84%E3%82%8B%E9%9A%9B%E3%81%AB%E5%80%8B:embed:cite]

Pypyで提出すると、無事ACとなりました。
低級言語への乗り換え圧力を強く感じましたが、なんとか乗り切ったことにします。