kysmo’s blog

株式会社キスモの技術ブログです。

データサイエンティストが知っておくといいpythonコーディングの知識

こんにちは株式会社キスモの役員の寺澤です。2回目の担当となります。 前回は高度な内容になってしまいましたが、今回は機械学習・データサイエンス始めたてでも役立つ内容としました。

今回の内容は、コーディングに関する内容となっております。使用言語はpythonのみで解説しますのでご了承ください。

f:id:kodamayu:20171211133000p:plain

機械学習をする分にはnumpy、pandas、scikit-learn、kerasなど有名なライブラリ、案件次第ではSQLHadoopのような並列分散処理エンジンを使えれば十分です。 新規の機械学習モデルを組む分にも線形代数ベイズの知識があれば、tensorflowなどの有名なライブラリで組むことができます。

過去にkaggleやDeepAnalyticsに関する記事があげられましたが、挑戦したことはあるでしょうか? 例えばこれらのコンペティションで、pythonを使って機械学習モデルの学習・推論をするだけなら、あまりコーディングに関して気にしないでしょう。 kaggleに上がっているコードを見たことあればわかると思いますが、コメントで処理をうまく解説すれば読めるコードにはなります。 githubに上がっているディープラーニングのモデルもその傾向が見られます。

ただし、仕事で機械学習モデルなどデータ処理を組み込んだアプリケーションを作ろうとすれば、それだけでは大変です。 プロダクトを作るにあたっては、コードの保守性(コードが綺麗でデバッグ・アップデートしやすいか)を担保する必要があります。 コーディングに気を使わなければ改善のしづらいプロダクトとなり、開発が円滑に進みません。 あまり関係はありませんが、先日話題になった、データ処理のPDCAを行えばいいやと思ったら、リッチな統計可視化が必要になって、web技術も使い大変だったという話1もありました。 何がいいたいかというと、仕事柄によっては、機械学習だけができればいいということではない、ということです。エンジニアリングスキルも必要となります。

本記事は、 アプリ開発機械学習モデルを組み込むデータサイエンティスト を想定した内容となります。 記事自体は長くなりますが、あまり踏み込んだ話はしませんので読むのには苦労しないと思います。 ドンピシャな人はぜひご覧ください。また、 コードを公開したり人に見せるような方 にも非常に役に立つ内容となりますので、 初心者データサイエンティストにもとても役に立つ内容となっております。

前置きが長くなりましたが、この記事ではpythonで知っておくといいコーディング方法を伝授します。

目次

linterを使おう

linterとは、プログラムの文法をチェックするツールのことをいいます。エラーの検知や書き方に対して自動で注意をするツールです。 pythonにもlinterが存在し、pep8やflake8といったツールがありますが、 flake8がオススメです 。flake8はpep8の拡張版となります。 自動エラーチェックの他にも、pep8に従ったコーディングルールを守れているかをチェックするので、自然とコードが見やすくなります。 flake8はpipでインストールできます。

pip install flake8
flake8 mycode.py # コマンドでチェックできます。

linter全てに言えることですが、大抵emacsvimsublime text、atomvscodeなどの有名なエディタでは、自動でflake8のコードチェックをする機能があります。 これを使えばコーディングにどの行にエラーが起きたかすぐにわかるようになります。入れたら必ず設定しましょう。

コードを書いているうちに「flake8に注意されるけど、コードをいちいち直すのが面倒」と思うこともあるでしょうが、自動で直してくれるツールもあります。 flake8準拠のコード修正ツールとして、autoflake8と言うものがあります。これもpipでインストール可能です。修正が面倒になったらこれを使うのもありですね。

pip install autoflake8
autoflake8 mycode.py # mycode.pyを自動修正したコードを出力

ただし、flake8に従うだけではいいコードが書けるとは限りません。以降で説明するpythonのコードテクニックなどを使えば、より好まれるコードが書けます。

便利な書き方を覚えよう

ここでは、プログラミングの基礎としてはあまり紹介されてはいないものの、pythonに組み込まれている便利な文法や関数を紹介します。

ジェネレータ

ジェネレータとはその名の通り、値を生成するオブジェクト(変数)のことです。 for文で用いることが多く、listと扱いが似てループするたびに指定した変数に値を代入します。 ジェネレータを使えばどういうコードが書けるか、例を挙げて解説します。

例えば、以下のようにwhile文を使う場合です。 while文は処理を書き間違えれば、ループを抜けないなどのバグに直面します。 しかもwhile文のバグはデバッグしづらいです。for文が使える場合はwhile文は好まれません。

import cv2  # opencvを使う前提です。
vid = cv2.VideoCapture(video_path)

frame = 0

while True:
    retval, img = vid.read()
    if not retval:
        break
    # 以降にフレームに対する処理
    frame += 1 # フレームを数える場合は行末に挿入

上記のように処理が長くなければ問題なく動きますが、そうでない場合、例えばframeが思った通りに加算されないバグに直面することも多々あります。 特にwhile文内の処理が長い場合には、コード読みづらく、デバッグも非常にしづらいです。 しかし、ジェネレータを使って処理を分ければ、読みづらさを改善でき、デバッグもしやすくなります。

def vid_iter(vid):
    # この関数がジェネレータ
    while True:
        retval, img = vid.read()
        if not retval:
            break
        yield retval, img

vid = cv2.VideoCapture(video_path)
fps = int(vid.get(cv2.CAP_PROP_FPS))

for retval, img in vid_iter(vid):
    some_process(frame, img) # while文に手をかけずに処理を行える

# 後に紹介するenumerateと合わせるとframeを数えられるようになります
for frame, (retval, img) in enumerate(vid_iter(vid)):
    print(frame / fps) # ループ毎にframeが加算される
    some_process(frame, img)

ジェネレータの書き方としては、関数の返り値に return の代わりに yield を使います。 上記の例では、for文のループ毎に yield の行までの処理を実行します。 yield 行の retvalimgfor 行の retvalimg に代入されます。 ただし、関数内の処理が全て終わらない限りはforループは続きます。 特に while を使う場合はループの終了に注意しなければなりません。デバッグをしやすいよう処理を絞るべきです。 ジェネレータは1ループごとで必要な値を受け取るだけの最小限の関数にしましょう。

ジェネレータにはもう一つの利点があります。 ループ毎に yield の行まで処理を行いますが、次のループまでその処理が停止することです。 画像処理をしたり、one-hotベクトルを作る場合に一度にそういったデータを読み込もうとすると、 読み込み切れずプログラムがエラーを吐くことがあります。 ディープラーニングなどミニバッチごとに学習を行う場合は、一度に読み込む必要はありません。 必要なものを必要な時点だけで読み込めれば、前述のエラーの対策ができます。 それを可能とする方法の一つがジェネレータとなります。読み込みやデータ加工処理をミニバッチごとに yield で返します。 すると各ループごとでのみ各ミニバッチを保持するので、ほとんどの場合は読み込み切れます。

内包表記

pythonではリスト内にfor文を書けます。そういった書き方は内包表記と呼ばれています。

以下のような冗長なfor文をひとまとめにできます。

data = [1, 2, 3, 4, 5, 6]
# 以下を...
new_data = []
for d in data:
    new_data.append(d * 2)
# 以下のように書ける!
new_data = [d * 2 for d in data]

また、内包表記では for を複数書けるので、以下のような複雑な処理も一つのリストに書けます。

# 以下のような複雑な処理に対しては...
for d in data:
    processed1, additional_data = some_process1(d)
    processed2 = some_process2(processed1)
    new_data.append((processed2, additional_data))

# こんな書き方も可能
new_data = [(processed2, additional_data)
            for d in data
            for processed1, additional_data in some_process1(d)
            for processed2 in some_process2(processed1)]

一般に、内包表記が使える場合は、内包表記を使ったコードの方が処理が早いです。

また、内包表記を使ってジェネレータを書くこともできます。前述のデータが読み込み切れないなどの問題に対処ができます。

new_data = (d * 2 for d in data) # タプルにはならないことに注意!
for d in new_data:
    some_process(d)

forを複数書ける性質を利用すれば、二重三重のfor文をひとまとめにでき、コードが見やすくなります。

# 以下のような二重ループも...
for epoch in range(100):
    for batch_X, batch_y in get_batch_data():
      loss = model.forward(batch_X, batch_y)
      print('epoch: ' + str(epoch), end='')
      print(' loss: ' + str(loss))
      model.backward()

# 一つにまとめることが可能(for文の書く順番に注意!)
itr = ((epoch, batch_X, batch_y)
       for epoch in range(100)
       for batch_X, batch_y in get_batch_data())
for i, batch_X, batch_y in itr:
  loss = model.forward(batch_X, batch_y)
  print('epoch: ' + str(epoch), end='')
  print(' loss: ' + str(loss))
  model.backward()

enumerate

リスト内の値とインデックス(順番)を一緒に扱いたくなるときありますよね? その場合に以下のように書かれることがあります。こういう書き方したことありますよね?

data = ['りんご', 'バナナ', 'みかん', 'ぶどう']

for i in range(len(data)):
    print(str(i) + '番目:' + data[i])

range(len(data)) とかちょっと冗長ですね。 この書き方をすると、インデックス i の指定を忘れることもありました。 そこで enumerate です。ループごとに数え上げてくれます。 以下の例では、実質 data のインデックスを数え上げます。

for i, d in enumerate(data):
    # dには for d in data と書いた時と同様、dataの要素を順に代入
    # iには0からスタートする自然数を順に代入
    # 出力は上記と同じ
    print(str(i) + '番目:' + d)
# のちに紹介する、formatを使えばもっと見やすくなります
for i, d in enumerate(data):
    print('{}番目:{}'.format(i, d))

ポイントは数え上げとリストの値を同時に取得できることにあります。 前述のような冗長さやヒューマンエラーを解消できますね。 前述でも紹介しましたが、enumerateはジェネレータに対しても使えます。

zip

zip関数、便利ですよね。すでに使われている方の方が多いと思い、ここでは基本の解説は省略します。 もし使い方のわからなければ、検索すればいくらでも出ますでそちらを。 ここでは応用的なことを解説します。

以下のような、二つの要素を順番に処理する書き方に対して、zip関数が使えます。

parsed_sentence = ['今日', 'は', 'すごく', '美味しい', '牛丼', 'を', '食べ', 'た']

for i in range(len(parsed_sentence) - 1):
    bigram_process(parsed_sentence[i:i+2])

インデックスを使ったfor文ですね。インデックスで調整するのは結構間違えやすいので避けた方がいいです。 それをzip関数で比較的改善できます。

# こう書直せます
for bigram_morph in zip(parsed_sentence[:-1], parsed_sentence[1:]):
    # bigram_morphの中身は ('今日', 'は'), ('は', 'すごく'), ... となる
    bigram_process(bigram_morph)

このコードでもインデックスの調整をしていますが、for文内ではインデックスを使っていません。 ずらして値を取る、最後尾の取り方の2点に着目すればこんな書き方もできます。

range

rangeも同様によく使われていますが、あまり使われていない応用方法も紹介します。 rangeは0から指定した値までを返すだけではなく、開始も指定できたり、いくつか飛ばして数え上げることもできます。

例えば、ミニバッチ学習をするとき、よく使うrangeの書き方は以下のようになります。

batch_size = 32
for i in range(batch_size):
    start = i * batch_size
    end = (i + 1) * batch_size
    fit(data[start:end])

ちょっと冗長ですね。かといって startend の内容を直接最後の行に書いても長くなります。 ここで、飛ばして数え上げる書き方を使いましょう。すると以下のようになります。

for i in range(0, len(data), batch_size):
    # i の中身は、 0, 32, 64, .. と32飛ばしになる
    fit(data[i:i+batch_size]) # 同様に batch_size分の処理が可能!

行数も減りましたし、インデックスの指定もそんなに長くないですね。

map, filter

pythonに標準で組み込まれている、 mapfilter の紹介です。 内包表記でも表現できますが、これらの方が有利な場合もあります。 以下が例となります。

# これらの表現は
new_data = [str(d) for d in data]
new_data2 = [d for d in data if d > 10]

# 同様にこのように書ける
new_data = list(map(str, data))
new_data2 = list(filter(lambda x: x > 10, data))

# mapやfilterはジェネレータなので読み込みの効率もよくなる
for d in map(str, data):
    some_process(d)

mapやfilter内にlambda式を書かなければ内包表記よりも処理が早いそうです。

format

文字列の中に変数の値を入れたいときが多々ありますよね。 文字列を + でつなげると冗長になりますよね。 + を書き忘れてしまうこともあります。 formatを使えば見やすくもなりますし、冗長性も解消できます。 以下が例となります。

# プラスを使った書き方
name = 'John'
print('My name is ' + name + '.')

# formatを使った書き方
print('My name is {} .'.format(name))

# 小数点2桁までの表示も可能
pi = 3.14159265
print('pi = {:.2f}') # pi = 3.14 と出力される

冗長性の解消について議論しましたが、表示の仕方も簡単に変えられます。

型を付けてあげましょう

python3からは型宣言機能がつきました。通常、pythonは「この関数の引数は数値だけ」と宣言しなくても実行できました。 しかし、プロダクトの保守性を考慮すると、宣言してくれた方がエンジニアはデバッグしやすいです。 ディープラーニングモデルなんかをアプリに組み込む時、つけてあげるとエンジニアは幸せになります。

def add(num1: int, num2: int):
    # 入力のnum1とnum2はint型のみを受け付けると言う意味
    return num1 + num2
print(add(1, 2)) # 3 と出力
print(add('寿司', '食べたい')) # 文字列(str)をとっているのでエラー

# 数値だけを受けとる宣言も可能
from typing import TypeVar
Number = TypeVar('Number', int, float) # 引数
def add_extend(num1: Number, num2: Number):
    # num1、num2はintでもfloatでも扱えるようになる
    return num1 + num2
print(add_extend(1, 2.3)) # 3.3 と出力

# クラスを使う場合は型にクラスを指定することも可能
class Hoge(object):
    def __init__(self, num: int):
      self.value = num

def get_num_from_hoge_object(hoge: Hoge):
    return hoge.value

print(get_num_from_hoge_object(Hoge(1))) # 1 と出力

詳しい書き方は公式2で見ることができますが、わかりやすく解説されている記事3もあります。

gitを使おう

gitでは、変更内容を保存できたり、変更前の内容を見返すことができます。 「この行とりあえず消したいけど、後で使うかもしれないし・・・」と思ってコメントアウトしたりしませんか? gitならばそんな行も遠慮なく消せます。そして必要であればすぐに復元できます。 また、複数のバージョンを作ることができるので、日常の解析でも「モデルAを使ったパターンと、モデルBを使ったパターンと、モデルBに特殊なチューニングを加えたパターンと...」 といったようなこともgitを使って複数パターンのコードを管理することもできます。 特に他人に見せるようなコードはgitを使って変更内容を保持しましょう。そしてコメントアウトに頼ったコードを書かないようにしましょう。

基本的なことはウェブにチュートリアルがありますが、使いこなそうとする場合は難しいので、エンジニアに教えてもらいましょう。

ドキュメントを書こう

defやclassといった関数やクラスの宣言の行の次に文字列を宣言しているコードを見たことはありますか? pythonにおいてはドキュメントと呼ばれていて、関数やクラスを説明を示します。 pythonコードの先頭に書く場合もあり、その場合はモジュール自体の説明をします。 ドキュメントを書くことで、pythonのhelp関数でそのドキュメントが読めたり、ipythonやjupyter、エディタの機能でも読むことができます。 特にモジュールとして他の人に使わせる場合にとても便利です。共有するコードには面倒臭がらずに書きましょう。

def add(num1: int, num2: int):
    '''
    自然数の足し算をする

    パラメータ
    num1 (int): 足し合わせる数値
    num2 (int): 足し合わせる数値

    返り値
    int num1とnum2を足し合わせた数値を返す
    '''
    return num1 + num2

開発チームによっては、このドキュメントの書き方が決まっているので、書く時はチーム内で書き方を確認しましょう。

リーダブルコードを読もう

プログラミング全般で、コーディング作法の有名な技術書があります。リーダブルコードです。 聞くところによれば、リーダブルコードはできて当たり前と言うエンジニアチームもあります。というかほとんどのエンジニアチームがそうだと思います。 業務はデータサイエンスだけだというデータサイエンティストも興味があれば買ってもいいと思います。コードを人に見せるために学ぶ場合でも有用です。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

オブジェクト指向プログラミングを覚えよう

ここからは実践的なエンジニアリングの内容となっていきます。 pythonでもオブジェクト指向プログラミングができますが、javaなどとは異なりガチガチなオブジェクト指向を要求される言語ではありません。 pythonは手続き型と呼ぶべきものでしょうか。しかし規模の大きいアプリケーションほどオブジェクト指向でのプログラミングが要求されます。 この記事ではあまり触れませんが、もし勉強したい場合はjavaを使ったプログラミングを学ぶことがオススメです。 ガチガチなオブジェクト指向が強要されるので嫌でも身につきます。 また、ウェブで検索しても基本を学べますし、オブジェクト指向の入門書もあります。

オブジェクト指向でプログラミングすることはディープラーニングモデルを組むことにおいても有用です。 chainerやpytorchではオブジェクト指向でモデルを組むことが前提になりますし、kerasでもサポートされていない層を自分で組む場合に使います。 特にこれらを使うことになった場合は、せめてpythonに置けるオブジェクト指向の基本は覚えておきましょう。

ただし、単にオブジェクト指向でコードが書けることが保守性のコードを書けるとは言えません。 オブジェクト指向の基本を習得したら、デザインパターンと呼ばれる、オブジェクト指向におけるコーディング作法を覚えましょう(前述のリーダブルコードでは紹介されていないので注意)。 特にオブジェクト指向においては、 GoFデザインパターン というものが有名で、現在でも使われています。 同様にあまり触れませんが、検索すればまとまっているサイトもありますし、紹介されています本もあります。

アプリケーション開発現場などに放り込まれそうであったり、既に携わっているデータサイエンティストは是非覚えましょう。

終わりに

かなり長くなりましたがいかがでしょうか。最後辺りの内容はかなりエンジニア寄りの内容になりましたが、flake8と紹介した書き方を使うだけでもコードの書き方が改善されます。

ぜひともご参考に。

参考URL