nikkie-ftnextの日記

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

2020年振り返りと2021年の目標(データサイエンティストとしてのnikkie編)

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
休暇最終日(1/4)を迎え、「振り返りをしていなかったな」とこの記事を書いています。
業務とプライベートの活動とで分けて書くことにしました。
今回は業務編です。

目次

nikkie as データサイエンティスト

2019年にユーザベースに転職し、データサイエンティストとして自然言語処理に携わっています。

「データサイエンティスト」は会社によって職務内容が異なると思うのですが、私はチームの一員として以下に取り組んでいます:

  • データ収集〜モデル訓練のパイプライン実装
  • モデルをAPIとしてデプロイ
  • モデル訓練

上2つがメイン1で、モデル訓練(Kaggleのコンペのようにいいモデルを作る)は少しずつ取り組む時間が増えてきました。

2020年🐭 業務で触った技術

自然言語処理

2020年1月〜3月まで、週1でブログを書き自然言語処理のキャッチアップに取り組みました(以下から始まります)。

『入門 自然言語処理』写経を中心に、ときには興味に任せてBERTなど気になる題材で手を動かした3ヶ月。
OOCのスタッフをした週など、書き上げるのが大変な週もあったのですが、3ヶ月やりきった達成感は気持ちよかったです。

『入門 自然言語処理』のおかげで全体像は大まかにつかめたと思っていますが、まだまだキャッチアップすべきところは広がっています。

パイプライン実装(データ収集)

パイプライン実装の中では、MySQLデータベースやGoogleスプレッドシートからデータを取得する処理の実装を経験しました。
スプレッドシートの扱いは、PyCon JP スタッフの自動化(カスタムリマインダーなど)で手を動かした経験がリンクした感じです。

モデル訓練

PyTorchを触るようになりました。

自動化やデータ取得処理を実装するときと比べると、モデル訓練スクリプトが動くようになるまでに、実装やデバッグで時間がかかるのが直近の悩みの種です。
このあたりの経験値が積めるよう、PyTorchのチュートリアルやKaggleへの取り組みなどを考えています。

API実装・デプロイ

以下の技術に触れました。

Kubernetesは4月時点では素振りを試みてハマっていましたが

業務中のペアプロなどを通して、kubectlコマンドの使い方を盗み、
9月にはminikubeでArgoを動かすなど、この1年でできるようになったことが多いと感じています。

MLOpsへの興味

2020年で一番影響を与えた文言が以下:

To make great products:

do machine learning like the great engineer you are, not like the great machine learning expert you aren’t.

「すごいエンジニアのように機械学習をやりなさい」、この言葉はサーバーサイドエンジニアを出自とする私の琴線に触れました。

2019年から仕事でPythonを書き始め、開発の仕方や設計など、エンジニアとして数々の伸びしろに直面した私は、データサイエンスの領域よりもエンジニアとしての力を付けることを優先しました(特に2019年)。
TDDには慣れましたが、設計は依然伸びしろ豊富、そんな状態でこの言葉を見て、「学んできたことが機械学習に活かせるし、両輪で歩めるんだ」と、興味関心がデータサイエンス側に振れました。
まだまだ知らないことばかりですが、エンジニアの考え方はデータサイエンスにも活かせると信じて取り組んでいきます。

MLOps関連のトピックでは、以下などで素振りしています:

アジャイル

いくつかの本でインプットしました。

Uncle Bobの『Clean Agile』がなかなか興味深かったです。

業務での開発はXPを実践していますが、「あのプラクティスはこういう考えで採用されているのか」と経験が理論付けされていきました。

もう1つ、社内勉強会で引用された「アジャイルは青春」というフレーズがじわじわ来ています。
私にとっての「青春」が想起するものは、アニメに対する「こんな青春送りたかった」というフレーズなのですが、
主人公たちが青春真っ只中にいる(青春するのではなくて青春である)ように、
アジャイルもするんじゃなくて、アジャイルであるものなんだなと思います2

リモートワーク

そうそう、3月くらいからリモートワークしています。

得たもの

  • 8時間程度働いたあと、シームレスにプライベートの開発やPyCon JPスタッフ活動に移行する生活
  • (5月以来)GitHubに毎日Contributeする習慣

失ったもの

  • 3ヶ月に1回のペースで体調を崩していたのがなくなった
  • 筋肉(体重)
  • 遠くを見なくなったせいか視力は落ちている気がします。。

2021年🐮 目標

2020年は「US PyConにプロポーザルを通す」というアホな目標を掲げた結果、海外での登壇を達成できた年だったのかなと考えています。
US PyConにこだわらず、海外PyConにプロポーザルを出し(まくり)、オンライン開催という変化と相まって、ムーンショット的に機能しました。

2021年もアホな目標ということで、「Oさんを倒す」という目標を掲げました!
Oさんは、今の職場に誘ってくださった凄腕データサイエンティストです。
倒すの定義は、私が訓練したモデルのスコアで、Oさんの作ったモデルのスコアを超えることです。

私とOさんの経験や技術力を比べると天と地の開きがあります(なので、この目標は相当アホだと思います)。
そして、先に述べたように、現状の私は独力でのモデルの訓練に課題感を抱えています。

目標の「倒す」にフォーカスするというよりも、取り組みや考え方を根本から変えて、達成できるラインが見えるようになることを狙っています。
なお、現実的ではなかったと痛感したら、意気消沈しても仕方ないのでこの目標は修正します。

2021年はPyCon JP座長など、これまでの年よりも「緊急かつ重要」なタスクを抱える年になる見込みです。
この記事で掲げた目標は「緊急ではないけれど重要」な目標です。
緊急なタスクに追われるだけでなく、データサイエンティストとして今よりももっと価値を出していきたいので、2021年やってみます!

P.S.

もしこの記事を読んで、データサイエンティスト職に興味を持たれた方は、お気軽にお声がけください。


  1. 会社によっては「データエンジニア」という呼び方が当てはまるのかもしれません。ただ機械学習モデルも作ります。データサイエンティストは名前空間が重要そうですね

  2. The Art of Agile Development, Second Editionのレビュー原稿にも「アジャイルをするのではなく、アジャイルである(アジャイルの考え方を身につける)」といった内容が書かれていました。アジャイルを"する"とは、考え方は変えずにアジャイルなメソッドだけを取り入れるという理解です

『Clean Architectures in Python』を写経しました(1st Edition, Part 2 - Chapter 2)

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
新年明けましておめでとうございます🎍🐮
ブログの書き初めは、冬の休暇に手を動かした、PythonでClean Architecture本についてです。

目次

Clean Architectureと私

ソフトウェアはやっぱりソフトに(=変更しやすいように)作りたいなと私は強く思います。
2020年はClean Architectureについてたびたびインプットしました。
例えばnrslibさんのYouTube配信です。

こういったインプットを経て、「Clean Architecture、概念の理解は深まったと思うけれど、Pythonで具体的にどうやればいいんだろう?」という課題感を感じていました。
層の概念はいわゆる"完全に理解した"けれど、それをPythonのコードに落とし込んだらどうなるのかが分からなかったわけです。

7月のEuroPythonで『Clean Architectures in Python』という本を知りました1

これは積ん読になっていたのですが、ヴァケイションには「緊急なことじゃなくて、自分にとって重要なことをやろう」と写経してみました。
学んだことをここにアウトプットします。

『Clean Architectures in Python』(1st edition)

この本では、物件情報を一覧にするWeb APIを例にClean Architectureが解説されます。

本は2つのPartに分かれます:

  • 前半(Part 1)はTDD導入パート
  • 後半(Part 2)は物件情報一覧API(面積や賃料、緯度経度)を実装
    • TDDで進める(まずテストコードが出てくる)
    • Flask

nikkieのレベル感は以下です:

  • ふだんはunittestでTDD。pytestのコードも雰囲気で読めそうなのでPart 1はskip
  • FlaskHello Worldから先はあまり踏み込めておらず。この本では blueprint や configuration を駆使2してAPIを実装。
    • Flaskの本ではないとのことですが、ドキュメントへのリンクは充実しています。深く理解しているわけではない概念についてもキャッチアップしてついていけました

今回はClean Architectureの理解が目的なので、pytestFlaskの厳密な理解はヤクの毛刈りと考え、あまり深追いせずに進めました。

Part 2 - Chapter 2 での学び

ビジネスロジック部分をClean Architectureで作ると、UIはCLIでもWeb APIでもなんでも採用できそうという感触を得ました。
12月のPHPカンファレンスでnrslibさんが共有していた、「次のフレームワークに持っていく」ってこういう考え方なんですね!
PythonはWebフレームワークが乱立してますが、Clean Architectureで作ったビジネスロジック部分は移動しやすそうです。

そう思った2章のコードはこちら:

repo = mr.MemRepo([room1, room2, room3])
use_case = uc.RoomListUseCase(repo)
result = use_case.execute()

やっていることは

  1. インメモリなリポジトリを作り
  2. そのリポジトリを渡して部屋一覧ユースケースを作り
  3. ユースケースを実行(execute)して結果(=部屋の一覧)を得る

これだけ!
このコード以外はCLIを扱うコードでもWeb APIを扱うコードでもいいわけです。

3つの層:エンティティ/ユースケース/外部システム

この本ではClean Architectureを3つの層のアーキテクチャとして紹介しています(Part 2 - Chapter 1参考)。

3つの層は

内側:エンティティ < ユースケース < 外部システム:外側

という包含関係にあります。
これらの層の間には

Talk inwards with simple structures, talk outwards through interfaces

(意訳:内側の層に対してはPythonのデータ構造やエンティティに定義された単純なデータ構造で依存せよ。外側の層に対してはインターフェースを介して依存せよ)

という原則があります。

この本を読んで、一番腑に落ちたのは

  • ユースケースは内側にあるエンティティの実装の詳細を知っている
  • ユースケースは外側の外部システムの実装の詳細は知らない
    • ただ外部システムのAPIは知っているので、外部システムを利用できる

というところです。

先のコードで use_case.execute() を実行すると

class RoomListUseCase:
    def __init__(self, repo):
        self.repo = repo

    def execute(self):
        return self.repo.list()

ユースケース初期化時に渡されたリポジトリlistメソッドを呼び出します。
ユースケースリポジトリ(外部システム)のAPIを知っていて、listを呼び出せば物件が取得できると分かっているわけです。

ユースケースという概念

今回ユースケースという概念を知れたのが大きいと思っています。

私がプログラミングに入門したとき、クラスの入門例がDogやCat、CarやSuperCarでした。
これらの例は、現実の具体的な事物と対応しているので(私が過学習してしまって)、現実の具体的な事物と対応しないクラスは独力ではなかなか思いつきません。
処理を表すクラス(現実の具体的な事物と対応するわけではない)を作るというのを知ったときは衝撃でした。
今回のユースケースもそのときと同じくらいの衝撃です。

ユースケースを初期化する時にリポジトリを渡すというのも変更しやすさの妙だなと思います。
Chapter 2ではインメモリのリポジトリを渡していますが、これはDBに接続するリポジトリに変わるでしょう。
そうなってもユースケースからはlistというAPIを使うだけなので、リポジトリを付け替えるだけで動作確認が簡単にできますよね。
DBを準備しなくてもインメモリのリポジトリを用意すれば動作させられます(nrslibさんたちが言ってたやつだ!)

用語:モデル/リポジトリ

この本でありがたかったのは、用語の誤解に先手を打っていたこと。

エンティティはドメインモデルの表現と説明する中で、ドメインモデルはDjangoなどのフレームワークにおける"モデル"とは異なると言っています。
ドメインモデルは軽量(lightweight)なモデルで、自身をストレージに保存するメソッドや、JSON文字列としてダンプするメソッドは持ちません。

リポジトリもGitのリポジトリとは無関係です。
外部システムのストレージにアクセスするものはリポジトリと呼ばれます。
リポジトリドメインモデルを返します
先の例ではユースケースリポジトリから返ってきたドメインモデルを扱うわけです。

Clean Architecture以外での学び

JSON文字列を返すためのserializer実装

json.JSONEncoderを継承したクラスを定義し、defaultメソッドを実装、それをjson.dumpscls引数に指定しています。

pytestのdeprecation

py.testというコマンドが出てきて気になったのですが、pytestコマンドが推奨されているそうです。

pytest-flask向けにtests/conftest.pyを作ったところ、@pytest.yield_ficturesPytestDeprecationWarningが上がりました。
deprecateされていて、@pytest.fixtureでいいそうです。

終わりに

「Clean Architecture、Pythonで具体的にどうやればいいんだろう?」
冒頭の問に対して暫定的な答えは得られました。
ユースケースを使った実装を練習していきます!

層の数の違いなどUncle Bobの『Clean Architecture』とは厳密には違うのかもしれませんが、「変更しやすい設計でどう作ればいいか」かなり具体的に分かったので、今の私としては大満足です。

今回のコードはこちら

続くChapter 3では部屋を絞り込む機能を追加します。
実装する上で、リポジトリへのリクエストを表すオブジェクトとレスポンスを表すオブジェクトが登場しました。
これらの扱いも学びが多く、またの機会にアウトプット予定です!

ここまで読んで『Clean Architectures in Python』に興味を持った方、先日2nd Editionがリリースされました🎉
サンプルは変わっていないそうですが、dataclassを使っていたり、分かりやすそうな図が追加されていたりするので、復習がてら読んでみようかと思います。

そしてなんと、月末にはUncle Bobの話が聞ける機会があるので、今よりもう少し理解度を上げて参加するのを目指します。

それでは!


  1. Clean Architectures in Python — EuroPython 2020 Online · 23-26 July 2020

  2. FlaskHello Worldで入門するのは簡単ですけど、うまく使おうと思ったらblueprintやconfiguration など結構キャッチアップがいりますよね(キャッチアップの総量はDjangoとあまり変わらない感覚)

魔法みたいな機能満載のStreamlit。その中でもとびっきりの魔法、キャッシュについて

はじめに

この記事はデータ可視化 Advent Calendar 2020 21日目の記事です。

頑張れば、何かがあるって、信じてる。nikkieです。
先日参加した #pyhack でデータ可視化アドベントカレンダーに空きがあることを知り1、最近推しているStreamlit2について締切駆動でアウトプットしてみることにしました。

目次

Streamlitとは

サーバサイドのロジックからUIまでカバーした、ラピッドプロトタイピングツールです。
データ可視化アプリや機械学習モデルを使ったアプリが簡単に作れると注目を集めている印象です。

nikkie的にポイントが高いのは、Streamlitを使えば、Pythonを書くだけでブラウザを通して操作できるアプリができるという点。
チュートリアルを見て「こんなに簡単に作れるのか!」と舌を巻きました。

import pandas as pd

import streamlit as st

st.title("My first app")
df = pd.DataFrame(
    {"first column": [1, 2, 3, 4], "second column": [10, 20, 30, 40]}
)
df

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

Galleryを覗けば、いろいろなアプリが見つかります。

上記のチュートリアルのTIPで@st.cacheというデコレータが紹介されているのですが、このキャッシュ機能も、Streamlitのヤバい✨機能の1つでした!
この記事では@st.cacheについてドキュメントに沿って見ていきます。

お断り:データ可視化アドベントカレンダーではありますが・・

今回の@st.cacheのアウトプットのサンプルアプリは、データ可視化アプリではありません。
ですが、この記事で登場する@st.cacheの使い方は、Streamlitを使ったどんなアプリにも適用できると考えています。

Streamlitはデータ可視化アプリや機械学習アプリに使われることが多いと思いますが、私自身は「Pythonスクリプトを他の人に使ってもらうように変換する」のにも使えるツールととらえています。
今回は画像リサイズスクリプトを元にした便利ツールアプリを例に@st.cacheを見ていきます。

ソースコードはこちら(のディレクトリ)

開発環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.8.6
$ pip install streamlit==0.72.0 Pillow==8.0.1 pandas==1.1.4

@st.cacheとは

ドキュメントの「Improve app performance」というページに、使い方が詳しく書かれています。
Streamlitを使うなら一読して損はないと思います。

※誤読と思われる箇所に気づきましたら、コメントやTwitterでお知らせください。大変ありがたいです

かいつまんで言うと、StreamlitのキャッシュはインメモリのKVS(key-value store)です3
@st.cacheでデコレートされた関数には、ざっくり言うと、入力された引数のハッシュをキーとし4、関数の実行結果をバリューとするキャッシュが用意されます。

関数を呼び出したとき、引数(のハッシュ)がキャッシュにヒットすれば、キャッシュが使われます(関数は実行されません!)。
キャッシュミスがあった場合は関数が実行され、引数(のハッシュ)と返り値がキャッシュに登録されます。

例(ドキュメントより)

ドキュメントにあるのが以下の例(Example 1):

import time

import streamlit as st


@st.cache
def expensive_computation(a, b):
    time.sleep(2)
    return a * b


a = 2
b = 21
res = expensive_computation(a, b)

st.write("Result:", res)
  • @st.cacheが付いていないとき、ブラウザでページをリロードするたびにexpensive_computationが実行されて、表示までに毎回2秒かかります
  • @st.cacheを付けると、初回表示では2秒かかりますが、2回目以降のリロードはキャッシュが使われるので2秒待つことはなくなります

この状態から

  • expensive_computationに渡す引数bの値を変更(Example 2)
  • expensive_computationの中の実装を変更(Example 3)
  • expensive_computationから別の関数を呼ぶように変更(Example 4)
  • さらに呼び出し先の関数の実装も変更(Example 4)

とすると、いずれも(ハッシュ(すなわちKVSのキー)が変わるので)キャッシュミスが起き、関数が実行されるとドキュメントは説明しています。

ヤバい!と思ったのはExample 5。
ユーザが画面のスライダで選択した値をbとするのですが、

  1. あるタブでスライダで9を選ぶ(→キャッシュミスにより2秒待つ)
  2. 別のタブでも表示し、スライダで9を選ぶ→1で登録されたキャッシュがヒットするので、2秒待たずに画面が表示される!!

Streamlitのキャッシュはグローバルにしており、あるユーザが作ったキャッシュが他のユーザのパフォーマンス向上に寄与できる設計とのことです。

This happens because the Streamlit cache is global to all users. So everyone contributes to everyone else’s performance.

キャッシュは参照なので、ValueがMutable objectのとき、Valueを書き換えると、キャッシュのValueも書き換わります(Example 6。CachedObjectMutationWarningが出ました)。

@st.cache適用 Before / After

Before

画像5リサイズアプリでは当初以下のような実装をしていました。

uploaded_files = st.file_uploader(
    "Specify image(s) to be resized in your computer",
    type=["png", "jpg", "jpeg"],
    accept_multiple_files=True,
)
max_length = st.slider("Specify max length", 100, 500, 300, 50)
if uploaded_files:
    random_id = uuid.uuid4()
    shrinked_dir_path = Path(f"images/{random_id}")
    shrinked_dir_path.mkdir(exist_ok=True)

    for uploaded_file in uploaded_files:
        resized_image_path = shrinked_dir_path / uploaded_file.name
        has_resized = resize_image(  # 画像を縮小する処理の呼び出し
            uploaded_file, resized_image_path, max_length
        )
        if has_resized:
            st.image(str(resized_image_path))

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

この実装で気になった点は、選択した画像を1つ削除したり、スライダーのサイズ指定をいじったりすると、すでに縮小済みの画像があったとしても毎回縮小した画像が作られる点です。
ファイルが重複してできていって、リソースを無駄にしている感覚がありました。

After

@st.cacheの存在を知り、画像を縮小する処理を関数にしてからデコレートしました。

  • 入力:画像ファイル1枚、縮小サイズ
  • 処理:画像を縮小して保存する
  • 出力:縮小した画像のパス(表示するのに使う)
@st.cache
def resize_uploaded_image(image_file_object, max_length):
    random_id = uuid.uuid4()
    shrinked_dir_path = Path(f"images/{random_id}")
    shrinked_dir_path.mkdir(exist_ok=True)

    resized_image_path = shrinked_dir_path / image_file_object.name
    has_resized = resize_image(
        image_file_object, resized_image_path, max_length
    )
    if has_resized:
        return resized_image_path


uploaded_files = st.file_uploader(
    "Specify image(s) to be resized in your computer",
    type=["png", "jpg", "jpeg"],
    accept_multiple_files=True,
)
max_length = st.slider("Specify max length", 100, 500, 300, 50)
if uploaded_files:
    for uploaded_file in uploaded_files:
        resized_image_path = resize_uploaded_image(uploaded_file, max_length)
        if resized_image_path:
            st.image(str(resized_image_path))

画像ファイルと縮小サイズの組(のハッシュ)がキャッシュにヒットしたら、すでに縮小済みの画像があるということなので、それを画面に表示します。
キャッシュを使うように処理を書き換えたことで、入力を変更するたびに縮小されなくなりました!

なお、st.file_uploaderの返り値はファイル名以外にファイルのサイズ(や内容?)も見ているようで、全く異なる画像をキャッシュにあるのと同名にしてアップロードしても、キャッシュがヒットしませんでした。
すごいですね!

終わりに

Streamlitの@st.cache、味方につけるためには関数の設計をカスタマイズする必要がありますが、パフォーマンスにもたらす効果は覿面と感じています。
データ可視化にとどまらず、Pythonスクリプトをアプリにするツールとして可能性を感じているStreamlit、2021年も使い倒していく予定です。

ここまで「魔法」が揃っているStreamlit、魔法をどう実現しているか気になってきますよね。
依存関係で入ってくるtornadoがサーバのようですし、UIはReact6と聞いています。
アドベントカレンダーに手を挙げた時点ではStreamlitの中を見るというのもネタの候補にありました。
今回は間に合わず、みんなのPython勉強会の登壇準備中に知ったネタを使ったのですが、時間のある年末年始に中を見てみようと思っています。

Happy prototyping with Streamlit!


  1. この記事は12月のみんなのPython勉強会発表のプチ続編でもあります。「Streamlitヤバい!」と連呼してましたね(語彙力😅)

  2. Think of the cache as an in-memory key-value store, (冒頭(=Example 1より前))

  3. ラピッドプロトタイピングなので、関数の中身が変更されることもあるでしょう。そんな状況でも対応できるように、引数のハッシュ以外に関数のコード(The body of the function)などもキーにしています。上記ドキュメントでは「Advanced caching」の節に詳しいです

  4. 画像はPyCon JP 2019のアルバムより

  5. ref: GKEでStreamlitをホスティングして社内用アプリを作った話 - エムスリーテックブログ

2020年積み残し解消:django-allauthを使ったソーシャルアカウント(GitHub・Slack)連携、素振りの記

はじめに

この記事はDjango Advent Calendar 2020 14日目の記事です。

頑張れば、何かがあるって、信じてる。nikkieです。
2020年はPyCon JPスタッフ活動の中で、力を付けたくてDjangoアプリを作りました。
Slackアカウントでアプリにログインする機能を実装する際に、素振り不足を理由にdjango-allauthの採用を見送りました。
これがちょっとした心残りとなっており、年内に解消すべくアドベントカレンダーの締切駆動でdjango-allauthを触ることにしました。

目次

経緯:積み残したdjango-allauth

※8月のPyCon JPで話した内容1と重なります。

PyCon JPスタッフ活動の中で、レビューに使うスタッフ内部用のWebアプリ2Djangoで実装しました。
スタッフだけがログインできるように、PyCon JP Slackのアカウントを使ってログインする機能を実装しました(スライド31〜33)。
実装中はdjango-allauthも候補にあったのですが、ドキュメントを見た感じすぐに使えるイメージがつかめず、別のパッケージを使いました。
「会期が終わった後に同じ実装をするならdjango-allauthを試したい」と思っていたので、今回試しました。
今回の素振りのゴールは、簡易的なアプリでdjango-allauthを使ってSlackアカウントログインを実装することです。
「レビューアプリにdjango-allauthを導入できそうという感触を持てたらいいな」と取り組みました。

開発環境

django-allauthPython 3.8までだったので、3.8を選択しました。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.8.6
$ pip install Django==3.1.4 django-allauth==0.44.0 django-environ==0.4.5

Django Girls Tutorialに沿ったディレクトリ構成で今回の練習用アプリを実装しています。
DBはsqliteを使い、ローカルでのみ動かしました。

リポジトリはこちらです。

素振り1:『現場で使えるDjangoの教科書 実践編』に沿ってGitHubアカウントでログイン

参考にしたのは、akiyokoさんの『現場で使えるDjangoの教科書 実践編』。

django-allauthはソーシャルアカウント連携だけではなく、allと名に負う通り、ログイン周り全般も面倒を見てくれます!
※ただし、今回はソーシャルアカウント連携に絞って見ていきます。

  1. settings.py を変更
  2. GitHubでOAuth Appを作成
  3. Django AdminからSocial applicationを作成

前提

前提として、以下の状態とします(Django Girls Tutorialベースです)。

# 前提:作業ディレクトリにいる
$ python3.8 -m venv myvenv
$ . myvenv/bin/activate

# ... 上で挙げたパッケージをpip installする

$ django-admin startproject mysite .

# ... Django Girls Tutorialに沿って「設定変更」する(mysite/settings.pyを変更)

$ python manage.py migrate

作業ディレクトリの状態

.
├── db.sqlite3
├── manage.py
├── mysite
└── myvenv

settings.py 変更(URL & テンプレート追加)

django-allauthを使うために、まず mysite/settings.py を変更。

INSTALLED_APPS = [
    # : すでにあるものの下に以下を追加
    'django.contrib.sites',  # django-allauthに必要

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',
]

# ... 途中を省略 ... 以下は一番下に追加

##################
# Authentication #
##################

SITE_ID = 1

LOGIN_REDIRECT_URL = "home"
ACCOUNT_LOGOUT_REDIRECT_URL = "account_login"

django-allauthをインストールしたので、migrateが必要です。

続いて、以下の2つのファイルを用意します。

mysite/urls.py3

urlpatterns = [
    path('admin/', admin.site.urls),
    # 以下2行を追加
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('accounts/', include('allauth.urls')),
]

templates/home.html4(新規作成)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>ホーム</title>
  </head>
  <body>
    <h2>すごいアプリ</h2>
    {% if user.is_authenticated %}
      ようこそ {{ user.get_username }} さん
      <p><a href="{% url 'account_logout' %}">ログアウト</a></p>
    {% else %}
      <p><a href="{% url 'account_login' %}">ログイン</a></p>
    {% endif %}
  </body>
</html>

GitHub OAuth App作成

以下を参考にしました。

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

Authorization callback URLにはhttp://127.0.0.1:8000/accounts/github/login/callback/を設定。

Django Adminの操作

createsuperuserしてからDjango Adminにログインします。
「外部アカウント」の「Social applications」から追加していきます。

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

以上で、GitHubアカウントでログインできるようになります。

127.0.0.1:8000/(templates/home.htmlの表示)

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

「ログイン」をクリックするとログインページを表示(ビューとテンプレートはdjango-allauthが用意)
127.0.0.1:8000/accounts/login/

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

GitHubで初回ログイン(認可を求められる)※同じブラウザですでにGitHubにログインしています

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

ログイン状態で 127.0.0.1:8000/ に戻る

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

素振り2:Slackアカウントでもログイン

半年前にやりたかったSlackアカウントでのログインに挑戦してみます。

  1. Slack App作成
  2. Django Adminの操作の代わりにmysite/settings.pyに設定追加

Slack App作成

SlackはWorkspaceごとにアカウントがありますが、ログインに使いたいアカウントのあるWorkspaceでAppを作ります。

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

「OAuth & Permissions」に以下を設定しています

  • Redirect URLs
    • http://127.0.0.1:8000/accounts/slack/login/callback/5
  • Scopes > User Token Scopes
    • 未設定だとAppのインストールができないと表示されました
    • Slackのドキュメントによるとidentityが必要なようなので、identity.basicを設定

mysite/settings.pyに設定追加

Django Adminで操作しなくても、mysite/settings.pyに設定を追加してもできるようです。

INSTALLED_APPS = [
    # 省略。以下を追加
    'allauth.socialaccount.providers.slack',
]

# 省略

##################
# Authentication #
##################

# 省略。一番下に以下を追加
SOCIALACCOUNT_PROVIDERS = {
    # GitHub用のSocial applicationを削除して以下でも動きます
    # "github": {
    #     "APP": {
    #         "client_id": env("GITHUB_OAUTH_CLIENT_ID"),
    #         "secret": env("GITHUB_OAUTH_SECRET"),
    #     }
    # },
    "slack": {
        "APP": {
            "client_id": env("SLACK_OAUTH_CLIENT_ID"),
            "secret": env("SLACK_OAUTH_SECRET"),
        }
    },
}

127.0.0.1:8000/accounts/login/ にSlackアカウントでログインするためのリンクが追加されます!

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

リンクをクリックすると、Slackアカウントでのログインにあたり許可が求められます。

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

なお、今回はGitHubアカウントで連携してログインした後、http://127.0.0.1:8000/accounts/social/connections/ にアクセスしてSlackアカウントも連携しました。
なので、SlackアカウントでログインするとGitHubアカウントでログインしたときと同じように表示されます。

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

account_loginを経由せず、homeから直接ログインできるようにする

ここまでで作ったアプリは、homeからaccount_login(django-allauthが用意するログインページ)に遷移してログインします。

templates/home.html(新規作成)

      <p><a href="{% url 'account_login' %}">ログイン</a></p>

homeから直接ログインできるように、ソースコードを見ていじってみました(もっといいやり方をご存知の方いたら教えていただけると嬉しいです)。

      <!-- <p><a href="{% url 'account_login' %}">ログイン</a></p> -->
      お持ちの外部アカウントでログインをどうぞ
      <ul class="socialaccount_providers">
        {% include "socialaccount/snippets/provider_list.html" with process="login" %}
      </ul>

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

参考にしたのは、allauth/templates/account/login.html6
ソースコードを少し見たところ、settings.pyで有効にしたprovider(今回はGitHubとSlack)について、ログインに使うリンクが作られる挙動のようでした。

素振りしてみて

素振りする前は、キャッチアップが大変そうという印象のdjango-allauthでしたが、akiyokoさんの本を経由してドキュメントを読み、「ソースコード周りはsettings.pyの設定だけでソーシャルアカウント連携が実装できる!便利」と印象は大きく変わりました。
今回の素振りを元にやってみたい項目は以下です(年内にできたらいいな)。

  • レビュー用WebアプリでSlackアカウントでのログインを置き換える(力試し)
  • django-allauthリポジトリにあったexampleで素振りしてみる

django-allauthを使うと、ソーシャルアカウント連携は設定が大部分という感触を得ました。
来年作るDjangoアプリでは積極的に使っていけそうです!

補足:django-environ

先のコードで登場した env("SLACK_OAUTH_CLIENT_ID") などについて補足します。

OAuthの設定に必要なClient IDやSecret、DjangoSECRET_KEYなどは環境変数に設定していますが、コードではdjango-environで扱っています。
これも知ったのはakiyokoさんの本(実践編)です。

使い方は簡単で、ドキュメントの冒頭のコードが参考になります。

mysite/settings.py

import environ

env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(env_file=str(BASE_DIR / ".env"))

今回はリポジトリのルート(BASE_DIR)に.envファイルを置きました。
.envファイルを示すpathlib.Pathが受け取れないようなので、文字列に変換して渡しています。
環境変数.envファイルでまとめて管理でき、便利です。

.envファイルのイメージ

DEBUG=on
SECRET_KEY=very_very_secret_key

SLACK_OAUTH_CLIENT_ID=2****************4
SLACK_OAUTH_SECRET=hi_mi_tsu

  1. YouTubeアーカイブもあります。

  2. 嬉しいお言葉もいただきました。ありがとうございます。「いつ作ってるんあろうと思うぐらい作りこまれてまして」ref: まったりLog残し - PyCon JP 2020に参加してきました

  3. django.views.generic.TemplateViewはakiyokoさんの『基礎編』の教科書に解説があります

  4. リポジトリルートのtemplatesディレクトリからテンプレートを探せるように、mysite/settings.pyTEMPLATESDIRSBASE_DIR / "templates" を追加する必要があります

  5. ref: https://django-allauth.readthedocs.io/en/latest/providers.html#slack

  6. urls.pyを見たところ、account_loginという名前のパスに対応するビューはLoginViewでした。settings.pyで設定をしていない場合、account/login.htmlという名前のテンプレートが使われるようでした

イベントレポート | #はんなりPython python3.9を語る LT会 に参加しました(2020年11月)

はじめに

この記事ははんなりPython Advent Calendar 2020 1日目の記事です。

頑張れば、何かがあるって、信じてる。nikkieです。
2020年10月にPython 3.9がリリースされましたね!
はんなりPythonで開催されたPython 3.9についてのLT会1でインプットしたことをまとめます。

目次

勉強会の概要

【オンライン】はんなりPython #34 python3.9を語る LT会 - connpass

はんなりPythonは毎月3週目の金曜日に行っています。

参加者同士の交流や情報交換がしやすいような雰囲気を作っています。
現役のエンジニアや女性エンジニア、プログラミング学習者など幅広い方々との出会いの場になってほしいです。

11月のはんなりPythonはLT会,テーマは「Python3.9」です。
LT時間は7分から14分です。

21時〜23時というちょっと遅い時間帯が特徴だと思っています。

勉強会の様子

この会をまとめた公式ブログはこちらです。

発表資料

共同編集 Scrapbox

What's New in Python3.9を読む hide ogawaさん

変更点の概要(What's New)をdeepnoteで編集・実行可能な形式でまとめた発表。
触って気になるところの理解を深められるのがいいですね!
deepnote初めてでしたが、これで共同編集できるとなると、話題になるのも納得でした。

3.9 で追加された ast モジュールの新機能 ast.unparse t2yさん

ast.unparseドキュメント)で抽象構文木からソースコード戻せるようになったそうです。
ソースコード→抽象構文木(AST)ソースコードができる!
デモで、抽象構文木を加工してソースコードを書き換えていて、私はおおっ!と興奮しました。

プログラミング言語の抽象構文木に興味を持ったのは以下の記事です。
a = 2といったコードはもっと深く理解できるんだ。知りたい!」とワクワクしました。

hyというDSLの例では、今年PyCon Africaで聞いたteal-lang2を思い出しました。
どう作ったのか気になってましたが、抽象構文木を操作すれば作れるんですね。

t2yさんによると、抽象構文木を学ぶには、書籍は以下がオススメだそうです:

新規モジュール graphlib のご紹介 molo21stさん

みんなのPython勉強会で私もお世話になっている辻さんのツイートをきっかけに触ってみたとのこと。

モジュールのソースコードまで踏み込んだ解説でとても分かりやすかったです。
裏で何が起こっているかつかめ、木のように見た時に根元の部分から並べていることが分かりました。

素人考えですが、組織図のようなものを一直線上に表す時に使えるかもと思いました。
スタッフなどで複数の役割を兼務しているケースに重宝しそうです。

import graphlib as gl

graph = {
    "低音": {"黄前", "田中"},
    "トランペット": {"中世古", "高坂"},
    "北宇治": {"低音", "トランペット"},
}

ts = gl.TopologicalSorter(graph)
print(tuple(ts.static_order()))
# ('田中', '黄前', '中世古', '高坂', '低音', 'トランペット', '北宇治')
# このタプルを逆順にすれば、組織図の上から幅優先でたどった感じになりますね

PidfdChildWatcherについて Yasshieeee(やっしー)さん

PidfdChildWatcher の紹介 + 並列処理周りの整理。
質疑タイムで発表内容への補強もあり、並列処理を使いこなせていない私はいくつも学びがありました

multiprocessingの例(記憶を元に、またドキュメントを参考に)

import time
from multiprocessing import Process

def f(num):
    print("start", num)
    time.sleep(2)
    print("end", num)

if __name__ == "__main__":
    p1 = Process(target=f, args=(1,))
    p2 = Process(target=f, args=(2,))
    p1.start()
    p2.start()

# start 1
# start 2
# end 2  # 以下は2秒後に表示
# end 1

asyncioの例(記憶を元に、またドキュメントを参考に)

import asyncio


async def f(num):
    print("start", num)
    await asyncio.sleep(2)
    print("end", num)


async def main():
    await asyncio.gather(f(3), f(5))


if __name__ == "__main__":
    asyncio.run(main())

# start 3
# start 5
# end 3  # 以下は2秒後に表示
# end 5

zoneinfoを使おう hrsano645さん

# Python 3.9.0で確認
# Python 3.8まで
>>> from datetime import datetime, timedelta, timezone
>>> jst_tz = timezone(timedelta(hours=9))
>>> d1 = datetime(2020, 10, 31, 12, tzinfo=jst_tz)
# Python 3.9から
>>> from zoneinfo import ZoneInfo
>>> d2 = datetime(2020, 10, 31, 12, tzinfo=ZoneInfo("Asia/Tokyo"))
# どちらの作り方でも等しい
>>> d1 == d2
True

datetime.datetime.now()ドキュメント)はタイムゾーン指定なしではnaiveなローカル日時が紛らわしいは同意ですし、
これがnaiveなせいでawareなdatetimeと差を取ろうとしてエラーというのもあるあるです。

感想

Python自体について色々と知られて、楽しかった!これに尽きます。
みんなのPython勉強会やPyCon JPはスタッフとして参加なので、なかなかインプット全集中しにくいところがあります(運営タスクが脱集中してくるのです)。
参加者として参加すると、インプットに集中できるのが新鮮ですね。

また、Pythonを始めてそろそろ3年ですが、「Pythonのことは何でも知りたい!」とドキュメントをひたすら引いています3
そんな私にはPython 3.9縛りのLTはどれも聴きごたえがあり、とても楽しめました!

終わりに

はんなりPythonさんは第3金曜日以外も精力的に(週1ペース!で)勉強会を開催されているので、「これは!」と思うものがあったら、ぜひぜひ飛び込んでみてはいかがでしょうか。
12月はKaggle部や年末LT会がありますよー!

登壇者、運営スタッフ、参加者、スポンサーの皆様、ありがとうございました。


  1. 私もPEP 585について登壇したのですが、登壇報告はまたの機会に。ref: https://twitter.com/ftnext/status/1329775771766771713?s=20

  2. リポジトリを見たら今はhark-langというそうです

  3. Pythonに対してややヤンデレな気もします

"型ヒントの簡単な導入"と言われる「PEP 483 -- The Theory of Type Hints」を読む(その1 Subtype relationships)

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
この週末(11/14, 15)、PEP 483 -- The Theory of Type Hints を読んでみました。
読んだ時に控えたメモを数回に分けてアウトプットします。

目次

なぜ読んだか

11/20(金)にはんなりPythonでLTが控えています1
PEP 585(組み込みGeneric型2)について、登壇駆動でキャッチアップしようと申し込みました。
LTの準備として、型ヒントまわりのドキュメントを読んでみることにしました。
恥を忍んで言えば、型ヒントはドキュメントを読み込まず、雰囲気でやっている勢3でした。🙈

typingモジュールのドキュメントの冒頭に

型ヒントの簡単な導入は PEP 483 を参照してください。

とあるのを見つけ、PEP 483を読むことにしました。

注意事項

  • 翻訳ではありません。読んだときのメモです。なるべくPEP本体と対応するように進めます
    • 私の理解のためのメモ📝は斜体で追加します
    • 元の文章の斜体は太字にしています
  • 誤読と思われる箇所はコメントやTwitterでお知らせください。大変ありがたいです

検証環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.8.6
$ mypy --version
mypy 0.790

PEP 483とは

タイトルは「型ヒントの理論」。
Abstractによると、「PEP484が参照する理論をはっきり説明する」もの。

著者はGuidoさんとIvanさんで、2014年12月に作られました。

Introduction

Python3.5向けの新しい型ヒントのプロポーザルをはっきり説明する(詳細には多く立ち入らない)

説明すること

  1. 理論の基本概念を思い出すことから始める(←この記事ではここだけ扱います
  2. gradual typing(漸進的型付け)
  3. 一般的なルールを述べ、Unionのようなアノテーションで使われる新しく特殊な型を定義する
  4. Generic型(generic types4)と、型ヒントの実用面を定義する

表記の慣例

  • t1, t2u1, u2は型。t1, ... のいずれかとしてtiと表記する
  • TUは型変数(TypeVar()(Document)で定義される)
  • オブジェクト5、class文で定義されるクラス、PEP 8の慣例に従って示されるインスタンス
  • 型に適用される == は、このPEPでは、2つの式が同じ型であることを表す
  • PEP 484では、型は型チェッカー向けの概念で、クラスはランタイムの概念と区別する。本PEPでは区別を明らかにするが、型チェッカーの実装の柔軟性のために不必要な厳密性は避ける

Background

型という概念にはいくつも定義がある

  • 値の集合で定義する(値を列挙)
    • bool型:True, False
  • 値に適用できる関数の集合で定義する
    • Sized型:__len__メソッドを持つオブジェクト6
  • 単なるクラス定義による。intを継承したUserIDクラスの例(このクラスの全てのインスタンスUserIDという型を形成する)
class UserID(int):
    pass
  • より込み入った例:int, strまたはそれらのサブクラスのインスタンスのみを含む全てのリストをFancyList型と定義([1, "abc", UserID(42)]FancyList型)

Subtype relationships(サブタイプ関係)

静的型チェッカーに対して決定的な関係がサブタイプ関係。
次の質問から生じる:
first_type型の変数first_varと、second_type型の変数second_varがあるとき、first_var = second_varという代入は安全(safe)か?

安全であるときの強い基準:

  • second_typeのどの値もfirst_typeの値の集合に含まれる、かつ
  • first_typeのすべての関数7second_typeの関数の集合に含まれる

この関係をサブタイプ関係と呼ぶ。

📝上記は、second_typefirst_typeのサブタイプ関係にある」の定義
📝second_typefirst_typeのサブタイプ関係にあるとき、first_var = second_varという代入は安全である。

上記の定義より、

  • 全ての型は、自身のサブタイプ
  • サブタイプ化が進むに連れ、値の集合は小さくなり、関数の集合は大きくなる

(1) first_typeをAnimal、second_typeをDogとする。
DogはAnimalのサブタイプ(📝 =Dog型の変数をAnimal型の変数に代入できる animal = dog

📝なぜなら、以下のように「second_typefirst_typeのサブタイプ関係にある」の定義を満たすから

  • Dog型(second_type)の値(種々のわんこ🐶)であれば、Animal型(first_type)の値の集合(動物たち)に含まれる
  • Dog型(second_type)はbarkメソッドのようにAnimal型(first_type)よりも多くの関数を持つので、Animal型のすべての関数はDog型の関数の集合に含まれる

逆に、AnimalはDogのサブタイプではない

(2) 整数は実数のサブタイプである。

  • 全ての整数は実数でもある(📝=second_typeのどの値(整数)もfirst_typeの値(実数)の集合に含まれる)、かつ
  • 整数はシフト演算8 << や >> のように多くの演算をサポートする(=📝first_type(実数)のすべての関数はsecond_type(整数) の関数の集合に含まれる

📝つまり「second_typefirst_typeのサブタイプ関係にある」を満たしている

lucky_number: float = 3.14
lucky_number = 42  # intはfloatのサブタイプなのでsafe
lucky_number * 2
lucky_number << 5  # PEP483では「Fails」ですが、mypyはスルー。どういうことでしょう?🤔

unlucky_number: int = 13
unlucky_number << 5
unlucky_number = 2.72  # unsafeなので、error: Incompatible types in assignment (expression has type "float", variable has type "int")

(3) List[int]型はList[float]型のサブタイプではない

List[int] は整数だけを含む全てのリストから構成される型を意味する。
List[float]は実数だけを含む全てのリストから構成される型を意味する。

  • 📝List[int]型の値は、List[float]型の値の集合に含まれる(=second_typeList[int])のどの値もfirst_typeList[float])の値の集合に含まれる)
  • しかし、実数を末尾に追加する関数はList[float]でのみ機能する(→📝first_typeList[float])のすべての関数はsecond_typeList[int])の関数の集合に含まれるわけではない
from typing import List


def append_pi(lst: List[float]) -> None:
    lst += [3.14]


my_list: List[int] = [1, 3, 5]
append_pi(my_list)  # error: Argument 1 to "append_pi" has incompatible type "List[int]"; expected "List[float]"
my_list[-1] << 5  # mypyで検出されるので、実行時のエラーを避けられる

型チェッカーにサブタイプの情報を宣言する方法

型チェッカーにサブタイプの情報を宣言する方法は2つのアプローチが普及している

(A) nominal(名目的) subtyping
クラス木(class tree)に基づくタイプ木(type tree)
例:UserIDはintのサブタイプと見なせる(📝UserIDはintを継承したクラス9だから
Pythonでは互換性のないやり方で属性をオーバーライドできるので、このアプローチが型チェッカーの管理のもとで使われるべき

class Base:
    answer = "42"


class Derived(Base):
    answer = 5  # error: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "str")

(B) structural(構造的) subtyping
宣言されたメソッドからサブタイプの関係にあるか推論する
例:UserIDとintは同じ型と見なされる
時折混乱を招くが、こちらはより柔軟と見なされている

アプローチ両方とも10にサポートを提供する。
nominal subtypingに加えて、 structural information(構造的な情報)も利用できる

終わりに

1の内容は以上になります。
Generic型の共変・反変を除いて、一通り読んでいるので、時間を見つけて続きを上げていきます。

サブタイプ関係にあるかどうかを考えていると、大学時代の数学の講義を思い出しました。
ふだん使っているプログラミング言語と学問体系のつながりの一端を実感できました。


  1. お時間ありましたらどうぞ。Python3.9について語るLT会です 【オンライン】はんなりPython #34 python3.9を語る LT会 - connpass

  2. python.jpの Python 3.9の新機能 - python.jp が分かりやすいと思います。PEP 483や他のドキュメントと行き来して、ようやく分かってきました

  3. ドキュメントを読むと、アンチパターンに手を出していたことが分かりました。知らないって怖いですね

  4. genericの訳が難しいですね。python.jpにならって「Generic型」としています。typingのドキュメントでは「ジェネリクス」となっているところもあります

  5. このObjectsは何がかかるのか今ひとつ分かっていません

  6. https://docs.python.org/ja/3/library/collections.abc.html#collections.abc.Sized

  7. 原文はevery function from first_type。メソッドも関数と理解しています

  8. https://docs.python.org/ja/3/reference/expressions.html#shifting-operations

  9. BがAを継承している(BはAのサブクラス)とき、「B型のどの値もA型の値の集合に含まれ」、かつ、(継承しているから)B型に適用できる関数はA型より増えるのでサブタイプ関係にあると結論付けられますね

  10. この部分の強調はnikkieによる

週末ログ | PyTorch Lightningの"Lightning in 2 steps"を触りました⚡️

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
最近v1.0が出たPyTorch Lightning、Getting startedのドキュメントに沿って週末に素振りしました。

目次

PyTorch Lightningとは

世はまさに大AI時代といった趣で、TensorFlowをはじめとする深層学習フレームワークが盛んに開発されています。
その中に、Facebook発の深層学習フレームワーク PyTorch があります。
PyTorch Lightning(以下、Lightning)はPyTorchのラッパーライブラリです。

PyTorchで頻繁に書くボイラープレートのコードがなくなるように設計されています。
機械学習スクリプトエントリポイントが劇的に薄くなるので、「めっちゃイケてる✨」と心ときめきました。
実験に使うスクリプトのエントリポイントが長大になりがちなんですよね。。

if __name__ == "__main__":
    autoencoder = LitAutoEncoder()
    
    dataset = MNIST(Path.cwd(), download=True, transform=transforms.ToTensor())
    train_loader = DataLoader(dataset)

    trainer = pl.Trainer()
    trainer.fit(autoencoder, train_loader)

10月のv1.0.0リリースに合わせて開発チームからBlogがポストされています。

  • v1.0.0でLightningのAPIがfix
  • Metricクラス追加
  • 劇的に簡単なlogging!(self.log

開発チームはGrid AIというモデルの訓練・サーブプラットフォームも開発していくそうです。

Lightningの哲学

LightningはPyTorchのラッパーなので、書き換え方を押さえなければなりません。
使い始めるのに必要な学習コストが気になりますが、リポジトリのREADMEで紹介されている原則(Lightning Philosophy)がヒントになると思いました。

Principle 4: Deep learning code should be organized into 4 distinct categories.

  • Research code (the LightningModule).
  • Engineering code (you delete, and is handled by the Trainer).
  • Non-essential research code (logging, etc... this goes in Callbacks).
  • Data (use PyTorch Dataloaders or organize them into a LightningDataModule).

先のコードでは、LitAutoEncoderLightningModuleクラスを継承したResearch codeです。
Engineering codeは全てTrainerに寄せています。

以下、素振りで作ったコードを備忘録代わりに残します。

今回の環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.8.1
$ pip install pytorch-lightning torchvision
# pytorch-lightning      1.0.3
# torchvision            0.7.0

# -- Lightningの依存により以下が入った ---
# numpy                  1.19.2
# torch                  1.6.0
# tensorboard            2.3.0

Lightning in 2 stepsのコード

冒頭で紹介したページのMNISTの例(オートエンコーダー訓練)を進めました。
ページ冒頭の3分動画も参考にしています。

from argparse import ArgumentParser
from pathlib import Path

import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from torch import nn
from torch.utils.data import DataLoader, random_split
from torchvision import transforms
from torchvision.datasets import MNIST

Research code: LightningModule

class LitAutoEncoder(pl.LightningModule):
    def __init__(self):
        super(LitAutoEncoder, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(28 * 28, 64),
            nn.ReLU(),
            nn.Linear(64, 3),
        )
        self.decoder = nn.Sequential(
            nn.Linear(3, 64),
            nn.ReLU(),
            nn.Linear(64, 28 * 28),
        )

    def forward(self, x):
        embedding = self.encoder(x)
        return embedding

    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=1e-3)
        return optimizer

    def training_step(self, batch, batch_idx):
        x, y = batch
        x = x.view(x.size(0), -1)
        z = self.encoder(x)  # forwardを呼び出す self(x) でもよい
        x_hat = self.decoder(z)
        loss = F.mse_loss(x_hat, x)
        self.log("train_loss", loss)
        return loss

    def validation_step(self, val_batch, batch_idx):
        x, y = val_batch
        x = x.view(x.size(0), -1)
        z = self.encoder(x)
        x_hat = self.decoder(z)
        loss = F.mse_loss(x_hat, x)
        self.log("val_loss", loss)
        return loss

ポイントと思ったところ1

  • LightningModuleは1つのモデルではなく、複数のモデルからなるシステムを扱える2(この例でもencoderとdecoderを扱っている)
  • forwardメソッドとtraining_stepメソッドを分離3forwardはデプロイしたモデルの推論でも使える)
  • training_stepメソッド(訓練)でlossを返している限りはLightningによって自動で最適化される(backward、optimizerの更新)

Engineering code: Trainer

  1. 訓練時の設定(GPUを使うかなど)を元にTraninerを初期化し、
  2. Trainerにモデル(LightningModuleを継承したクラス)とデータを渡して訓練します

1のTranierの初期化は、コマンドラインからも指定できます4
Trainer初期化の引数をいじらなくていいので便利そうです。

def parse_args():
    parser = ArgumentParser()
    parser = pl.Trainer.add_argparse_args(parser)
    args = parser.parse_args()
    return args


if __name__ == "__main__":
    args = parse_args()
    
    trainer = pl.Trainer.from_argparse_args(args)

コマンドラインからTrainerに渡すと便利そうだと思ったオプション

  • --gpus 個数
    • to(device)というPyTorchで頻出するコードから解放されたのはすごい!
  • --fast_dev_run:コード全体のユニットテストオプション5
    • single batchのtrain, val, testデータで実行
    • これにより、つまらないミスで訓練中に落ちる悲しい事件とさようならできる!
  • --limit_train_batches バッチ数, --limit_val_batches バッチ数:少量のデータで実行
  • --max_epochs, --max_stepsも設定できる
  • --deterministic:再現性の担保6
    • seed_everything関数と一緒に使う(numpy, torch, random, PYTHONHASHSEED全部のシードを固定)

Data: PyTorch Dataloader & LightningDataModule

train, val, testの各データの扱いをエントリポイントから、データモジュールクラスに移せるのが好感触です。

class MNISTDataModule(pl.LightningDataModule):
    def __init__(self, batch_size=32):
        super(MNISTDataModule, self).__init__()
        self.batch_size = batch_size

    def prepare_data(self):
        MNIST(Path.cwd(), train=True, download=True)
        MNIST(Path.cwd(), train=False, download=True)

    def setup(self, stage):
        if stage == "fit":
            mnist_train = MNIST(
                Path.cwd(), train=True, transform=transforms.ToTensor()
            )
            self.mnist_train, self.mnist_val = random_split(
                mnist_train, [55000, 5000]
            )
        if stage == "test":
            self.mnist_test = MNIST(
                Path.cwd(), train=False, transform=transforms.ToTensor()
            )

    def train_dataloader(self):
        mnist_train = DataLoader(self.mnist_train, batch_size=self.batch_size)
        return mnist_train

    def val_dataloader(self):
        mnist_val = DataLoader(self.mnist_val, batch_size=self.batch_size)
        return mnist_val

    def test_dataloader(self):
        mnist_test = DataLoader(self.mnist_test, batch_size=self.batch_size)
        return mnist_test
if __name__ == "__main__":
    args = parse_args()

    autoencoder = LitAutoEncoder()

    dm = MNISTDataModule()

    trainer = pl.Trainer.from_argparse_args(args)
    trainer.fit(autoencoder, dm)
    # trainer.test(後ほど試す) 

Non-essential research code: Callbacks

今回は手を動かしていませんが、early stoppingはCallbackで実現するそうです。

logging7self.logに指標名と一緒に渡すだけ。
プログレスバーtensorboardで確認できます(logの引数で調整もできます)。

tensorboard --logdir ./lightning_logs

lightning_logsディレクトリにチェックポイントが保存されています

感想

エントリポイントが薄くなった高揚感でここまで書き上げました。
Lightningはすごく有用そうです!
ただ自動最適化はブラックボックス化でもあるので、PyTorchのボイラープレートコードも理解し、Lightningで賢く楽をしたいですね。

今後は実際に学習を回して素振りを繰り返し、ドキュメントで知ったことを知識に変えていこうと思います。
チュートリアルのMNIST 60000件はCPUでは訓練がサクサクいかないので、データを間引くか、他のデータセットを探すかですね。

訓練したモデルはtorchscriptなる形式で掃き出せるそうです。
この週末DeNA AIチャンネルで知って、よさそうに感じたstreamlitでdecoderをアプリ化しても面白そうだなと思いました。