nikkie-ftnextの日記

イベントレポートや読書メモを発信

近況報告:2020夏、登壇の夏、予定していた全公演を駆け抜けました!🎤

f:id:nikkie-ftnext:20200914224142j:plain

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
朝晩には涼しさを感じるようになり、夏真っ盛りが過ぎたような今日このごろですね。

私はこの夏、世界各地のPyConでのオンライン登壇をやりきりました!
報告ブログを書きます。

目次

駆け抜けた全公演💃

  • 7/23 EuroPython(ネタ1)
  • 7/25 July Tech Festa(ネタ2’)※日本語
  • 8/7 PyCon Africa(ネタ2)
  • 8/28 PyCon JP(ネタ2)※日本語
  • 9/5 PyCon Taiwan(ネタ2)
  • 9/13 PyCon APAC(ネタ1)

「※日本語」の記載がないところは英語登壇への挑戦です。

7/23 EuroPython(ネタ1)

Zoom Webinar + Discord(タイムキープの音声連絡用)という構成での登壇でした。

この4連休で開催されていた pyhack 合宿で terada さんがEel + Vue.jsできれいな見た目のGUIアプリを作っていて、「JS力をつければこんなにできるんだ」と舌を巻きました。

7/25 July Tech Festa(ネタ2’)

Pythonでの自動化だけでなく、スタッフ活動の紹介もしています(なので、ネタ2' 扱いです)。

Zoom Meetingでの登壇でした。

8/7 PyCon Africa(ネタ2)

hopinというプラットフォームでの登壇でした。

8/28 PyCon JP(ネタ2)

Zoom Meetingでの登壇でした。

9/5 PyCon Taiwan(ネタ2)

Google Meet + Discord(タイムキープの音声連絡用)という構成での登壇でした。
この夏私が参加したPyConの中では、Taiwanだけが現地開催しており、そこにリモート登壇するという貴重な経験でした。

9/13 PyCon APAC(ネタ1)

EuroPythonからWorkshop仕様にアップデートしました。

Google Meetでの登壇でした。

テキストチャットの質問を拾ってインタラクティブに進めていきました。
情報量が多すぎたために1時間をオーバーしそうになり、後半は講義形式に切り替えました。
ネタ1はWorkshopの題材として可能性を感じましたし、もっとブラッシュアップできるという手応えです。

総括

昨年末、エンジニアの登壇を応援する会のLT大会で「US PyConにプロポーザルを通す」という"アホ"な目標を打ち立てていました。

3ヶ月で2ネタ6登壇という今年の夏はだいぶ"アホ"でしたね 笑
「US PyConにプロポーザルを通す」こと自体はこれからですが、海外PyConで登壇する経験をした今、達成感を感じています。
「海外のカンファレンスでアウトプットしてみる」という経験ができたことに大満足です!

この夏の登壇が100点だったというつもりは毛頭ありません。
ネタの選び方、構成やプレゼンスタイルなど試してみたい案がいくつか浮かんでいます(特に技術的な「深さ」という点が一番の課題かなと感じています)。
ですが、これらはやってみて見えたことなので、この夏登壇に明け暮れたことに後悔はなく、とても清々しい気持ちです。

6登壇は全く同内容で登壇したわけではありません。
前回のフィードバックや気づきを可能な限り取り込み、その時点での全部(できなかったことも含めて)をぶつけています。
ネタ1、ネタ2それぞれで振り返り予定です。

今感じている清々しさの要因として、PyCon JPの準備と並行してやりきったという点もあると思います。

PyCon JP 2020 コンテンツチームリーダーとしての動き方はやはりたくさん間違えました😅
トークまわりのかなりの部分を1人で動いた(そうならない選択肢が選べないほどに抱え込みすぎた)ことには反省しています。
(そんな状況でも可能な限り支えてくれたスタッフの皆さん、ありがとうございました。今年の私の動き方は持続可能という観点を著しく欠いており、今後の取り組み方を考えねばいけません)
1人で抱え込んだ大量のスタッフタスクと海外PyCon準備の両方をやり切ったのは、自分でもちょっと信じられません。

両方にコミットしていた期間(特に7月、8月)は、プライベートの時間で収まらずに有給を使うこともありました。
快く送り出してくれた同僚諸氏に感謝ですし、「応援いただいた分は仕事で返さなくては」と思っています。

わたし、頑張りましたー四月は君の嘘より)

アニメネタつながりでは、この歌詞が脳内でリフレインしています(興味ある方は、アクエリアスのCMをYouTubeで探してみてください)

君の夢が叶うのは、誰かのおかげじゃないぜ。風の強い日を選んで走ってきた

P.S. ボーナスステージ!

9/26, 27のPyCon Koreaでも登壇予定です。(公演中に採択が決まりました)
事前収録なのですでにZoomで撮り終わっているのですが、スライドの修正やリポジトリ側の更新などできることはあるので、ちょこちょこ完成度を上げていこうと思っています。

Image by Rudy and Peter Skitterians from Pixabay画像リンク

イベントレポート | オンライン開催のみんなのPython勉強会#60(マーケティング回)に(半分)スタッフ参加しました #stapy

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
8/12の「みんなのPython勉強会」の裏側と、そこで知ったある本の感想をアウトプットします。

目次

勉強会の概要

みんなのPython勉強会#60 - connpass

「みんなのPython勉強会」では、Pythonを中心としてプログラミングを仕事、研究、趣味など様々なシーンに生かす方法を一緒に学びます。

今回の主なテーマはマーケティングです。Pythonが得意とするデータ分析のニーズはビジネスやマーケティング分野で高まりを見せています。

勉強会の様子

YouTube アーカイブ

Togetter

配信の裏側:痛恨のZoom設定ミス!😫

これまでのオンライン開催では、スタッフと登壇者がZoom Meetingに集まり、参加者の方はYouTube Liveで配信を見ていました。
今回は、参加者もZoom Meetingに集まる趣向を試しました。

始まるまでお品書きスライドを自動プレゼンしながら、参加者が増えていくのを見てワクワクしていました1
100人に届く直前で本編がスタートしたのですが、直後に100人上限が設定されていることが判明😱
アーカイブ用にYouTube Live配信もあったので、そちらを案内し、100人しか参加できない状態は回避しました。
勉強会中に設定が直り、途中からは再度Zoom参加を案内しています。

Zoomの参加人数の設定は怖いですね。
懇親会では「事前リハーサルで100人入って確認できないですもんね」という話になりました。
毎回振り返りしているので、そのときにスタッフ全員で対策を考えていきます。

個人的には、「Zoomの設定がエクスポートできればいいのに」と思います。
OSCなどZoomを使ったカンファレンスではノウハウの1つとして設定値が共有されるようになりました。
例:新しいZoomミーティングの設定について紹介します - 宮原徹の日々雑感
これはありがたいことですが、退屈な手動操作が大嫌いな私は、設定をコード化したいと思わずにいられません。
人がやるのではなくコードにやらせようとすると、ブラウザ操作自動化かなあ?(設定をいじれるAPIがあるのかな?)
落ち着いたときの素振り材料の1つですね。

閑話休題:ぬいぐるみ🤗

技術者と企画力についてお話しいただいた吉政さんが、パイちゃんとソンくんのぬいぐるみ背景を公開してくださいました!

めっちゃかわいい!😍😍😍
これがあればパツパツなmtgデーも何のそのですね(シリアルなmtg中でも、顔が緩んでしまうのが悩み)

感想:『その仕事、全部やめてみよう』が面白い🤩

冒頭で阿久津さんが紹介していた1冊。
小野さんは過去の登壇者2でもあるそうです。

目次の中の「俺がやったほうが早い病」の治し方に惹かれて読み始めました3
エンジニアの先輩の言葉として、ハッとさせられるところがいくつもありました。

特に興味深かったポイント

  • 阿久津さんが紹介していた、「谷」を埋めるな、「山」を作れ!
  • チームで働くについて示唆があった、職場は「猛獣園」である

「谷」を埋めるな、「山」を作れ!

  • 谷(=弱点)は最小限カバー、山(=強み)を尖らせる
  • 周りの製品を見て谷を埋めようとすると、仕事が増える(99%の仕事は谷を埋めるもの)

例えば文書のレビューは、谷を埋めようとすると、ちょっとした単語選びや敬語などなど、めちゃくちゃ時間がかかるものです。
山があって、谷が最小限カバーされていれば、レビューはパスでいいんだと気づきました。

なぜ山を作るかというと、ユーザを喜ばせる仕事をしたいから。
時間は有限ですし、ユーザを喜ばせるのに直結する仕事が増やせるのは、やりがいがあると思います。
ユーザを喜ばせるのに直結しない仕事はまずやめることを考えてみようと思います。

脱線ですが、「ユーザを喜ばせる」を読んだ後、「ユーザを喜ばせるプロダクトって実はあまりないかも」と気づきました。
読んだ翌日、職場でチームランチがあって、menuでちょっといいランチを取り寄せたのですが、ぶかぶか容器で運んだらそうなるよねという「ウーバーイーツ系の洗礼」を浴びました。
見た目がひどいことになり、全然ちょっといいランチ感はなかった😭ので、しばらくウーバーイーツ系を使うことはないと思います。
店舗は無料で導入できるそうですが、持ち運ぶ部分は伸びしろ(工夫の余地)があるなと思いました。
ここがクリアできれば、ユーザは喜び、広く受け入れられそうです(コンビニの丼もの弁当の容器のノウハウとか使えそうじゃないですか?)

職場は「猛獣園」である

  • とがった部分(山)が異なるメンバーを重ね合わせたチーム→猛獣圏
  • エンジニア風林火山(とがりかたの4類系)
  • キレるのは高みを目指しているから

「キレる」についての考察は興味深かったです。
「高みを目指しているから」、高い理想と目の前の現実にギャップを感じているからキレる。
実は私もよくキレていて(例えば、PyCon JPスタッフ活動で、運用でカバーしようとする提案が出たときはキレてます4)、キレることに対して「よくない」と思っていたのですが、この本で捉え方が少し変わりました。
ただキレて取り繕わなかった結果、同席した方に不快な思いをさせては元も子もないので、マイルドな言い方など、追求の余地があります。

運用でカバーや複雑なもの以外には、人が生み出したわかりにくいもの、性能が悪いものにもキレます5
例えば、以下の2つの製品にはキレました。

  • dropbox:注釈機能の実装がひどすぎる
    • 注釈が増えてくるともう本当にダメです
    • 何百のレビューが付いたdezero3公開レビューでは、ブラウザがもっっっっっっさりして、レビューになりませんでした😡
  • Adobe Document Cloud:共有設定がわかりにくい
    • フォルダという概念がない(常にファイル単位で共有!?)
    • 元ファイル1に対して多の共有ファイルができる(コメントできる人を分けられるようですが、どんなユースケース想定しているんだろう?)
    • Googleドライブなどのファイル共有ツールと類似で理解できない(=わかりにくすぎ😡)
    • 日本語ドキュメントも直訳すぎて意味が取れず、怒りに拍車をかけました

終わりに

8月のstapyについて、ミスの共有と、知った本についての感想を書きました。
途中で抜けてライブで見られていない松本さんパートは後ほど見られたらと思っています。

9月は11日に開催です。
みんなのPython勉強会#61 - connpass

内容はまだ禁則事項ですが、よろしければお越しください!(なんだろう、本が積まれていますね)
懇親会のLTもお待ちしています!!


  1. 『サマー・ウォーズ』を思い出し、夏を感じていました

  2. 私がまだstapyを知らない時代ですね。歴史を感じます

  3. PyCon JPのスタッフ業で忙the殺されている現状はこれがあると思っています。この話はまた別の機会に

  4. 繰り返しますが、私は退屈な手動操作が大嫌いです。また、複雑な手順で運用でカバーするのは本当に嫌です。過去に複雑な手順にしたために、誰でもできる作業ではなくなる(属人化する)というツライ経験もしてきました。複雑は可能な限り避けて単純にを信奉しています(これも谷を埋めようとして複雑にするのではなく、単純に考えて山を尖らせると通じるかも)

  5. 書いていて思ったのですが、エンジニア3大美徳の怠惰と短気が当てはまりそうですね

近況報告:100DaysOfContribution 達成しました!💚

f:id:nikkie-ftnext:20200816022144p:plain

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
オンライン登壇に、今月末のPyCon JPの準備にと、熱い夏を過ごしています。
そんな中、最近達成したあることを記事にします。

目次

100DaysOfContribution

「草」、ご存知でしょうか?🌱
笑いの方の草(ww)ではなく、GitHubのプロフィールの方です。

GitHubのプロフィールにはコントリビューションカレンダーがあります。
このたび、自分のカレンダーを緑で100日埋めることを達成しました1!(現在も継続中)
5月のGWから8月前半で100日です。

  • 5月はremote.pyへの登壇に向けた開発
  • その後PyCon JPのスタッフ活動で使うWebアプリ開発へ移行(〜6月)
  • 7月はEuroPython登壇準備
  • 8月はPyCon Africa登壇準備

と細ーくですが、継続してコードを書いてきました。
やってみて思うのは、100DaysOfContributionであれば、それほど難しくはなかったということです。

戦略:100DaysOfContribution

コントリビューションとしてのカウントは上記のGitHubのページに詳しいです。

  • Issueを立てる
  • プルリクエストを作る
  • デフォルトブランチにコミット(プルリクエストのマージも含む)

言ってしまえば、これをサイクルで回せば、毎日コードを書かなくても薄ーく草を生やすことができます。

  • 1日目:小さいIssueを立てる
  • 2日目:ブランチを切って実装、プルリクエスト作る
  • 3日目:プルリクエストをデフォルトブランチにマージ

1日目と3日目にはコードを書いていません2
それでも草は維持できています。

GWに1週間草が生え、「これを維持してみようかな」と思って続けるうちに上記のサイクルに至りました。
続けられたのはリモートワークに移行し、通勤時間がなくなったことが大きいです。
通勤時間だった時間を使って、コードを書いています。

なお、プルリクエストをデフォルトブランチにマージするとコミットの日付は変わらずに取り込まれます。
これは、カレンダー上は緑が絶えていても、その間フィーチャーブランチに毎日コミットしていれば、プルリクエストマージで緑が復活するということだと思っています。
ただ、私は緑が絶えるのを見たくなかったので、上記サイクルに落ち着きました。
平日に1回か2回は強制的にコードを書くリズムが作れています。

この先の世界:Write Code Every Day

この記事を書きたいと思っていたところで、Write Code Every Dayを知りました3

薄く緑が生やせるようになったら、次のステージは緑を濃くすることだと思っています。
というわけで、薄い緑を続けつつ、濃くする方向で試行錯誤してみようと思います。

終わりに

100DaysOfContributionは実質34CyclesOfContributionでした(1Cycle = 3Days)。
うっすらとでも毎日草を生やすのは、やる前は高いハードルと感じていました。
ですが、やってみるとハードルはそこまで高くはなかったです(少し頑張ったらできたなという感じ。達成した立場のバイアスがかかっているかもしれませんね)
よろしければ、2020年の残り4ヶ月半にいかがでしょうか?


  1. 私の場合、仕事で書いたコードはカレンダーに反映されません。全てプライベートの時間に書いたコードです

  2. コードを書けば、実装が加速するので、インセンティブはあります。無理なく続ける形として、このサイクルに落ち着いています。

  3. https://twitter.com/yosuke_furukawa/status/1294460648999403520?s=20 で知りました

イベントレポート | Python mini Hack-a-thon(オンライン)で"機械学習しました" #pyhack

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
週末の #pyhack で手を動かした内容についてブログに残します。

目次

勉強会の概要

(第112回)Python mini Hack-a-thon(オンライン) - connpass

基本的に毎月開催です。スプリントのゆるい版みたいな感じで各自自分でやりたいことを持ってきて、勝手に開発を進めています。参加費は無料です。 初めての方も常連さんもぜひご参加ください。

オンライン開催にしました

取り組んだこと

日本語テキストの分類タスクに取り組みました。

取り組んだ問題とは異なりますが、履歴書のテキストデータから採用するかしないかを分類するような問題設定です。
手元にはこれまで人がつけた「採用」「不採用」というラベルがあるので、教師あり学習が使えると見込んでいます。
性能のいい機械学習モデルができると、採用されやすい履歴書から人が見ることで、人のリソースを効率的に使えそうです。

データサイエンティストとして1年、独力でどこまででき、どこを補強した方がいいのかの確認を目的に、「機械学習で課題解決できたらいいな」とワクワクしながら取り組みました。

取り組みのログは以下にあります:

うまくいったこと

ホットケーキ作りです!

というのは冗談で、日本語テキストの前処理のための環境設定と思っています。
特にmecab-ipadic-NEologdを更新し、ふだん使っているmecab-python3ではなくfugashiを試しました。

2020年Q1の週1ブログでの素振りにより、前処理をするためのスクリプトはスラスラ書けました。
(今回はモデル訓練までを通して行うことを優先したので、前処理はとても大雑把です)
また、jupyterではなく小分けにしたスクリプトを作って進めたのも感触がよかったです。

課題に感じていること

前処理のコードに比べて、モデルの交差検証での性能評価のコードが書きにくかったです。

  • 正例の方が少ない不均衡なデータのため、推論された確率を使って、閾値以上なら正例とするように処理を加える必要があった
  • これによりsklearnに準備されたcross_val_scorecross_validateを使うのを断念。KFoldを繰り返し処理してモデルの指標の値を算出するように変更

結果、繰り返しが多いコードになり、「もうちょっとうまく書けるのでは?」と感じています。

今後

できたモデルはあまり性能がよくはありません。
前処理が単純すぎるので、工夫する余地が多分にあるという感触です。

またモデルの評価指標を決めずにいろいろなスコアを出力して交差検証したのはあまりよくなかったと感じています。
2クラス分類モデルの評価の指針は、例えば以下の本の5章にありそうです。

懇親会では形態素解析のライブラリの話になりました。
最近はginzaが登場してきたとのことでした。
分かち書き教師なし学習で行う手法も登場していますね(sentencepieceなど)。

参加者、そして、運営の皆さま、1日ありがとうございました!

イベントレポート | オンライン開催のみんなのPython勉強会#58(Sphinx回)にスタッフ参加しました(配信まわり編) #stapy

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
ふた月ぶりの更新です。
5月はPyCon JPスタッフ活動に打ち込んでいました1
「少しずつでもブログにアウトプットしたい」という気持ちが出てきたので、直近でスタッフ参加した「みんなのPython勉強会」の配信運営まわりについて書きます。

目次

勉強会の概要

みんなのPython勉強会#58 - connpass

「みんなのPython勉強会」では、Pythonを中心としてプログラミングを仕事、研究、趣味など様々なシーンに生かす方法を一緒に学びます。

今回のテーマは「Python製ドキュメント生成ツールSphinx丸わかり」

勉強会の様子

YouTube アーカイブ(今後編集されるかもしれませんが17分過ぎから始まります)

Togetter

うまくいったこと

開始前の自動スライドショーです。

5/30のOSC nagoyaにスタッフ参加したところ、幕間にパワポ製のスライドを自動再生した状態でZoomに画面共有するという方法を知りました2
Zoomの機能でYouTube配信に連携している場合は、画面共有されたスライドが自動で進んでいきます。
この方法でタイムテーブルなどを案内するのはいいなと思いました。

これはGoogleスライドでもできそうだったので、今回試してみました。
G Suite アップデート ブログ: Google スライドのプレゼンテーションでの空白画面への切り替えと、自動再生の切り替わり時間オプションの設定

f:id:nikkie-ftnext:20200613013638p:plain

これまで(4月や5月)は開始前の打合せや世間話が配信されていました。
今回は打合せの代わりに自動再生スライドを流せたことで、待機している方も待機しやすくなったんじゃないかと思います。

音楽がほしいというのは、試してみないと得られなかった反響ですね。

課題に感じていること

発表のタイムキープ(残り時間のお知らせ)です。

Zoomの発表者画面はZoomのウィンドウが消える挙動なので、チャットでお知らせしても発表者が気づかない(そもそも開いていない)可能性がありますよね。
そうすると、「ミーティング内の参加者カメラで残り時間を伝えたほうがいいのかな」というのが現時点の感触です。

Zoomで行った懇親会ではスマホのアプリ3をカメラに映して、LTのタイムキープをしました。
5分のLTなのでできましたが、本編のトーク(25分程度)となると別の方法が必要そうです(腕が辛そう)。

小さく始めるならカンペを試してもいいかもしれません。
最終的には5月のOSCのLTで見たドラおばけさんのタイムキーピングを真似したいと思っています(どう実現しているのか、アーキテクチャを調べるところから)。

終わりに

YouTube Liveをご覧いただいた皆さま、登壇いただいた皆さま、そして懇親会までお付き合いいただいた方々、誠にありがとうございました。
スタッフの皆さん、お疲れさまでした!

みんなのPython勉強会の配信の裏側に興味がある方は過去のレポートもどうぞ!

おまけ:ふた月ぶりにブログ更新のきっかけ

ゆめちさんのブログを購読しているのですが、以下の記事に触発されました。
ゆめちさんはPyCon JPスタッフ活動もされつつ、直近はブログを継続更新されています。
スタッフに極振りしがちな私も「ちょっとずつでもブログにアウトプットしていこう」と思った次第です。


  1. この間の私のアウトプットに興味がある方は PyCon JP Blog をご覧ください

  2. 当日の中継を見た方は分かると思います(分割後の動画がアップされたため、当日の中継は現在非公開でした)

  3. Presentation Timerを使っています

イベントレポート | オンライン開催のみんなのPython勉強会#56(サーバサイドエンジニア回)にスタッフ参加しました #stapy

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2020/04/15にみんなのPython勉強会がオンライン開催されました。
3月に続けてスタッフ参加ブログを書きます(驚くほど順調に行ったので、今回は裏側の話は少なめです)。

目次

勉強会の概要

【オンライン+無料開催】みんなのPython勉強会#56 - connpass

"サーバーサイド上級エンジニア"に!!! おれはなる!!!!

今回のテーマは、サーバーサイドエンジニアです。このテーマにぴったりな有識者が勢揃いました。 初級、中級のサーバーサイドエンジニアにターゲットを絞り、もう一段上のレベルを目指せる講演を用意しましたのでご興味のある方はお申し込みください。

Ansible、AWS Lambda、アジャイルとDevOpsと、たしかに一歩進んだ印象を受けるトピックが揃いました。
著名な登壇者が揃った効果か、申込みは560名
PyCon JP現地参加者の半分くらいですね。
現地開催のみんなのPython勉強会ではこうはいきません(オンライン開催で初めて受け入れられる参加者数です!)。

勉強会の様子

YouTube アーカイブ

3 〜stapy #56 Online〜

オンライン開催の舞台裏(約3時間)の共有です。

前回うまくいかなかったZoom→YouTube Liveというアーキテクチャで配信しました。

  • (18:30過ぎ)nikkie、スタッフ&登壇者用のZoomに参加(今回はスタッフも全員自宅から参加です)
  • (18:40頃)画面共有チェックスタート、YouTube Liveにも配信開始
  • (19:00)定刻スタート
  • (21:00過ぎ)機材トラブルもなく(!)、少し押すくらいで終了
  • (21:15)Remoを使ったオンライン懇親会へ移行
  • (22:00過ぎ)オンライン懇親会終了

前回と比べると信じられないくらい順調に進みました


このような感想もいただき、ありがたい限りです。

Remo懇親会の舞台裏

最近注目を浴びている1Remo Conferenceを試しました。
Zoom懇親会は厳しそう、かつ、個人的に試したいと思っていたので、やってみました。

560人の参加者なので300人の部屋を2つ用意。
Present機能を使うと全参加者に対して配信できるようなので、1部屋を「懇親会LT」(オフラインでおなじみ)もできる部屋として用意していました。

20時をすぎた段階での視聴者は270名程度。
「これは2部屋あると混乱する」と直感し、1部屋にまとめて運用しました。

実際の参加者は30名くらいでした。
視聴者の1割くらいというのはやってみての発見です。

ひとまず無事に終わり、今はほっと胸を撫で下ろしています(冷静に考えてみると600人全員来たら大冒険でした)。

参加いただいた方から嬉しい感想もいただきました!

トークの感想も共有します。

『Ansible で始めるサーバーサイドのインフラ構築』(佐藤さん)

Ansibleを知らない/聞いたことがある方向けに、Windows Serverの設定例を通してAnsibleを導入するトーク
「始める」というタイトルの通り、とっつきやすい説明でした。
playbookやroleといった概念を出さずにAnsibleを導入したのが、入門にはうってつけだと思いました。

私自身は業務でAnsibleを触り始め、inventory, playbook, roleなどの概念がまだ掴みきれていません。
このあたりは佐藤さんも執筆に加わった『Ansible実践ガイド』を読んでみようと思います。

Ansibleには、KatacodaのコンテンツやコミュニティのSlackもあるんですね!

『Pythonistaに贈るAWS Lambda入門』(西谷さん)

AWS Lambdaを知らない方向けにサーバレスの概念やLambdaの実装例〜実装ポイントを解説するトーク
Lambdaを触ったことのある身2には、実装ポイントがありがたかったです3

寡聞ですが最近のAWSのマネージドは本当にすごくて4、以下の言葉が印象に残っています(個人的に好きな言葉と結びつきました)。

私がエンジニアとしてのキャリアを始めた時点と比べて、「やらなくてもいいこと」はすでにかなり変わっている印象です。
それにキャッチアップし、使いこなせるようになった上で、自分が価値が出せることを追求しなきゃなと思います。

西谷さんはAWS Black Belt Online SeminarでもLambdaについて話されていて、以下の動画は積ん読です:

アジャイルとDevOps』(長沢さん)

アジャイルやDevOpsの本質(なぜ必要なのか)を考える機会になるトークでした。
私の今いる環境はアジャイルを実践しているのですが、私自身はアジャイルについて深掘ったことはありません。

印象的なところをメモ:

  • ビジネルもサービス/アプリもチームも「変わらない」から「変わる」ようになった(裏にテクノロジーの変化)
  • 技術の不確実性×合意の難しさによる複雑なプロジェクトが増えてきている
  • ソフトウェア自体は安定していない(→安定させようとする)
  • マネジメントからすればアジャイルチームはブラックボックスフルスタックになっているチームを信じる
  • グループ(組織)とチーム。同じ景色を見ているのがチーム(自分ごと)

変わるようになった(=パラダイムが変わった)からこそ、それが前提の「アジャイル」や「DevOps」、さらに「ドメイン駆動設計」(コードの変更しやすさ)の価値が上がっているんじゃないかというのが気づきです。

"サーバーサイド上級エンジニア"に!!! おれはなる!!!!に込められた思い

あべんべんさんの導入から

長所で助ける(補い合う)などコミュニティに通じるところがあるとしみじみ思いました。
ちなみにワンピースは最近広告で攻勢をかけていますね(伏線回収!?)

参加いただいた方のアウトプット!

オフライン開催と一番違うと思ったのは、懇親会の裏で参加レポートが上がってきたこと。
運営スタッフからすると、参加レポートは本当にありがたいです。
オンライン開催で見る人が今回一気に増えたからこそ、参加レポートが即アップされたんだと思います。

感想

濃密な時間でしたー。
とくに本編のコンテンツは過去の開催と比較してもかなり濃かったと思います。
スタッフ活動しつつたくさんインプットできました!

個人的に課題を感じたところを書き出します。

  • オンライン登壇
    • 私は慣れてしまった5のですが、話しているときにリアクションがないので話しづらいんですよね(懇親会の話から)
    • 解決の方向性としては、発表者にリアクションが見えるようにする or Zoomに入っている参加者が聞いている感じを積極的に出す?
  • アナウンスまわり
    • 懇親会のLTについては事前にアナウンスできなかったので、オフラインのLT常連の方には申し訳なかったです(私が急遽Remoにしましょうと提案したためです)
    • 次回は懇親会のLTについて申込時には発表できるように個人的にはしたいです(懇親会LT文化を継続したい気持ち)
    • connpassにRemoの詳細を追記したのも当日なのですが、connpass最後まで読む方ってきっと少数ですよね。これはメッセージを送るべきだったなと思います
  • Remoの使用感
    • オンライン懇親会でビデオチャットって結構障壁が高かったんじゃないかと思います
    • 事前アンケートによると参加者の過半数は初めてでした
    • 見知らぬ人とのビデオチャット自体、すごく勇気がいると思います
    • なので初参加で懇親会まで参加していただいた方には感謝しかありません
    • ビデオチャットの障壁が高いと感じたので、最初はテキストチャットからの方が参加者は増えるのではないかと思います(テキストでどう懇親するか再考が必要)
    • もしくはRemoでのアイスブレイクの工夫(緊張緩和 & 期待される振る舞いを全員で確認)
    • ある程度顔見知りどうしなら運営の労力はあまりいらなそうですが(実際今回は顔見知りの方が多かった)、初参加の方に参加しやすい懇親会にするにはもっと労力をかける必要がありそうというのが個人の感触です

YouTube Liveをご覧いただいた皆さま、登壇いただいた皆さま、そして懇親会までお付き合いいただいた方々、誠にありがとうございました。
スタッフの皆さん、お疲れさまでした!

次回は5周年記念です。お楽しみに!


  1. ZoomでできなくてRemoでできること(参考noteリンク追加)|黒田 悠介 / 議論メシ|noteWeb会議システムRemoを使ってみた。イベントでのWeb懇親会に使えそう!|kurita|note。前者には参考リンクが追加されていました。感じた課題感にアプローチがあるのか、目を通したいところです

  2. 過去の懇親会LTでも扱いました:AWS Lambdaでpip installしたパッケージを使うときにハマったこと 〜zipでアップロードといわれても〜

  3. PyCon Singapore 2019でも聞いたトピックを再び聞いてようやく消化されたように感じています:SG 2019 Day1 16:00〜 Python on AWS Lambda · Issue #16 · ftnext/PyConTalkSummary · GitHub

  4. 機械学習関連でSagaMakerがヤバかったです。学習の自動化が来ました!それも日本リージョンでも使えるんです!

  5. 登壇報告 | 2/29にオンラインで開催されたPyCon mini ShizuokaでDjango入門トークをしました #pycon_shizu - nikkie-ftnextの日記に書きました

週末ログ | GKEでKFServingを使ってPyTorchのモデルのサーブを試しハマりました(後編:デプロイしたら暗中模索)

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。

週末ログの後編です。
PyTorchでモデルを訓練した後、KFServingでサーブする部分です。

目次

1. KFServingが使える環境を用意する

前編を参照ください。

2. PyTorchでテキスト分類するモデルを用意

PyTorchのチュートリアルの中から、テキストの多クラス分類のモデルを選択しました。
AG_NEWSの記事を4つのカテゴリのいずれかに分類します1
KFServingのサンプルにあるCIFAR10を使った画像分類から、問題設定とモデルを変えました。

Python環境

$ python -V  # venvを使用
Python 3.7.3
$ pip list  # 手で入れたパッケージを示します
black                    19.10b0
flake8                   3.7.9
ipython                  7.13.0
requests                 2.23.0
torch                    1.4.0
torchtext                0.5.0

モデルの学習スクリプト

続く3-1のステップを通ったもの(ag_news.py)を示します2

import os
import time

import torch
from torchtext.datasets import text_classification
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.data.dataset import random_split
from torchtext.data.utils import get_tokenizer, ngrams_iterator


NGRAMS = 2
BATCH_SIZE = 16
EMBED_DIM = 32
N_EPOCHS = 5
min_valid_loss = float("inf")


class Net(nn.Module):
    def __init__(self, vocab_size=1308844, embed_dim=EMBED_DIM, num_class=4):
        super().__init__()
        self.embedding = nn.EmbeddingBag(vocab_size, embed_dim, sparse=True)
        self.fc = nn.Linear(embed_dim, num_class)
        self.init_weights()

    def init_weights(self):
        initrange = 0.5
        self.embedding.weight.data.uniform_(-initrange, initrange)
        self.fc.weight.data.uniform_(-initrange, initrange)
        self.fc.bias.data.zero_()

    def forward(self, text, offsets=None):
        if offsets is None:
            offsets = torch.tensor([0])
        embedded = self.embedding(text, offsets)
        return self.fc(embedded)


def generate_batch(batch):
    label = torch.tensor([entry[0] for entry in batch])
    text = [entry[1] for entry in batch]
    offsets = [0] + [len(entry) for entry in text]
    offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
    text = torch.cat(text)
    return text, offsets, label


def train_func(sub_train_):
    train_loss = 0
    train_acc = 0
    data = DataLoader(
        sub_train_,
        batch_size=BATCH_SIZE,
        shuffle=True,
        collate_fn=generate_batch,
    )
    for text, offsets, cls_ in data:
        optimizer.zero_grad()
        text, offsets, cls_ = (
            text.to(device),
            offsets.to(device),
            cls_.to(device),
        )
        output = model(text, offsets)
        loss = criterion(output, cls_)
        train_loss += loss.item()
        loss.backward()
        optimizer.step()
        train_acc += (output.argmax(1) == cls_).sum().item()
    scheduler.step()
    return train_loss / len(sub_train_), train_acc / len(sub_train_)


def test(data_):
    loss = 0
    acc = 0
    data = DataLoader(data_, batch_size=BATCH_SIZE, collate_fn=generate_batch)
    for text, offsets, cls_ in data:
        text, offsets, cls_ = (
            text.to(device),
            offsets.to(device),
            cls_.to(device),
        )
        with torch.no_grad():
            output = model(text, offsets)
            loss = criterion(output, cls_)
            loss += loss.item()
            acc += (output.argmax(1) == cls_).sum().item()
    return loss / len(data_), acc / len(data_)


if __name__ == "__main__":
    if not os.path.isdir("./.data"):
        os.mkdir("./.data")

    train_dataset, test_dataset = text_classification.DATASETS["AG_NEWS"](
        root="./.data", ngrams=NGRAMS, vocab=None
    )
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # VOCAB_SIZE = len(train_dataset.get_vocab())
    # NUM_CLASS = len(train_dataset.get_labels())
    model = Net().to(device)

    criterion = torch.nn.CrossEntropyLoss().to(device)
    optimizer = torch.optim.SGD(model.parameters(), lr=4.0)
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9)

    train_len = int(len(train_dataset) * 0.95)
    sub_train_, sub_valid_ = random_split(
        train_dataset, [train_len, len(train_dataset) - train_len]
    )

    for epoch in range(N_EPOCHS):
        start_time = time.time()
        train_loss, train_acc = train_func(sub_train_)
        valid_loss, valid_acc = test(sub_valid_)

        secs = int(time.time() - start_time)
        mins = secs // 60
        secs = secs % 60

        print(f"Epoch {epoch+1} | time in {mins} minutes, {secs} seconds")
        print(
            f"\tLoss: {train_loss:.4f}(train)\t|",
            f"\tAcc: {train_acc * 100:.1f}%(train)",
        )
        print(
            f"\tLoss: {valid_loss:.4f}(valid)\t|",
            f"\tAcc: {valid_acc * 100:.1f}%(valid)",
        )

    print("checking the results of test dataset...")
    test_loss, test_acc = test(test_dataset)
    print(
        f"\tLoss: {test_loss:.4f}(test)\t|\tAcc: {test_acc * 100:.1f}%(test)"
    )

    torch.save(model.state_dict(), "model.pt")

    ag_news_label = {1: "World", 2: "Sports", 3: "Business", 4: "Sci/Tec"}

    ex_text_str = "MEMPHIS, Tenn. – Four days ago, Jon Rahm was \
        enduring the season’s worst weather conditions on Sunday at The \
        Open on his way to a closing 75 at Royal Portrush, which \
        considering the wind and the rain was a respectable showing. \
        Thursday’s first round at the WGC-FedEx St. Jude Invitational \
        was another story. With temperatures in the mid-80s and hardly any \
        wind, the Spaniard was 13 strokes better in a flawless round. \
        Thanks to his best putting performance on the PGA Tour, Rahm \
        finished with an 8-under 62 for a three-stroke lead, which \
        was even more impressive considering he’d never played the \
        front nine at TPC Southwind."

サーブするモデルに渡すテンソルを用意

今回は小さく試すため、テキストではなく、テンソル化した(=前処理済みの)テキストをモデルに与えるとします3

$ python -i ag_news.py
 :
Epoch 5 | time in 0 minutes, 22 seconds
    Loss: 0.0022(train) |   Acc: 99.0%(train)
    Loss: 0.0000(valid) |   Acc: 90.7%(valid)
checking the results of test dataset...
    Loss: 0.0002(test)  |   Acc: 89.6%(test)
>>> tokenizer = get_tokenizer("basic_english")
>>> ex_tokenized = tokenizer(ex_text_str)  # ag_news.py中に定義したテキストをトークナイズ
>>> vocab = train_dataset.get_vocab()
>>> tokens = [vocab[token] for token in ngrams_iterator(ex_tokenized, NGRAMS)]
>>> len(tokens)
237
>>> import json
>>> # テンソル化したテキストのJSONを作成
>>> with open('input.json', 'w') as f:
...     json.dump({'instances': [tokens]}, f, indent=4)

3. KFServingでサーブを試す

3-1 ローカルでモデルをサーブする

まず、PyTorchのモデルをKFServingでサーブするサンプルで使っているpytorchserverを入手します(python -m pytorchserver ...)。
これはPyPIには上がっておらず、リポジトリのコードからインストールする必要がありました。

$ git clone git@github.com:kubeflow/kfserving.git
$ cd kfserving/python/pytorchserver/
$ pip install -e .
$ cd ../../..  # 元の階層に戻る

ディレクトリ配置

.
├── __pycache__
├── ag_news.py  # 2で作成
├── env
├── input.json  # 2で作成
├── kfserving  # 3-1でclone
├── model.pt  # 2で作成
└── pytorch.yaml  # 3-2で作る

model.ptがあるディレクトリでpython -m pytorchserver --model_dir ./ --model_name pytorchmodel --model_class_name Netを実行します。
--model_dirで指定したディレクトリにあるPythonスクリプト(ここではag_news.py)をimportするようです。

  • Pythonスクリプト複数あるとエラー になりました
  • if __name__ == "__main__":を書いていなかったら、学習を1から実行しました

サーバが起動したら、サンプルに沿ってrequestslocalhostにinput.jsonの内容をPOSTします。

In [1]: import json

In [11]: with open('input.json') as f:
    ...:     form_data = json.load(f)
    ...:

In [14]: import requests

In [15]: res = requests.post('http://localhost:8080/v1/models/pytorchmodel:predict', json=form_data)

In [16]: res
Out[16]: <Response [200]>

In [17]: res.text
Out[17]: '{"predictions": [[-1.8232978582382202, 7.340854644775391, -1.85544753074646, -4.12541389465332]]}'

ここでつまづいたのは2点:

(1) モデルをロードする際に引数を渡せません4

self.model = model_class().to(self.device)

そこで、Net.__init__の引数にデフォルト値を指定しました(マジックナンバーの正体です)。
以下のようにデフォルト値を調べましたが、引数のデフォルト値は一度だけ評価されるので、len(train_dataset.get_vocab())を指定してもいいかもしれません。

In [2]: from torchtext.datasets import text_classification

In [3]: train_dataset, test_dataset = text_classification.DATASETS["AG_NEWS"](
   ...:         root="./.data", ngrams=2, vocab=None)

In [4]: len(train_dataset.get_vocab())
Out[4]: 1308844

In [5]: len(train_dataset.get_labels())
Out[5]: 4

モデルのロードで引数を渡したいと、Issueも上がっていました:

(2) 送られてくるJSONは以下のように操作されます5

inputs = torch.tensor(request["instances"]).to(self.device)
self.model(inputs).tolist()

model(inputs)と渡すためNet.forwardoffsets引数にもデフォルト値が必要でした6
現状では、POSTしたJSONからoffsets引数に値は渡せないと思います。

また現状は、POSTするデータとしては、一度に1テキストしかポストできないようです(キーがinstancesですが、値の実体はinstance)。

2. GKEでモデルをサーブする(ハマり中)

訓練したモデルをGCSに置きます(今回バケットはコンソールから作りましたが、gcloudコマンドでも作れそうですね)。

$ gsutil cp model.pt  gs://nikkie-knative-project/models/pytorch/ag_news/

サンプルに沿ったyamlファイルを準備します。

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "pytorch-agnews"
spec:
  default:
    predictor:
      pytorch:
        storageUri: "gs://nikkie-knative-project/models/pytorch/ag_news/"
        modelClassName: "Net"

kubectl applyでKFServingのリソースをデプロイ!

$ kubectl apply -f pytorch.yaml
inferenceservice.serving.kubeflow.org/pytorch-agnews created

ところが、READYがFalseとなって、URLを取得できません😱

$ kubectl get inferenceservices
NAME             URL   READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
pytorch-agnews         False                                      95s

暗中模索

似た事象のIssue

k8sの練度が低く、Issueの情報が活かせていません。
特に、どのリソースについてログが見られるかがよく分かっておらず、「Issueに書いてある情報はどうやって出すんだろう」という状況です。

別の問題を解消

GitHub - kubeflow/kfserving: Serverless Inferencing on Kubernetes にあったインストール後のテストを試しました。

$ kubectl get po -n kfserving-system
$ kubectl apply -f kfserving/docs/samples/sklearn/sklearn.yaml
$ kubectl get inferenceservices sklearn-iris
NAME           URL   READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
sklearn-iris         False                                      19s

READYがFalseだった7ため、テストに続くトラブルシューティング8からk8s 1.15以上でオススメのコマンドを試します(リソースはdeleteしています)。

kubectl patch mutatingwebhookconfiguration inferenceservice.serving.kubeflow.org --patch '{"webhooks":[{"name": "inferenceservice.kfserving-webhook-server.pod-mutator","objectSelector":{"matchExpressions":[{"key":"serving.kubeflow.org/inferenceservice", "operator": "Exists"}]}}]}'

すると、READYがFalseという事象は解決しました。
しかし、リクエストを送ると503が返ります。

$ kubectl get inferenceservices sklearn-iris
NAME           URL                                                              READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
sklearn-iris   http://sklearn-iris.default.example.com/v1/models/sklearn-iris   True    100
$ kubectl port-forward --namespace istio-system $(kubectl get pod --namespace istio-system --selector="app=istio-ingressgateway" --output jsonpath='{.items[0].metadata.name}') 8080:80

# 別のターミナルで
$ curl -v -H "Host: sklearn-iris.default.example.com" http://localhost:8080/v1/models/sklearn-iris:predict -d @./kfserving/docs/samples/sklearn/iris-input.json
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /v1/models/sklearn-iris:predict HTTP/1.1
> Host: sklearn-iris.default.example.com
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 76
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 76 out of 76 bytes
< HTTP/1.1 503 Service Unavailable
< date: Sun, 12 Apr 2020 09:28:08 GMT
< server: istio-envoy
< connection: close
< content-length: 0
<
* Closing connection 0

CIFAR10の例でも同様のエラー

git cloneしていたので試してみました。
しかしながら、READYはFalseのままです。

前掲のトラブルシューティングで知ったログの出力を試すと

$ kubectl logs -l app=networking-istio -n knative-serving
 :
W0412 13:10:04.732258       1 reflector.go:299] runtime/asm_amd64.s:1357: watch of *v1.ConfigMap ended with: too old resource version: 137817 (139711)

too old resource version ... 🤔(分からない。。)

お片付け

逆順にdeleteしていきます。

$ kubectl delete -f pytorch.yaml
$ kubectl delete -f $CONFIG_URI
$ kubectl delete --filename https://github.com/knative/serving/releases/download/v0.13.0/serving-istio.yaml
$ kubectl delete -f https://raw.githubusercontent.com/knative/serving/master/third_party/istio-${ISTIO_VERSION}/istio-minimal.yaml
$ kubectl delete -f https://raw.githubusercontent.com/knative/serving/master/third_party/istio-${ISTIO_VERSION}/istio-crds.yaml
$ kubectl delete --filename https://github.com/knative/serving/releases/download/v0.13.0/serving-core.yaml
$ kubectl delete --filename https://github.com/knative/serving/releases/download/v0.13.0/serving-crds.yaml

# 1-1で参照したドキュメントより Cleaning up
$ gcloud container clusters delete $CLUSTER_NAME --zone $CLUSTER_ZONE

感想

ローカルでは動くので、GKE上の環境の構築ミスだと思うのですが、k8sの練度が低くて今回は切り分けられませんでした。
k8sの理解が甘いので、ひとまずリソースとログの関係をキャッチアップしたいところです。
IstioやKnativeをそれぞれ触って理解を深めるのもいいかもしれません。
また、今回は中を見ずにapplyしたyamlファイルを覗いて9、どうあるべきかを掴むことも考えられます。

KFServingはじめKubeflowは機械学習環境を自力で自由に整えられるツールと理解しています。
好きなように整えられるのは魅力ですが、自由を謳歌するにはやはり技術力が必要ですね。
他方では、マネージドな機械学習環境として、SageMakerなどクラウドベンダー各社が提供するものを使うという選択肢もあります。

KFServingのyamlは非常にシンプルだったので、k8sの練度を上げて、今回の事象を解決したいところです。
読まれた方でピンときた方はコメントやTwitterで教えていただけると大変助かります。


  1. torchtext.datasets — torchtext 0.5.1 documentation

  2. PyTorchを使っての発見もいくつかあったのですが、それはまたの機会とします

  3. 今回デプロイするリソースはpredictorだけですが、pre-processorpost-processorをデプロイすることにより、KFServing側で前処理(例:テキストのテンソル化)や後処理(例:出力されたテンソルをクラスラベルに変換)ができそうです(前掲の101 Slidesより)

  4. https://github.com/kubeflow/kfserving/blob/8c261457b3ec8017736b882c4ffd3379914471ac/python/pytorchserver/pytorchserver/model.py#L57

  5. https://github.com/kubeflow/kfserving/blob/8c261457b3ec8017736b882c4ffd3379914471ac/python/pytorchserver/pytorchserver/model.py#L66

  6. この検証には Saving and Loading Models — PyTorch Tutorials 1.4.0 documentation が参考になりました。対話モードでモデルをロードし試行錯誤しました。

  7. もしかするとkubectl get inferenceservicesで確認するタイミングが早かったのかもしれません

  8. https://github.com/kubeflow/kfserving/blob/master/docs/DEVELOPER_GUIDE.md#troubleshooting

  9. GitHub - kubeflow/manifests: A repository for Kustomize manifests