Pay money To my Life

Spend time for myself, and you... "Knowledge was the only way to remember that your past is true"

Pythonの変数空間汚染との戦い・完結編 ~ noglobal

はじめに

本記事は、Pythonにおける変数、特にグローバル変数による変数の汚染との戦いへ終止符を打つ、完結編となっています。ちなみに、前回までの話は特にありません。一話完結型です。

なお、最終的なコードや本記事で実験に使用したコードらは、Githubに上げています。

今回の目的は、noglobalを完成させること。それだけです。では参りましょう。



謝辞

本編に入る前にまず、今回の執筆に至るまでの経緯と関連する方々への謝辞を。

最初に問題提起をしnoglobalの存在を知るきっかけになった@fkubotaさん、fkubotaへ回答を投げた@nyker_gotoさん、さらに回答を広げてくれたまますさん、そして最終的なnoglobalが完成するきっかけをくれたもみじあめさんには感謝します。また、ベースとなるコードを作成してくださったax3lさん、raven38さん、yoshiponさんにも御礼申し上げます。とても勉強になりました。

最終的に完成したコードはGithubに上げていますが、可能ならPyPIに、pip経由でinstallできるようにしておいてもいいのではないかと思います。しかしそれはまた別のお話なので、別の機会に。

最終的に採択されたコード

以下のコードを採用しました。コメント等は省略しています。詳細は、後述の本文をみるか、Github/noglobal.pyを参照してください。
以下のコードを適当に、noglobal.pyというファイルにでも保存して、それをfrom noglobal import noglobalとするだけで使用できます。

このnoglobalは、以下のような挙動をします。

  • 任意の関数にデコレータとして@noglobal()とつけることで、その関数内からはグローバル変数を参照できないようにする
  • グローバルな関数については参照可能(従来通り関数を使用できる)
  • builtins関数(つまり、printとかlenとか)も使用可能
  • @noglobal(excepts=["hoge"])に渡すことで、使用したいグローバル変数を指定して使用することが可能

なお、このnoglobalは、本記事中ではnoglobal::ver05という位置付けであり、noglobal::ver04_momijiameに数行付け足したものです。

各versionについては後述しますが、各versionのオリジナルへのリンクを掲載しておきます。

noglobal::ver05-2は、ver5よりもimportが楽になるというメリットがあります。個人的には、ver05-2があれば十分です。特別理由がなければ、ver05-2を使用しましょう。

ただ、今回アップデートしたver05とver05-2は、あくまでver04をベースにしているため、ver04とは完全に互換性があります。ver04で使用されている関数名を変更していないので、そのままnoglobal.pyを置き換えるだけでver05に更新することができます。また、ver03以前のものについては、そもそもver04以降はデコレータとして使用する際に()が必要になったので、そこの改変が必要です。そればかりは、仕方ない。そこまで面倒みる力はありません。

ちなみに、今回紹介したいnoglobal::ver05では、従来のnoglobalと比較して以下の点が改善されています。

  • 他のファイルで宣言されたnoglobalをimportで呼び出して使用できるように (これがver04での改善点)
  • それを拡張し、importを経由して繋がる全ての他のファイル内でもnoglobalが使用可能に (ver05での改善点)
  • noglobalを使用するための定義が、import文だけで完結するように (ver05-2に限る)

注意事項も記載しましたので、使用例までぜひみていってください。

最新のnoglobal.pyのコード

from functools import partial
import inspect
from typing import List, Dict, Optional, Callable, Any
from types import FunctionType


def globals_with_module_and_callable(globals_: Optional[Dict[str, Any]] = None,
                                     excepts: Optional[List[str]] = None) -> Dict[str, Any]:
    def need(name, attr):
        if name in excepts:
            return True
        if inspect.ismodule(attr):
            return True
        if callable(attr):
            return True
        return False

    if globals_ is None:
        globals_ = globals()
    if excepts is None:
        excepts = []
    filtered_globals = {
        name: attr
        for name, attr in globals_.items()
        if need(name, attr)
    }
    
    if not inspect.ismodule(globals_["__builtins__"]):
        for name, attr in globals_["__builtins__"].items():
            if need(name, attr): filtered_globals[name] = attr
    
    return filtered_globals


def bind_globals(globals_: Dict[str, Any]) -> Callable:
    def _bind_globals(func: FunctionType) -> FunctionType:
        bound_func = FunctionType(code=func.__code__,
                                  globals=globals_,
                                  name=func.__name__,
                                  argdefs=func.__defaults__,
                                  closure=func.__closure__,
                                  )
        return bound_func
    return _bind_globals


def no_global_variable_decorator(globals_: Optional[Dict[str, Any]] = None):
    partialled = partial(globals_with_module_and_callable, globals_=globals_)

    def _no_global_variable(excepts: Optional[List[str]] = None):
        partialled_globals_ = partialled(excepts=excepts)
        bound_func = bind_globals(globals_=partialled_globals_)
        return bound_func

    return _no_global_variable

global noglobal
class noglobal:
    def __init__(self, excepts=None):
        self.excepts = excepts
    
    def __call__(self, _func):
        return no_global_variable_decorator(
            globals_=_func.__globals__
          )(excepts=self.excepts # arg of _no_global_variable
          )(func=_func)          # arg of _bind_globals 

使用例

ここで示すのは、noglobal::ver05とnoglobal::ver05-2です。この2つは、noglobalのimportを介した定義方法が異なるだけで、内部的な処理には違いはありません。

注意したい挙動なのが、最後のfunc_use_exceptsです。デコレータとして、@noglobal(excepts=["a"])とすることで、func_use_excepts内ではグローバル変数であるaを特例的に使用することを許可しています。しかし、そのfunc_use_excepts内にネストされている関数func_nestは、宣言時にグローバル変数の使用に関して特例を認めていません。そのため、func_use_excepts内ではグローバル変数aを参照できるけど、ネストされている関数func_nestではaを参照できない、という挙動になります。

noglobal()による変数名前空間の限定は、関数の宣言時にfixされます。この辺りには注意する必要があります(使用したければ、明示的に引数として渡す、などが必要です。むしろこのstrictさこそがnoglobalの存在意義なのですが)。

# どちらで宣言しても挙動は同一
# use as ver05
# from noglobal import no_global_variable_decorator
# noglobal = no_global_variable_decorator(globals())

# use as ver05-2 <- 特別な理由がなければこちら推奨
from noglobal import noglobal

a = "hoge"

def run(f):
    try:
        f()
    except NameError as ne:
        print(ne)
    print()

@noglobal()
def func_usual():
    print("This is func_usual")
    print(a)

import numpy as np
@noglobal()
def func_nest():
    print("This is func_nest")
    print(np.arange(0,10))
    print(a)
    run(func_usual())

@noglobal(excepts=["a"])
def func_use_excepts():
    print("This is func_use_excepts")
    print(np.arange(0,10))
    print(a)
    run(func_nest()) # Raised NameError because func_nest
                        # does not allow to use global variable; a.


>>> run(func_usual)
This is func_usual
name 'a' is not defined

>>> run(func_nest)
This is func_nest
[0 1 2 3 4 5 6 7 8 9]
name 'a' is not defined

>>> run(func_use_excepts)
This is func_use_excepts
[0 1 2 3 4 5 6 7 8 9]
hoge
This is func_nest
[0 1 2 3 4 5 6 7 8 9]
name 'a' is not defined

noglobalをめぐる大まかな流れ

まずは、これから作成していくnoglobalってなんぞや?何がしたいんや?っていうあたりの話を進めていきましょう。歴史を振り返ります。

序:先人たちの変数汚染との戦いの記録

今回のターゲット、noglobalを最初に知ったきっかけは、とあるTwitterでの質問に遡ります。質問をした@fkubotaさんがQiitaの記事を執筆しています。そちらもご参照ください。

本質問についてざっくりと概要について触れると、「jupyter notebookでは、無秩序にグローバル変数が定義・使用されている。それによって、変数名が重複してしまったり、定義した関数内で意図せずに既に宣言されているグローバル変数を参照してしまったりすることで意図しない動作を生む恐れがある。これを回避するために、関数内ではグローバル変数は使用しない、というように制限をかけれないか?」という旨です。

それに関しては、@nyker_gotoさんからすぐに回答がありました。

ほう。そんな便利なものがあるのですね。都合上、このax3lさんによるnoglobalnoglobal_ver01としましょう。

しかしこれに関して、nyker_gotoさんへ本関数の存在を知らしめたまますさんから、追加の情報が。

さすがは現代、世は令和。早いもんです。このツイートに対してもすぐに回答がつきました。

どちらもax3lさんによるnoglobalをupdateしたもので、それぞれraven38さん、yoshiponさんによるものです。オリジナルのax3lさんによるnoglobalに倣って、これらをそれぞれver02ver03とします。

これをjupyter notebook内の適当なセル内で宣言することで、かなり快適になりました。どんな動作になるかについては、後述の実験の項目をみてもらってもいいし、前述の最終的に採択されたコード内の例をみてもらえればと思います。

Pythonコンパイル言語ではないこと、jupyter notebookを使用することでかなりインタラクティブにデータをいじったりコードを書いたりすることができること。これらがとてもメリットを有している反面、変数が乱立して名前空間が汚染されていきます(毎回.pyスクリプトで記述してしまえばいいのですが、なんやかんやで実験的にコードを書いていく時にはjupyterを使用するのが便利ですよね)。そんな中で、こういったものを使用することで、かなり保守的でバグの起こりにくいコードか書けるようになるのではないかなと思います。事実、ボクはそうです。

なお、このver03にはバグがあり、限定的な状況でTypeErrorが発生します。それは、noglobalwrapされる関数の引数にデフォルト値が代入されていない場合、です。これに関しては、その状況時にのみ局所的に対応できるようにif`文を一箇所に追加するだけで対処できたので、そこまで大きな問題ではないかなと思います。が、一応、対処したものはnoglobal::ver03-2_K-PTLとして区別することにします。

破:狭い用例への苦悶

人間なのです。結局、ボクは人間で、面倒くさがりで、サボりたくて。


面倒なのです。

毎回、jupyter notebookにnoglobalの定義、宣言を記述するのが。

スクリプトに書いてそれをimportしてしまえばもっと簡単に使えるようになるんじゃ?!と思うわけですね。importしてやれば別ファイルに書いたコードがインライン展開されていい感じになるんじゃ??と考えました。実行しました。

うまくいきません。

builtins関数が全て使用不可能になりました。

理由は自明で、c++includeと違って、pythonimportではそれをコード内にインライン展開するわけではないからなのですね。これが、非コンパイル言語の壁か。と強く感じました。

同じglobal変数空間でも、今メインで使用しているファイル(実行環境)内でのそれと、importしてくる関数(変数)が置かれている環境でのそれとでは、ものが違うのです。そのため、同様に使用したくてもできない。

これはpythonのバージョンを3.8系に上げたことによる(これまでは3.7系だった)弊害なのか?とinspectの挙動などを調査していましたが解決できず、ぼそっとTwitterでこぼしたところ、またまた登場。まますさんがやってきました。

いやぁ本当に助かった。助かりまくった。ここで紹介されているのは、これまでのnoglobalをさらに発展させた-importで使用できるように改良された-もみじあめさんによるnoglobalです。importできるようになったということで、これはかなりの進化です。これも便宜上、ver04と呼ぶことにしましょう。

あぁ、noglobalがこれでimportを介して使用できるようになりました。これは、先に述べたglobals()が実行環境とimport元のファイルとで異なっていることが問題なので、importした後に実行環境中のglobals()を渡すことで、builtinsに対しても問題なく使用できるようになったものです。

これによって、noglobalは以下のたった2行を記述するだけで実現するようになったのです。素晴らしい。

from noglobal import no_global_variable_decorator
noglobal = no_global_variable_decorator(globals_=globals())

importで気楽に使用できるnoglobalを手に入れた私は、バンバン使いまくります。

jupyterを使用してコードを記述しているときはもちろん、.pyスクリプトで記述し、そのまま実行するときさえもnoglobalをつけて、変にグローバル変数の影響を受けないようにコードを記述していくようになります(実際、本当にコード内で「この変数どこで宣言したっけ?」とか、「既にこの変数名使用したっけ?」とか、そういった問題もなく、かなり各コードの質が上がったように感じます)。

ただ、この使い方をしていると、新たな問題にぶつかりました。またか、と。

ただ、この問題を解決すると同時に、このnoglobalは縛られることのない、自由を手に入れることになるのです。

急:完全体の目醒め 〜 no global, yes python

ぶつかった問題は、ここで書くには些か面倒なのですが、以下のようなフォルダ構成だったとしましょう。

~./
 |- a.py
 |- b.py
 |- noglobal.py

a.py

from noglobal import no_global_variable_decorator
noglobal = no_global_variable_decorator(globals_=globals())

@noglobal()
def hoge():
    print("hoge")

b.py

from noglobal import no_global_variable_decorator
noglobal = no_global_variable_decorator(globals_=globals())
from a import hoge

@noglobal()
def fuga():
    hoge()

こんな状況です。a.py内で宣言された、noglobalでwrapされた関数をb.pyで使用しています。するとどうでしょう。NameErrorが発生します。なんのって?

NameError: name `print` is not defined

builtins関数が使用できなくなりました。オーマイガー。一瞬、一瞬ですが絶望しました。まさかこんなことになるとは。

この問題に関して、実際にコードを走らせて調べてみました。実際の詳細は省略します(気になったら手元で調べてみてください)が、原因はglobals()そのものにありました。

現在実行しようとしているb.py上でのglobals()は、以下のような中身になっています。builtinsは__builtins__として保持され、その中身は となっています。また、importしたnumpyなど、グローバル変数等がそのままjson形式(dict)で羅列されています。これが、b.pyにおけるglobals()の中身です。

{
    '__name__':'__main__', 
    '__doc__': b.pydocstring,
    '__package__': None, 
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001CEE11E3FD0>, 
    '__spec__': None, 
    '__annotations__': {}, 
    '__builtins__': <module 'builtins' (built-in)>, 
    '__file__': 'b.py', 
    '__cached__': None, 
    'np': <module 'numpy' from '~/Python/Python38/site-packages/numpy/__init__.py'>, 
    ...
}

それに対して、b.py実行時のa.pyにおけるglobals()はどうなっているのでしょうか。確認すると、以下のようでした。そうです。builtinsがモジュールではなく、展開された形(つまり、グローバル変数と同じように)保持されているのです。これが、両者の大きな違いです。

{
    '__name__':'__main__', 
    '__doc__': b.pydocstring,
    '__package__': None, 
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001CEE11E3FD0>, 
    '__spec__': ModuleSpec(name='a', loader=<_frozen_importlib_external.SourceFileLoader object at 0x00000224BAC376A0>, 
        origin='~/a.py'), 
    '__annotations__': {}, 
    '__builtins__': {
        '__name__': 'builtins', 
        '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", 
        '__package__': '',
         '__loader__': <class '_frozen_importlib.BuiltinImporter'>, 
         '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), 
         '__build_class__': <built-in function __build_class__>, 
         '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 
         'all': <built-in function all>, 
         'any': <built-in function any>, 
         'ascii': <built-in function ascii>, 
         'bin': <built-in function bin>, 
         'callable': <built-in function callable>, 
         'chr': <built-in function chr>, 
         'compile': <built-in function compile>, 
         'delattr': <built-in function delattr>, 
         'dir': <built-in function dir>, 
         'divmod': <built-in function divmod>, 
         'eval': <built-in function eval>, 
         'exec': <built-in function exec>, 
         ...
    '__file__': 'a.py', 
    '__cached__': None, 
    'np': <module 'numpy' from '~/Python/Python38/site-packages/numpy/__init__.py'>, 
    ...
}

これらの変数らに対して、弾く対象の変数であるかどうかを判定しているnoglobalのコードは、以下の部分です(「最終的に採択されたコード」で示したコードの抜粋です)。globals()内のキーを走査し、それがexceptsに入っているとき(name in excepts)、モジュールであるとき(inspect.ismodule(attr))、呼び出し可能なオブジェクトであるとき(callable(attr))にwrapされた関数内でも使用可能な変数である、と判定しています。つまり、builtinsは実行環境中ではmoduleとして認識されていますが、importされる時にはその中ではmoduleとしてではなく、それぞれが独立した変数として動作していたため、この判定の部分で弾かれてしまったのです。

def need(name, attr):
    if name in excepts:
        return True
    if inspect.ismodule(attr):
        return True
    if callable(attr):
        return True
    return False

if globals_ is None:
    globals_ = globals()
if excepts is None:
    excepts = []
filtered_globals = {
    name: attr
    for name, attr in globals_.items()
    if need(name, attr)
}

というわけで、これに対する処理を愚直に足します。追加するのは、以下の3行だけです。   forを使用しているため、処理としては決して最速なものではないですが、対応としては十分だと思います(ベターな手法があったら、いつでもPRお待ちしています)。

最初のif not inspect.ismodule(globals_["__builtins__"])で、globals()内におけるbuiltinsがモジュールなのか、辞書型なのかを判別します。余談ですが、今回のこのnoglobalと闘う中でinspectというライブラリを知ったのですが、かなりこの子便利ですね。早速、結構活用しています。

余談は置いておいて、もしbuiltinsがモジュールでない場合、これはimportされるpythonスクリプト内であることを意味します。そしてその時、builtins関数は辞書としてglobals()["builtins"]でアクセスできますので、それらに対して要素を走査します。最初のglobals()に対してと同様に、need()関数でbooleanの判定をします。

if not inspect.ismodule(globals_["__builtins__"]):
    for name, attr in globals_["__builtins__"].items():
        if need(name, attr): filtered_globals[name] = attr

これで、importするpythonスクリプト内でnoglobalを使用していたとしても、問題なく運用できるようになりました。これも便宜上、番号で管理しましょう。noglobal::ver05_K-PTLとします。素晴らしいですね。もう欠点が見当たりません。最強です。






、、、とはならないんですね。まだやれることがあります。それは、使うまでのプロセスがまだ一手間多い、という面倒臭さに対する対処です。ちなみに、現在、使用するために必要な記述はこんな感じです。

from noglobal import no_global_variable_decorator
noglobal = no_global_variable_decorator(globals_=globals())

importする対象のスクリプト内でnoglobal()を使用していても、問題なく現在の実行スクリプト内でnoglobal()が機能するのです。ならこれ、逆も成立すると思いませんか?

つまり、importするpythonスクリプト内でnoglobal()をdeclareして、そのnoglobal()自体をimportしても、現在の実行スクリプト内で問題なく動くと思いませんか?

実は、これはできません。というのも、これは全くワケが違うからです。実行中のスクリプトで参照されるglobals()と、import対象となるpythonスクリプト上でのglobals()とでは、参照範囲が異なるglobal symbol tableになっているのです。つまり、実行中のスクリプトでimportしてきた関数やオブジェクトは、callableであろうともimport対象となるスクリプト内のglobal symbol tableからは見えないため、例えばnumpyやpandasと言ったライブラリも使用できなくなってしまいます。とても不便です。

でも、やはりimportだけで運用できるようにならないと、現実的に手間が多く面倒です。

そこで、以下のような革新的なnoglobalを定義します。classとしてnoglobalを保持し、デコレータとして書いた時の引数は__init__へ渡され、wrapされた関数は__call__へと渡される仕組みです。よく思いついた、ボク。褒めて欲しい。すでに記載した関数に関しては、関数名だけの記載としています。

noglobal.py

from functools import partial
import inspect
from typing import List, Dict, Optional, Callable, Any, FunctionType

# Function prototype
global noglobal

def globals_with_module_and_callable(globals_: Optional[Dict[str, Any]] = None,
                                     excepts: Optional[List[str]] = None) -> Dict[str, Any]:

def bind_globals(globals_: Dict[str, Any]) -> Callable:

def no_global_variable_decorator(globals_: Optional[Dict[str, Any]] = None):


# substance of noglobal function
class noglobal:
    def __init__(self, excepts=None):
        self.excepts = excepts
    
    def __call__(self, _func):
        return no_global_variable_decorator(
            globals_=_func.__globals__
          )(excepts=self.excepts # arg of _no_global_variable
          )(func=_func)          # arg of _bind_globals 

a.py

from noglobal import noglobal

glb = "global variable"

@noglobal()
def hoge():
    print("hoge")
    print(glb) <- NameError: name glb is not defined

@noglobal(excepts=["glb"])
def fuga():
    print("fuga")
    print(glb) <- No Error

通常のライブラリ同様、import文だけでnoglobal()が使用できるようになりました。このままb.pyに読み込ませても、問題なく動作します。これが、ボクの中で最新最善のnoglobalです。これをnoglobal::ver05-2とします(使用する関数は基本的に同一なので、ver06とはしませんでしたが、個人的なおすすめは圧倒的にver05-2です。ただ、ver05までの変数名には特別手を加えていないので、これまで使用していたnoglobal.pyを今回の最新のverにそのまま置き換えて、これから新たに使用する時にはver05-2の使用方法にすれば問題なく動作すると思います)。

ただ、現時点で全てのケースに対してテストできた訳ではありません。もしかしたら、正常に動作しない状況が存在するかもしれません。その際には、連絡してくださると幸いです(なんならPRしてください)。

以上をまとめると、今回紹介したいnoglobal::ver05では、従来のnoglobalと比較して以下の点が改善されています。

  • 他のファイルで宣言されたnoglobalをimportで呼び出して使用できるように (ver04での改善点)
  • それを拡張し、importを経由して繋がる全ての他のファイル内でもnoglobalが使用可能に (ver05での改善点)
  • noglobalを使用するための定義が、import文だけで完結するように (ver05-2に限る)


(再掲)


検証

noglobalのversion一覧

ここまで、先人たちのnoglobalを発展させて、最終的に得られたnoglobal::ver05を示しましたが、これまでの全バージョンへのリンクを再掲しておきます。ここからは、各versionの挙動を確認・比較するための簡単な実験です。興味のない方は読まなくても平気です。自分のメモも兼ねていますので、多少殴り書きになりますがご容赦ください。

実験概要

これから、各versionのnoglobalを比較していきます。これらの関数は、importして呼び出されたり、inlineにベタ書きされていてそのまま実行されたりします。実行環境も、python scriptを直接実行するか、jupyter notebookを介するかで分かれます。さらに、wrapに使用するnoglobalも、前述のように複数種類存在します。そのため、実験のパターンとしては結構な数になってしまいます。

そこで、簡潔に比較するために、共通した関数を使用します。その関数にはそれぞれの関数の使用される状況(importされるのかinlineで書かれているのか、どのnoglobalでwrapされているのか、script上で実行するのか、notebook上で実行するのか)が分かるように命名します。

いずれの関数を実行するにしても、想定される結果は以下の3通りです。発生する例外はNameErrorもしくはTypeError以外あり得ないので、それらの例外をキャプチャする関数 run_functionに関数を渡して実行することにします。この関数にはnoglobalを付けないので、グローバル名前空間へ容易にアクセスできる状態のものです。

使用する関数の雛形

def run_function(func):
    try:
        func()
    except NameError as ne:
        print(f"got NameError with {func.__name__}; ")
        print('\t', ne)
    except TypeError as te: # noglobal::ver03 でのみ発生
        print(f"got TypeError with {func.__name__}; ")
        print('\t', te)
    return None

val_global = "This is a global variable" # グローバル変数

@noglobal5() # wrapするnoglobalの種類は、ここでの記述を変えることで切り替えます
def script_import_func5():
    print("This is func5 at script wrapped by import `noglobal5`") # Description of this function ; どういった関数なのか、簡単にその説明を出力します
    val_local = "This is a local variable" # ローカル変数
    print(val_local) # ローカル変数の出力
    print(val_global) # グローバル変数の出力;noglobalが機能すればNameErrorが発生する
    script_import_func4() # 関数をネストする場合はこんな感じに関数内部に記載する

run_function(script_import_func5)

想定される結果

# 1. script_funcがnoglobalでwrapされていない場合、もしくはnoglobalが機能していない場合
>>> run_function(script_func)
This is func5 at script wrapped by import `noglobal5`
This is a local variable
This is a global variable

# 2. script_funcがnoglobalでwrapされていて、正常に動作した場合
>>> run_function(script_func)
This is func5 at script wrapped by import `noglobal5`
This is a local variable
got NameError with script_func; 
         name 'val_global' is not defined

# 3. script_funcがnoglobalでwrapされているが、builtins関数を認識しない場合
>>> run_function(script_func)
got NameError with wrapper; 
         name 'print' is not defined

検証実験のケース一覧

大きく分けて、検証実験は2種類を実施します。

  • noglobal のversionごとの動作の比較

以下のケースに対応するように、関数には以下の雛形に則って命名します。なお、versionの- (ハイフン)は関数名として使用できないので、_ (アンダースコア)に変換します。

{$実行環境}_{$noglobalの宣言方法}_func{$noglobal.__version__}

実行環境 noglobal.__version__ noglobalの宣言方法
notebook {無し, 1, 2, 3, 3-2, 4, 5, 5-2} {import, inline}
script {無し, 1, 2, 3, 3-2, 4, 5, 5-2} {import, inline}
  • noglobalでwrapされた関数をimportして、それをnoglobalでwrapされた関数内部で呼び出す(noglobalがネストされる)場合

以下のケースに対応するように、関数には以下の雛形に則って命名します。なお、versionの- (ハイフン)は関数名として使用できないので、_ (アンダースコア)に変換します。

nest_{$実行環境}_{$noglobalの宣言方法}_func{$noglobal.__version__}

実行環境 noglobal.__version__ noglobalの宣言方法
notebook {4, 5, 5-2} {import}
script {4, 5, 5-2} {import}

実験結果

それぞれの実験に対して、実行結果がどうであったかを「想定される結果」に記載した「1 ~ 3」の番号で示します。実行結果の詳細に関しては、Githubに実験に使用したpyスクリプトとJupyter notebookを上げておりますので、そちらをご参照ください。

各verのnoglobalの比較

各条件での比較結果を一気に表で示します。 f:id:K_PTL:20210225010136p:plain

ネストしない場合には、ver04以降を使用すれば問題ありません。ただし、このnestというのは、numpyなどの有名モジュールも含まれます。もしimport経由でnoglobalを定義する場合には、ver04では正常に動作しないので注意してください(これはいわゆる関数をnestした状態と見做せるので、ここでは実験していません。次の実験結果を参照してください)。

noglobalをネストした場合の挙動

f:id:K_PTL:20210225010202p:plain

ver04からver05にかけて、nestした際の挙動が大きく改善できています。ver05に関しては、ver05だろうとver05-2だろうと、正直非の打ちどころがありません。問題なく使用できることでしょう。

実験・まとめ

各versionmのnoglobbal等を、同一条件でひたすらに比較しました。歴史を追うと、一歩一歩改善されて行っているのが目に見えて、とても興味深かったですね。今回新たに作成したnoglobal::ver05ではimport時にもnestして使用する場合にも問題なく挙動することを確認しました。特に、importを容易にするためにclassを活用したnoglobal::ver05-2は、いずれのversionよりも正常に機能し、かつ導入方法も最も容易であることが改めて示されたことでしょう。

おわりに

ひょんなことから、noglobalの問題点が目についてしまって、もっと良くならないか、もっと、もっと、と試行錯誤をしていたら、最終的に今回のver05-2を完成させることができました。地道に考え続けることを辞めないでよかったと心から思います。

また、冒頭にも述べましたが、雛形のコードは先人等が作り上げてくれたものです。感謝します。

これにて、noglobalをめぐる戦いが、それぞれの中で終結を迎えられんことを祈ります。

次のフェーズで、またお会いしましょうね。



最後にこの曲を聞いてお別れです。

”今日お前が歌った唄は、お前が未来のお前に向けて歌った唄なんだよ”
”未来で辛い時苦しい時、お前あの時あんなに、心込めて、良い声で、生きてる証拠を歌ってたじゃねぇかよって”
- BUMP OF CHICKEN 2019 Aurora Ark Tour Final @東京ドーム MCより

(せーのっ)

\\\ Flare //




ここだけのハナシ、flareをある言語からある言語に翻訳するとCHAMAになるんですよ。BUMPが4人、再び揃う日はいつになるのやら。

またね、ばいばい。

Python高速化・小技メモ

本記事について

 Pythonでの記述に関して、小技をメモしていく。自分用が主たる目的なので、記述に関して詳説はしない。目的と、その手法の2段ツリー構造でまとめていく。

高速化

配列の、指定したindexの要素で構成された新しい配列の作成

何がしたいって、こういうこと。配列dataがあって、それに対応する各行ごとのindexをまとめた配列idxsがある。これらを組み合わせて、新たにidxsに対応するdataの要素で構成された配列expected_outを作成したい。ってこと。

>>> from platform import python_version
>>> print(python_version())
3.7.4

>>> import numpy as np
>>> np.__version__
'1,17,2'
data = np.array(
    [[1, 3, 2, 4, 8],
     [8, 9, 3, 4, 2],
     [7, 8, 1, 2, 3]])
indexes = np.array(
    [[0, 1, 3, 4, 2],
     [1, 1, 2, 3, 4],
     [3, 2, 3, 4, 3]])
expected_out = np.array(
    [[1, 3, 4, 8, 2],
     [9, 9, 3, 4, 2],
     [2, 1, 2, 3, 2]])

np.take_along_axisを使用

[doc] こんな便利な関数があったのね、やるじゃん、numpy。

>>> np.take_along_axis(arr=data, indices=indexes, axis=1) == expected_out
array([[ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True]])

idx + W*y を使用

なんのこっちゃねーんってね。
 これは、index用の配列indexesの各行にそれぞれそれまでの行に含まれる要素数を足す(今回の例だと、5x5の配列なので、0行目には0を、1行目には5を、2行目には10をそれぞれ足す)。それを、対象となるdataを1次元に変換した時の配列data.ravel()のindexとして使用し、元の配列の形状にその後戻す、というやり方。強引だけどパッとこんなやり方思いついたらかっこいいよね。。

>>> idx_ = indexes + (np.arange(0, indexes.shape[0]) * indexes.shape[1])[:, None]
>>> out = data.ravel()[idx_].reshape(data.shape)
>>> out ==  expected_out
array([[ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True]])

実行時間の比較

 timeitモジュールを使用して実行速度の比較。numpyのメソッドであるnp.take_along_axisに軍配。一行で記述できることからも、こっちが実用上は正解の選択肢になりそう。それでも、アイデアとしては後者の方がかっこいいけどなぁ(内部で同じようなことしていたりして)。

>>> from timeit import timeit
>>> LOOP = 100000

>>> result = timeit("np.take_along_axis(arr=data, indices=indexes, axis=1)", globals=globals(), number=LOOP)
>>> print(result / LOOP)
7.96317385000293e-06

>>> def func(org, idx, ans):
>>>     idx_ = idx + (np.arange(0, idx.shape[0]) * idx.shape[1])[:, None]
>>>     ret = org.ravel()[idx_].reshape(data.shape)
>>>     return None
>>> 
>>> result = timeit("func(data, indexes, expected_out)", globals=globals(), number=LOOP)
>>> print(result / LOOP)
9.105247100014822e-06

小技

010101...な配列を生成したい

>>> from platform import python_version
>>> print(python_version())
3.7.4

>>> import numpy as np
>>> np.__version__
'1,17,2'

正攻法だと、

>>> N = 10
>>> a = np.ones(N)
>>> a[::2] = 0
>>> a
array([0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])

>>> b = np.ones(N)
>>> b[1::2] = 0
>>> b
array([1., 0., 1., 0., 1., 0., 1., 0., 1., 0.])

>>> c = np.ones((N, N))
>>> c[::2, ::2] = 0
>>> c[1::2, 1::2] = 0
>>> c
array([[0., 1., 0., 1., 0., 1., 0., 1., 0., 1.],
       [1., 0., 1., 0., 1., 0., 1., 0., 1., 0.],
       [0., 1., 0., 1., 0., 1., 0., 1., 0., 1.],
       [1., 0., 1., 0., 1., 0., 1., 0., 1., 0.],
       [0., 1., 0., 1., 0., 1., 0., 1., 0., 1.],
       [1., 0., 1., 0., 1., 0., 1., 0., 1., 0.],
       [0., 1., 0., 1., 0., 1., 0., 1., 0., 1.],
       [1., 0., 1., 0., 1., 0., 1., 0., 1., 0.],
       [0., 1., 0., 1., 0., 1., 0., 1., 0., 1.],
       [1., 0., 1., 0., 1., 0., 1., 0., 1., 0.]])

ちょっと洒落たことやる。np.indicesを使用する。

>>> d = np.indices((N,N))
>>> d.shape
(2, 10, 10)
>>> d
array([[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
        [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
        [4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
        [5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
        [6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
        [7, 7, 7, 7, 7, 7, 7, 7, 7, 7],
        [8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
        [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]],

       [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]])

>>> np.sum(d, axis=0) % 2
array([[0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
       [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
       [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
       [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
       [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
       [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
       [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
       [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
       [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
       [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]])

# 1 dimensional
>>> (np.sum(np.indices((N,1)), axis=0) % 2).ravel()
array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])

>>> (np.sum(np.indices((N,1)), axis=0) % 2).ravel() ^ 1
array([1, 0, 1, 0, 1, 0, 1, 0, 1, 0])

2020年輪読会@DLGを振り返る〜良い輪読会の条件は?

はじめに

 みなさまどうもこんにちはこんばんはおはようございます、データラーニングギルド Advent Calendar 2020の9日目担当のKです(ギルドではKと名乗らせてもらっています、なんとあのPTPのVocalと同じ名前!BOCの2ndアルバムLiving deadに収録されている名曲と同じ名前!ブラッディマンデイにも出てきた?ポケモンにも出てきた?そんな奇跡に満ち満ちた名前です)。

 ずっっっっっっとここ数ヶ月はひたすらにinputしていたので、こうやって久しぶりにoutputする機会が得られたのは嬉しいですね。いい機会です。さて、私はネットで活動する際には本名や所属している企業やコミュニティに関しては基本的には口にしないため、所属しているデータラーニングギルド(以下、ギルド or DLG)でももちろん本名も所属している企業に関しても公表していません。そもそも、私がDLGに所属している、ということさえもリアルの友達や身内でも知っている人は何人いるんだろうか?パッと思い出せるのは1人だけですね。なのでまず、簡単にDLGについてと、なぜ私がDLGに入ったのかについてお話しようと思います。

忙しい方向け

  • 輪読会はしっかり参加しないと分かったつもりで終わるぞ、何も手に入りゃしねぇぞ
  • 輪読会を実施する意義
    • 積本消化
    • モチベーションの維持
    • 議論による理解の促進
    • 一人では時間的に厳しいレベルまで深堀できる
    • 実際に使用する際の使い方を実際に使用している人から聴ける
  • こんな輪読会はやめとけ
    • 企画時点で、章や範囲の分担決めが完了していない
    • 発表者の準備が不十分で本に書いてあったことを読むだけ
    • 参加者が受け身で質問すらしない空気的存在
  • 社会人ならエクストリーム輪読会をやれ
    • 2時間程度で資料を作成、質疑応答まで含めて発表は30分で終わり
    • 担当するテーマは1つだけで良い(極端な話、数式1つでも良い、とにかく深堀しろ)
    • 毎週 or 隔週、コンスタントに実施して習慣化しろ

データラーニングギルドについて

 (完全にボクから見た解釈・主観増し増しで紹介します。村上さんに怒られちゃうかもデータラーニングギルドは、名前の通りデータを取り扱っている人間が集う場です。ギルドって響きかっこいいですよね。幼き頃、FAIRY TAILの魔道士ギルドとか憧れましたよね。データのインフラに関わっている人から、データサイエンティストとして活動している人、フリーランスでマルチに活動している人までいろいろいます。もちろんボクのようにデータサイエンスをほとんど使用しない職に就いている人もいます。そして何より、皆が習学中です。DLGがそこらにあふれているコミュニティと決定的に違うのは、有料であることです。所属している人たちは、お金を払ってでも学び、成長することを望んでいるのですね。もしかしたら、お金を払うんだから成長させてくれよ、金使えば成長できるに決まってるだろ、なんて馬鹿げた思想を持っている愚か者もいるかもしれませんがゲフンゲフン、冬はやはり乾燥がひどいですね。喉の調子が少しおかしいようです。

なぜDLGに入ったのか

 前述の通り、ボクはデータ分析に従事している人間ではありません。ではなぜ、ギルドに入っているのでしょう。簡単にいうと、同志を得るためです。何か勉強会をしたくなったら、人を集める必要があります。ギルドでもコミュニティでもなんでもいいですが、そういった団体に所属していない場合、特に勉強をともにしてくれる人を探すというのは、非常に困難なことです。Twitter等で呼び掛けてもいいですが、そこに責任は生じません(実際、Twitterで呼び掛けられたことをきっかけにいくつかの勉強のコミュニティに所属していますが、最初はドカドカ任ずが集まるものの、最終的には活動しているのは限られた人のみで、ほとんどの人は活動しなくなる(活動しているにしても、あくまで情報を受信するばかりで発信しなくなる)というのをいくつも見てきました)。

 お金を払って、勉強に対して強い思いを持っている人たちと一緒に活動することは、自身を机に張り付けるために非常に有効です。周りがやっていれば自分もやらなくては、となるし、自分の担当が回ってきたらやらなければ他の人に迷惑をかけてしまう。根っからのサボり魔であるボクには、お金を払う以上に効果的でした。いくつか活動はありましたが、ここではタイトルにもあるように輪読会についてとりあげようと思います。輪読会の実施こそ、ボクがギルドに所属した大きな理由の一つだからです。ギルドに参加するまでは輪読会といふものをやったことがなかったボクが、今になって感じていることです。ただし一点、補足を。ここから記載する内容は、かなりボクの主観が入っているため一般的ではない可能性が高いこと、そして自分への戒めも込めて書き殴っています。「あぁ、こういった人間もこの地球上には生息しているのだ」と、思い出してくれるきっかけになれたなら光栄です。また、これから輪読会を開催しようとしている方へ、こんな輪読会はやっていけないのだ、と反面教師に企画してもらえれば光栄です。

輪読会とは

 輪読会の歴史は実は古く、江戸時代に全国の私塾、藩校で広がった読書会=解読での経験とそこで培われた精神が明治維新を準備したのだと知られています。輪読会は開催こそ困難であるが、一度完遂してしまえばそれは輪読会が終わってもなお自身の周囲に知的共同体が形成され続けることの他にありません。

と、まぁ、ここまで知的にカッコよく決めてみましたが、所詮は読書猿さんの独学大全の引用に過ぎません。これ以上は無理です。ボロが出ます。なので自分の言葉で話すことにします(この本は厚さに反して内容の読みやすさ故にかなりリズムよく読めます、とても好きな本です)。

 輪読会とは、読んで字の如く『輪になって読む会』ですね。噛み砕けば、同一の書物を複数人で読むこと、と認識しています。似た言葉に読書会などがありますが、輪読会は『参加している個人個人が担当するセクションを有し、皆にその部分の内容を理解できるように紹介、議論をする』というものだと解釈しています。読書会は、(正直よくわからないのですが)読んで感想を言い合って議論・理解を深めるという行為かと認識しています。責任と難易度が、輪読の方が上だと考えています。輪読会は、大学のゼミや研究室なんかでやられたりもしますね。学生であればまだ実施のハードルは社会人よりも圧倒的に低いかもしれません。

 一つ保険を張らせてもらいますが、読書会を否定するわけではありません。ただ、『専門書ではやるなら輪読会』だと考えています。なぜか。専門書(ここでは、大学水準以上の学術的知見が記された書物を想定しています)は、みんなで読んでも意味がないからです。それなら一人で読みましょう。専門書はその難易度故、みんなで文脈や行間を読み解いていく必要があるため、しっかりと準備する、責任を持って調べたり勉強をする輪読会が望ましいと考えています。ここが、ボクがギルドに入ってまで輪読会を実施したかった理由の一つですね。

"輪読会"の沼

 小難しい専門書をみんなで協力してやり切って、さぞ輪読会は気持ちの良いものなのでしょう。達成感もあり、やり切った自信もつくのでしょう。最初に、注意するべきポイントについて書いておきます。

  • ネット上(Speaker Deck等)には、「○○第△章輪読会資料」というものが多く落ちていますが、本の内容を追っているだけのものがあまりに多い。何かあなたの中に残りましたか?本当に"理解"できましたか?それが"理解"した人の作る資料なのですか?
  • 誰かが発表しているのを聞いていても得られるのは分かったつもりという虚構に満ち満ちた自信だけですよ
  • 自分で読んで大部分を理解できないのなら、まだその本に手を出す段階ではないのだと思いますよ、その段階で周りに理解を求めても得られるのは(略
  • こんなことを言っているけども、分からないことを知ろうと全力で頑張るあなたは素敵だし、きっと報われますよ(そっと自分に言い聞かせる)

輪読会を実施する意義

 さて、では輪読をする意味はどこにあるのでしょうか。難しい書籍をみんなで協力してより理解するためにやるのでしょうか。いくつか、意義を列挙していこうと思います。もちろんこれ以外にもあることでしょう、逆にこれは私には該当しないよ、というのもあることでしょう。それはそれで構いません。それが主義というものでしょう。ちなみに、順番に意味はありません。


  • 積本消化
  • モチベーションの維持
  • 理解の促進
  • 深堀する
  • 実際に使用する際の使い方

 さて。一つ目に『モチベーションの維持』を挙げました。これはそのままですね。輪読会の他のメンバがやっているから自分もやる。置いていかれる。自分が担当の時に進捗が遅ければ、他のメンバに迷惑をかける(猛烈反省中)。やらなければいけない、と強く感じることに意義があります。

 次に、『積本消化』はかなり大きい理由になると思います。専門書はどれも大抵は難易度は高く、数ページで数時間かかるなんてザラにあります(この部分は、実際に数学や物理の専門書を手に取ったことがない方には大袈裟と感じてしまうかもしれませんが、今私が勉強している数学の本は1時間で2ページくらいしか進んでいません。1週間で50ページやる予定だったのに)そのため、一つの本をやりきるには、膨大な時間を要するのです。さらには、一つの単元を一冊の本で完了させようというのはかなり危険です。書籍に記載されている情報は既に古い考え方かもしれない(歴史を学ぶことも大切ですが)。はたまた、書籍には、筆者の主義主張がふんだんに盛り込まれているからです。ある本では流していることが、他の書籍ではとても重要とされているかもしれない。筆者のバックグラウンド(専門分野や業界等)に強く依存することでしょう。すなわち、何かを書籍ベースで勉強するならば、複数冊の本に手を出す必要があるのです。とてもじゃないけど独学で進めるにはやり切れないかもしれません。そこで、例えば全部で5章構成の専門書を5人で輪読するとした場合、どうでしょう。一人一章を全力で勉強し、それを他の輪読メンバへ共有すれば個人の勉強の労力はかなり減ります。加えて、メンバと議論することで自分の勘違いを是正できるかもしれない、自分が疑問に思わなかったことでも実は本質的に理解できていないと思い知らされるかもしれない。理論的には、5倍速以上のスピードで専門書の海を駆け巡ることが可能かもしれません。まぁ、現実はこんなにうまくいくことはそうそう無いのですが。。。

 『理解の促進』ですが、これは1つ前の最後の方に記載した、内容についてメンバと議論する、ということに加えて、他のメンバに理解してもらうために準備するということがかなり効果的に効いてくることでしょう。議論に関しては言わずもがなですね。肝心なのは準備の方で、分かったつもりの状態では良い資料を用意することなんてできません。実際、(言葉は悪いですが)大した資料じゃ無いな(ここでは、本をなぞっているだけで何の追加の情報もない、などあくまで内容についてのみ言及させてね)と感じる輪読資料は、いざ発表になると、なるほどこの程度の理解ではこの程度の資料しか作れないわな、と感じることがあります。参加の意義のある輪読会を実施するためには、少なくとも発表者/担当者は担当の箇所をしっかり理解するよう努める必要があり、それゆえに準備することは必然として深い理解を要求されることになります。逆に言えば、努力しても理解できない、説明できないのであれば、その書籍はあなたにとってはまだ時期尚早なのでしょう。もう一つ基礎に立ち返ることを推奨します。

 『深堀する』も、理解の促進に関連しますが、全てを理論等深掘りするととてもじゃないけど時間が足りません。そこで、セクションを分担していることのメリットが出てきます。自分が深堀するのは自分の担当しているセクションだけにしておき、他のセクションの深堀を他人に委ねることで、効率的に深く知見を得ることができます。そこに付随して気になることが出てきたら、そこだけ自分でさらに深堀して調べれば良いのです。効率的ィ!!

 最後に、『実際に使用する際の使い方』を挙げました。個人的には、これはギルドで輪読会をするに際してのとても面白いと思っている点で、学生にはなかなか難しい、社会人の特権ではないでしょうか。ギルドには、実際にデータ分析に携わっている人がいます。統計や効果検証、統計モデルの選択など、理論は書籍から学ぶことができます。知ることができます。ただし、実際の現場ではどの程度使われるのか、デファクトスタンダードは本当にこれなのかなどといった知見は、実際に業務に従事している人からしか得ることができません。輪読会で理論について深く理解することに努めつつ、同時に実務ではどんな感じに使用されるのかについてまで知ることができる。イメージができたら、理解の速度は飛躍的に向上することが期待されますね。ボールを投げるという単純に感じる動作一つでさえ、ボールを持っていない腕で壁を作って足は投げたい方向へ向けて胸は開かないで肘は下げずに肩と肘を柔らかく鞭のようにしならせて体よりも前で地面に叩きつけるようにそしてギリギリまで離さずに人差し指と中指でスピンをかける、なんて言われてもまともな投げれません。実際にこんな感じだよ、ここでこう力を入れるのがポイントなんだ、と有識者の知見があった方がいいというのは理解に容易いことでしょう。

意味のある輪読会のために

 ここまでで、いくつか輪読会の意義を挙げました。では、これらの意義を身を持って感じるには、輪読会はどうあるべきなのでしょう。企画の仕方、および発表者と参加者のそれぞれの視点から、簡単に整理します。


  • 企画
    • 本はどんな決め方でも構わない(個人的に思う向いている本/向いていない本については後述)
    • 本は参加者全員購入せよ(著作権には気を付けろ、我々が書籍を覗き見ている時、書籍もまた我々を覗いているのだ)
    • 本の分割とそれぞれの担当者は企画時点で決定せよ(これがすごく大事、ここが抜けるとグダグダになります)
    • 可能であれば、日程まで決めよ(ここまでできると、本当にグダグダせず最後まで走り抜けられます、保証します)
    • 日程は、1ヶ月も空かないようにする(伸びるとどんどんグダグダしていきます)
  • 発表者
    • 準備は全力でする(発表者がサボったらその瞬間輪読会の質は落ちる)
    • 理解できていない点を隠さない(議論の対象になる)
    • 深堀する(本の内容を追うだけなら個人輪読でもいい)
    • 輪読会前に自分が作成した資料に目を通せ(何だっけとかほざくなそんな保険いらん)
  • 参加者
    • 問題意識を持って参加する(今回の輪読会で自分が知りたいのは何なのか?参加する目的はあるか?)
    • 必ず質問する(議論を起こせ、貴殿は本当に理解できているのか?)
    • 自分の担当じゃなくても必ず読んで理解しようと努めよ(得るものが減るぞ)
    • 資料がショボかったら質問は諦めろ(これは仕方ない)
    • 議論には自分も参加しろ(傍観者になるな、議論に揉まれないと定着効果はないぞ)

さて、思いついたものをつらつらと書きましたが、どんどん出てきます。ここらでやめておきます。企画に関しては、最初に担当者と担当箇所を割り振ることが大事です。本当に大事です。本当なんだってば!!!!信じなさいよ(逆ギレ)!!!!!!

(反省中)

本当に大事なんです。ここでメンバーに割り振ることで、今後の見通しが立てやすくなるだけでなく、メンバーが途中で輪読会を離脱してしまうことを予防することにも繋がります。実際、ギルドで最初に実施された輪読会(はじめてのパターン認識)では、最初は何人かいたのですが、途中からたった3人になってしまいました。3人でも輪読会としては問題なく進めることができていますが、やはり離脱してしまった人が多かったというのは何となく脱力感に襲われる上、個人の負担がかなり大きくなります。そして本書はゴリゴリの数式だらけの本であり、行間もスパースでかなり読者には優しくない本です。こういった本こそ、複数人で分担して、一人一人が一部分を全力で掘り下げるという輪読会が望ましいのですが。。。残念です。。。

 発表者と参加者が努めるべき事項に関しては、どれが重要とは言えず、どれも重要です。かっこの中にボクの気持ちの一部を書き連ねておきましたので、そちらを優しい目で見ていただければ理由は分かっていただけることでしょう。いずれにしても、主体性なく、学ぼうとしなければ何も得られません。そして、少し皆が軽視していると感じるのは、発表者が分かっていない点を隠してしまうことです。分からないなら、投げ掛ければ良いのです。正直ボク自身これはできていませんが、すごく大事なことだと感じています。

 そして、輪読会は発表者の準備は当然大事なのですが、参加者の質問がなければ基本的に議論が起こりにくいです。人前でボケて、白けてしまったとして、それはボケがつまらないことがもちろん最たる原因ですが、周りの人が反応してあげないことがもっと大きな原因なのです。「つまんな。」とでも反応してやれば良いのに、しないから空気が死ぬんです(突如脳裏を巡る懐かしい景色)。輪読会も一緒です。周りが反応しなければ、何も生まれず終わります。実際、質問してくれたり反応をくれる方と輪読会をやると、その輪読会は非常に有意義なものになることがボクの経験上、保証されています。逆も然りです。

輪読に向いている本と向いていない本

これまで、ギルドで実施してきた書籍を紹介します。結構有名な本が多数入っているので、見たことがあるものもいくつかあるかもですね。それぞれの書籍名の頭に、【☆○△】のいずれかの記号を付与しています。それぞれ、☆:オススメ、○:良い感じ、△:あまりお勧めしないということを表現しています。

 ☆に相当するものは、理論と実用例のバランスが良く、理解もしやすい良書だとボクが感じているものでもあります。また、先に述べたとおりギルドには実務で使用している方が在籍しているため、本の内容と実務での使用時の乖離など、より深い話を聞くことができた点も評価しています。
 ○に相当するものは、輪読には問題なく使用できる、という点を評価しています。輪読会の内容は実施するあなた方次第ということにはなりますが、僕らが実施した際には一人だとやらないな、という領域まで勉強の範囲を広げることができたことがとても良かったです。
 △に相当するものは、残念ながら輪読には向かない書籍です。今回は一つだけ取り上げることになましたが、こちらの本は「Pythonで時系列解析ができる本」という理由で選定されました。しかし、実際にはコードの例に多くページが割かれてしまっていたり、それによって理論的な内容は薄かったりとあまり輪読向けではなかったです。もう少し理論よりな方が、輪読には向いていると思われます。

 これらから、ボクは輪読会に向いている書籍の特徴は、 - 理論がしっかり載っている(行間を省かれすぎていない) - 実務に則した内容(実務だったらどうするか、を議論できる内容) - コード例の有無は影響ない(むしろコードの存在によって理論が削られるのであればそれは害悪) - 深堀したりコメントすることができる人が輪読メンバーにいる本(みんながど素人だとそれはそれで大変ですよね、、)

辺りであると考えています。まだまだN数が少ないので、これからこの条件はどんどんアップデートされるのではと思います。

新しい輪読の形;エクストリーム輪読会

 もうすぐギルド内で輪読会を始めてから1年が経とうとしていますが、ごく最近、ギルドの中では新しい輪読会の形が生まれました。それが通称"エクストリーム輪読会"です。エクストリームって何やねん。いやまじで。

 さて、輪読会の問題点は何だったのでしょう。輪読会の実施の最大の障壁になっているのは、他でもない本職です。どうしようもありません。輪読会で飯は食っていけません。なぜ障壁になっているのでしょう。前述の通り、発表者は相応に準備することが要求されます。そしてその準備というのは、仕事が終わった後や休日の、個人の時間を消費して行われます。仕事後や休日の過ごし方は人それぞれだし、本職を疎かにしてしまっては元も子もありません。つまるところ、圧倒的に時間不足なのです。

 このエクストリーム輪読会は、その問題の解決策を提供します(突然の論文口調)。実施方法は、以下の通りです。

  • 輪読担当者は15分で発表が終わる程度の担当範囲を決定する
  • テーマは一つで良い(どちらかというと一つを深堀するカタチ)
  • 資料を1〜2時間で作成する
  • 発表は15分、質疑応答15分程度の計30分
  • 毎週 or 隔週開催

すごく単純でしょう。まだ3回分しか実施されていませんが、準備に時間がかからないことがとてもメリットになっています。やってみればそのメリットは強く感じられると思います。とても気楽に実施できます。テーマは一つで良い、と記載しましたが、何ならこれは一つの公式等を導出するのに15分使用する(つまり、書籍の中の担当箇所は数式一本のみ)でも良いのです。あくまで、テーマは小さく、中身を深くというコンセプトです。一回一回の実施に要する時間的コストを最小化しつつ、毎週or隔週で実施することでその分野について勉強することを強制的に習慣づけることもできます。良いことだらけです。

 このエクストリーム輪読会はまだまだ走り出したばかりで、これから問題点が見えてくるかもしれません。ただ、社会人の、時間がない人らの輪読会はこれくらい軽い気持ちで実施できるものであることが、一番の最適解である、というように今は感じています。

結び

 今考えても、輪読書を決定するよりも、資料を作るよりも、発表するよりも、一番大変なのは輪読会に参加するメンツ集めです。これは間違いありません。ギルドにいることで、メンツを揃えることがとても容易になっていることを実感します。2020年、コロナで外に出れず新しい出会いは少なかったはずなのに、ギルドを通してたくさんの知的共同体が得られたことが誇らしい、そんな一年でした。

 まだまだ邁進します、ともに明日に向かって、藻掻いて生きて行こうね、負けねーからな、オラオラ(๑╹ω╹๑ )



最後にこの曲を聞いてお別れです。

”今日お前が歌った唄は、お前が未来のお前に向けて歌った唄なんだよ”
”未来で辛い時苦しい時、お前あの時あんなに、心込めて、良い声で、生きてる証拠を歌ってたじゃねぇかよって”
- BUMP OF CHICKEN 2019 Aurora Ark Tour Final @東京ドーム MCより

(せーのっ)

\\\ スノスマイル //

BUMP OF CHICKEN「スノースマイル」

Sports Analyst Meetup #8参加しました

Sport Analyst Meetupって?

 Sport Analyst Meetupは、「現役スポーツアナリストとスポーツ分析に興味のある方々で情報共有をする」ことを掲げたMeetupです。Meetupの目的は、リンク先から抜粋すると、

  • スポーツアナリストおよびスポーツデータ分析に興味のある方に向けたイベントです。
  • 本イベントはスポーツのジャンルを問わずスポーツアナリスト(を目指す人)にとって有益な情報共有の場になることを目的としています。

である。発表者は、本職のアナリストでもいいし、アカデミアでもいいし、趣味の一環でスクレイピングして分析したらこういった知見が得られましたよーって人もおk。いろいろなスポーツに関する知見を共有できればいいなっていう意向らしいので、みなさま是非是非。
 そして、今回参加するのは第8回のSport Analyst Meetup #8です。発表を聞きながらダダダダッとこの記事を執筆しているので、間違えているところがあったら教えてください。。。

前回(Sport Analyst Meetup #7)に参加した時の記録はこちらです。

k-ptl.hatenablog.com

なお、Sport Analyst Meetupに関する参加者の声などはTwitterでは #spoana のハッシュタグで呟かれています。

https://twitter.com/search?q=%23spoana&src=recent_search_click

タイムスケジュール

No. 競技 タイトル 発表者
1 サッカー サッカーxポーカー gshirato
2 全体 スポーツアナリティクスのオリジナリティを考える kanakei
3 いろいろ 現場で近いうちに必要になるであろうスキル K_shoppi
4 サッカー・バスケ 集団スポーツの軌道予測 Keisuke Fujii
5 バスケットボール(Bリーグ) Bリーグにおける勝敗とアリーナ集客分析 hikarut
6 野球 予想外に変化する投球の軌道は打者に学習されやすい?~Predictive Codingと予測誤差を添えて~ nowism_sports
7 サッカー 無観客試合におけるホームアドバンテージ konakalab
8 サッカー・卓球 個人スポーツ、団体スポーツにおけるアスリートのパフォーマンスのレーティングについて MasashiW
9 テニス サーブ制球力の定量的評価 AkinohiAkatsuki
10 フィギュアスケート 回転不足判定選手権をやってみた! hironowa_ru

はじめに

  • 司会進行はu++さんtsuyuponさん
  • 使用しているツールは相変わらずPythonがぶっちぎりで多い(159名)、次点でExcel(41名)、R(20名)
  • スポーツとしてはサッカー、野球、バスケ、テニス、、、
  • LT10本で今回はもりもり

LT

#1 (サッカー) - サッカーxポーカー by gshirato

  • 都合によりビデオでの発表
  • テキサスポールデムでも場面場面での判断(意思決定)が非常に大切
  • サッカーの不確実性:足でボールを扱うこと、選手の振る舞い
  • ポーカーの不確実性;不確実情報ゲーム(相手のカードが不明)
  • 情報を収集し(相手から引き出し)、こちらが有利になるように(意思決定による確率(期待値)の最大化)

#2 (全体) - スポーツアナリティクスのオリジナリティを考える by kanakei

  • 「スポーツアナリストの機能を因数分解し、スポーツという集落から問い出すことで、新たな価値を創造できるのではないだろうか」 by JSAA
    • これを紐解いていく
  • スポーツアナリティクスのオリジナリティとは?
    • 課題の当事者にとって科学(サイエンス)であること
      • ゲーム構造やパフォーマンス結果・遂行などの課題対象に対して、課題の当事者(これらについてはGMや監督、スタッフなど)がいる、それらに対してアナリスト(ここではDS、戦術コーチ)がいて、彼らが持つ科学的専門性(統計モデリング)などを用いてアナライズしてくれることを期待している
    • アナリストにとって芸術(アート)であること
      • する人々の課題
        • うまくならない、勝てない
      • みる人々の課題
        • わからない、つまらない
      • 共通の背景
        • 一人一人が正解が違う -> 芸術
        • 共通の正解がある -> 科学
    • これら2つを併せ持つことがスポーツアナリティクスだが、科学に注目されがち(アート思考が認識されにくい)
  • つまり、スポーツアナリストは広く知識(専門性)を有し、広くアナライズすることが大切で、それが最初のJSAAの言論につながる
  • HiVEという24時間の、スポーツアナリストを世に輩出するためのイベントが実施される(こちら)
  • アート領域と融合することで個人に強く訴えることができる新しいフィールドが開ける可能性がある

#3 (いろいろ) - 現場で近いうちに必要になるであろうスキル by K_shoppi

  • スポーツアナリストの現状と展望について
  • 現状について
    • 現場でのアナリスト業務 = データ分析ではなく総合格闘技(DSのように強い分析ができているわけではない)
    • いろいろなツールを使用する必要がある
    • データ分析については、データ集計がほとんどであることが多い
    • 映像分析・制作も、すごいものというよりはスピード感が大事(明日のミーティングで使用する)
    • 良くも悪くも器用貧乏(転身クラスがいれば世界獲れる)
    • アカデミックからコーチになった人が少ない
    • 現場主導ではこれらは変わることは難しい
  • 展望
    • スポーツアナリストにはそもそも理系卒が少ない
    • コーチング寄りになっている、給料が高くはない(理系職よりは低い)
    • DSになれる素質を持っているスポーツアナリストは少ない
    • DSは内部組織で保有するというよりは外部から誘致することになる方が現実的
    • 当面、データ分析能力はそこまで必要ではないが、データを蓄積する技術に関してはすごく重要(神エクセルや方眼紙エクセルが横行、、、キッツ、、、)
    • やがて来るであろうDSを受け入れるためのデータ基盤作りが、現状のスポーツアナリストに求められること
  • スポーツアナリストは、競技者(現場)とDSをつなぐ役割になる可能性
  • 教義への理解が求められる一方で、優秀なDSがデータ分析を蹴ってまで映像分析などをやるか?という二面性を有する課題
  • 日本にも海外みたいに転換点があるか?? -> 厳しい、まずは誰かが日本で研究の有効性を現場で示していく必要がある、GMがデータ分析屋さんを連れてきて、それが偶然結果につながるとそれがブレイクスルーになって流行る可能性がある

shimpeimiura.tokyo

#4 (サッカー・バスケ) - 集団スポーツの軌道予測 by Keisuke Fujii

  • 前回(#7)も発表されていた
  • 選手の動きをどのように予測するのか
  • 長期予測のためにはRNNのようなNNが有効であると言われている(隠れ状態の更新)
  • GraphNNやGANなども研究されているが、今回は模倣学習に基づく長期予測に関する研究
  • 1~3秒の軌道から~10秒の起動を予測する
  • 各選手に1つのモデルを充てることが多い
  • RNNだけでは長期予測ができない(並びまで考慮されないので役割に関する情報が欠落する、1stepごとなので誤差が蓄積される)
  • 誤差は速度ではわかり難いので、位置のズレを用いる
    • Gaussian HMMを用いて役割割当
    • DAgerrを用いて予測範囲を徐々に伸ばす
  • 予測誤差だけでなくて守備指標([守備のボール(ゴールまでの軌道)までの距離]/[ボールのゴールまでの距離])も導入することで予測性能を向上させる
  • さらに発展的な、長期予測の性能向上のために
    • RNNの改善;変分RNN
    • 目標(弱教師情報)の利用 など

#5 (バスケットボール(Bリーグ)) - Bリーグにおける勝敗とアリーナ集客分析 by hikarut

  • スポーツは勝ってこそのビジネスなのではないか?負けても集客できる仕組みこそがスポーツビジネスなのか?
  • 仮説:勝てるチームの方が集客が多い
  • 単純にPearsonの相関係数では相関は非常に弱め
  • 集客数は場所に依存している?
  • スター選手が入ると集客が伸びる(話題性の重要性;スター性)
  • 対戦相手の影響は? -> 対戦相手が人気(集客力が強い)時にはやはり伸びる傾向にある
  • Bリーグでの復業のきっかけは、、求人サイトに出ていた、、笑 運強い、、、

#6 (野球) - 予想外に変化する投球の軌道は打者に学習されやすい?~Predictive Codingと予測誤差を添えて~ by nowism_sports

  • 前回(#7)も発表されていたなういずさんの発表。今回も野球について。
  • Predictive Codingは現在、脳科学でも注目されている
  • イレギュラーバウンドに対する反応は、脳科学だと選択反応時間(Choice reaction time)が関係しそう(chat内で Hajime さんに回答いただきました)
  • 先発投手で学習した後にリリーフ投手と戦うことになるので、先発投手よりもリリーフ投手の方が特異球を持つ投手が多い可能性もありそう
  • 特異球は後半(4回以降)で攻略されがちなら、それを4回まで伏せることで長期リリーフが可能になるかも?

#7 (サッカー) - 無観客試合におけるホームアドバンテージ by konakalab

  • (#2)(#4)でも発表している
  • 世界120のリーグの結果を分析(基礎統計)するとビッグリーグであるかどうかにかかわらずホームアドバンテージがあることが確認される
  • 日本は比較的ホームアドバンテージがマイルド
  • Crowd Effect(観客)が大きく明確で、これが支配的であると信じているファンが多い
  • 今、無観客試合が増えたため、この観客数がホーム云々に関係するかどうかが分析できるようになった
  • 対比較法、詳細はスライド参照、面白い
  • 無観客試合が多いほど、Wilcoxonの順位和検定での差に優位性が確認される、つまり観客の有無がホームアドバンテージに影響があると言える(特にスペインとドイツでは観客の影響が大きい)
  • ホームアドバンテージがある、と思い込んだ状態で試合に取り組むと実際にホームアドバンテージが発揮される、という研究成果もある
  • 観客の声援は審判に判断(認知科学)に影響を及ぼしている可能性

#8 (サッカー・卓球) - 個人スポーツ、団体スポーツにおけるアスリートのパフォーマンスのレーティングについて by MasashiW

  • Fand!という会社が有している情報を使用しての分析
  • アスリートの活躍度をレーティングする(どの舞台で、どの相手に、どんな勝ち方)
  • プレーごとにポイントとゲインを設定し、EFFを計算している(計算公式は企業秘密)
  • オフサイドラインの管理など、定量化されていない影の活躍(オフザボールの動き)も測定することは可能(ツールが発展してきている)だけど、影の活躍の関しては点数とかと違って速報性は減ってしまっている(もちろん入れた方がいいが、難しい、そのプレーに対するスコアの付け方もポジションによってどうなのか;バロンドールがオフェンスにばかり与えられている現状などもある)
  • B2Cとしてのビジネスなので、EFFはわかりやすい式にすることが求められている

#9 (テニス)- サーブ制球力の定量的評価 by AkinohiAkatsuki

  • ライン側に集めたとしてもそれが得点に直結しないのであればそのコントロールに対する良し悪しの評価はそれだけでいいのか?
    • よりサーバー側に有利になるようにサーブを集めることができる能力がコントロールの良し悪しを評価する指標になりうる
  • サーブの着弾点に関するデータは座標スクレイピングで収集(きつい)
  • SVMでサーブの結果をWon/Lostの2値分類、境界検出(右vs右、ハードコート
  • バック側の方が得点率が高くなる傾向がある
  • フェデラーはやはりコントロールがいいし、錦織は(コントロールが悪いと言われているが)確かにコントロールが良くはない(worstランキングのtop10)
  • ラオニッチのコントロールが世界一である可能性(ほへ〜)

#10 (フィギュアスケート) - 回転不足判定選手権をやってみた!by hironowa_ru

docs.google.com

  • 第一回(#1)でも発表している
  • 回転不足は二段階で定義されている(軽度:1/4回転以上・1/2回転未満不測の場合、重度:1/2回転以上不測の場合)
  • 回転不足は基礎点・出来栄え点の両方が減点される
  • 回転不足の動画比較は素人目には全くわからない
  • わからない
  • わからないのさ。。。
  • 重度回転不足だと基礎点は1ランク下になるし、さらに出来合え点が大きく原点になるので、最初から1ランク下の技をやった方が得点が大きくなるというトリック
  • 回転不足判定選手権〜〜〜!!!
  • 実際の競技者でも判定は容易ではない、、
  • 認識のすり合わせが非常に難しい、、
  • 検定を繰り返し使用する場合にはαエラーも考えましょう
  • !?!? 回転不足に公式の定義がないの!?!? 爪先なのか、とかとか

終わりに

相変わらず面白いし、何かやってみたいし、副業を探してみることにしました。スポーツを科学するって面白いなぁ。



最後にこの曲を聞いてお別れです。

がんばーれってー言ってーやるー!!
でっかい声でー言ってやるー!!
聞こえるか? がんばれーーっっ!!!(くーりーやまっ!くーりーやまっ!)
(埼玉西武ライオンズ)

(せーのっ)

\\\ 人にやさしく //

人にやさしく - THE BLUE HEARTS '87 9 27

効果検証入門 Day03 - 回帰分析

はじめに

 こちらは、効果検証入門の備忘録Day03です。
 前回記事はこちらからどうぞ。
k-ptl.hatenablog.com

k-ptl.hatenablog.com

 また、使用している書籍は、こちらの『効果検証入門』です。

 前回の更新からだいぶ時間が経ってしまいましたが、今回は効果検証入門第2章 回帰分析が対象です。

 回帰分析は非常に多くのシーンで(それも無意識に、特別勉強せずとも自然に)使われている分析手法であり、2つ以上のパラメータの関係を見たい時には真っ先に使用される分析手法だと思います。それも踏まえて、回帰分析とは何か、何ができるのか、単純ゆえ何に気をつけなければならないのか、について、本書の内容および私の経験や世間的に有名な話も交えつつ、解説していこうと思います。
 ただし、本記事で述べる内容は効果検証入門を参考に、私なりの理解を文章に興したものです。怪しい点がございましたら、コメントにて教えてくださると助かります。また、証明等については他のブログや書籍等に譲ります。ここでは詳細は記述しません。

本記事について

内容と対象者

 回帰分析を効果の検証に用いる場合、の手法について解説します。あくまで、介入がKPI(売上、等)に及ぼす影響を評価する上での手法と、注意しなければならない点について記述していきます。
 逆に、予測・補間するために用いる回帰については説明しません。ここは少しややこしい部分なので照査は後述しますが、あくまで介入による効果を見積もることが目的です。そのため、予測のための回帰分析を知りたい方は本記事では不十分かと思います。他の記事を参考にしてください。

キーワード

  • 目的変数 objective variable ... 介入による効果を期待する変数
  • 介入 treatment ... 効果を生むためのアクション
  • 説明変数 explanatory variable ... 目的変数と関わりのある変数
  • 共変数 control variable ... 説明変数のうち、セレクションバイアスを生み出していると分析者が考える変数
  • 交絡因子 confounding factor ... 共変量のうち、介入と目的変数の双方に相関を有する変数
  • OVB Omitted Variable Bias ... 介入の偏回帰係数を正しく推定するのに必要であるにも関わらず、モデルから抜け落ちている交絡因子のこと
  • CIA Conditional Independence Assumption ... 介入が目的変数と独立している、という考え方で、効果を正しく検証するために満たしている必要があるもの
  • Sensitivity Analysis ... 分析者が重要だと認識している共変量以外の共変量をモデルから抜くことで、効果の推定値が大きく変動しないかどうかを確認する分析
  • Post Treatment Bias ... 介入よりも後に決定される変数をモデルに組み込むことで発生するバイアス
  • 多重共線性 Multicollinearity ... 回帰モデルに含まれている変数のうち2つが強い相関を持つこと

本記事で使用する記号・添字

  •  X :: 説明変数
  •  Y :: 被説明変数(目的変数)(本記事内では売上を表す)
  •  Y^{(0)} :: 介入が無かった場合の被説明変数
  •  Y^{(1)} :: 介入が有った場合の被説明変数
  •  Z :: 介入(本記事ではメール配信の有無;0 or 1を表す)
  •  E[Y] :: 期待値(母集団における Yの平均)
  •  E[Y|Z, X] :: 条件付き期待値(母集団における Y|Zの平均;ある変数 Z, Xがある値をとるときの Yの期待値)
  •  \bar{Y} :: 平均(標本における平均)
  •  V :: 分散(標本における不偏分散

回帰分析とは

 回帰分析、と聞くと、散布図を書いて直線を引いて決定係数を観察して、一喜一憂して、、、というのが真っ先に浮かびます(ボクの場合)。ボクは、大学院を修了してから、企業に研究職で入社してから今日に至るまでずっと、工学(特に力学)に携わっています。そちらの分野での実験とその実験結果の解釈というと、実験結果に当てはまりのいい数理モデルを探索して、現象を定式化・解釈することが目的とされることが多く、そのための手法として回帰モデルが使われることが多いと思います。この五臓六腑に染み付いた線形モデルに対する考え方こそが、私の中の最大の思考バイアスであり、効果検証(因果推論)への理解を困難にさせています
 でもボクはマイノリティではなく、むしろマジョリティだと思っていて、線形回帰というのはこういう使い方が最も一般的なのではないでしょうか。他の人と議論してようやく、これから取り組む『効果検証(因果推論)における回帰分析は、これまでボクが取り組んできた回帰分析と大差なく、ほんの少し使い方が違うだけである』と納得することができました。それについて、まず解説します。何が、どう違うのか。

想定するシチュエーション

 適当にシチュエーション(パラメータ)を決定します。高校時代にタイムスリップします(若返りたい)。
 文化祭で、焼きそばを販売することになりました(タピオカの方が現代っぽい?じゃタピオカ屋さんにしようか)。できるだけ売り上げを伸ばしたいです。売り上げを伸ばすために、出店しているタピオカ屋さんのチラシを配ることにしました。ただ、チラシはタダではありません。少なからずコストが発生します。そこで、あなたはこう考えるわけです。

『チラシって売上に関係しているの?』

と。さぁ、調べましょう。

パラメータの設定

 ここでは、出店しているタピオカ屋さんへ来店した民が支払った金額を Yとし、その民へチラシを配ったかどうか(介入)を Z = \{0, 1\} とします。また、何回このお店の前を通ったか、知り合いがこのタピオカ屋さんにいるか、来訪してきた時間はいつ頃か、普段1ヶ月にどれくらいタピオカを飲んでいるのか、何杯買って行ったのか、、、などなど、来店した民の特徴を X_1, X_2, ,,,とします。思いつく限りたくさん特徴の特徴があることでしょう。

線形回帰をしよう

 さて、今知りたいのは、チラシの有無が、来店した民の支払い金額へどのような影響を及ぼしているか、ということです。単純に線形モデルを組むと、 i さんの支払い金額 Y_iは、

 Y_i = \alpha_0 + \alpha_z Z_i + \epsilon

と立式可能です。ここで、 \alpha_0, \alpha_z, \epsilonはそれぞれ介入の切片、偏回帰係数、誤差項(残渣を表す)であり、 Z_i = 1である時にはその民  i へチラシを配った、と言うことを意味します。チラシを配らなかった場合( Z_i = 0)、その民が支払う金額は \alpha_0 + \epsilonです。ここで、 \epsilonは単純な誤差項ではない( E[\epsilon] \neq 0 可能性があることに注意です。と言うのも、支払い金額 Y_i Z_iで説明しようとした際に、説明しきれない値(誤差、と言うよりは表現しきれなかった値、取りこぼした値なので残渣と表現した方がわかりやすい)が、 \epsilonには含まれるためです。逆に言うと、説明変数 Z, Xで来店した民の支払い金額を完璧に表現できた時、 E[\epsilon] = 0 となることが期待されます。

線形回帰の解釈;工学実験的

 さて。ここから、線形回帰で表現されたこの式の解釈方法についてお話ししていきましょう。まずは、工学実験的解釈手法についてです(これは勝手に命名しました。いわゆる物理現象等科学を対象に実験した際に得られた結果に対して、モデル化を目的として行われる線形回帰における解釈という意味です)。改めて線形回帰式を示します。

 Y_i = \alpha_0 + \alpha_z Z_i + \epsilon

支払い金額 Yの説明変数はチラシの有無という介入 Zだけで表現された、最も単純な単回帰モデルです。ここからは、チラシが配られた民は、配られていない民に比べて \alpha_zだけ高い金額を支払う、ということがわかります。
 ただ、ここでの目的は"チラシを配ることでどれだけ売り上げが伸びるか"を予測するということであるため、普段1ヶ月にどれくらいタピオカを飲んでいるのか、何杯買って行ったのか、など M個の説明変数をモデルに加えることで、より正確にその増加量を予測することを試みます。

 {
\displaystyle
\begin{equation}
Y_i = \alpha_0 + \alpha_z Z_i + \Sigma_{m=1}^M \bigl( \alpha_m X_{m,i} \bigr) + \epsilon 
\end{equation}
}

もし、説明変数の漏れがない場合、 E[\epsilon] = 0 となることが期待されます。すると、より正確に数理モデルは構築されたことになり、チラシの有無(介入 Z)によってどれくらい支払い金額が変化するかだけでなく、他の説明変数、例えば1ヶ月にどれくらいタピオカを飲んでいるのか、その量に応じて支払い金額がどれくらい増加するか、まで予測することができるようになります。つまり、介入 Zだけでなく、線形回帰モデルに含まれる説明変数 X_mが目的変数 Yへどれだけ影響を与えるのかを予測することができるということです。ただし、目的変数と説明変数との間で、相関係数がある程度大きいことが前提です(多重共線性などはここでは無視する)。
 また、誤差項 \epsilonに関しては、 E[\epsilon] = 0 であることが期待されます。

線形回帰の解釈;効果検証(因果推論)的

 では、工学実験的な線形回帰分析の解釈に対して、効果検証(因果推論)では線形回帰分析をどのように解釈するのでしょうか。
 まず、下の単回帰分析について考えましょう。

 Y_i = \alpha_0 + \alpha_z Z_i + \epsilon

ここで、因果推論の場合、『介入 Zは来店した民の支払い金額 Yの大きさに寄与しない』という帰無仮説を棄却するためにt検定のp値が有意水準を下回っている必要があります。さらにいうと、他の説明変数 Xを線形回帰モデルに入れたとしても、それらの目的変数 Yとの統計的優位性には関心がない(帰無仮説を棄却する必要がない)のです。
 そして、先ほどと同じように M個の説明変数をモデルに加えると、

 {
\displaystyle
\begin{equation}
Y_i = \alpha_0 + \alpha_z Z_i + \Sigma_{m=1}^M \bigl( \alpha_m X_{m,i} \bigr) + \epsilon 
\end{equation}
}

となります。しかし、ここでは追加された M個の説明変数 X_mのうち、本モデルに必要なのは目的変数 Yと介入 Zのどちらにも相関があるもののみです。これはどういうことでしょうか。
 効果検証(因果推論)の場合、関心があるのは介入 Zの偏回帰係数 \alpha_zのみであり、他の説明変数の偏回帰係数には関心がないのです。それはつまり、介入 Zの偏回帰係数 \alpha_zのみ最適化することができれば良く、正確に目的変数 Yの予測をする必要がないということです。介入 Zと相関のない説明変数 Xをモデルに追加しても、介入の偏回帰係数 \alpha_zはそれほど影響を受けません。そのため、効果検証(因果推論)に必要なのは、目的変数 Yと介入 Zのどちらにも相関があるもののみあれば、 \alpha_zは最適化されるのです。
 また、誤差項 \epsilonに関しては、 E[\epsilon] = 0 である必要がありません。というのもの、モデルが目的変数の全てを表現しているわけではない(そこが目的ではない)ためです。
 これらのパラメータの算出(計算)方法については、本記事の最後で簡単に説明します。

回帰分析についてまとめ

 ここまで、大雑把に「工学実験的」と「効果検証(因果推論)的」な線形回帰の解釈の仕方の違いを述べました。それを対比の形でまとめると、次のようになります。

工学実験的 効果検証(因果推論)的
目的 目的変数の予測、介入・説明変数の寄与度 介入の目的変数への寄与度のみ
説明変数 自由に入れてOK 目的変数と介入に相関のあるものをMECE
解釈に使うパラメータ 任意の係数 介入の偏回帰係数のみ
誤差項 期待値0が理想 期待値0である必要がない

なお、ここではまだ説明変数の選択方法については言及していません。それについては後述します。端的にいうと、効果検証(因果推論)における回帰分析は、介入の偏回帰係数のみ最適化する(すなわち、セレクションバイアスを最小化する)ことが求められます。では、如何様にして最適化すれば良いのでしょうか。



回帰分析における説明変数(共変量)の選択

 ここからは、回帰分析を実施するための説明変数を選択する上で、考慮するべき考え方を紹介していきます。なお、ここで紹介するのは『効果検証入門』第2章に記載されている内容についてです。

変数を固定(統制)する

 この表現が、いろいろなところで使われている(ただし「効果検証入門」では使用されていない)。、、、どういう意味かわからなくない??????ボクは何言ってるのかよくわからなかった。。。結論からいうと、

 変数 X_1を固定する、とは『回帰モデルに X_1を組み込む』ことを意味しています。

、、、どういうことでしょうか。簡単に、概念を説明します(厳密には間違っている可能性があります、あくまでイメージです)。
 まず、次の2式を考えます。

 {
\displaystyle
\begin{eqnarray}
Y &=& \alpha_0 + \alpha_1 X_1 + \epsilon_a \tag{1} \\
Y &=& \alpha_0 + \alpha_1 X_1 + \alpha_2 X_2 + \epsilon_b \tag{2}
\end{eqnarray}
}

ここで、 \epsilon_a, \epsilon_bはそれぞれ式(1), (2)の誤差項です。式(1)について、誤差項 \epsilon_aにはまだ説明変数 X_1と相関を持つ説明変数が含まれている、と仮定すると、誤差項を \epsilon_a = \epsilon_a \bigl( X_1 \bigr)と表現することができます。すると、式(1)の X_1での偏微分は、

 {
\displaystyle
\begin{eqnarray}
\frac{\partial Y}{\partial X_1} &=&  \alpha_1 + \frac{\partial \epsilon_a}{\partial X_1} \tag{1’} 
\end{eqnarray}
}

となることから、説明変数 X_1の目的変数 Yへ与える影響には誤差項が乗る、すなわち正確に X_1 Yへの寄与度を知ることができないことを意味しています。
 では、 X_1と相関のある説明変数; X_2をピックアップするとどうなるでしょうか(式(2))。この時、誤差項 \epsilon_bにはもう説明変数 X_1と相関を持つ変数が存在しないと仮定します。すると、式(2)の X_1での偏微分は、 \frac{\partial X_2}{\partial X_1} = 0であることを考慮すると、

 {
\displaystyle
\begin{eqnarray}
\frac{\partial Y}{\partial X_1} &=&  \alpha_1  \tag{2’} 
\end{eqnarray}
}

と表されます。式(2')を見ると、誤差項の影響なく、正確に X_1の影響を推定することができていることがわかります。
 つまり、変数を回帰式に入れることで、その変数の影響(変動)による介入(対象としている説明変数)の偏回帰係数の推定結果のばらつきを抑えることができる、これを『変数を固定(統制)する』と表現します。

Omitted Variable Bias

 ここでは、Omitted Variable Bias; OVB(脱落(欠落)変数バイアス)について考えます。OVBとは、先ほどの式(1)において、説明変数 X_1と相関のあるのだけれどモデルに入れられていない交絡因子 X_2のことです。つまり、説明変数 X_1の偏回帰係数を正しく推定するために必要であるにも関わらず、モデルから抜け落ちている交絡因子のことをOVBと呼びます。OVBは、観測できない交絡因子が存在する場合には必ず発生します。それに加えて、全ての交絡因子を観測できるケースはほとんど存在しないこと、セレクションバイアスを取り除くために必要な共変数を全て把握することが現実的に不可能であることを鑑みると、OVBの発生しない回帰分析はほぼあり得ない、と考えられます。
 しかも、OVBはバイアスの大きさを示しているものではありません。あくまで、その交絡因子が入っている場合と入っていない場合との、モデル間でのバイアスの変化を示すだけなので、どれくらいバイアスが残っているかという評価をすることができません。なので、分析者は建てた仮説をもとに、モデルに入っていない変数によるOVBが全て0になる(近づく)よう変数を選択する必要があります。

Conditional Independence Assumption

 次に、Conditional Independence Assumption; CIAについて考えます。これは、因果推論において基本となる考え方です。OVBが0になった時、介入 Zは目的変数 Y^{(0)}, Y^{(1)}とは独立しているという状況になり、これはつまり共変量が同一のサンプルにおいて、介入がランダムに振り分けられている状況とみなすことができます。このような状況を、CIAと呼び、以下のように表現されます。

 \{ Y_i^{(0)}, Y_i^{(1)} \} \perp Z_i | X_i

回帰モデルで推定された介入の効果が正しいかどうかを考える場合、CIAが満たされているかどうかを確認する必要があります(ボクの解釈ですが、CIAが満たされている時、擬似的/局所的にRCTが実施されている状況と近似することができるようになるため、その時の介入の効果は信頼性の高いものとなる、と考えることができるということです)。
 ただし、OVBの項目でも述べたとおり、残っているバイアスの量を評価することはできませんし、必要な交絡因子がデータから欠落している場合にも、その分のバイアスをモデルから取り除くことは困難です。そのため、それらを踏まえてもCIAを主張することができるかどうか、というのが因果推論を実施する上で大きなポイントになります。

Sensitivity Analysis

 手持ちのデータには含まれていない交絡因子がバイアスを発生させているかどうかを検証するための手法の一つに、Sensitivity Analysisがあります。Sensitivity Analysisは、分析者が重要だと認識している共変量以外の共変量をモデルから抜くことで、効果の推定値が大きく変動しないかどうかを確認する、という分析です。もしも変動が小さい場合、その共変量の回帰分析への影響は小さく、モデルから抜けていても影響が小さい(無視することができる)ことを表しています。

Post Treatment Bias

 介入の効果を検証したい時に、介入後に決定される介入の影響を受けた共変量(例えば、メール配信という介入に対するサイト来訪という共変量)を分析に入れてしまうことで起きるバイアスのことをPost Treatment Biasと呼びます。これは、どんな介入の割振り方をしても発生するバイアスであるため、介入云々ではなくそもそも分析に入れてはいけない共変量です。
 どの共変量がPost Treatment Biasの対象となるかについては、大まかには介入よりも後のタイミングで値が決まる変数と捉えておけば良いですが、判断は分析者の知識に依存します。

Multicollinearity

 これは多重共線性と呼ばれる現象です。マルチコ、なんて呼ばれたりもします。ちょっと前に、Twitter上で機械学習において多重共線性をどう取り扱うか、ということが結構話題になっていましたね。その、多重共線性です。  多重共線性とは、簡単にいうと、回帰モデルに組み込まれている説明変数間で強い相関を持つことです。回帰モデルは説明変数を増やすことで決定係数が高くなりがちなので、ホイホイと説明変数を追加してしまうと陥ってしまう可能性がある現象です。
 この多重共線性によって引き起こされる悪影響の例として、次のようなものがあります(参考:株式会社サイカ・ブログ)。

  • 分析結果における係数の標準誤差が大きくなる
  • t値が小さくなる
  • 決定係数が大きな値となる
  • 回帰係数の符号が本来なるべきものとは逆の符号となる

 多重共線性についてもう少し理解を深めるために、回帰分析で得られる推定値の分散について観察します。分散は、以下の式で得られます。

 {
\displaystyle
\begin{eqnarray}
\bar{x}_{k} = \frac{1}{N} \Sigma_{i=1}^{N} x_{k, i} \\
V[\hat{\beta}_{k}] = \frac{\sigma^2}{(1-R_k)\Sigma_{i=1}^{N} (x_{k, i} - \bar{x}_k)^2 }
\end{eqnarray}
}

 R_kは変数 kと多重共線性を起こしていると考えられる変数の相関を表しています。相関が強くなるにつれて分母が0に近づくため、分散が発散することがわかります。つまり、多重共線性は、分散が異常に大きくなり、推定されたパラメータの標準誤差が信頼できなくなることが最たる問題なのです。ただし、これは多重共線性を持つ変数間でのみ問題となります。どういうことかというと、もし知りたい(正しく推定したい)偏回帰係数が介入のそれだけである時、介入変数以外で生じている多重共線性は無視して良いということを意味しています。
 これは、回帰モデルをどのような目的で構築し、使用するかに依存しているため、必要に応じて対処する必要があります。なお、相関係数以外の多重共線性の指標としては、Variational Inflation Factor; VIFというものが知られています(wiki)。



回帰モデルを使用する際に知っておくべきこと

 共変量の選択方法とはまた違う観点から、本書で取り上げられている、回帰分析をする際に念頭に置いておくべきことを最後にまとめます。

Limited Dependent Variable

回帰分析を使用する理由が予測であり、モデルの予測性能や説明力を重視する場合には、目的変数 Yの分布に応じてモデルを選択する必要があります。例えば、

  •  Yが0 or 1(購入の有無) ... ロジスティック回帰
  •  Yが0以上のみ(購入金額)... ポアソン回帰

のような状況のことです。このように、目的変数 Yが特定の値しか取らないような制約がある状態制限被説明変数(Limited Dependent Variable)と呼びます。

 もし、効果に関心のある変数(つまり、介入 Z)が2値である場合、モデルを線形回帰で構築しても問題ありません( E[Y|Z=0 \propto E[Y|Z=0] ])が、介入 Zが連続値であり目的変数 Y非線形な関係を持つ場合には、線形回帰では効果の関数を無理やり線形で示すことになるため、妥当性の検証が必要になる、と言うことに注意する必要があります。

対数を用いた回帰分析

しばしば、説明変数や目的変数に対数を用いることがありますが、その場合の推定されたパラメータ(偏回帰係数)の解釈は以下の通りです。

  • 目的変数の対数を取る場合 ... 各説明変数が何%、 Yに対して影響を与えているか
  • 説明変数の対数を取る場合 ... 説明変数 Xを1%変化させたときに Yにどれくらい影響を与えるか

対数を取る目的は、(本書では)2通りに以下の絞られています。

  • 目的変数に対する介入の効果が比率で表されるべきであるとき(スケールの統一)
  • 共変量と目的変数の関係が比率で扱われるべきであるとき(比率が影響する共変量を取り扱う場合にはセレクションバイアスが発生する可能性がある)

なお、目的変数の分布を正規分布に近くするために対数化を実施するわけではないことに留意すべきです(この妥当性は、しばしば批判の対象になります)。

おわりに

 回帰分析を因果推論の目的のもとで使用するというこの考え方は、これまで近似としてのみ使用してきた身としてはかなり眼から鱗で、最初は何を言っているのかさっぱり理解できませんでした。ただ、深く読み、他の文献の情報も取り入れることでようやく理解しあえそうな気がしてきました。奥が深いですね。
 ただ、因果推論は非常に難しく、これを生業としている人ですらも未だに基本的な考え方について勉強し続けているような、とても深い分野です。この本ではさっくりと"実用"にのみフォーカスして説明しているため、情報としては不十分であることが多々あります(特に、本書では計量経済学を対象としているため、疫学や心理学等に適用したい場合には用語のすり合わせや基本的な考え方・価値観の違いを考慮する必要もあるため、他分野に使用する場合には十分注意してください)。
 それでも、こういった考え方を知っておくことは、物事の本質を見抜くためにはとても重要なことだと私は信じています。信じています。手法は古典的で、確立されている面も多いため、時間をかけてしっかり習得していきたいです。





最後にこの曲を聞いてお別れです。

偶然テレビで銀魂がやっており、そのEDテーマ。

去年解散してしまったので、すごく懐かしい気持ちになりました。

(せーのっ)

\\\ Destiny //

NEGOTO - Destiny

PythonでPDFを読み込むメモ(decrypt有)

はじめに

 ネット上にはPDF化されたファイルが多いが、そのデータを読み込むには一癖必要である。帳票や授業で撮ったノートなんかを画像からPDFに変換した時だったり、オフィシャルなPDFファイルだったり。
 そんなPDFファイルからPythonで文字データを抽出するにはどうすれば良いか?というメモ

環境

  • macOS Catalina 10.15.4
  • Python==3.7.4 on Anaconda==1.7.2 (conda==4.8.3)
  • pip==19.2.3
  • PyPDF2==1.26.0
  • pikepdf==1.13.0

参考URL

なお、参考にしたURLはこちらに先に示しておく。

対象としているPDFファイル

本記事で対象としているPDFファイルは、

  • 英語/数字で記述
  • ロックの有無(decrypted/encrypted)は問わない

である。日本語のPDFに関しては、本記事では試していないので、読者らで試してみて欲しい(なんなら所感をコメントに残してほしいです

PythonにおけるPDF reader

こちらの記事に纏まっているので、詳細(詳細というレベルで記述されている記事ではないが)を知りたければそちらを。PythonでPDFを読み込むために使用できるpackageは、以下のようなものがある。

Package 対象 install command
PyPDF2 英語 pip install PyPDF2
pdfminer.six 日本語 pip install pdfminer.six
Apache Tika 日本語 pip install tika
Tesseract OCR 画像データのPDFや帳票PDF pip install pyocr

今回私が使用するのは、英語を対象としているためPyPDF2である。なお、こちらに関しては、すでにPyPDF3が開発されている(PyPDF2を開発していた人らが開発を中断してしまったが、こちらのソフトが如何せん有用だったため有志で開発が進められている(しかしそこまで活発ではないし、現状はPyPDF2で十分だろう))。

pypi.org

なお、フォントに依存して読み込めないものも存在することには留意しよう。

復号化(Decrypt)の必要性

読み込ませたいPDFファイルが、例えばネット上から拾ってきたものであったり、自分配布用に作成したものであったりすると、大抵はロックがかけられているだろう(第三者に編集させないようにするため)。その状態のものをPyPDF2で読み込ませようとすると、以下のようなエラーを吐く。

import PyPDF2
with open("hogehoge.pdf", mode='rb') as f:
    reader = PyPDF2.PdfFileReader(f)
    print(f"Number of pages: {reader.getNumPages()}")

>>> PdfReadError: File has not been decrypted

これは、「暗号化されているため復号化してくださいね」というエラーである。

PyPDF2上における復号化

もし、そのPDFにかけられているロックのパスワードが既知("fugafuga")である場合、PyPDF2上でアンロックする(decrypt)ことが可能である。

import PyPDF2
with open("hogehoge.pdf", mode='rb') as f:
    reader = PyPDF2.PdfFileReader(f)
    if reader.isEncrypted:
        reader.decrypt("fugafuga")
    print(f"Number of pages: {reader.getNumPages()}")

if reader.isEncrypted:と、ロックされているかどうか(復号化の必要があるか)を確認できる。これでもロックを解除することができない場合には、

NotImplementedError: only algorithm code 1 and 2 are supported

というエラーを吐くことがある。その際には、qpdfと呼ばれるツールを使用することを推奨している記事が多数見受けられる(NotImplimentedErrorが、そもそもその復号化処理をPyPDF2で担うことができないということに起因するので、復号化処理だけqpdfに担ってもらおうという考え方)が、これもまたパスワードが既知であるPDFファイルにしか対応できない。現実問題、そんなにパスワードが分かっている、それを読み込みたいなんていう状況があるだろうか?(それなら普通にword形式でもデータを持っておけよという話

パスワードがわからない場合の復号化 (pikepdf)

 さて、ではパスワードがわからない時にはどうするか。その中でも最もスマートな解が、stackoverflowに上がっていた。簡単にいうと、pikepdfを使おうというものである。pikepdfは、以下のコマンドでインストール可能である。

pip install pikepdf

あとの使い方は簡単で、

import pikepdf

pdf = pikepdf.open("unextractable.pdf")
pdf.save("extractable.pdf")

とすれば良い。ここで保存したPDFファイルを、PyPDF2を使って読み込むだけである。

終わりに

 英語のPDFを読み込む必要が出てきたので、対応しているツール周りを簡単に調査した。1時間足らずでここまで調査・実行できるので、そこまで知見が転がっていない話ではないのでだろう。。。現場、問題は起きていないが、また問題が発生した場合にはこちらに追記していこう。



緊急事態宣言もついに終わりを迎えた、1日目の朝。
ってことで、今回はこちらの曲を聞いてお別れです。

(せーのっ)

\\\ 朝は来る ///

Amelie「朝は来る」Music Video

BERT応用勉強会に参加しました

はじめに

BERT(Bidirectional Encoder Representations from Transformers、「バート」と発音)は、Googleが2018年に提案した、TransformerのEncoderを使用しているモデルであり、NLP自然言語処理において単語の分散表現を生成する手法としては現在最も使用されている、勢いのあるモデルです。BERTについては、他の記事を参照してください。下に貼っておきます。

qiita.com

そのBERTモデルの応用をテーマにした勉強会が開催されました。コロナの影響もあり、オンライン(YouTubeでのライブ配信、clusterでの参加)で開催された。Connpassを下に貼っておきます。

nlpaper-challenge.connpass.com

配信されたYouTubeはこちら。

https://youtu.be/6QFmYVTAxK4

Twitterは、ハッシュタグ #xpaperchallenge twitter.com

BERT応用勉強会

タイムテーブル

時間 内容 発表者
19:00~19:10 nlpaper.challengeの紹介 yamamoto
19:10~19:30 LT1: 医療言語処理へのBERTの応用 ―BioBERT, ClinicalBERT, そして― Yuta Nakamura
19:30~19:50 LT2: Multilingual BERTの二言語領域適応に基づく対訳文同定 siida
19:50~20:10 LT3: BERTのMulti Modalタスクへの活用 ymym3412
20:10~20:30 LT4: BERTをブラウザで動かすには ―MobileBERTとTensorFlow.js― Shion Honda
20:30~20:50 LT5: テキスト生成の評価 × BERT cfiken

nlpaper.challengeの紹介

  • 2019年から活動開始
  • cvpaper.challengeのnlpバージョン
  • 論文のサーベイだけでなく、国際会議での発表まで目標にしている

LT1: 医療言語処理へのBERTの応用 ―BioBERT, ClinicalBERT, そして―

  • 医療の言語処理をやっている人が少ない、、
  • 2014.09...Attension Mechanism(RNN/LSTM)が主流2017Transformer,2018BERT after BERT
  • 医療業界においては、技術自体はすでにafter BERT
  • Biomedical NLPにもBERTは浸透してきている、しかしより現場に近い研究への流入は始まったばかり(実際の医療現場にある文書を対象にしているのはいまだにロジスティック回帰だったり、古典的なものが主流)
  • 診療記録に対してのDL手法としては、まだまだRNN/LSTMが主流
  • 今現在として、少しずつBERTの論文が出始めた(ようやくBERTが現場に流れ始めた)
  • 医療ドメインに特化したBERTが登場してきている
  • BioBERT...BERTをさらに医学論文で学習させたもの
  • CriticalBERT...ICU診療機構(MIMIC-Ⅲ)で学習(実際のカルテ)
  • EhrBERT...BioBERTから実際のカルテを学習させたもの
  • BioBERTで病名正規化(病名表記揺れの正規化)の精度向上(MetaMap f1=0.572 -> 0.886 (SOTA))
  • FigSumというモデル(論文中のキャプションと図も読み込ませたもの)もある
  • ClinicalBERT...診療記録の文書分類AUC0.67, i2b2も大きく改善
  • 含意関係認識...MedNLI(xとyという二つの症状の含意関係をY/Nで)
  • BeHRtという、BERTを言語でないものに使用しようというモデルが生まれている
  • 診断に使用する場合、トークン列を[SEP]で区切るのではなく年齢等を[SEP]で区切る、診断の一部を[MASK]にして復元させる学習をすることで、数ヶ月後に患者の診断がどうなるかということを予測する
  • TAPERという、患者さんの入院記録そのものをモデル化するやつも
  • 言語でーた:診療記録、非言語データ:ICDコード、薬剤コード、処置コード、それぞれをベクトル化し和をとる -> 非言語データを入れることで精度が向上したをいう研究成果も
  • ここまでの新しいBERTモデルらは英語のもの
  • 東京大学医療AI開発講座から、日本語診療記録に特化したBERTモデルが公開された(ようやく出てきた)
  • 臨床現場との距離はまだ遠いのが現状、だけどこれから発展していくだろう(日本語版も発展していってほしい)
  • (質疑応答: FineTuningはどうしているの?)BioBERTとかは基本的には学習データが変わっただけでそれ以外は変わっていない、だけど最近は診断コードや患者さんの情報など、非言語情報や文書情報でないものも取り込まれるようになってきている
  • (質疑応答: 実際にこういった技術はすぐに医師が使えるのか?BERTが医療現場で使われていないハードルは何?)NLP自体がすぐに医療に役に立つ、というところにまできていない現状(2つの医学的記述が含意関係にあると分かった(ClinicalBERTx診療記録)として、そこから何ができるか?エビデンスとして十分なのか?という部分で、数字に役立ちましたとわかりやすい形で出るまでは広く受け入れられないだろう

LT2: Multilingual BERTの二言語領域適応に基づく対訳文同定

  • NLP2020で発表したものと同じ
  • BERT [Devlin+, NAACL -2019]はGoogle検索に2019/10/25に導入された
  • 機械翻訳の対訳文をBERTを使って収集しようというもの(日英の対訳を大量に収集する手法としてBERTを使いましょうというもの)
  • LASERの登場により対訳文収集へのNN活用が進んだ、BERTを使ってやってみよう
  • 特許データをもとに収集
  • LASERだと図(Fig)とかの対応は取れるが、図番号の対応までは取れない
  • Masked Language Model (MLM)Fine Tuningに約20万の文章を使用
  • 文対応手法により、文対応スコアが0.05以上のものを訓練用に使用
  • 英文、和文の対応が1対1でないことも多い
  • 二段階の学習を実施している:二言語領域適応、fine-tuningによる対訳文分類器の作成
  • 公開されているBERTはWikipediaで学習しているため特許独特の表現にそのままでは適応しない
  • M-BERTのMLMを再訓練することによって領域適応し、各言語ごとにそれぞれ言語モデルが訓練されるとともに言語間の対応が取れていないものにも適応可能になる
  • fine-tuningにはQQPを使用(入力した2つの文章が同一の意味か否かを判定する英語おける手法;2つの英文間をみる)、これを日本語と英語間として適用した
  • 学習には、NVIDIA GPU, バッチサイズ8
  • (質疑応答:二言語領域適応のみだとどうなの?※ボクの質問!うぇい!)二言語領域適応のみだと、今後教師データが不要になったりとさらによくなる、今後の課題
  • (質疑応答:翻訳性能は測っていないの?)翻訳性能は測っていない、同じ研究室の別のものがやっているので今度学会で発表されることでしょう
  • (質疑応答:sentence-BERTではなくLASERと比較した理由はなんでしょうか)今だったらSentence-BERTを使った方がいいかも、最初のとっかかりとしてLASERと比較した

LT3: BERTのMulti Modalタスクへの活用

  • MultiModal...複数のモダリティから推測する(皮肉は文章だけでなく、表情とかも読み取った方が精度良く推定できるでしょう?っていう)
  • MultiModalで使用されているBERTは複数ある; ViLBERT, LXMERT, VLBERT, UNITER
  • BERT使って解きたいタスク: 画像と文章を組み合わせた質問、画像にアノテーションされているものに対してテキストでその状況説明のうち正しいものを選択する、文章中で指定されている対象を図中から同定する、など両方のModarityをしっかり理解する必要がある
  • ViLBERTについて
  • Self-attension(Query/Key/Valueは全て自身から)の代わりにCo-attension(Query自身から、Key/Valueは他のModarityから)のレイヤーを使用
  • [, v1, ..., , w1, ..., ]の形式、ランダムにマスクしたvi/w1のクラスを予測するor画像を文章の対応を予測する
  • LXMERTについて
  • ModelごとのTransformer encoderとModarityを統合・複数個スタックする
  • 事前学習タスクには複数ある(発表中では5つ紹介)
  • VLBERTについて
  • Encoderを分離せずに、imageとtextを同じencoderに投入・self-attentionする
  • Unicode-VLについて
  • VBERTと差分は少ない、fine-tuningした後のタスクが少し違う
  • 違いってなんや?っていう論文登場
  • UNITERについて
  • VLBERTと同じようにimageとtextを同じencoderに投入、そのご4種類の事前学習タスクに投じる; Masked Language Modeling, Image-Text Matching, Word-Region Alignment(最適輸送問題として解く;IPOTで求め、その距離を損失とする), Masked-Region Modeling(回帰、クラス等3種類の損失関数を使用)
  • UNITERつおい
  • 俯瞰してみると、ModalごとにEncoderを分けて使用するのが主流になっている
  • モデル固有の事前学習タスクが設定されることで他のモデルとの差を生み出している
  • (質疑応答:ビジネスで応用される、ビジネスとして活用できる場面は?)テキストから画像、画像からテキストを検索といった、Modalをまたいだ動きができる
  • (質疑応答:マルチモーダルは単一モーダルよりも認識精度は、やはり向上するのでしょうか?その分、難易度も高くなるのでしょうか?トレードオフ?)一概には言えない、imageとtext両方にencodeしなくてはいけないため学習の難易度が上がるため。実際やる場合には、modal同士のalignmentが機能するような損失関数を設計すること、簡単な学習タスクから始めて少しずつ難しくしていくなど学習方法にも工夫が必要
  • (質疑応答:画像とテキストの事前学習、ファインチューン、それぞれのデータ数の目安)数万〜数十万を複数Modalで組み合わせるので、10万、20万、多くて50万とか

LT4: BERTをブラウザで動かすには ―MobileBERTとTensorFlow.js―

  • 質問文を投げると、文章中から回答を探してきてくれる(文章が存在していない場合、No Answerっていうのも答えとして出力してくれる)
  • MobileBERTの適用例として、Ctrl+Fでできるキーワード検索に質問を入力することで答えをサイト内から探す、という応用例(うまくいく時といかない時はある)
  • モバイル端末向けに、軽量化・高速化したBERT(学習方法の工夫、アーキテクチャの工夫)
  • 学習方法...BERT Largeからの蒸留を採用(Progressive K.. Transfer下層から順に一層ずつ学習、各層の特徴マップと注意マップを近づけていく)
  • アーキテクチャの工夫...PKTのために教師と生徒で各層の入出力サイズは一致させなければならない(教師は逆ボトルネック型、生徒はボトルネック型)
  • inputのパラメータを揃えつつも、他のレイヤのパラメータはIB-BERTと比較して大きく削減(精度としてはBERT Largeには負けている)
  • 日本語版公開軽量BERTはALBERTがある
  • TensorFlow.jsでは、学習済みのモデルを呼び出すだけで使えるのでとっても便利
  • (質疑応答:MibileBERTに限らず、BERTの性能をほぼ維持しつつ高速化させるアプローチは多数報告されていると思います(パラメータの量子化やPoorMan's BERTなど)が、タスクや実装環境によって得手不得手があったりするのでしょうか?)ハンか性能と軽量性、速度を両立するのは困難

LT5: テキスト生成の評価 × BERT - BERTScore, MoverScore, BLEURT

  • BERTを用いた評価についてがメイン
  • テキスト生成;xxx-to-xxx (text-to-text, data-to-text, image-to-text, etc.)
  • 教師あり学習なので、ラベルと出力の分を比較することで生成文を評価する
  • 課題の一つに、評価の難しさがある(どうやって定量的にやる?単語の一致度?意味?機械が評価するには、単語の一致数などの一致度を見て評価してしまいがち、利用する側からすれば意味が一致している方が嬉しいのだが、、、)
  • 現状、自動評価に加えて人で評価も行っている、すっごい手間。
  • 既存指標は単純に単語のマッチングを見ていることが多い;n-gram matching base (precisionをベースにしたBLUE、F値を使用したMETEOR)
  • Word Mover's Distance (輸送距離)
  • 学習可能な評価指標も(人手評価の値を推測する回帰モデル)
  • 既存の評価指標は、多くが表面上の違いにばかりフォーカスしてしまっている
  • 意味まで見るためにBERTを使用する
  • 1.BERTScore
    • BERTの出力を用いてreferenceとcandidateをともにBERTに突っ込む、各単語についてmax(場合によってはidfで重みづけ)をとる
    • 実装が簡単
    • 元データのQQPでもadversarial sapを加えたPAWSでも性能高
  • 2.MoverScore
    • word mover's distanceをBERT出力に適用
    • BERTScoreでは最も炊事どの高い単語だけを参照するが、MoverScoreでは最適輸送問題をそくのでどのように分布を移動させればよいかをみる
    • Uni-gramではBERTScoreとほとんど差がない、Bi-gramでは、まぁみる範囲が大きくなるので差が出る
  • 3.BLEURT
    • BLEUやBERTScore、Backtransitionを用いて学習
    • wikiのテキスト180万から擬似ペアデータ650万を作成
    • 15種類のラベルを作成してpre-trainしている
    • これをやった後に、人手で作成したラベル付きペアデータに対してfine-tuningしている
  • 指標は3つに分類できる;ルールベース、汎用的なモデルの出力を比較する、人手評価をモデリング。3つ目はアーキテクチャ合戦になるため、1と2のハイブリッドがこれから研究が大きく進んでいくと考えられる
  • (質疑応答:テキスト生成の評価指標,五月雨式にさまざま提案されている印象ですが,どれも定着せず,結局BLEUが使われ続けているように思います.なぜ新しい評価指標は定着しないのか,また定着させるためには,どのような努力(アカデミアに向けてのアピールの仕方など)が必要だと思われますか? and モデルを利用した評価は学習データの変化や、より精度の高いモデルが出た時にベースラインが変わってしまう点が課題で、結局ロジックベースのスコアが公平性という点で使われ続けている気がします。こういった問題の解決に向けた取り組みはありますか?)人手評価をモデリングする手法が出てきている、なんでBLEURが使われないのか、扱うのが難しいから。BERTScoreは名前から信用しやすそうなのと、扱いが簡単なため。今は研究においても、これと比較すればいいっていう王道の基準がない。BERTScoreっていう基準が出てきてくれたのは大きい。これからに期待。
  • (質疑応答:モデルをBERTで推論すれば,BERT-score高くなる気がする.実質リークになってたりしないのでしょうか?)可能性はあるけど、そこまで問題はないだろう。入力が文章で、サンプリングされているため、ほとんど問題ないと思われる, BERTScoreの癖をよっぽど掴まない限りリークはしないだろう
  • (質疑応答:スコア計算にかかる時間はどれくらいなんでしょうか) BLUEの1.5~2.0倍くらい(GPU環境)

終わりに

世界的に見れば、そりゃほとんどの国で英語が使われているんだから英語のBERTモデルはどんどん進んでいくよねって話なわけで。日本語にもこれから期待したいし、英語を覚える必要ないくらいにBERTが言語間の壁を打ち破ってくれる存在になって欲しいなと。



言葉を、ダイジに。
昨夜の野田洋次郎/RAD x Taka/OORのインスタライブには震えましたな。
ってことで、今回はこちらの曲を聞いてお別れです。

(せーのっ)

\\\ バイ・マイ・サイ ///