20201216のPythonに関する記事は30件です。

Pythonパッケージ版のMediaPipeが超お手軽 + 簡易なMLPで指ジェスチャー推定

MediaPipe(Pythonパッケージ版)

Pythonパッケージ版のMediaPipeが超お手軽です。

pip install mediapipe

pipでインストールして、、、

import mediapipe as mp

importして、、、

mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
    min_detection_confidence=0.7,
    min_tracking_confidence=0.5,
)

インスタンス作って、、、

image = cv2.imread('XXXXXXXX.jpg')
image = cv2.cvtColor(image, cv.COLOR_BGR2RGB)
results = hands.process(image)

process()に画像投げ込んだら、手のランドマークをゲット?!

※別途、描画処理が必要です。

CPU動作でもかなり早い!
むしろGPU対応していない気がする(ドキュメントのどっかに非対応な旨が書いてあるはあず、、、

MediaPipe(Pythonパッケージ版)がサポートする機能(2020/12/17時点)

Pythonパッケージ版のMediaPipeは、現時点で以下の4機能をサポートしています。

Hands

Pose

Face Mesh

Holistic(Pose, Face Mesh, Handsを一度に推定)

MediaPipe(Pythonパッケージ版)サンプルプログラム

上記のサンプルプログラムは以下で公開しています。

おまけ:簡易なMLPで指ジェスチャー推定

あまりにも簡単に手のランドマークが取れるようになったので、
簡易なMLPと組み合わせて、以下のようなプログラムも作ってみました。
せっかくMediaPipe自体の動作が早いので、なるべく邪魔しないようにTensorflow-Lite
で作っています。

手のランドマーク座標を入力に3分類(パー、グー、指差し)するモデルと、
指差し時の軌跡を入力に4分類(静止、時計回り、反時計回り、移動)するモデルを
組み合わせて動かしています。
Finger Gesture Demo

「パー」「グー」「指差し」の分類

hand_landmarks.png
手のランドマークのIDはこんな感じ↑なので、、、

preprocess1.png
こんな感じに前処理して、、、(ID:0が最終的に0,0になっているの少々無駄ですが、、、)


こんな感じの簡易なMLPにぶち込んでます?

「静止」「時計回り」「反時計回り」「移動」の分類

preprocess2.png
人差指の座標履歴を16点ほど取って、こんな感じに前処理して、、、(T-15が最終的に0,0になっているの少々無駄ですが、、、)


こんな感じの簡易なMLPにぶち込んでます?

プログラム

上記のプログラムもGithubで公開しています。
適当MLPなので、もっとイカした構造にすると、もっと精度が上がります?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mitsuba 2でinverse renderingしてみました

はじめに

タイトルにアルファベットが多くて、ちまたで発生しているイキった人みたいになっていますが、決して調子に乗ってるわけではなくて、Mitsuba 2 ( https://www.mitsuba-renderer.org/ )というレンダリングのフレームワークを使って、inverse renderingをしてみたので、その通りのタイトルにしています。

inverse renderingが何かというと、コンピュータグラフィックス分野で、レンダリング結果を目標画像に近づけるために、オブジェクトや光源などの環境を求めるという逆問題があり、それを解く技術をinverse renderingと呼びます。
Mitsuba 2では、このinverse renderingの一機能が組み込まれており、使い勝手が良い技術が実装されているともっぱら噂だったので、興味があり、使ってみた次第です。
ちなみに、例を参考に、コードを書いているだけなので、この記事では、Mitsuba 2を使って、Mitsuba 2に用意された機能を実験しただけの内容です。
ただ、例を動かして喜んでいるだけでは芸がないので、少しだけ発展的なことをやってみます。

このページを読み終えて、windows 10でNVIDIAのGPUが刺さっているPCを持っていれば、以下のような結果を作れるようになります。
まず、左の図のような見た目になる光源環境、オブジェクトのマテリアルが設定されたシーンがあるとして、光源環境は変えないで、マテリアルの反射だけを変えて、右の図のような見た目にしたいと思ったとします。
asampleout0_000.pnga0_sample.png
inverse renderingができれば、マテリアルの設定を自動で修正してくれて、制約の中でも、近い見た目を作ることができます。
一例が下の図です。これは初期状態から色の調整をして、近づけていく様子をアニメーションにしてみました。
onesample.gif
無論変更できないような部分の反射率は変わらないので、全体がそういう色になるとかではありません。
変えられるところのパラメタを自動で修正するというものです。

このページではインストールから、inverse renderingで結果を出すまでの流れを説明します。

注意

説明すると言ったそばからですが、ところどころに、「本家サイトを読んでくれぇ」と書いてあります。
本家の解説がわかりやすいので、本当に読めばわかることばかりです。
このページでは、わかりにくいところ、若干はまったところを重点的に書いていきます。

あと、コーディングの簡単さについて軽く触れておきます。
私は普通の人なのでプログラミングはあまり得意ではないですが、一応思ったように動かせる程度のことまではできるくらいに簡単です。
Mitsuba2でinverse renderingするためには、Pythonプログラミングが必要となりますが、まともにPythonを書いた経験がない私でもできる範囲のことだけで、inverse renderingできるようになります。
これまでレンダラーを書いて最適化してということをしていた身としては、かなり衝撃的なことです。

また、先に文体や構成について言い訳します。
私は、普段はアル中のごとく、飲み屋のブログくらいしか読まないため、このような様態の技術記事の書き方を理解していません。今回は、周囲の記事を読み、なんとなく皆と同じ様相を持つ文体で記すことに努めました。
また、Markdownの記述も不慣れなため、構造的に読みにくいところがあってもご容赦いただきたいです。

私の実験環境

読み終えたあとに「私のGPUじゃ合わないじゃん!」とかならないために、先に私の実験PCの構成を書きます。重要なのは、GPUだけですが、一応OS、CPU、RAMも並べておきます。
- Windows 10 64bit
- Intel Core i7
- NVIDIA GeForce GTX 1080 Ti
- 64GB RAM

GPUは、NVIDIA製で、CUDAが使えないとダメです。お持ちでない方は買ってくるしかないみたいです。
試験的に別コンピュータで試しましたが、GPUが刺さっていない場合、プログラミング環境を構築できませんでした。

ソフト側の環境は、以下です。
- Visual studio 2019
- Miniconda 3
普通ですね。

少しだけMitsuba 2のこと

コンピュータグラフィックスの学術界隈で、2010年にWenzelさんがMitsubaという物理則に従ったレンダリングのフレームワークを開発しました。
Mitsubaは正確なレンダリング結果を出力するので、精度に厳しい界隈の研究者らは、Mitsubaを使ってレンダリングするようになってきました。
これによってWenzelさんはすごく有名になったのですが、それだけでは止まらず、さらにいくつかの機能を追加したいと思っていたようです(去年本人がどっかでしゃべってた)。
そして、やりたかったことを含めたMitsuba 2が2019年に開発され、SIGGRAPH Asia 2019で発表されました。
その機能のひとつがinverse renderingのひとつの手法であるdifferentiable renderingでした。

ここでは、このdifferentiable renderingを使ってみます。

インストール

以下のページを開いて、読んだ通りにすればインストールできました。
https://mitsuba2.readthedocs.io/en/latest/

簡単に流れを説明します。
最初のGetting startedと次のCloning the repositoryは、読んだ通りです。
やってください。

Choosing variantsからは注意深くいきましょう。
必ず、Configuring mitsuba.confの項を読んでください。
"enabled": で、gpu_が付いたものを選択しないと、目的のdifferentialbeできないので後悔することになります。
説明文では、gpu_autodiff_rgb かgpu_autodiff_spectralを入れたほうがいいよと書いてありますが、それだけでも後悔するので、以下をそのままコピーしたほうがいいです。

"scalar_rgb",
"scalar_spectral",
"gpu_autodiff_rgb",
"gpu_autodiff_spectral",
"gpu_rgb",
"packet_rgb",
"packet_spectral"

次にCompiling the systemでは、各OSの説明の前に、もうGPUを検索して、GPU variantsに飛んでください。
ここで、CUDA ToolkitとOptiXのインストールを先に済ませておきましょう。
終わったらCMakeして、Mitsuba 2をビルドします。
ビルドは、バッチビルドで、ALL_BUILDを選びましょう。個別にチェックしてもいいですが、面倒です。ここでDegugのチェックなんかつけません。Releaseだけです。
mitsuba2_build.png

ビルドが終わったら、distフォルダの中にdllやexeファイルがあるので、そこを環境変数のPathに入れておきます。
setpath.batを走らせてくれ、と本家サイトでは書いてありますが、管理者権限で実行しても、セキュリティのせいか機能しなかったので、手打ちで入力です。
dllのコピーとかだけだと参照漏れがあったときイラつくので、Pathは頭悪い方法ですが確実です。

インストールはこれで終わりです。
要点は以下です。
- CUDAインストール
- OptiXインストール
- configuration.confでもろもろ追加
- CMake+ビルド
- 環境変数PathにMitsubaのdistを追加

Pythonでの使い方

本家サイトにそこそこ書いてあるのですが、初心者の私が、はじめに理解できなかったところを書いていきます。

まずmitsuba 2は、xml形式のシーンファイルを読み込んで、その設定どおりにレンダリングします。

本家に書いていないネタですが、実装されていることとして、カメラのタグであるsensorはxml内でいくつも設定できます。
Pythonを使ったとき、xml内に書かれた順のリストになっています。これを切り替えて、カメラ位置を変えてレンダリングができます。

xmlのなかでは、オブジェクトのメッシュデータまでは記述されていません。
Mitsuba2では、.objとか.plyとか使い勝手の良いデータを読めるようになっているので、それら外部ファイルを読み込んで、メッシュとして扱います。

あとは、これらを読むとできます。
https://mitsuba2.readthedocs.io/en/latest/src/getting_started/file_format.html
https://mitsuba2.readthedocs.io/en/latest/src/plugin_reference/intro.html

初めてのdifferentiable rendering

とりあえず、球でdifferentiable renderingやってみます。
レンダリングしたら、左図の状態ですが、球の反射率を変更して、中図に近づけて、結果的に右図を作ります。
画像A 画像B画像C

下のようにコードの冒頭で、variantを設定します。ここではdifferentialbe renderingが目的なので、gpu_autodiffを使います。色はrgbとします。ここでエラーが出る人は、インストールからやりなおしです。

import numpy
import enoki
import mitsuba
mitsuba.set_variant('gpu_autodiff_rgb')

次に、xmlは調べれば誰でもわかるので、いつか別の機会に話すとして、とりあえずなのでコードに球とかライトとかもろもろを組み込んじゃいます。
このページを参考にします。
https://mitsuba2.readthedocs.io/en/latest/src/python_interface/parsing_xml.html

from mitsuba.core import Float, Thread, Bitmap, Struct,ScalarTransform4f
from mitsuba.core.xml import load_file
from mitsuba.python.util import traverse
from mitsuba.python.autodiff import render, write_bitmap, Adam
import time

from mitsuba.core.xml import load_dict

scene = load_dict({
    "type" : "scene",
    "myintegrator" : {
        "type" : "path",
    },
    "mysensor" : {
        "type" : "perspective",
        "near_clip": 0.10,
        "far_clip": 10.0,
        "to_world" : ScalarTransform4f.look_at(origin=[0, 0, 5],
                                               target=[0, 0, 0],
                                               up=[0, 1, 0]),
        "myfilm" : {
            "type" : "hdrfilm",
            "rfilter" : { "type" : "box"},
            "width" : 512,
            "height" : 512,
        },
        "mysampler" : {
            "type" : "independent",
            "sample_count" : 4,
        },
    },
    "myemitter" : {
        "type" : "point",
        "intensity" : 100.,
        "position" :  [5, 5, 5],

    },
    "myshape" : {
        "type" : "sphere",
        "mybsdf" : {
            "type" : "diffuse",
            "reflectance" : {
                "type" : "rgb",
                "value" : [0.5, 0.5, 0.5],
            }
        }
    }
})

ここまでで、オブジェクトのもろもろは終わりです。
次にdifferentiableの過程を保存するフォルダを作ります。

# output folder
outputfolder = 'output/'
import os
if not os.path.exists(outputfolder):
    os.mkdir(outputfolder)

outputというフォルダを作って、その中に画像を保存します。
ここからが、目的のコードです。
まず、何がdifferentiableなパラメタかを確認するために、シーンをチェックします。
そのコードが以下です。

# Find differentiable scene parameters
params = traverse(scene)
print(params)

これを実行すると、このような文字列が現れます。それぞれシーンの要素で、*が先頭についているものがdifferentialbeです。

ParameterMap[
* PointLight.intensity.value,
PerspectiveCamera.near_clip,
PerspectiveCamera.far_clip,
PerspectiveCamera.focus_distance,
PerspectiveCamera.shutter_open,
PerspectiveCamera.shutter_open_time,
Sphere.to_world,
* Sphere.bsdf.reflectance.value,
]

ということで、differentialbeのものを選びます。ここでは球の反射率を選択します。
具体的な関数の意味とかはドキュメントに書いてありますので割愛です。

# to optimize val
optparam = 'Sphere.bsdf.reflectance.value'

# Discard all parameters except for one we want to differentiate
params.keep([optparam])
print(params)

これで反射率だけを最適化するパラメタとして絞ることができました。
次に、レンダリングの画像サイズと目指す画像の読み込みと、最適化対象の反射率の初期化をします。

#camera sensor
crop_size = scene.sensors()[0].film().crop_size()

#reference
bitmap_ref = Bitmap('myscene/r.png').convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, srgb_gamma=False)
image_ref = numpy.array(bitmap_ref).flatten()

# Change parameter 
params[optparam] = [0.5, 0.5, 0.5]
params.update()

このあたりで書くのが面倒になってきたので、ざっと行きますが、Adamで、レンダリングの結果と目標の画像を比較して、この差が最小となるように、最適化します。
逐次、結果を先に設定した出力フォルダに保存していきます。

# Construct an Adam optimizer that will adjust the parameters 'params'
opt = Adam(params, lr=.02)
time_a = time.time()
iterations = 100
for it in range(iterations):
    # Perform a differentiable rendering of the scene
    image = render(scene, optimizer=opt, unbiased=True, spp=3)
    write_bitmap(outputfolder + 'out%03i.png' % it, image, crop_size)
    ob_val_t = enoki.hsum(enoki.sqr(image - image_ref)) / len(image)
    # Back-propagate errors to input parameters
    enoki.backward(ob_val_t)
    # Optimizer: take a gradient step
    opt.step()
    print('Iteration %03i, %s' % (it, params[optparam]), end='\r')

100回ループしたら、最後に、時間を計測して終了です。

time_b = time.time()
print('%f ms per iteration' % (((time_b - time_a) * 1000) / iterations))

結果は、以下のような様子です。gifアニメなので画質があれですが、実際はもっと階調が豊かです。

どんどん目標画像に向かうのがわかります。

これ、何が起きているんだ?と思うかと思いますが、連続的なパラメタ指定できるものを何でも微分できるようなフレームワークである、autodiffを使って、勾配方向を推定し、勾配法でコストが最小となるようにパラメタを寄せていっています。

今回の実験設計

さて、上のテストでdifferentiable renderingの使い方が分かったので、次は、単一視点のカメラではなく、複数の視点を与えてみます。
それで、視点ごとに色が変わるようなオブジェクトを作ってみます。

コンピュータグラフィックスの研究で、光の吸収率の異なる要素をオブジェクトに良い感じに分布した状態で視点を変えると、同一オブジェクトでも違う見え方になることが明らかになっています。
極論、ホログラムみたいな効果を出すことができるようになります。
こういうものが発展していくと、たとえば、背景に合わせた画像を出力するマントを作れば、纏った者はステルス状態になり究極のボッチを楽しめたり、ひとつの看板であるにもかかわらず見る方向で表示内容が変わり、災害時にどこから見ても適切な逃げ道を提示するような表示ができたりします。
このようなものの基礎となる状態をdifferentiable renderingで作ってみます。

実験

オブジェクト表面を(誘電体と訳すのでしょうか)dielectricという透明度のあるマテリアルを設定し、光の吸収率の分布をテクスチャで与えるようなシーンを作ります。
また、視点とそれに対応する画像も与えます。
テクスチャを最適化のパラメタとして、狙いの画像に近づくように最適化することで、視点ごとに異なる模様が見えるようになるはずです。

stanford bunnyモデルで実験してみます。環境マップはmitsuba公式サイトにあるものを用います。以下が初期状態です。

上のレンダリング結果を下のようなRGBの縞模様を目指して、モデルの透明度を変更してみます。

データの入力とか設定なんかは、もうこの際、説明するのもだるいので皆様の想像にお任せするとして、最適化のループだけ書いておきます。
ここでは、3画像を目指すので、一回の最適化ループの中で、3回のdifferentiable renderingを実行します。
感覚的な説明になりますが、全パラメタを同時に最適化をせずに、パラメタの部分ごとの最適化を繰り返して収束させます。もしかしたら発散するかもしれませんが、それは仕方ありません。どんなときに発散するかの条件も知りません。

iterations = 100
for it in range(iterations):
    ob_val = 0.0
    for ind in range(len(image_ref)):
        # Perform a differentiable rendering of the scene
        image = render(scene, optimizer=opt, unbiased=True, spp=3, sensor_index = ind)
        write_bitmap(outputfolder + 'out%i_%03i.png' % (ind, it), image, crop_size)
        ob_val_t = enoki.hsum(enoki.sqr(image - image_ref[ind])) / len(image)
        ob_val = ob_val+ob_val_t
        # Back-propagate errors to input parameters
        enoki.backward(ob_val_t)
        # Optimizer: take a gradient step
        opt.step()

        #constraint
        params[optparam] = numpy.where(params[optparam]>1., 1., params[optparam])
        params[optparam] = numpy.where(params[optparam]<0.01, 0.01, params[optparam])

    write_bitmap(outputfolder + 'texture_%03i.png' % it, params[optparam], (im_res[0], im_res[1]))
    print('Iteration %03i, %s' % (it, ob_val), end='\r')

ちなみに、image_refには、目標画像となるRGBの縞模様がそれぞれ入っています。
また、テクスチャとして透明度を操作しているので、テクスチャが[0,1]の間に入ってほしいです。そのため、numpyで1から0の間の値に押し込んでいます。

結果

一つのオブジェクトに対して、透明度テクスチャをパラメタとして、differentiable renderingしてみた結果をここに示します。

まず最適化の過程を見てみます。
下の図は、それぞれ異なる視点のカメラで単一のオブジェクトを見ています。

最適化が進むについれて、RGBの縞模様が現れてくるのがわかります。
各視点では、以下の見た目になります。止めていればいい感じに見えます。

最後に得られた透明度で、視点を回してみます。狙いの角度の時は、それっぽい絵が出てますが、ほかはいまいちです。一瞬を見逃したらもう色が違うので、わけがわかりません。

とりあえず、奥側の面に色が付いたり、屈折で光が届くところに色がついていたりして、指定の視点のみで成立するような結果が得られました。
differentiable renderingすごい。
ということで、ここで終わりです。

終わりに

この記事では、Mitsuba 2でinverse rendering してみました。とは言っても、例を読んで、リファレンスを読んで修正したくらいなので、特別、新しいことはしていません。
これくらいのことなら、健康な方なら誰でも実装できるだろうが、その辺は遊びだと思って許容してくれるとうれしいです。
ちなみに、最後の視点を移動させる操作は、カメラの座標をエクセルで作って、xmlに貼り付けました。やっぱりPythonわからんです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのオブジェクトからplantumlのオブジェクト図を生成出力する

オブジェクト図

Pythonでは、値、変数、関数、クラス、メソッド、モジュールなど、すべてがオブジェクトで、メモリ上に存在するものです。
メモリ上にどんなオブジェクトが存在しているのか把握することで、Pythonの動作を理解したり、実行状態を把握することができます。
実際、他の方のPython記事に対して、変数辞書からのオブジェクト図を示してコメントすることがあります。

そこで、指定したオブジェクトからplantumlのオブジェクト図(テキスト形式)を生成出力するプログラムを作ってみました。
なお、plantumlから画像に変換するには別の作業が必要です。

色の説明:

  • ピンク: 出力指定したオブジェクト
  • 紫: 関数
  • 緑: クラス
  • 青: インスタンス
  • オレンジ: 変数辞書

プログラム

plantumlオブジェクト図生成出力スクリプト

objectuml.py
import sys
import importlib


def empty_function():
    pass


class EmptyClass:
    pass


_exists = []

FUNCTION = type(empty_function)
NOLINK_TYPES = {
    'method', 'staticmethod', 'classmethod', 'ABCMeta',
    'mappingproxy', 'member_descriptor', 'wrapper_descriptor',
    'builtin_function_or_method',
}
BUILTIN_TYPES = {
    'object', 'NoneType', 'bool', 'int', 'float', 'complex', 'str',
    'list', 'tuple', 'dict', 'set', 'function', 'type', 'module',
    *NOLINK_TYPES,
}
BUILTIN_MEMBER = {name
                  for names in map(dir, (dict, empty_function, EmptyClass))
                  for name in names}
BUILTIN_MEMBER.update(globals())

BLUE = '#c6ffff'
GREEN = '#c6ffc6'
ORANGE = '#ffe2c6'
PURPLE = '#c6c6ff'
PINK = '#ffc6ff'


def link(obj):
    if obj is None or obj == {}:
        return '=> {obj}'
    if type(obj).__name__ in NOLINK_TYPES:
        return f'=> {type(obj).__name__}'
    try:
        if obj.__name__ in NOLINK_TYPES:
            return f'=> {obj.__name__}'
    except:
        pass
    dump(object_diagram(obj, color=PURPLE if isinstance(obj, FUNCTION) else BLUE))
    return f'*--> {id(obj)}'


def number_vars(obj):
    yield f'real => {obj.real}'
    if isinstance(obj, complex):
        yield f'imag => {obj.imag}'


def str_vars(obj):
    yield f'~__repr__() => {repr(obj)}'


def dict_vars(obj):
    for key, value in obj.items():
        if not (key in BUILTIN_MEMBER or
                key.startswith('__') and key.endswith('__')):
            yield f'[{repr(key)}] {link(value)}'


def set_vars(obj):
    yield ''


def iter_vars(obj):
    for i, item in enumerate(obj):
        yield f'[{i}] {link(item)}'


def name_vars(obj):
    yield f'~__name__ => {repr(obj.__name__)}'


def object_vars(obj):
    try:
        variables = vars(obj)
    except:
        return
    dump(object_diagram(variables, ORANGE))
    yield f'~__dict__ *--> {id(variables)}'


OBJECT_VARS = {
    'int': number_vars,
    'float': number_vars,
    'complex': number_vars,
    'str': str_vars,
    'list': iter_vars,
    'tuple': iter_vars,
    'dict': dict_vars,
    'mappingproxy': dict_vars,
    'set': set_vars,
    'function': name_vars,
    'module': name_vars,
}


def class_diagram(cls, color=GREEN):
    if cls in _exists or cls.__name__ in BUILTIN_TYPES:
        return
    _exists.append(cls)
    name = f'{id(cls)}'
    base = cls.__base__
    yield ''
    yield f'map {name} {color} {{'
    yield f'~__class__ => {type(cls).__name__}'
    yield f'~__name__ => {repr(cls.__name__)}'
    if base.__name__ in BUILTIN_TYPES:
        yield f'~__base__ => {base.__name__}'
    else:
        dump(class_diagram(base))
        yield f'~__base__ *-> {id(base)}'
    if type(cls).__name__ != 'ABCMeta':
        yield from object_vars(cls)
    yield f'}}'


def object_diagram(obj, color=BLUE):
    if isinstance(obj, type):
        yield from class_diagram(obj)
        return
    if obj in _exists:
        return
    _exists.append(obj)
    name = f'{id(obj)}'
    cls = type(obj)
    yield ''
    yield f'map {name} {color} {{'
    if cls.__name__ in BUILTIN_TYPES:
        yield f'~__class__ => {type(obj).__name__}'
    else:
        yield f'~__class__ *-> {id(type(obj))}'
    if hasattr(obj, '__len__'):
        yield f'~__len__() => {len(obj)}'
    if cls.__name__ in OBJECT_VARS:
        yield from OBJECT_VARS[cls.__name__](obj)
    if cls.__name__ not in BUILTIN_TYPES:
        yield from object_vars(obj)
    yield f'}}'
    if cls.__name__ not in BUILTIN_TYPES:
        dump(class_diagram(cls))


def dump(lines):
    for line in list(lines):
        print(line)


def plantuml(obj):
    print('```plantuml')
    print('@startuml')
    dump(object_diagram(obj, color=PINK))
    print()
    print('@enduml')
    print('```')
    _exists.clear()


def main():
    if len(sys.argv) != 2:
        print(f'{sys.argv[0]} MODULE_NAME')
        return
    plantuml(vars(importlib.import_module(sys.argv[1])))

if __name__ == '__main__':
    main()

工夫点

  • ジェネレータ関数でplantuml要素を出力
  • オブジェクトリンクがあれば、リンク先をジェネレートして先にprint出力

制限事項

オブジェクトの相互参照(循環参照)があるとplantumlから図を生成できません。

使用方法

コマンド実行

以下に示す sample.py のオブジェクト図を出力したい場合、次のコマンド実行でplantumlが出力されます。

plantuml生成
$ python3 objectuml sample

標準ライブラリのオブジェクト図を出力することも可能です。
構造が複雑なライブラリはリンクが正しく出力されないので、BUILTIN_TYPES定義の追加が必要そうですが。

$ python3 objectuml math

ライブラリ呼び出し

import objectuml

objectuml.plantuml(オブジェクト図出力対象変数名)

オブジェクト図用サンプルコード

sample.py
class A:
    prop1 = 123
    prop2 = prop1
    def hoge(self):
        return 'superhoge'
    fuga = hoge

class SubA(A):
    prop1 = 777
    def hoge(self):
        return 'hoge'

a = SubA()

出力されたplantuml

@startuml

map 15810144608 #c6ffff {
~__class__ => int
real => 123
}

map 123145300012704 #c6c6ff {
~__class__ => function
~__name__ => 'hoge'
}

map 123145300427112 #ffe2c6 {
~__class__ => mappingproxy
~__len__() => 8
['prop1'] *--> 15810144608
['prop2'] *--> 15810144608
['hoge'] *--> 123145300012704
['fuga'] *--> 123145300012704
}

map 34361518792 #c6ffc6 {
~__class__ => type
~__name__ => 'A'
~__base__ => object
~__dict__ *--> 123145300427112
}

map 123145300207888 #c6ffff {
~__class__ => int
real => 777
}

map 123145300012840 #c6c6ff {
~__class__ => function
~__name__ => 'hoge'
}

map 123145300427784 #ffe2c6 {
~__class__ => mappingproxy
~__len__() => 4
['prop1'] *--> 123145300207888
['hoge'] *--> 123145300012840
}

map 34361519736 #c6ffc6 {
~__class__ => type
~__name__ => 'SubA'
~__base__ *-> 34361518792
~__dict__ *--> 123145300427784
}

map 123145300050208 #ffe2c6 {
~__class__ => dict
~__len__() => 0
}

map 123145299742280 #c6ffff {
~__class__ *-> 34361519736
~__dict__ *--> 123145300050208
}

map 123145300050064 #ffc6ff {
~__class__ => dict
~__len__() => 11
['A'] *--> 34361518792
['SubA'] *--> 34361519736
['a'] *--> 123145299742280
}

@enduml

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ベイスターズの投手の配球を分析する

経緯

ここ2,3年、野球(特に横浜DeNAベイスターズ)にはまっており、前から野球データの分析がしたいと考えていた。
試合中にTwitterでこのキャッチャーの配球は○○だから嫌い、とかよく見かけるが、私は野球をやったことがないのでよくわからない。

そこで、「配球」という観点で分析したい。

データ

データはYahooスポーツナビの一球速報を使う。
スクレイピングにより投手、打者、球の位置、球速などを取得。
今年の全公式試合のベイスターズの守備のときのデータを用意。

(以下データ一部)
image.png

配球を散布図にする

htmlのピクセル値からストライクゾーン(長方形の枠)を推定して、散布図にした。
image.png
おっと、、、サイト側の都合なのか謎の区切りが出現した。
とりあえずボール判定されたボールがストライクゾーンの外に行っている(若干ずれているが気にしない)ので、これであっているだろうとして続けることにする。

打者の左右で比べてみる

右打者(10040球)、左打者(5840球)で比べてみる。

image.png
分かりずらいのでヒートマップにすると、

image.png

両方ともいわゆるアウトローに投げられている。左打者に対してはストライクゾーンの外低めにも多く投げられている。

ピッチャーの比較

ベイスターズの今年の投球数ランキング上位3人(大貫選手、井納選手、濱口選手)の配球を比較してみる。
image.png

こちらもヒートマップにしてみる。
image.png

今年活躍した大貫投手はアウトロー、ストライクゾーン低めにしっかり投げている。ストライクゾーン中段にも多く投げているらしい。
井納投手もアウトロー、ストライクゾーン低めに多く投げられている。
濱口投手はほか2選手に比べストライクゾーン外の配球が多い。確かに四球が多い印象。

今後

球速や球種などの分析をしたいし、いつかは予測モデルなんかに挑戦してみたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ベイスターズの投手の配球をスクレイピングして可視化する

経緯

ここ2,3年、野球(特に横浜DeNAベイスターズ)にはまっており、前から野球データの分析がしたいと考えていた。
試合中にTwitterでこのキャッチャーの配球は○○だから嫌い、とかよく見かけるが、私は野球をやったことがないのでよくわからない。

そこで、「配球」という観点で分析したい。

データ

データはYahooスポーツナビの一球速報を使う。
スクレイピングにより投手、打者、球の位置、球速などを取得。
今年の全公式試合のベイスターズの守備のときのデータを用意。

スクレイピングのコード : https://github.com/remihp/baseball-data/blob/main/baseball_scraping.ipynb

(以下データ一部)
image.png

配球を散布図にする

htmlのピクセル値からストライクゾーン(長方形の枠)を推定して、散布図にした。
image.png
おっと、、、サイト側の都合なのか謎の区切りが出現した。
とりあえずボール判定されたボールがストライクゾーンの外に行っている(若干ずれているが気にしない)ので、これであっているだろうとして続けることにする。

打者の左右で比べてみる

右打者(10040球)、左打者(5840球)で比べてみる。

image.png
分かりずらいのでヒートマップにすると、

image.png

両方ともいわゆるアウトローに投げられている。左打者に対してはストライクゾーンの外低めにも多く投げられている。

ピッチャーの比較

ベイスターズの今年の投球数ランキング上位3人(大貫選手、井納選手、濱口選手)の配球を比較してみる。
image.png

こちらもヒートマップにしてみる。
image.png

今年活躍した大貫投手はアウトロー、ストライクゾーン低めにしっかり投げている。ストライクゾーン中段にも多く投げているらしい。
井納投手もアウトロー、ストライクゾーン低めに多く投げられている。
濱口投手はほか2選手に比べストライクゾーン外の配球が多い。確かに四球が多い印象。

ここまでのコードは
https://github.com/remihp/baseball-data/blob/main/ball_distribution.ipynb

今後

球速や球種などの分析をしたいし、いつかは予測モデルなんかに挑戦してみたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoで構築したWEBサーバに最低限のセキュリティ対策を施す ① ログイン認証

はじめに

本稿ではDjangoで構築したサーバに以下の最低限のセキュリティ対策を行う手順を解説します。(DjangoでWebアプリケーション構築済である前提とします。)

  • ログイン認証の追加
  • SSL対応 (記事作成中)

ログイン用アプリケーションの作成

まずプロジェクト配下でログイン機能用のアプリケーションを作成する。

$ python manage.py startapp accounts

アプリケーション作成後のプロジェクト構成は以下。

sample_project
├── accounts <- ログイン用アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── urls.py
│   └── views.py
├── sample_app <- 固有アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── views.py
├── config
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

プロジェクト配下のsetting.pyとurls.pyにログイン用アプリケーション用の設定を追記。

setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'sample_app.apps.SampleAppConfig', # Custom App
    'accounts.apps.AccountsConfig',    # 追加
]
config/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('sample_app/', include('sample_app.urls')), # Custom App
    path('', include('accounts.urls')),              # 追加 ログイン用
]

ログイン用フォーム、ビュー、テンプレートの作成

以下、ログイン画面の部品をそれぞれ作成する。

  • ログイン用フォームの定義:forms.py
  • ログイン用ビューの定義:view.py
  • ログイン画面用テンプレート:login.html

フォームの定義はDjango標準クラスのAuthenticationFormを用いる。

forms.py
from django.contrib.auth.forms import AuthenticationForm

class LoginForm(AuthenticationForm):
    """ログインフォーム"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['placeholder'] = field.label   

importしているクラスは以下。

クラス名 機能
LoginRequiredMixin ログインしたユーザだけ閲覧できるようにする
LoginView ログイン機能
LogoutView ログアウト機能
LogoutForm 上記で定義したログイン用フォーム
view.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import(LoginView, LogoutView)
from .forms import LoginForm

class Login(LoginView):
    """ログインページ"""
    form_class = LoginForm
    template_name = 'login.html'


class Logout(LoginRequiredMixin, LogoutView):
    """ログアウトページ"""
    template_name = 'login.html'
login.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>LOGIN</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <form action="" method="POST">
        {% csrf_token %}
        {% for field in form %}
            {{ field }}
        {% endfor %}
        {% for error in form.non_field_errors %}
            {{ error }}
        {% endfor %}
        <button type="submit">ログイン</button>
    </form>
</body>
</html>

フォーム、ビュー、テンプレート追加後のプロジェクト構成は以下。

sample_project
├── accounts <- ログイン用アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── login.html
│   ├── urls.py
│   └── views.py
├── sample_app <- 固有アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── views.py
├── config
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

ログイン用アプリケーションのurls.pyにURLとビューの紐づけを追記する。

accounts/urls.py
urlpatterns =[
    path('login/', views.Login.as_view(), name='login'),
]

リダイレクトURLの設定

setting.pyに未ログイン時に表示されるURL、ログイン・ログアウト時のリダイレクトURLを設定する。

setting.py
LOGIN_URL = '/login' # ログインしていないときのリダイレクト先
LOGIN_REDIRECT_URL = '/sample_app/index' # ログイン後のリダイレクト先
LOGOUT_REDIRECT_URL = '/login' # ログアウト後のリダイレクト先

ログイン認証を追加するアプリケーションの設定

ログイン先のアプリケーションのビューにログイン必須設定を行う。
クラスベースのビューの場合、LoginRequiredMixinをimportして継承することでログイン必須となる。

sample_app/view.py
from django.contrib.auth.mixins import LoginRequiredMixin

class Test(LoginRequiredMixin, TemplateView):
    template_name = 'index.html'

メソッドベースのビューの場合、@login_requiredのアノテーションを付与することでログイン必須となる。

from django.shortcuts import render
from django.contrib.auth.decorators import login_required

@login_required 
def index(request):
    return render(request, 'index.html')

ユーザーの作成

プロジェクト配下でユーザーを作成する。

$ python manage.py createsuperuser

テスト

これでアプリケーションを起動してログインURLにアクセスするとログインフォームが表示されるはずなので、作成したユーザーのID/Passでログインできればログイン認証成功です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeetCodeに毎日挑戦してみた 111. Minimum Depth of Binary Tree(Python、Go)

Leetcodeとは

leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。

golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)

26問目(問題111)

111. Minimum Depth of Binary Tree

問題内容

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

Note: A leaf is a node with no children.

(日本語訳)

二分木が与えられたら、その最小の深さを見つけます。

最小深度は、ルートノードから最も近いリーフノードまでの最短パスに沿ったノードの数です。

注: リーフは子のないノードです

Example 1:

img

Input: root = [3,9,20,null,null,15,7]
Output: 2

Example 2:

Input: root = [2,null,3,null,4,null,5,null,6]
Output: 5

考え方

  1. 再帰処理を用います

  2. rootのleft,rightをそれぞれ潜っていって、noneになったらreturnします。

  3. 左右どちらもnoneでなかったらreturnされた合計の小さい方をreturnします

  4. 最終的に最小の深さを戻り値とします

解答コード

class Solution:  
    def minDepth(self, root):
        if root == None:
            return 0
        if root.left==None or root.right==None:
            return self.minDepth(root.left)+self.minDepth(root.right)+1
        return min(self.minDepth(root.right),self.minDepth(root.left))+1
  • Goでも書いてみます!
func minDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }
    if root.Left == nil {
        return minDepth(root.Right) + 1
    }
    if root.Right == nil {
        return minDepth(root.Left) + 1
    }

    return min(minDepth(root.Right), minDepth(root.Left)) + 1
}

func min(a int, b int) int {
    if a < b {
        return a
    }
    return b
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

spaCy 業務で使える自然言語処理ライブラリ

こんにちワンコ。
ちゅらデータ株式会社の山内です。

この記事は、"ちゅらデータアドベントカレンダー"の16日目です。
私の好きな分野の一つ「自然言語処理」について書きたいと思います。

はじめに

自然言語処理は、データ分析の様々な研究分野の中でも大きく注目されているテーマで、日々たくさんの論文が発表されていますが、最近では、実際の業務レベルでの要望、課題解決の手段としても利用されることが多くなってきました。

この記事は、そのような業務で利用される自然言語処理のライブラリのひとつ「spaCy」について書きたいと思います。

spaCyについて

spaCy は Python と Cython で実装されたオープンソースの自然言語処理ライブラリです。
プロダクト環境で利用するためにデザインされていることが特徴的です。
本家サイトには、自然言語処理の「Ruby on Rails」のようなものだと記載されています(^^;)

私が spaCy を利用しようと考えたのも、「プロダクト環境で利用できる」という点で目についたからです。

NLPのライブラリには、NLTK や CoreNLP などの有名なものがいくつかありますが、研究で利用されることを目的に実装されています。
これらは、BERTやTransformerをお試しで実行するには便利なライブラリですが、
顧客の用意した自由なフォーマットデータに対して前処理を行う場合や、ビジネスロジックに沿ったデータオブジェクトを実装したりするといった用途では使い勝手が良いとは言えません。

spaCyを使うことで、業務コードを便利に実装することができます。

具体的な特徴

自然言語処理のさまざまな概念が基本APIとして用意されている

下記の図は、spaCyのアーキテクチャ図です。
ドキュメントやトークン、形態素などの基本オブジェクトや、
トーカナイザー、Tagger(品詞のタグ付などに利用)、Dependency Parser(構文解析)、固有表現抽出など、設計段階で構想されています。

これらは、処理を実行するための具体的な実装だけでなく、
ビジネスロジックを拡張実装するためのベースコードとして利用できるようになっています。

image
https://spacy.io/api#_title Library architectureより

「Span」は、単語分割がややこしい日本語NLPではとても便利な概念です。
https://spacy.io/api/span

パイプライン処理

NLPで必須な前処理に関しても便利な機能がたくさんあります。
実用で必須なパイプライン処理は、使いやすく独自処理の追加も簡単な実装で実現することができます。
image

ルールベースの利用

深層学習モデルに注目があつまるNLPですが、
業務要件では、ルールベース(単純な単語マッチなど)や正規表現を利用した手法も課題解決のため必要な実装です。
辞書を用いたMatcherや、辞書や言語資源をを効率的に扱うために、 Vocab , StringStore などのデータ格納のための概念も設計に組み込まれています。
https://spacy.io/usage/rule-based-matching

モデルの組み込み

統計モデルやML、深層学習系モデルを組み込むことも想定されており、
言語ごと(英語、ドイツ語など)のモデルや、いくつかのアルゴリズムがすぐに利用できる状態で実装されています。
もちろん、自前のモデルも利用することが可能です(使い方にやや癖があります)
https://spacy.io/models

処理の効率化

処理速度やメモリ効率化のため、Cythonでの実装を筆頭に、巨大データを扱うための実装や、データ格納時サイズ削減の機構など(Hashing化)、実運用を見据えた様々な工夫がなされています。

おわりに

以上、本記事はspaCyの紹介のみに終わってしまいますが、
プロダクトレベルで実装で役立つライブラリーとして、spaCyの良さが少しでも伝わっていると幸いです。

弊社では、実際のプロダクトコードとしての利用や、この設計を元に独自ライブラリの実装の参考にしたりと便利に利用しています。

データ分析業界は、「AI」というバズワードが先行していた夢物語のフェーズから、
実際に業務課題に対してどのように実装するか、機能としてどう組み込むかといったフェーズに入っています。

これからは、spaCyのような実務で使えて役に立つOSSライブラリがたくさん開発されていくことでしょう。

ちゅらデータもそういったライブラリやサービスをどんどん開発して顧客やデータ分析業界の力になれるよう日々技術磨いています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FastAPIで作ったアプリをApp ServiceとAzure Database for MySQL上にデプロイする

はじめに

こちらの記事は求ム!Pythonを使ってAzureで開発する時のTips!【PR】日本マイクロソフト Advent Calendar 2020の16日目の記事です。
最近触り始めたFastAPI.このフレームワークで作成したサンプルアプリをAzure上にデプロイすべく手順をまとめてみます。
サンプルアプリと言いつつもHelloWorldだけだと少し味気ないのでDBが絡むような簡単なコードを実装してみました。

動作環境

以下のシステム要件を想定しております。

  • Docker最新版 ※ローカルで動作確認を行うため
  • App Service on Linux(Python3.7系)
  • Azure Database for MySQL5.7
  • VSCodeのApp Service拡張機能

手順

ローカルでの動作確認

(12/16執筆時点でローカルでエラーとなるようなので解消後更新します、)
* ここからローカルにソースをクローンします。
* .env.sample から .envをコピーする。※編集はこの後行います。
* docker-compose up --buildで起動します。
* 起動したら http://localhost:8000/ にアクセスし {"message":"Hello World"} が表示されたらOKです。

App Serviceを作成

  • こちらの手順を元にApp Service、WebAppsを作成します。ランタイムは python3.7 としてください。

Azure Database for MySQLを作成

  • Azure Potalにアクセスします。
  • 左上のハンバーガーメニューから[リソースの作成]をクリックし、[データベース]を選択ください。 image.png
  • Azure Database for MySQLをクリックし、[単一サーバー] or [フレキシブルサーバー]の選択となります。今回はサンプルということもあるので[フレキシブルサーバー]にしたいと思います。
    image.png

  • 基本設定は下記の通りとしてます。

    • サブスクリプション[任意]
    • リソースグループ[任意]
    • サーバ名[任意] ※こちらは控えておいてください
    • リージョン[先ほど作成したApp Serviceと同一リージョン]
    • ワークロードの種類[実稼働(小規模/中規模)]
    • MySQLバージョン[5.7]
    • 管理者アカウント[任意] ※こちらは控えておいてください image.png
  • SKUは下記の通りとしてます。※今回はミニマムとしてます。

    • Computer Tier[バースト可能]
    • コンピューティングサイズ[Standart_B1s]
    • ストレージサイズ[10GB] image.png
  • ネットワーク設定ですが、今回は[パブリックアクセス]とし、 AppServiceからの接続とローカル(開発端末) からのみ許可しました。要件によってはVNET IntegrationとPrivate Endpointを組み合わせてセキュアにしても良いかもしれません。送信元IPはこちらのページを参考に確認ください。
    image.png

DB初期設定

  • 下記の手順で初期設定を行います。
# 接続
mysql -h fastapi.mysql.database.azure.com -u <ユーザ名> -p

# DB作成 ※こちらは控えておいてください。
create database <DB名>;

# DB指定
use <DB名>;

# テーブル作成
CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL DEFAULT '',
    email VARCHAR(255) NOT NULL DEFAULT '',
    password VARCHAR(255) NOT NULL DEFAULT '',
    token TEXT,
    created_at datetime DEFAULT CURRENT_TIMESTAMP,
    updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

# 初期データ追加
INSERT INTO users (name, email, password) VALUES ("admin", "admin@example.com", "<hash password>");
※passwordの作成方法は後述します。

環境変数の設定とアップロード

  • ローカルでの動作確認の項で作成した .env を下記の通り更新します。
.env
...
SECRET_KEY="<任意の文字列>"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=60

# MySQL
MYSQL_SERVER="<Azure Database for MySQLのサーバー名>"
MYSQL_USER="<Azure Database for MySQLのユーザ名>"
MYSQL_PASSWORD="<Azure Database for MySQLのパスワード>"
MYSQL_DB="<Azure Database for MySQLのスキーマ名>"
...

※フレキシブルサーバの場合はユーザ名は単一サーバー版とは異なり、hoge@hostnameではなく、hogeのみで良さそうです。

更新後、VSCodeからアップロードします。
image.png

スタートアップファイルの設定

Webappsの[構成]ブレードの全般設定タブのスタートアップコマンドに startup.txt を登録しておきます。
image.png

startup.txtの中身はこのような感じです。必要に応じてオプションを付け足してみてください。 オプションの詳細はこちら

startup.txt
python -m uvicorn src.main:app --host 0.0.0.0

ソースのデプロイ

VSCodeからアップロードします。赤囲みの矢印ボタンをクリックし
image.png
デプロイ中は[output]タブにて進行状況が確認できます。
image.png

デプロイが正常終了するとこのような表示になります。
TOPページ
image.png

docsページ
image.pngimage.png

試しにJWTを取得するAPIを叩いても問題なくレスポンスが返ってきます。
image.png

補足

  • パスワードの作成について
    passlibを用いハッシュしてます。 pipでpasslib, bcryptをそれぞれインストールした上で下記を実行すると作成できます。
import passlib.hash

secret = "plain text"
salt = "saltsalt"
hashed = passlib.hash.sha512_crypt.hash(secret, salt=salt)
print(hashed)

最後に

FlaskやDjangoをApp Serviceにデプロイする記事は見かけることはありましたが、FastAPIの記事はまだ少ない印象でしたので、参考になれば幸いです。
サンプルコードがまだまだ成熟し切っていないので、さわりだけ掴んでもらえればと思います。
雛形にできるよう今後も少しずつ改善してきます。

参考

3:Visual Studio Code から App Service を作成する
IPアドレスを見つける

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのオブジェクトを一時的に保存し、別のPythonで再利用する

この記事はTakumi Akashiro ひとり Advent Calendar 2020の16日目の記事です。

遂に数日分あった当アドカレのストックも切れ、
朝に事前投稿できなくなるほど、締め切りに追われるようになりました!

追い込まれている感じが楽しいですね!

始めに

みなさまはPythonのオブジェクトを一時保存したいと思ったことはないでしょうか?

例えば、デバッグとか……いや、今はDCCツールならVSCodeでptvsd経由でデバッグするのがメジャー1ですもんね……
例えば、Mayaで使った複雑な構造の変数を他プロセスのPythonで使いたいとか……これはギリギリありそうですね。
今日はこれを想定にやっていきましょう!

シリアライズ とは

オブジェクト指向プログラミングにおいて、あるオブジェクトをファイルとして書き出すことをシリアライズ、と呼ぶそうです。

Wikipedia曰くは以下のように書かれています。

シリアライズ - Wikipedia
第二の意味の直列化(ちょくれつか)は、オブジェクト指向プログラミングにおいて使われる用語で、ある環境において存在しているオブジェクトを、バイト列やXMLフォーマットに変換することをいう。より具体的には、そのオブジェクトの状態を表す変数(フィールド)と、場合によってはオブジェクトの種類(クラス)を表すなんらかの識別子を、ファイル化できるようなバイト列やXMLフォーマットの形に書き出すことをいう。これにより、オブジェクトの表すデータを、ファイルとしてセーブしたり、ネットワークで送信したりすることができるようになる。このようにして得られたバイト列やXMLフォーマットは、直列化復元ないしデシリアライズによって、元のオブジェクトに復元される。

また、オブジェクトを直列化してファイルなどの永続記憶に保存することを永続化という。`

…自分は単語自体は知っていましたが、今日の記事のために調べて、初めてオブジェクト指向の用語ってのは知りましたね。

そしてPythonでserializeを行ってくれる標準モジュールは……pickleです!!
とりあえず使っていきましょう!

pickle を使ってみる

まずは保存してみましょう!

#! python3.8
import pickle
import tempfile
import pathlib
import re

data = {"nemui": "nero", "ohayou": "oyasumi", "nanka": "kanka", "hoge": pathlib.Path("d:/"), "reg": re.compile("\D{2}\d{16}")}

with open(pathlib.Path(tempfile.gettempdir()) / 'sample.pickle', 'wb') as f:
    pickle.dump(data, f)

てきてますね、次はこれを読んでみましょう。
image.png

#! python3.8
import pickle
import tempfile
import pathlib

with open(pathlib.Path(tempfile.gettempdir()) / 'sample.pickle', 'rb') as f:
    data = pickle.load(f)

for k, v in data.items():
    print(f"\t{k}:\t{v} ({type(v)})")

image.png

ちゃんと変数を復元できていますし、Python3のdictの実装通りに順番も保持されてますね!
(実装側の都合で順番が保持される特性がついたわけだから、当然か……)

Python3 で出力した pickle をPython2で読んでみる

さあ、これをPython2に持っていってみましょう!
アッ……今の私用PCにPython2が入ってねえ!

仕方ない、古いHoudini内で動かすか……
image.png

tempの位置が違うんか、お前……
と茶番(?)はさておき、気を取り直して、今度こそPython2で実行してみましょう。

#! python2
import pickle
import tempfile

with open(tempfile.gettempdir() + '/sample.pickle', 'rb') as f:
    data = pickle.load(f)

for k, v in data.items():
    print("\t{0}:\t{1} ({2})".format(k, v, type(v)))

image.png

「プロトコル ガ チガイマス」 とエラーが出てきます!

やったね!(?)

公式ドキュメントを読んでみる

困ったときは公式ドキュメントを読んでみましょう!

pickle --- Python オブジェクトの直列化 — Python 3.9.1 ドキュメント
現在、pickleに使用できるプロトコルは6種類あります。
使用されるプロトコルが高ければ高いほど、生成されたpickleを読むために必要なPythonの最新バージョンが必要になります。

  • プロトコルバージョン3はPython 3.0で追加されました。
    これはbyteオブジェクトを明示的にサポートしており、Python 2.xではpickleを解除できません。

  • プロトコルバージョン4はPython 3.4で追加されました。
    非常に大きなオブジェクトのサポート、より多くの種類のオブジェクトのピックリング、いくつかのデータフォーマットの最適化が追加されました。これはPython 3.8から始まるデフォルトのプロトコルです。プロトコル4による改善点についてはPEP 3154を参照してください。

  • プロトコルバージョン 5 は Python 3.8 で追加されました。
    帯域外データのサポートと帯域内データの高速化が追加されました。プロトコル 5 の改良点については、PEP 574 を参照してください。

(中略)

pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)

というわけなので、pickle(data, f, protocol=2)と置き換えると……

image.png

アッ…そうだ…pathlibはPython2系にはなかった……
仕方ないので、pathlibにしてたパスをstringに置き換えると

image.png

python3の変数も無事、Python2系で読めましたね!
(もうPython2とか使わなくなるのに、このくだり要る?)

締め

自分もごくまれにしか使わないですが、まあまあ便利なのでお勧めです。

まあ、簡単な構造の変数だったり、速度を犠牲にしていいのであれば、
個人的には可読性の高いjsonの方が好みですけど。

トラブったときに対応しやすいですし。


  1. 今年はほぼPythonに向き合ってないので、この方法すら時代遅れなのかもしれない……と恐怖しながら書いてます。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

17行でTwitterのトレンドを取得する

最近は寒くなってきたので暖房をつけ始めました。

目標

アドベントカレンダー17日目ということで、何かと話のきっかけになるTwitterのトレンドも17行で取得したいと思います。

環境

Python 3.9.1

準備

①tweepyのインストール

pip install tweepy

②Twitter APIの認証コードの取得

下のサイトが分かりやすいのでお勧めです。
Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報

実装

日本のTwitterのトレンドを50位まで表示します。

コード全体

trend.py
import tweepy
import random

CK="取得したConsumer Key"
CS="取得したConsumer Secret"
AT="取得したAccess Token"
AS="取得したAccess Token Secret"

auth = tweepy.OAuthHandler(CK, CS)
auth.set_access_token(AT, AS)
api = tweepy.API(auth)

woeid = {
    "日本": 23424856
}

for area, wid in woeid.items():
    print("--- {} ---".format(area))
    trends = api.trends_place(wid)[0]
    for i, content in enumerate(trends["trends"]):
        print(i+1, content['name'])

解説

ここに、Twitter Developerのページで取得したTwitter APIの認証コードを入れます。

CK="取得したConsumer Key"
CS="取得したConsumer Secret"
AT="取得したAccess Token"
AS="取得したAccess Token Secret"

tweet範囲の設定

woeid = {
    "日本": 23424856
}

結果

できました!

$ python trend.py
--- 日本 ---
1 #あいつ今何してる
2 ヴリトラ
3 #にじホロ宇宙人狼
4 福原くん
5 #kuizy
6 #あなたの苦手なタイプの人間を診断
7 #三代目クリパするよ全員集合
8 梅ちゃん
9 ベストナイン
10 リア充カップル
11 ヤバめ地雷人間
12 ウェイ系陽キャ
13 根暗コミュ障
14 アランバローズ
15 スピナー
16 声の小ささ
17 存在感の薄さ
18 ガチの陰キャ
19 あがり症
20 十文字槍
21 全日本2歳優駿
22 梅原さん
23 ARASHI Widget
24 nanaoさん
25 寮母さん
26 真田の槍
27 ボクサー
28 クリスマスリリィ
29 マルタさん
30 GoToイベント
31 新刀剣男士
32 私のスコア
33 デュアリスト
34 梅原裕一郎
35 莉犬くん
36 マイナカード
37 三枝明那
38 角田裕毅
39 横浜流星
40 リリィちゃん
41 対象小中学生
42 福原さん
43 ランリョウオー
44 俳優部門
45 公立小学校
46 ルーチェドーロ
47 嵐ウィジェット
48 全学年35人学級
49 運動能力テスト
50 丸ちゃん

参考

Tweepyドキュメント

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blenderと立体漫画のメモ

この記事は Blender Advent Calendar 2020 17日目の記事です。

立体漫画について

「立体漫画」と言われる漫画は色々あるわけですが、最初の立体漫画(3-D Comic Book)は1953年にアメリカのKubertとMaurerによって作られたものだと言われています。
Mighty-new-anaglyph.gif
http://www.3dfilmarchive.com/home/images-from-the-archive/comic-books
この時代、日本でも雑誌の付録なんかの形で何本か立体漫画が作られたようですが、まとまった資料を見つけることはできませんでした。
https://kazekozoo.hatenablog.com/entry/2020/11/19/085930
自分も小さい頃、学研の学習か何かで見たような気がするのですが、気のせいかもしれません。
まあそれはさておき。

以下ではBlenderを使って、ClipStudioで描いた漫画からこんな感じのアナグリフを作る方法について書いています。

本当はBlenderなんて使わずに、手作業でレイヤーを赤と青に複製して左右にずらすのが簡単というか、現実的なんですけど、ここは敢えてBlenderを使います。

2020年の現在では、立体を表現する技術はアナグリフ以外にも色々ありますし、これから立体漫画を作るなら、まず共通の「立体原稿」を作成して、それをVRゴーグルとか、モバイルなら視差効果とか、デバイスに合わせた形式に出力するワークフローがいいと思うわけですよ。

アドオンの概要

そうは言っても、Blenderは初心者なんで、アドオン書くのも今回初めてですし、作りながら「これじゃダメだ」と気づいたところも多々あるのですが、どうにも時間が足りません。いろいろ残念な感じです。

サンプルも本当は自分で描けばいいのですが、時間もないのでセルシスのサイトにあるものを使わせて頂きました。

スクリーンショット.png

まずClipStudio上で、レイヤーを整理して高さ別にいくつかのグループに分けています。
マスクは3Dに変換できないので、コマ枠の外側は白に塗りつぶして誤魔化しました。ホールドアウトシェーダーとか使えばできるんだろうか。

ClipStudioからレイヤーをPSDに書き出して、アドオンでBlenderにimportして、各レイヤーを立体空間に並べて、立体視をオンにしてレンダリング……というのが今回の流れです。

スクリーンショット2.png

アドオンの基本は、丸写しですね。
こことかここを参考にしました。

bl_info = {
    "name": "Import PSD to 3D Comic",
    "description": "",
    "author": "funige",
    "version": (1, 0, 0),
    "blender": (2, 80, 0),
    "support": "TESTING",
    "category": "Import-Exoprt",
    "location": "File > Import > PSD to 3D Comic",
    "warning": "",
    "wiki_url": "",
}

import bpy
from bpy.props import StringProperty
from bpy_extras.io_utils import ImportHelper

class IMPORT_OT_psd_to_3dcomic(bpy.types.Operator, ImportHelper):
    bl_idname= "import_image.psd_to_3dcomic"
    bl_label = "Import PSD to 3D Comic"
    filename_ext = ".psd"
    filter_glob = StringProperty(default="*.psd", options={'HIDDEN'})

    def execute(self, context):
        path = self.properties.filepath

        # ここは後で
        print(path)
        return {'FINISHED'}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

# -----------------------------------------------------------------------------
# Register

def menu_fn(self, context):
    self.layout.operator(IMPORT_OT_psd_to_3dcomic.bl_idname, text="PSD to 3D Comic")

classes = [
    IMPORT_OT_psd_to_3dcomic,
]

def register():
    bpy.types.TOPBAR_MT_file_import.append(menu_fn)
    for c in classes:
        bpy.utils.register_class(c)

def unregister():
    bpy.types.TOPBAR_MT_file_import.remove(menu_fn)
    for c in classes:
        bpy.utils.unregister_class(c)

if __name__ == "__main__":
    register()

以下でexecuteの中身を書いていきます。

PSDの読み込み

PSDの読み込みをアドオンで書いたのは……失敗でした。
うちのMacBookでは2分ぐらい固まってしまいます。python遅すぎです。

import os
from psd_tools import PSDImage
from bpy_extras.image_utils import load_image
...
    def execute(self, context):
        path = self.properties.filepath
        psd = PSDImage.open(path)
        for layer in psd:
            print(layer)

        # psdと同名のフォルダを作成
        basename = bpy.path.basename(path)
        self.dir = os.path.join(os.path.dirname(path), os.path.splitext(basename)[0])
        if not os.path.isdir(self.dir):
            os.makedirs(self.dir)

        # 各レイヤーをpngに出力
        self.names = {}
        y = 0
        for layer in reversed(psd):
            if layer.is_visible():
                image = self.create_layer_image(layer, directory)

        return {'FINISHED'}

    def create_layer_image(self, layer):
        texture_name = self.get_unique_name(layer.name) + ".png"
        layer.composite().save(os.path.join(self.dir, texture_name))
        return load_image(texture_name, self.dir, force_reload=True)

    def get_unique_name(self, name):
        if name in self.names:
            self.names[name] += 1
            return name + '.' + format(self.names[name], '0>3')
        else:
            self.names[name] = 0
            return name

psd_toolsを使っているので、別にインストールが必要です。
macOSの場合、外部ライブラリのインストールは、こんな感じでした。

ここはfSPYみたいに外部ツールに分離して、アドオンにはテクスチャのパスとか座標だけ渡す形にしたらいいのではないか、と思います。

レイヤーをカメラの前に配置する

ここが一番Blenderらしい処理だと思うのですが……残念ながら詳細は省略です。
きちんと仕上げたいのですが、時間がありません。年末なのです。
アドオンのコードはGitHubにあげましたので、興味がある人は見てください。
ほぼほぼ、Blender付属の Image as Planes プラグイン(io_import_as_plane.py)からの丸写しです。

sample.png

アドオンをインストールすると、[ファイル]->[インポート]に[PSD to 3D Comic]が追加されますので、適当なPSDを読み込んでください。
なぜか全体に暗くレンダリングされることがあるのですが、原因不明です。
新規に2D Animationを作成してimportすると正しい色でレンダリングされるので、設定が足りない感じですね。

おまけ

本気で立体漫画の作成を考える人がいるかもしれないので、もうちょっとだけ。
2Dの原稿から立体漫画を作る方法としてもう一つ押さえておきたいのは、ディスプレースメントマップを使う方法です。
スクリーンショット3.png

スクリーンショット4.png

https://www.youtube.com/watch?v=wSeflVaWS28

ポリゴン数を増やす代わりに法線マップを使うのと似ていると言えば、似ているかもしれません。
これもBlenderで実装できるはずですが、今後の研究課題としておきましょう。

もっと書きたいこともあるのですが、また来年ということで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでfirestoreのデータを更新するときにハイフンを含むフィールド名を使いたい場合

前回も解決方法がわからなかって探し回ったので、簡単にgoogle検索でヒットするようにqiita記事を書く。
QiitaのSEOを間借り。

TL;DR

バッククォートで囲む!

db.collection('users').document('AAAAABBBBB').update({
   u"status.`xxxx-yyyyy-zzzz`": "hogehoge"
})

解説

いつものノリで下のようなコード書くと詰む。

db.collection('users').document('AAAAABBBBB').update({
   u"status.xxxx-yyyyy-zzzz": "hogehoge"
})
ValueError: Non-alphanum char in element with leading alpha: xxxx-yyyyy-zzzz

参考

https://github.com/googleapis/google-cloud-python/issues/8086

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pytorch初挑戦の記録、tf.kerasとの比較実装

連載中?の会議見える化シリーズこの先に進むにはPytorchやtorchaudioを使っていく必要がありそうです。そのため、Pytorchに入門してみました。その記録です。

こちらの記事では、Pytorchとtf.kerasとの比較も述べられています。それぞれお互いの長所を取り込みつつ進化しており、対応力を広げていくにはPytorchとtf.kerasのバイリンガルになっておく必要がありそうです。Udemyには、Pythonや機械学習の基礎は知ってますよ、という人向けのPytorch入門コースがあり、参考になります。

実装テーマとしては以下としています。

  • 4次関数(+乱数ノイズ)を多層パーセプトロン(MLP)で近似
  • train〜testの2分割モデル、validationは作らない
  • testの精度は計算せずに、testデータと推論結果をグラフ表示する

4次関数を題材にしたのは、単純でありながら、多層のありがたみがそれなりに出るからです。

tf.kerasは伝統的?なSequential型の実装をしていますが、Pytorch風の実装も可能とのことです。今回の実装方法で比較すると、tf.kerasは記述の抽象度が高く、Pytorchはいろいろ記述が必要ですがpython風の記述であるためカスタマイズがしやすいように感じます。

一方、Pytorchはtorch.Tensor型への依存度が高く、scipyとかpandasとかsklearnとかのnumpyエコシステムを直接活用できないもどかしさがあります。実際、今回使ったsklearnのtrain_test_splitに対してのピッタリしたPytorch側のライブラリが無いため(torch.utils.data.random_splitを使うようですがピッタリしない・・・)、最初はnumpyで仕立てつつ、途中でtorch.Tensorに変換してみました。

以下がtf.kerasでの実装です。10か月前の機械学習入門時の記事と比較して、個人的にこなれた感があるなーと思いました。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense

# データ作成
x = np.linspace(-1.5, 1.5, 1000)
noise = np.random.randn(1000) * 0.1
y = x ** 4 - 2 * x ** 2 + noise
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8)

# モデル作成
model = Sequential([
    Dense(128, input_dim=1, activation='relu'),
    Dense(64, activation='relu'),
    Dense(32, activation='relu'),
    Dense(16, activation='relu'),
    Dense(1)
])
model.compile(optimizer='adam', loss='mse', metrics='mse')

# 学習
result = model.fit(x_train, y_train, batch_size=128, epochs=500)

# 推論
y_pred = model.predict(x_test)

# 結果表示
plt.scatter(x_test, y_test)
plt.scatter(x_test, y_pred, color='red')
plt.show()

以下がPytorchでの実装です。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn

# データ作成
x = np.linspace(-1.5, 1.5, 1000)
noise = np.random.randn(1000) * 0.1
y = x ** 4 - 2 * x ** 2 + noise
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8)
# numpyからtorch.Tensorへの変換
x_train = torch.from_numpy(x_train.astype(np.float32)).view(-1, 1)
x_test = torch.from_numpy(x_test.astype(np.float32)).view(-1, 1)
y_train = torch.from_numpy(y_train.astype(np.float32)).view(-1, 1)
y_test = torch.from_numpy(y_test.astype(np.float32)).view(-1, 1)

# モデル作成
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.classifier = nn.Sequential(
            nn.Linear(1, 128), nn.ReLU(),
            nn.Linear(128, 64), nn.ReLU(),
            nn.Linear(64, 32), nn.ReLU(),
            nn.Linear(32, 16), nn.ReLU(),
            nn.Linear(16, 1)
        )
    def forward(self, x):
        x = self.classifier(x)
        return x

model = MLP()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters())

# 学習
for epoch in range(500):
    optimizer.zero_grad()
    y_pred = model(x_train)
    loss = criterion(y_pred, y_train)
    loss.backward()
    optimizer.step()
    print(f'epoch: {epoch}, loss: {loss.item()}')

# 推論
y_pred = model(x_test)

# 結果表示
plt.scatter(x_test, y_test)
plt.scatter(x_test, y_pred.detach(), color='red')
plt.show()

以下が出力結果のグラフです。当然ながら、tf.keras、Pytorchともにほぼ同様の結果が出ます。
スクリーンショット 2020-12-16 20.23.19.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyTorch初挑戦の記録、tf.kerasとの比較実装

連載中?の会議見える化シリーズこの先に進むにはPyTorchやtorchaudioを使っていく必要がありそうです。そのため、PyTorchに入門してみました。その記録です。

こちらの記事では、PyTorchとtf.kerasとの比較も述べられています。それぞれお互いの長所を取り込みつつ進化しており、対応力を広げていくにはPyTorchとtf.kerasのバイリンガルになっておく必要がありそうです。Udemyには、Pythonや機械学習の基礎は知ってますよ、という人向けのPyTorch入門コースがあり、参考になります。

実装テーマとしては以下としています。

  • 4次関数(+乱数ノイズ)を多層パーセプトロン(MLP)で近似
  • train〜testの2分割モデル、validationは作らない
  • testの精度は計算せずに、testデータと推論結果をグラフ表示する

4次関数を題材にしたのは、単純でありながら、多層のありがたみがそれなりに出るからです。

tf.kerasは伝統的?なSequential型の実装をしていますが、PyTorch風の実装も可能とのことです。今回の実装方法で比較すると、tf.kerasは記述の抽象度が高く、PyTorchはいろいろ記述が必要ですがpython風の記述であるためカスタマイズがしやすいように感じます。

一方、PyTorchはtorch.Tensor型への依存度が高く、scipyとかpandasとかsklearnとかのnumpyエコシステムを直接活用できないもどかしさがあります。実際、今回使ったsklearnのtrain_test_splitに対してのピッタリしたPyTorch側のライブラリが無いため(torch.utils.data.random_splitを使うようですがピッタリしない・・・)、最初はnumpyで仕立てつつ、途中でtorch.Tensorに変換してみました。

以下がtf.kerasでの実装です。10か月前の機械学習入門時の記事と比較して、個人的にこなれた感があるなーと思いました。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense

# データ作成
x = np.linspace(-1.5, 1.5, 1000)
noise = np.random.randn(1000) * 0.1
y = x ** 4 - 2 * x ** 2 + noise
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8)

# モデル作成
model = Sequential([
    Dense(128, input_dim=1, activation='relu'),
    Dense(64, activation='relu'),
    Dense(32, activation='relu'),
    Dense(16, activation='relu'),
    Dense(1)
])
model.compile(optimizer='adam', loss='mse', metrics='mse')

# 学習
result = model.fit(x_train, y_train, batch_size=128, epochs=500)

# 推論
y_pred = model.predict(x_test)

# 結果表示
plt.scatter(x_test, y_test)
plt.scatter(x_test, y_pred, color='red')
plt.show()

以下がPyTorchでの実装です。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn

# データ作成
x = np.linspace(-1.5, 1.5, 1000)
noise = np.random.randn(1000) * 0.1
y = x ** 4 - 2 * x ** 2 + noise
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8)
# numpyからtorch.Tensorへの変換
x_train = torch.from_numpy(x_train.astype(np.float32)).view(-1, 1)
x_test = torch.from_numpy(x_test.astype(np.float32)).view(-1, 1)
y_train = torch.from_numpy(y_train.astype(np.float32)).view(-1, 1)
y_test = torch.from_numpy(y_test.astype(np.float32)).view(-1, 1)

# モデル作成
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.classifier = nn.Sequential(
            nn.Linear(1, 128), nn.ReLU(),
            nn.Linear(128, 64), nn.ReLU(),
            nn.Linear(64, 32), nn.ReLU(),
            nn.Linear(32, 16), nn.ReLU(),
            nn.Linear(16, 1)
        )
    def forward(self, x):
        x = self.classifier(x)
        return x

model = MLP()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters())

# 学習
for epoch in range(500):
    optimizer.zero_grad()
    y_pred = model(x_train)
    loss = criterion(y_pred, y_train)
    loss.backward()
    optimizer.step()
    print(f'epoch: {epoch}, loss: {loss.item()}')

# 推論
y_pred = model(x_test)

# 結果表示
plt.scatter(x_test, y_test)
plt.scatter(x_test, y_pred.detach(), color='red')
plt.show()

以下が出力結果のグラフです。当然ながら、tf.keras、PyTorchともにほぼ同様の結果が出ます。
スクリーンショット 2020-12-16 20.23.19.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GPT-2に科研費報告書のデータを学習させて架空研究報告書を生成する

はじめに

研究者にとって、研究費の申請書や報告書を書くのがしんどいのはよくあることです。
しんどさの軽減を試みるべく、GPT-2に報告書っぽいテキストを生成させてみました。
やっていることはこちらの二番煎じで、transformersパッケージのGPT-2に過去の科研費報告書のデータ(日本語)を学習させて自動生成をします。

環境

  • Google Colaboratory GPUランタイム

データの準備

深い意味はありませんが、この項目はローカルのjupyter notebookで実行しました。

科研費データベースからcsvのダウンロード

科研費データベースから、「コロナ」というキーワードで期間指定なしの2767件をcsvでダウンロードしました。
コロナウイルスだけでなく、太陽コロナなどに関する研究も含まれます。
ちなみに研究期間を2020年のみに指定すると、「コロナ」というキーワードでヒットする研究は55件でした。

研究報告のテキストデータをまとめる

科研費データベースからダウンロードしたcsvの扱いは、こちらの記事を参照してください。研究概要に相当するテキストのリストを作ります。
前例では学習させるデータに対して形態素解析をしていないので、今回も形態素解析は省きます。

python
import pandas as pd
df = pd.read_csv("corona.csv")

column_list = ['研究開始時の研究の概要', '研究概要', '研究成果の概要', '研究実績の概要']
abstracts = []

for column in column_list:
    abstracts.extend(df[column].dropna().tolist())

スクリーンショット 2020-12-15 22.54.47.png

train.txt, eval.txtに出力

リストabstractsから、学習に必要なtrain.txteval.txtを作ります。
1行ずつに分けて行数で分割してもいいですが、今回は要旨単位で分けることにしました。
evalは予約語なので、eval.txtに出力したいデータの変数名はtestにしてあります。

python
from sklearn.model_selection import train_test_split
import codecs

train, test = train_test_split(abstracts, test_size = 0.1)

with codecs.open('train.txt', 'w', encoding='utf-8-sig') as f:
    for abstract in train:
        f.write("%s\n" % abstract)

with codecs.open('eval.txt', 'w', encoding='utf-8-sig') as f:
    for abstract in test:
        f.write("%s\n" % abstract)

スクリーンショット 2020-12-15 23.02.32.png
スクリーンショット 2020-12-16 20.13.48.png

train.txt, eval.txtをGoogle Driveにコピー

所定のフォルダに入れておきます。

GPT-2を学習させる

ここからGoogle Colabで実行しています。
基本的に前例に倣っています。
学習に3時間半ほどかかりました。

python
# Google Driveをマウント
from google.colab import drive
drive.mount('/content/drive')
python
# transformersをコピーしてくる
!git clone https://github.com/huggingface/transformers.git -b v3.4.0 '/content/drive/My Drive/transformers'
python
# 環境構築
cd '/content/drive/My Drive/transformers'; pip install -e .

学習

python
## 初回実行、まだチェックポイントが無い場合
!python ./examples/language-modeling/run_language_modeling.py \
    --output_dir=../GPT-2/output_gpt2 \
    --model_type=gpt2 \
    --model_name_or_path=gpt2 \
    --do_train \
    --train_data_file=../GPT-2/train.txt \
    --do_eval \
    --eval_data_file=../GPT-2/eval.txt \
    --per_device_train_batch_size=2 \
    --per_device_eval_batch_size=2 \
    --num_train_epochs=10 \
    --save_steps=5000 \
    --save_total_limit=3

save_steps=5000だと学習開始して1時間半ぐらい経ってからじゃないと途中のチェックポイントが保存されず不安になったので、容量に余裕があれば1000ぐらいがいいかもしれません。

テキストの生成

元の例に倣ってlength=1000にするとなぜかRunTimeErrorになりました。
500にすると正常に生成されました。

python
!python ./examples/text-generation/run_generation.py \
    --model_type=gpt2 \
    --model_name_or_path=../GPT-2/output_gpt2 \
    --prompt "本研究では、以下の成果を得た。" \
    --seed=${RANDOM} \
    --length 500
出力結果1
=== GENERATED SEQUENCE 1 ===
本研究では、以下の成果を得た。
1.「気管呼吸」とされた「感情染晶磁性粒子の粒子血管膜の粒子量感染晶法」との関係、理論の基礎研究、世界の線数値シミュレーション研究、様々な本研究所本講演的な提案に基づく理論や心臓粒子の同定および解析研究所本講演の最適化
本研究では、3分散年齢の目標のデータを組み合わせる手法を開発し、以下の成果を得た。
1.「粒子の晶磁性粒子を持つ感染の粒子量感染晶法」により、細胞内の多くは生殖材料の研究と気管呼吸の原理に換えた場合、生殖材料の結合、それについて確立する解析を行った。
2.「アジアミング分散年齢の目標のデータ」によるデータは、生殖材料の会話、細胞内のそれ
出力結果2
=== GENERATED SEQUENCE 1 ===
本研究では、以下の成果を得た。
1.アルゴリズムの計算機、アルゴリズム等の地球面の影響について、アルゴリズム等の遺伝子は、ビーム、ヌス、トラボ、分子過程、データ等の影響がある。
2.これらの原理によるステラ等に関する最適像領域では、シースを精製して、再生に関する領域である。面の影響についても、ステラ領域では、分子横軌道を側面に入って、シースを側面で進化させ、電力、ビームが混合しているが、面経験では保存なステラ領域である。また、その経験ではステラ領域ではステラ領域では、最も種々の生成に依存している。
3.次世代者と他代者の構成に関する研究を実施した。
4.メッシュのステラ等に関する伝送排出を行い、動作による要因の電磁界値が高いと考えられる。
5.生体の観測研究では、二値のステラ関数を印加するため
出力結果3
=== GENERATED SEQUENCE 1 ===
本研究では、以下の成果を得た。
1.京都シート結果と双極端の相互作用シート法
シート結果は、双極端の相互作用シート法という主に近傍で、先端ずかに見られる原子エネルギーの構造を試みた。これまでにこのエネルギーの構造は、天然填麻磁鎖でも、精度の存在することが判明した。
2.ガスミッタ集合器の正学やラベル・アレイのモデルの開発
マルチディエンタンの非線形表面とラベルのセミナーションとの関連は、ネットワーク程度の測定で調べ、ディエンタンの研究により明らかにした。
3.レキュレーション機構の結果
平成30年度は、以下の成果を得た。
1.レキュレーション機構の結果
コンケール応用の実用化について、カベオリクス速度を持つチミュレーション効果を解析し、ナノチューブからの取り込まれる場合の運用を定量的に解析することができた。
2.ビジュールおよびビジュール基板・�

無意味ですが、それっぽいフレーズが出力されていますね。内容はともかく、箇条書きの番号がちゃんとしているのは少し驚きました。

MeCabで分かち書きして学習させた結果

上記の例では、「感情染晶磁性粒子」「アジアミング分散年齢」「ヌス、トラボ」「天然填麻磁鎖」「ガスミッタ」「マルチディエンタン」「セミナーション」「レキュレーション」「コンケール」「カベオリクス」「チミュレーション」「ビジュール」など、それっぽいがよくわからない単語が出ていたため、MeCabで分かち書きすると改善するかな?と思い、やってみました。
promptの文章も分かち書きしたものを使っています。
以下に示す結果は、原文に改行が少なくて見づらかったので、適宜改行を入れています。(例えば、「出力結果5」の原文にはまったく改行がありませんでした。)

出力結果4
=== GENERATED SEQUENCE 1 ===
本 研究 で は 、 以下 の 成果 を 得 た 。 
( 1 ) シグナル 株 データ の 電気 流体力学 的 、 生体 的 な 実験 を 実施 し 、 実験 実験 によって 明らか に さ れ た 。 

本 研究 で は 明らか に する よう な コロナ質量放出 を 計測 する ため に 、 代表者 対象 における 放出 システム の 不断行論 的 手法 を 用い た 振動 実験 の 結果 、 外乱 部分 性 実験 用 の 製作 を 行い 、 放出 システム の 把握 を 行っ た 。 
また 、 放出 システム の 導出 を 実証 する こと で 検討 し た 。 
コロナ質量放出 分野 を 基本的 に する と 放出 システム の 双方 で の 実態 観測 を 目的 と し た 。 

本 研究 で は , 空間 容器 において 放電 線 粒子 の 化学 計算 を 開発 する こ
出力結果5
=== GENERATED SEQUENCE 1 ===
本 研究 で は 、 以下 の 成果 を 得 た 。 
1 ) 超 微細 波 の 分離 により 多く の 異なる 、 分離 及び 静 電 界 の 活性化 を 示す もの と 、 同じ で ある 現象 が ある 。 
2 ) 電子 の 異常 の プロモート を 介し た 細胞 に 拡張 し た プロモート の ディスク トリポン が 多く 、 宇宙 発光 が 現れる こと が 示唆 さ れ た 。 
3 ) 昨年度 の 結果 を まとめ て 、 分離 及び 静 電 界 活性化 は 今後 の 実現 として 検討 さ れ て いる が 、 微細 波 の 企画 を 生成 し て 細胞内 へ 伸び 共同 さ れる 。 
4 ) 宇宙 発光 測定 により 宇宙 発光 時 の 不明 で ある ステルコロナリング と プロパノール 転写真 を 使用 し て いる 。 また 、 昨年度 に 通常 の 応用 に 基づき 、 電子 の 昨年度 に 調べ た 方法 は
出力結果6
=== GENERATED SEQUENCE 1 ===
本 研究 で は 、 以下 の 成果 を 得 た 。 
1. 単一 周波数 基盤 方式 の 適用 研究 の 国際 図 の 設計 値 を 評価 する ため に 、 成果 報告細胞 における 一方向 の 適用 研究 の 予想 と なる 設計 データ を 提案 し た 。 
2. 適用 研究 の 表面 地域 マウス を 加え た 適用 条件 を 進め て き た 。 また 、 適用 条件 の 重要性 を 明らか に し た 。 

本 研究 で は 、 正規模 の 情報 を 用い た 統計的 統計 的 相互作用 の 対象 を 図っ て 、 地下 観測 および 統計的 ・ 現在 、 地下 大気 による 接着 法 を 開発 し 、 地下 大気 面 と 編集 し た 場合 の 検討 の ため の 研究 を 行っ た 。 

本 研究 の 目的 は 、 新た に 研究

「ディスク トリポン」「ステルコロナリング」「転写真」「報告細胞」などの謎単語は出現しているものの、分かち書きしない場合に比べて頻度は下がったように見受けられます。一方で、定量化は難しいものの、なんとなく文章全体の不自然さが増したような印象もあります。
実用に耐える文章の自動生成まで、まだまだ道のりは遠そうです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Lambda+EFSで自然言語処理ライブラリ(GiNZA)使ってみる

背景

アドベントカレンダー用記事を書いていて、サイズが大きい自然言語処理ライブラリをLambdaで使う部分で技術的障壁が出てきている。そんな中、EFSにセットアップしたPythonライブラリをLambdaにimportする方法という記事を見つける。こちらの技術で要件が満たせそうなので試してみる。

関係する拙記事

背景で述べた技術的障壁を乗り越えるべく各種技術を検証した時の記事。
LambdaLayer用zipをCodeBuildでお手軽に作ってみる。
LambdaでDockerコンテナイメージ使えるってマジですか?(Python3でやってみる)

GiNZA とは

形態素解析を始めとして各種自然言語処理が出来るpythonライブラリ。spaCyの機能をラップしてる(はず)なのでその機能は使える。形態素解析エンジンにSudachiを使用したりもしている。

前提

リソース群は基本CloudFormationで作成。AWSコンソールからCloudFormationで、「スタックの作成」でCloudFormationのTemplateを読み込む形。すいませんが、CloudFormationの適用方法などは把握している方前提になります。

KeyPairの準備(無い場合)

後ほどのCloudFormationのパラメーター指定で必要になるので、AWSコンソールから作成しておく。もちろん、.sshフォルダへの配置など、sshログインの為の準備はしておく。(SSMでやれという話もあるが・・・)

VPCとかSubnetの準備(無い場合)

公式ページ AWS CloudFormation VPC テンプレート に記載のCloudFormationテンプレートを修正し、AWSコンソールから適用。
修正内容は以下の通り

  • 料金節約の為にPrivateSubnetとかNATを削除
  • 別のCloudFormationで使う値をExport
  • 実際にはリソース名など変更しています

修正後のVPC+SubnetのCloudFormation
# It's based on the following sample.
# https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/cloudformation-vpc-template.html
Description:  This template deploys a VPC, with a pair of public and private subnets spread
  across two Availability Zones. It deploys an internet gateway, with a default
  route on the public subnets. It deploys a pair of NAT gateways (one in each AZ),
  and default routes for them in the private subnets.

Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16

  PublicSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.192.10.0/24

  PublicSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
    Type: String
    Default: 10.192.11.0/24

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ1)

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ2)

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Routes

  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

  NoIngressSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: "no-ingress-sg"
      GroupDescription: "Security group with no ingress rule"
      VpcId: !Ref VPC

Outputs:
  VPC:
    Description: A reference to the created VPC
    Value: !Ref VPC
    Export:
      Name: "VPC"

  PublicSubnets:
    Description: A list of the public subnets
    Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]]
    Export:
      Name: "PublicSubnets"

  PublicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref PublicSubnet1
    Export:
      Name: "PublicSubnet1"

  PublicSubnet2:
    Description: A reference to the public subnet in the 2nd Availability Zone
    Value: !Ref PublicSubnet2
    Export:
      Name: "PublicSubnet2"

  NoIngressSecurityGroup:
    Description: Security group with no ingress rule
    Value: !Ref NoIngressSecurityGroup

EFS+EC2(AutoScaling)の準備

公式ページ Amazon Elastic File System サンプルテンプレート に記載のCloudFormationを修正し、AWSコンソールから適用。VPCなどを既存の物を使う場合、適宜修正お願いします。

修正内容は以下の通り

  • インスタンスタイプなど要らない部分削除
  • AMIのImageIDは直接指定する形に(ami-00f045aed21a55240:Amazon Linux 2 AMI 2.0.20201126.0 x86_64 HVM gp2を使用)
  • MountTargetを2つ(AZ分)に変更
  • 別のCloudFormationで使うMountTargetなどをExportして参照可能に
  • AccessPointのpathなど修正
  • 実際にはリソース名など変更しています

修正後のEFS+EC2(AutoScaling)CloudFormation
# https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/quickref-efs.html
AWSTemplateFormatVersion: '2010-09-09'
Description: This template creates an Amazon EFS file system and mount target and
  associates it with Amazon EC2 instances in an Auto Scaling group. **WARNING** This
  template creates Amazon EC2 instances and related resources. You will be billed
  for the AWS resources used if you create a stack from this template.
Parameters:
  InstanceType:
    Description: WebServer EC2 instance type
    Type: String
    Default: t3.small
    AllowedValues:
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
    ConstraintDescription: must be a valid EC2 instance type.
  AMIImageId:
    Type: String
    # Amazon Linux 2 AMI (HVM), SSD Volume Type
    Default: ami-00f045aed21a55240
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Name of an existing EC2 key pair to enable SSH access to the ECS
      instances
  AsgMaxSize:
    Type: Number
    Description: Maximum size and initial desired capacity of Auto Scaling Group
    Default: '1'
  SSHLocation:
    Description: The IP address range that can be used to connect to the EC2 instances
      by using SSH
    Type: String
    MinLength: '9'
    MaxLength: '18'
    Default: 221.249.116.206/32
    AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
  VolumeName:
    Description: The name to be used for the EFS volume
    Type: String
    MinLength: '1'
    Default: efsvolume
  MountPoint:
    Description: The Linux mount point for the EFS volume
    Type: String
    MinLength: '1'
    Default: efsmountpoint
Mappings:
  AWSInstanceType2Arch:
    t3.nano:
      Arch: HVM64
    t3.micro:
      Arch: HVM64
    t3.small:
      Arch: HVM64
    t3.medium:
      Arch: HVM64
    t3.large:
      Arch: HVM64
  AWSRegionArch2AMI:
    ap-northeast-1:
      HVM64: ami-00f045aed21a55240
Resources:
  CloudWatchPutMetricsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
  CloudWatchPutMetricsRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: CloudWatch_PutMetricData
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: CloudWatchPutMetricData
          Effect: Allow
          Action:
          - cloudwatch:PutMetricData
          Resource:
          - "*"
      Roles:
      - Ref: CloudWatchPutMetricsRole
  CloudWatchPutMetricsInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: "/"
      Roles:
      - Ref: CloudWatchPutMetricsRole
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Fn::ImportValue: VPC
      GroupDescription: Enable SSH access via port 22
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '22'
        ToPort: '22'
        CidrIp:
          Ref: SSHLocation
  MountTargetSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Fn::ImportValue: VPC
      GroupDescription: Security group for mount target
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '2049'
        ToPort: '2049'
        CidrIp: 0.0.0.0/0
  FileSystem:
    Type: AWS::EFS::FileSystem
    Properties:
      PerformanceMode: generalPurpose
      FileSystemTags:
      - Key: Name
        Value:
          Ref: VolumeName
  MountTarget1:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: FileSystem
      SubnetId:
        Fn::ImportValue: PublicSubnet1
      SecurityGroups:
      - Ref: InstanceSecurityGroup
      - Ref: MountTargetSecurityGroup
  MountTarget2:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: FileSystem
      SubnetId:
        Fn::ImportValue: PublicSubnet2
      SecurityGroups:
      - Ref: InstanceSecurityGroup
      - Ref: MountTargetSecurityGroup
  EFSAccessPoint:
    Type: 'AWS::EFS::AccessPoint'
    Properties:
      FileSystemId: !Ref FileSystem
      RootDirectory:
        Path: "/"
  LaunchConfiguration:
    Type: AWS::AutoScaling::LaunchConfiguration
    Metadata:
      AWS::CloudFormation::Init:
        configSets:
          MountConfig:
          - setup
          - mount
        setup:
          packages:
            yum:
              nfs-utils: []
          files:
            "/home/ec2-user/post_nfsstat":
              content: !Sub |
                #!/bin/bash

                INPUT="$(cat)"
                CW_JSON_OPEN='{ "Namespace": "EFS", "MetricData": [ '
                CW_JSON_CLOSE=' ] }'
                CW_JSON_METRIC=''
                METRIC_COUNTER=0

                for COL in 1 2 3 4 5 6; do

                 COUNTER=0
                 METRIC_FIELD=$COL
                 DATA_FIELD=$(($COL+($COL-1)))

                 while read line; do
                   if [[ COUNTER -gt 0 ]]; then

                     LINE=`echo $line | tr -s ' ' `
                     AWS_COMMAND="aws cloudwatch put-metric-data --region ${AWS::Region}"
                     MOD=$(( $COUNTER % 2))

                     if [ $MOD -eq 1 ]; then
                       METRIC_NAME=`echo $LINE | cut -d ' ' -f $METRIC_FIELD`
                     else
                       METRIC_VALUE=`echo $LINE | cut -d ' ' -f $DATA_FIELD`
                     fi

                     if [[ -n "$METRIC_NAME" && -n "$METRIC_VALUE" ]]; then
                       INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
                       CW_JSON_METRIC="$CW_JSON_METRIC { \"MetricName\": \"$METRIC_NAME\", \"Dimensions\": [{\"Name\": \"InstanceId\", \"Value\": \"$INSTANCE_ID\"} ], \"Value\": $METRIC_VALUE },"
                       unset METRIC_NAME
                       unset METRIC_VALUE

                       METRIC_COUNTER=$((METRIC_COUNTER+1))
                       if [ $METRIC_COUNTER -eq 20 ]; then
                         # 20 is max metric collection size, so we have to submit here
                         aws cloudwatch put-metric-data --region ${AWS::Region} --cli-input-json "`echo $CW_JSON_OPEN ${!CW_JSON_METRIC%?} $CW_JSON_CLOSE`"

                         # reset
                         METRIC_COUNTER=0
                         CW_JSON_METRIC=''
                       fi
                     fi
                     COUNTER=$((COUNTER+1))
                   fi

                   if [[ "$line" == "Client nfs v4:" ]]; then
                     # the next line is the good stuff
                     COUNTER=$((COUNTER+1))
                   fi
                 done <<< "$INPUT"
                done

                # submit whatever is left
                aws cloudwatch put-metric-data --region ${AWS::Region} --cli-input-json "`echo $CW_JSON_OPEN ${!CW_JSON_METRIC%?} $CW_JSON_CLOSE`"
              mode: '000755'
              owner: ec2-user
              group: ec2-user
            "/home/ec2-user/crontab":
              content: "* * * * * /usr/sbin/nfsstat | /home/ec2-user/post_nfsstat\n"
              owner: ec2-user
              group: ec2-user
          commands:
            01_createdir:
              command: !Sub "mkdir /${MountPoint}"
        mount:
          commands:
            01_mount:
              command: !Sub >
                mount -t nfs4 -o nfsvers=4.1 ${FileSystem}.efs.${AWS::Region}.amazonaws.com:/ /${MountPoint}
            02_permissions:
              command: !Sub "chown ec2-user:ec2-user /${MountPoint}"
    Properties:
      AssociatePublicIpAddress: true
      ImageId:
        Ref: AMIImageId
      InstanceType:
        Ref: InstanceType
      KeyName:
        Ref: KeyName
      SecurityGroups:
      - Ref: InstanceSecurityGroup
      IamInstanceProfile:
        Ref: CloudWatchPutMetricsInstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum install -y aws-cfn-bootstrap
          /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchConfiguration --configsets MountConfig --region ${AWS::Region}
          crontab /home/ec2-user/crontab
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource AutoScalingGroup --region ${AWS::Region}
  AutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn:
    - MountTarget1
    - MountTarget2
    CreationPolicy:
      ResourceSignal:
        Timeout: PT15M
        Count:
          Ref: AsgMaxSize
    Properties:
      VPCZoneIdentifier:
        - Fn::ImportValue: PublicSubnet1
        - Fn::ImportValue: PublicSubnet2
      LaunchConfigurationName:
        Ref: LaunchConfiguration
      MinSize: '1'
      MaxSize:
        Ref: AsgMaxSize
      DesiredCapacity:
        Ref: AsgMaxSize
      Tags:
      - Key: Name
        Value: EFS FileSystem Mounted Instance
        PropagateAtLaunch: 'true'
Outputs:
  MountTargetID1:
    Description: Mount target ID
    Value:
      Ref: MountTarget1
  MountTargetID2:
    Description: Mount target ID
    Value:
      Ref: MountTarget2
  LambdaEFSArn:
    Description: File system Arn
    Value: !GetAtt FileSystem.Arn
    Export:
      Name: !Sub "LambdaEFSArn"
  LambdaEFSAccessPointArn:
    Description: File system AccessPointArn
    Value: !GetAtt EFSAccessPoint.Arn
    Export:
      Name: !Sub "LambdaEFSAccessPointArn"
  InstanceSecurityGroup:
    Description: A reference to the InstanceSecurityGroup
    Value: !Ref InstanceSecurityGroup
    Export:
      Name: "InstanceSecurityGroup"
  MountTargetSecurityGroup:
    Description: A reference to the MountTargetSecurityGroup
    Value: !Ref MountTargetSecurityGroup
    Export:
      Name: "MountTargetSecurityGroup"

EC2へログインしてモジュールインストール

EFSにセットアップしたPythonライブラリをLambdaにimportする方法をトレースさせて頂く。

ログイン

AWSコンソールからPublicIPを調べてssh。

ssh -i ~/.ssh/hogehoge-keypair.pem ec2-user@xx.yyy.xxx.zzz

マウント確認

コマンド実行
df -h
結果表示
Filesystem                                      Size  Used Avail Use% Mounted on
devtmpfs                                        469M     0  469M   0% /dev
tmpfs                                           479M     0  479M   0% /dev/shm
tmpfs                                           479M  388K  479M   1% /run
tmpfs                                           479M     0  479M   0% /sys/fs/cgroup
/dev/nvme0n1p1                                  8.0G  1.6G  6.5G  20% /
xx-yyyyyyyz.efs.ap-northeast-1.amazonaws.com:/  8.0E     0  8.0E   0% /efsmountpoint
tmpfs                                            96M     0   96M   0% /run/user/1000

/efsmountpoint にEFSがマウントされているのを確認。

Pythonなどのモジュールインストール

su にならないとginzaが上手くインストールできなかったのでその部分修正

sudo su -
cd /efsmountpoint
yum update
yum -y install gcc openssl-devel bzip2-devel libffi-devel
wget https://www.python.org/ftp/python/3.8.6/Python-3.8.6.tgz
tar xzf Python-3.8.6.tgz

cd Python-3.8.6
./configure --enable-optimizations
make altinstall

# check
python3.8 --version
pip3.8 --version

GiNZAインストール

pip3.8 install --upgrade --target lambda/ ginza==4.0.5

# 念のためフル権限にしておく
chmod 777 -R lambda/

※ここまででEC2は必要無くなります。AWSコンソールからEC2 => AutoScalingグループ => 対象のAutoScalingグループ選択 => グループの詳細 の「編集」で 「希望する容量」「最小キャパシティ」「最大キャパシティ」を全て0にしてインスタンスを終了。でないと不必要なお金がかかってしまうので注意!!!!

テスト用Lambdaを登録(メイン部分)

こちらのCloudFormationをAWSコンソールから適用。重要なのはインラインで記載されてるソースの以下部分。あと、FileSystemConfigs プロパティの設定。EFSを使うので、VPCに属するLambdaにしています。

ポイント部分
sys.path.append("/mnt/efs0/lambda")
EFSマウント指定部分
      FileSystemConfigs:
      - Arn:
          Fn::ImportValue: LambdaEFSAccessPointArn
        LocalMountPath: "/mnt/efs0"

テスト用LambdaのCloudFormation(Policy+Lambda)
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda test with EFS
Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "LambdaRole"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: "LambdaPolicy"
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: "*"
            - Effect: Allow
              Action:
                - cloudwatch:GetMetricStatistics
              Resource: "*"
            - Effect: Allow
              Action:
                - dynamodb:GetRecords
                - dynamodb:GetItem
                - dynamodb:BatchGetItem
                - dynamodb:BatchWriteItem
                - dynamodb:DeleteItem
                - dynamodb:Query
                - dynamodb:Scan
                - dynamodb:PutItem
                - dynamodb:UpdateItem
              Resource: "*"

            - Effect: Allow
              Action:
                - ec2:CreateNetworkInterface
                - ec2:DescribeNetworkInterfaces
                - ec2:DeleteNetworkInterface
                - ec2:DescribeSecurityGroups
                - ec2:DescribeSubnets
                - ec2:DescribeVpcs
              Resource: "*"
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: "*"
            - Effect: Allow
              Action:
                - elasticfilesystem:ClientMount
                - elasticfilesystem:ClientWrite
                - elasticfilesystem:DescribeMountTargets
              Resource: "*"

  LambdaEFSTest:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: efstestlambda
      Handler: index.handler
      Runtime: python3.8
      Code:
        ZipFile: |
          import sys
          sys.path.append("/mnt/efs0/lambda")
          import json
          import spacy
          import logging
          from ginza import *

          logger = logging.getLogger()

          def handler(event, context):
              logger.info(context)
              target_text = event['text']
              nlp = spacy.load('ja_ginza')

              doc = nlp(target_text)
              morpheme_list = []
              for sent_idx, sent in enumerate(doc.sents):
                  for token_idx, tk in enumerate(sent):
                      wk_morpheme = {}
                      wk_morpheme['text'] = tk.text
                      wk_morpheme['dep'] = tk.dep_
                      wk_morpheme['pos'] = tk.pos_
                      wk_morpheme['tag'] = tk.tag_
                      morpheme_list.append(wk_morpheme)
              return morpheme_list
      FileSystemConfigs:
      - Arn:
          Fn::ImportValue: LambdaEFSAccessPointArn
        LocalMountPath: "/mnt/efs0"
      Description: Lambda test with EFS.
      MemorySize: 2048
      Timeout: 15
      Role: !GetAtt LambdaRole.Arn
      VpcConfig:
        SecurityGroupIds:
          - Fn::ImportValue: InstanceSecurityGroup
          - Fn::ImportValue: MountTargetSecurityGroup
        SubnetIds:
          - Fn::ImportValue: PublicSubnet1
          - Fn::ImportValue: PublicSubnet2

テストする

  • 「テスト」ボタンを押す
  • イベント名は適当に
  • {"text":"テストしてみる"} をテスト用Bodyに指定
  • 「作成」を押す
  • 元の画面に戻る。テストが作成されているのでその状態で「テスト」ボタンを押す。

image.png

成功!(2回目以降の実行なので622msになってます。1回目は4秒以上かかりました)

終わりに

いくつかの検討を経て、ようやくサーバーレスで自然言語処理が出来そうです(EFSはストレージなので許容します)。
LambdaコンテナもEFSとのマウントも今年の機能っぽいです。去年検討していたら諦めていた事になります。AWSの機能追加速度には目を見張るものがあります。すなわち日々キャッチアップが必要という事になる訳で。大変ですw

参考にさせて頂いた良記事

EFSにセットアップしたPythonライブラリをLambdaにimportする方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【自分用備忘録】初めてPyinstallerを使って躓いたから解決策を書く

はじめに

最近Python が気になっていてexe化を実行しようと思ったのですが、めちゃくちゃ躓いて時間ばかり使ったので、初めて使う人が引っかかりやすい(自分が引っかかった)所を少しでもお助けできたらなと思います。
国語が苦手で、文章が分かりにくいかもしれないです。。。(自分用なのでご容赦ください(´・_・`))

pyinstallerのインストール

インストールは超カンタンで、コマンドプロンプトで以下のコマンドを打つだけで出来ました。

pip install pyinstaller

サンプルコード

僕はPython初心者なので、コードが雑なところがあると思いますが、今回は以下のコードをexe化したいと思います。(適宜numpyをインストールしてください。)コードは出来るだけ行数を減らすようにしているので読みにくいと思います。
filetree.jpg

qiita_tutorial.py
import tkinter, os, sys, numpy, import_file
x = numpy.array([1, 2, 3])
print(x)
print(import_file.add(3, 5))    
root = tkinter.Tk()
root.wm_iconbitmap("icons/icon.ico")
root.mainloop()
import_file.py
def add(a, b):
    return a + b

※補足※
2つファイルがあったり、GUI作ったり、変なimportをしてるのは説明のときに使うためです。
iconsフォルダには icon.icoというアイコンファイルが入っています。
iconフォルダ内のicon.icoqiita_tutorial.pyを実行するときに使用します。画像にあるicon.icoファイルはpyinstallerでexeファイルに埋め込むときに使います。

SPECファイルってなんやねん

ファイルの準備が終わったので、さっそくPyinstallerを使ってexe化してみたいと思います。

pyinstaller qiita_tutorial.py --onefile --icon=icon.ico

カレントディレクトリを合わせて、このコマンドを実行するとdistフォルダの中にqiita_tutorial.exeが生成されていると思います。とりあえずこれでexe化は出来ました。
それに合わせてqiita_tutorial.pyと同じ階層にqiita_tutorial.specというファイルが生成されています。SPECファイルというのは簡単に言うと、exe化する時の設定を変更しやすくする為のファイルです。

qiita_tutorial.spec(一部縮小)
block_cipher = None
a = Analysis(['qiita_tutorial.py'],
             pathex=['G:\\ProgrammingFiles\\VScode\\Python\\qiita_tutorial'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='qiita_tutorial',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=True , icon='icon.ico')

これをテキストエディタで編集する事で、任意のファイルを埋め込んだり、アイコンを設定したりできるようになります。(コマンドのオプションで解決できるものもありますが、テキストエディタで編集する方が個人的には楽です。)
設定例として console=False にすればコンソール(黒い画面)を非表示にすることができます。

あとSPECファイルを有効にするにはpyinstaller実行時の引数にSPECファイルを指定してあげないと設定が反映されないので、実行する時pythonファイルを指定しないように気をつけてください。

pyinstaller qiita_tutorial.spec

以下のお話はSPECファイルのお話です。

外部ライブラリの埋め込み

自分の環境ではPythonの標準ライブラリ(tkinter, os)などは埋め込まなくても動きました。
うまく動かないときは、SPECファイルのhiddenimportの場所に名前を入力すると埋め込む事ができました。
SPECファイルに設定しなくても上手くいく奴もあるっぽいのですが、 (import_file.py は何故か読み込まれてました)

qiita_tutorial.spec
hiddenimports=["numpy"],

このように、配列に追加してあげると上手くいくみたいです。
自動追加してくれるやつの基準が分らないので、「なんか起動しねぇ!」ってときは試してみてください。

(numpyインストール時に躓いたのでこの記事を読んで解決しました。)

どうやってファイルを埋め込むの?

アイコンとか画像とかはdataの配列に追加すると埋め込む事ができるみたい。
tkinterのGUIアイコンはpyinstallerのアイコンを変更してもうまく行かないので、exeに埋め込む必要があります。

datas=[], の所を datas=[('icons/icon.ico', 'icons')], に変更すると icon.icoファイルが埋め込まれます。タプルは ("ファイル名", "フォルダ名") という感じに指定するとうまくいきます。フォルダ名は必須みたいで空欄にすることは出来ませんでした。
埋め込んだファイルは、exeが起動しているときに、C:\Users\<ユーザー名>\AppData\Local\Temp\_MEI<数字>\icons\icon.ico に展開されます。(exeを終了すると削除されます。)

埋め込んだファイルは以下の関数を通して取得することが出来ます。
exeのときはTempファイルから、Debug実行のときは普通にパスを取得できるようにする関数です。

resource_path()
def resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return sys._MEIPASS + "\\" + relative_path 
    return os.path.join(os.path.abspath("."), relative_path)

以上の関数を qiita_tutorial.pyに追加して、下から2行目を以下のように変更します。

qiita_tutorial.py
root.wm_iconbitmap(resource_path("icons/icon.ico"))

そうすればexeファイル単体でもGUIのアイコンが設定されると思います。

subprocess が動かない!

noconsoleのオプション(SPECファイルではconsole=False)を、つけてsubprocessを実行すると、コンソールが起動せずコマンドが実行されないみたいです。
調べたところ回避策があったので、以下の関数をsubprocessの引数にアスタリスクでくっつけると良いっぽい。この関数は stdin, stderr, startupinfo, env を返り値として待ってるので、それらをオプションに付けるのは出来ないです。(cwd とかは指定できます。)

subprocess_args()
def subprocess_args(include_stdout=True):
    # The following is true only on Windows.
    if hasattr(subprocess, 'STARTUPINFO'):
        # Windowsでは、PyInstallerから「--noconsole」オプションを指定して実行すると、
        # サブプロセス呼び出しはデフォルトでコマンドウィンドウをポップアップします。
        # この動作を回避しましょう。
        si = subprocess.STARTUPINFO()
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        # Windowsはデフォルトではパスを検索しません。環境変数を渡してください。
        env = os.environ
    else:
        si = None
        env = None

    # subprocess.check_output()では、「stdout」を指定できません。
    #
    #   Traceback (most recent call last):
    #     File "test_subprocess.py", line 58, in <module>
    #       **subprocess_args(stdout=None))
    #     File "C:Python27libsubprocess.py", line 567, in check_output
    #       raise ValueError('stdout argument not allowed, it will be overridden.')
    #   ValueError: stdout argument not allowed, it will be overridden.
    #
    # したがって、必要な場合にのみ追加してください。
    if include_stdout:
        ret = {'stdout': subprocess.PIPE}
    else:
        ret = {}

    # Windowsでは、「--noconsole」オプションを使用してPyInstallerによって
    # 生成されたバイナリからこれを実行するには、
    # OSError例外「[エラー6]ハンドルが無効です」を回避するために
    # すべて(stdin、stdout、stderr)をリダイレクトする必要があります。
    ret.update({'stdin': subprocess.PIPE,
                'stderr': subprocess.PIPE,
                'startupinfo': si,
                'env': env })
    return ret

subprocess_args() 参照元

subprocess
subprocess.run(cmd, **subprocess_args(True)) # cmd = コマンドプロンプトで実行するコマンド(string)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoに入門してからデプロイするまでに読んで知っておいてほしいこと

Djangoに入門したが、デプロイまでやったことがなく不安、、、という方に読んでもらいたい記事です。

ちなみに、数日前までの僕のことです。
記憶が確かなうちに書いちゃいたいと思います。

よく「こうしておくべきだ」と言われていること(~がベストプラクティス的なこと)があると思いますが、その理由がわかっていないと、やる意味もわからず、モチベが下がるだけになります(なりました)。

僕は、全く理解できずに面倒なことをしなきゃいけないのかと思って萎えた経験あります。

ちなみにですが、主に

  • 仮想環境
  • pipの管理
  • 設定ファイル
  • Gitの管理について
  • サーバーに教えること
  • 環境変数
  • staticの扱い

について話します。

詳細はぐぐったら解決するので、それまでの道案内的な感じに捉えていただければと思います。

普段から絶対に仮想環境を使う

仮想環境を使っておかなくては、デプロイするときに困ります。
その理由は、

  • pipの管理が用意(開発時にも)
  • デプロイ時にrequirements.txtファイルが必要

pipの管理が用意

これは開発時にも大切で、pythonバージョンごとに対応しているpipが異なっていたりしたため、相性が悪くて思いもよらぬエラーがでたことがあります。
それ以降は、必ず仮想環境を用意するようにしています。

個人的にはvenvが楽です。Windowsでは、

py -m venv venv_name
.\venv\scripts\activate

でかんたんに作成できます。

デプロイ時にrequirements.txtファイルが必要

サーバーにPythonだけでなく、pipをインストールする必要があるのですが、そのときに必要です。

ちなみに、pip freeze > requirements.txtで仮想環境に入っているpipを書き込むことができます。

もちろんローカル環境での開発し始めてすぐのときに使うこともできます。

例えば、最初からrestframework,pillowだけでなく数多くのpipを使うとき、requirements.txtを最初から用意しておき、pip install -r requirements.txtとすることで一気にインストールするできて時短です。

設定ファイルは開発用と本番用が必要

これては、デプロイするときに「分けなきゃだめだ」と気づきました。

その理由は、

  • 開発環境では、ログを表示させるが本番環境ではすべてを表示させてはならない
  • メディアファイル(主に写真)の保存場所が異なる

ログは、開発段階では有用です。Httpリクエストとその内容を上手に確認できるからです。
メディアの設定も、django-debug-toolbarの設定もDEBUG=Trueのときにだけ使うには以下のようにします。

メディアファイルも、ローカルでは自分のPCに保存することになりますが、本番ではそうはいきません。

S3などの静的ファイルを保存するクラウドを使うのが一般的だと思います。

よって、それに適したセッティング内容を記述しましょう。

例えば、AWSなら

if not DEBUG:
    STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
    AWS_ACCESS_KEY_ID = env('AWS_ACCESS_')
    .
    .
    .

です。
ローカルでは、

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += [
        path('__debug__/', include(debug_toolbar.urls)),
    ] 

などとする必要があります。

以下に分け方の例を載せますが、本番環境で使うsettings.pyに、baseとなるsetting内容をimportし、DEBUGのTrue,Falseで読み込みを変更する、というのが肝になります。

後のGitにも絡むのですが、

  • ローカルで使う設定ファイルをGitの管理から外すことで
  • 本番環境にはローカルでも設定ファイルがimportされないことになり
  • そのなかにDEBUG=Trueも含めて書いていたものが読み込まれない

ことで、本番環境で思った通りの動作をさせることになります。

設定ファイルの分け方

一番理想的なのは、

config - settigs.py 
       - wsgi.py 
       - ...

を、

config - settigs - __init__.py 
                 - base.py 
                 - local.py 
                 - product.py 
       - wsgi.py 
       - ...

のようにわけることです。ただ、これは最初にやろうとしたときに少し煩雑です。
その理由は、

  • セッティングファイルのディレクトリがひとつ深くなってしまうため設定の変更が必要
  • 初めてデプロイするレベルなら、分けるほどの設定項目がない

から、です。

もちろん、やり方はぐぐれば分かるのですが、僕は最初にやるには以下の方法が良いと感じました。

それは。

config - local_settigs.py 
       - settings.py 
       - wsgi.py 
        ...

こうすると、読み取るセッティングファイルの変更も不要だし、ディレクトリの変更も不要なのでかなり楽です。もちろん、今後はもう少し大規模に開発したいと思っているので、settingsディレクトリを作ってその中に分けて入れたいと思います。

Gitで管理してはならないこと

Djangoを始めるまえに何らかのウェブフレームワークを始めていたり、Gitで開発を進めていたら問題なく分かることですが、僕は完全未経験だったので知っておく必要がありました。
その内容は、

  • SECRET_KEY(Djagnoの)
  • AWSやその他APIのKEY
  • localのsettings.py

です。
その理由は明白ですが、他人に知られてはならないからです。

AWSについては青天井なので細心の注意を払う必要があります。

rootユーザーではなく、IAMを作成して、権限を制限することも必須です。

僕の場合、静的ファイルを扱うだけなので、S3の権限だけを与えるユーザーを作成するといった感じです。

また、git init してからcommitするまでに、そういったことは分けておかなくては見られてしまいます。

最初にすべてのKEYが含まれたファイルを用意します。.envファイルです。

そのために、はじめの段階でGitを知って使い始めておくべきです。gitignoreを作り、そこに見られたくないファイルをまとめます。

Qiitaに書き方、Gitの始め方など載っているので見てみるといいです。

Gitで管理しないが読み込む必要のあること

もちろん、Gitで管理対象外にしたからといって読み込まなければDjangoは使えません。

よって、.envファイルを作り、Gitignore内に入れ、HerokuならHerokuの環境変数に代入する、といった感じです。

.envに書き方は、

SECRET_KEY=dsoifjsoidjf
XX_ID=ijfdoijfd

のような感じです。それをdjango-environというpipから読み取るのが楽です。

デプロイするサーバーに教えなきゃいけないこと

外部のLinux環境にサーバーを建てるので、

  • どんなバージョンのPythonを使うか
  • どのpipを使うのか

が必要で、

  • Procfile
  • runtime.txt
  • requirements.txt

を用意する必要があります。書き方はググってください。

collectstaticについて

自分でつまづいたことは、いつ、どこでcollectstaticするかです。

Herokuを使用した場合ですが、本番環境、つまりgitでremoteにつないでからcollectstaticをするということです。
VPSでは、その中の仮想サーバーでcollectstaticをすることになります。

一度ローカルでcollectstaticをしてから、またstyle.cssなどを書き換えて、そこからcollectstaticをしても反映されませんでした。

つまり、collectstaticする前のファイルをサーバーなどにあげてから、本番環境でcollectstaticをするということで解決できました。

さいごに

Djagnoを始めて、基本は書籍とグーグル検索でやってきましたが、とても楽しいものでした。

ポートフォリオとなるものも時期に公開できそうでインターンなどで活かしたいと思っています。

ちなみに。学習の際にはAkiyokoさんの参考書も使用させていただきました。何度も読む価値があり、とてもためになりました。

参考文献・学習に使った記事

学習に使用したもの全てではなく、履歴に残ってたものや記憶にあるのもだけですみません。
現場で使える Django の教科書《基礎編》
herokuについて
https://qiita.com/Shitimi_613/items/60d994f0a8b9e8890d4c
https://qiita.com/frosty/items/66f5dff8fc723387108c
https://qiita.com/akiko-pusu/items/dec93cca4855e811ba6c

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pydanticを使って実行時にも型情報が適用されるPythonコードを書く

この記事はPythonその2 Advent Calendar 2020、16日目の記事です。

Python3.5でType Hintsが導入され、元々動的型付け言語であったPythonでもコードに型情報を記述することが現在では当たり前になってきました。

今回は、この型情報を最大限活用してより堅牢なPythonコードを書く大きな助けになるライブラリ、pydanticを紹介します。

pydanticとは

最近話題のPython製WebフレームワークFastAPIでも使用されているので、存在自体は知っている方も多いのでは無いでしょうか。
実は私もFastAPIを初めて使ったときにこのpydanticの存在を知りました。

pydanticはずばり以下の機能を実現してくれるライブラリです。

  • 実行時の型情報の提供
  • 不正なデータにはユーザーフレンドリーなエラーを返す

これだけだとなんのこっちゃ、って人の方が多いですよね。
この後に例を用いて解説します。

公式リソース

GitHub: samuelcolvin/pydantic: Data parsing and validation using Python type hints
公式ドキュメント: pydantic

Example

pydanticpydantic.BaseModelという基底クラスを継承したユーザー定義クラスにおいてその機能を発揮します。

まずはpydanticを使用しないクラス定義を考えてみます。
dataclasses.dataclassを使います。

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class NonPydanticUser:
    name: str
    age: int

このNonPydanticUserクラスのインスタンスを1つ作成してみます。
この例では、2つのフィールドnamestr型、ageint型です。
クラス定義の通りのデータ型を保持していますね。

Ichiro = NonPydanticUser(name="Ichiro", age=19)
print(Ichiro)
#> NonPydanticUser(name='Ichiro', age=19)
print(type(Ichiro.name))
#> <class 'str'>
print(type(Ichiro.age))
#> <class 'int'>

もう一つ別のインスタンスを作成してみます。

Samatoki = NonPydanticUser(name="Samatoki", age="25")
print(Samatoki)
#> NonPydanticUser(name='Samatoki', age='25')
print(type(Samatoki.name))
#> <class 'str'>
print(type(Samatoki.age))
#> <class 'str'>

この例では、namestr型ですが、agestr型になってしまいます。
TypeErrorなどの例外も送出されません。

あくまで型アノテーションによって与えられる型情報がコーディング時にのみ機能しているということが改めて分かりますね。
確かにmypyPylanceなどを使えばこういった型の不整合はコーディング時に検出できますが、コード実行時に型の不整合や不正値で例外送出をしたい場合は自前で入力値チェックをする必要があります。

一方pydanticを使用したクラス定義は以下の様になります。

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

一見するとdataclasses.dataclassを使った場合と似ていますね。
ですが明確な違いがあります。

まずは正常なフィールド値を使ったインスタンスを作成してみます。

Ramuda = User(name="Ramuda", age=24)
print(Ramuda)
#> name='Ramuda' age=24
print(type(Ramuda.name))
#> <class 'str'>
print(type(Ramuda.age))
#> <class 'int'>

これだけならばあんまり違いが分かりませんね。
次にage"23""45"といったstr型の数値を与えてみます。

Jakurai = User(name="Jakurai", age="35")
#> name='Jakurai' age=35
print(type(Jakurai.name))
#> <class 'str'>
print(type(Jakurai.age))
#> <class 'int'>

Jakurai.ageint型にキャストされています。

ちなみに、agehogefugaなどのint型にキャストできない値を与えるとどうなるのでしょうか。

Sasara = User(name="Sasara", age="ホンマか?")
#> ValidationError: 1 validation error for User
#> age
#>   value is not a valid integer (type=type_error.integer)

ValidationErrorという例外が送出されました。
特にバリデーションを実装していないのに、不正値を検出しています。

この様にpydanticを使用すると、記述した型情報がコーディング時だけではなくコード実行時にも適用され、更に不正値に対しては分かりやすい例外を投げてくれる(後述)ので、動的型付け言語であるPythonで型に厳格なコードを書くことができます!

pydanticはこんな人にオススメ!!

  • 簡単なバリデーションはできるだけ省略したい
  • GoやTypeScript、Swiftなど型に厳格な言語からPythonに入ってきて、Pythonでも型を気にしたい人
  • とにかく堅牢なコードが書きたい人
  • とにかく型に縛られたい人

pydanticの基本

公式のExampleの以下のコードを使って基本的な解説をします。

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []


external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}
user = User(**external_data)
print(user.id)
#> 123
print(repr(user.signup_ts))
#> datetime.datetime(2019, 6, 1, 12, 22)
print(user.friends)
#> [1, 2, 3]
print(user.dict())
"""
{
    'id': 123,
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'friends': [1, 2, 3],
    'name': 'John Doe',
}
"""

pydantic.BaseModelという基底クラスを継承してユーザー独自のクラスを定義します。
このクラス定義の中ではidnamesignup_tsfriendsという4つのフィールドが定義されています。
それぞれのフィールドはそれぞれ異なる記述がされています。ドキュメントによると以下の様な意味があります。

  • id (int) ... Type Hintsのみ宣言した場合、必須フィールドとなる。もしインスタンス生成時にstrbytesfloat型の値が与えられた場合は強制的にintに変換する。それ以外のデータ型(dict, listなど)の値が与えられると例外を送出する。
  • name (str) ... John Doeというデフォルト値からnamestr型と推論される。またデフォルト値が宣言されているので、nameは必須フィールドではない。
  • signup_ts: (datetime, optional) ... Noneが許容されるdatetime型。またデフォルト値が宣言されているので、sign_upは必須フィールドではない。int型のUNIX timestamp(e.g. 1608076800.0)や日付と時刻を表すstr型文字列を引数に与えることができる。
  • friends: (List[int]) ... Pythonの組込みのtyping systemを利用している。またデフォルト値が宣言されているので、必須フィールドではない。idと同様に、"123""45"などはint型に変換される。

pydantic.BaseModelを継承したクラスのインスタンス生成時に不正値を与えようとするとpydantic.ValidationErrorという例外を送出することは触れました。

以下のコードを使用してValidationErrorの中身を覗いてみましょう。

from pydantic import ValidationError

try:
    User(signup_ts='broken', friends=[1, 2, 'not number'])
except ValidationError as e:
    print(e.json())

このコードに対するValidationErrorの中身は以下の様になります。
各フィールドにおいてそれぞれどの様な不整合が起こっているのかが分かります。

[
  {
    "loc": [
      "id"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  },
  {
    "loc": [
      "signup_ts"
    ],
    "msg": "invalid datetime format",
    "type": "value_error.datetime"
  },
  {
    "loc": [
      "friends",
      2
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }
]

Tips

この記事だけではpydanticの全てを紹介することはできませんが、以降ではすぐに使えそうな要素をTips的に紹介していきたいと思います。

Field Types

pydanticに対応しているデータ型は本当に多種多様です。
その一部を紹介します。

Standard Library Types

intstrlistdictなどのプリミティブなデータ型はもちろん使用できます。
その他にtypingipaddressenumdecimalpathlibuuidなどの組込みライブラリにも対応しています。

以下はipadress.IPv4Addressを使用した例です。

from pydantic import BaseModel
from ipaddress import IPv4Address

class IPNode(BaseModel):
    address: IPv4Address

client = IPNode(address="192.168.0.12") 

srv = IPNode(address="hoge")
#> ValidationError: 1 validation error for IPNode
#> address
#>  value is not a valid IPv4 address (type=value_error.ipv4address)

URLs

pydanticではhttps://example.comftp://hogehogeといったURLにも対応しています。

from pydantic import BaseModel, HttpUrl, AnyUrl

class Backend(BaseModel):
    url: HttpUrl

bd1 = Backend(url="https://example.com") 

bd2 = Backend(url="file://hogehoge")
#> ValidationError: 1 validation error for Backend
#> url
#>  URL scheme not permitted (type=value_error.url.scheme; allowed_schemes={'https', 'http'})

Secret Types

ログなどの出力に吐きたくない情報も取り扱うことができます。
例えばパスワードに対してはpydantic.SecretStrが使用できます。

from pydantic import BaseModel, SecretStr

class Password(BaseModel):
    value: SecretStr

p1 = Password(value="hogehogehoge")
print(p1.value)
#> **********

EmailStr

メールアドレスを扱える型です。
ただし、使用する際にはpydanticとは別にemail-vaidatorというライブラリをインストールしておく必要があります。

このEmailStrと前節のSecret Typesを使用してみます。

from pydantic import BaseModel, EmailStr, SecretStr, Field

class User(BaseModel):
    email: EmailStr
    password: SecretStr = Field(min_length=8, max_length=16)

# OK
Juto = User(email="juto@mtc.com", password="hogehogehoge")
print(Juto)
#> email='juto@mtc.com' password=SecretStr('**********')

# NG, emailがメールアドレスのフォーマットになっていない
Rio = User(email="rio", password="hogehogehogehoge")
#> ValidationError: 1 validation error for User
#> email
#>   value is not a valid email address (type=value_error.email)

# NG, passwordの文字数が16文字を越えている
Gentaro = User(email="gentaro@fp.com", password="hogehogehogehogehoge")
#> ValidationError: 1 validation error for User
#> password
#>   ensure this value has at most 16 characters (type=value_error.any_str.max_length; limit_value=16)

# NG, passwordの文字数が8文字未満である
Daisu = User(email="daisu@fp.com", password="hoge")
#> ValidationError: 1 validation error for User
#> password
#>   ensure this value has at least 8 characters (type=value_error.any_str.min_length; limit_value=8)

Constrained Types(条件付き型)

from pydantic import BaseModel, HttpUrl, AnyUrl, SecretStr, conint

# 正の数だけ許容する様にしてみる
class PositiveNumber(BaseModel):
    value: conint(gt=0)

# OK
n1 = PositiveNumber(value=334)

#NG, 負の数である
n2 = PositiveNumber(value=-100)
#> ValidationError: 1 validation error for PositiveNumber
#> value
#>   ensure this value is greater than 0 (type=value_error.number.not_gt; limit_value=0)

Strict Types

記事冒頭の例で"23""45"といったstr型の数値をint型にキャストして受け入れる礼がありました。
このキャストすら認めないより厳格なフィールドも宣言できます。

from pydantic import BaseModel, conint, StrictInt

# キャストを認めないint
class StrictNumber(BaseModel):
    value: StrictInt

# OK
n1 = StrictNumber(value=4)

# キャストしてint型になれるstr型であっても、int型ではないのでNG
n2 = StrictNumber(value="4")
#> ValidationError: 1 validation error for StrictNumber
#> value
#>   value is not a valid integer (type=type_error.integer)

前節のConstrained Typesと組み合わせることもできます。

from pydantic import BaseModel conint

# 自然数だけ許容する
class NaturalNumber(BaseModel):
    value: conint(strict=True, gt=0)

# OK
n1 = NaturalNumber(value=334)

# NG, 負の数である
n2 = NaturalNumber(value=-45)
#> ValidationError: 1 validation error for NaturalNumber
#> value
#>  ensure this value is greater than 0 (type=value_error.number.not_gt; limit_value=0)

# キャストしてint型になれるstr型であっても、int型ではないのでNG
n3 = NaturalNumber(value="45")
#> ValidationError: 1 validation error for NaturalNumber
#> value
#>   value is not a valid integer (type=type_error.integer)

# float型も許容されない
n4 = NaturalNumber(value=123.4)
#> ValidationError: 1 validation error for NaturalNumber
#> value
#>   value is not a valid integer (type=type_error.integer)

validators

簡単なバリデーションはフィールド宣言時に記述することが可能ですが、ユーザー定義のバリデーションをpydantic.validatorを使用して作成することが可能です。

基本的なvalidator

簡単な例を考えます。
nameフィールドに半角スペースを含む場合のみ許容するvalidatorを定義します。

from pydantic import BaseModel, validator

# nameに半角スペースが含まれていない場合を許容しない
class User(BaseModel):
    name: str
    age: int

    @validator("name")
    def validate_name(cls, v):
        if ' ' not in v:
            raise ValueError("must contain a space")
        return v

# OK
Jiro = User(name="山田 二郎", age=17)

# NG
Saburo = User(name="山田三郎", age=14)
#> ValidationError: 1 validation error for User
#> name
#>   must contain a space (type=value_error)

複数のフィールドを使ったvalidatorを実装する

例えば、ある予定の開始時刻と終了時刻をそれぞれbeginendとして保持するEventクラスを考えます。

from datetime import datetime
from pydantic import BaseModel

class Event(BaseModel):
    begin: datetime
    end: datetime

event = Event(begin="2020-12-16T09:00:00+09:00", end="2020-12-16T12:00:00+09:00")

この時、endフィールドに代入される時刻が、beginフィールドに代入される時刻よりも後であることを保証したいです。
beginendの時刻が一致している場合も不正値であることにします。

やり方はいくつかあると思います。私からは2つ紹介します。
1つめの方法はpydantic.validatorの代わりにpydantic.root_validatorを使う方法です。

from datetime import datetime
from pydantic import BaseModel, root_validator

class Event(BaseModel):
    begin: datetime
    end: datetime

    @root_validator(pre=True)
    def validate_event_schedule(cls, values):
        _begin: datetime = values["begin"] 
        _end: datetime = values["end"]

        if _begin >= _end:
            raise ValueError("Invalid event.")
        return values

# OK
event1 = Event(begin="2020-12-16T09:00:00+09:00", end="2020-12-16T12:00:00+09:00")

# NG
event2 = Event(begin="2020-12-16T12:00:00+09:00", end="2020-12-16T09:00:00+09:00")
#> ValidationError: 1 validation error for Event
#> __root__
#>  Invalid event. (type=value_error)

# NG
event3 = Event(begin="2020-12-16T12:00:00+09:00", end="2020-12-16T12:00:00+09:00")
#> ValidationError: 1 validation error for Event
#> __root__
#>  Invalid event. (type=value_error)

もう一つは、validatorの仕様を活用します。
先にコードを紹介します。

from datetime import datetime
from pydantic import BaseModel, root_validator, validator

class Event(BaseModel):
    begin: datetime
    end: datetime

    @validator("begin", pre=True)
    def validate_begin(cls, v):
        return v

    @validator("end")
    def validate_end(cls, v, values):
        if values["begin"] >= v:
            raise ValueError("Invalid schedule.")
        return v

このコードでは2つのvalidatorを定義しました。

このEventクラスのインスタンス生成時には、pre=Trueという引数がセットされたvalidate_beginが先に実行されます。validate_beginではインスタンス生成時に引数beginに指定された値をそのままbeginフィールドにセットしています。

次にvalidate_endが処理されます。

ただし、validate_endvalidate_beginとは異なり第3引数としてvaluesという引数が指定されています。
pydantic.validatorの仕様として、あるvalidatorの前に実行されたvalidatorで入力値チェックされたフィールドに第3引数valuesを使用してアクセスすることができます。
このvalues_valuesでもValuesでもダメです。一種の予約語だと思ってください。

つまりこのコードの場合、各フィールドの入力値チェックの順序は以下の様になります。

  1. 先にvalidate_beginによるbeginの入力値チェックが実行される
  2. その後validate_endによってendの入力値チェックが実行される。この時validate_endのスコープ内からbeginフィールドにvalues["begin"]で参照することができる。

以上2通りの方法を紹介しました。もっといい方法があれば教えてください。

ListDictSetなどに含まれるそれぞれの要素に対するvalidator

以下の仕様を満たすRepeatedExamsクラスを考えます。

  • ちょうど10回の試験の点数(int型)を格納するList[int]型フィールドscoresを持つ。
  • それぞれの試験結果は50点以上でなければならない。
  • 10回の試験結果の合計点は800点以上でなければならない。

コードにすると以下の様になります。
ListDictSetなどの型のフィールドの要素のそれぞれに対して、あるvalidatorによる入力値チェックを行いたい場合はそのvalidatoreach_item=Trueを設定します。
下のコードでは、validate_each_scoreというvalidatorに対してeach_item=Trueを設定しています。

from pydantic import BaseModel
from typing import List

class RepeatedExams(BaseModel):
    scores: List[int]

    # 試験結果の回数がちょうど10回であるか検証
    @validator("scores", pre=True)
    def validate_num_of_exams(cls, v):
        if len(v) != 10:
            raise ValueError("The number of exams must be 10.")
        return v

    # 1回の試験結果が50点以上であるか検証
    @validator("scores", each_item=True)
    def validate_each_score(cls, v):
        assert v >= 50, "Each score must be at least 50."
        return v

    # 試験結果の合計が800点以上であるか検証
    @validator("scores")
    def validate_sum_score(cls, v):
        if sum(v) < 800:
            raise ValueError("sum of numbers greater than 800")
        return v

# OK
result1 = RepeatedExams(scores=[87, 88, 77, 100, 61, 59, 97, 75, 80, 85])

# NG, 9回しか試験を受けていない
result2 = RepeatedExams(scores=[87, 88, 77, 100, 61, 59, 97, 75, 80])
#> ValidationError: 1 validation error for RepeatedExams
#> scores
#>   The number of exams must be 10. (type=value_error)

# NG, 50点未満の試験がある
result3 = RepeatedExams(scores=[87, 88, 77, 100, 32, 59, 97, 75, 80, 85])
#> ValidationError: 1 validation error for RepeatedExams
#> scores -> 4
#>   Each score must be at least 50. (type=assertion_error)


# NG, 10回の試験の合計が800点未満である
result4 = RepeatedExams(scores=[87, 88, 77, 100, 51, 59, 97, 75, 80, 85])
#> ValidationError: 1 validation error for RepeatedExams
#> scores
#>   sum of numbers greater than 800 (type=value_error)

Exporting models

pydantic.BaseModelを継承したクラスのインスタンスは、辞書形式やJSON形式に変換したり、コピーを生成したりすることができます。
ただ変換・コピーできるだけではなく、対象となるフィールドを指定して特定のフィールドだけ出力することができます。

from pydantic import BaseModel, conint

class User(BaseModel):
    name: str
    age: conint(strict=True, ge=0)
    height: conint(strict=True, ge=0)
    weight: conint(strict=True, ge=0)

Kuko = User(name="Kuko", age=19, height=168, weight=58)
print(Kuko)

# 全フィールドを対象にdictに変換
Kuko_dict_1 = Kuko.dict()
print(Kuko_dict_1)
#> {'name': 'Kuko', 'age': 19, 'height': 168, 'weight': 58}

# nameだけを対象にdictに変換
Kuko_name = Kuko.dict(include={"name"})
print(Kuko_name)
#> {'name': 'Kuko'}

# 全フィールドを対象にコピー
print(Kuko.copy())
print(Kuko_2)
#> name='Kuko' age=19 height=168 weight=58 

# ageだけ除外してコピー
Kuko_3 = Kuko.copy(exclude={"age"})
print(Kuko_3)
#> name='Kuko' height=168 weight=58

# 全フィールドを対象にJSONに
Kuko_json = Kuko.json()
print(Kuko_json)
#> {"name": "Kuko", "age": 19, "height": 168, "weight": 58}
print(type(Kuko_json))
#> <class 'str'>

終わりに

Model ConfigSchemaをはじめとする他の要素は執筆時間があまり確保できず、断念しました。
今後追記できたらいいな...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python3.7以上のデータ格納はdataclassを活用しよう

はじめに

Pythonでデータを格納する際に辞書や普通のクラスを使っていませんか?Python3.7からはデータ格納に便利なdataclassデコレータが用意されています。

この記事では公式ドキュメントPEP557の説明ではいまいち掴めない、どういった時に便利で、なぜ使うべきなのかという点に触れつつ、使い方を説明していきます。

なお、以前のバージョンではPython3.6に限りpip install dataclassesによって使えるようになります。執筆時点ではGoogle Colaboratoryの環境がPython3.6.9ですが、デフォルトでdataclassesがインストールされています。

想定読者

  • dataclassの存在を知ったが何なのかよく分からない人
  • 可読性高くデータを扱いたい人
  • 「前はこんな機能なかったし、自分は別に使わなくて良いよ・・・」と思っている人

よく見かける最低限の説明

↓これが

class Person:
    def __init__(self, number, name='XXX'):
        self.number = number
        self.name = name

person1 = Person(0, 'Alice')
print(person1.number) # 0
print(person1.name) # Alice

↓こう書けます。(区別のためクラス名を明示的に変更しています)

import dataclasses
@dataclasses.dataclass
class DataclassPerson:
    number: int
    name: str = 'XXX'

dataclass_person1 = DataclassPerson(0, 'Alice')
print(dataclass_person1.number) # 0
print(dataclass_person1.name) # Alice

デコレータ@dataclasses.dataclassを付けて、__init__()の代わりに定義したい変数名を型アノテーション付きで書くことで使えます。

__init__()が自動的に作られたり、型アノテーションが必須になったりしている

何が変わったのかというと、まず__init__()引数をわざわざインスタンス変数に代入する必要がなくなりました。__init__()を自動的に作ってくれているということです。変数が多い時には面倒じゃなくなるし、すっきりして嬉しいです。また、後述しますが__eq__()__repr__()といった他の特殊メソッドも自動的に作られています。

そして、型アノテーションが必須になっているので、型が分かって嬉しいです。(ただし、これは通常のクラスでもdef __init__(self, number: int, name: str='XXX')としておきたいところ)

このクラスはデータを格納するために存在しているんだぞと明示できるというのも可読性の観点では重要な要素です。

辞書は避けたい

上記の例をやるだけであれば、辞書を使えばできます。なんでわざわざクラス、ましてdataclassデコレータなんて使うのでしょう。入出力はとりあえず辞書にしているという人は多いかと思われます。

dict_person1 = {'number': 0, 'name': 'Alice'}
print(dict_person1['number']) # 0
print(dict_person1['name']) # Alice

辞書の分かりやすいデメリットとしては、こんなところでしょうか。

  1. ドットアクセスができない。(ただし、できなくても別に良いかもしれない)
  2. 格納時の処理といったメソッドは入れられない。
  3. 型アノテーションができない。
  4. 決まった形になっていることがコードから掴みにくい。

後から読みやすい、メンテナンスしやすいコードを目指す上では3と4は大切なため、メソッドが不要な場合でも辞書を避ける理由となります。ただし、これらは通常のクラスでもカバーできます。

dataclassのメリット

dataclassデコレータを使ったクラスが通常のクラスよりどう優れているかを深堀していきます。

メリット:__eq__()が自動的に作られunittestもしやすい

インスタンスを比較した時、通常のクラスでは中身が同じでも異なるインスタンスはFalseとなります。id()が返す値を比較しているためですが、これはあまり役立ちません。unittestをするようなことを考えると、要素が一致している時はTrueになって欲しいです。

↓通常のクラスで何もしないとこうなります。

class Person:
    def __init__(self, number, name='XXX'):
        self.number = number
        self.name = name

person1 = Person(0, 'Alice')

print(person1 == Person(0, 'Alice')) # False
print(person1 == Person(1, 'Bob')) # False

↓通常のクラスで要素で比較するためには、__eq__()を自分で定義することになります。

class Person:
    def __init__(self, number, name='XXX'):
        self.number = number
        self.name = name

    def __eq__(self, other):
        if not isinstance(other, Person):
            return NotImplemented
        return self.number == other.number and self.name == other.name

person1 = Person(0, 'Alice')

print(person1 == Person(0, 'Alice')) # True
print(person1 == Person(1, 'Bob')) # False

↓dataclassデコレータを使えば、この__eq__()は自動的に作られます。手間が減りますし、見た目もすっきりします。

@dataclasses.dataclass
class DataclassPerson:
    number: int
    name: str = 'XXX'

dataclass_person1 = DataclassPerson(0, 'Alice')

print(dataclass_person1 == DataclassPerson(0, 'Alice')) # True
print(dataclass_person1 == DataclassPerson(1, 'Bob')) # False

また、@dataclasses.dataclass(order=True)とすれば、大小比較の演算のための__lt__()__le__()__gt__()__ge__()も作られます。これらはタプルを比較した時と同様に、最初に異なる要素同士で比較する仕様です。やや分かりづらいため、必要な場合は自分で定義した方が良いかもしれません。

メリット:asdictを使うとネストしていても綺麗に辞書に変換できる

JSONとして出力したいと時など、辞書に変換したい時にはdataclasses.asdict()を使います。dataclassをネストしていても問題ありません。

@dataclasses.dataclass
class DataclassScore:
    writing: int
    reading: int
    listening: int
    speaking: int

@dataclasses.dataclass
class DataclassPerson:
    score: DataclassScore
    number: int
    name: str = 'Alice'

dataclass_person1 = DataclassPerson(DataclassScore(25, 40, 30, 35), 0, 'Alice')
dict_person1 = dataclasses.asdict(dataclass_person1)
print(dict_person1) # {'score': {'writing': 25, 'reading': 40, 'listening': 30, 'speaking': 35}, 'number': 0, 'name': 'Alice'}

import json
print(json.dumps(dict_person1)) # '{"score": {"writing": 25, "reading": 40, "listening": 30, "speaking": 35}, "number": 0, "name": "Alice"}'

通常のクラスでも__dict__を使うことで辞書の形式に変換できますが、ネストしている時は一手間が必要です。

辞書からクラスに戻す時はアンパックを使い以下のようになります。

DataclassPerson(**dict_person1)

メリット:簡単にイミュータブルにできる

dataclassを使えば簡単にイミュータブルにできます。書き換えることがないデータに対してはイミュータブルにしておけば、どこかで変わっているのではないかという不安から逃れられます。

↓何も指定しないとミュータブルですが、

@dataclasses.dataclass
class DataclassPerson:
    number: int
    name: str = 'XXX'

dataclass_person1 = DataclassPerson(0, 'Alice')
print(dataclass_person1.number) # 0
print(dataclass_person1.name) # Alice

dataclass_person1.number = 1
print(dataclass_person1.number) # 1

↓デコレータの引数でfrozen=Trueとすると、イミュータブルになります。この時には__hash__()が自動的に作られ、hash()を使いハッシュ値を取得することもできます。

@dataclasses.dataclass(frozen=True)
class FrozenDataclassPerson:
    number: int
    name: str = 'Alice'

frozen_dataclass_person1 = FrozenDataclassPerson(number=0, name='Alice')
print(frozen_dataclass_person1.number) # 0
print(frozen_dataclass_person1.name) # Alice
print(hash(frozen_dataclass_person1)) # -4135290249524779415

frozen_dataclass_person1.number = 1 # FrozenInstanceError: cannot assign to field 'number'

イミュータブルにできるnamedtupleとは何が違うのか

イミュータブルにしたい用途では以下のような標準ライブラリもあります。

  • collections.namedtuple
  • typing.NamedTuple (Python3.6.1から)

これらを使うと、ドットアクセスができるタプル(=イミュータブルなオブジェクト)が作れます。

from collections import namedtuple

CollectionsNamedTuplePerson = namedtuple('CollectionsNamedTuplePerson', ('number' , 'name'))

collections_namedtuple_person1 = CollectionsNamedTuplePerson(number=0, name='Alice')
print(collections_namedtuple_person1.number) # 0
print(collections_namedtuple_person1.name) # Alice
print(collections_namedtuple_person1 == (0, 'Alice')) # True

collections_namedtuple_person1.number = 1 # AttributeError: can't set attribute

↓さらにtyping.NamedTupleは型アノテーションも可能です。

from typing import NamedTuple

class NamedTuplePerson(NamedTuple):
    number: int
    name: str = 'XXX'

namedtuple_person1 = NamedTuplePerson(0, 'Alice')
print(namedtuple_person1.number) # 0
print(namedtuple_person1.name) # Alice
print(typing_namedtuple_person1 == (0, 'Alice')) # True

namedtuple_person1.number = 1 # AttributeError: can't set attribute

詳しくはnamedtupleで美しいpythonを書く!(翻訳) - Qiitaが分かりやすいです。

dataclassとtyping.NamedTupleは似ていますが、細かい点では異なります。上記コードに載せたように、同じ要素を持つタプルとの比較でTrueになることはデメリットと言えそうです。

typing.NamedTupleの方が便利な機能としては、タプルですからアンパック代入ができることが挙げられます。使い所によっては無理にdataclassにするより良いでしょう。

各種機能

__repr__()が作られているので中身が簡単に確認できる

__repr__()が自動的に作られているので、print()などで中身が簡単に確認できます。

@dataclasses.dataclass
class DataclassPerson:
    number: int
    name: str = 'XXX'

dataclass_person1 = DataclassPerson(0, 'Alice')
print(dataclass_person1) # DataclassPerson(number=0, name='Alice')

通常のクラスで同じ表示をさせようとすると、以下を書く必要があります。

class Person:
    def __init__(self, number, name='XXX'):
        self.number = number
        self.name = name

    def __repr__(self):
        return f'{self.__class__.__name__}({", ".join([f"{key}={value}" for key, value in self.__dict__.items()])})' 

person1 = Person(0, 'Alice')
print(person1) # Person(number=0, name=Alice)

__post_init__()で初期化後の処理を書ける

通常のクラスの__init__()で代入以外の処理をしていたような時は、__post_init__()を使います。代入後にこのメソッドが呼ばれることになります。また、引数として渡さないインスタンス変数を作る場合はdataclasses.field(init=False)を使います。

@dataclasses.dataclass
class DataclassPerson:
    number: int
    name: str = 'XXX'
    is_even: bool = dataclasses.field(init=False)

    def __post_init__(self):
        self.is_even = self.number%2 == 0

dataclass_person1 = DataclassPerson(0, 'Alice')
print(dataclass_person1.number) # 0
print(dataclass_person1.name) # Alice
print(dataclass_person1.is_even) # True

InitVarで初期化用の引数を渡せる

以下の例のように、初期化時に引数として渡したいが、インスタンス変数にはしたくない値がある場合もあります。

class Person:
    def __init__(self, number, name='XXX'):
        self.name = name
        self.is_even = number%2 == 0

person1 = Person(0, 'Alice')
print(person1.name) # Alice
print(person1.is_even) # True

そういった時にはInitVarを使います。

@dataclasses.dataclass
class DataclassPerson:
    number:  dataclasses.InitVar[int]
    name: str = 'XXX'
    is_even: bool = dataclasses.field(init=False)

    def __post_init__(self, number):
        self.is_even = number%2 == 0

dataclass_person1 = DataclassPerson(0, 'Alice')
print(dataclass_person1.name) # Alice
print(dataclass_person1.is_even) # True

さいごに

入社後1年弱でのアドベントカレンダーということで、個人開発だとまあテキトウで良いかとなりがちだけど、チーム開発だと大事にしたい箇所の紹介でした。

使うと便利だけど、使わなくてもどうにかなる機能はキャッチアップを怠りがちですが、新機能には追加されるだけの理由があります。最近のPythonは型アノテーションが随分と取り入れられたりと数年前とは雰囲気もだいぶ変わりつつあります。好き嫌いはあるかもしれませんが、まずは知っておかないことには考えることもできませんので、置いてかれないようにしたいものですね!

参考文献

dataclasses --- データクラス — Python 3.9.1 ドキュメント
PEP 557 -- Data Classes | Python.org


おしらせ

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!

また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!
Follow @DeNAxTech

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

検証無視は蜜の味?多くのプログラマーが1度は手を染めたことがあるかもしれない(?)闇魔術

はじめに

この記事は、おそらく多くの人が既に知っているであろう、ごく簡単な内容です。
ほぼポエムなので、逆に分かりにくくなっている説もありますが、気軽に読んでください。

こんなコード見たこと、または、書いたことありますか?
3つの言語の例を書きますが、他の言語やライブラリなどでも同様の方法があるはずです。
(もしなかったら、なんていい環境なんだろうか!!)

中身は、ざっと斜め見する程度で大丈夫です。
絶対に手を染めてはいけない闇魔術なので、邪悪な例として捉えてください!!

  • Python + requestモジュール
dark_magic.py
# え?
res = requests.get('https://darkside.example.com/', verify=False)

おまけでこんなの付いてることあります。

dark_magic.py
# エエェェ??Σ(*゚□゚Σ
from requests.packages.urllib3.exceptions import InsecureRequestWarning 

requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 
  • C#
DarkMagic.cs
// え?
private static bool TrustAnyway(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    return true;
}

とやった上で、こうとか。

DarkMagic.cs
// エエェェ??Σ(*゚□゚Σ
System.Net.ServicePointManager.ServerCertificateValidationCallback = TrustAnyway;
  • Java
DarkMagic.java
// え?
public class TrustAnyway implements X509TrustManager {

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

とやった上で、こうとか。

DarkMagic.java
// エエェェ??Σ(*゚□゚Σ
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[] { new TrustAnyway() }, new SecureRandom());

慈悲の心か?

上のコードはすべて、HTTPSやTLSの暗号化された通信の初期段階で、
サーバー側の証明書の検証を無効にする、
もしくは、どのような通信相手の証明書も信頼するという実装です。
本来、検証をカスタマイズするような機能ですが、
検証をしないという実装にカスタマイズしている危険な闇魔術です。

誰でも信頼する慈悲深い実装にも見えますが、果たしてそうでしょうか?

サーバー星のダニエル君を信頼すべきか?

TLSの細かい動作の説明が目的ではないので、全然正しくない説明をします。
厳密には全然違うので、TLS/HTTPSなどの詳細が知りたい場合は、他の解説記事に譲ります。
この例えでは、いろんな部分が表現しきれていません。。。
あくまでも雰囲気だけです。

あなたは、異星(サーバ星)の外交官(仮にダニエル君と呼びましょう)と
秘密の情報をやりとりすることになりました。
ダニエル君は、身分証明書のようなものを見せてくれて、
「よかったら、この超強力な暗号機を使って、暗号化した上で秘密の情報をお互い交換しよう。
絶対に他の人に盗み見られることはなくて、君と僕しか暗号解読できないよ。」と言いました。

身分証明書には、特殊な塗料を使った刻印があって、
正式な審査を経て発行された身分証明書には、限られた認定製造元の塗料で刻印されています。
塗料の製造元が異なると同じ成分の塗料を作ることはできません。

特殊な光を当てることで、どの製造元の塗料なのかを見分けることができます。
反応する光は、塗料ごとにすべて異なります。
塗料は誰でも(自分でも)作ることもできますが、反応する光は、他の塗料とそれぞれが異なります。
また、塗料は経年劣化して、一定期間で光に反応しなくなるので、
身分証明書は定期的に発行する必要があります(正式な審査を経れば認定製造元の塗料で刻印される)。

身分証明書の刻印に、認定製造元の塗料に反応する特殊な光を当てて確認していけば、
認定製造元の塗料が塗られているかどうか確認することができるので、
正式な審査を経て発行された身分証明書かどうかが確認できます。

さて、前述のダニエル君は、ダニエルだと名乗り、なにやら身分証明書のようなものを持っていて、
しかも超強力な暗号機を使って暗号化するので、
あなたは、面倒な光による塗料の確認は省略して、
暗号機を使った秘密の情報のやりとりを行うことにしました。

お、お前、本当にダニエルか?

確認してないなら分かりません。
塗料を確認していれば、正式な審査を経て発行された身分証明書かどうか一定の確認ができます。

暗号機がどれだけ強力でも、あなたとダニエルは、やり取り内容を復号できます。
ダニエル君が、あなたが思っているダニエル君ではなくて、
悪徳星のエージェントだったら、あなたの秘密の情報だけ入手して、さよならされることでしょう。

相手が「ダニエルだよ」って言ってるし、まあ知っている相手だったら、
確認なんてしなくても大丈夫でしょうか?
悪徳星のエージェントが、ダニエル君を装っている可能性も考えましょう!!

信頼できる相手だから大丈夫だなんて、なんてお優しい。
とはなりません。
信頼できると言っているのは、あなたの感性であって、検証されたものではありません。
検証していないなら、信頼に値するかの確認ができません。

しかも厄介なのは、ほぼ最初に聞いてきます。
「信頼しているなら、あなたの認証情報を教えて」と。
(例えば、どこかに入るためのIDとパスワードとか。)
相手が偽物だった場合、認証情報渡した時点でジ・エンドです。
2度と連絡は来ないでしょう。
その代わり、あなたの認証情報使って、
あなたに代わって悪の限りを尽くしてくれるかもしれません。

ちなみに、最初の方に記載した以下のコードは、
「さすがにヤバいでしょ」とアドバイスをくれる友人を撃ち殺しているようなものです。
絶対にやめましょう!

dark_magic.py
# エエェェ??Σ(*゚□゚Σ
from requests.packages.urllib3.exceptions import InsecureRequestWarning 

requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 

さらに、場合によっては、本物のダニエル君であっても、悪の道に落ちている場合もあります。
悪の道に落ちた人リストの確認も重要になります。
(この部分は、TLS関連でいうと、証明書のRevoke(失効)、CRLなどが関連キーワードです。
失効理由はいろいろあって、悪の道に落ちたことだけが理由になるとは限りません。)

ここまでの手順で、実際にはダニエル本人かどうかの確認は行っていません。
「厳格なプロセスと手順を踏んで発行された有効な証明書かどうかの検証」に重きを置いています。
その厳格なプロセスを通過した有効な証明書を持っていると確認されたダニエル君なら、
信用しましょうといった感じです。
ダニエル本人たる十分な証拠があるかどうかの検証には別途確認が必要ですが、
その確認までやるというのは、デメリットの方が大きくなる場合もあるかもしれません。
一時期、TLS/SSLの公開証明書のピン留め(Pinning)しようぜという時代もありましたが、
デメリットの方が多いんじゃないかという風潮になって、今ではピン留めは、お勧めせず、
証明書の有効性の検証をより高いレベルにしていこうという意見が多いと思います。
Certificate Transparency(CT)などのキーワードも是非調べてみてください。

確認できないなら拒否!

これが基本です。
https/TLSを利用する時点で、証明書の検証は必須です。
例外はありません。
あえていえば、アンチパターンの説明をするときにのみ、
絶対に手を染めるべきではない悪い実装例として見せることはあるかと思います。

証明書は、お金を払って公的な発行機関(認証局)で発行してもらうのが必須というわけではありません
相手が信頼たる機関が認めた相手かどうかの確認、検証が必須なので、
どの証明書発行機関(認証局)が発行した(これは自分が本当に信頼するどこかかもしれません)有効な証明書か、
もしくは、どんな証明書かなどが重要です。

相手の名前(サーバでいうとホスト名など)の確認だけでは、確認、検証したことにはなりません。
TLSのサーバ証明書を適切に検証することが重要です。

自分達で証明書の発行機関(OpenSSLなど利用)を厳重に管理しているのであれば、
その認証局の公開証明書を決め打ちで信頼することもできます。
どの認証局を信頼するかは、自分達で明確に決めることもできます。

多くの場合は、OSやフレームワーク、ランタイム側で、
一般的な公的な証明書発行機関(認証局)がデフォルトで信頼されています。
より狭く、厳しく信頼範囲を制限する実装も可能です。

なぜ、多くのプログラマーが、この手の闇魔術に手を染めるか

作ったアプリが動かなかったからです。
「証明書の検証エラーが出たから、とにかく動くようにしたい」という理由に1点懸けでもいいくらいです。
とりあえず、手っ取り早く、その場をしのぎたいという場合が多いです。

最近でこそ、ちゃんと検証しようと考えている人が多くなったと思いますが、
10年前だったら、この手の闇魔術に手を染めた、染めそうになった方も多いのではないかと思います。

よくある言い訳と、その反論

証明書の検証を無効にしたり、どの証明書でも受け入れる実装をしている場合に、
よくある言い訳と、それに対する反論を列挙します。

この種の闇実装は、結構広まりやすいので、見かけるたびに、みなさんの手で防いでいきましょう!
水際対策が本当に大事です!

このような闇実装なコードを書くこと自体よりもむしろ、
これを他人が真似して広まってしまうことを問題として強く意識して欲しいです!!
広めないためにも、自分は絶対にやらないことも重視!

  • 「デモアプリだから。」

そのコード見た、後輩が、あなたの真似してもいいんですか?
癖になりますよ!

  • 「サンプルだから。」

それ、あなたがコントロールできない範囲まで広まりますよ!
みんなが真似してもいいんですか?

  • 「コメントで、"製品とかではちゃんと検証すべき"って書いてあるから」

あなたもちゃんとやりましょう!
今やりましょ!

システム側でデフォルトで信頼されている発行機関以外の証明書の検証の例

いくつかの例になります。
より厳しい検証を実装する方法があったり、
他の言語やフレームワークでは、その言語、フレームワークの方法があったりします。
また、証明書失効リスト(CRL)の処理は別途考慮が必要な場合があります。

ここで、「アプリが動作しないから、とりあえず、この証明書信頼しちゃえ」
というようなことをやってしまうと、
結局、上で説明した闇実装と大して変わりはないことになるので、その点は注意ください。

  • Python + requestモジュール

信頼する証明書発行機関(認証局)の証明書のパス指定もできます。

verify.py
res = requests.get('https://darkside.example.com/', verify='/path/to/certfile')
  • Java

keytoolなどを利用して、信頼する証明書発行機関(認証局)の証明書をトラストストアに追加できます。

環境変数の、javax.net.ssl.trustStorejavax.net.ssl.trustStorePasswordで、
トラストストアのパスや、パスワードを指定できます。

  • C#

Windowsの場合は、OS側に信頼する証明書発行機関(認証局)として証明書を登録するなど。

証明書の期限切れでエラーになって困っている場合

  • 自分または関係者が運用しているサーバーの場合

一刻も早く、証明書を更新しましょう。
本来は期限が切れる前に対応しておくべきだったと反省しましょう。

  • 自分ではどうすることもできない運営者が運営している場合

たぶん、やる気がないです。
その接続先と、さよならすることを考える時が来たのかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WatsonをPythonから使おう! -- Developer Cloud Python SDKの使い方

この記事は IBM Cloud Advent Calendar 2020 の 16 日目の記事です。

IBM CloudのAIサービスWatsonはRESTのAPIで呼び出し可能です。また各種メジャーな言語でSDKも利用できます。

当記事はPythonからSDKを利用してIBM Watsonのサービスを使う方法を説明します。なお当記事は以下のバージョンを元にテストしています:
v5.0.0

結構バージョンによって使い方が変わっているので、注意しましょう。

v5.0.0では以下のAPI呼び出しができます。

  • assistant_v1
  • assistant_v2
  • compare_comply_v1
  • discovery_v1
  • discovery_v2
  • language_translator_v3
  • natural_language_classifier_v1
  • natural_language_understanding_v1
  • personality_insights_v3 (2020/12/1よりインスタンス作成不可, 2021/12/1サービス停止)
  • speech_to_text_v1
  • text_to_speech_v1
  • tone_analyzer_v3
  • visual_recognition_v3 (2021/1/7よりインスタンス作成不可, 2021/12/1サービス停止)
  • visual_recognition_v4 (2021/1/7よりインスタンス作成不可, 2021/12/1サービス停止)

0. 前提条件

  • IBM Cloudアカウントを持っている
  • サービスを作成済み(以下の例ではLanguage Translator)

    • やり方がよくわからない場合はこちらを参考にしてください
  • python 3.5以上の実行環境がある

1.前準備

作成したサービスのAPIKEYとURLを確認できるようにしておいてください。
やり方がよくわからない場合はこちらを参考にしてください:
サービスの資格情報取得

2. Watson SDKの使い方の基本

まずは言語に関係なく、一般的な使い方の調べ方をご紹介します。
各種メジャーな言語のSDKの使い方は、実はIBM Cloud上のAPIドキュメントにあります!

API & SDK リファレンス・ライブラリーのカテゴリAI / Machine Learningにあるものが、各サービスのAPI&SDKのドキュメントへのリンクです。

当記事ではLanguage Translatorを例に説明しますので、Language Translatorのサービスを作成しておいてください。

基本1: API & SDK リファレンス・ライブラリーから使用するサービスをクリック

今回はLanguage Translatorを使いますので、API & SDK リファレンス・ライブラリーのカテゴリ AI / Machine Learning から Language Translatorをクリックします。
image.png

基本2: 右側の「Curl Java Node Python Go」などと書いてあるタブから、自分の使いたい言語をクリック

今回はPythonですので、「Python」をクリックしてください。
ちなみに2020/12/15に確認したところ、Curl, Java, Node, Python, Go, .NET, Ruby, Swift, Unityがタブにありました。
image.png

基本3: 左側のメニューから使い方を知りたいSubjectをクリック。

例えば「Introduction」だったらpipでのインストール方法が載っています。コピペして実行すればよいですね。
image.png

例えばMethod「Translate」だったら、選んだ言語の定義とサンプルコードが右側に表示されます。サンプルコードはコピペして、実行可能です。
ただし、{apikey}, {url}は自分のサービスのものに置き換える必要があるので注意しましょう。1. 前準備で準備したものに置き換えてください。
image.png

3. ではPythonから使おう!

2. Watson SDKの使い方の基本にのっているドキュメントって英語なんですけど、、、」と引いてしまった方のために、一通り使い方を説明します。
ただ細かいSDKの使用はやっぱりSDKリファレンスをみないとわからないので、感覚を掴んだら、ぜひSDKリファレンスで詳細を調べてみてください。Codeは共通語ですよね❤️

ここではLanguage Translatorを例に説明します

3.1 まずはライブラリのインストール

以下のpipコマンドでインストールできます。

pip install --upgrade ibm-watson

3.2 インスタンスの作成

ほぼここのサンプルコード(example code)のコピーです。

ここのサンプルコードで例えば'{apikey}' と書かれていると、'{xxxxxxxxx}'(xxxxxxxxxは自分のapikey)とコードに書いてしまう方が、ハンズオンをやったりすると結構います。わかりにくいのですが、{ }は不要です。{apikey}ごと置き換えます。下記のコードは変数として最初に定義して{ }の記述は無くしました。

APIKEY='自分のAPIKEYを入れる'
URL='自分のURLを入れる'
VERSION='2018-05-01' #使いたいVersionを入れてください

from ibm_watson import LanguageTranslatorV3
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator

authenticator = IAMAuthenticator(APIKEY)
language_translator = LanguageTranslatorV3(
    version=VERSION,
    authenticator=authenticator
)

language_translator.set_service_url(URL)

上記のVERSIONはAPIドキュメントの右側の「Versioning」をクリックすると、ドキュメントで想定しているVersionが記載されていますので、そちら値を使用するのがよいかと思います。

image.png

3.3 Methodの呼び出し

Language Translatorは翻訳サービスので、翻訳メソッド「Translate」を呼び出してみましょう。

呼び出し方法のMethod名、パラメータの内容などは基本3: 左側のメニューから使い方を知りたいSubjectをクリック。に書いたように、APIドキュメントの右側から知りたい内容をクリックすればサンプルコードと共に表示されます。

以下のコードを3.2 インスタンスの作成のコードの次に書きましょう。
model_id='en-es'なので、英語(en)からスペイン語(es)への翻訳となります。

translation = language_translator.translate(
    text='Hello, how are you today?',
    model_id='en-es').get_result()

language_translator.translate()の戻り値にget_result()して何が戻ってくるかはAPIドキュメントの真ん中の列に書かれています。
image.png

また戻り値全体の話はAPIドキュメントData HandlingResponse detailsに載っています。get_result()以外に、Header情報やHTTP Status Codeを取得することが可能です。
image.png

結果を表示してみましょう.インデントをつけて見やすく表示させています:

import json

print(json.dumps(translation, indent=2, ensure_ascii=False))

表示されるもの:

{
  "translations": [
    {
      "translation": "Hola, ¿cómo estás hoy?"
    }
  ],
  "word_count": 7,
  "character_count": 25
}

翻訳結果だけ欲しい場合は以下のように指定します(辞書型になります):

print (translation[ "translations"][0]["translation"])

表示されるもの:

Hola, ¿cómo estás hoy?

以上です。

4. 最後に

使い方のポイントを抑えれば、APIドキュメントが英語でも簡単にCodeがかけると思いますので、ぜひ他のAPIでも試してみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IWATSU製オシロスコープから得たcsvデータを自動でグラフ化したい!

自動化したい!

私は研究を行う上で、頻繁にIWATSU製のオシロスコープのお世話になっております。

オシロスコープから電圧やらの波形を得て、それをcsvデータとしてUSBに出力することが度々あるのですが、オシロから直接出力されるcsvデータを再びグラフ化するには少し面倒な手段を踏む必要があります。

と言いますのも、IWATSUのオシロから得られるcsvデータは以下のような形式にまとめられています。

csv.png

1~19行目までに計測条件、20行目にチャンネル、21行目以降から電圧値が表示されます。
もちろんこの計測条件の表示は重要な要素なのですが、やはりグラフ化や信号処理の足かせになります。また、電圧値の表示はありますが時間軸のデータはありません。
csvデータが2,3個しかないなら手動でグラフ化しても良いのですが、私の研究ではデータが100個ほど生じます。全部手動でやったら気が狂っちゃいます。

そこでPythonでコーディングを行い、以下のようなグラフを自動生成します。
signal1_modified.jpg

動作環境

OS : Windows10
開発環境 : Anaconda Spyder 4.1.5
オシロスコープ : IWATSU DIGITAL OSCILLOSCOPE DS-5412

自動化プログラム

oscilloGraph.py
import tkinter
import tkinter.filedialog
import glob
import os
import csv
import numpy as np
import matplotlib.pyplot as plt

DeltaRow = 6                #時間幅が格納された行
DataRow = 21                #電圧値が格納された最初の行
global delta                #サンプリング周期

#----------GUIからcsvを選び、選ばれたデータのみを戻す----------#
def file_open():
    #---ファイルオープン---#
    root = tkinter.Tk()
    typ   = [('csvファイル', '*.csv')]
    DirIn = r'C:/Users'
    fni = tkinter.filedialog.askopenfilenames(filetypes = typ, initialdir = DirIn)
    root.destroy()

    #選択したcsvデータのパスを格納
    print(fni[0])
    return fni

#----------GUIからcsvを選び、ディレクトリ内の全csvデータを戻す----------#
def files_open():
    #---ファイルオープン---#
    root = tkinter.Tk()
    typ   = [('csvファイル', '*.csv')]
    DirIn = r'C:/Users'
    fni = tkinter.filedialog.askopenfilenames(filetypes = typ, initialdir = DirIn)
    root.destroy()
    #ディレクトリ内の全てのcsvを選択
    for i in range(len(fni)):
        fno  = fni[i][0:len(fni[i])-4]              #fniのタプルからパスのみを抽出
        filename= os.path.basename(fno)
        csv_path = fno.replace(filename,'')  
        csv_files = glob.glob(csv_path + "*.csv")    #ディレクトリ内の全てのcsvを格納

    #戻り値:選択されたファイルのパス(type of tuple)
    return csv_files

#----------csv_file内の数値をdata,timeリストに格納する----------#
def data_storing(csv_file_path):
    count = 0
    global delta    #グローバル関数宣言
    data = []   #Ch1の電圧
    time = []   #時間を格納
    with open(csv_file_path,'r') as file:
        for row in csv.reader(file):
            if(count == DeltaRow - 1):  #サンプリング周期の格納
                delta = float(row[1])
                count += 1
            elif(count >= DataRow - 1):
                data.append(float(row[0])) #Ch1をdata1に格納
                count += 1
            else:
                count += 1
    #時間の格納
    for time_count in range(len(data)):
        time.append(time_count * delta)
    #戻り値 : 電圧値、時間軸が格納されたリスト
    return data,time

#----------グラフ出力----------#
def plot_graph(data,time,csv_file):
    #data,timeをnp.array化
    data = np.array(data)
    time = np.array(time)    

    #パラメータの設定
    fig = plt.figure(figsize=(15,10))           #Figure設定
    fig.align_labels()
    ax = fig.add_subplot(111)                   #Axes設定
    ax.set_xlabel("Time[s]",fontsize=35)        #xlabel
    ax.set_ylabel("Voltage[V]",fontsize=35)     #ylabel

    #---全体のパラメータ設定---#
    plt.rcParams["font.family"] = "Times New Roman"
    plt.rcParams["xtick.direction"] = "in"
    plt.rcParams["ytick.direction"] = "in"
    plt.rcParams["axes.linewidth"] = 1.0

    #---上、右にも目盛を置く---#
    ax.tick_params(top = True)
    ax.tick_params(right = True)
    ax.tick_params(labelsize = 25) 

    #---目盛範囲制限---#
    plt.xlim(0,5)
    plt.ylim(-0.05,0.06)
    plt.grid()

    #---軸設定---#
    plt.tick_params(width = 2,length = 15)  #目盛りサイズ
    plt.xticks(np.arange(0,5.1,0.5),position=(0.0,-0.015))   #時間軸設定
    plt.yticks(position=(-0.015,0,0))
    plt.plot(time,data,color="red",linewidth=3)

    #画像の保存
    graphs_path = csv_file.replace(os.path.basename(csv_file),"") + "/Graphs"
    if(not(os.path.exists(graphs_path))):
        os.mkdir(graphs_path)
    plt.savefig(graphs_path + "/" + os.path.basename(csv_file)[:-4] + "_modified.jpg")    
    plt.show()
    plt.close()

#----------取得したデータをcsv保存----------#
def save_csv(data,time,csv_path):
    #保存先のパス
    save_path = csv_path.replace(os.path.basename(csv_path),"") + "/modified_data"
    if(not(os.path.exists(save_path))):
        os.mkdir(save_path)

    output = []

    for j in range(len(data)):
        if(j == 0):
            output.append("Time[s]" + "," + "Voltage[V]" )
        else:
            output.append(str(time[j-1]) + ',' + str(data[j-1]))
    #csv保存先
    save_path = save_path + "/" + os.path.basename(csv_path)[:-4] + "_modified.csv"
    print("save_path : " + save_path)

    #保存
    with open(save_path, 'w',encoding="UTF-8") as fo:
        writer = csv.writer(fo, delimiter='\n')  
        writer.writerow(output)


if __name__ == "__main__":
    csv_files = files_open()   #複数のcsv_fileを処理
    #csv_files = file_open()     #単一のcsv_fileを処理

    #csv_files内のデータを1つづつ関数に送り処理
    for csv_file in csv_files:
        csv_data = data_storing(csv_file)   #csv_data[0] : 電圧 , csv_data[1] : 時間

        data = csv_data[0]
        time = csv_data[1]

        plot_graph(data,time,csv_file)
        save_csv(data,time,csv_file) 

プログラム実行後

正常に処理が終了するとディレクトリ内部にGraphsディレクトリとmodified_dataディレクトリが自動生成されます。Graphsには生成されたグラフ画像がjpg形式で格納され、modified_dataには時間軸とそれに対応する電圧値が出力されたcsvファイルが格納されます。

処理後のディレクトリ
ディレクトリ.png
出力されるmodified.csvデータ
modified.png

コードの解説

プログラムを実行すると、tkinterからGUIが起動するので目的のcsvデータを選択します。
main関数内の

    csv_files = files_open()   #複数のcsv_fileを処理
    #csv_files = file_open()     #単一のcsv_fileを処理

をコメントアウトで選択することで処理するcsvの数を制御できます。
file_open()を選択すると選んだcsvデータのみを処理し、files_open()を選択すると選んだcsvデータの存在するディレクトリ内部の全てのcsvを一気に処理します。

file_open(),files_open()内の変数DirInに代入するパスを書き換えると、プログラム実行時にそのパスのディレクトリが開きます。

    DirIn = r'C:/Users'

data_storing()の引数として、対象となるcsvデータの絶対パスを送ることでリスト型変数にcsvの値を格納します。IWATSU製オシロから出力されるcsvデータには、セルB6にサンプリング周期が格納されているので、まずこれを変数deltaに格納します(DeltaRowに行数の6が代入されています)。
その後、リスト型変数dataに電圧値を追加していきます。

    with open(csv_file_path,'r') as file:
        for row in csv.reader(file):
            if(count == DeltaRow - 1):  #サンプリング周期の格納
                delta = float(row[1])
                count += 1
            elif(count >= DataRow - 1):
                data.append(float(row[0])) #Ch1をdata1に格納
                count += 1
            else:
                count += 1

次に時間軸の値を設定する必要があります。n番目の電圧値に対応する時間は、

(サンプリング周期 delta) * n

で計算ができますので、データ番号とdeltaを掛けてリスト型変数timeに追加していきます。

    #時間の格納
    for time_count in range(len(data)):
        time.append(time_count * delta)

これで電圧軸と時間軸の準備は完了です。これらをplot_graph()に送ることでグラフを自動生成し、ディレクトリに保存します。
グラフの表示範囲を変更したい場合は、以下の数値を書き換えます。

    #---目盛範囲制限---#
    plt.xlim(0,5)
    plt.ylim(-0.05,0.06)
    plt.grid()

    #---軸設定---#
    plt.tick_params(width = 2,length = 15)  #目盛りサイズ
    plt.xticks(np.arange(0,5.1,0.5),position=(0.0,-0.015))   #時間軸設定
    plt.yticks(position=(-0.015,0,0))
    plt.plot(time,data,color="red",linewidth=3)

最後にsave_csv()で電圧値と時間軸のみが格納されたcsvデータを保存します。これで信号処理などが簡単になると思います。

最後に

IWATSU製のオシロスコープでのみテストを行っています。他のオシロから得たcsvでも作動するのかは不明です。他社のオシロを持っている方がいらっしゃいましたら、csvの出力形式をご教授いただければ有難いです。

本プログラムは自分の卒業研究に用いるために作ったものです。非常に限定された用途ではありますが、お役に立てれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Power BI+Pythonで主成分分析

はじめに

PythonやR等のプログラミング言語では、統計解析のライブラリが豊富で、無料で利用できるメリットがありますが、ソースコードの修正やコマンドラインからの操作は煩雑です。
ここでは、主成分分析を例に、Power BIのクエリーからデータを読み込み、Pythonによる統計解析、PowerBIダッシュボードによる可視化を試みてみます。

サンプルデータ

ポケモンで多変量分析・主成分分析を始めよう! RとTableauの連携の記事を参考に、Kaggleで公開されているThe Complete Pokemon Datasetのpokemon.csvを使います。

Power BIクエリーの編集

1.ダウンロードしたPockemon.csvを読み込みます。
image.png

2.分析に不要な列を削除し、Name列、データ列1、データ列2...とします。

image.png

3.Pythonスクリプトを追加します。
このスクリプトでは、1列目をName、2列目以降をデータとして、skikit-learnのライブラリを使って主成分分析を行います。主成分分析のPythonコードは、意味がわかる主成分分析を参考にしています。

image.png

# 'dataset' はこのスクリプトの入力データを保持しています
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA

X=dataset.drop(dataset.columns[0],axis=1).values
pca = PCA()
pca.fit(X)
pca_point = pca.transform(X)
dataset['PC1']=pca_point[:,0]
dataset['PC2']=pca_point[:,1]
evr=pd.DataFrame(data=pca.explained_variance_ratio_,  columns={'explained_variance_ratio'}, dtype='float')
evr['PC No.']=evr.index+1

4.evrには各成分ごとの寄与率がセットされており、値を確認します。(各成分の影響力をを示しており、第1主成分が0.46、第2主成分が0.19)

image.png

5.datasetに主成分分析結果、主成分1(PC1)と主成分2(PC2)が追加されており、これを読み込みます。

image.png

6.散布図に、X軸 PC1,Y軸 PC2をプロットします。(カテゴリOnでデータラベルを表示)

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SOQLでSELECT * FROM SOME-TABLEっぽいことする

はじめに

この記事は 2020 年の RevComm アドベントカレンダー 17 日目の記事です。 16 日目は @shuheikatoinfo さんの「音声合成・激動の10年を振り返る」でした。

こんにちは、 RevComm でサーバサイドエンジニアをしている @enotesupa です。

今回は、Python から Salesforce の REST APIを実行するためのパッケージの一つである、 simple-salesforce を利用して、SOQL では実行できない、オブジェクトの全てのカラムの値を取得する方法を紹介します。

標準的な SQL には、 SELECT * のように、ワイルドカードを使ってテーブル(オブジェクト)の全てのカラム(項目)の値を取得する事ができますが、 Salesforce の SOQL では使うことができません。しかし、実際にクエリを実行する際に、オブジェクトの項目名がわからないため、全ての項目について取得したい場合もあるでしょう。

ただし今回紹介する方法は、一部の標準オブジェクトなどでは、項目数が多すぎて Malformed Request のエラーレスポンスが返される場合があるのでご注意ください。

SOQLって何?

Salesforceには、Trailheadという、eラーニング形式で学習できる多数のコンテンツが用意されています。その内容の一つに SQL から SOQL への移行という単元があり、 SQL と SOQL の違いについて紹介されています。

すぐに気づく大きな違いは、SOQL には SELECT * などというものは存在しないことです。SOQL は Salesforce データを返し、そのデータはマルチテナント環境に存在しますが、マルチテナント環境では全員が「データベースを共有」しているようなものであるため、* などのワイルドカード文字を使用すると問題が発生します。率直に言うと、特にテーブルの項目名がわからない場合などに、新しい SQL クエリを開始して、SELECT * FROM SOME-TABLE と入力してしまいがちですが、このアクションは、共有環境内の他のテナントに多大な影響を及ぼす可能性があります。それは日曜日の朝 7 時に庭の芝生を刈るようなもので、まったく思いやりのない迷惑な行為です。

とあり、サクッと SQL で馴染んだ文を書いても実行エラーが発生し、一切使えないことが分かります。

アプリケーション側で使わないカラムまで取ってくることは SQL でもアンチパターンであるとも言われたりするので、今回はテーブルの項目名がわからない場合の調査として 1 回だけ使用するようなユースケースで実現できるものを作ってみます。

前提

  • Python 実行環境(僕は Jupyter Notebook を使用します)
  • API 連携ができる Enterprise Edition 以上の Salesforce アカウント(僕は Enterprise のサンドボックス環境アカウントを使用します)

ライブラリのインストール(Jupyter Notebook)

Jupyter では ! 始まりでコマンドを実行できます。

!pip install simple-salesforce

インポート

表示名であるあるですが - であるものが _ になっているので typo に注意。また、結果表示のために pandas も活用します。

import simple_salesforce
import pandas as pd

認証

接続アプリケーションを作成して OAuth を使った認証方式も使えますが、今回はユーザ名とパスワードを用いた認証方式を用いました。以下のように、simple_salesforce.Salesforce オブジェクトを作成します。

username = "hogehoge@fugafuga.com"
password = "hogehogepass"
org_id = ""
proxies = None
security_token = None
domain = "test"

sf = simple_salesforce.Salesforce(
    username=username, password=password, domain=domain,
    organizationId=org_id, proxies=proxies, security_token=security_token)

これでクエリを叩けるようになります。

クエリを作成・実行する

今回の記事のためにカスタムオブジェクト・カスタム項目を新たに作成し、その中で更新日時が最新の 10 件を取得します。

# カスタムオブジェクトの項目の一覧を取得する
fields = [x['name'] for x in sf.RevCommAdventCalendar__c.describe()['fields']]
print(fields)

# クエリを作成する
query = "SELECT {} FROM RevCommAdventCalendar__c order by LastModifiedDate desc limit 10".format(", ".join(fields))
print(query)

# クエリを実行する
records = sf.query(query)
print(records)

出力結果

スクリーンショット 2020-12-17 11.41.12.png

実行結果

pd.DataFrame を利用して結果をテーブル表示させます。

pd.DataFrame(records['records'])

スクリーンショット 2020-12-17 10.44.04.png

おわりに

制約はありますが、カスタムオブジェクトにおいて SOQL でも禁じ手の SELECT * FROM SOME-TABLE っぽいことができるという検証結果でした。

実行結果の通り、一度に多くの項目を取得するため、実際にアプリケーションに組み込んで使う際には、必要なカラムに絞って実行しましょう。

明日は @zomaphone さんの投稿です。お楽しみに! 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Let's challenge LeetCode!! _3

こんにちは
今回は初めて medium にチャレンジしてみました。

2.Add Two Numbers
You are given two non-empty linked lists representing two non-negative integers.
The digits are stored in reverse order, and each of their nodes contains a single digit.
Add the two numbers and return the sum as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

--ザックリ翻訳--
正の整数が必ずいくつか詰まったリンクリストをプレゼントします。
リスト内の数字は各桁を表しており、反転して格納してあります。
両者をリンクリストとして合算してください。

これは↓ なにを言いたいのか今一でした。分かる方、教えてください m(_ _)m
You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Example
*Input: l1 = [2,4,3], l2 = [5,6,4]
*Output: [7,0,8]
*Explanation: 342 + 465 = 807.

なるほど、とりあえず、
以下のステップでアプローチでどうでしょう。

Step1
link list になっているので、演算用に編集

Step2
演算したものをリストに埋め込み直す。

すいません、説明が雑スギかもしれません(笑)
スマートではないですが、こんな感じで一応通りました。

AddTwoNumbers.py
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        str_l1,str_l2="",""

        while l1:
            str_l1 += str(l1.val)
            l1 =l1.next
        while l2:
            str_l2 += str(l2.val)
            l2 =l2.next

        Ans = str( int(str_l1[::-1]) + int(str_l2[::-1]) )[::-1]
        self.p = ListNode(int(Ans[0]))

        for i in range(1,len(Ans)):
            self.addNode(int(Ans[i]))
        return self.p

    def addNode(self,val):

        if self.p is None:
            self.p.val = val
        else:
            runner = self.p
            while runner.next is not None:
                runner = runner.next
            runner.next =ListNode(val)

リンクリストに埋め込み直すスマートな方法が思いつかなかったので、
泥臭いですが、別途 def addNode を用意しました。

自分の成長の為には、ほかの人のコードから
アプローチを学ぶのはありかもしれません。
面白いものがあれば、パクって、この記事にアップすると思います、、こっそり。

シンプルな書き方は python 特有のライブラリを使う感じなんでしょうか?
汎用性が高くないと、実用性が低いかもしれません。
とりあえず、ほかの人のコード読みに行ってきます(。・ω・)ノ゙ イッテキマ-ス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python で外部プログラムを実行して結果の行をパースするメモ

外部プログラミング実行して結果取得には, 基本 subprocess.run を使います.

subprocessについてより深く(3系,更新版)
https://qiita.com/HidKamiya/items/e192a55371a2961ca8a4

ありがとうございます.

Python には C での scanf 相当が存在しません. regex で頑張るのも面倒です.
{} な記述で scanf 的なのができる parse が便利です.

https://pypi.org/project/parse/

pip install parse でぺろっと入ります.

外部プログラム実行と結果取得は以下のような感じです.

import subprocess
import parse

x = 3.13

ret = subprocess.run("python func.py {}".format(str(x)), shell=True, capture_output=True)

def extract_result(lines):
    for line in lines:
        print(line.decode("utf-8"))
        ret = parse.parse("ret = {:g}", line.decode("utf-8"))
        if ret:
            return ret[0]

    raise RuntimeError("`ret = ...` line not found.")

lines = ret.stdout.splitlines()

print(extract_result(lines))
# func.py

import sys
import numpy as np

x = float(sys.argv[1])

print("bora")
print("ret = {}".format(np.sin(x)))
print("dora")

shell=True はお好みで(False の場合(デフォルトの場合), シェルの機能(e.g. PATH 変数)使えないので /usr/bin/python などと絶対パス指定が必要)
capture_output で stdout の結果を取得します.

subprocess の stdout の結果は byte 文字列になっているのでデコードが必要です.
通常は utf-8 かと思います.

Convert bytes to a string
https://stackoverflow.com/questions/606191/convert-bytes-to-a-string

TODO

subprocess.run で encoding 指定してもよいかもしれません.

Python の subprocess
https://qiita.com/tanabe13f/items/8d5e4e5350d217dec8f5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Class '~~~' has no 'objects' memberpylint(no-member)のエラー/警告について(@Python/Django)

はじめに

今回は、Djangoでアプリ作成する際に発生したエラー・警告について触れていきます。

参考記事はこちら

エラー詳細

Djangoのクラスであるmodelsのオブジェクトを参照する際にエラー検知されてしまいました。。

スクリーンショット 2020-12-16 15.13.22.png

実行した処理

①pylint-djangoをインストール
ターミナルを開き以下のコマンドを実行します。

$ pip install pylint-django

②VSCodeの設定変更

VSCodeを利用しているため設定に変更を加えていきます。

画面左下の歯車マークから設定を開き(command+,)、
Python › Linting: Pylint Argsの箇所に以下の項目を追加します。

--load-plugins=pylint_django

以上で完了しました。

まとめ

エラーや警告の内容を深掘ると興味深いですが、
気付いたら1日経ってたりします。。
頑張っていきます!!?

参考にさせていただいた記事の作成者様ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flask, Vue.js, OpenCV, Pytorchで画像認識アプリをHerokuにデプロイする

MNISTで学習したモデルを使って複数桁の数字を認識できないかと思った.ついでにWebアプリとしてデプロイもしてみた.
- コード
- アプリ

作業工程

  • 機械学習モデルを学習
  • 複数桁の数字を一桁の数字へ変換するモジュールを作成
  • Flask & Vue.jsのWebアプリを作成
  • Herokuへデプロイ

機械学習モデルを学習

PyTorch MNIST example - GitHubを参考にして学習.
学習に使用したコード→ ./server/modules/mnist.py

複数桁の数字を一桁の数字へ変換するモジュールを作成

コード→ ./server/modules/processes.py
メソッドについて説明する.

__init__()

コンストラクタではフロントから送信された1次元の画像データ(1channel)と画像のサイズ(width, height)を受け取り,(width, height, channel)へ変換する.
例) MNISTだったら (784, ) → (28, 28, 3)

_labeling()

OpenCVを使って,画像を2値化・ラベリング処理し,それぞれを正方形の画像データに変換する(_to_square()を使用).

_to_square()

MARGIN=5,つまりラベル付けされたピクセルから縦横方向に最低5ピクセル余白を取った正方形画像データに変換する.

divide_to_digit()

_labeling(),_to_squre()で変換したそれぞれの画像をbase64でエンコードする.(あとでフロントに渡すため)

Flask & Vue.jsのWebアプリを作成

フロンエンド

package.jsonは以下のようになっている.開発時はnpm run watchを実行すると便利.コンパイルされたファイルが./distに作成される.

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "watch": "vue-cli-service build --watch"  # 追加文
  }

サーバサイド

以下のパッケージをインストールする.
- Flask
- Flask-Cors
- gunicorn
- numpy
- opencv-python
- Pillow
- torch
- torchvision

開発時はpython server/app.pyを実行してサーバを立ち上げる.→ http://127.0.0.1:5000

Herokuへデプロイ

まず,Heroku CLIをインストールする.

その後,ログインしてHerokuへpushする.

heroku login
git add.
git commit -am "[update]"
git push heroku main

最後に

Herokuにデプロイする時に,OpenCVやPytorchが結構厄介だった.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む