BizDeep

個人の見解です

それでもぼくが大企業機械学習エンジニアを続ける理由

なんか急に以下の記事がバズりだして焦ってます。 biz-and-deep.hatenablog.com biz-and-deep.hatenablog.com

ぼくは大企業で機械学習のエンジニアをしています。

機械学習はめっちゃ流行りだし、上の記事でそんなに文句があるならはよ転職しろよ!と思われそうですが、ぼく自身は転職を迷うことはあれ、大企業で働くことにも大きなメリットを感じています。

今回はそれを記事にまとめました。

ぼくは最初から大企業に就職することを志していた訳ではなく、そもそも最初はプログラマーとして働くことすら考えていませんでした。
まずは、そんなぼくがどの様な経緯で大企業に就職したか、そこから書いていきます。

[注意] 今度は実話フィクションではなく、あくまで著者個人のお話です。

ぼくが大企業に入るまで

ベンチャーを一度志してめっちゃ冷めた話

ぼくは大学では情報系の学科にいたのですが、もともとは数学が好きで、情報数学だったり数理工学を専攻したくて学科に入学しました。
しかし、入学して数年経った頃にソーシャルネットワークの映画が大ヒットしたりなどSNSやソシャゲをはじめとするスマホアプリの人気が沸騰し、たくさんのベンチャー企業が登場し、情報系を専攻する学生の需要が驚くほど加速しました。

大学にはたくさんのインターンシップの求人が出され、ぼくもアルバイト目的でとあるベンチャー企業インターンに参加しました。 その時にベンチャー企業に大きく惹かれることになります。

そのインターンは10日間でiphoneアプリを企画して実際に作って役員にプレゼンする、というものでした。
内容自体はよくあるものだったのですが、 とにかくインターン仲間が良かったのが驚き でした。実際にインターンで作ったアプリを継続して開発してリリースしてTVに出たり、アプリが10万ダウンロードされたりする様な優秀な人達がいました。

そして、インターン後もその仲間とは頻繁に会ったり連絡を取り合う関係になり、仲間内でもう一度なんかアプリを作ってリリースしよう!という流れになりました。
貸し会議室を借りて、インターン先のベンチャー企業の人も読んでアドバイスをもらいながらアプリの企画をしたりなど、今思い返しても結構本格的でした。

そして、表題の事案が起こりました。
企画会議を重ね、みんなで作ろうと決めたアプリは「満員電車で女子高生に痴漢をするおじさんをタップして消しながら守るゲーム」でした。 企画を出したのは僕が当時一番優秀だと思っていた人で、アドバイザーのベンチャーの人もそれいいじゃん!とか言ってました。

その時にぼくの中で何かが切れ、ふと我に返って何だこいつらはと思って本格的にベンチャー企業に冷めて、就活を辞めて大学院に進学する決意をしました。でもその時は何でぼくがあんな一気に冷めたかをうまく言語化することができませんでした。

コンサルを経験して冷めた理由が分かった話

ベンチャーに冷めてからも企業のインターンは面白かったので多数参加したのですが、その時に面白かったのが経営コンサルタントインターンです。

コンサルタントインターンは比較的短くて給料が高かったので美味しくてしこたま受けたのですが、そこで良かったのが「思いやり」の大切さを身にしみて理解できた事でした。
インターンを通して、コンサルタントという職業はお客さんにお願いされた事をただやるのではなくて、お客さんのお願いを聞いて何に困っているかを想定し、それを根本解決する職業だということを学びました。

インターンしか受けてないので本業の方から叩かれそうですが、ぼくがインターンを受けてコンサルという職業についてevernoteに考えをまとめたのが以下の様に残っていました。

コンサルの能力は資料が綺麗とか論理的思考とか言われるけどそうではなくて、人を思いやる事に尽き、それがいかに難しい能力であるかをインターンを通して理解した。この能力をつけるためには10年以上はかかると思った。
コンサルタントが対象とする顧客は社長・株主・事業担当者・事業担当者の顧客・事業の協力会社などたくさんいて、その全ての人は本当に困ってる事と違うことをお願いしてきます。(事業部単位でコンサルをかけることも多いだろうが、分散性は同様)
これらのお願いを素直に受けていくと施策が対立したり矛盾が生じてしまうどころか、そもそも会議体が成り立たないような事態に陥る。 人によっては他者を落とし自分が有利になるような悪い考えをそのお願いに含めていることも稀ではないため、お願いをそのまま実施してしまうと不利益を被るひとが出てくることもあり、その結果会社全体としてみると不利益の方が大きいような事態が起こったりする。(コンサルの代表的な失敗例がこれか??)
よって、最初にヒアリングを行った時点から各人が本当に何を困っているのか何を変えたいと思っているのかを全体に対する「思いやり」のフィルターをかけてストーリー化する事がコンサルタント業務の最重要項目である。

コンサルタントインターン体験を経た後、自分が以前ベンチャー志望の敏腕エンジニアに対して冷めた理由を言語化する事ができました。 彼らは自分のやりたい事をやるだけで、作った製品にお金を払う人や、協力者や、出資者などへの思いやりは一切持っていなかったためぼくは冷めたのでした。
どうしてもベンチャーボトムアップだと、コンサルのように あらゆる顧客を想定して市場やニーズを深掘りするのに時間が使えないので、自分がやりたい事をやるために取り繕ったビジネスモデルを出資者や協力者にプレゼンする様な見え方になってしまい、そこに不安感を覚えたということでした。

大企業エンジニアを志した訳

ベンチャー・コンサルでインターンを経験し、大学院の就活時期に入って実際に自分が40年間社会人を行うことを想定した時に以下の考えになりました。こちらもevernoteに当時書いてたやつ。

ベンチャーをディスってコンサルを褒めたけど、全てぼくが経験した範囲の話で、実際はもっといろんな考えがあって、ぼくが経験したのはごく僅かなことで実際に働いたら考えも変わる。
ぼくは経営者になる訳じゃないから、何か明確にこれをしたい!というビジョンは持っていないし。
そんな時に社会人生活で何を一番重視するべきか?ぼくは経験だと思う。多分インターン経験だけでこれだけ思想が変わるのだから、実際に働いて経験を積んで考えをもっと蒸留した方がいい。
ただ、ぼくが経験できる範囲にも限界があるから、歴史から経験を学びたい。そういう意味で、企業の成功失敗の歩みがあっていろんな経験の歴史を持った企業は戦後の経済成長の中で活躍した大企業であることは間違いない。

これが僕が大企業に就職するに至った大きめの理由の1つです。
あとは、自分の専門分野に関連して今後伸びそうな技術で機械学習があり、大企業で機械学習エンジニアができるところを探して今の会社に至りました。

実際に大企業エンジニアになってよかったこと

働いている企業によって変わるかもしれませんが、あくまで個人の観測の範囲内。

上場していることはやはり大きい

大企業は意思決定が遅いとか、実際に僕も別の記事でディスったりしてますが、逆に上場している大企業の意思決定の信頼性と透明性の高さは働いている上でとても大きいです。
非上場のワンマンベンチャーとかだと、社長の一存で事業のstop and goを最悪決めてしまえることが問題だと思います。 自分が経営者に回るなら話は別ですが、エンジニアの従業員として働く以上自分がやりたい仕事だけをやる、というのには限界があるとぼくは思っており、どうせやらされる仕事が多いのなら意思決定の信頼性の高い大企業が良いとぼくは思います。

思ったより裁量権は大きい

学生時代に考えていた様な"最低10年間は歯車として修行"みたいなことは実際ありませんでした。 少なくとも10年間も無責任な歯車でいさせてくれる程大企業は甘くなく、ぼく自身は1年目の冬時点で自分が直接顧客とお話しして自分でライブラリ化した技術を提供する様な立ち位置にアサインされており、修行期間は半年くらいでした。

生涯プログラマで生きてるすごい人もいる

大企業はプログラム研修をして数年プログラマーしたら後は設計職になってコードを書かないエンジニアになる、といった様な考えを学生時代は持っていましたが、少なくともぼくがいるところではそんなことはなく、定年までプログラマーで定年後に再就職でプログラマーしている人もいます。

なんだかんだ生涯年収はめっちゃ良い

初任給の高さでベンチャーに行ったり、年収の高さでGAFAに転職する様なエンジニアが昨今多い様に思いますが、実際年収の伸びや安定性を考慮すると、大企業で真っ当に成功するのが一番生涯年収は高い様に思います。
コンサルや金融といった高所得の文系職と比較すると少し見劣りもありますが、プログラマの業界で見ると年収は高く、何より伸びが大きいですし、さらに年収を追ってマネジメント層に進むキャリアパスも選ぶことができます。

ホワイト

基本ホワイトです。有給は申請すればすぐ取れるし、コアタイム性で働きながら残業は月30時間くらい。 めっちゃ忙しい時期や終電で帰る日もない訳ではないですが、残業代はちゃんと支給されるし、めっちゃ残業したら多分月収めっちゃ多いです。

勉強する時間がある

大企業は研修制度が整っていたり、社内外の勉強会/展覧会などに見学に行けたりなど、業務外のことを勉強する時間の余裕があります。 また、新しい業務にアサインされるときはいきなりやってみて!みたいなことはなく、ちゃんとスケジュールを立てれば学習する時間をくれます。
ベンチャーでもそういう勉強時間はくれると思いますが、多分大企業の方が時間は多く取れるんじゃないかな。

社内異動という選択肢

大企業はたくさんの事業を展開しており、社内で異動するだけでいろんな経験が積めます。 転職に比べて、社内異動は比較的自分の専門性の少ない領域にチャレンジできるケースが多いと思われます。

最悪転職することになっても幅が広い

多分転職を考えた時には、ベンチャーから転職を考えた時よりいろんな選択肢があると思います。 これは上で書いた様に社内異動などを使って幅広い経験を積めることと、そもそも大企業出身のネームバリューがやっぱり大きいです。

最悪自分の競争力が落ちても多分生きていける

今は若いから良いですが、家族を持ったり老化によりパフォーマンスが落ちてくることを考えると、いつまで自分が最先端のエンジニアとして競争力を持てるかは不明です。
仕事以外に人生をかけたい様な趣味が生まれるかもしれません。

その時にも大企業は優しくて、競争力がなくなったからといっていきなり解雇!とかはないです。
流石に無能おじさんになったらやばいかもしれないですが、自分が想定できる範囲で仕事以外のことに多く時間を使う様な選択肢も取ることはできると思います。

地頭が良い人が多く資産の質が高い

大企業にいる人はそれなりの学歴がある人がほとんどなので、ロジカル的な思考力も高くて会議などの理解力がとても高いです。 またそういう人たちが時間をかけて資料やコードを作っているので、例えば、これはDockerfileにしてくれよ〜〜、と思う様なREADMEはあっても"READMEが一切ない"ということは多分ありえないです。

定年していく人がいるという安心感

ベンチャーとかだと社長が30代で社員はほとんど20代とか結構多いと思います。やっぱりそういう会社だと自分がおじさんになった時のことを会社が考えてくれないんじゃないかとかの不安があると思います。

一方大企業だと、病気や介護で長期休暇する人もいるし、子供ができて家で仕事をする人もたくさんいて、それを支える様な制度や風土が整っており、自分が歳をとった時にも働きやすい環境が整っているため、「いずれ辞めなくてはならないのではないか」といった様な変な不安感を持たずに安心して仕事ができます。

圧倒的な情報量と経験値

大企業には有能ベンチャーの様に体系立てたりDBに格納された様な綺麗なデータはあんまりないですが、データそのものの母数は引くくらいあります。15年前の事業説明パワポ資料とかもちゃんと残っていて、それを読むだけである程度学びになることは多いです。

また、業界歴30年のエンジニアおじさんがいたりして、その人の持っている経験値から来るアドバイスは大学の教授から指摘をいただいている様なもので、飲み込みづらいときもありますがそもそもめっちゃ貴重です。

終わりに

なんかバズった記事の方で「なんでこいつ転職しないんだろう」とかコメントされてましたが、ぼく自身は大企業をクソだと思うこともそりゃありますがそれ以上の恩恵を非常に多く感じています。
バズった記事がいろんな人の大企業の辛みイベントを恣意的にまとめ上げた様な構成にしたため、僕が大企業を忌み嫌っている様な誤解を招きそうで、それが嫌でこの記事を書きました。今度は個人の観測の範囲内ですが、フィクションではなく本当のこと。
これが就活でベンチャーにいくか大企業に行くか迷ってる方などの参考になれば幸いです。

3大幸福論+αを読みました

幸福について色々調べて、考えました。

手始めに幸福について書かれた本を読みました。読んだのは3大幸福論と呼ばれるヒルティ・アラン・ラッセルの「幸福論」に加えて、ショーペンハウアーの「幸福について」を読みました。

この記事ではこの本から得た知見を借りて、自分が幸福について考えたことをまとめていきます。

書いてることは全部当たり前の様なことなのですが、 "幸福"という曖昧な概念をそういった当たり前のことで定義づけていくことは自分にとってとても価値のあるもの でした。

幸福とは何か?

「幸福は生活の目標であり、自分の思想の鍵である。」

ヒルティの言葉です。自分の中で腑に落ちたのはこれでした。

要するに、 僕たちは幸福になりたくて生きていて、何かに思い巡らす時は幸福になりたくてそれを考えている、ということ。

「なんで自分が今仕事してるんだろう?」とか「将来どうなっていたいだろう?」とかをコンサルマンが大好きなWHY分析で深く深く掘っていったときの一番の原点が”幸福”であるということです。

当たり前の様ですが、何かを考えるときの原点が決まるというのはとても大きいです。

幸福の大前提

「"不幸"でない事は"幸福"であることの前提条件である。」

まず前提として不幸であれば幸福にはなれない様です。

では不幸とは何か?これは同じくラッセルの幸福論が9章かけて分解してくれています。

僕にはあまり響かなかったので本記事では不幸の分解は割愛しますが、ロシアの文学者のフリーチェとがこんなことを言っています。

「人生は学校である。そこでは幸福よりも不幸のほうがよい教師である。」

不幸や苦しみは精神を発達させ、その先の幸福を理解するのに役立ちます。

そう言った意味で、 不幸でないことは幸福の前提ではありますが、不幸を経験する事は幸福には欠かせないものである ことは覚えておく様にしてます。

幸福は何によって得られるか?

人間は3つの根本規定に帰着させられる。
(1) 人のあり方 (人徳や人柄などの人間性、知性や力など)
(2) 人の有するもの (お金などの資産、家族など)
(3) 人の印象の与え方 (他人の印象、名誉名声など)

上はショーペンハウアーの言葉です。人間に幸福を与える要素はこの3つに分解できるとのことで、ぼくが得た知見で最も興奮したもの。笑

幸福感がMECEに分解されました!!

また、ショーペンハウアーは、この3要素の中でも特に(1)の人のあり方を良くして幸福を得ることが一番大事だと言っています。

(2)は無くってしまうことがあったり、(3)は裏切られたり手のひらを返されたり自分で操れない要素であるため、この2つから得た幸福は不確定なものになってしまいます。一方で、人徳を磨いたり、勉強をして知性をつけたり、筋トレをしてムキムキになった時の幸福感は裏切りません。

ただ、(2)(3)の要素で不幸なことがあると前提が崩れてしまうため、お金や彼女や他人との人間関係を意識することも忘れてはいけません。あくまでバランスの問題。

「富は手に入れたら退屈になるものと、活力を生み出すものの二種類がある」

これはアランの言葉で、(2)についてぼくがとても大事だと思ったものです。

お金や資産、美人な彼女などの(2)の要素は 手に入れたらそれで満足して終わったり自分の活動を制限する様なものではなく、自分がより活動的になるものを選んで得る努力をすること がとっても大切です。

「幸福の活力は喜びである。感謝は喜びに非常に近い感情。他人に喜びを与えると自分の喜びになる。」

これはヒルティの言葉で(3)について大事だと思ったもの。感謝は喜びに近いというのは、結構なるほどなーと思いました。

誰かに感謝をしたり誰かを喜ばせて活力を作って行けば自ずと他人の印象は良くなるのではないかと思います。

他人を喜ばせても自分は幸福になれないなーとは思うけど、 他人を喜ばせることは自分が幸福になるための活力になる ってのは凄い納得です。

(3)の活動は活力を得るために行うべきですね。

あと、個人的な見解ですが、宗教的な神や先祖への"祈り"という行為も非常に感謝に近い行為だと考えられます。
喜びや感謝はそう毎日訪れるものではないですが、神への祈りは毎日やろうと思えば可能ですね。つまり、 宗教と幸福感の関連性はこの"祈る"行為と喜びとの関連性が寄与しているのではないかと考えられます。

宗教や思想については深くなりそうなので、また別の記事にします。

じゃあ、どうやったら幸福になれるか?

「幸福になるには人間を知ることが大切。その第一歩は自分を知ること。」

ヒルティの言葉です。 ここまで幸福とは何か、幸福は何によって得られるものかを分解してきましたが、 幸福がどうやったら得られるか、つまりHowの部分は"人それぞれ"というのが答え です。

それを知るためには自分のことを自分で知る必要があるとヒルティは言っています。

「ゆっくりした生き方や暮らしも幸福である。落ち着かないと見えないものがたくさんある。」

アランの言葉です。幸福を得るために頑張りすぎたらしんどいよってこと。

「色即是空 空即是色」 (般若心経より)

仏教の言葉です。 "この世の中で自分が意味があると思っているもの(色)は実際は意味なんかなくて(空)、でも意味がないと思っているもの(空)は意味があるんだよ(色)"という深い言葉。

ぼくは自分が何かに追われて辛い時は"色即是空"を思って無駄なことに時間を費やさない様にし、自分が何をしたらいいか分からなくなった時は"空即是色"を思って自分が今まで興味がなかったことに目を向けることを意識しています。

さいごに

最後に”幸福”という言葉について考えてみます。幸福は"幸"と"副"という言葉からできています。 記事の中ではあえて"幸福"と書いていましたが、最近は"幸せ"だけにフォーカスされることが多いですね。

"幸せ"は英訳するとhappyです。 でもhappyって"幸せ"っていうより"楽しい"って理解の方が強いですよね。 個人的には楽しいと幸せは全然違うものであると感じます。 つまり 僕らの思ってる日本人的な"幸せ"って概念は現代のグローバルな理解とは乖離がある ってことです。

"福"は単体で使うことがなくなってしまった言葉ですが、福という言葉はお年玉の袋だったり節分だったり七福神だったり、日本の伝統や神々に所以のある言葉である様な理解があります。

つまり、我々が持っている"幸福"という概念は、遊んでる時の楽しい!という感覚以上の伝統的な価値観があるという事です。 この辺りは先に述べた宗教と幸福の関連性に繋がってくる話でもあります。

それが昨今、欧米由来のサービスや食べ物や価値観が日本に入ってきた事で、日本古来の幸福感が満たされていないことに自分でも気がつかなくなってしまっているのではないかと思います。

また、 日本人は統計により諸外国と違って歳をとるごとに幸福感がどんどん下がっていく、ということが分かっています。 このままだと僕らの未来はどんどん不幸になっていくのやばすぎ。。。

もう一度、他人や周りの環境でなく"自分"としっかり向き合って自分の幸福について考え直すことができれば良いなと思っています。

大企業機械学習エンジニアに立ちはだかる3つの壁おじさん

昨今大企業やめました記事が盛り上がってますね。

anond.hatelabo.jp

kumagi.hatenablog.com

僕もかくいう大企業ソフトウェアエンジニアで、機械学習を担当していて、クソみたいなことがいっぱい起こって辞めようかと思うことが非常に多い。 怒りの原因を分析したところ、僕のケースでは以下の3人の壁となるおじさんがいることがわかった。

[ここで注意]
本記事は著者含め大企業で働く機械学習エンジニアの友達とslackやtwitterで盛り上がった内容を、全て”僕”一人の体験としてまとめ上げたものです。つまり 実話に基づいたフィクションです
分散する様々な大企業の悪いところをまとめて3人のタイプのおじさんを作っているので、とある大企業の批判をしていたり、著者個人がこの様な体験をしている訳ではないことにご留意ください。

(1) 幼稚園児でもわかる結果でしか技術を評価できないマネージャー

大企業のマネージャーはマネージするべき部隊の技術的なバックグラウンドの教養が十分にないケースが非常に多い。

これはたとえ技術職であってもお金と人の管理だけで出世できる日本企業の悪しき風習と、大企業ならではの異動して空いたポストにハマり込むことで出世していく文化が重なって起こっていると思われる。

ただ、業務時間を使って何かを行おうとするとこの人を説得して工数を確保する必要がある。 最初の大きな壁はここにある。

僕たちは機械学習の最新の論文や他社の動向に対する知識があるので、競合他社のエンジンのアウトプットを見ればおおよそどういう技術で実装されているか推測でき、それを凌駕するにはどういうアルゴリズムを採用すれば良く、さらにどうやって発展させれば競合の手が届かないところまで技術を高められるかの計画が立てられる。

だが、こういった技術ベースの内容では課長は一切理解を示してくれず、本当にやりたいなら結果を見せろと言われる。
当然作っていないので結果を見せられないというと、どれくらいの時間で競合と同等の結果が出せて、それからどれくらいの時間で競合に勝つ精度が出せるかのスケジュールを建てろと言われる。

これはつまり、他社を超えられる証拠を見せろということである。論文も読まずに。コードも読まずにである。
これをまずクリアするには、他社のエンジンの定量評価を行う環境を作るところから始め、それによる他社エンジンの弱みが定量・定性的に理解できる資料作りが必要となる。 これに1ヶ月はかかる。はい壁。クソ。

また、スケジュールを建てるということは完遂する以前からオンスケジュールであることを定期的に証明する必要があるということであり、度々現状の動向を非技術レベルで理解できる資料を作って説明する必要があった。
どうせ資料を作って説明しても、出来てたらそのまま進め、出来てなかったらダメじゃんって言うだけなのに。

(2)基礎タスクに対して2週間ごとに目に見える結果を求めるアジャイルおばけ

うちのチームの開発はアジャイルで進められている。
大企業の場合、プロダクトリーダーが課長で、プロダクトマネージャーとしてコードの分からない謎のおじさんがアジャイルのリーダーとして一人挟まっていて、アジャイルのタスク進捗はその謎のおじさんに報告し、そのおじさんが幼稚園課長に報告するというスタイルで仕事が進められている。

アジャイルといっても、まずは(1)をクリアしているので大まかなスケジュールを立てて、それをクリアする様に詳細なタスクレベルまで分解してアジャイルで進めると言うスタイルである。(なので実情は設計のないウォーターフォールの様なお粗末な代物)

そしてスクラム開発の真似事をするので、2週間ごとにプロダクトリーダー(課長)に対して、成果物の説明が求められる。
何かリリースをする様な開発タスクの場合はプロダクトが開発環境で動いているものを見せたり、設計タスクの場合は資料や図を見せたり、検証タスクの場合はグラフ化した資料を作ってそれを見せたりする。

製品開発の場合はこれで説明が成り立つのだが、機械学習の基礎タスクの場合は意味が全然変わってくる。
たとえば、実際の業務では2週間かけて学習環境を立てて、データを整備して、マシンで学習を開始してロスの動きを見て、ハイパーパラメータを修正して、などを行うのだが、ここに成果物の途中経過はない。
ロスが全然収束しなかったり過学習した学習結果に対するアウトプット例を見せたところでなんの意味もないし、ロスがこうだからハイパーパラメータをこうします、といった様な内容を幼稚園児マネージャーにわかる様な資料に落とし込んでいる時間はない。

ただ、これをアジャイルのリーダーは分かってくれない。
2週間ごとには絶対ドキュメント作ってねとか、なんなら3日ごとにタスクを細切れにして途中経過をドキュメントで伝えてスケジュール通り進められてることを納得させてね、とか言ってくる。論文も読まずに。コードも読まずにである。

これをクリアすることはとても困難で、信じてくださいというか、今は結果を出せませんと言って戦うしかなかった。

(3) 基礎タスク担当者に製品化~検証までの責任を負わせるエンジニアリーダー

僕はだいたい機械学習の最新論文の検証など基礎タスクを担当することが多いが、チーム自体はとあるソフトウェアプロダクトを作っており、それに実装するという名目で基礎タスクの工数をそのプロダクトの費用から捻出していた。 つまり、あるソフトウェアの開発チームにリサーチエンジニアと開発運用エンジニアとかがごった煮にされている状態であった。

そのソフトウェアには開発のリーダーとなるおじさんがいて、この人はコードが書ける人で、アジャイルおばけのおじさんと相談してスクラムのタスクを決めたりリリース物の設計をしたりする。

僕もそのソフトウェアのチームに所属しているので、基礎タスクを解きながらもソフトウェアプロダクト自体の開発メンバとしてアサインされ、コードレビューや資料のレビュー、会議等に非常に多くの時間を割かれていた。

さらに、プログラマあるあるだが、だいたいソフトウェア開発のスケジュールは工数削減のためクソみたいに詰め詰めに見積もられていることが多く、リリース前などは毎度のように想定外のタスクが発生し、炎上が起こる。 炎上が起こると、真っ先に検討系のタスクが消されて、炎上対応タスクが増える。つまり僕の工数機械学習でなくCIのエラー対応とかAWSのシステムワークフローの見直しとかに充てられる。

この時点で、(1)で作ったスケジュール通りに基礎タスクが進められるはずはなく、当初作るはずだったエンジンの質は下がり、やりたかったアルゴリズムは実装できず、無理やり結果をうまく見せる様な後処理実装を入れ込んだものが最初に出来上がることになる。

それを見て、課長がエンジン全然ダメじゃん、何やってるのとか言ってくる。 その辺りで、脳内で奇行種の巨人が課長を食べてくれる姿を思いながら、すみませんと言ってまたアップデートスケジュールを立て、(1)からのループに入る。

終わりに

この経験から僕が学んだことは、大企業では時間をかけて既存の技術をアルゴリズムレベルで上書くような業務はまずできないし、説得してスケジュール建ててもめちゃめちゃにされるし、無理して作っても部署内の頭の悪い人間と戦った際のダメージで正当な評価を受けることはない、ということである。

最初にスケジュール立てる時に炎上対応のタスク積んどけばいいじゃんと思うかもしれないが、炎上対応のタスクはそもそも全体のソフトウェア開発のスケジュールに積まれてないから炎上するわけで、事前に積めるはずはなく。。

あと、蛇足的ではあるが、上の学びを裏返すと、既存の技術をなあなあで発展させる分かりきったタスクをあたかも難しいように解釈させる厳格な資料作って長めのスケジュールを建てて、意味のない資料をたくさん作って時間をかけて実装すると大企業では一番評価される。 その経験を積むと嘘をついて工数確保するのがうまい技術力のないサイコパス、もしくは確実な小さい改善を積み続けるだけのつまらないエンジニアが出来上がり、これが大企業のなかで出世して行く技術者ってことになる。

どうしたらいいかというと、分からないのだけど、せめてスケジュール化しなくても作業が進められるいわゆる20%ルールのような物がもう少し大企業にも入ってこれば、少し未来はあるのかなとも思う。

(追記) 本記事は大企業の悪いところだけ抽出しすぎて、これだけ読んだら「いやベンチャー外資に転職すれば良いじゃん」と思われがちだと思って、以下の記事を書きました。

biz-and-deep.hatenablog.com

DeepLearning/機械学習を始めると必ずいるカス

(2018.1227)なんか急にバズったのでちょっと追記しました。

ディープラーニング人材はやばい奴だらけ

これから「AIを仕事に導入したい!」と思う人は沢山いるでしょう。
ただ「ディープラーニング」や「AI」という言葉に関しては世間で色々な誤解がされており、正しく現状を理解できている人はとても少ないように思います。

ディープラーニングという言葉はAlexNetがでた2012年頃に流行り出しました。
実際に企業が仕事としてディープラーニングに手を出し始めたのはAWSがGPUインスタンスをリリースした頃からだと思うので2014年ぐらいからでしょうか。
まだ流行り出して5年も経っていない技術であるため、最新の研究レベルでもディープラーニングの全容は明らかになっていなかったり(参照: ディープラーニングの解釈に関するサーベイ論文) 、正しくディープラーニングを理解するための教科書や参考書などもまだ充実していません。

ただ、試すこと自体は簡単で、簡単な学習と適用であれば非エンジニアの人でも1日あれば余裕で出来てしまいます。

ぼくは仕事で機械学習を使ったプロジェクトを担当して3年になりますが、実際に仕事をしていて、ディープラーニングに対して誤った理解や偏った解釈を持ったまま仕事をしている人が沢山いるのを見て来ました。
また、流行りの技術なので、ただ飛びついているだけのような人も沢山いるのを見て来ました。

こういう状況なので、 仕事で正しく機械学習を使い、マネジメントしていくためにはこういった人達を反面教師として利用し進めていくしかありません。
本記事では、この反面教師たちをまとめることで、これから機械学習を始めたい人や仕事で使いたい人、人材を探している人達の新しい気づきに繋げられることを願います。

(反面教師達は随時追加/詳細化していきます!)

やばいマネージャー

設計不足棚上げマン

実は機械学習を使わなくてもできる物を設計が曖昧なため、その曖昧を機械学習で吸収するしかなくなってるカス

顧客からの要求を具体化するときに、機械学習という言葉は役に立ち過ぎます。
例えば、本来はルールベースでなんとかなる処理に対して、そのルール設計がめんどくさい時は機械学習使ってクラスタリングしましょう!とか言うと成り立ったりします。
ただ、機械学習はどうしても誤認識や誤検知を生んでしまうため、機械学習を使わない方が良いものが出来たりマネージしやすいことが多いです。

機械学習やりたいだけマン

自分が流行に乗りたいため機械学習でやらなくてもできる機能に無理やり機械学習を入れるカス

設計不足棚上げマンと似てますが、こちらは機械学習を使った実績が欲しいためにあえて機械学習の利用を選択するカスを指します。
社内にノウハウを貯めるという目的では正しい面もありますが、やりたいだけマンの人は機械学習を選択したことによる追加コストや工数の増加を考えていないことが多いです。

勝手にできるマン

”できる”と"使える"の間の大きな壁を無視して機械学習使えばなんでもできます!とか高らかに謡ってるカス

機械学習(特にディープラーニング)は、対象のデータがあれば少工数で何らかの結果を出すことは出来ます。
ただ、多くの場合その結果は精度が低過ぎて使い物にならなず、エンジニアはそれを使える精度に達させるために多くの時間と労力を使います。
ただ、勝手にできるマンはその労力を無視して、とりあえず結果が出せるのでやりましょう!!といってプロジェクトを勝手に始め、精度が出ないとキレてくるカスです。

リテラシー謎マン

エンジニアのやってることは詰めまくるのに、Qiitaのやってみた記事とかよく分からない論文とかを疑いなくパワポに参照したりするカス

エンジニアが報告した内容に関しては5W1Hにのっとって「何でこの技術を選択したの?」とか「このグラフはどうやって出したの?」とか聞いてくるのに、誰が書いたか分からない個人ブログの記事や参照の少ない論文は全信頼してしまう人が多いです。

サイコパスマン

機械学習を使った実サービスの開発改善の経験がゼロのくせに玄人みたいな顔してプロジェクトとってきて失敗するカス

機械学習ってドメインが変わると難しさがめちゃくちゃ変わるのに、簡単な画像認識のサンプルで成果が出たら調子に乗って劇的に難しい動画解析案件をドヤ顔で取ってきて盛大に失敗するカスです。(本当に多いです。)
精度が出なかった時にどう向上させるかという取り組みの経験がないので、このカスは精度不足に陥った時に正しいマネジメントが出来なくなって、暴れ出して死にます。

シンギュラリティーマン

機械学習のエンジニアをマネージするために、シンギュラティーの本とか読んで分かったつもりになってるカス

技術書ではなく、ディープラーニングのことを取り上げた記事や経済本から知識を得ようとしているカスです。
思考が偏り過ぎていてエンジニアと会話が出来ない人になります。

やばいエンジニア

動作確認マン

githubの動作確認しただけで機械学習できる!と名乗ってるカス

最近はgithubが充実していて、README通りに動かすと最新の論文の技術を試すことができたりします。
ただ、結局試すだけだと中身が全然分からないのでなんの役にも立たないのですが、それでスキルを得たと勘違いしている人が非常に多いです。

Qiitaマン

論文を読んだり手を動かさずにQiitaのまとめを読んだだけで技術を知り尽くしたつもりになってるカス

論文のバズりそうな部分だけ取り出して要約してる記事を読んで論文を読み尽くしたつもりになって、Qiitaの動かしてみた記事を読んで動かしたつもりになっているカスです。

trainでtestした結果を報告マン

言語道断のカス

もはや説明不要で絶対ダメですけど初心者が一番最初にやりがちな過ちなので注意です。

データセット質無視マン

質を問わずデータセットの量だけあればなんとかなると思ってるカス

特にディープラーニングは教師となるデータセットの量が沢山いることは周知されていますが、質が精度の上限値を司ることは無視されがちです。
質の悪いデータを10万枚集めたところでなんの意味もないですが、無意味なデータ集めて王様みたいな顔してるカスが結構多いです。

ブラックボックスマン

ディープラーニングの仕組みを知らなくても良いと思っているカス

ディープラーニングの全容が明らかになっていないことは序章で記載しましたが、だからといって全てブラックボックスでいいかというと違います。
例えば、ConvolutionやPooling系の処理の意味を知らないと正しいベースネットが選べなかったり改善が出来ませんし、学習率や最適化手法を知らないと過学習モデルが出来て終わります。

エラー分析できないマン

データセットを闇雲に増やす以外に性能を向上させる手段を持たないカス

ディープラーニングしかできないマン

簡単な信号処理で解ける問題をわざわざRNNとか使って解こうとして失敗するカス

サンプルコードマン

だれかが作ったGithubのサンプルコードを少し改変するのが限界のカス

ハイパーパラメータ固定マン

最適化手法とか学習率とかの意味を知らず、バイアス効きまくりの過学習モデルを作って今のデータだとこれが限界ですとか報告するカス

長期運用無視マン

よく分からない大学生がcaffeを魔改造してるのをさらに魔改造してサービスに入れ込もうとするカス

フレームワーク依存マン

caffeとかtensorflowとかchainerとか特定のフレームワークしか使おうとしないカス

[追記 2018.1227] このナレッジを溜めていた当時はフレームワークごとにGPUメモリ消費や演算速度/学習速度にまだ差があったり、オリジナルのレイヤー実装の重みが大きく異なったので記載しました。 特にcaffeはpython layerが実装される以前はCで独自レイヤーやloss関数を記載した上でビルドし直す必要があり、地獄でした。。 現在はtensorflow/kerasかpytorch/caffe2等に落ち着いてきており、ONNXのような共通フォーマットも整いだしているので、むしろ単一フレームワークを深く使いこなす方が有益かもしれないです。

やばい顧客

勝手に偉くなりますよねマン

AIって導入して使ってたら勝手に人を超えてシンギュラリティ起こすんですよね!!とか言ってくるカス そういう人には自分が子供を産んで適当に育てたらどうなるか想像できます??と煽ってあげましょう

基本的に強化学習教師あり学習の区別が付いていない人が多く、教師あり学習のモデルに強化学習的な改善を求めてくる人が非常に多いです。 頭がいい人にはまず教師あり学習には正解データが必要で強化学習には報酬が必要で両者は違う手法であることを説明してあげると良いと思います。

ゴミデータ渡して権利主張マン

自然言語的な統一もされてなくて全角スペースとか入りまくった10万行のエクセルファイルと画像送ってきて、それをこっちが必死にクレンジングして学習モデルに反映させたら50%くらい権利主張してくるカス

データをもらってPoCをするケースは機械学習系に非常に多いと思います。後で喧嘩にならないように、データをもらったらまずクレンジング作業の工数とクレンジング後に使い物になるデータの割合、それが学習モデルの精度に影響する見立てを作業前に報告するのが経験上、最優先です。

最新論文送りつけマン

「最近マイクロソフトが少量データで学習可能な手法を考案したとありますが...」とか言って最強企業の最新論文を送りつけてきてそれの実現を求めてくるカス。

だいたい論文が出てから本サービスに導入するまでにはネットワークの再実装とオープンデータでの追試、その後自分たちのモデルのデータに適用するための拡張の検討などいわゆる企業R&D的な前段工数が非常に多くかかります。 特にCOCOなどオープンデータセットでSoTAなモデルが自分たちのデータセットでも高精度になるかというのは結構違ってきますので、まずは追試検証に工数がかかることを説明するのが良いと思います。

最後に

同期とslackでキレあってた内容から抽出して攻撃力強めで書きましたが、 実際ぼくも人をディスれるほどの専門家ではなく、ぼく自身何度もカスでした 。 問題なのはここら辺のリテラシーがまだ一般的になっていないことで、機械学習に限らず新規技術ではよくあることと思います。

解決するには自分たちにノウハウを作るしかなく、そのためにこういった失敗事例を共有していくのが役に立つかと思い、記事を作った次第です。 もっと課題を体系立てて会社とかで共有したい人は以下記事かその英語ソースが非常にロジカルでやさしい文章なので使うといいかと! tjo.hatenablog.com

【SIer先輩 v.s. プログラマー先輩】

大企業に潜む2パターンの「ソフトウェアエンジニア」

ぼくは割と大きめの会社でソフトウェアエンジニアをしています。働いて少し経つとソフトウェアエンジニアと名乗る人間には以下の2パターンあることが分かりました。ずばり

です。

同じ顧客から同じ要求が来た時、プログラマー先輩がアサインされると、彼は要求を聞くなり早々に関連論文を読み漁って最高の技術を選択し、実装にかかるスケジュールを算段し、それ通りにシステムを自分で作って提供します。
対して、SIer先輩がアサインされると、彼は顧客と何度も打ち合わせをして要求と価格を明確化し、ニーズと価格にマッチした人材を派遣として登用し、自分は管理者としてシステムを他人に作らせて提供します。

この2種類の人間達は同じ部署の中で敵対していて、片方と飲み会に行くと片方の悪口を一生しゃべっているような関係です。
僕が見る限り、どちらの言い分も正しくて、どちらが優れているとも劣っているともつけ難いような微妙な関係性を感じています。

ということで、今日は自分の今後のキャリアパスを考える意味も込めてこの両者の言い分と自分の考えをまとめたいと思います。

[ここで注意]
本記事は著者含め大企業で働くエンジニアの友達とslackやtwitterで盛り上がった内容を、全て”ぼく”一人の体験としてまとめ上げたものです。つまり 実話に基づいたフィクションです
とある企業にこの様な記事内の先輩方が実在していたり、著者個人がこの様な体験をしている訳ではないことにご留意ください。

プログラマー先輩から見たSIer先輩へのディスり文句

中抜きしてるだけやん

あったまります。
受注して得たお金で人を安く雇って浮いたお金で飯食って美味いかアハーーン? ってことです。

SIer先輩からしたら、仕様設計をしてニーズを具体化してる所にお金が発生してるでしょ!と言いたいのでしょうが、プログラマー先輩は無形物に価値を見出し難い生き物なのでそんな会話は通じません。悲しい

人に作らせて楽しい?

これもあったまります。
結局実装を投げているので自分のスキル的な成長に繋がらなくて仕事してる意味なくねー? ってことです。

SIer先輩からしたら、人を動かしたり相手先と話をまとめることこそ一番楽しい部分でしょ!と言いたいのでしょうが、プログラマー先輩は基本機械以外と会話する事にストレスしか感じない生き物なのでそんな会話は通じません。悲しい

Qiitaを読まずに論文と仕様書を読め

ぐぬぬ、って感じ。
誰かが読みやすくまとめた資料をさらに斜め読みしただけで分かったつもりになって穴だらけの設計してんじゃねーよ! ってことです。

SIer先輩からしたら、読む時間が無いしそれよりお前はまずビジネス感を養うためにカーネギードラッガーを読め!と言いたいのでしょうが、プログラマー先輩は行き帰りの電車でニコニコしながら仕様書を読めるし最悪個人で起業してもなんとか生計を立てられるという謎の自信に溢れた人間なのでそんな会話は通じません。悲しい

SIer先輩からプログラマー先輩へのディスり文句

自分のキャリアを真面目に考えたら?

あったまります。
あなたはこれからも上がっていく年収に耐えられるだけの技術力を保証し続けられるのですか?無理なんだからマネジメントに回ったら?ってことです。

プログラマー先輩からしたら、それはそもそも日本のプログラマーの地位がおかしいし会社の人事を変えるべき!って言いたいのでしょうが、 SIer先輩は日々ビジネス書を読んでマネジメントの大切さを身に染みて理解しており日本が今後も変わらないことを確信しているのでそんな会話は通じません。悲しい

ビジネス感なさ過ぎヌルすぎ...

あったまります。
品質技術うんぬんよりニーズに合わせて柔軟に人をアサインしていち早く作らないと、顧客は待ってくれませんよ? ってことです。

プログラマー先輩からしたら、ちゃんと事前に技術的な検討をして要素技術の特定をしてスケジュールを練って確定させて進めれば成り立つ!って言いたいのでしょうが、SIer先輩は顧客が途中で手のひらを返してきたり新しい要望を出してきたりして仕様を大きく変更したりなどを何度も実際に経験しているのでそんな会話は通じません。悲しい

派遣の若い奴の方が仕事がデキるけど?

ぐぬぬ、ってなります。
あなたが作れば他人に出来ない技術を入れるか、さぞ品質が高いものを作れるだけの自信があるんですねすごいですねー?? ってことです。

プログラマー先輩からしたら、実際に俺の書いたコードと派遣が書いたコードを比較してみれば俺の方が発展性させやすくて綺麗じゃん!と言いたいのでしょうが、SIer先輩は基本動くものや定量的な指標以外に興味を持たない人間なのでそんな会話は通じません。悲しい


ぼくが思うこと

以上、だいぶ盛ってフィクションぽく書いてますが、実際に働いていて僕が体験したことです。
実際にこれ以外にもSIer v.s. プログラマーの議論は尽きないほどあると思いますが、僕が思うことは大きく以下2点にまとめられます。

以下に詳細をまとめています。

プログラマー先輩に教えられたこと

技術はなるべく1次ソースをしっかり読もう

Qiitaを含めプログラマの情報シェアがすっごい盛んで便利な時代で、論文や技術の名前で検索するとそれを解説してくれる人がいるのが当たり前の時代になっています。
でもやっぱりQiitaを読むだけでは分からないことや見落としていることが絶対にあります。

例えば、 Qiitaで論文のPreviousWorkやFutureWorkを取り上げてくれている人はほとんどいませんが、 実際技術を選択する上で大事なのは他に比べた優位性やその後の発展性 だったりします。

なのでたとえ自分はSIer先輩側に回ったとしても、少なくとも自分が担当する範囲では人がまとめた意見ではなくて自分が技術をしっかり理解して思ったことをベースに話を進めて行きたいと感じました。

目先の成果以外でもプロジェクトを評価できる人間になろう

コードを実際に書いていて思うのは洗練されたコードとただ動くだけのコードは東京大学と東京カスカビアン産業大学くらい違う!ってことです。

洗練されたコードは担当者が変わってもすんなり回りますし、新しい機能追加の要求が来ても既存のコードの抽象化の粒度を変えず美しく実装できます。
対してただ動くだけのコードは鉄の塊みたいな構成でREADMEとは呼べないRunbookが共有されているが、時間が立って依存パッケージがアップデートされて動かなくなると大規模なリファクタリングをしないともう誰も対応できない...みたいな状態になります。

ただ、単純に目先の成果ベースで評価だけしているとそこらへんの差異は見えないことが多いです。
特に派遣の人が書いたコードなどはプロジェクトが発展した時には自分はアサインされていないのでしーーらない!みたいな感じで書かれていることが多い印象です。

なので、 目先の成果とは別にしっかり定量的にプロジェクトの熟練度を評価する仕組みを自分の中で確立させておきたい と思っています。
例えばCMMIなどのフレームワークを使うのが良いと思います。

こちらについてはまた別の機会にじっくりまとめたいと思います。

SIer先輩に教えられたこと

技術者でもなるべく顧客と話そう

ぼくは実際に何度か機会をいただいて、技術者として顧客の人と直にお会いしてお話をした経験がありますが、一度話してみると本当に驚きます。

生粋のプログラマーの人だとイメージつきにくいかもしれないですけど、お客さんとビジネスの話をするのってめちゃくちゃ難しいです。
なんか 技術に関しては夢みたいなことを語って、それに見合ったお金は全然出してくれないけど、本人は技術のことは知らないから悪気は全然ない みたいな状態がほとんどでした。
これから実際にニーズを抽出して要件を定義してプロジェクトをまとめ上げるSIer先輩は本当に優秀だなと心から思いました。

なので、実際に顧客と話してくれる人が別にいたとしても、しっかり会議の議事録を読んだり相手側からのメールを転送してもらったり、マネジメントの人が説明に使ったパワポ資料をもらって顧客の意識を直で把握するのが技術者でも絶対必要だと思います。

お客さんの会社のビジョン・ミッションとか最近力入れてることとかをIR資料とかから探してくることもめちゃくちゃ重要だと思います。

これらは僕が今後どんな立ち位置になろうと絶対にマストな事項です。
僕が常日頃コード書きながら思っているのは、 プログラマーがコードを書いている時でも一番に考えるべきはお客さんへの思いやり であるということです。

プロジェクトで動いているお金を把握しよう

プログラマーの人、特に研究開発よりの人は自分が関わっているプロジェクトにどれくらいのお金が割り当てられているかとか考えたことある人少ないんじゃないかと思います。
マネジメントの人が立てたスケジュールやマイルストーンに合わせて開発をなんとかする、って感じのスタイルの人がほとんどじゃないでしょうか。

社内プロジェクトだと自分たちのチームの工数にかかってる費用((給料ではなく)、社内外から投資を受けている場合はその額を知っておくべきだと思います。
これも僕の持論ですけど、 仕事としてお金をもらっている以上、そのお金をくれた人に対してより多くの利益をもたらさすように動くべき と思ってます。

いっぱい給料もらったり投資してもらったらヤッター儲けたー!って思うのがプログラマーの人に多いような気がするので、僕は常に注意していたいと考えています。

最後に

本章は【SIer v.s. プログラマー】っていうお話でしたが、結局自分の結論は どっちもの悪いところを直して良いところをより伸ばした人材になれば良いじゃん ってことでした。おしまい

TensorFlowチュートリアル和訳(Image Recognition)

TensorFlowのチュートリアルを愚直に和訳していきます。 今回は画像認識についてのチュートリアルです。
TensorFlowの中で動いているC++のコードの説明が行われています。

Image Recognition

私たちの脳は簡単に画像認識をしてしまいます。人間の脳は非常に優れているため、ライオンとジャガーの違いが分かるし、看板を読んだり、人の顔を認識するのは簡単です。
しかし、これらをコンピュータにさせるにはとても難しい問題です。

この問題に対処するために、ここ数年機械学習の分野は非常に進歩しました。
特に、deepな畳み込みニューラルネットワーク(CNN)が難しい画像認識タスクで高いパフォーマンスを達成できることを発見しました。

研究者たちは、画像認識の学術的ベンチマークであるImageNetに対するパフォーマンスを検証することで、認識技術の着実な進歩を実証しています。
QuocNetAlexNetInception(GoogleNet)BN-Inception-v2などの最先端のstate-of-the-artの結果が出ており、モデルの改善が続けられています。
Google社内外の研究者も、これらのモデルを全て記述した論文を発表していますが、その結果はまだ再現するのが難しい状況です。
現在では最新のInception-v3モデルで画像認識を実行するコードを公開しており、次のステップに踏み出しています。

Inception-v3は2012年のImageNet large Visual Recognition Challengeのデータで学習しています。
これは、シマウマやダルメシアン、皿洗い機などを含む1000クラスの分類を行う画像認識の標準タスクです。
例として、以下にAlexNetの分類結果を示します。

f:id:minison130:20170507154001p:plain

モデルの比較をするために、上位5つの推測結果に正解が含まれない頻度である"top-5 error rate"を各モデルごとに調べます。
AlexNetは2012年の検証データセットでtop5 error rateが15.3%を達成しています。
さらに、Inception(GoogleNet)は6.63%、BN-Inception-v2は4.9%、Inception-v3は3.46%を達成しています。

ちなみに、ImageNet Challengeで人間はどれぐらいの精度が出ているのでしょうか?
Andrej Karpathyがブログ記事の中で自身のパフォーマンスを測定しており、top5 error rateが5.1%となっています。
(つまり、最新の結果は人間の精度を凌駕している!)

このチュートリアルでは、Inception-v3の使い方について説明します。
ここでは、PythonC++を使って1000クラスの画像分類をする方法を学びます。
また、他の画像分類タスクに適用させるために、このモデルからより高いレベルでの特徴量を抽出する方法についても説明します。

Usage with Python API

classify_image.pyはプログラムの初回実行時に、訓練されたモデルをtensorflow.orgからダウンロードします。
このために、ハードディスクには約200Mの空き容量が必要です。

まず、GitHubからTensorFlowモデルのリポジトリをcloneし、次のコマンドを実行します。

cd models/tutorials/image/imagenet
python classify_image.py

上のコマンドは以下のパンダ画像を分類します。

f:id:minison130:20170507161400p:plain

モデルが正しく実行されると、スクリプトは以下の出力を生成します。

giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca (score = 0.88493)
indri, indris, Indri indri, Indri brevicaudatus (score = 0.00878)
lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens (score = 0.00317)
custard apple (score = 0.00149)
earthstar (score = 0.00127)

他のJPEG画像を認識したい場合は、--image_file引数を編集してください。

モデルデータを別のディレクトリにダウンロードする場合は、使用するディレクトリを--model_dirで指定する必要があります。

Usage with the C++ API

本番環境で使用するために、C++で同じInception-v3モデルを実行できます。
以下のコマンドを行うことで、モデルを定義するGraphDefを含むアーカイブをダウンロードすることができます。(TensorFlowリポジトリのルートから実行します。)

curl -L "https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz" |
  tar -C tensorflow/examples/label_image/data -x

次に、グラフをロードして実行するコードを含むC++バイナリをコンパイルする必要があります。
the instructions to download the source installation of TensorFlowに従ってTensorFlowを構築している場合は、シェルターミナルからいかのコマンドを実行してサンプルをビルドすることができます。

bazel build tensorflow/examples/label_image/...

このやり方では、バイナリ実行可能ファイルを作成して次を実行する必要があります。

bazel-bin/tensorflow/examples/label_image/label_image

これはフレームワークに同梱されているデフォルトのサンプルイメージを使用しており、次のような出力が得られます。

I tensorflow/examples/label_image/main.cc:206] military uniform (653): 0.834306
I tensorflow/examples/label_image/main.cc:206] mortarboard (668): 0.0218692
I tensorflow/examples/label_image/main.cc:206] academic gown (401): 0.0103579
I tensorflow/examples/label_image/main.cc:206] pickelhaube (716): 0.00800814
I tensorflow/examples/label_image/main.cc:206] bulletproof vest (466): 0.00535088

このケースでは、Grace Hopper提督の画像をデフォルトに使用しています。
認識してみると、ネットワークが軍服を着用していることを正しく認識していることがわかります。
スコアは0.8となります。

f:id:minison130:20170507165129p:plain

次に、–image=引数を指定して自身の手持ち画像で試してみてください。

bazel-bin/tensorflow/examples/label_image/label_image --image=my_image.png

tensorflow/example/label_image/main.ccファイルの中身を調べると、その動作を知ることができます。
このコードはTensorFlow独自のアプリケーションに統合する際に役立ちますので、主な昨日を順を追って説明します。

コマンドラインフラグでは、ファイルのロード元と入力画像のプロパティを制御します。
このモデルでは、正方形の299x299のRGB画像が得られると予想されており、input_width及びinput_heightフラグが対応します。
またピクセル値は0~255の整数値からグラフが操作する浮動小数点値にスケールする必要があり、これらはinput_meaninput_stdフラグが対応します。
ここでは、各ピクセル値からinput_meanを減算し、次にそれをinput_stdで除算します。

これらの値はマジックバリューに見えるかもしれませんが、元のモデル作成者が訓練画像に基づいて設定したものです。
自分で訓練したグラフがある場合は、訓練プロセス中に使用した値に合わせて調節すれば良いです。

ReadTensorFromImageFile()関数を見れば、どのように画像が適用されているかを見ることができます。

// Given an image file name, read in the data, try to decode it as an image,
// resize it to the requested size, and then scale the values as desired.
Status ReadTensorFromImageFile(string file_name, const int input_height,
                               const int input_width, const float input_mean,
                               const float input_std,
                               std::vector<Tensor>* out_tensors) {
  tensorflow::GraphDefBuilder b;

実行のスタートとして、ロードするモデルを指定するために使用するオブジェクトであるGraphDefBuilderを作成しています。

string input_name = "file_reader";
  string output_name = "normalized";
  tensorflow::Node* file_reader =
      tensorflow::ops::ReadFile(tensorflow::ops::Const(file_name, b.opts()),
                                b.opts().WithName(input_name));

次に、入力値をメインモデルの期待値を合わせるために、ピクセル値のロード、サイズ変更、スケーリングを行うためのノードを作成しています。
最初に作成するノードは、ロードする画像のファイル名を保持するテンソル用のConst演算子です。
この値は最初の入力として、ReadFile操作に渡されます。
全ての操作では、最後の引数としてb.opts()を渡しています。
この引数はノードがGraphDefBuilderに保持されているモデル定義に追加されることを保証するものになっています。
また、b.opts()WithName()メソッドの中で名前の指定を行なっています。
これにより、ノードに名前が割り当てられます。
この名前付けは必須ではなく、WithName()メソッドを使わない場合は自動で名前が割り当てられますが、名前付けを行なっておいた方がデバッグが楽になります。

// Now try to figure out what kind of file it is and decode it.
  const int wanted_channels = 3;
  tensorflow::Node* image_reader;
  if (tensorflow::StringPiece(file_name).ends_with(".png")) {
    image_reader = tensorflow::ops::DecodePng(
        file_reader,
        b.opts().WithAttr("channels", wanted_channels).WithName("png_reader"));
  } else {
    // Assume if it's not a PNG then it must be a JPEG.
    image_reader = tensorflow::ops::DecodeJpeg(
        file_reader,
        b.opts().WithAttr("channels", wanted_channels).WithName("jpeg_reader"));
  }
  // Now cast the image data to float so we can do normal math on it.
  tensorflow::Node* float_caster = tensorflow::ops::Cast(
      image_reader, tensorflow::DT_FLOAT, b.opts().WithName("float_caster"));
  // The convention for image ops in TensorFlow is that all images are expected
  // to be in batches, so that they're four-dimensional arrays with indices of
  // [batch, height, width, channel]. Because we only have a single image, we
  // have to add a batch dimension of 1 to the start with ExpandDims().
  tensorflow::Node* dims_expander = tensorflow::ops::ExpandDims(
      float_caster, tensorflow::ops::Const(0, b.opts()), b.opts());
  // Bilinearly resize the image to fit the required dimensions.
  tensorflow::Node* resized = tensorflow::ops::ResizeBilinear(
      dims_expander, tensorflow::ops::Const({input_height, input_width},
                                            b.opts().WithName("size")),
      b.opts());
  // Subtract the mean and divide by the scale.
  tensorflow::ops::Div(
      tensorflow::ops::Sub(
          resized, tensorflow::ops::Const({input_mean}, b.opts()), b.opts()),
      tensorflow::ops::Const({input_std}, b.opts()),
      b.opts().WithName(output_name));

上では、さらに多くのノードを追加し、画像ファイルをデコードし、整数を浮動小数点にキャストし、サイズを変更し、ピクセル値に対して減算と除算を以下のように実行しています。

  // This runs the GraphDef network definition that we've just constructed, and
  // returns the results in the output tensor.
  tensorflow::GraphDef graph;
  TF_RETURN_IF_ERROR(b.ToGraphDef(&graph));

最後に、b変数に格納されたモデル定義を取得し、ToGraphDef()関数を使って、完全なグラフ定義を作っています。

  std::unique_ptr<tensorflow::Session> session(
      tensorflow::NewSession(tensorflow::SessionOptions()));
  TF_RETURN_IF_ERROR(session->Create(graph));
  TF_RETURN_IF_ERROR(session->Run({}, {output_name}, {}, out_tensors));
  return Status::OK();

グラフ定義ができたら、次にtf.Sessionを作っています。
これは、グラフを実行し、出力を取得するノードを出力データをどこに置くかを指定しています。

これにより、Tensorオブジェクトのベクトルが得られます。この場合、存在するオブジェクトは1つのみであることがわかります。
このコンテキストではテンソルは多次元配列として考えることができ、浮動小数点値として299ピクセルの高さと幅、3チャネルの画像を保持しています。
もし、すでに自身の画像処理フレームワークを成果物に適用している場合は、上の代わりに自身のフレームワークを使用する方が良いです。

これはC++で小さなTensorFlowグラフを動的に作成する簡単な例ですが、事前に訓練されたインセプションモデルを使う場合は、なかなか大きなファイル定義を読み込む必要があります。
LoadGraph()関数を見ることで、どのように読み込みが行われているかを知ることができます。

// Reads a model graph definition from disk, and creates a session object you
// can use to run it.
Status LoadGraph(string graph_file_name,
                 std::unique_ptr<tensorflow::Session>* session) {
  tensorflow::GraphDef graph_def;
  Status load_graph_status =
      ReadBinaryProto(tensorflow::Env::Default(), graph_file_name, &graph_def);
  if (!load_graph_status.ok()) {
    return tensorflow::errors::NotFound("Failed to load compute graph at '",
                                        graph_file_name, "'");
  } 

他の画像読み込みコードを見たことがある人は、上で使われている用語もよく知っているはずです。
上では、GraphDefオブジェクトを生成するためにGraphDefBuilderを使用するのではなく、GraphDefを直接含むprotobufファイルを読み込んでいます。

  session->reset(tensorflow::NewSession(tensorflow::SessionOptions()));
  Status session_create_status = (*session)->Create(graph_def);
  if (!session_create_status.ok()) {
    return session_create_status;
  }
  return Status::OK();
}

次に、上ではそのGraphDefからSessionオブジェクトを生成し、呼び出し元に渡して後で実行できるようにしています。

以下で使っているGetTopLabels()関数は画像の読み込みによく似ていますが、この場合はメイングラフを実行した結果を取得し、最高スコアのラベルのソート済みリストに変換しています。
これはGraphDefBuilderを作成し、いくつかのノードを追加して短いグラフを実行して出力テンソルのペアを取得します。
このケースでは、スコアが最高になるインデックス値と、ソートされたスコアを返します。

// Analyzes the output of the Inception graph to retrieve the highest scores and
// their positions in the tensor, which correspond to categories.
Status GetTopLabels(const std::vector<Tensor>& outputs, int how_many_labels,
                    Tensor* indices, Tensor* scores) {
  tensorflow::GraphDefBuilder b;
  string output_name = "top_k";
  tensorflow::ops::TopK(tensorflow::ops::Const(outputs[0], b.opts()),
                        how_many_labels, b.opts().WithName(output_name));
  // This runs the GraphDef network definition that we've just constructed, and
  // returns the results in the output tensors.
  tensorflow::GraphDef graph;
  TF_RETURN_IF_ERROR(b.ToGraphDef(&graph));
  std::unique_ptr<tensorflow::Session> session(
      tensorflow::NewSession(tensorflow::SessionOptions()));
  TF_RETURN_IF_ERROR(session->Create(graph));
  // The TopK node returns two outputs, the scores and their original indices,
  // so we have to append :0 and :1 to specify them both.
  std::vector<Tensor> out_tensors;
  TF_RETURN_IF_ERROR(session->Run({}, {output_name + ":0", output_name + ":1"},
                                  {}, &out_tensors));
  *scores = out_tensors[0];
  *indices = out_tensors[1];
  return Status::OK();

PrintTopLabels()関数はソートされた結果を受け取り、使いやすい形式でそれらを出力します。
CheckTopLabel()関数はそれと非常によく似ていますが、デバッグ目的でトップラベルが期待通りのものであることを確認しています。

最後にmain()でこれらの呼び出しを全て結びつけます。

int main(int argc, char* argv[]) {
  // We need to call this to set up global state for TensorFlow.
  tensorflow::port::InitMain(argv[0], &argc, &argv);
  Status s = tensorflow::ParseCommandLineFlags(&argc, argv);
  if (!s.ok()) {
    LOG(ERROR) << "Error parsing command line flags: " << s.ToString();
    return -1;
  }

  // First we load and initialize the model.
  std::unique_ptr<tensorflow::Session> session;
  string graph_path = tensorflow::io::JoinPath(FLAGS_root_dir, FLAGS_graph);
  Status load_graph_status = LoadGraph(graph_path, &session);
  if (!load_graph_status.ok()) {
    LOG(ERROR) << load_graph_status;
    return -1;
  }

上ではメイングラフを読み込んでいます。

  // Get the image from disk as a float array of numbers, resized and normalized
  // to the specifications the main graph expects.
  std::vector<Tensor> resized_tensors;
  string image_path = tensorflow::io::JoinPath(FLAGS_root_dir, FLAGS_image);
  Status read_tensor_status = ReadTensorFromImageFile(
      image_path, FLAGS_input_height, FLAGS_input_width, FLAGS_input_mean,
      FLAGS_input_std, &resized_tensors);
  if (!read_tensor_status.ok()) {
    LOG(ERROR) << read_tensor_status;
    return -1;
  }
  const Tensor& resized_tensor = resized_tensors[0];

上では、画像の読み込みとリサイズをして処理を行ないます。

  // Actually run the image through the model.
  std::vector<Tensor> outputs;
  Status run_status = session->Run({ {FLAGS_input_layer, resized_tensor}},
                                   {FLAGS_output_layer}, {}, &outputs);
  if (!run_status.ok()) {
    LOG(ERROR) << "Running model failed: " << run_status;
    return -1;
  }

さらに上では画像を入力としてロードされたグラフを実行しています。

  // This is for automated testing to make sure we get the expected result with
  // the default settings. We know that label 866 (military uniform) should be
  // the top label for the Admiral Hopper image.
  if (FLAGS_self_test) {
    bool expected_matches;
    Status check_status = CheckTopLabel(outputs, 866, &expected_matches);
    if (!check_status.ok()) {
      LOG(ERROR) << "Running check failed: " << check_status;
      return -1;
    }
    if (!expected_matches) {
      LOG(ERROR) << "Self-test failed!";
      return -1;
    }
  }

また、上ではテスト目的として、期待した出力を確実に得ることができるか確認することができています。

  // Do something interesting with the results we've generated.
  Status print_status = PrintTopLabels(outputs, FLAGS_labels);

最後にTOPラベルをprintしています。

  if (!print_status.ok()) {
    LOG(ERROR) << "Running print failed: " << print_status;
    return -1;
  }

上で書いているエラー処理はTensorFlowのStatusオブジェクトを使用しています。
これを使うことでok()チェッカーでエラーが発生したかどうかを知ることができ、その後にprintして読みやすいエラーメッセージとして表示できるので便利です。

このチュートリアルのケースだと、オブジェクトの認識を実演していますが、他のモデルでも、あらゆるドメインでも非常に似たコードを使用できるはずです。
このサンプルを作り変えることで、いろんなTensorFlowの使い方が考えられるので、参考にしてみてください!

Resources for Learning More

Michael Nielsenのfree online bookは一般的なニューラルネットワークについて学ぶための優れた資料です。
特に畳み込みニューラルネットワーク(CNN)の場合、Chris Olahの素晴らしいブログ記事があります。
また、上のMichael Nielsenの書籍にも素晴らしい章があります。

畳み込みニューラルネットワーク(CNN)の実装についての詳しい部分はTensorFlow deep convolutioal networks tutorialにジャンプするか、ゆっくり始める場合はML begginers(和訳)、ML expert(和訳)から読むといいです。
より深い内容が知りたい場合は、このチュートリアルでリンク付けした論文の動向を読むと良いです。

終わりに

C++の説明がメインとなっていたので少し難しかったですが、画像認識の発展からTensorFlowの内部の実装までが細かく説明されていて良いですね。
また他のCaffeなどのフレームワークも内部でC++が動いているので、他のフレームワークの内部コードの理解にも繋がる重要な知見が書かれていると思います。

TensorFlowチュートリアル和訳(Deep MNIST for Experts)

TensorFlowのチュートリアルを愚直に和訳していきます。
今回はディープラーニングを使ってMNISTを解くチュートリアルです。ようやくCNNに入ってきますね。 MNISTや機械学習の基礎がわからない人はMNIST for ML Begginersの和訳から読むと良いです。 共通している部分もあるので、中級者の方も先に読み飛ばしておくと理解がスムーズです。

Deep MNIST for Experts

TensorFlowは、大規模な数値計算を行うための強力なライブラリです。
TensorFlowが優れているタスクは深いニューラルネットワークを実装して訓練することです。
このチュートリアルでは深い畳み込みMNIST識別器を構築することで、TensorFlowでのモデルの構築方法を学びます。

About this tutorial

このチュートリアルの最初の部分では、Tensorflowモデルの基本的な実装であるmnist_softmax.pyで何が起きているのかを説明します。
後半では精度を改善するいつくかの方法を示します。

このチュートリアルの各コードをPython環境にコピペしても、コードを読むだけでもOKです。

このチュートリアルを読むことで以下のことが達成できます。

  • MNISTの数字認識を行うために、画像内の全てのpixcelに注目してSoftmax回帰関数を作ります。
  • 数千の画像を使って数字認識モデルを訓練します。(それを行うTensorFlowの最初のセッションを構築します。)
  • テストデータを使ってモデルの精度を確認する
  • 結果を改善するために多層畳み込みニューラルネットワーク(CNN)を構築、訓練、テストする

Setup

モデルを作成する前に、まずMNISTデータセットをロードし、TensorFlowセッションを開始します。

Load MNIST Data

以下のコードをコピペすれば、たった2行でMNISTデータをダウンロードして自動的に読み込みを行うことができます。

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

上のmnistは訓練(training)、検証(validation)、テスト(test)の用データセットをNumpy配列として格納する軽量クラスです。
また、データミニバッチを反復処理するための関数も用意されています。

Start TensorFlow InteractiveSession

TensorFlowは非常に効率的なC ++バックエンドを使用して計算を行います。
このバックエンドへの接続をセッションと呼びます。
TensorFlowプログラムの一般的な使い方は、まずグラフを作成してからセッションで起動することです。

ここでは、InteractiveSessionという便利なクラスを使用します。
これによりコードの構造がより柔軟になります。
このクラスを使うことでグラフの実行と演算グラフの構築を交互に行うことができます。

これはIPythonのようなインタラクティブなコンテキストで作業するときに特に便利です。
InteractiveSessionを使用しない場合はセッションを開始してグラフを起動する前に演算グラフ全体を構築する必要があります。

import tensorflow as tf
sess = tf.InteractiveSession()

Computation Graph

Pythonで効率的な数値計算を行う際には通常NumPyのようなライブラリを使用します。 しかし、これはまだ不十分でGPUで計算をする場合やデータを転送するコストが高い場合などには多くのオーバーヘッドを生じさせてしまいます。

TensorFlowはPythonの外でも重い作業をしていますが、このオーバーヘッドを避けるためにさらにステップが必要です。 TensorFlowはPythonとは独立した単一の高価なオペレーションを実行するのではなく、Pythonの外部で実行される対話型操作のグラフを記述します。 (この様なアプローチは他の機械学習ライブラリでも同様に行われています。)

Build a Softmax Regression Model

このセクションでは、単一の線形層を持つsoftmax回帰モデルを構築します。 次のセクションではこれを多層畳み込みネットワークに拡張します。

Placeholders

演算グラフの構築を始めます。まず、入力画像と出力クラス用のノードを作成します。

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

上の定義では、xy_には特定の値が入っていません。これらはplaceholderと呼ばれるもので、TensorFlowに計算させるために用意する値です。

入力画像xは2次元テンソル浮動小数点数で構成され、ここでは[None、784]のサイズを割り当てます。
この784は[28\times 28]次元のMNIST画像のピクセル数に対応しており、Noneは任意サイズを示します。
出力クラスy_は2次元テンソルで構成され、10次元の1ホットベクトルで構成されます。
この10はMNIST画像がどの数字(0~9)の10種類を示します。

placeholderの引数であるshapeはオプションですが、明示的に指定することでTensorFlowがテンソルの要素数を間違えるバグを防ぐことができます。

Variables

ここで、モデルの重みWとバイアスbを定義します。
これらは追加の入力値としてxy_と同様に扱うことができますが、TensorFlowではそれらを処理するための優れた方法を持っています。それがVariableです。
VariableはTensorFlowの演算グラフで使われる値です。
これは演算の中で修正しながら使うことができます。
機械学習の中でモデルパラメータと呼ばれるものの大半がこのVariableに当たります。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

上ではtf.Variableの呼び出しで各パラメータの初期値を与えています。
この場合、Wbは要素値が全て0のテンソルとして初期化しています。
Wは784×10の行列です。(入力が784次元でと出力が10次元のため)
bは10次元のベクトルです。(出力が10次元のため)

Variableがセッション内で使用される前に、セッション内で変数を初期化する必要があります。
このステップでは、すでに指定されている初期値(この場合は全ての要素が0となる)を取り、それらを各Variableに割り当てます。
これは全てのVariableに対して同時に実行されます。

sess.run(tf.global_variables_initializer())

Predicted Class and Loss Function

ここでは回帰モデルを実装します。たった1行書くだけです!
以下のように、ベクトル化された入力画像xに重み行列Wを乗算し、バイアスbを加えます。

y = tf.matmul(x,W) + b

損失関数も簡単に指定することができます。
損失はモデルの予測値がどれほど悪いかを示すものです。
モデルの訓練では全てのデータを使って損失を最小化するように学習します。
ここで、以下で使う損失関数は目標値とモデルの予測にsoftmax関数を適用したものとの間でクロスエントロピーをとります。
for ML Biginersのチュートリアルと同様に、以下の安定した定式化を使用します。

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))

このtf.nn.softmax_cross_entropy_with_logitsは正規化していないモデルの予測値にsoftmaxを内部で適用し、全てのクラスで合計を取ります。
さらにtf.reduce_meanはこの合計に対して平均を取っています。

Train the Model

モデルと損失関数を定義したので、訓練を行うことはもう簡単です。
TensorFlowは計算グラフ全体を把握しているため、自動微分を使用して各変数に対する損失の勾配を見つけることができます。
ensorFlowにはさまざまな最適化アルゴリズムが組み込まれています。
この例では、クロスエントロピーを下降させるために勾配をステップで減少させる方式を取り、ステップ長を0.5とします。

train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

この1行で実際にTensorFlowが行ったことは、新しい演算を演算グラフに追加することです。
この操作には、勾配の計算、パラメーター更新ステップの計算、更新ステップのパラメーターへの適用、が含まれています。

上のtrain_stepが実行されると、勾配降下の更新がパラメータに適用されます。
つまり、train_stepを以下のように繰り返し実行することでモデルの訓練を行うことができます。

for _ in range(1000):
  batch = mnist.train.next_batch(100)
  train_step.run(feed_dict={x: batch[0], y_: batch[1]})

上の各反復では、100個のトレーニングサンプルデータをbathとして読み込みます。
その後、feed_dictを使用してplace_holderテンソルxy_を各反復用のデータに置き換えて、train_stepの演算を実行します。
ここで、feed_dictをう使うことで演算グラフのテンソルを置き換えることができることに注意してください。
この置き換えplace_holderだけに限定されるものではありません。

Evaluate the Model

作ったモデルはどれぐらい良いものになっているのでしょうか?

ここで、最初に正しくラベルを予測するかどうかを検証してみましょう。
tf.argmaxはある軸に沿ったテンソルの最高エントリーのインデックスを与える極めて有用な関数です。 例えば、tf.argmax(y,1)は学習したモデルが各入力に対して最も可能性が高いと考えるラベルを表し、tf.argmax(y_, 1)には正しいラベルが入っています。 この予測値と正解が一致するかどうかはtf.equalを使って調べることができます。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

上の操作の結果はbooleanのリストで返されます。 どの部分が正しいかを判断するために、浮動小数点にキャストして平均をとります。 例えば[True, False, True, True][1, 0, 1, 1]になり、平均をとると0.75になります。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最後に、テストデータでの精度を求めます。この結果はだいたい92%くらいになるはずです。

print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

Build a Multilayer Convolutional Network

MNISTで精度92%というのは悪い値です。恥ずかしいほどに。
このセクションでは、モデルを小さな畳み込みニューラルネットワーク(CNN)に変更します。
これにより、精度が99.2%になります。 これは最先端の精度ではないですが、それに近いものです。

Weight Initialization

このモデルを作成するには、多くのweightとbiasを作成する必要があります。
パラメータを多くする場合、勾配が0になるのを防止したり対称性を破壊するために、少量のノイズでweightを初期化する必要があります。
ここではReLu)を使用しているので、微小な正の初期バイアスをかけて初期化を行うことで「ニューロンが死ぬ」ことを避ける習慣が必要です。
モデルを構築する時にこれを何度も行うのは面倒なので、以下で2つの関数を定義しましょう。

def weight_variable(shape):
  initial = tf.truncated_normal(shape, stddev=0.1)
  return tf.Variable(initial)

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)

Convolution and Pooling

TensorFlowは畳み込みとプーリングの操作で多くの柔軟性を持ちます。
境界の処理はどうしよう?stride sizeを何にしよう?という疑問があると思います。
この例では、常にバニラバージョンを選択します。
TensorFlowでの畳み込みのデフォルトではstride sizeが1で出力が入力と同じサイズになるように0で埋められます。
また、プーリングでは2x2ブロックのmax poolingを行います。
コードを綺麗に保つために、この操作を以下の関数で抽象化しましょう。

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

First Convolution Layer

これで最初のレイヤーを実装できます。最初のレイヤーは畳み込みとmax poolingで構成されます。
この畳み込みでは5x5パッチごとに32個の特徴を計算します。
この時の重みテンソル[5, 5, 1, 32]の形状を取ります。この形状の最初の2次元はパッチサイズを表しており、その次が入力チャネル数、最後は出力チャネル数を表しています。
さらに、この出力チャネルに対応したバイアスベクトルも用意します。 この時の重みとバイアスは以下のようになります。

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

このレイヤーを適用するには、最初にxを4dテンソルに再構成します。
つまり、第2次元と第3次元を画像の幅と高さにし、カラーチャネルの数を第4次元にしたもので以下のようにxx_imageとして作り直します。

x_image = tf.reshape(x, [-1,28,28,1])

その後x_imageを上の重みで畳み込み、バイアスを加え、ReLu関数を適用し、最後にmax poolingを適用します。
max_pool_2x2メソッドは画像のサイズを14x14に縮小します。

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

Second Convolution Layer

より深いネットワークを構築するために、上で作ったタイプのレイヤーを複数スタックします。
2番目のレイヤーは、5x5パッチごとに64個の特徴を計算します。

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

Densely Connected Layer

2回目のmax poolingで画像サイズは7x7に縮小されています。
さらに、画像全体を処理できるように1024個のニューロンを備えたフルコネクションレイヤーを追加します。 以下では、テンソルをプーリング層からベクトルのバッチに再構成し、重み行列を乗算し、バイアスを加え、ReLuを適用します。

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

Dropout

パラメータのオーバーフィット(過学習)を軽減するために、読み出しレイヤーの前にdropoutを適用します。
まず、dropout中にニューロンの出力が保持される確率を表すplaceholderを作成します。
これにより、トレーニング時にはdropoutを適用し、テスト時にはオフにすることができます。
TensorFlowのtf.nn.dropoutはマスキングに加えてスケーリングニューロンの出力を自動的に処理するので、dropout自体は追加のスケーリングなしで動作します。

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

Readout Layer

最後に以前のセクション(単層sofrtmax回帰)でやったとの同様のレイヤーを追加します。

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

Train and Evaluate the Model

このモデルはどれくらいうまく機能するのでしょうか?
それを訓練して評価するのは以前のセクション(単層softmax回帰)と同じコードで可能です。

違う部分は以下です。

  • 最急勾配降下最適化法をより良いADAMオプティマイザーに変更する
  • feed_dictに追加パラメータkeep_probを追加して、dropout率を制御する
  • 訓練100回ごとにログを吐き出す

以下のコードをコピペして実行すると訓練と評価が可能ですが、2万回の訓練を行なっているので、(プロセッサにもよりますが)おそらく30分 ぐらいかかると思います。

cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
sess.run(tf.global_variables_initializer())
for i in range(20000):
  batch = mnist.train.next_batch(50)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0})
    print("step %d, training accuracy %g"%(i, train_accuracy))
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

このコードを実行すると、最終的な精度は99.2%になっています。

チュートリアルは以上です。
このチュートリアルを行うことで、TensorFlowを使ってより洗練されたディープな学習モデルを素早く簡単に構築し、訓練し、評価する手法を学ぶことができましたね。

終わりに

MNIST Fro ML Beginnersの和訳を読んでいる方はsoftmax回帰の詳細などについての理解ができているはずなので、今回はすんなり理解できそうですね。 ConvolutionやPoolingに入るまでで躓いたり、理解が少し難しかった方は一読されると良いかもしれません。