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だと予想できます」という事前の予想と合致した出力になっている。

pythonのcodecsモジュールでrot13暗号の変換をした

ブログのネタが無さすぎて苦しんでいたが、ふと見るとペットボトルが目に留まった。今年のpyconjpでもらった水のペットボトルだ。

そして、外側のフィルムには妙な問題が書いてある。

super_difficult_encryption = '?????'

import codecs
print(
	codecs.decode(
		'FRAFL ybirf Clguba naq NV!',
		super_difficult_encryption
	)
)

そのままじゃ読めない暗号を解読してくれってことだろう。
俺がいた大学の数理情報工学では暗号理論も多少勉強するし、東大の五月祭の出展では暗号を題材にしたこともある。暗号の話は多少心得がある。
復号のための鍵もないから、これで戻すとなるとrot13あたりじゃねーか?

と思ってpythonのcodecモジュールを確認すると、rot13があったので、'?????'を'rot13'に変えて実行。

SENSY loves Python and AI!

よし。無事に暗号が解読できた。なおSENSYというのはPyCon JPのスポンサーで、この水を提供してくれた企業である。

rot13とは

アルファベットを13文字ずらす暗号である。(アルファベットは全部で26文字なので、もう一度この操作をすると元に戻る。)
暗号文'FRAFL ybirf Clguba naq NV!'の最初のFを復号すると、Fの13文字先にあるSになる。

codecsモジュールとは

codecsモジュールは暗号を作るためのモジュールなのかと思ったが、そうではなさそうだ。

7.2. codecs ? codec レジストリと基底クラス
このモジュールは、標準的な Python codec (エンコーダとデコーダ) 用の基底クラスを定義し、codec とエラー処理検索プロセスを管理する内部の Python codec レジストリへのアクセスを提供します。多くの codec はテキストをバイト形式にエンコードする テキストエンコーディング ですが、テキストをテキストに、またはバイトをバイトにエンコードする codec も提供されています。
7.2. codecs — codec レジストリと基底クラス — Python 3.6.5 ドキュメント

うーん。分かりにくい。初めての人がこれを読んでも分からないよ……

このページの方に各国のエンコーディングが載っていたので、文字エンコードの方式変換をするんだろう、きっと。

ただ、codecモジュールでできることは文字エンコードの変換に限らない。
例えばBase64エンコード/デコードも可能だ。
ただし入力はbytes-like objectに限られるので、文字列をそのまま入れることはできない。

import codecs

print(
	codecs.encode(
		b'Machine Learning', 'base64_codec'
	)
)

出力:
b'TWFjaGluZSBMZWFybmluZw==\n'


入力文字列の前のbを省くと
TypeError: encoding with 'base64_codec' codec failed (TypeError: expected bytes-like object, not str)
というエラーが出た。
base64_codecを使うときは、文字列(str)じゃなくて、bytes-like objectを入力してください」という意味である。

日々のアウトプットが変える!あなたのエンジニア・ライフ イベントレポート

2018年10月9日に開催されたこのイベントに参加してきました。 ※増枠【エンジニアのキャリアアップを語る】日々のアウトプットが変える!あなたのエンジニア・ライフ - connpass

まつもとりー(松本亮介)とKwappa(塩谷啓)さんのお二人による発表でした。 早速お二人の資料が上がっています。

資料を読めば分かることは適宜省いて、口頭で補足していたところを中心に書いています。資料と併せて読んでいただければと思います。

なぜポートフォリオが必要なのか - エンジニアと人事(評価・採用)両方向の視点から 松本亮介(まつもとりー)

なぜポートフォリオが必要なのか - エンジニアと人事(評価・採用)両方向の視点から - Speaker Deck

  1. はじめに
  2. 自分の体験
    • 6年前、28歳のとき…… 博士課程の1年目
    • OSS開発:Webではよくあるやり方
      • ビルドは通ってるけど「これ何に使うんだろう……」というあたりで公開していた
    • 博士課程に入って一番びっくりしたのは、オープンにできないことが多いこと
    • オープンに学術研究したいと思ったから大学院に入ったのに……!
    • でもオープンにやってみた
    • 批判を受けた
      • 論文ネタをオープンするなんて……!
      • 怒られたとしてもやりたいようにやっていこう
    • 指導教員の教授に相談してみた
      • 「松本さんが良いと思うやり方で好きなようにやってください。今のやり方、良いと思いますよ」
      • 迷いが消えた
    • ブログを書いても書いてもはてブ3とかしかつかない……
    • 徐々に広がり始める
      • 技術ってフェアというか平等なものなので、ネガティブなことを言われたら技術で見返そうと考えた。
      • 徐々に連鎖がつながっていって、少しずつ講演も依頼されるように
    • なんとなく自分の立ち位置が見えてくる
      • 強者の中で、みんなやってる中で自分も頑張ろう……と勉強を続けていると、どんどん成長してきた
      • 情熱が飛び火する 技術がただただ面白い
    • 頂上が見えてきた
      • 自分の得意分野、専門分野がどこか?
      • トップレベルの技術や人や企業はどこかが分かる
      • 自分を活かせる道が見えてくる
      • アウトプットというのは、自分のことを整理して外に出すこと
      • 脳内でふわっと曖昧だったものが、アウトプットすると明確化される
  3. 体験からポートフォリオの重要性を検討
    • エンジニア目線
      • 自身のスキルの明確化と再認識
        • 曖昧なままだと綺麗に整理理解できない。
        • アウトプットする中できちんと整理する
      • 周りを巻き込むことができる
        • アウトプット→スキルがつく→よりレベルの高いアウトプットができる
      • 先を予測できるようになる
        • アウトプットというものが、自分のスキルのストーリーのようなものになる
        • 自分のこの先目指す道、自分はこれが得意なんだ、などなど
    • 人事目線
      • スタンフォード助教とか取るときはまるまる3日間かけるらしい!
      • ちょっとの面接で採否を決めるのは本当に大変
      • アウトプットを永続的に残すことで、客観的な実力がわかる
    • アウトプットをまとめて、スキルのストーリーを作る

実践的アウトプット入門 塩谷啓(Kwappa)

実践的アウトプット入門 なぜ?なにを?どうやって? / Kwappa さん - ニコナレ 「日々のアウトプットが変える!あなたのエンジニア・ライフ」というイベントに登壇してきたよ #forkwell | Kwappa研究開発室

  • アウトプットとは?
  • なぜアウトプットするか?
    • データソースとして
      • 問題とその解決をコツコツログに書いて積み重ね
      • 学校の授業でノートに書くのって、後から見返す必要ないらしいですよ
    • 学びの定着と共有
      • 見せる前提で書くと理解が深まる
        • 人に見せられるようにしようと、綺麗なコード、より良い設計などを目指すから
      • 伝えることによって価値が増える
        • 誰か他のエンジニアの調べる時間を節約できる
    • コミュニケーションツールとして
      • 社内だと評価面談とか
      • 社外だと採用面接とか
  • 何をアウトプットするか?
    • 仕事メモ
      • データソースとして良い
      • 仕事中、何があって、何が困って、どうやって解決したかを書き留めたもの
      • 評価面談に使えるという面もあるけど
      • ネタ帳であるという面を意識しておく
    • 技術メモ
      • 上記の仕事メモの中から技術的な面を選り分ける、フィルターしたもの
      • 可搬性(社外に公開できるやつ)
        • 社外秘の部分を公開するわけにはいかないので 
      • 課外活動 趣味でアプリ作ってみたとか。
        • 「面白そうな技術があったので、触ってみた」でおしまいじゃなくて、困ったこと解決した方法を書いておけばネタ帳になる
    • プロダクト
  • どうやってアウトプットするか?
    • 自分用に書く
      • とにかく書く
      • ネタ帳、可搬性
      • メモツールにこだわってみよう Day Oneを使ってます
      • 「とにかく書く」といいながらも、再利用性も大事
        • 評価面談のネタにする
        • ネタ帳 などなど
    • 公開用に
      • ソースコードの場合はGitHub最強
      • 著書があると Amazonセントラルのページが作れる
        • フリーランスの人が住宅を購入するとき、融資の参考に使われたりするらしい……
      • こんな俺がこんな記事を書いてもつまらないんじゃ……って過度に萎縮しない。
        • 今の自分にできる、最高のアウトプットをし続けていればそれで良い
    • 人前でしゃべる

ポートフォリオレビュー

まずは、まつもとりーさんKwappaさんのポートフォリオを見た。その後、参加者の中から事前に選ばれた2人のポートフォリオのレビューの時間となった。

@ikedaosushi Forkwell Portfolio - エンジニア向けのポートフォリオサービス
@sachi21 Forkwell Portfolio - エンジニア向けのポートフォリオサービス

2人ともかなりの強者エンジニアで、すごいなーと思いながら見ていた。

質問:どういうことを書いたら自分の実績としてアピールできるだろうか?
書くことないなーと思ってたけど、毎日8時間仕事をやってれば何かしらあるわけで
 書き出す 自分の実績 日々やってきたこと中から紡ぎ出す

 OSSに関わる(自分でOSS製品を作る)という方法もあるよ
 OSSにするコツがだんだん出来てくる

質問:技術ブログ書いても反応がないから心が折れてきた
 Matzさんが「自分を踏み台にしてほしい」と言っていたことがあるけど
 自分のやっている分野で影響力ある人、影響力あるプロダクトとかに絡んでいく

(ikedaosushiさんに対してのコメント)
自分の強みをアピール出来ていて良い。
職歴を見ていると色々と面白そうなことをしてるのが分かる。その中に興味深いものがあって、もう少し知りたい、と思ったときに、その点の関連ブログ記事が(一般化されたとこで)ブログで書いてあったりするとさらに良い。

(sachi21さんに対してのコメント)
継続性 「いろいろやってるけどあんまり継続してないんじゃないか?」と思われがちなので、それに対して「GitHubで4年前から毎日草をはやしてます」と示せるのは素晴らしい。
手を動かすのに勝るものは無い

感想

雑感2点。

GitHubの共通言語っぷりがヤバい

最近、scounty、findy, そしてこのforkwell portfolioと、「どんなコードを書いてるか、GitHubリポジトリから分析しますね」というサービスがあまりに多い。
もはや「GitHub上にないコードは、あなたが書いたものではないと思え」という勢いだなと感じる……
(今更気づいたのかよ、とツッコまれそうだが)

ちゃんとGitHubに出すことを心がけよう。いきなり毎日やるとかは無理だけど。

課題をどう解決したか、を整理する

普段の業務でもプライベートの趣味の開発でも言えることだが。
「課題にぶつかって、いろいろ考えて解決しました」は誰しもやっていることである。
しかし大事なのはそれを明確に筋道立てて説明できること。
悲しいかな、この辺がしっかり説明できないと、「作業するとき何も考えてませんでした」に見えてしまう。
一旦整理してまとめたほうが良いな。

それでは。

機械学習モデルを解釈する方法 Permutation Importance / Partial Dependence Plot

Machine Learning for Insights Challengeで勉強した「Permutation Importance」と「Partial Dependence Plot」についてまとめる。

Machine Learning for Insights Challengeとは

9月18~21日に、kaggleの「Machine Learning for Insights Challenge」という講座が開催された。
1日に1通メールが来て、機械学習関連の話を学べる。
この記事では、4日のうち前半2日間の部分をまとめる。

なお、教材は公開されているので、今からでも同じように学習できる。
それぞれの日の説明には演習問題がついていて、kaggle kernel上で実行できる。

さてInsightsを日本語に直すと「洞察、物事の本質を見抜くこと」となるが、具体的には何なんだろうか。
「コースの始めに」に相当するUse Cases for Model Insights | Kaggleを見ると、このコースでは以下について学べると書いてある。

データのどの機能が最も重要であるとモデルは判断したか?
モデルからの単一の予測について、データ内のそれぞれの特徴量がその特定の予測にどのように影響したか?
全体的に見て、それぞれの特徴量はモデルの予測にどのような影響を与えるのか(多数の起こりうる予測を考慮した場合、典型的な効果は何か?)

以下の記事に出てくるような「解釈性(Interpretaility)」の話題と近いと考えておけば良さそうだ。
【記事更新】私のブックマーク「機械学習における解釈性(Interpretability in Machine Learning)」 - 人工知能学会 (The Japanese Society for Artificial Intelligence)

Permutation Importance

資料はこちら。下の方のリンクから演習問題に行ける。
Permutation Importance | Kaggle

機械学習のモデルを構築したときに「どの特徴量が重要なのか(feature importance)」を知る方法の一つが「Permutation Importance」である。

  1. モデルの訓練をする
  2. 1つの列の値をシャッフルして、その結果のデータセットを使用して予測を行う。 これらの予測値と真の目標値を使用して、シャッフルすることで損失関数がどれだけ悪くなったかを計算する。 そのパフォーマンス低下は、あなたが今シャッフルした変数の重要性を測定している。
  3. データを元の順序に戻す(手順2のシャッフルを元に戻す)。データセットのそれぞれの列で手順2を繰り返して、各列の重要度を計算する。

演習問題では「タクシーの料金」の予測をしていた。
乗車地点の緯度・経度、降車地点の緯度・経度、乗客の人数というデータを元に機械学習モデルを作成する。
学習用のデータの一部は以下の通り。

fare_amount pickup_longitude pickup_latitude dropoff_longitude dropoff_latitude passenger_count
2 5.7 -73.982738 40.761270 -73.991242 40.750562 2
3 7.7 -73.987130 40.733143 -73.991567 40.758092 1
4 5.3 -73.968095 40.768008 -73.956655 40.783762 1
6 7.5 -73.980002 40.751662 -73.973802 40.764842 1
7 16.5 -73.951300 40.774138 -73.990095 40.751048 1

バリデーション用データのうち、ある特徴量を互いに入れ替えてから、予測を実行して、損失関数の値がどう変わるかを観察する。
例えば、乗車地点の緯度を入れ替えてから予測をすると、予測結果は多く変わってくるから、損失関数の値も大きくなる、
一方で、乗客の人数は料金に影響しないので、乗客の人数を入れ替えても結果は殆ど変わらないだろうと予想される。

すなわち、精度が下がったらモデル構築の上で重要な特徴量、精度が下がらなかったらモデル構築の上で重要でない特徴量と判断できる。

Partial Dependence Plot (PDP)

2日目のテーマはPartial Dependence Plotである。
Partial Dependence Plotは、それぞれの特徴量が予測にどのような影響を与えるかを知るのに役に立つ。

資料はこちら。下の方のリンクから演習問題に行ける。
Partial Plots | Kaggle

モデルの訓練をしたあとに、
検証データのある点を元にして、影響を見たい1つの特徴量だけを変化させて、学習済みのモデルで予測を実施する。すると「1つの特徴量が変化すると予測はどう変わるのか?」が分かる。
検証データの中から多数の点について同様の操作を実施し、その平均を描画する。

演習問題の最後の方は、1日目のPermutation Importanceと2日目のPartial Dependence Plotを組み合わせた複合問題だった。これによって両者の挙動の違いが分かった。

問題:予測可能な特徴量が特徴量AとBの2つしかない場合を考える。どちらも最小値は-1、最大値は1である。
特徴量AのPartial Dependence Plotを描画すると、全ての範囲にわたって急激に増加する。一方、特徴量BのPartial Dependence Plotは、全ての範囲にわたってよりゆっくり(急激ではなく)増加する。
このとき、特徴量AのPermutation Importanceは必ず特徴量BのPermutation Importanceより必ず大きいと言えるか?

ちょっと考えると正しそうかなーっと思ったけど、実はこれは必ずしも正しくない。
たとえば、特徴量Aが変化する場合には大きな影響を与えるが、ほとんどのデータでは全く同じ値を取る、という場合が考えられる。この場合、Permutation Importanceを計算してもほとんどの値は変わらないままであり、特徴量AのPermutation Importanceの値はそれほど大きくならない。

pyconjp 2018 感想

PyCon JP 2018に行った。火曜日は有給休暇を取った。PyConJPは初参戦。大規模なカンファレンスってデブサミくらいしか行ったことなかったから、ある特定言語のカンファレンスは独特な感じがした。 (Rをほとんど使わないのにJapan.R 2016に行ったことはある)

資料や動画のリンクは以下の2つの記事でまとめてくれている。
PyCon_JP_2018カンファレンス資料まとめ(仮) - 筋肉で解決しないために。
【PyCon JP 2018】発表資料まとめ - Qiita

また、一部のセッションだけではあるが、ログミーTechに詳細な書き起こしが上がっている。 PyCon JP 2018の記事書き起こし - ログミーTech(テック)

【1日目】

基調講演 Argentina in Python: community, dreams, travels and learning

pythonを多くの人々に広めるために、車に乗って各地を回って講座を開催した人の話。すごい情熱だ。

東大松尾研流 実践的AI人材育成法

Twitterハッシュタグを追っている限りでは酷評されてた。 pythonの話というよりは機械学習人工知能の話をやたらと長く説明していた。普段Djangoとか触ってる人は「分からん……」だし、普段から機械学習に携わっている人は「長々と説明しなくてもええわ、退屈」になるし、加減が難しいな。 numpy力を判定する試験のシステムを開発したということなので、一般公開してほしい。

Webアプリケーションの仕組み

普段SIerのお仕事でWeb開発は全くやらないので、良い機会に勉強しとこうと思って聞いた。 まずはDjangoやFlaskのようなWebフレームワークを使わずに非常に単純なWebアプリケーションを書いてみて、少しずつ拡張する様子を実演していた。

Interpretable Machine Learning, making black box models explainable with Python!

機械学習システムで学習してみたは良いけど、そのモデルをどう解釈してよいか分からないのは問題である。モデルを解釈する主な手法を紹介する……という話。 Twitterで講演のjupyterリンクをツイートする、と講演者さんが当日は言ってたけど、結局ツイートしてないね(´・ω・`) David Low(@davidlowjw)さん | Twitter

Partial Dependence Plot (PDP)、Individual Conditional Expectation (ICE)、permutation によるFeature Importance、Local Surrogate Models (LIME)などの手法を説明していた。

この講演とは直接関係ないが、以下のサーベイ記事がまとまっていると思ったので、後で読んでおきたい。 【記事更新】私のブックマーク「機械学習における解釈性(Interpretability in Machine Learning)」 人工知能学会 (The Japanese Society for Artificial Intelligence)

Jupyterで広がるPythonの可能性

最初はメルカリのセッションに行こうとしたけど、満員で入れなかったので途中からこちらを聴講した。 内容がメチャクチャに盛りだくさんだったので追いつけなかった……これは手を実際に動かして習得したほうが良さそう。

Pythonistaの選球眼(せんきゅうがん) - エンジニアリングと野球の目利きになる技術

rettypy主催者の中川さんによる発表。私も2回ほど参加しております。 自分に馴染みのないWeb周りの話が主体だったのでちょっと感想が書きづらい。 主題となった「選球眼」すなわち「イシューからはじめよ」だが、自分は「イシューからはじめる」おとができているのかよく分からない……多分できていない気がする。 どのテーマについて何をどこまで作業するのかを明確にした上で始めましょう、手当たり次第に何でも飛びつくのはやめましょう、というやつだな。 俺自身の勉強は現在、基本的には機械学習周りだけとしているが、機械学習とひとくちに言っても、動画講座はあるわ、論文はあるわ、フレームワークは更新されるわ、と日々の更新が猛スピードで起きている。選択と集中が難しい……

【2日目】

Pythonでやってみた」:広がるプログラミングの愉しみ

気づいたら音声信号処理プログラミングになっていたので音声信号処理エンジニアの俺歓喜

  • プログラミングの動機は2種類 「面倒くさいことを簡単に」「一体どうなってるんだ? おもしろそう」
  • 作り始めるときには、最も単純な構成を考える! 最初から全部やろうとしないのがミソ。
  • 大事なのは、これらの技術を「学ぼう」とか「使おう」と思って制作をスタートしたわけではないということ。技術ドリブンじゃなくて、「何かをやりたい」と思って、あれこれ試行錯誤やってたら結果的に技術が身についた、というのが超大事。
  • 知識と学習は鶏と卵みたいな関係であり、「学習するためには知識がなきゃいけない」「知識を得るためには学習をしなきゃいけない」というジレンマがある。しかしどちらかを始めればあとはグルグル回り始める。

niconicoにおけるコンテンツレコメンドの取り組み

ニコニコの一人のユーザーとしては気づかないけど、色々なレコメンドシステムが裏側で動いているみたいだ。

Pythonistaに贈るコンテナ入門

同時刻にはGraphQLもあり、「なんだ、この時間帯はPythonに直接関係ないことも取り扱おうってセッションなのか?」と思いつつ聞いた。 Dockerは弊社内ではほとんどその名を聞かないのだが(SIだから?)、世の中的には既にかなり普及しているようなので、知っておかないとな、と思いつつ聞いた。

契約書データ関連のAI開発に伴う、前処理及び匿名化処理についての実例

センシティブな情報を扱う際の前処理や匿名化の問題。 発表者(CTO)が「まず私が10万件くらい手動でアノテーションしました」と言っていた。アノテーションがメチャクチャ大変そう。

1次元畳み込みフィルターを利用した音楽データのオートエンコーダ

ずいぶんストライドの大きい畳込みをしていたけど、あれってどういう効果があるんだろうか。音楽まわりの理論をよく分かってないまま実装しているように見受けられた。

料理写真が美味しく撮れる! 開発現場から覗くAI料理カメラの裏側

2月のデブサミでRettyの発表があり、そこでも「おいしく見える写真」を判別する機械学習システムを構築していた。Rettyでは機械学習させる際の教師データはは人力でラベルをつけたが、ヴァズ(サービス名SnapDish)では「いいねがたくさんついた投稿写真が美味しく見える写真」という手法をとっていた。あー、サービス運営してるとその手があるんだ!と思った。

ブース

アイシン精機が出てきたのが印象的で、「ずっと愛知県にいたけど、去年初めて東京都のお台場に開発拠点を立てました」って言っていて「あぁ、これは本気を出して新規事業に取り組もうとしているんだな」と感じた。 少し前に、デンソーが及川さんを技術顧問に迎えたというニュースを見たときも本気度を感じた。自動車業界の各社は、激しい変化にさらされて生き残りの道を探しているんだろう。

まとめ/おまけ

会場が1~6階にまたがっていたので結構移動が大変。 また部屋ごとに定員の数が大きく違うので、どのセッションをどの部屋に割り当てるのかは大変そう。(pyconjpの人がもしここを見ていたらお伝えしたいのですが、音声信号処理の分野は結構独特なので聞く人はそれほど多くないと思います。)

しかしそれを差し引いても、ためになる話が多く勉強になった。来年も参加しようと思う。 (あとはパーティーのときに話す相手がいなくてぼっちだったので……コミュニティに顔を出していろいろな人と知り合いになればいいのかな……?) まずは、1日目の「Jupyterで広がるPythonの可能性」の内容をもう一度見直して手を動かすことから始めようかと思う。 それでは。