- 投稿日:2020-12-16T23:56:26+09:00
Pythonパッケージ版のMediaPipeが超お手軽 + 簡易なMLPで指ジェスチャー推定
- この記事はTensorFlow Advent Calendar 2020の9日目の記事です。
MediaPipe(Pythonパッケージ版)
Pythonパッケージ版のMediaPipeが超お手軽です。
pip install mediapipe
pipでインストールして、、、
import mediapipe as mpimportして、、、
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分類(静止、時計回り、反時計回り、移動)するモデルを
組み合わせて動かしています。
「パー」「グー」「指差し」の分類
こんな感じに前処理して、、、(ID:0が最終的に0,0になっているの少々無駄ですが、、、)「静止」「時計回り」「反時計回り」「移動」の分類
人差指の座標履歴を16点ほど取って、こんな感じに前処理して、、、(T-15が最終的に0,0になっているの少々無駄ですが、、、)プログラム
上記のプログラムもGithubで公開しています。
適当MLPなので、もっとイカした構造にすると、もっと精度が上がります?
- 投稿日:2020-12-16T23:50:32+09:00
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を持っていれば、以下のような結果を作れるようになります。
まず、左の図のような見た目になる光源環境、オブジェクトのマテリアルが設定されたシーンがあるとして、光源環境は変えないで、マテリアルの反射だけを変えて、右の図のような見た目にしたいと思ったとします。
inverse renderingができれば、マテリアルの設定を自動で修正してくれて、制約の中でも、近い見た目を作ることができます。
一例が下の図です。これは初期状態から色の調整をして、近づけていく様子をアニメーションにしてみました。
無論変更できないような部分の反射率は変わらないので、全体がそういう色になるとかではありません。
変えられるところのパラメタを自動で修正するというものです。このページではインストールから、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 RAMGPUは、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だけです。
ビルドが終わったら、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やってみます。
レンダリングしたら、左図の状態ですが、球の反射率を変更して、中図に近づけて、結果的に右図を作ります。
![]()
下のようにコードの冒頭で、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.htmlfrom 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わからんです。
- 投稿日:2020-12-16T23:45:09+09:00
Pythonのオブジェクトからplantumlのオブジェクト図を生成出力する
オブジェクト図
Pythonでは、値、変数、関数、クラス、メソッド、モジュールなど、すべてがオブジェクトで、メモリ上に存在するものです。
メモリ上にどんなオブジェクトが存在しているのか把握することで、Pythonの動作を理解したり、実行状態を把握することができます。
実際、他の方のPython記事に対して、変数辞書からのオブジェクト図を示してコメントすることがあります。そこで、指定したオブジェクトからplantumlのオブジェクト図(テキスト形式)を生成出力するプログラムを作ってみました。
なお、plantumlから画像に変換するには別の作業が必要です。
- コメントの例: https://qiita.com/risuoku/items/c71e4fdb7ea9d9800e20#comment-7b342ec242f0debc8e41
- 自分で書いたオブジェクト図の例
![]()
- 指定したオブジェクトからplantumlを生成出力して作成した図の例(上の図を90度回転したような配置):
![]()
色の説明:
- ピンク: 出力指定したオブジェクト
- 紫: 関数
- 緑: クラス
- 青: インスタンス
- オレンジ: 変数辞書
プログラム
plantumlオブジェクト図生成出力スクリプト
objectuml.pyimport 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.pyclass 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参考資料
- https://plantuml.com/ja/
- https://plantuml.com/ja/object-diagram
このサイトの各図にあるEdit online
ボタンを押して、上記の plantuml コードをコピー&ペーストしてSubmit
ボタンを押すと図が生成されます。
- 投稿日:2020-12-16T23:22:03+09:00
ベイスターズの投手の配球を分析する
経緯
ここ2,3年、野球(特に横浜DeNAベイスターズ)にはまっており、前から野球データの分析がしたいと考えていた。
試合中にTwitterでこのキャッチャーの配球は○○だから嫌い、とかよく見かけるが、私は野球をやったことがないのでよくわからない。そこで、「配球」という観点で分析したい。
データ
データはYahooスポーツナビの一球速報を使う。
スクレイピングにより投手、打者、球の位置、球速などを取得。
今年の全公式試合のベイスターズの守備のときのデータを用意。配球を散布図にする
htmlのピクセル値からストライクゾーン(長方形の枠)を推定して、散布図にした。
おっと、、、サイト側の都合なのか謎の区切りが出現した。
とりあえずボール判定されたボールがストライクゾーンの外に行っている(若干ずれているが気にしない)ので、これであっているだろうとして続けることにする。打者の左右で比べてみる
右打者(10040球)、左打者(5840球)で比べてみる。
両方ともいわゆるアウトローに投げられている。左打者に対してはストライクゾーンの外低めにも多く投げられている。
ピッチャーの比較
ベイスターズの今年の投球数ランキング上位3人(大貫選手、井納選手、濱口選手)の配球を比較してみる。
今年活躍した大貫投手はアウトロー、ストライクゾーン低めにしっかり投げている。ストライクゾーン中段にも多く投げているらしい。
井納投手もアウトロー、ストライクゾーン低めに多く投げられている。
濱口投手はほか2選手に比べストライクゾーン外の配球が多い。確かに四球が多い印象。今後
球速や球種などの分析をしたいし、いつかは予測モデルなんかに挑戦してみたい。
- 投稿日:2020-12-16T23:22:03+09:00
ベイスターズの投手の配球をスクレイピングして可視化する
経緯
ここ2,3年、野球(特に横浜DeNAベイスターズ)にはまっており、前から野球データの分析がしたいと考えていた。
試合中にTwitterでこのキャッチャーの配球は○○だから嫌い、とかよく見かけるが、私は野球をやったことがないのでよくわからない。そこで、「配球」という観点で分析したい。
データ
データはYahooスポーツナビの一球速報を使う。
スクレイピングにより投手、打者、球の位置、球速などを取得。
今年の全公式試合のベイスターズの守備のときのデータを用意。スクレイピングのコード : https://github.com/remihp/baseball-data/blob/main/baseball_scraping.ipynb
配球を散布図にする
htmlのピクセル値からストライクゾーン(長方形の枠)を推定して、散布図にした。
おっと、、、サイト側の都合なのか謎の区切りが出現した。
とりあえずボール判定されたボールがストライクゾーンの外に行っている(若干ずれているが気にしない)ので、これであっているだろうとして続けることにする。打者の左右で比べてみる
右打者(10040球)、左打者(5840球)で比べてみる。
両方ともいわゆるアウトローに投げられている。左打者に対してはストライクゾーンの外低めにも多く投げられている。
ピッチャーの比較
ベイスターズの今年の投球数ランキング上位3人(大貫選手、井納選手、濱口選手)の配球を比較してみる。
今年活躍した大貫投手はアウトロー、ストライクゾーン低めにしっかり投げている。ストライクゾーン中段にも多く投げているらしい。
井納投手もアウトロー、ストライクゾーン低めに多く投げられている。
濱口投手はほか2選手に比べストライクゾーン外の配球が多い。確かに四球が多い印象。ここまでのコードは
https://github.com/remihp/baseball-data/blob/main/ball_distribution.ipynb今後
球速や球種などの分析をしたいし、いつかは予測モデルなんかに挑戦してみたい。
- 投稿日:2020-12-16T23:02:09+09:00
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.pyINSTALLED_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.pyurlpatterns = [ 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.pyfrom 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.labelimportしているクラスは以下。
クラス名 機能 LoginRequiredMixin ログインしたユーザだけ閲覧できるようにする LoginView ログイン機能 LogoutView ログアウト機能 LogoutForm 上記で定義したログイン用フォーム view.pyfrom 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.pyurlpatterns =[ path('login/', views.Login.as_view(), name='login'), ]リダイレクトURLの設定
setting.pyに未ログイン時に表示されるURL、ログイン・ログアウト時のリダイレクトURLを設定する。
setting.pyLOGIN_URL = '/login' # ログインしていないときのリダイレクト先 LOGIN_REDIRECT_URL = '/sample_app/index' # ログイン後のリダイレクト先 LOGOUT_REDIRECT_URL = '/login' # ログアウト後のリダイレクト先ログイン認証を追加するアプリケーションの設定
ログイン先のアプリケーションのビューにログイン必須設定を行う。
クラスベースのビューの場合、LoginRequiredMixinをimportして継承することでログイン必須となる。sample_app/view.pyfrom 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でログインできればログイン認証成功です。
- 投稿日:2020-12-16T22:47:02+09:00
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:
Input: root = [3,9,20,null,null,15,7] Output: 2Example 2:
Input: root = [2,null,3,null,4,null,5,null,6] Output: 5考え方
再帰処理を用います
rootのleft,rightをそれぞれ潜っていって、noneになったらreturnします。
左右どちらもnoneでなかったらreturnされた合計の小さい方をreturnします
最終的に最小の深さを戻り値とします
解答コード
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 }
- 投稿日:2020-12-16T22:27:27+09:00
spaCy 業務で使える自然言語処理ライブラリ
こんにちワンコ。
ちゅらデータ株式会社の山内です。この記事は、"ちゅらデータアドベントカレンダー"の16日目です。
私の好きな分野の一つ「自然言語処理」について書きたいと思います。はじめに
自然言語処理は、データ分析の様々な研究分野の中でも大きく注目されているテーマで、日々たくさんの論文が発表されていますが、最近では、実際の業務レベルでの要望、課題解決の手段としても利用されることが多くなってきました。
この記事は、そのような業務で利用される自然言語処理のライブラリのひとつ「spaCy」について書きたいと思います。
spaCyについて
spaCy は Python と Cython で実装されたオープンソースの自然言語処理ライブラリです。
プロダクト環境で利用するためにデザインされていることが特徴的です。
本家サイトには、自然言語処理の「Ruby on Rails」のようなものだと記載されています(^^;)私が spaCy を利用しようと考えたのも、「プロダクト環境で利用できる」という点で目についたからです。
NLPのライブラリには、NLTK や CoreNLP などの有名なものがいくつかありますが、研究で利用されることを目的に実装されています。
これらは、BERTやTransformerをお試しで実行するには便利なライブラリですが、
顧客の用意した自由なフォーマットデータに対して前処理を行う場合や、ビジネスロジックに沿ったデータオブジェクトを実装したりするといった用途では使い勝手が良いとは言えません。spaCyを使うことで、業務コードを便利に実装することができます。
具体的な特徴
自然言語処理のさまざまな概念が基本APIとして用意されている
下記の図は、spaCyのアーキテクチャ図です。
ドキュメントやトークン、形態素などの基本オブジェクトや、
トーカナイザー、Tagger(品詞のタグ付などに利用)、Dependency Parser(構文解析)、固有表現抽出など、設計段階で構想されています。これらは、処理を実行するための具体的な実装だけでなく、
ビジネスロジックを拡張実装するためのベースコードとして利用できるようになっています。
https://spacy.io/api#_title Library architectureより「Span」は、単語分割がややこしい日本語NLPではとても便利な概念です。
https://spacy.io/api/spanパイプライン処理
NLPで必須な前処理に関しても便利な機能がたくさんあります。
実用で必須なパイプライン処理は、使いやすく独自処理の追加も簡単な実装で実現することができます。
ルールベースの利用
深層学習モデルに注目があつまるNLPですが、
業務要件では、ルールベース(単純な単語マッチなど)や正規表現を利用した手法も課題解決のため必要な実装です。
辞書を用いたMatcherや、辞書や言語資源をを効率的に扱うために、 Vocab , StringStore などのデータ格納のための概念も設計に組み込まれています。
https://spacy.io/usage/rule-based-matchingモデルの組み込み
統計モデルやML、深層学習系モデルを組み込むことも想定されており、
言語ごと(英語、ドイツ語など)のモデルや、いくつかのアルゴリズムがすぐに利用できる状態で実装されています。
もちろん、自前のモデルも利用することが可能です(使い方にやや癖があります)
https://spacy.io/models処理の効率化
処理速度やメモリ効率化のため、Cythonでの実装を筆頭に、巨大データを扱うための実装や、データ格納時サイズ削減の機構など(Hashing化)、実運用を見据えた様々な工夫がなされています。
おわりに
以上、本記事はspaCyの紹介のみに終わってしまいますが、
プロダクトレベルで実装で役立つライブラリーとして、spaCyの良さが少しでも伝わっていると幸いです。弊社では、実際のプロダクトコードとしての利用や、この設計を元に独自ライブラリの実装の参考にしたりと便利に利用しています。
データ分析業界は、「AI」というバズワードが先行していた夢物語のフェーズから、
実際に業務課題に対してどのように実装するか、機能としてどう組み込むかといったフェーズに入っています。これからは、spaCyのような実務で使えて役に立つOSSライブラリがたくさん開発されていくことでしょう。
ちゅらデータもそういったライブラリやサービスをどんどん開発して顧客やデータ分析業界の力になれるよう日々技術磨いています。
- 投稿日:2020-12-16T21:51:59+09:00
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にアクセスします。
- 左上のハンバーガーメニューから[リソースの作成]をクリックし、[データベース]を選択ください。
![]()
Azure Database for MySQLをクリックし、[単一サーバー] or [フレキシブルサーバー]の選択となります。今回はサンプルということもあるので[フレキシブルサーバー]にしたいと思います。
基本設定は下記の通りとしてます。
SKUは下記の通りとしてます。※今回はミニマムとしてます。
ネットワーク設定ですが、今回は[パブリックアクセス]とし、 AppServiceからの接続とローカル(開発端末) からのみ許可しました。要件によってはVNET IntegrationとPrivate Endpointを組み合わせてセキュアにしても良いかもしれません。送信元IPはこちらのページを参考に確認ください。
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のみで良さそうです。スタートアップファイルの設定
Webappsの[構成]ブレードの全般設定タブのスタートアップコマンドに
startup.txt
を登録しておきます。
startup.txtの中身はこのような感じです。必要に応じてオプションを付け足してみてください。 オプションの詳細はこちら
startup.txtpython -m uvicorn src.main:app --host 0.0.0.0ソースのデプロイ
VSCodeからアップロードします。赤囲みの矢印ボタンをクリックし
デプロイ中は[output]タブにて進行状況が確認できます。
デプロイが正常終了するとこのような表示になります。
TOPページ
試しにJWTを取得するAPIを叩いても問題なくレスポンスが返ってきます。
補足
- パスワードの作成について
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の記事はまだ少ない印象でしたので、参考になれば幸いです。
サンプルコードがまだまだ成熟し切っていないので、さわりだけ掴んでもらえればと思います。
雛形にできるよう今後も少しずつ改善してきます。参考
- 投稿日:2020-12-16T21:37:35+09:00
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)#! 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)})")ちゃんと変数を復元できていますし、Python3のdictの実装通りに順番も保持されてますね!
(実装側の都合で順番が保持される特性がついたわけだから、当然か……)Python3 で出力した pickle をPython2で読んでみる
さあ、これをPython2に持っていってみましょう!
アッ……今の私用PCにPython2が入ってねえ!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)))「プロトコル ガ チガイマス」 とエラーが出てきます!
やったね!(?)
公式ドキュメントを読んでみる
困ったときは公式ドキュメントを読んでみましょう!
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)
と置き換えると……アッ…そうだ…pathlibはPython2系にはなかった……
仕方ないので、pathlibにしてたパスをstringに置き換えるとpython3の変数も無事、Python2系で読めましたね!
(もうPython2とか使わなくなるのに、このくだり要る?)締め
自分もごくまれにしか使わないですが、まあまあ便利なのでお勧めです。
まあ、簡単な構造の変数だったり、速度を犠牲にしていいのであれば、
個人的には可読性の高いjsonの方が好みですけど。トラブったときに対応しやすいですし。
今年はほぼPythonに向き合ってないので、この方法すら時代遅れなのかもしれない……と恐怖しながら書いてます。 ↩
- 投稿日:2020-12-16T20:58:02+09:00
17行でTwitterのトレンドを取得する
最近は寒くなってきたので暖房をつけ始めました。
目標
アドベントカレンダー17日目ということで、何かと話のきっかけになるTwitterのトレンドも17行で取得したいと思います。
環境
Python 3.9.1
準備
①tweepyのインストール
pip install tweepy②Twitter APIの認証コードの取得
下のサイトが分かりやすいのでお勧めです。
Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報実装
日本のTwitterのトレンドを50位まで表示します。
コード全体
trend.pyimport 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 丸ちゃん参考
- 投稿日:2020-12-16T20:50:09+09:00
Blenderと立体漫画のメモ
この記事は Blender Advent Calendar 2020 17日目の記事です。
立体漫画について
「立体漫画」と言われる漫画は色々あるわけですが、最初の立体漫画(3-D Comic Book)は1953年にアメリカのKubertとMaurerによって作られたものだと言われています。
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は初心者なんで、アドオン書くのも今回初めてですし、作りながら「これじゃダメだ」と気づいたところも多々あるのですが、どうにも時間が足りません。いろいろ残念な感じです。
サンプルも本当は自分で描けばいいのですが、時間もないのでセルシスのサイトにあるものを使わせて頂きました。
まずClipStudio上で、レイヤーを整理して高さ別にいくつかのグループに分けています。
マスクは3Dに変換できないので、コマ枠の外側は白に塗りつぶして誤魔化しました。ホールドアウトシェーダーとか使えばできるんだろうか。ClipStudioからレイヤーをPSDに書き出して、アドオンでBlenderにimportして、各レイヤーを立体空間に並べて、立体視をオンにしてレンダリング……というのが今回の流れです。
アドオンの基本は、丸写しですね。
こことかここを参考にしました。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 namepsd_toolsを使っているので、別にインストールが必要です。
macOSの場合、外部ライブラリのインストールは、こんな感じでした。ここはfSPYみたいに外部ツールに分離して、アドオンにはテクスチャのパスとか座標だけ渡す形にしたらいいのではないか、と思います。
レイヤーをカメラの前に配置する
ここが一番Blenderらしい処理だと思うのですが……残念ながら詳細は省略です。
きちんと仕上げたいのですが、時間がありません。年末なのです。
アドオンのコードはGitHubにあげましたので、興味がある人は見てください。
ほぼほぼ、Blender付属の Image as Planes プラグイン(io_import_as_plane.py)からの丸写しです。アドオンをインストールすると、[ファイル]->[インポート]に[PSD to 3D Comic]が追加されますので、適当なPSDを読み込んでください。
なぜか全体に暗くレンダリングされることがあるのですが、原因不明です。
新規に2D Animationを作成してimportすると正しい色でレンダリングされるので、設定が足りない感じですね。おまけ
本気で立体漫画の作成を考える人がいるかもしれないので、もうちょっとだけ。
2Dの原稿から立体漫画を作る方法としてもう一つ押さえておきたいのは、ディスプレースメントマップを使う方法です。
https://www.youtube.com/watch?v=wSeflVaWS28
ポリゴン数を増やす代わりに法線マップを使うのと似ていると言えば、似ているかもしれません。
これもBlenderで実装できるはずですが、今後の研究課題としておきましょう。もっと書きたいこともあるのですが、また来年ということで。
- 投稿日:2020-12-16T20:31:03+09:00
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
- 投稿日:2020-12-16T20:29:11+09:00
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()
- 投稿日:2020-12-16T20:29:11+09:00
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()
- 投稿日:2020-12-16T20:20:27+09:00
GPT-2に科研費報告書のデータを学習させて架空研究報告書を生成する
はじめに
研究者にとって、研究費の申請書や報告書を書くのがしんどいのはよくあることです。
しんどさの軽減を試みるべく、GPT-2に報告書っぽいテキストを生成させてみました。
やっていることはこちらの二番煎じで、transformersパッケージのGPT-2に過去の科研費報告書のデータ(日本語)を学習させて自動生成をします。環境
- Google Colaboratory GPUランタイム
データの準備
深い意味はありませんが、この項目はローカルのjupyter notebookで実行しました。
科研費データベースからcsvのダウンロード
科研費データベースから、「コロナ」というキーワードで期間指定なしの2767件をcsvでダウンロードしました。
コロナウイルスだけでなく、太陽コロナなどに関する研究も含まれます。
ちなみに研究期間を2020年のみに指定すると、「コロナ」というキーワードでヒットする研究は55件でした。研究報告のテキストデータをまとめる
科研費データベースからダウンロードしたcsvの扱いは、こちらの記事を参照してください。研究概要に相当するテキストのリストを作ります。
前例では学習させるデータに対して形態素解析をしていないので、今回も形態素解析は省きます。pythonimport pandas as pd df = pd.read_csv("corona.csv") column_list = ['研究開始時の研究の概要', '研究概要', '研究成果の概要', '研究実績の概要'] abstracts = [] for column in column_list: abstracts.extend(df[column].dropna().tolist())train.txt, eval.txtに出力
リスト
abstracts
から、学習に必要なtrain.txt
とeval.txt
を作ります。
1行ずつに分けて行数で分割してもいいですが、今回は要旨単位で分けることにしました。
eval
は予約語なので、eval.txt
に出力したいデータの変数名はtest
にしてあります。pythonfrom 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)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. 適用 研究 の 表面 地域 マウス を 加え た 適用 条件 を 進め て き た 。 また 、 適用 条件 の 重要性 を 明らか に し た 。 本 研究 で は 、 正規模 の 情報 を 用い た 統計的 統計 的 相互作用 の 対象 を 図っ て 、 地下 観測 および 統計的 ・ 現在 、 地下 大気 による 接着 法 を 開発 し 、 地下 大気 面 と 編集 し た 場合 の 検討 の ため の 研究 を 行っ た 。 本 研究 の 目的 は 、 新た に 研究「ディスク トリポン」「ステルコロナリング」「転写真」「報告細胞」などの謎単語は出現しているものの、分かち書きしない場合に比べて頻度は下がったように見受けられます。一方で、定量化は難しいものの、なんとなく文章全体の不自然さが増したような印象もあります。
実用に耐える文章の自動生成まで、まだまだ道のりは遠そうです。参考
- 投稿日:2020-12-16T19:56:48+09:00
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 NoIngressSecurityGroupEFS+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 --versionGiNZAインストール
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に指定- 「作成」を押す
- 元の画面に戻る。テストが作成されているのでその状態で「テスト」ボタンを押す。
成功!(2回目以降の実行なので622msになってます。1回目は4秒以上かかりました)
終わりに
いくつかの検討を経て、ようやくサーバーレスで自然言語処理が出来そうです(EFSはストレージなので許容します)。
LambdaコンテナもEFSとのマウントも今年の機能っぽいです。去年検討していたら諦めていた事になります。AWSの機能追加速度には目を見張るものがあります。すなわち日々キャッチアップが必要という事になる訳で。大変ですw参考にさせて頂いた良記事
- 投稿日:2020-12-16T19:29:28+09:00
【自分用備忘録】初めてPyinstallerを使って躓いたから解決策を書く
はじめに
最近Python が気になっていてexe化を実行しようと思ったのですが、めちゃくちゃ躓いて時間ばかり使ったので、初めて使う人が引っかかりやすい(自分が引っかかった)所を少しでもお助けできたらなと思います。
国語が苦手で、文章が分かりにくいかもしれないです。。。(自分用なのでご容赦ください(´・_・`))pyinstallerのインストール
インストールは超カンタンで、コマンドプロンプトで以下のコマンドを打つだけで出来ました。
pip install pyinstallerサンプルコード
僕はPython初心者なので、コードが雑なところがあると思いますが、今回は以下のコードをexe化したいと思います。(適宜numpyをインストールしてください。)コードは出来るだけ行数を減らすようにしているので読みにくいと思います。
qiita_tutorial.pyimport 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.pydef add(a, b): return a + b※補足※
2つファイルがあったり、GUI作ったり、変なimportをしてるのは説明のときに使うためです。
iconsフォルダにはicon.ico
というアイコンファイルが入っています。
iconフォルダ内のicon.ico
はqiita_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.spechiddenimports=["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.pyroot.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 retsubprocesssubprocess.run(cmd, **subprocess_args(True)) # cmd = コマンドプロンプトで実行するコマンド(string)
- 投稿日:2020-12-16T19:25:19+09:00
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
- 投稿日:2020-12-16T19:23:19+09:00
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
公式ドキュメント: pydanticExample
pydantic
はpydantic.BaseModel
という基底クラスを継承したユーザー定義クラスにおいてその機能を発揮します。まずは
pydantic
を使用しないクラス定義を考えてみます。
dataclasses.dataclass
を使います。from dataclasses import dataclass, field from typing import Optional @dataclass class NonPydanticUser: name: str age: intこの
NonPydanticUser
クラスのインスタンスを1つ作成してみます。
この例では、2つのフィールドname
はstr
型、age
はint
型です。
クラス定義の通りのデータ型を保持していますね。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'>この例では、
name
はstr
型ですが、age
はstr
型になってしまいます。
TypeError
などの例外も送出されません。あくまで型アノテーションによって与えられる型情報がコーディング時にのみ機能しているということが改めて分かりますね。
確かにmypy
やPylance
などを使えばこういった型の不整合はコーディング時に検出できますが、コード実行時に型の不整合や不正値で例外送出をしたい場合は自前で入力値チェックをする必要があります。一方
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.age
がint
型にキャストされています。ちなみに、
age
にhoge
、fuga
などの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
という基底クラスを継承してユーザー独自のクラスを定義します。
このクラス定義の中ではid
、name
、signup_ts
、friends
という4つのフィールドが定義されています。
それぞれのフィールドはそれぞれ異なる記述がされています。ドキュメントによると以下の様な意味があります。
id
(int
) ... Type Hintsのみ宣言した場合、必須フィールドとなる。もしインスタンス生成時にstr
、bytes
、float
型の値が与えられた場合は強制的にint
に変換する。それ以外のデータ型(dict
,list
など)の値が与えられると例外を送出する。name
(str
) ...John Doe
というデフォルト値からname
はstr
型と推論される。またデフォルト値が宣言されているので、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
int
やstr
、list
やdict
などのプリミティブなデータ型はもちろん使用できます。
その他にtyping
やipaddress
、enum
、decimal
、pathlib
、uuid
などの組込みライブラリにも対応しています。以下は
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.com
、ftp://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を実装する
例えば、ある予定の開始時刻と終了時刻をそれぞれ
begin
、end
として保持する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
フィールドに代入される時刻よりも後であることを保証したいです。
begin
とend
の時刻が一致している場合も不正値であることにします。やり方はいくつかあると思います。私からは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_end
はvalidate_begin
とは異なり第3引数としてvalues
という引数が指定されています。
pydantic.validator
の仕様として、あるvalidator
の前に実行されたvalidator
で入力値チェックされたフィールドに第3引数values
を使用してアクセスすることができます。
このvalues
は_values
でもValues
でもダメです。一種の予約語だと思ってください。つまりこのコードの場合、各フィールドの入力値チェックの順序は以下の様になります。
- 先に
validate_begin
によるbegin
の入力値チェックが実行される- その後
validate_end
によってend
の入力値チェックが実行される。この時validate_end
のスコープ内からbegin
フィールドにvalues["begin"]
で参照することができる。以上2通りの方法を紹介しました。もっといい方法があれば教えてください。
List
、Dict
、Set
などに含まれるそれぞれの要素に対するvalidator以下の仕様を満たす
RepeatedExams
クラスを考えます。
- ちょうど10回の試験の点数(
int
型)を格納するList[int]
型フィールドscores
を持つ。- それぞれの試験結果は50点以上でなければならない。
- 10回の試験結果の合計点は800点以上でなければならない。
コードにすると以下の様になります。
List
、Dict
、Set
などの型のフィールドの要素のそれぞれに対して、あるvalidator
による入力値チェックを行いたい場合はそのvalidator
にeach_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 Config、Schemaをはじめとする他の要素は執筆時間があまり確保できず、断念しました。
今後追記できたらいいな...
- 投稿日:2020-12-16T18:52:06+09:00
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辞書の分かりやすいデメリットとしては、こんなところでしょうか。
- ドットアクセスができない。(ただし、できなくても別に良いかもしれない)
- 格納時の処理といったメソッドは入れられない。
- 型アノテーションができない。
- 決まった形になっていることがコードから掴みにくい。
後から読みやすい、メンテナンスしやすいコードを目指す上では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
- 投稿日:2020-12-16T18:41:03+09:00
検証無視は蜜の味?多くのプログラマーが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.pyres = requests.get('https://darkside.example.com/', verify='/path/to/certfile')
- Java
keytool
などを利用して、信頼する証明書発行機関(認証局)の証明書をトラストストアに追加できます。環境変数の、
javax.net.ssl.trustStore
やjavax.net.ssl.trustStorePassword
で、
トラストストアのパスや、パスワードを指定できます。
- C#
Windowsの場合は、OS側に信頼する証明書発行機関(認証局)として証明書を登録するなど。
証明書の期限切れでエラーになって困っている場合
- 自分または関係者が運用しているサーバーの場合
一刻も早く、証明書を更新しましょう。
本来は期限が切れる前に対応しておくべきだったと反省しましょう。
- 自分ではどうすることもできない運営者が運営している場合
たぶん、やる気がないです。
その接続先と、さよならすることを考える時が来たのかもしれません。
- 投稿日:2020-12-16T18:39:03+09:00
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をクリックします。
基本2: 右側の「Curl Java Node Python Go」などと書いてあるタブから、自分の使いたい言語をクリック
今回はPythonですので、「Python」をクリックしてください。
ちなみに2020/12/15に確認したところ、Curl, Java, Node, Python, Go, .NET, Ruby, Swift, Unityがタブにありました。
基本3: 左側のメニューから使い方を知りたいSubjectをクリック。
例えば「Introduction」だったらpipでのインストール方法が載っています。コピペして実行すればよいですね。
例えばMethod「Translate」だったら、選んだ言語の定義とサンプルコードが右側に表示されます。サンプルコードはコピペして、実行可能です。
ただし、{apikey}
,{url}
は自分のサービスのものに置き換える必要があるので注意しましょう。1. 前準備で準備したものに置き換えてください。
3. ではPythonから使おう!
「2. Watson SDKの使い方の基本にのっているドキュメントって英語なんですけど、、、」と引いてしまった方のために、一通り使い方を説明します。
ただ細かいSDKの使用はやっぱりSDKリファレンスをみないとわからないので、感覚を掴んだら、ぜひSDKリファレンスで詳細を調べてみてください。Codeは共通語ですよね❤️ここではLanguage Translatorを例に説明します
3.1 まずはライブラリのインストール
以下のpipコマンドでインストールできます。
pip install --upgrade ibm-watson3.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が記載されていますので、そちら値を使用するのがよいかと思います。
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ドキュメントの真ん中の列に書かれています。
また戻り値全体の話はAPIドキュメントの
Data Handling
→Response details
に載っています。get_result()以外に、Header情報やHTTP Status Codeを取得することが可能です。
結果を表示してみましょう.インデントをつけて見やすく表示させています:
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でも試してみてください!
- 投稿日:2020-12-16T18:15:36+09:00
IWATSU製オシロスコープから得たcsvデータを自動でグラフ化したい!
自動化したい!
私は研究を行う上で、頻繁にIWATSU製のオシロスコープのお世話になっております。
オシロスコープから電圧やらの波形を得て、それをcsvデータとしてUSBに出力することが度々あるのですが、オシロから直接出力されるcsvデータを再びグラフ化するには少し面倒な手段を踏む必要があります。
と言いますのも、IWATSUのオシロから得られるcsvデータは以下のような形式にまとめられています。1~19行目までに計測条件、20行目にチャンネル、21行目以降から電圧値が表示されます。
もちろんこの計測条件の表示は重要な要素なのですが、やはりグラフ化や信号処理の足かせになります。また、電圧値の表示はありますが時間軸のデータはありません。
csvデータが2,3個しかないなら手動でグラフ化しても良いのですが、私の研究ではデータが100個ほど生じます。全部手動でやったら気が狂っちゃいます。そこでPythonでコーディングを行い、以下のようなグラフを自動生成します。
動作環境
OS : Windows10
開発環境 : Anaconda Spyder 4.1.5
オシロスコープ : IWATSU DIGITAL OSCILLOSCOPE DS-5412自動化プログラム
oscilloGraph.pyimport 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ファイルが格納されます。
処理後のディレクトリ
出力されるmodified.csvデータ
コードの解説
プログラムを実行すると、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の出力形式をご教授いただければ有難いです。
本プログラムは自分の卒業研究に用いるために作ったものです。非常に限定された用途ではありますが、お役に立てれば幸いです。
- 投稿日:2020-12-16T18:03:36+09:00
Power BI+Pythonで主成分分析
はじめに
PythonやR等のプログラミング言語では、統計解析のライブラリが豊富で、無料で利用できるメリットがありますが、ソースコードの修正やコマンドラインからの操作は煩雑です。
ここでは、主成分分析を例に、Power BIのクエリーからデータを読み込み、Pythonによる統計解析、PowerBIダッシュボードによる可視化を試みてみます。サンプルデータ
ポケモンで多変量分析・主成分分析を始めよう! RとTableauの連携の記事を参考に、Kaggleで公開されているThe Complete Pokemon Datasetのpokemon.csvを使います。
Power BIクエリーの編集
1.ダウンロードしたPockemon.csvを読み込みます。
2.分析に不要な列を削除し、Name列、データ列1、データ列2...とします。
3.Pythonスクリプトを追加します。
このスクリプトでは、1列目をName、2列目以降をデータとして、skikit-learnのライブラリを使って主成分分析を行います。主成分分析のPythonコードは、意味がわかる主成分分析を参考にしています。# '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+14.evrには各成分ごとの寄与率がセットされており、値を確認します。(各成分の影響力をを示しており、第1主成分が0.46、第2主成分が0.19)
5.datasetに主成分分析結果、主成分1(PC1)と主成分2(PC2)が追加されており、これを読み込みます。
6.散布図に、X軸 PC1,Y軸 PC2をプロットします。(カテゴリOnでデータラベルを表示)
- 投稿日:2020-12-16T17:29:49+09:00
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)出力結果
実行結果
pd.DataFrame
を利用して結果をテーブル表示させます。pd.DataFrame(records['records'])おわりに
制約はありますが、カスタムオブジェクトにおいて SOQL でも禁じ手の
SELECT * FROM SOME-TABLE
っぽいことができるという検証結果でした。実行結果の通り、一度に多くの項目を取得するため、実際にアプリケーションに組み込んで使う際には、必要なカラムに絞って実行しましょう。
明日は @zomaphone さんの投稿です。お楽しみに!
- 投稿日:2020-12-16T16:52:47+09:00
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.pyclass 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 特有のライブラリを使う感じなんでしょうか?
汎用性が高くないと、実用性が低いかもしれません。
とりあえず、ほかの人のコード読みに行ってきます(。・ω・)ノ゙ イッテキマ-ス
- 投稿日:2020-12-16T16:05:52+09:00
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-stringTODO
subprocess.run
で encoding 指定してもよいかもしれません.Python の subprocess
https://qiita.com/tanabe13f/items/8d5e4e5350d217dec8f5
- 投稿日:2020-12-16T15:59:44+09:00
Class '~~~' has no 'objects' memberpylint(no-member)のエラー/警告について(@Python/Django)
はじめに
今回は、Djangoでアプリ作成する際に発生したエラー・警告について触れていきます。
参考記事はこちら
エラー詳細
Djangoのクラスであるmodelsのオブジェクトを参照する際にエラー検知されてしまいました。。
実行した処理
①pylint-djangoをインストール
ターミナルを開き以下のコマンドを実行します。$ pip install pylint-django②VSCodeの設定変更
VSCodeを利用しているため設定に変更を加えていきます。
画面左下の歯車マークから設定を開き(command+,)、
Python › Linting: Pylint Argsの箇所に以下の項目を追加します。
--load-plugins=pylint_django
以上で完了しました。
まとめ
エラーや警告の内容を深掘ると興味深いですが、
気付いたら1日経ってたりします。。
頑張っていきます!!?参考にさせていただいた記事の作成者様ありがとうございました。
- 投稿日:2020-12-16T15:51:47+09:00
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:5000Herokuへデプロイ
まず,Heroku CLIをインストールする.
その後,ログインしてHerokuへpushする.
heroku login git add. git commit -am "[update]" git push heroku main
最後に
Herokuにデプロイする時に,OpenCVやPytorchが結構厄介だった.