nykergoto’s blog

機械学習とpythonをメインに

python の DateTime・Timezone と Django での取扱い

django でアプリケーションを作っていて timezone 周りで困ったことがあったので、初めて django および python での時間の取り扱いについて真面目に調べてみた記録です。

はじめに・ われわれが使っている時計の時間とは何か

まず時計が示している時間とは何かということを考えてみましょう。 世界中に時計がひとつしかなければ(すべての人間がひとつの時計を正として生活していれば)特に問題はなくその時計の時刻自体を YYYY-m-dd HH:mm などで保存したものが常に正しいので何も考えることはありません。 皆同じものを参照しているので不整合はありません。

しかし実際には世界には時差があります。時差があるというのはどういうことかというと時計を文字通り読み取った値が同じでも、物理的には違う時間なことがあるということです。

例えば東京の 4/23 18:00 とニューヨークの 4/23 5:00 は物理的には同じ時間を表していますが、時計は違う時刻を指しています。それは東京・ニューヨークが別の Timezone に属していているためです。

Timezone ごとに、標準時 (イギリスのあたり) の時計に対して何時間ずらすのかが決まっています。例えば東京では標準時から +9H ・ ニューヨークなら -4H ときまっているので、先の 18:00 と 5:00 が同じ物理時間を指していることが確認できます (差分が 9 + 4 = 13 時間であるからです)。

python で時刻を扱う

これらの問題は python で時刻を取り扱う場合でも同様なので、 単に数値としての時刻と Timezone の情報を持った時刻を区別して取り扱うように実装されています。 前者を naive time, 後者を aware time と呼びます。

naive time は単に datetime として作成したときの状態です。標準ライブラリの datetime で現在時刻をとったような状態では naive time です。

from datetime import datetime

naive = datetime.now()  
# datetime.datetime(2020, 4, 23, 10, 15, 27, 886221)

aware な時刻かどうかは datetime object が持っている tz によって判定されています。naive な datetime は tz を持っていません。

naive.tzname() is None  # True

tzを設定するひとつの方法が naive.astimezone です。この関数は tzinfo instance を引数にとってその timezone の aware な時刻のオブジェクトを返します。 tzinfo の設定は pytz を使うのが楽ちんです。例えば UTC 時刻として登録する場合は以下のようになります。

import pytz
naive.astimezone(pytz.UTC) 
# datetime.datetime(2020, 4, 23, 10, 15, 27, 886221, tzinfo=<UTC>)

一度 aware になってしまうと naive な時刻とは引き算をすることができません。

くどいですが区別するのは Timezone 込の情報と Timezone 無しのものは比較することができないからです。 これら2つを同様に扱ってしまってナイーブに引き算などすると例えば東京とニューヨークを同様に扱って引き算できてしまうので、先の例だと時間差がゼロになってしまいます。

実際 naive time と aware time を引き算すると TypeError が発生します。親切ですね。

naive - naive.astimezone(pytz.UTC)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-24-98bec8f1194b> in <module>
----> 1 naive - naive.astimezone(pytz.UTC)

TypeError: can't subtract offset-naive and offset-aware datetimes

astimezone で変換さた時刻は、元の naive な時刻を標準時で持つような tzinfo の時刻となります。 そのためここで tokyo や newyork を設定しても引き算すると時刻の差はゼロです。

tokyo_tz = pytz.timezone('Asia/Tokyo')
nyc_tz = pytz.timezone('America/New_York')

naive.astimezone(tokyo_tz) - naive.astimezone(nyc_tz)
# datetime.timedelta(0)

もし「東京の時計で naive な時刻」を持つような時間を作りたい場合には timezone.locale を使います。 例えば東京とニューヨークそれぞれで同じ時計が表示されている状態の差分を見る場合は以下のようになります。期待通り時差 +9H から -4H を引くので 13時間のズレになっていることがわかります。

# 東京とニューヨークで文字盤上同じ時刻を引き算すると時差と一致する
diff = tokyo_tz.localize(naive) - nyc_tz.localize(naive)
diff.total_seconds() / 60 ** 2  # -13.0

Django での時間の取り扱い

Django では USE_TZ=True のとき時差を考慮した処理を行なうようになります。すなわち djangopython object として扱う時刻はすべて aware な datetime になるということです。反対に USE_TZ=False にしていると tz を設定しない naive な datetime を使います。この場合現在時刻を知りたければ datetime を使うことができます。

from datetime import datetime

now = datetime.now()

一方で USE_TZ=True の場合は aware な方法で now を作成しなくてはいけません。このような場合のために django では django.utils.timezone というモジュールがありその内部に now が実装されています。

from django.utils import timezone

aware_now = tiemzone.now()

この内部では datetime.now を呼び出したあとに UTC を tz に設定しています。 このように django では時差考慮するとなった時には基本的に UTC として取り扱います。これによってデータベースに保存する際にすべての時刻が UTC であることが担保されますので、引き算等の演算での不整合が発生することを防ぐことができます。

# django/utils/timezone.py

# UTC time zone as a tzinfo instance.
utc = pytz.utc

# 中略

def now():
    """
    Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
    """
    if settings.USE_TZ:
        # timeit shows that datetime.now(tz=utc) is 24% slower
        return datetime.utcnow().replace(tzinfo=utc)
    else:
        return datetime.now()

webapplication ではリクエスト元のユーザーが違う timezone に属していることがあります。このような場合にはリクエストごとに Timezone を変更したくなりますが、この用途のために django には activate という関数があり、これを呼び出すとデフォルトの TimeZone を上書きすることができます。

from django.utils.timezone import activate
activate('Your-Favorite-Timezone')

テンプレートエンジンや DRF のシリアライザなどユーザーに情報を返す際にはここで設定された TimeZone での locale datetime への変換がされるので、ユーザーの TimeZone の時計で見た時の時刻をユーザーごとに設定して返すことができます。

Example: time から datetime への変換

「TimeField を今日の時間とみなして、その時間と DateTimeField の値を比較したい」といいう要望があったとしましょう。 Time を date に設定する関数 combine を使うと以下のように書くことができるように思えますが USE_TZ の場合エラーになります。それは to_today_datetime が返している datetime が naive だからです。

from django.utils import timezone

def to_today_datetime(time, now):
    today = now.today()
    dt = timezone.datetime.combine(today, time)
    return dt

time = # TimeField の値. Time 型

now = timezone.now()

# Log という model があったとする
created_at = Log.objects.first().created_at # created_at は DateTimeField
created_at - to_today_datetime(time, now) # TypeError: can't subtract offset-naive and offset-aware datetimes

では utc にすれば OK かというとそうでもありません。 なぜなら今日と認識しているのはリクエストを送ったユーザーですから、ユーザーにとっての時間として表現する必要があるからです。 今設定されている timezone は get_current_timezone で取得することができます。 得られた tzinfo を使って localize することで時間をユーザーのtimezoneでtimeを時計で見たときの時刻に変更することができます。

def to_today_datetime(time, now):
    today = now.today()
    dt = timezone.datetime.combine(today, time)

    # これでは UTC の人にとって `dt` が時計に表示されているときの時刻になる。
    # dt = dt.astimezone(timezone.utc)
    
    # 今のリクエスト context での timezone を取得して、その timezone で `dt` が表示された時の時間に直す
    tz = timezone.get_current_timezone()
    dt = tz.localize(dt)
    return dt

参考

python で logging を止める

はじめに: 基本的なお作法

python の logging の話です。logging そのまま呼び出しもできるのですが若干やんちゃやで、ということが公式ドキュメントに書いています。

ロガーに名前をつけるときの良い習慣は、ロギングを使う各モジュールに、以下のように名付けられた、モジュールレベルロガーを使うことです: https://docs.python.org/ja/3/howto/logging.html#advanced-logging-tutorial

おとなしくガイドに則って, お行儀よく getLogger で名前を指定して logger を取得するようにしましょう。

from logging import getLogger

logger = getLogger(__name__)

# この段階ではコンソールには何も出ない
logger.info('foo')

Handler

  • logger を単に call しても何もおこならないのは logger 単体はログの管理をしているだけで、実際にファイルやコンソールに書き出す仕事は Handler インスタンスが行なっているからです。
  • Handler はいろいろと種類があるのですが普通使うのは StreamHandler(コンソール書き出し) と FileHandler(外部ファイルへの書き出し) が多いと思います。
  • この handler を logger に追加することで、logger → handler に情報伝達されます。たとえば StreamHandler であればログがコンソールに表示されるようになります。
from logging import StreamHandler

handler = StreamHandler()
logger.addHandler(handler)

Level

logging にはその重要度を定める level という概念があります。 logger と handler にはそれぞれ level を設定することができます。 logger は自分に設定されている level よりも高い log が call された時に handler を実行します。

handler にも level があり、呼びだされた level が自分に設定されている level よりも高いときに handler の method が呼び出されます。

# このハンドラが感知するのは INFO よりもレベルが高いもの (i.e. info / warning / error / critical) になる。 
handler.setLevel('INFO')

logger.info('foo') # この段階では logger のレベルが設定されていないので handler も動かない

# logger に level を設定すると動くようになる
logger.setLevel('INFO')
logger.info('foo') # foo

Logging を一時的に止めたい時

本題です。 logger.disabled = True にすると handler の呼び出しが止まります。一時的にログを止めたい時とか便利です。 level=NOTSET でも同じことになりますが, この場合前に設定していたレベルをどこかに覚えておかないと同じ状態に戻せないため disabled を使ったほうが便利です。

logger.disabled = True
logger.info('hoge') # なにもでない

logger.disabled = False
logger.info('piyo') # piyo

Wantedly さんの Machine Learning 輪講に参加しました!

先週、wantedly さんの Machine Learning 輪講に参加させていただきました。

www.wantedly.com

Machine Learning 輪講は最新の技術や論文を追うことで、エンジニアが「技術で解決できること」のレベルをあげていくことを目的にした会です。

とのことで社内外の有志の方が集まって機械学習に関する論文などを紹介する会です。 こういう楽しげな勉強会はだいたい東京で、参加しようにもできずで枕を濡らすことが多々あるのですが、今回はコロナの影響もありオンライン開催ということで僕も出れる!!と喜んで参加応募したところ快くOKしていただき参加することになりました。@yu__ya4 さん、ありがとうございます!

内容については github の issue https://github.com/wantedly/machine-learning-round-table/issues/44 にもまとめてくださっていますが、以下はそのときに聞いた内容の僕なりのメモになります。

hakubishin3 さん: Managing Diversity in Airbnb Search

https://arxiv.org/abs/2004.02621

「検索結果に価格と場所の多様性を持たせてユーザー体験を良くしたい」というモチベーションのもと書かれた推薦モデル構築に関する論文

hakubishin3 さんの git: https://github.com/hakubishin3/papers/issues/8 がめちゃ詳しいのであわせてどうぞ。

  • ローカルで最適化した指標はモデルが出す推薦結果(リスティングと読んでいるよう)の多様性が少なくなるという傾向があり、それを改善するというのが目的
  • 新しい指標として、特定のアイテムを推薦すると決めた時に、すでに推薦した集合の中に新しいものと近いアイテムが含まれている時ペナルティになるような項を付与。
  • 加えて「上位に入っているものは上位にいるというだけでクリックされやすい(そのアイテムの推薦のクオリティだけではないバイアスがある)ことを考慮するためにこちらにも罰則を付与。上位に推薦されたものに対して経験則で罰則をつけるっぽい
  • 場所に関してもユーザーの傾向からこういう場所にいきたいだろうな〜という分布を作り推薦結果がその分布に近くなるようなロスを加える
  • これらを組み込んだモデルで出した推薦結果を入力として二段目のモデル!に入れて出力を作る (どんだけネットワークあるんだ…)
  • [質問] 場所に関しては直接緯度経度を使っているのか?
    • 多分ブロックごとに区切ったものを使用している。Airbnb の他の論文でもそうだから

推薦の論文は自分ではまず読まないので、個人的にとても刺激になりました。 KPIになりそうな指標をあえておいておいて、多様性によるユーザー体験を最適化しようとするのが Airbnb らしいというふうな意見が出たのが印象的でした。推薦にもいろんな文化があって面白いですね。

ninopira さん: Emergent Tool Use From Multi-Agent Autocurricula

https://arxiv.org/abs/1909.07528

  • かくれんぼを強化学習で解いたら創発的に戦略を学習したよという論文
    • 学習が進むに連れてどんどん戦略の芸が細かくなっていくのは見ていて面白い
    • 筆者らが想定していなかったような戦略も出てきたりしていてある種人のナイーブな思考にも勝つ部分が存在しているのも良かった
  • かくれんぼで学習してから他のタスクを解くときの収束速度でタスクの汎用性を判断できるかどうかが言える? (ちょっと理解足りていない気がするのでブログを読んでください)
  • 面白い一方で強化学習やっぱり時間かかるし大変そうだなあ

https://pira-nino.hatenablog.com/entry/introduce_openai_hide-and-seek に丁寧にまとめて頂いているので、詳細はそちらを見てもらえれば。

agatan さん: Evolving Normalization-Activation Layers

https://arxiv.org/abs/2004.02967

進化計算を使って最強の Normalization Layer をデザインしようぜ! っていう論文の紹介

  • Normalization - Activation は組として考える必要がある
    • 不勉強なので知らなかったですが、これ以前にもそういう議論はあったよう。
    • Initialization も活性化関数とひも付けて議論されていたし、勾配の安定性を担保するっていう観点で導入されている Normalization も同様の考え方するべきなのは言われてみればなるほどという感じ
  • 本質的に良い物はどのタスク・ネットワークに適用してもいいはず。
    • それはそう。
  • すべて同じ解き方・初期値の決め方でネットワーク・タスクをいろいろ試して、見つかったレイヤがうまく機能することを確認
    • いろいろ試すが本気 (Big GAN / Mask R-CNN / ImageNet)。 計算量で殴ってきている感がある。

JunpeiTakubo さん: Approximate Feature Collisions in Neural Nets

https://papers.nips.cc/paper/9713-approximate-feature-collisions-in-neural-nets.pdf

Adversarial Examples の発生条件が relu での変換で違う入力が同一の特徴量になる性があるんじゃない?っていう論文

  • RELU だと負になった瞬間すべてが 0 になるので、正になるところだけ一致するような変換が行われると入力が異なっていても同じ出力 (中間レイヤでも良いので特徴量でも良い) になって結果も同じなる
  • 制約条件を満たす領域がpolytopeになるので頂点を求めれば内点がすべて条件を満たすのは relu ならではで面白い。(preluもそうだし線形関数のmaxとるような関数が活性化関数ならば全部そうなんかな?)

nyk510: Do CNNs Encode Data Augmentations?

https://arxiv.org/abs/2003.08773

画像の Augmentation って CNN のどの部分で吸収されているの? っていう論文を紹介しました。

若干やり方が乱暴かな?っていうところはありつつも、Augmentation の違いを CNN のどこでエンコードされているかという問題として考える発想が好きです。 どこでエンコードするか?っていうのは、ある種 Augmentation を分類していることなので、これがもっと進んでいくと「こういうタスク・ネットワークにはこういう Augmentation が良い」といった議論をする際の指標の一つにできるのではないかな?と勝手に期待しています。

今は全部試していいものを残すという若干乱暴?な方法に頼っているような気がしていて、実務で画像タスクを解く際にも Augmentation の選定考えるのは悩みポイントの一つだったりするので、指針ができるととてもうれしいなあ。

あとこの論文で行っている Augmentation の度合いがどちらが強いかを当てるタスクが事前学習として使えるのでは? (NLP の BERT のようなイメージ) というのも面白いですね、と話したら結構以前からある発想らしいと agatan さんに教えてもらったので調べます。

感想

論文を聞いたり読んだりするの、とても楽しいですね😆 僕も軽くですが発表することになり、久しぶりに人に説明するという気持ちで読んだのでリハビリになってよかったです。(読まないと読めなくなりますね…)
僕が一人では読まないようなジャンルのもの(強化学習とか推薦とか)を聞けるのもいいですし、質問の中で知らない概念・知識をいただけるのもとても刺激になりました。今回は機械学習という枠組みですが別の会では推薦を主に取り扱ったものもあるそうなので、そちらにもぜひ出ていろいろとお話聞けたらいいなあと思っています。

オンラインで勉強会に参加するのが今回がはじめてで最初結構緊張していたのですが、wantedlyの皆さんが優しい雰囲気を作ってくださったのもあり、質問も普通にできるしディスカッションもできるしでとても楽しく過ごすことができました。
こうやってオンライン開催増えると、勉強会の数が少ないところに住んでいてもいろいろな知見の共有ができて嬉しいのでどんどん広まってほしいです。

あと、この輪読会はあえてスライドなどの準備をしないという制約があるのですが、発表のハードルが下がることと発表者・聴講者の双方向のやり取りが増えてとても良かったです。 どうしてもスライド準備するとまずしんどいし聞く方としても受け身になってしまいがちだなというのを前々から思っていたので、素敵なデザインで解決されているのが流石だなと思いました。atma勉強会でも真似していきたいところ…

結論

とにかく楽しかったのでまた機会があれば是非参加したいです :D

gitlab registry にログインできなくなった時の対処法

自分の環境

ubuntu 16.04

➜ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.6 LTS"

起こったこと

現象は単純で gitlab registry へのログインができないというもの

my-project on  master on 🐳 v18.06.1 took 2s 
➜ docker login registry.gitlab.com          
Username: nyker510
Password: 
Error response from daemon: Get https://registry.gitlab.com/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

gitlab registry はたまにアクセスできないことがあるので何回かやればできるかなーと思っていたが15分ほどリトライするも治らず。前は普通にログインできてたのにな…

解決方法

DNSの設定を変更すると良いらしい。

github.com

~ took 6s 
➜ sudo nano /etc/resolv.conf      

これでファイルを以下のように編集

# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 10.0.0.10

するとログインできました。ぱちぱち。