プロテインを溶かしやすくする方法

私はふだんこのプロテインを飲んでいます。

値段は高めですが WPI 製法のプロテインだとおなかが痛くならないので良いんですよね。 味がないので、前に大量購入してしまった野菜ジュースで割って飲んでます。

で、先月くらいからこの MCT オイルを混ぜてシェイクするようにしてみました。

きっかけは前に MCT オイルを買ったときに Amazon から間違って複数個届き (そのままもらってくれと言われた)、処理に困っていたので試しに入れてみたという感じです。 MCT オイルはよくわかってないけど体に良いらしいです。

このオイルをいれると野菜ジュースが乳化して、粉がいい感じに溶けます。

プロテインがのみやすくなって、かつ MCT オイルも適当に消費できるのでおすすめです。

マットレス周りの試行錯誤の話

最近睡眠周りで抱えていた問題が解消されたのでメモ。 自分用 & 他の人の参考になればいいなとおもっています。 ふと振り返ってみると昔から睡眠にそれなりの時間とお金 (合計100万弱) をかけてきている事に気づいたので、多少参考になるかもしれないです。

あと、ベッドに関しては体型によってかなり最適なものが変わると思いますが、色々と試行錯誤してきた期間の身長体重は 174~6cm/62~65kg のような感じでした。 また、ベッドサイズはずっとセミダブルを使っています。一人で寝ているのですがシングルよりセミダブルのほうが広々していてよく寝られます。

始めて買った高級ベッド

昔からベッドを使っていましたが、始めて睡眠の質を意識してお金をかけたのは 2017 年の大学院生の頃でした。 この頃たまたまインターンシップでまとまったお金が手に入り、よくある人生の 1/3 は睡眠に使うのだからそこにお金をかけるべきといった言葉に影響されて高級マットレスを買うことにしました。

色々とネットで情報を調べていくつかの店舗で実際に試した後、結局買ったのが Serta のスーペリアデイプレミアム というフレームを合わせると当時値引きしたうえで 30 万円弱の超高級マットレスでした。 これだけお金をかけているのだから素晴らしい寝心地になるだろうとワクワクしていたのを覚えています。

しかし実際に届いて使ってみた次の日の朝、腰が異様に痛くて絶望しました。 確かに寝心地は良かったのですが、クッションが分厚すぎるせいか寝ている間常に腰の筋肉に力が入っている感じになり、これが腰痛の原因になっていました。 裏返してクッションのない方を上にして寝たり色々と試してみたのですが、結局この問題は解消せず、5年ほど頑張って使った後次のマットレスに買い替えることになりました。

この時の経験から、これ以降よく高級マットレスブランドである高価格帯のクッションが上についているマットレスは腰痛の原因になる印象で検討すらしなくなりました。 余談ですが、某有名配信者が似た問題に悩まされているのをたまたまみて自分以外でも同様のケースがあると知ったので、珍しい話ではないのかなと思っています。

結局ニトリのベッドに戻る

次に買ったのが具体的な商品名は忘れましたがニトリの N sleep の 5~6 万くらいのベッドでした。 当時妻がニトリのベッドを使っていてそれがすごく良いと絶賛していたので、試しに買ってみたのがこのベッドでした。

このベッドが実際に寝心地が素晴らしく良く、すごく気に入っていました。 ただ 2 年ほどでだいぶヘタって腰が沈むようになってしまい、首や頭でそれを支え続けて翌朝痛くなるということが置き始めたので次のベッドに買い替えました。

高級ベッドにリトライ

ニトリのベッドの代わりに買ったのが日本ベッドのマットレスでした。 一度ニトリのベッドで十分だとは思ったのですが、やはりお金をかければより良い睡眠を手に入れられるのではないかという期待を拭えず再度高級ベッドにリトライしました。

ニトリのもので腰が沈むのが問題だったこともあり、シリーズは忘れたのですが硬めのものを買いました。シルキーポケットではなかったですが 10 万以上したと思います。

ただこのベッドが最悪で (ベッドが悪いというより体にあっていなかった)、買って 1~2 ヶ月で買い替えることになりました。 あまりに硬すぎて寝れないし、寝て起きたら更に疲れが溜まっているみたいな感じで流石に生活に支障が出ていました。

シモンズに行き着く

早急にベッドを買い替える必要がでたので、シモンズギャラリー東京などで試した後にシモンズのゴールデンバリューを購入しました。 これが今使っているマットレスです。 もともとシモンズは広告費にお金をかけすぎているみたいな話をネットで見て敬遠していたのですが、ホテルで使って寝心地が良かったマットレスがことごとくシモンズだったので試しに買ってみたというのがシモンズに行き着いた背景です。

買ってすぐは前回と同じくちょっと硬すぎて絶望していたのですが、一ヶ月ほど使うとそれなりに柔らかくなってそれなりに寝れるようになりました。

ただ 1 年ほど使ってもなおすこし硬すぎて、具体的には背中のアーチ型が制限されている感がある、朝起きると背中が少し痛い、横向きで寝ると方が沈みきらずしんどいという状況に不満をずっと感じていました。 この次点で正直購入時に迷ったもう一つ柔らかいニューフィットにしておけばよかったと後悔していました。

ベッドパッドで最高のマットレスが完成

そんなとき、厚めのベッドパッドで調整できないかという発想を持つようになりました。 結論から言うとこれが大正解で、これにより今は最高の寝心地が手に入っています。

買ったのはプレミアムレストベッドパッドというシモンズのベッドパッドで、これがぶ厚めなので寝心地に改善に大きく寄与してくれます。 トップにクッションがあるマットレスのときのように若干腰に力が入る感じはありますが、ほぼ気にならないし朝起きてもいたいということは全く無いです。 横向きに寝る際の不自由さも完全に解消されました。

ベッドパッドというのはシーツとマットレスの間に入る寝具で、シーツの上に敷く敷きパッドとは別のものになります。 このニトリの説明等調べるとわかりやすい図が出てきます。

ちなみに敷きパッドもかなり寝心地の改善に寄与してくれて、今使っているのはニトリの 2-3000 円くらいのタオル生地ものですが、触り心地が良くてかなり寝心地の改善に寄与してくれます。

まとめ

マットレス周りのこの 10 年弱の試行錯誤をまとめてみました。 誰かの役に立てば幸いです。

枕についてもいくつか試行錯誤してるので、また後日時間があれば書きたいです。

Certified ScrumMaster (CSM) training course に参加した話

概要

タイトルの通り、CSM training course に参加しました。

この記事では主に以下の情報を未来の自分と興味のある人向けに書きます。

  • CSM や Scrum についての基礎知識・知識への導線
  • CSM training を受けた感想・学び
  • training の内容 (public なものではないので)

背景

私は普段スクラムチームで働いているのですが、今年から Scrum Master (SM) ロールが空いてしまうので私が SM になろうとしたのが背景です。前任の SM の方に CSM が良い training の場になるとおすすめして頂いたのでこのコースを選びました。

ちなみに私自身はこれまで Scrum でいう Developers (Dev) ロールの SWE として働いていて、今後も SM 専属でやりたいというよりはメインでは Dev として働きたいと思っていました。ただ SM と Dev の兼任は非推奨らしく、この辺どうするかな〜というのが一つの悩みです。(なので SM を検討している人はキャリアの舵取り観点でこの辺も一度考えたほうが良いかも...)

CSM とは

CSM とは Odd-e Japan によると

スクラムマスターの認定資格にはいくつか種類がありますが、最もメジャーと言われている認定資格は、Scrum Alliance® の認定スクラムマスター(Certified ScrumMaster®:CSM®)です。

Scrum Alliance® においては、まずこの認定資格を取得し、実践経験を積んだうえで、さらに上位資格へステップアップしていくことになります。

とのことで、Scrum Alliance における Scrum Master のための基礎資格です。より上位の資格としては CSM® → A-CSM® → CSP®-SM があるらしいですが、今回私が受けたのは CSM® 向けの training です。

私が受けた training は日本人唯一の CSP Educator である認定スクラムトレーナー(CST®)の 江端一将(ebacky)san が講師を務めるもので、13:00~19:00 * 5 days 税込み ¥330,000 のものです。Tech会というサイトを経由して参加しました。なお Product Owner (PO) 向けの CSPO や Dev 向けの CSD というものもあり、SM 以外の方向けの training もあります。ちなみに CSM は training に参加すれば全員が資格をもらえるというわけではなく、

本トレーニング中に能力を評価された方は、認定スクラムトレーナー(CST®)によって Scrum Alliance® に登録されます。

残念ながら、本トレーニングは、お金で資格を取得(購入)するための研修ではございません。

Scrum Alliance® に登録されるとオンライン試験の受講資格が与えられ、このオンライン試験に合格すると認定スクラムマスター(CSM®)と認定されます。認定スクラムマスター (CSM®) は、スクラムマスターとして必要なスクラムの基礎を理解していることを示します。

とのことで、ちゃんと training 内で SM としての素養を認められ、かつその後試験に合格しなければ CSM を取得できないです。

Scrum とは

Agile の代表的なフレームワークの一つで、Scrum Guide (日本語 ver) にその内容が書かれています。ここで書かれているのは基本的にはスクラムフレームワークであり、

スクラムフレームワークは意図的に不完全なものであり、スクラムの理論を実現するために必要な部分のみが定義されている。

ここで概要を述べたように、スクラムフレームワークは不変である。スクラムの⼀部だけを導⼊することも可能だが、それはスクラムとは⾔えない。すべてを備えたものがスクラムであり、その他の技法・⽅法論・プラクティスの⼊れ物として機能するものである。

と書かれているように、実際にはプロダクト開発に適用するためフレームワークの上に状況に応じた様々なプラクティスが乗って運用がなされるものだと理解しています。例えば Transparency Inspection Adaptation を pillars としたり Sprint Planning などのイベントの実行はフレームワーク、一方でどのように pillars を活用したり Sprint Planning を実行 (ex. N hours で〇〇という内容をする) するかといった話がプラクティスと理解しています。

Scrum のイメージ図 from https://www.pastoraldog.com

感想

ここからは参加した感想を書いていきます。といっても training の内容にふれるのであまり踏み込んだ話はできないですが...

まず、この training はめちゃくちゃタフでした...。3 日目くらいに一度胃薬を飲んだ日がありました。何がつらいかというと、私の場合は ① それぞれが全く異なる個性を持っていることにより自然に共通認識を持てることを期待できない人々 (年齢・職種・タイプ・経歴・etc...)② 大勢集まった集団において ③ 理解しきれていない Scrum を駆使しつつ ④ CSM を取得するに値するように集団の成果に貢献しないといけない という部分がきつかったです。結局 training 完了後の今でも十分パフォーマンスを出せたと思っていなくて、テストの受験資格を貰えない可能性が十分あると思っています...。

また、学びに関してはかなりありました。Scrum を学びたい人にとって、参加する価値は十分にある training だと断言できます。まず理論に関して、public なガイドや書籍を読んだだけでは知り得ない内容を体験を伴うことで非常に効率よく学ぶことができました。具体的には、フレームワークの正しい解釈とその背景にある考え方を知ることができ、またその重要性自体を学ぶことができました。またフレームワークに則ったプラクティスのデザインについても独学では得られない有効な学びを得られたと思っています。次に実践についても、何が難しくてその場合どのように対処していけば良いかということを 5 days の間常に考え続けており、ピュアにスクラムの実践に向き合い続けることができたためこの形式でしか得られないような貴重な経験を得られたと思います。これらは独学を頑張れば学び得るが難易度が高いので training に参加した方が良い、というよりは独学では学び得ないものと言ってもよいものだと思っていて、この意味で私はある程度 Scrum を真面目にやりたいのであればこの training に参加することをおすすめします。(精神的にはつらいですが...)

学んだこと

これもあまり踏み込んだ内容については書かないですが、一般的な key takeaways だけ残しておきます。どちらかというと training でこう教えられたというより自分がこう解釈したやこう思ったという内容なので、training で実際に学べることとは乖離がある可能性もあります。

  • Scrum Team や Scrum 自体の現状を知るために、Pillars (Transparency Inspection Adaptation) が保てているか、そして Values (Commitment Focus Openness Respect Courage) が育めているかという観点で自問自答すれば改善点が見えてきそう。
  • SM として正しく本質的にあるいは未然に問題解決を行うために、チームやプロジェクトの状態やアイデアのモデル化及びその包括的な解釈が役に立ちそう (ex. チームで次の Quarter にどんなものを作るか考える際にどうやって決めるかを一度モデル化してみて、それで意思決定を行う上でどんな問題が起こるか考えてみる)
  • SM は Scrum がうまくワークするために team だけでなく organization に対してもアプローチする必要がある。
  • (個人の反省として) Scrum とは関係ないけど、集団に対する提言のやり方は結構反省点が多かった。例えばただ可能性を提示するだけでは、特に集団の複雑性が高い場合、ただ議論の複雑度を増していることにしかならない場面は結構ある。

おわりに

この training に参加して、Scrum への理解が飛躍的に増しました。ふと現職に転職して最初 team としての開発能力や適応能力などの完成度が異常に高いことにびっくりしましたが、その一部の要因が Scrum をある程度以上ちゃんと運用できていたからだと思いました。

本文でも書きましたが、Scrum にある程度真面目に取り組みたい方は training への参加を考えても良さそうだと思いました。個人的には多少 Scrum を経験したあとに参加するとより理解が深まる気がします。

また、初めて Scrum をやる人は、(もしかしたらかえって表層だけの理解に陥る原因になってしまうかもしれないですが) 私が読んだこの本は漫画形式で実際に Scrum を適用する話とともに Scrum を学べるので、一読してから参加すると多少イメージが湧きやすくなるかもしれないです。

HHKB Professional HYBRID Type-S を買った話

概要

最近 Happy Hacking Keyboard Professional HYBRID Type-S 無刻印/雪(英語配列) を購入したので周辺情報を書き残しておきます。

https://www.pfu.ricoh.com/direct/hhkb/detail_pd-kb800yns.html

購入理由と使用感

もともと HHKB Professional BT を使用していたのですが、以下の理由で HYBRID Type-S 無刻印/雪 へと買い替えました。
1. desktop PC (win&linux) の購入に際してキー入力で接続先を変えられる HYBRID がほしい
2. よく妻にうるさいと言われる 😇 ので静音性の高い Type-S がほしい
3. 無刻印/雪 の見た目が良すぎる... しかも白いキーボードながら ABS樹脂 の採用により黄ばみに強いらしい
4. あわよくば打鍵感とタイピング速度が向上してほしい
5. BT ではできなかった有線接続が可能

買ってみた感想としては ↑ に上げた5つのモチベーションがすべて満たされ、最高の買い物をしたと思います。
1. Fn+ctrl+数字 で簡単に切り替え可能!
2. めちゃくちゃ静かになった!部屋のドアを開けていても妻に何も言われなくなった
3. 実物は更に写真に勝ってた...愛着が湧く
4. 打鍵感が想像より気持ち良かった。BT より軽くタイピングできる感覚があってタイピング速度も上がった
5. 実はまだ試してない...。けど恐らく可能なので PC 初期設定用に有線キーボードを別持ちしなくて良いし、電池がなくなった際の非常用に良い

最初少し困ったのは BT の時できていたコマンドが効かなくなったことでしたが、これは win mode や mac mode の切り替えを DIP スイッチで行えば解決しました。ちなみにキー入力でもモード切り替えできます (ex. Fn+ctrl+(W|M) → (win|mac))。

アームレスト

折角なら周辺環境を整えようとアームレストを買いました。(HHKB で今まで使っていなかったというと同僚に驚かれた...)

私は元々 macbook のキーボードの上に HHKB を置くいわゆる『尊師スタイル』でタイピングしていたので、公式の セパレート型ウッドパームレスト(ウォールナット)を買いました。購入後しばらく試していると、たしかにタイピング速度は上がるし腕は楽になるのですが、元々 macbook の厚さ分高さがあったところにさらにアームレストで高さが増すので流石に肩こりがキツいと感じるようになりました。

尊師スタイル + セパレート型

そこで、思い切って magic trackpad を手に入れクラムシェルモードで作業をするようにし、そうなるとセパレートである必要もなくなったので非セパレートのアームレストを新たに購入しました。公式のアームレストは言ってしまえば木の板なのにあまりにも高すぎるので、今回はメルカリで個人の方が作成しているものを購入しました。届いたものは十分クオリティが高く、肩こりも減りタイピング速度も早くなりました。

しかし、更にしばらく試していると若干アームレストの高さが足りない気がしてきました。公式のものもそうなのですが、HHKB 用のアームレストは通常高さ 18mm ほどで作られていることが多い気がしています。一方で私が手元で確認したところ、24mm ほどがベストだという結論に至りました。そこでまたメルカリを探し回り、オーダーメイドでアームレストを作ってくれる人を見つけたので 24mm のものを作ってもらいました。値段も公式の半額程度で、送られてきたものは今度こそ完璧でした。今ではこの高さ 24mm ものを愛用しています。

最終形態

おわり

リーダブルコードを読んだ際のメモ

概要

リーダブルコードを読んだので、有用だと思った知見をほぼ自分用にメモとして残しておく。自分は研究時代から考えると6年コードを書いているので割と自分もそうしてるな〜と思うところが多かったが、メモしておいても良さそうなものを書き残す。

2章 名前に情報を詰め込む

ループイテレータ

↓ みたいな感じのコードはよく見るが、これだと間違った参照 (ex. members[j]) が起こり得る。

for i in len(members):
    for j in len(clubs):
        print(f'{members[i]} / {clubs[j]}')

これは ↓ みたいな感じで index に意味をもたせることでリスク軽減可能

for member_i in range(len(members)):
    for club_i in range(len(clubs)):
        print(f'{members[member_i]} / {clubs[club_i]}')

長ったらしいなら ↓ も良い。

for mi in range(len(members)):
    for ci in range(len(clubs)):
        print(f'{members[mi]} / {clubs[ci]}')

変数の命名

結論、その変数が表す概念をその変数が関わるスコープで表すのに十分かつ最小な長さまで伸ばせば良い。長い命名 != 悪。必要なら (エディタの保管機能もあるし近年はディスプレイの画質も上がってきてるし) 伸ばせば良い。スコープの大きな変数であれば有るほど長い名前をつけると良い。

名前のフォーマットを決めると良い

class 名は CamelCase, 変数名は lower_separated, メンバ変数は後に _ をつける (ex. member_) 等。プロジェクトでこういう規約を作っておくと良さそう。

3章 誤解されない名前

動作が誤解される / 複数思いつく命名は避ける

  • filter → select or exclude
    • some_vec.filter("year <= 2021") だと 2021 以下が残るのか除外されるのかが分かり辛い
    • select (=残る) か exclude (=除外する) が良い
  • clip → remove or truncate
    • clip(text, length) だと後ろ length 文字を削除するのか length 文字になるように削除するのか分かり辛い
    • remove (=最後から length 文字削除) か truncate (=length 文字になるように切り詰める) が良い
    • length も max_length という書き方ができたり、単位が曖昧なのでこれに関する工夫が可能 (ex. max_chars, max_bytes 等)
  • bool 型 → is/has/can/should 等を付与
    • 否定形 (ex. disable_ssl = false) でなく肯定形 (ex. use_ssl = true) を使う (これ何度か問題になってるの見たことあるな...)
  • get*
    • get は常識的にはメンバ変数を返すだけの軽量な処理とみなされるらしい。なので、get 関数で重い処理をするのは NG らしい (本当か?)
    • 重い処理を書くなら、get → compute 等の方が良い。
    • 普段 get* で重い処理書いちゃってるので気をつけないと...。

6章 コメントは正確で完結に

入出力のコーナーケースに実例を使う

単にコメントに実例を書くとわかりやすいということ。sklearn の docstring とか見てても書いてあってわかりやすいよね。↓ みたいな感じ。

# 入力をソートする ex. [2, 1, 10, 9, 3] → [1, 2, 3, 9, 10]
def sort_values(inputs):
    ....
    return sorted_values

7章 制御フローを読みやすくする

条件式の引数の並び順

より『変化する』値を左に、より『安定的』な値を右に書く。例えば「もし君が 18 歳以上ならば」の方が「もし 18 年が君の年齢以下ならば」より理解しやすい。

13章 短いコードを書く

コードを小さく保つ

プロジェクトの巨大化に際し、コードを小さく・軽量に維持する保つコツ

  • util 系のコードを積極的に書く・利用する
  • 不要なコード・機能は積極的に削除する (そこに費やした時間が無に帰するように見えて心苦しいかもしれないが)
  • プロジェクトをサブプロジェクトに分割する
  • コードの『重量』を意識する。軽量で機敏にしておく。

14章 テストと読みやすさ

テストを読みやすくする

  • テスト関数の中の処理についても、できるだけ重複処理や冗長な処理をを関数化してより人間の思考でたどりやすくする。
    • 普段自分が書くテストでは結構テストデータとその加工を直書きしちゃったりしてるので気をつけよう...。
  • 関数が行っていることを極力分割して解釈し、2つ以上ある場合はそれぞれ個別にテスト (+できればどちらもをカバーしたものも) する。
  • テストしやすいように開発すると、より良いコードがかけるようになる。
    • ただし、テストのためにテスト対象のコードの可読性や性能を犠牲にするのはやりすぎ。

以上!

kaggle tweet コンペの闇と光 (コンペ概要と上位解法)

概要

先日 to be twitter masters というチーム名で Tweet Sentiment Extraction コンペ (以下 tweet コンペ) に参加したので、まとめに記事を書いておきます。チームメンバーは筆者@fuz_qwa @Kenmatsu4 @tkm2261 @yiemon773 の 5 人で、結果は 5 位となり見事金メダルを獲得できました。

本記事では上位解法の紹介と、これを理解するために必要なコンペ概要の紹介を主眼とします。また、チームでやったことはもう少し軽めの記事として後日まとめようと思っています。

いつも文字数多すぎてごちゃごちゃするので、できるだけ完結に書きたい...。情報足りないところは回答できるので、気軽に質問ください~~

コンペ概要

データ概要

tweet コンペでは下記のような train 27,481 行、public test 3,435 行のデータが与えられます。

f:id:guchio3:20200619235138p:plain:w500
データ概要

このコンペでは text と、その text が {positive, negative, neutral} の内どの感情に当てはまるかを表す sentiment から、text 中のどの部分を根拠に sentiment を選んだかを表す selected_text を予測します。例えば下図のように positive な text として I'd love that. And, don't think of it as easy. Think of it as enthusiastic. が与えられた時、最も良い予測は selected_text である Think of it as enthusiastic. ということになります。

f:id:guchio3:20200620003450p:plain
データの具体例

ちなみに、このコンペのデータは selected_text が存在しない既存のデータセットに対して kaggle がアノテーションしたもの *1 で、この既存のデータと train として与えられたデータの text が若干違います (つまり kaggle によって若干加工されて渡されている)。この元データには private test data にあたるものも入っているのですが、なんと普通にアクセス可能です *2。元データには selected_text は存在しないが text は存在するため、private test data の分析やこれを使った pseudo labeling も可能でした。

また、実はさらにこの元データは本当の元データ、つまり元の tweet からも若干加工されたものになっています。元の tweet をいくつかみてみると、大半が顔文字のはいったものになっており、少なくとも多くのケースでこの顔文字が除去されているようです。

f:id:guchio3:20200620004744p:plain
元textと元textの元textを追加した例

評価指標

評価指標は Jaccard score です。要はどれだけ過不足なく正解の selected_text を予想できたかという値になります。

baseline 的戦略

upvate が非常に多かった Chris の notebookの戦略を例に上げます。

preprocess

空白が続く等のノイズを除去するため、normalized_text = " "+" ".join(text.split()) の処理をします。先頭に空白を付与しているのは、多くの kernel で使われた RoBERTa が word の先頭に空白があるか否かを区別するためです。

また、sentiment を文字として text に追加します。NLP 系のモデリングメタデータを扱う際にはよく使う方法ですね。

加えて、char level で selected text に入るか否かを判別し、tokenizer で区切られた token 内の char が 1 つでも selected_text に入っていたらその token を選ぶようにラベル付けします。これは tokenizer の token 分割の仕方とアノテーションの区切りにズレが有るためで、このやり方の他にもいくつもの工夫が可能です。

f:id:guchio3:20200620011350p:plain
Chris の notebook の前処理

model

RoBERTa を使います。何故か他のコンペとは異なり、(少なくとも huggingface の transformers を使った場合は) BERT より RoBERTa の方がうまくワークするコンペでした。個人的には Tokenizer の違い (RoBERTa は ByteLevelBPETokenizer) かなぁと思っていますが、ちゃんと検証したわけではないです。

また、RoBERTa に付与する head には若干気をつける必要があり、系列方向の構造を保てる形にしないと経験上スコアが大きく下がります。つまり、例えば入力が 128 tokens で RoBERTa の最終層が (768 * 128) だった場合、これを (768) に圧縮すると良くなく、(128) に圧縮するほうが良いです。この kernel では kernel size 1 の 1d CNN で (768 * 128) -> (128) と加工しています。今回のコンペでは系列方向のどこが selected_text に当たるかを予測するコンペなので、この構造を保つことが重要だったのだと思います。

f:id:guchio3:20200620010724j:plain
chris の notebook の RoBERTa

objective

selected_text の開始 index と終了 index をそれぞれ softmax 関数をかけて予測する形を取ります。この他にも segmentation として解く方法や NER (Named Entity Recognition) として解く方法が議論されていました *3 が、少なくとも自分の経験上は結局開始・終了 index を予測する形が最もスコアが良く出ました。selected_text が必ず 1 つしかなく、また連続した領域だったため、この構造を自然に表現できる開始・終了 index の予測という形がハマったのだと解釈しています。

ちなみに、このやり方だと開始 index が終了 index よりあとになってしまうというありえない状況がモデルの予測結果となる可能性もありますが、これは postprocess で対応します。この Chris の notebook ではこの場合『text 全体を予測結果とする』としていましたが、例えば『自信度の低い方を 0 埋めして再判定する』等、色々と工夫が可能なポイントです。

ラベルノイズ

このコンペでは主に 2 つの観点に起因して正解ラベルにノイズがあり、これをどうハンドルするかが (虚無感ありますが) 勝つために最も重要なポイントです。

1 つ目が HTML codes に起因するアノテーションズレです。要は、kaggle が外部サービスにアノテーションを依頼した際、selected_text の index という形式でラベルを取得していたようなのですが、アノテーション時と kaggle が selected_text を作成するときで HTML codes を生のコードで扱うか記号として扱うかに整合性がなく、この差分だけ index がズレてしまったというものです。この現象は discussion で議論されており、某運営によりコンペ開催後に対応されました (leader board も update された)

f:id:guchio3:20200620014901p:plain
HTML codes に起因するアノテーションズレに関する discussion

2 つ目のノイズが (恐らく) 空白数に起因するアノテーションズレです。これは magic として discussion でも暗に議論されており、運営による対応もなかったため今回上位に入るか否かはこれを適切に扱えたか否かに大きく依存します。例えば下図のようなものがこれに当たります。この例は negative なもので onna が selected_text になっていますが、text を見る限り明らかに正解は miss です。これは実は text に大量の空白が含まれているためで、恐らく運営はアノテーション依頼時に複数の空白を一つに置き換えて依頼し、なぜか selected_text を抽出するときには元の空白数のまま text に対してアノテーションされた index を当てはめたと考えれば辻褄が合います。この前提に立ち、モデルの学習時には空白数分ずらした正解ラベルを使い、推論後に今度は逆方向に空白数分ずらすことで、精度良く学習した後アノテーションズレのある test データに fit させに行くことができます。

f:id:guchio3:20200619233334p:plain
アノテーションズレの例

ちなみに、我々のチームでは空白数分ずらすことはもちろんしたのですがそれだけではデータがきれいになりきらず、恐らく他にもなにかに起因して更にアノテーションズレがあった (単に元のアノテーションがおかしかった可能性もあるが) のではないかと予想しています。

上位解法

(虚無感ありますが) 全ての上位解法で最もスコアに寄与しているのはラベルノイズをどの様に加味するかという部分です。この扱い方は大別すると 2 種類あり、1 つ目が pre-post processing として人手で対応するやり方、2 つ目が char level の tokenizer を使ってモデルにラベルノイズごと扱わせるやり方です。これを踏まえた上で、上位解法の目に止まった部分を紹介していきます。(※ 図を各 solution から引用しています)

1st

いくつかの通常の token base のモデルによる予測を行った後、下図のようにこの予測結果を使って char level の stacking を行うというアプローチを取っています。

f:id:guchio3:20200620091912p:plain
1st の stacking pipeline

char NN としては CNN, Bi-LSTM, そして WaveNet を使っていて、最終的に private で 1st となったモデル構成は下図です。

f:id:guchio3:20200620092042p:plain
stacking のモデル具体構成

また、個々のモデルも色々と工夫が行われており、目に止まったものは下記です。

  • Custom loss (Jaccard-based Soft Labels)
    • 開始・終了 index を下図のようにしてなまらしたもの。2 乗の項は分布の smoothing のために入れられている (n 乗 (to inf) までこれを繰り返していくと jaccard = 1 の部分のみ 1 に近づいていくのでほんとに?という気がしているのでまだ理解できていない...)。
      f:id:guchio3:20200620092545p:plain
      Jaccard-based Soft Labels
    • 分布の例を図示したのが下図らしい。

      f:id:guchio3:20200620093110j:plain
      Jaccard-based Soft Labels 分布例

    • 確か類似アイデア自体は discussion で議論されていた (どの discussion 記事か忘れてしまった...) けど、自分達はうまくいかなかったので活用できててすごいと思った

  • MSD (多分 Multi-Sample Dropout)
  • Sequence Bucketing for faster training
  • Quest の解法であった leakless な pseudo-labeling
    • ちなみに我々は pseudo labeling は普通に試しても全く上手く行かなかったです...
  • Bertweet

2nd

このチームは pre-post process をマニュアルで設計してアノテーションズレに対応しています。

  • pre-postprocess
  • Quest の 1st の解法にある、学習可能な weight をつかって RoBERTa から hidden layer を抽出
  • Multi-Sample Dropout
  • Sentiment Sampler。sentiment 毎に精度がかなり違うため batch 間の sentiment のバランスを調整
    f:id:guchio3:20200620094359p:plain
    SentimentSampler の効果
  • SWA
  • Reranking-model training
    • 一度開始・終了 index を予測した後、予測結果の top-n から最良のものを選ぶモデルを再度作る
      1. top-n を予測し、それぞれについて selected_text との jaccard を計算
        1. の予測結果を元に 1. で計算した jaccard を回帰で求めるモデルを作成
        1. のスコア * 0.5 + 2. の jaccard を指標に、最良の組み合わせを選ぶ
    • モデルが 50 以上できるため、推論高速化のために Sequence Bucketing を使用

3rd

  • char level での予測をする Dieter のモデルと token level での予測をする Khoi のモデルが有り、空白が服う数個ある場合は Dieter のモデルを、そうでないときはアンサンブルを行うという方策。
  • Khoi のモデルは 学習は通常の開始・終了 index の予測のフレームワークで行い、推論時に開始・終了 index をそれぞれ top-3 つずつ (3 * 3 = 9 pairs) 使う。その後、start prob * end prob で最も高い確率のペアを選択。
  • Dieter のモデルは BERT 属モデルの Head に char level RNN を適用し、アノテーションズレを含めて e2e で学習。
    f:id:guchio3:20200620103630j:plain
    Dieter のモデル

4th

  • pre-postprocessing
  • model に 4 つの head を用意し、マルチタスク学習
    1. 1st と似ているが、開始・終了 index のラベルを 0.9、その両隣を 0.05 として KL-divergence loss で学習。文の先頭の場合は 0.9, 0.06, 0.04 とし、逆もまた然り。
    2. segmentation
    3. sentiment によって意味付けされた semantic segmentation
    4. sentiment を当てる (リークしてる...?よくわかっていない)
  • SWA
  • self-distillation に期待していたが、実装ミスかうまく行かなかったらしい
  • 1~3 番目の head の出力 + 4 つの head を持つモデルの出力した top-3 の候補それぞれが正解か否かを当てる external scorer の出力の系 4 つを weighted sum して予測を行う。

5th

これは我々なので詳しくは後日書きますが、概要は以下です。

  • pre-postprocessing
  • 開始・終了 index と segmentation のマルチタスクを行い、segmentation 側の loss 比率を比較的大きく (ex. 0.5 : 0.5 : 2 等) 設定
  • segmentation 用の出力を CuMax 関数で算出
  • segmentation 用の出力をゲートとして開始・終了 index 算出用の logits にかけるモデルも作成
  • batch size をかなり大きめに設定 (ex. batch accum 込で 32 * 16, SentimentSampler 的な効果...?)

11th

  • Psi はこの solo gold で GM に...!
  • merges.txt を修正することで、RobertaToeknizer が明示的に .! を分けて扱えるようにした (ref)
    • これは自分も軽く試したけどやり方わからなかったので復習しておきたい...
  • 開始 index を予測した後、これに依存させる形で終了 index を予測させる
    • これも自分は試したけどワークしなかった...
  • neutral は 1 epoch 目のみ使い、その後は positive, negative のみで学習。neutral 用の学習は別途行う。
    • 最初から neutral を除去するのは試したけどうまく行かなかった... こうすればよかったのか...

12th

  • 開始・終了 index のペアを適切に選択するため、torch.einsum を使ってペアとしてモデリングしてしまう。

13th

  • cutout data augmentation のアナロジーとして、入力の一部を [mask] に置き換える操作を行った。

まとめ

なにはともあれ金メダルをとれてよかった...うれしい...。個人的には magic 系のコンペは虚無感あるしわかるまでは辛いですが、得るものがまったくないかと言うとそうでもなくて、magic を見つけたり利用する分析力は実務でもかなり役に立つものだと思っています。それ以外の部分でもかなり学びのある solution があったので、これらを吸収して次は solo gold 取れるように頑張りたいと思います。

質問・ご指摘等あれば気軽に投げて頂ければと思いま す。あと、6/20 に tkm san youtube live に出るのでもしよかったらそちらも見て頂ければと...!

【rustdef】Rust on Jupyter Notebook で各種統計分布を生成する

はじめに

どうも、最近 Rust を勉強し始めた ぐちお@ihcgT_Ykchi です。

Rust を勉強しだしたのは、huggingface の tokenizer のように、python でコードを書く際にピンポイントで高速化できると良さそうと思ったのが一つですが、正直なところ単に春だし新しい言語勉強するか〜って気持ちになったのが大きいです。

ところで、ちょうど最近同期の @cruelturtle が rust を jupyter notebook で使える rustdef というツールを作ったようで、いい機会なので簡単に記事を書いてみようと思いました。

ちなみに私は Rust を勉強し出してまだ 1 week 程なのでかなり筋の悪い書き方をするかもしれないですが、ご容赦下さい。。

rustdef を使う準備

なんと pip install rustdef だけで ok です。ちなみに、(おそらく) Rust がインストールされている必要があって、また nightly な version が必要です。

その後 Jupyter notebook を起動し、%load_ext rustdef を実行したら準備完了です。あとは下記例の様に %%rustdef と記述したセル内で関数を記述すれば python で呼び出し可能な関数を作ってくれます。(下記例では '4' という出力が得られます。)

# =================================
# cell 1
# =================================

%load_ext rustdef

# =================================
# cell 2
# =================================

%%rustdef

#[pyfunction]
fn sum_str(a: usize, b: usize) -> String {
    (a + b).to_string()
}

# =================================
# cell 3
# =================================

sum_str(1, 3)

ちなみに、dependencies を追加したい場合は %rustdef depends CRATE というセルを実行すればよいです。

各種統計分布を生成してみる

以下、Rust を使って実際に各種統計分布を生成してきます。具体的には 一様分布二項分布指数分布正規分布 を生成していきます。

ちなみに、各種分布の生成にあたっては統計学の赤い本を参照しました。

統計学入門 (基礎統計学Ⅰ)

統計学入門 (基礎統計学Ⅰ)

  • 発売日: 1991/07/09
  • メディア: 単行本

一様分布

まず線形合同法を使って疑似的な一様乱数を生成し、これにより [0, 1] の一様分布を可視化します。

線形合同法には色々と問題があるので本来はメルセンヌ・ツイスタ使ったりした方が良いのでしょうが、まぁ良い乱数生成はこの記事のスコープ外としてとりあえず知ってるものを使いました。

# =================================
# cell 1
# =================================

%%rustdef

#[pyfunction]
fn gen_unif_rands_lgc(seed: i64, a: i64, c: i64, m: i64, sample_num: i64) -> Vec<f64>{
    let mut res: Vec<f64> = Vec::new();

    let mut x = seed;
    for _i in 0..sample_num {
        x = (a * x + c) % m;
        res.push(x as f64 / m as f64);
    }
    res
}

# =================================
# cell2
# =================================

%%timeit
res = gen_unif_rands_lgc(seed=71, a=111112, c=10, m=999999, sample_num=100_000_000)

timeit の結果は 5.88 s ± 442 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) となり、100,000,000 個の一様乱数生成に約 6s かかっている事がわかります。(多分頑張ればもっと高速化できると思います。)

ちなみに、python で同じ処理を書くと下記の様になり、22.1 s ± 400 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) とやはり rust で書いたほうが高速だったことがわかります。

# =================================
# cell 1
# =================================

def gen_unif_rands_lgc_py(seed, a, c, m, sample_num):
    res = []
    x = seed
    for i in range(sample_num):
        x = (a * x + c) % m
        res.append(x / m)
    return res

# =================================
# cell2
# =================================

%%timeit
res_py = gen_unif_rands_lgc_py(seed=71, a=111112, c=10, m=999999, sample_num=100_000_000)

また、分布は下記のようになり、期待通りの分布が得られていることが分かります。

from scipy.stats import uniform

plt.figure(figsize=(8, 7))

# pdf をプロット
x = np.arange(0, 1, 0.001)
y = uniform.pdf(x)
plt.plot(x, y, label='pdf', color='yellow', linewidth=5.0, alpha=0.5)

# 自作関数のヒストグラムを作成
unif_rands = gen_unif_rands_lgc(seed=71, a=111112, c=10, m=999999, sample_num=10_000_000)
plt.hist(unif_rands, bins=20, color='grey', alpha=0.5, normed=True, rwidth=.8)

plt.title('一様分布')
plt.legend()
plt.grid(None)
plt.show()

f:id:guchio3:20200329014456p:plain
一様分布の可視化

二項分布

次に、一様乱数を元に二項分布を生成します。0 ~ 1 の一様乱数を n 個生成し、これらの内 p より小さいものの個数を数えることで二項分布に従う確率変数を 1 つ観測する、というシミュレーションを繰り返すことでこれを実現します。

ここで、本当は上で作った gen_unif_rands_lgc 関数を使って一様乱数生成したかったのですが、実装が悪いのか乱数の質が悪いのか意図通りの二項分布が生成できなかったため、Rust の rand crate を使って乱数生成することにしました。 以降の指数分布、正規分布でも同様に rand crate の乱数生成器を使用します。

# =================================
# cell 1
# =================================

%rustdef depends rand

# =================================
# cell 2
# =================================

%%rustdef

use rand;

#[pyfunction]
fn gen_binomial_rands(p: f64, n: i64, sample_num: i64) -> Vec<i64> {
    let mut res: Vec<i64> = Vec::new();

    for _i in 0..sample_num {
        let mut unif_rands: Vec<f64> = Vec::new();
        for _i in 0..n {unif_rands.push(rand::random::<f64>());}
        let sample = unif_rands
            .into_iter()
            .map(|x: f64| -> i64 { (x < p) as i64 })
            .into_iter()
            .sum();
        res.push(sample);
    }
    res
}

# =================================
# cell3
# =================================

from scipy.stats import binom

plt.figure(figsize=(8, 7))

# pmf をプロット
x = range(11)
y = binom.pmf(x, 10, 0.2)
plt.plot(x, y, label='pmf', color='orange', linewidth=5.0, alpha=0.5)

# 自作関数のヒストグラムを作成
binomial_rands = gen_binomial_rands(p=0.2, n=10, sample_num=10_000_000)
weights = np.ones(len(binomial_rands))/float(len(binomial_rands))
plt.hist(binomial_rands, bins=20, color='grey', alpha=0.5, weights=weights, rwidth=.8)

plt.title('二項分布')
plt.legend()
plt.grid(None)
plt.show()

f:id:guchio3:20200329084832p:plain
二項分布の可視化

指数分布

続いて、指数分布を生成します。指数分布は確率が 0 でない定義域が広く一見生成が難しそうにみえますが、逆変換方という手法により生成できます。

逆変換法は、累積分布関数  F に従う確率変数  X を [0, 1] の一様乱数  U から  X = F^{-1}(U) で求める手法で、指数分布の場合  X = -log(U/\lambda) とかけます。ちなみに、 X = F^{-1}(U) とした時、 X F を累積分布関数とする確率変数となることの証明は下記のようにかけます。

\displaystyle{
    \begin{align}
        P(X \leq x) &= P(F^{-1}(U) \leq x) \\
                           &= P(U \leq F(x))\qquad  (\because P(U \leq u) = u \quad (0 \leq u \leq 1)) \\
                           &= F(x)
    \end{align}
}

コードとしては下記のようになり、可視化結果から期待通りの分布が得られていることが分かります。

# =================================
# cell 1
# =================================

%rustdef depends rand

# =================================
# cell 2
# =================================

%%rustdef

use rand;

#[pyfunction]
fn gen_exponential_rands(lambda: f64, sample_num: i64) -> Vec<f64> {
    let mut res: Vec<f64> = Vec::new();

    for _i in 0..sample_num {
        let unif_rand = rand::random::<f64>();
        let sample = -1. / lambda * (1. - unif_rand).ln();
        res.push(sample);
    }

    res
}

# =================================
# cell3
# =================================

from scipy.stats import expon

plt.figure(figsize=(8, 7))

# pdf をプロット
x = np.arange(0, 20, 0.1)
y = expon.pdf(x)
plt.plot(x, y, label='pdf', color='orange', linewidth=5.0, alpha=0.5)

# 自作関数のヒストグラムを作成
exponential_rands = gen_exponential_rands(1, sample_num=10_000_000)
plt.hist(exponential_rands, bins=20, color='grey', alpha=0.5, normed=True, rwidth=.8)

plt.title('指数分布')
plt.legend()
plt.grid(None)
plt.show()

f:id:guchio3:20200329124559p:plain
指数分布の可視化

正規分布

最後に、正規分布を生成します。ここでは、二項分布の n に大きい値を取り、中心極限定理により正規分布を作成するという方針を取ります。

ちなみに、rustdef ではセルをまたいだ関数の流用が今現在 (2020/3/29) はできないらしく、ここでは二項分布の生成関数を再度書いています。

# =================================
# cell 1
# =================================

%rustdef depends rand

# =================================
# cell 2
# =================================

%%rustdef

use rand;

#[pyfunction]
fn gen_binomial_rands(p: f64, n: i64, sample_num: i64) -> Vec<i64> {
    let mut res: Vec<i64> = Vec::new();

    for _i in 0..sample_num {
        let mut unif_rands: Vec<f64> = Vec::new();
        for _i in 0..n {unif_rands.push(rand::random::<f64>());}
        let sample = unif_rands
            .into_iter()
            .map(|x: f64| -> i64 { (x < p) as i64 })
            .into_iter()
            .sum();
        res.push(sample);
    }
    res
}

#[pyfunction]
fn gen_normal_rands(mu: f64, sigma: f64, sample_num: i64) -> Vec<f64> {
    let p: f64 = 0.5;
    let n: i64 = 10000;
    let binomial_rands = gen_binomial_rands(p, n, sample_num);
    let bi_mu = (n as f64) * p;
    let bi_sigma = ((n as f64) * p * (1. - p)).powf(0.5);
    let res = binomial_rands
        .into_iter()
        .map(|x: i64| -> f64 {((x as f64) - bi_mu) / bi_sigma})
        .into_iter()
        .map(|x| (x * sigma) + mu)
        .collect::<Vec<f64>>();
    res
}

# =================================
# cell3
# =================================

from scipy.stats import norm

plt.figure(figsize=(8, 7))

# pdf をプロット
x = np.arange(-5, 15, 0.1)
y = norm.pdf(x, 5, 3)
plt.plot(x, y, label='pdf', color='orange', linewidth=5.0, alpha=0.5)

# 自作関数のヒストグラムを作成
normal_rands = gen_normal_rands(5, 3, sample_num=1_000_000)
plt.hist(normal_rands, bins=20, color='grey', alpha=0.5, normed=True, rwidth=.8)

plt.title('正規分布')
plt.legend()
plt.grid(None)
plt.show()

f:id:guchio3:20200329125045p:plain
正規分布の可視化

おわりに

rustdef を使うと、python ベースの分析時にピンポイントで Rust を使って高速化するという Rust の使い方が捗りそうで良いなぁと思いました。セル内だと linter がないので (うまくやればできる?)、今回は一度エディタで関数を書いてからセルに移植するという方策をとりましたが、セルに書くくらいの簡単な関数であればそのうち linter なしで書ける様になると思うので問題ないかなぁと思います。

記事を書くのに思ったより時間がかかってしまった...。