継続を目指して取り組んだ、2018年のブログ活動を振り返る

write-blog-every-week Advent Calendar 2018 の7日目の記事です。
https://adventar.org/calendars/2925 6日目の記事は、id:jalemy さんの「テキスト校正くん」を導入して読みやすい文章を書きやすくなった - むにえる牧場 でした。

write-blog-every-weekとは、毎週月曜日から日曜日までの間に、ブログに1記事上げることを目標としている会です。できた経緯はここを参照。
勢いで週一ブログ書くslackグループを作った - もがき系プログラマの日常

最近のLTでも今年のブログについてちょっと触れました。
(speakerdeckのURLを追記予定……)

1年の締めくくりの良い機会なので、この記事では今年のブログ活動を振り返る。(というわけで基本的には自分語りです。) 今年の始めから時系列順に書いていこう。

@kakakakakkuさんの記事を見てブログ更新を頑張る→挫折

旧ブログの、今年の目標の記事より引用する。

・ブログ週1で書く
間違いなくこの辺の影響ですね。
「ブログを書く技術」を発表した - kakakakakku blog
アウトプット駆動学習を習慣化する - kakakakakku blog
残念ながら俺にはブログメンターなんて素晴らしいものが無いので、どれくらい続くかすこぶる怪しいですが、この記事で3週間目です。
2018年の目標 子供の落書き帳 Remix

今年の始めの時点で、@kakakakakkuさんの影響を受けて、週一で更新しようと頑張っていることがわかる。
2018/03/17 まで(かなり甘めにカウントして)9週間続いていた。しかしその後は更新頻度が落ち、1ヶ月以上間が空いてしまった。

なぜブログが書けなくなるのだろう。
ブログを書き始めたのは2007年と、ブロガー歴だけは長いので理由はよくわかっている*1。 書いていても反応が全く無いと、どうしても張り合いがなくて続かなくなってしまうからだ。 雑踏の中で孤独に一人演説をしているみたいな気分になってしまう。

ブログメンターを受ける

そうこうしているうちに、@kakakakakkuさんの「ブログメンターをするのでメンティー募集するよ」という投稿を見かけて、申し込みをする。

6月6日に@kakakakakkuさんに申し込みをしている。 ただ、そのあとしばらくやり取りがあったので、実際に週一更新を始めたのは6月の中旬からである。 (余談だが、機械学習の勉強会で前で発表したのが6月15日だった。この時期は俺のモチベーションが高かったらしい。「コンフォートゾーンを脱出するんだ!」と思ってた。) 6月の後半と7月にメンタリングを受けた。 (メンティを卒業したときにエントリーを書く人が多いが、俺の場合は機会を逸して書いてない。)

教えてもらったことの1つで「GitHubのtrendingを見て、良さそうなライブラリがあったら、その紹介/試してみた を書く」 たまに暇な時に検索してますけど、見てるだけでも最近の流行りが分かってきて良い。 まだ「記事にする」とこまでは行けてないけど。

7月末に卒業し、これで週1で技術ブログの更新がはかどり、俺は圧倒的成長を遂げて身長も伸び、モテモテになり、年収も3倍に…… であればハッピーなのだが。そう上手くは行かなかった。

8月は持ち堪えたものの、9月中旬から更新が滞り始める。

やっっっっぱり、無いのだ反応が。
自分のブログの名前でTwitter検索しては、「なんか1つでも反応があれば俺は嬉しいのに……何もない……」と思っていた。

FC2からはてなに移行

最初のブログはYahooブログ、その次のがFC2ブログだった。

世の潮流は圧倒的にはてなブログだよなぁ、どんなもんか、まずは試してみるか……とはてなブログで少し書いてみた。 8月12日にこのブログの最初の記事を書いている。

それでそのままFC2ブログには戻らなかったので、はてなが良かったということだろう。良いところを挙げると以下の通りだ。

良いと思うけどなかなか良いと言い切れないところは以下の通りだ。

  • 他のはてなブログから俺のブログに対してリンクがつくと通知される(便利は便利なのだが、もともとブログ全体にあったトラックバックを、はてなの中に閉じた形で採用したので、はてな以外のブログの人は恩恵をうけることができず、微妙な気分ではある)
  • 「読者になる」ボタンの存在(便利は便利なのだが、各種ブログに使えるRSSリーダーを、はてなの中に閉じた形で……以下略)

write-blog-every-week Slackグループに入る

9月下旬。勢いで誕生したwrite-blog-every-week Slackグループに、勢いで入った。 kojirooooocks.hatenablog.com

@kakakakakkuさんのメンターだった人が多いが、もちろん誰でも歓迎である。

ちょうどこの辺のツイートをしてて、ブログへのモチベーションが下がってきていた時期である。

「人がやってるなら俺も」で10週続いています

(↑実は1回更新落とした) write-blog-every-weekグループの良いところを挙げてみよう。

  • 人がやってるなら俺も頑張ろうと思う(心理学でいうピア効果?)
  • 書いたことに対して反応がある!!!*2
  • botが各人のブログの更新を管理し、1週間のうちに書いていないと煽って……リマインドしてくる。メンバーがbotをいい感じに作ってくれました。俺にはできない。技術の力ってすげー。

機械学習pythonがメイン……しかし無関係な記事がバズった 複雑……

一応、メインは機械学習pythonについて書くブログのはずである、しかし、この前何の気なしに書いた全然関係ない記事が 人気を集めてしまい、「嬉しい……けどちょっと微妙な気分だ……」となった。 linus-mk.hatenablog.com (なんか役に立つことを書いたわけでもなく「分からん。困った。皆どうしてるの?」って記事なのに、なんでブクマが増えたんやろ……)

反省点

ここまでつらつらと今年の軌跡をたどってきた。 ブログを書く上での反省点を挙げる。

更新が日曜夜になることが多い

その結果、「あーっ、間に合わねぇ、仕方ないからこれで投稿しよう」となって、記事のクオリティが落ちることが少なからずある。

ネタ探しが定着しない

ネタが出てきたらEvernote内のネタ帳に記録する……はずなのだが、この間みたらネタ帳が1ヶ月以上更新されていなかった。
まだネタの書き留めが習慣化できてないなー。しばらくうまく回ったと思ったら忘れる…… 「毎日朝に」みたいな定期的なものじゃなくて、「ネタを思いついたときに」という不定期だから習慣づけが難しい。

ブログに時間を割きすぎてコード書く時間が減る

つらい。

ブログの記事(=1週間)より長い単位での学習計画が無い

詳細はさっきの記事を参照: エンジニアの自己学習の中長期計画をどう決めたらいいんだろう? - 子供の落書き帳 Renaissance

ブログを書き続けるのは難しい、だから

ブログのメンターを受けた人は週1件(以上)の更新をしていたわけだが、その後どうなったのだろうか。
はてなブックマーク - kakakakakkuに関するky_yk_dのブックマーク
にブログメンティ卒業したエントリーが6件あった。そのうち3人はブログの更新が止まり、残りの3人は今も更新し続けている。ついでにいうと、後者の3人はwrite-blog-every-week グループのメンバーである。

更新をしなくなった人を批判するつもりは別に全然ない。だって俺自身もメンティ卒業後週1度更新ができてなかった時期があるし。 それにメンティを卒業したあとのあとの更新はノルマではないし。
自分から志願してメンターをお願いしに行くほどやる気のある人たちでさえ、その後更新を続けるのは難しい。というのが言いたいことだ。 グループメンバーのtadaken3さんの記事経由で知ったけど、1年間続くブログは30%(3ヶ月に一回書けばとりあえず継続とみなすという激アマ条件)というデータもある。
誰かの後押しがなくなったときに、自分一人で走り続けるのは難しいのだ。
そして、write-blog-every-weekは頑張るブロガーたちが応援し合ってブログの更新を積み重ねていく、素敵なグループである。


8日目の担当は、技術同人誌を精力的に書きながらブログの更新も熱心に続けているid:konosumiさんです。よろしく!

*1:このうち大半の時期は技術ブログではなく適当な雑記ブログであった

*2:Slack上では絵文字で気軽にリアクションできるため、反応がほしいときには絶好のツールである。

pythonの音楽解析ライブラリlibrosaを試してみた

librosaというのはpythonのライブラリの1つであり、音楽を解析するのに使う。

python 音楽 解析」で検索してみると、結構な割合でlibrosaを使っている。

Pythonを使った音楽解析をやってみる - のんびりしているエンジニアの日記

深層学習を使って楽曲のアーティスト分類をやってみた! - Platinum Data Blog by BrainPad
この記事はブレインパッドの公式ブログのもので、音楽からアーティストの分類をしている。なかなか面白いなと思う。ここでもスペクトログラムを描くのにlibrosaを使っている。

どうやらlibrosaは、pythonを使って音楽の分析をしようと思ったらメジャーに使われているライブラリらしい。
しかし、pythonの良いライブラリを紹介しているawesome pythonには入っていない。(何でやねん。)
awesome-python: A curated list of awesome Python frameworks, libraries, software and resources

ちょっと使い始めて、公式サンプルを動かしてみたところまでのメモです。

公式ドキュメント

https://github.com/librosa/librosa
https://librosa.github.io/librosa/
同じようで少し違うドキュメントが2ヶ所に存在している。

上のgithub.comのほうに「Introduction notebook」のリンクがあった。リンク先のjupyter notebookを落として、動かしてみる。
なお、下のgithub.ioのほうがより詳細で、ライブラリ内の各関数の使い方が書いてある。

サンプルを動かしたらエラーが出た

librosaのインストールは簡単で、pip install librosaでインストールできる。
ダウンロードしたnotebookを起動して、実行……
あれ、エラーが出てきた。

---------------------------------------------------------------------------
NoBackendError                            Traceback (most recent call last)
<ipython-input-5-9f5c39d613b9> in <module>
      5 # audio_path = '/path/to/your/favorite/song.mp3'
      6 
----> 7 y, sr = librosa.load(audio_path)

c:\program files\python37\lib\site-packages\librosa\core\audio.py in load(path, sr, mono, offset, duration, dtype, res_type)
    110 
    111     y = []
--> 112     with audioread.audio_open(os.path.realpath(path)) as input_file:
    113         sr_native = input_file.samplerate
    114         n_channels = input_file.channels

c:\program files\python37\lib\site-packages\audioread\__init__.py in audio_open(path)
    114 
    115     # All backends failed!
--> 116     raise NoBackendError()

NoBackendError: 

librosaのForum上でも同じ報告が上がっているし、
githubのissueも立っている
これらを読むと、「対象のファイルの読み込みに失敗している(読み込める形式でない)。ffmpegをインストールすれば解決する」ということが分かる。

audio_path
 -> 'c:\\program files\\python37\\lib\\site-packages\\librosa\\util\\example_data\\Kevin_MacLeod_-_Vibe_Ace.ogg'

audio_path = librosa.util.example_audio_file() で指定されるファイルは、拡張子oggである。デフォルトで(ffmpeg不使用で)どの形式の音楽ファイルが読み込めるのかよくわからない。oggがデフォルトでは読み込めず、ffmpegを使う必要があるということだろう。

http://librosa.github.io/librosa/tutorial.html の方には

The first step of the program:
filename = librosa.util.example_audio_file()
gets the path to the audio example file included with librosa. After this step, filename will be a string variable containing the path to the example audio file. The example is encoded in OGG Vorbis format, so you will need the appropriate codec installed for audioread. と書いてある。「appropriate codec installed」ってのが「ffmpegをインストールしておけ」ってことなんだろうか?

https://www.ffmpeg.org/
からffmpegを落とすが、Windows版ではソースからビルドすることができないので実行ファイル形式でダウンロードする。 https://www.ffmpeg.org/download.html
から上の「Download」を選ばずに下のWindowsロゴマークから進み、ビルド済みの実行ファイルをダウンロードする。exeファイルのあるディレクトリに対してパスを通せば完了だ。

librosaチュートリアルでは何をやってるの

  • メル・スペクトログラムの描画
  • Harmonic-percussive source separation (HPSS) 音程のある音と打楽器の音を分離する
  • クロマグラム(Chromagram) 12音のうちどれが鳴っているか
  • MFCC (メル周波数ケプストラム係数)

など。

Machine Learning Casual Talks #7 イベントレポート #MLCT

2018年11月20日に開催されたこのイベントに参加してきました。 Machine Learning Casual Talks #7 - connpass 以下、敬称略です。

発表

@_stakaya(高柳慎一) 「The load to Machine Learning Engineer from Data Scientist」

  • LINE DEVELOPER DAY_2018に参加しましょう(この発表の翌日だった)
  • LINEのDataLab所属

    • DataLabとは……LINEの多くのサービスに対して一気通貫でデータ管理をする、横串の位置付けにある組織。その中のエンジニアの人は、LINE全社で使える機械学習基盤を作ったりしている
  • クオンツからここまで来るのにどういう経緯を歩んできたのか?

  • 社会人歴は13年程度

  • 景気のウェーブを感じろ! と常日頃から言っている。

    • 2006年に新卒で就職した (2000年のバブル崩壊から復調の兆しが見えた頃。新卒就活もまぁまぁ何とかなった)
    • 2008年10月に転職……と同時にリーマンショックを喰らう
      • 年功序列のおかげで居続けると賃金が上がる
      • けどキャリア採用の募集は皆無
  • 不景気だったときに新卒で入った人たちは、厳しい競争を勝ち抜いて入社した人。優秀な人という補正をかけてよい

  • 景気を意識して行動しろ。

    • なぜなら俺は6年ほど(不景気で転職できなかった時期に)エクセルのアドインとかコンサルタントをやってたからだ
  • 2011年の東日本大震災

    • システム障害を起こした銀行が、手作業で業務をしていた報道を見ていた上司曰く「最後は人だぜ!?」……いやそうじゃねぇだろ
  • 意思決定に必要なのはデータに基づいた事実ではなく、相手の弱みだったりする(「獅子のごとく」って本を読むとそのへんが分かるよ)

  • リクルート系に転職して思ったこと:

    • エンジニアやってるよりも、媒体のUXを改善したほうが金になるんじゃね?
      • KDDに行ってきて(宣伝:LINEでは会議に社費で行かせてくれるよ!)、Airbnbの人たちがABテストのメトリクスをどう測ればいいかを論じていたのを聞いてそう思った
      • 機械学習の精度を何%上げましょう、よりも収益に直接結びつく
  • 本橋智光(前処理大全の著者)が同僚だったが転職した。ので、俺も移るか、と思ってLINEに来た

  • 景気が良いから、私には「機械学習エンジニア」というラベルが貼られて、お金をたくさんもらえているという状態です。

@xecus(大田黒) 株式会社 ABEJA 「(仮)ABEJA InsightにおけるML活用サービスのデリバリー」

  • 2015年新卒入社。趣味はいろいろ工作すること。

    • 脈拍計で異常検知したり、FPGAやったり
  • 会社紹介

    • 2012年に創業
    • NVIDIAから、アジアで唯一出資してもらった
  • 2つのサービスを運営している

    1. ABEJA Platform AIプラットフォーム
    2. ABEJA Insight 特定業種向けのアプリケーション群
  • ABEJA Insightの中の1つであるABEJA Insight for retail

    • 店舗経営をする人向けのプラットフォーム
    • 映像を解析し、性別年齢を推定する
    • 最近はリピーター推定機能に取り組んでいる
    • 顔の特徴量の抽出は法的にグレーゾーン……
    • 経済産業省のワーキンググループに参加して、規則を整備しつつ開発した
  • データのデリバリーは考えればできるのだが、運用観点でのバックエンド設計ができずに辛かった

  • データ転送関連

    • 映像をクラウドに垂れ流しているので、「P2Pやファイル共有とかやってるんじゃないか」と、ISPや情シスからブロックされたりした
    • PASMO決済ができなくなったことがあった
  • 設置環境が多様になって、エンジニアの対応工数が増えてきた

    • 天井がない店舗で、「数十メートル離れたところからカメラ設置して年齢性別を取れるか?」と言われたり
  • バイス品質の見える化が大事 システムを導入して安心していたけど、実は導入してからが本番

  • 環境が変わることでデータ採取に失敗する例

    • 特定時間帯だけデータが取れないんだけど→逆光だった
    • オクルージョン(遮蔽物により認識したい物体の一部が隠れてしまう事象)
      • 店舗内の配置が変わるとカメラに映る人の顔が隠れたりする
      • 七夕イベントは要注意!笹の葉によるオクルージョンが発生する

LT

@techeten : Jupyter だけで機械学習を実サービス展開できる基盤 ~ サイエンティストとエンジニアの共生へ ~

リクルートグループのサービス横断の利益創出チームに所属
プランナー・サイエンティスト・エンジニアという組織
いろいろなサービスの運用から企画が上がってくるので、当たりを見つけて、見つけたら他に応用していきたい

本番環境から呼べるABテストのサーバーが必要
サイエンティストがエンジニアをしている余力がない

構築した基盤の詳細については、リクルートライフスタイルの技術ブログを参照 engineer.recruit-lifestyle.co.jp

自由に使えるjupyter環境
papermill を使って、notebookをそのままバッチ実行できる

kubeFlow, MLflowなどがあるが使わないのか?
工数はあまりとらず、今のところ2人月くらいで開発した。要件を満たす物があれば置き換えも可能 →構築した基盤環境は疎結合なので、いいツールがあったらパーツを置き換えられるようにしている

@yukiyan_w「機械学習基盤を一人で構築するということ」

TimeTree ユーザ数はグローバルで1000万 予定に関するターゲティング広告を出したい。そのためにユーザの予定を分類する

機械学習が未経験の状態から、だいたい3ヶ月でリリースした 学習時に単語の前処理に時間がかかる→spark(databricks)

やったことないのない技術は、PoCの延長ぽくなってしまう
→タスクの洗い出し、見積もりに重点を置いた。プロダクトオーナーとの握りの部分

技術的負債が貯まるのを避けるために、ドキュメントかいて口頭で共有した。

KiichiUeta 「JapanTaxiにおける機械学習活用事例」

ドライバ向け 流し需要予測システム

BIチーム データ基盤整理、レポート作成化・可視化
AIチーム 機械学習の高度なロジックを考える

エリア別の需要予測 未来の需要を予測して可視化する。500メートル四方のメッシュ。
過去実績に加えて、天気、電車運行、イベント情報、人口動態を入力とする
電車が止まるとタクシー需要が増大するため。
人口動態は携帯のGPSデータの集積

ピンポイントで需要スポットの予測 タクシーの乗車実績を入力とし、クラスタリング DBscanを使う

ディスカッション

(回答者は発表者の高柳さん・大田黒さん。どちらの回答であるかは下記に書いていません。このパートはスライドが無いので、うまくメモできなかったところは私の判断でカットしています。)

今後年収を上げるために機械学習エンジニアが身につけるべきスキルは何だと思いますか?

業界が物を言うので、高収入な業界に行くと良さそう。
素直にエンジニアやってれば良いんじゃないかと思う。
金になりそうなプロジェクトに自分から首を突っ込め

AWSGCPのインフラ周り
業界独自のドメイン知識を十分に持っていると強い

GoogleのAutoMLのような、機械学習エンジニアは今後いらなくなるんじゃないかという風潮についてはどう考えてますか?

最終的にはエンジニアによって終わるんじゃないかなー
継続的な運用が肝心だと思うので、autoMLでやってるのを継続的に精度改善とかしていく形になるのではないか 大企業の大規模データだと、AutoMLを使うと一瞬で大量の課金が発生してしまうので、使用は現実的ではない

効果的なエンジニア採用方法
勉強会とかリファラルとか、色々ですね。wantedlyで採用をやってはいるけど、なかなか難しい。
面白いデータを扱っている感を出すとよい
伝手はめっちゃ重要。
応募者のコードを見よう

技術ブログを書かないと「あの会社、何やってるか分からん」と言われて応募されない傾向にある

機械学習システムはかなり利用企業の事情に反映されやすいと思いますが、kubeflowなどで汎化は可能だと思いますか?

繰り返しになるが、運用フェーズが肝だなと思っている
顧客企業が少ないうちはいいけど、増えてくると運用に耐えられないことが起きる

パブリッククラウドほとんど使ってない、自社で管理している
LINEの技術ブログのどこかに書いてあるけど、OpenStackを使ったプライベートクラウドがほとんど

(このあたりの記事が近いか?)
LINE Engineer Insights vol.4「OpenStackベースのPrivate cloud "Verda" の野望」 - LINE ENGINEERING
OpenStackベースの自社クラウド "Verda" - LINE DEVELOPER DAY 2017|開発ソフトウェア|IT製品の事例・解説記事

関連リンク

Togetterまとめ
Machine Learning Casual Talks #7 まとめ #MLCT - Togetter

私はこの勉強会にブログ枠で参加していましたが、同じブログ枠にいた他のお二人が先に記事を書いていたので紹介します。

@__john_smith__さん
Machine Learning Casual Talks #7に参加してきました #MLCT | DevelopersIO

@tai_hatakeさん
Machine Learning Casual Talks #7@メルカリを聞いてきました! | 稼げるエンジニアになる

それでは。

エンジニアの自己学習の中長期計画をどう決めたらいいんだろう?

そういや、自分は自己学習についてロクに計画を立ててないなー、とふと気になったので、ブログ記事にしてみる。
今の自分の状況は次の通り。

  • 週に1回はブログを書くことに決めている
  • そのために、題材をネタ帳から選んで、適宜検証作業をしてブログを書く
  • それ以上の単位の学習計画はなし

振り返ってみると、ブログのおかげで「今週はこれに取り組む」はあるんだけれど、それ以上のまとまりが無い。
「今月は/この四半期はこれに取り組む」を特に決めていない。
その結果、なんか、一週間ごとの取り組みがつながっていなくてバラバラになっているような感覚・印象がある。

(以下があったときはそれに沿って勉強を進めていた。この手の勉強会があると適切なペースで勉強を進めることができて、しかも結果的に一定のまとまった範囲を習得できる。ありがたい。

現時点で、以下のような困りごとが生じている。

  • 良さそうだと思って買った技術書を読まず、本棚にしまいっぱなしにしてしまう
  • GitとAWSがいつまでも分からないマンになり続ける*1
    • Udemyで、AWSの良い講座があったので買ったのは良いけれど、まだやっていない(少しは観ました。少しは。)

機械学習・データ分析・pythonを基本的に優先して学習しているので、それ以外の分野は後回しにすることが多い。
すると、「上記の分野以外で、エンジニアならば習得しておくべき基本的な事項」が抜けてしまう。AWSとか。
でも今どき、「AWSは全然分かりません」じゃあ通用しないですよね、きっと。

先に書いた「困りごと」をもう少し抽象的に言うと、中長期的な計画を立てずにやってるから、「気づいたら、自分のスキルセットが思ってたのと違う。こんなはずじゃなかったのに……」となりがちなのだ。

計画を立てて学習していったほうがいいなー。
ただし、計画を立てるってことは、予定と実際の管理をして、振り返りをするあたりまでセットでやらないといけない。
週次で自己レビューやるの、少しやってたけど、習慣にならなくてやめちゃったんだよな……

エンジニアの人ってみんな、自己学習に対して定期的に

をしてるもんなのかな?

プライベートな勉強の中で、いつどの本を読んで、いつ何のサービスを作って、いつどの分野の勉強をして……って、どうやって決めてるんだろうか、みんな。
誰か教えてほしい。

*1:AWSおよびGitは少しは触ったけれど、忘れてしまった部分も多いし、自分のスキルに書けるほど習得していない

numpyの[::-1].sort()で配列を降順にソートできる理由を考える

きっかけ

東京大学 松尾研究室の「第3期 Data Science Online Course」が、先週から始まった。
online course3 | 東京大学グローバル消費インテリジェンス寄付講座
「グローバル消費インテリジェンス寄付講座」、Global Consumer Intelligenceを略してGCI講座と書かれることもある。……名称が結構色々ある。
13週間にわたってデータサイエンスの講座をオンラインで受講する。また、受講生はWeb上の環境を利用することができ、毎週課題を提出する。

なお講座のテキストはこちらで一般公開されている。
GCIデータサイエンティスト育成講座・演習コンテンツ公開 | 東京大学松尾研究室 - Matsuo Lab

先週は初回であり、基本的な内容だったのであまり難しくはなかったのだが、1箇所だけ引っかかった。それが今回の話である。

問題のコード:numpyの[::-1].sort()で配列を降順ソート

以下のようにするとnumpy配列が降順にソートできる、とテキストには書いてあった。

import numpy as np  
  
sample_array = np.array([1,4,2,5,3])  
  
sample_array[::-1].sort()  
print("ソート後:", sample_array)  
  
#→ ソート後: [5 4 3 2 1]  

……あれ?

  • sort関数は標準では配列を昇順にソートする
  • [::-1]は配列を逆順にする
    ということも講座テキストには書いてある。

俺の予想は

  • sample_array[::-1]は配列を逆順にするから、[1,4,2,5,3]を[3,5,2,4,1]にする
  • その後、sort関数で配列を昇順にソートするから、表示されるのは[1 2 3 4 5]である
    だった。しかし実際には、予想と違って降順にソートされている。

なぜなんだろう?

以下、

  • 1次元のnumpy配列(ndarray)のみを対象にする。2次元以上については触れない。
  • メモリの効率性については触れない。
  • 計算時間の効率性については触れない。

numpy.sortと numpy.ndarray.sortがある

少し紛らわしいが、ndarrayをソートするにはnumpy.sortと numpy.ndarray.sortがある。
numpy.sortはndarrayを引数に取って、その配列をソートした配列を返す。
https://docs.scipy.org/doc/numpy-1.15.1/reference/generated/numpy.sort.html
一方、numpy.ndarray.sortは呼び出し元のインスタンスであるndarrayをソートする。返り値はNoneである。
https://docs.scipy.org/doc/numpy-1.15.1/reference/generated/numpy.ndarray.sort.html

type(sample_array)  
#→ numpy.ndarray  

というわけで、sample_array.sort()は、numpy.ndarray型のオブジェクトであるsample_arrayのメソッドを呼び出している。今回使っているのはnumpy.ndarray.sortだわ。
以下、ndarray.sortと書く。

なお、numpy.sortを用いて配列を降順ソートする方法は後述。

スライスの中身を変えて試す→「その部分列をソートし、他の部分は不変」になる

どういうわけで降順ソートになるのか、納得できなかったので、スライスの中身を変えて色々試してみた。

sample_array = np.array([9,7,6,5,1,2,3])  
  
sample_array[0:3:1].sort()  
print("ソート後:", sample_array)  
#→ソート後: [6 7 9 5 1 2 3]  

sample_array[0:3:1] は配列の最初の3要素を指す。
結果を見ると、最初の3要素だけが昇順にソートされていて、他の値は変わらない。

sample_array = np.array([1,100,4,400,3,300,2,200])  
  
sample_array[::2].sort()  
print("ソート後:", sample_array)  
#→ソート後: [  1 100   2 400   3 300   4 200]  

sample_array[::2] は配列の最初から1つずつ飛ばしていった要素(配列を1番めから数えたときの奇数番目)を指す。
結果を見ると、該当する要素だけが昇順にソートされていて、他の値は変わらない。

sample_array = np.array([1,100,4,400,3,300,2,200])  
  
print("ソート前の部分列:", sample_array[::-2])  
sample_array[::2].sort()  
print("ソート後:", sample_array)  
#→ソート前の部分列: [200 300 400 100]  
#→ソート後: [  1 400   4 300   3 200   2 100]  

sample_array[::-2] は配列の最後から最初に向かって1つずつ飛ばしていった要素を指す。

結果を見ると、該当する要素だけが降順にソートされていて、他の値は変わらない。
この結果から考えると、
sort関数に[200 300 400 100]を渡して[100 200 300 400]を得て、それを元の場所に入れ直した。
つまり、もともと200があった場所に100を入れて……ということをやっているみたいだ……?
sort関数がやってることって、配列[200 300 400 100]から[100 200 300 400]への写像作用素と思っておけば良いのかな?

その部分列を取り出してソートするには?

部分列を取り出して昇順にソートしたものを得たいときにはどうするか。
まず、いったん他の変数に代入するという方法がある。

sample_array = np.array([9,7,6,5,1,2,3])  
  
a = sample_array[0:3:1]  
a.sort()  
print("ソート後:", a)  
  
#→ソート後: [6 7 9]  

または、ndarray.sortではなくnp.sortを使い、引数に部分列を指定する。

sample_array = np.array([9,7,6,5,1,2,3])  
  
b = np.sort(sample_array[0:3:1])  
print("ソート後:", b)  
  
#→ソート後: [6 7 9]  

最初の例に戻る

最初の例も、sample_array[::-2]と同様に考えれば一応納得できる。下記に再掲。

sample_array = np.array([1,4,2,5,3])  
  
sample_array[::-1].sort()  
print("ソート後:", sample_array)  
  
#→ ソート後: [5 4 3 2 1]  

sort関数に最初の配列の逆である[3 5 2 4 1]を渡して[1 2 3 4 5]を得て、それを元の場所に入れ直した。
結果的に、配列は降順ソートされた[5 4 3 2 1]になった。
(sort関数に何かオプションを指定したわけではないので、sort関数のほうは「降順にソートした」とは思っていない)

色々と実際に試してみて挙動を確認してみたけど、この辺が限界っぽい。
試してみるだけだと、なんでこういう風になっているのか、が分からない。
「sample_array[::-1].sort()は配列を逆順にしたあとにソートしていると思うが、なぜそうならないのか?」と聞かれたら答えられないしなー。
ndarray.sort関数には(C++言語でいうところの)ポインタとか参照が渡っているのか……?
(ここで唐突にC++が出てきたのは、俺が普段業務で触っているのがC++だからである)
公式ドキュメントを「reference」とかで探してみてもうまくヒットしない。

おまけ 配列を降順にソートする他の方法

個人的にはこのやり方で降順ソートをするのは直感的でないと思ったので、
別の書き方のほうが読んで分かりやすいのなら、そっちを使おうと思った。

sample_array = np.array([1,4,2,5,3])  
sample_array.sort()  
print(sample_array[::-1])  
#→[5 4 3 2 1]  
  
sample_array = np.array([1,4,2,5,3])  
print(np.sort(sample_array)[::-1])  
#→[5 4 3 2 1]  

ただし、降順ソートした結果を実際に使おうとすると変数に代入しないといけないから配列のコピーが発生する。

……と、記事を最後まで書いて気づいたけど、ほとんど同じ質問がStack Overflowにあったわ。
python - Efficiently sorting a numpy array in descending order? - Stack Overflow

それでは。

python環境が壊れた(おそらくcondaとpipの競合が原因)

pythonの処理系が壊れて、動かなくなってしまった。何をやったらどう壊れたのかは以下に詳述する。

(色々いじったあとの最終的な)環境

PS C:\> anaconda --version
anaconda-script.py Command line client (version 1.6.0)
PS C:\> conda --version
conda 4.5.11
PS C:\> python --version
Python 3.6.7 :: Anaconda, Inc.

OSは windows 7 64bit。

注意事項

正確な経緯を記録していないので、以下の記述には不正確な部分があると思います。
また最終的に環境が破壊されたことに注意してください(つまり、このページに書いてあることを実行しても、問題が解決するとは限りません)

shapをインストールした→scikit-learnのインポートに失敗

前の記事でSHAP valueについての解説を書いた。

linus-mk.hatenablog.com

せっかくなら、簡単なデータを入力して実際に試してみようと思って、以下のコマンドを実行してshapのライブラリをインストールした。

conda install -c conda-forge shap

これが全ての元凶であるとは知る由もなかった。
そして試しにプログラムを動かそうとすると、なんだか挙動がおかしい。scikit-learnが動かない。

import sklearn と書くと

・DLL load failed: 指定されたモジュールが見つかりません。

というエラーが出るようになってしまった。

condaのupdateに失敗

この現象は上記のscikit-learn失敗と関係があるのか無いのか不明。
condaでライブラリをインストールしようとすると
「"conda update -n base conda"を実行してcondaをアップデートしろ」という旨の警告が出るんだけど、
そのコマンドを実際に実行してもcondaはアップデートされず、同じ警告が出続けるという謎の現象が起きた。警告文は以下の通り。

Please update conda by running
$ conda update -n base conda

検索して見つけた、https://github.com/conda/conda/issues/6591 などのページには

conda update -n base -c defaults conda

を実行すればOKと書いてあるけど、実際にはこのコマンドを打っても状況は変わらなかった。

conda update --all →jupyter起動失敗

conda update --allを実行して、全てのライブラリが更新されたというメッセージが表示された、と思う。
しかしその後からjupyter notebookが起動しなくなった。起動しても以下のエラーメッセージが表示される。

AttributeError: type object 'IOLoop' has no attribute 'initialized'  

エラーメッセージで検索して以下のページを発見。
怖いものなんてない!!: Jupyter Notebookを起動するとAttributeError: type object 'IOLoop' has no attribute 'initialized'とエラーが出る
AttributeError: type object ‘IOLoop’ has no attribute ‘initialized’の解決 – LilPacy.info

pyzmqとtornadoのバージョンが競合しているらしいので、
tornadoのアンインストール・再インストール(バージョンを4系にする)か、pyzmqのアップグレードか、どちらかかなと思った。
簡単な方のpyzmqのアップグレードからやってみることにした。

pyzmqアップデート

conda upgrade pyzmq
pyzmq:     16.0.2-py36_0     --> 17.1.2-py36hfa6e2cd_0

これでも動かなかったので、tornadoのアンインストール・再インストール。

tornadoアンインストール・再度インストール→jupyter kernel再起動

再インストールまでやってみたが、jupyterの調子が相変わらずおかしい。
何らかのライブラリをimportするコードを書いて実行すると、下記のメッセージがブラウザ上に出て、kernelが再起動する。

Kernel Restarting
The kernel appears to have died. It will restart automatically.

ここまで色々やってきて、これはもう直る見込みがないのではと諦めた。
(最初の環境はこの時点でのものである)

原因はcondaとpipの競合っぽい

いろいろ調べたけど、Anacoda内のパッケージ管理ツールであるcondaと、python標準のパッケージ管理ツールであるpipを混ぜて使うと
ライブラリの依存関係がおかしくなって危険らしい。
condaは独自の仕組みでパッケージをインストールするので、pipとは互換性が無いのだ。

この辺を参照。
condaとpip:混ぜるな危険 - onoz000’s blog
wheelのありがたさとAnacondaへの要望 - YAMAGUCHI::weblog
【python】scikit-learnのImportErrorが出たのでAnacondaを再インストールした(泣く) - 僕の世界観を変えてみる

既に構築済みの自分のconda環境でpipとcondaの衝突があるか確かめたい場合は、conda listを実行する。同じパッケージがpip経由とconda経由で入っている場合重複して表示される。何かがおかしくなっている可能性が高い。

condaとpip:混ぜるな危険 - onoz000’s blog

と書いてある。 実際にconda listを実行してみると、確かに同じパッケージがpipとcondaで二重に入っていた。
conda listを実行した出力は200行以上になったので、一部を抜き出して書いておく。pipと書いてあるもの、pipとcondaでダブっているものだけを下記に記載する。

# packages in environment at D:\Code\Anaconda3:
#
# Name                    Version                   Build  Channel
awscli                    1.11.158                  <pip>
botocore                  1.7.16                    <pip>
chainer                   4.1.0                     <pip>
easydict                  1.7                       <pip>
filelock                  3.0.4                     <pip>
jmespath                  0.9.3                     <pip>
mock                      2.0.0                     <pip>
numpy                     1.15.3           py36ha559c80_0  
numpy                     1.14.5                    <pip>
numpy-base                1.15.3           py36h8128ebf_0  
pbr                       4.0.0                     <pip>
protobuf                  3.6.0            py36he025d50_0  
protobuf                  3.6.0                     <pip>
rsa                       3.4.2                     <pip>
s3transfer                0.1.11                    <pip>
setuptools                39.2.0                    <pip>
setuptools                40.4.3                   py36_0  
six                       1.11.0                   py36_1  
six                       1.11.0                    <pip>

おそらく、chainerは公式ドキュメントを見てうっかりpipで入れたんだろうと思う。あとはよく覚えていない。numpyは二重に入ってしまっているようだ。

Anacondaをアンインストールした

環境を再構築するために、まずはAnacondaをアンインストールすることにした。
Anacondaは低品質のWeb記事が出回っているっていう話も見かけたし、ちゃんと公式ドキュメントを見て作業するか」と思い、公式ドキュメントの以下のページを参考にした。
Uninstalling Anaconda — Anaconda 2.0 documentation

とはいえ、作業自体は単純なものだ。
コントロールパネルを開いて「プログラムと機能」に進む。
Python 3.6.0 (Anaconda3 4.3.1 64-bit)を選択してアンインストールを実行した。
膨大なライブラリ群を全て削除する必要があるので、アンインストール完了まで10分程度の時間がかかった。

Anacondaは止めて……とは言え、この辺のページを見ていたら、パッケージの管理ツールって色々な種類があるらしい。
Pythonの環境管理ツール良し悪し - Zopfcode
Pythonの仮想環境構築(2017年版) pyenvとpyenv-virtualenvとvirtualenvとvirtualenvwrapperとpyvenvとvenv - Qiita
pyenvが必要かどうかフローチャート - Qiita
Pythonの環境構築を自分なりに整理してみる – Aki Ariga – Medium

  • virtualenv
  • venv
  • pyenv
  • pipenv
  • pyenv-virtualenv
  • virtualenvwapper
  • pyvenv

おいおい。種類ありすぎだよ。慣れてない人には難しすぎるわ!
でも単純に機械学習の勉強をするだけなら、環境を隔離して新たに作らなくても、ベースになる環境にnumpyとかmatplotlibとかを直接入れていけばいい気がする。
ちょっとどうするか考えよう……

続・機械学習モデルを解釈する方法 SHAP value

前回の話


kaggleの中に、Machine Learning for Insights Challengeという4日間の講座がある。
後半2日間はSHAP valueが題材だったので、SHAP valueについてまとめる。
Machine Learning for Insights Challengeの内容、前半2日間の内容については前回のエントリを参照。
linus-mk.hatenablog.com

ちなみに今気づいたのだが、この4日間講座はkaggle Learnの講座一覧の中に加わっている。以下のサイトから各日の講義や演習問題に行ける。
Machine Learning Explainability | Kaggle
……って、タイトルに『Explainability(説明可能性)』って書いてあるじゃん! やっぱり解釈可能性の話じゃん!
最初の講座のときはExplainabilityなんて一言も書いてなくて、謎の「insight」って書いてあったのに……

SHAP valueとは何か

SHAP value(SHapley Additive exPlanationsの略) は、それぞれの予想に対して、「それぞれの特徴量がその予想にどのような影響を与えたか」を算出するものである。

1つのインスタンスを指定すると、このような図ができる。(講座ページから引用)

f:id:soratokimitonoaidani:20181027155817p:plain
SHAP value の例

赤色の矢印は予測値の増加を表し、青色の矢印は予測値の減少を表している。
また、SAHP valueでは「ベースラインの値」との比較を実施している。実際には、「ベースライン」というのは(データセット全体の)期待値である。

用いたデータセットPredict FIFA 2018 Man of the Match | Kaggleであり、これはサッカーの試合のデータである。
「各試合でのチームの成績データを入力として、そのチームの選手がその試合でMan of the Match(サッカーなどスポーツの試合で最も活躍した選手)を獲得したかを予想する」
1つのインスタンス(ある試合での片方のチームの成績データ)を入力すると、モデルが「このチームがMan of the Matchを獲得する確率は0.70だ」と判定したが、その内訳を特徴量ごとに分解して示してくれる。

例えば、赤色の最長の矢印は「Goal Scored = 2」と書いてある。この矢印の長さは約0.11である。
そのチームの得点が、ベースラインの値(データの平均値)の代わりに2であったことで、Man of the Match獲得確率は約0.11ほど増加した、という風に読める。
逆に、青色の最長の矢印を見ると、そのチームのBall Possession % (ボールのポゼッション、支配率)が、ベースラインの値の代わりに38%であったことで、Man of the Match獲得確率は約0.06ほど減少した、という風に読みとれる。

応用(どういう目的で使えるか)

次のような応用に使うことができる。

  • 銀行がある人にお金を貸すべきでないと機械学習モデルは言っており、銀行はそれぞれのローン拒否の根拠を説明する法的な必要がある
  • 医療提供者は、それぞれの患者がある疾病にかかるリスクをどの要因が高めているのかを特定したい。狙いを定めた健康介入によって、それらのリスク要因に直接対処できるようにしたいからである。

数学的な背景については省略

1日目のPermutation Importanceや2日目のPartial Dependence Plot (PDP)では理論的な説明があった。しかしSHAP valueについては、数学的・理論的な仕組みにほとんど触れていない。

感覚的に言えば、SHAP valueは「特定のインスタンスとベースラインとの差分」を、それぞれの特徴量に関して「分割して」くれる。どうやって差分を「うまく分割」しているのかがアルゴリズムの鍵なのだが、その仕組については講座の中では言及していない。

4日目の方では、単純な例として
 y = 4x_1+2x_2

を考えて、 x_1が0(ベースライン)から2に変化したら、 x_1に対するSHAP_valueは8になるよ、と書いてある。
……いや、そりゃ、 x_1 x_2の影響が分離されているモデルなら各変数の寄与を求めるのは簡単である。
モデルが仮に
 y = 4x_1+2x_2 +x_1x_2
だったら、 x_1がyにいくら寄与したかを判定するのは難しくなる。
 (x_1, x_2) が(0, 0)から(2, 3)に変化したらyは0から20 (4×2+2×3+2×3) に変化するわけだが、ではこの20のうち x_1による変化はいくつだろうか。これはそんなに自明ではない。
ましてや、機械学習モデルのような複雑な式の場合にはどうやってそれぞれの特徴量の寄与を計算するんだろうか?

個人的には背後の理論はとても気になるところだが、理論を追うよりも、手を動かしてコードを書けるようになることに注力したいので
今は数学の理論は一旦おいておこう。

オプション:Summary Plots 特徴量ごとに分布を見る

SHAP valueのライブラリにはいくつかの描画方法があり、その1つには特徴量ごとに分布を見られるsummary_plot関数がある。

以下の図は講座ページからの引用。 f:id:soratokimitonoaidani:20181027173512p:plain

  • 縦にそれぞれの特徴量が並んでいる。
  • 1つの点が1つのインスタンスを表す。
  • 点の色は、その特徴量の値が大きいか小さいかを表す。
  • 横の位置は、そのインスタンスの特徴量のSHAP valueの値を示す。

その他の色々な描画方法については以下を参照。
SHAPライブラリのGitHubGitHub - slundberg/shap: A unified approach to explain the output of any machine learning model.
SHAPライブラリの公式ドキュメント:Explainers — SHAP latest documentation

例:Irisデータセットの場合

最後に、試しにIrisデータセットに対してRandom Forestを適用して、SHAP valueを表示させてみよう。

from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier

iris = load_iris()
rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1, random_state=42)
rnd_clf.fit(iris["data"], iris["target"])

row_to_show = 76
data_for_prediction = iris["data"][row_to_show]  # use 1 row of data here. Could use multiple rows if desired
data_for_prediction = pd.Series(data_for_prediction, index=iris["feature_names"])
data_for_prediction_array = data_for_prediction.values.reshape(1, -1)

rnd_clf.predict_proba(data_for_prediction_array)

# → array([[0.   , 0.966, 0.034]])

76は適当なサンプル。(クラス1で、正しいクラスの確率が高くなりすぎないものを適当に選んでいる)
今回の76番はクラス1のサンプルである。そして学習したモデルの予測結果は、「確率0.966でクラス1」となっている。すなわち、正しく予測できている。
では、このサンプルに対して正しく学習できたのはどの特徴量のおかげだろうか?

Irisデータの特徴量は4次元であるが、最初の2次元の特徴量はあまり重要ではない。
pythonで散布図行列を描く でIrisデータの散布図を描いているのを見ると、
最初の2つの特徴量を見ても、'versicolor'と'virginica' (クラス1と2)は分離できないことが分かる。
大雑把に言えば「最初の2つの特徴量を与えられてもクラス1だとは分かりません。だけど、後半の2つの特徴量を見るとクラス1だと予想できます」という結果になるはずだ。
以上の予想を踏まえて、SHAP valueを表示させてみる。

import shap  # package used to calculate Shap values

# Create object that can calculate shap values
explainer = shap.TreeExplainer(rnd_clf)

# Calculate Shap values
shap_values = explainer.shap_values(data_for_prediction)

target_class = iris["target"][row_to_show]
shap_values[target_class]
# → array([0.02789272, 0.00855236, 0.23464138, 0.36383353])

iris["data"][row_to_show]
# → array([6.8, 2.8, 4.8, 1.4])

shap.initjs()
shap.force_plot(explainer.expected_value[target_class], shap_values[target_class], data_for_prediction)

f:id:soratokimitonoaidani:20181028225048p:plain

確かに前半2つの特徴量のSHAP valueは小さく、後半2つの特徴量のSHAP valueは大きい。
「最初の2つの特徴量を与えられてもクラス1だとは分かりません。だけど、後半の2つの特徴量を見るとクラス1だと予想できます」という事前の予想と合致した出力になっている。