- 投稿日:2021-12-30T23:32:45+09:00
multiprocessingで一つのtqdmを複数プロセスから更新する
複数プロセス(worker)を並列実行して,一つのプログレスバーを更新したい. overview Pool + imap_unordered()という手もあるが, 引数にリストなどのiterableを渡さなければならない 一つのiterableしかimap_unordered()の引数に渡せない. workerの引数も一つしか渡せない.(単純にやるからだけど) そこでProcess + queueを使ってみる. リストの代わりにqueueに詰める. tqdmをManagerで管理する. workerにqueueとtqdmも渡す. workerの中でtqdmをupdate motivation pool.imap_unordered()を使っていたけれども,各workerに渡すのが別々のものと共通のものとが混在していて,iterableを作る効率が悪い. そこで別々のものはqueueに詰めてしまって,各workerはそこから取り出すことにする.共通のものは単にworkerの引数にわたすだけ. code ではまず必要なモジュールのインポート. モジュールのインポート from multiprocessing import Process, Manager from multiprocessing.managers import BaseManager from tqdm import tqdm import queue 次にworkerの定義. 引数はキューq,ロックオブジェクト,tqdmのプログレスバー,worker番号 worker番号iは単なる例.なんでもよい. キューから取り出す.キューが空なら即終了.キューが空になるまでループ. 取り出したデータを処理 lockしてtqdmのプログレスバーを更新 lockしないと更新表示がずれる def worker(q, lock, pbar, i): while True: try: item = q.get(timeout=0) except queue.Empty: return # ここでitemを処理 with lock: pbar.update(1) pbar.set_postfix_str('{0:3d} {1:3d}'.format(item, i)) tqdmをmanagerで管理するための定義. class TqdmManager(BaseManager): # https://docs.python.org/ja/3/library/multiprocessing.html#customized-managers pass TqdmManager.register('Tqdm', tqdm) ではメイン. キューq,ロック,tqdmプログレスバーpbarのオブジェクトをmanagerで作成. qにデータを詰める Processで個数num_workerのプロセスを生成. 引数はargsで指定.ここでiterable以外にいろいろ渡せる. workerをスタート. joinで終了まで待つ. closeでクリーンアップ. 最後にpbarをclose メイン if __name__ == '__main__': with TqdmManager() as tqdm_manager, Manager() as manager: num_workers = 13 len_data = 1182 q = manager.Queue() lock = manager.Lock() pbar = tqdm_manager.Tqdm(total=len_data) for item in range(len_data): q.put(item) p_all = [Process(target=worker, args=(q, lock, pbar, i)) for i in range(num_workers)] [p.start() for p in p_all] [p.join() for p in p_all] [p.close() for p in p_all] pbar.close() execute $ python3 ./queue_test.py 100%|█████████████████████████████████| 1182/1182 [00:01<00:00, 601.64it/s, 1181 1] $ limitation managerを使うと,そのための別プロセスが起動するのでやや遅くなる. misc tqdmをBaseManagerで派生して管理するという例は見かけない.なんでもmanagerにできるので便利. [p.start() for p in p_all]というインライン表記もあまり見かけない.なぜ? Yet another version queueにデータを入れるのが時間かかる場合,先にworkerをスタートしてからqueueにデータを投げ入れる. queueに入れればすぐにworkerに受け渡される workerではq.getのtimeoutを1程度にしておく(データ待ちの時間が発生するため) queueに入れるデータが尽きたら,queueに終了目印のNoneを入れておく. workerの方はNoneが来たら即終了 queue_test.py from multiprocessing import Process, Manager from tqdm import tqdm import queue from multiprocessing.managers import BaseManager def worker(q, lock, pbar, i): while True: try: item = q.get(timeout=1) except queue.Empty: return if item is None: return # ここでitemを処理 with lock: pbar.update(1) pbar.set_postfix_str('{0:3d} {1:3d}'.format(item, i)) class TqdmManager(BaseManager): # https://docs.python.org/ja/3/library/multiprocessing.html#customized-managers pass TqdmManager.register('Tqdm', tqdm) if __name__ == '__main__': with TqdmManager() as tqdm_manager, Manager() as manager: num_workers = 13 len_data = 1182 q = manager.Queue() lock = manager.Lock() pbar = tqdm_manager.Tqdm(total=len_data) p_all = [Process(target=worker, args=(q, lock, pbar, i)) for i in range(num_workers)] [p.start() for p in p_all] for item in range(len_data): q.put(item) for _ in range(num_workers): q.put(None) [p.join() for p in p_all] [p.close() for p in p_all] pbar.close()
- 投稿日:2021-12-30T23:24:41+09:00
【matplolib】2次元ヒートマップを3次元で表示する方法
概要 matplolibで作成した2次元ヒートマップをmatplolibで3次元で表示する方法を紹介します。 また、番外編ですがplotlyを使い、インタラクティブな3次元のグラフを作成する方法も紹介します。 Google Colabで作成したコードは、こちらにあります。 手順 各種インポートします。(各モジュールの実行時のversionは、Google Colab内で作成したコードにあります。) 2次元ヒートマップに使う2次元配列を用意します。 2次元配列をimshowを使い、2次元ヒートマップで表示します。 matplolibのAxes3Dを使い、3次元で表示します。 (番外編)plotlyを使い、matplolibで作ったグラフをインタラクティブな3次元のグラフで表示します。 1. 各種インポート 各種インポート import numpy as np from matplotlib.image import imread import matplotlib.pyplot as plt import matplotlib.colors from mpl_toolkits.mplot3d import Axes3D import plotly.graph_objects as go 各モジュールの実行時のversionは、Google Colab内で作成したコードこちらにあります。 2. 2次元ヒートマップに使う2次元配列を用意 適当なサンプルの2次元配列を用意します。 サンプルとして使う、2次元ヒートマップ用の2次元配列 def func(x, y): z = 0.01*(x*np.sin(0.1*x) + y*np.cos(0.1*y)) return z y_shape, x_shape = (50, 100) image = np.empty((y_shape, x_shape)) for y in range(y_shape): for x in range(x_shape): image[y][x] = func(x, y) 3. matplolibのimshowを使い、2次元ヒートマップで表示 2次元ヒートマップで表示 fig = plt.figure(figsize=(8, 4)) ax = fig.add_subplot(111) color = ax.imshow(image, cmap='CMRmap', origin='lower') ax.set_xlabel('x') ax.set_ylabel('y') plt.colorbar(color, shrink=0.8, label='z') plt.show() 出力結果 4. matplolibのAxes3Dで3次元で表示 x軸とy軸の縮尺を統一するために、共通の軸の範囲ax.set_xlim(0, np.max(image.shape))、 ax.set_ylim(0, np.max(image.shape))を使います。 2次元ヒートマップを3次元で表示 fig = plt.figure(figsize=(13, 10)) ax = fig.add_subplot(111, projection='3d') # 格子点を生成 y, x = np.mgrid[:image.shape[0], :image.shape[1]] color = ax.plot_surface(x, y, image, cmap='CMRmap', edgecolor='k', rstride=5, cstride=5) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('z') ax.set_xlim(0, np.max(image.shape)) ax.set_ylim(0, np.max(image.shape)) cbar = plt.colorbar(color, shrink=0.6, label='z') plt.show() 出力結果 角度を変えたい場合 ax.view_init()を使うと、任意の角度で表示できます。 fig = plt.figure(figsize=(13, 10)) # 3Dプロット ax = fig.add_subplot(111, projection='3d') y, x = np.mgrid[:image.shape[0], :image.shape[1]] color = ax.plot_surface(x, y, image, cmap='CMRmap', edgecolor='k', rstride=5, cstride=5) ax.view_init(80, 270) # 表示角度の追加, ax.view_init(縦方向の角度, 横方向の角度) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('z') ax.set_xlim(0, np.max(image.shape)) ax.set_ylim(0, np.max(image.shape)) cbar = plt.colorbar(color, shrink=0.6, label='z') plt.show() 出力結果 5. (番外編)plotlyを使い、matplolibで作ったグラフをインタラクティブな3次元のグラフで表示 Plotlyを使い、インタラクティブな3次元で表示 fig = go.Figure(go.Surface( x=np.arange(image.shape[1]), y=np.arange(image.shape[0]), z=image, opacity=1.0, # 透明度(透明0-1不透明) colorbar=dict(title='z') # colorbarのタイトル )) fig.update_layout( scene=dict( xaxis=dict(nticks=10, range=[0,np.max(image.shape)],), # x軸の範囲 yaxis=dict(nticks=10, range=[0,np.max(image.shape)],), # y軸の範囲 ), width=700, # インライン表示の横幅のエリア height=700, # インライン表示の縦幅のエリア margin=dict(l=50, r=50, b=50, t=50) ) # 等高線の表示 '''パラメータ show : グラフと面に常に等高線を表示するか usecolormap : 等高線にカラーマップの色を表示するか highlightcolor : colorの色 project_z : 面に等高線を投影するか ''' fig.update_traces(contours_z=dict(show=True, usecolormap=True, highlightcolor='limegreen', project_z=True )) fig.show() 出力結果 Google Colab内でもインタラクティブに動きます。 まとめ matplolibで作成した2次元ヒートマップを3次元で表示する方法を紹介しましたが、2次元ヒートマップの良さもあるので、状況によって使い分けましょう。 3次元は角度によって見えない領域があるので、番外編で紹介したPlotlyが3次元を表示するのに便利です。 参考資料 matplotlib mplot3d tutorial Plotly 3D Surface Plots in Python
- 投稿日:2021-12-30T23:24:41+09:00
【matplotlib】2次元ヒートマップを3次元で表示する方法
概要 matplolibで作成した2次元ヒートマップをmatplolibで3次元で表示する方法を紹介します。 また、番外編ですがplotlyを使い、インタラクティブな3次元のグラフを作成する方法も紹介します。 Google Colabで作成したコードは、こちらにあります。 手順 各種インポートします。(各モジュールの実行時のversionは、Google Colab内で作成したコードにあります。) 2次元ヒートマップに使う2次元配列を用意します。 2次元配列をimshowを使い、2次元ヒートマップで表示します。 matplolibのAxes3Dを使い、3次元で表示します。 (番外編)plotlyを使い、matplolibで作ったグラフをインタラクティブな3次元のグラフで表示します。 1. 各種インポート 各種インポート import numpy as np from matplotlib.image import imread import matplotlib.pyplot as plt import matplotlib.colors from mpl_toolkits.mplot3d import Axes3D import plotly.graph_objects as go 各モジュールの実行時のversionは、Google Colab内で作成したコードこちらにあります。 2. 2次元ヒートマップに使う2次元配列を用意 適当なサンプルの2次元配列を用意します。 サンプルとして使う、2次元ヒートマップ用の2次元配列 def func(x, y): z = 0.01*(x*np.sin(0.1*x) + y*np.cos(0.1*y)) return z y_shape, x_shape = (50, 100) image = np.empty((y_shape, x_shape)) for y in range(y_shape): for x in range(x_shape): image[y][x] = func(x, y) 3. matplolibのimshowを使い、2次元ヒートマップで表示 2次元ヒートマップで表示 fig = plt.figure(figsize=(8, 4)) ax = fig.add_subplot(111) color = ax.imshow(image, cmap='CMRmap', origin='lower') ax.set_xlabel('x') ax.set_ylabel('y') plt.colorbar(color, shrink=0.8, label='z') plt.show() 出力結果 4. matplolibのAxes3Dで3次元で表示 x軸とy軸の縮尺を統一するために、共通の軸の範囲ax.set_xlim(0, np.max(image.shape))、 ax.set_ylim(0, np.max(image.shape))を使います。 2次元ヒートマップを3次元で表示 fig = plt.figure(figsize=(13, 10)) ax = fig.add_subplot(111, projection='3d') # 格子点を生成 y, x = np.mgrid[:image.shape[0], :image.shape[1]] color = ax.plot_surface(x, y, image, cmap='CMRmap', edgecolor='k', rstride=5, cstride=5) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('z') ax.set_xlim(0, np.max(image.shape)) ax.set_ylim(0, np.max(image.shape)) cbar = plt.colorbar(color, shrink=0.6, label='z') plt.show() 出力結果 角度を変えたい場合 ax.view_init()を使うと、任意の角度で表示できます。 fig = plt.figure(figsize=(13, 10)) # 3Dプロット ax = fig.add_subplot(111, projection='3d') y, x = np.mgrid[:image.shape[0], :image.shape[1]] color = ax.plot_surface(x, y, image, cmap='CMRmap', edgecolor='k', rstride=5, cstride=5) ax.view_init(80, 270) # 表示角度の追加, ax.view_init(縦方向の角度, 横方向の角度) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('z') ax.set_xlim(0, np.max(image.shape)) ax.set_ylim(0, np.max(image.shape)) cbar = plt.colorbar(color, shrink=0.6, label='z') plt.show() 出力結果 5. (番外編)plotlyを使い、matplolibで作ったグラフをインタラクティブな3次元のグラフで表示 Plotlyを使い、インタラクティブな3次元で表示 fig = go.Figure(go.Surface( x=np.arange(image.shape[1]), y=np.arange(image.shape[0]), z=image, opacity=1.0, # 透明度(透明0-1不透明) colorbar=dict(title='z') # colorbarのタイトル )) fig.update_layout( scene=dict( xaxis=dict(nticks=10, range=[0,np.max(image.shape)],), # x軸の範囲 yaxis=dict(nticks=10, range=[0,np.max(image.shape)],), # y軸の範囲 ), width=700, # インライン表示の横幅のエリア height=700, # インライン表示の縦幅のエリア margin=dict(l=50, r=50, b=50, t=50) ) # 等高線の表示 '''パラメータ show : グラフと面に常に等高線を表示するか usecolormap : 等高線にカラーマップの色を表示するか highlightcolor : colorの色 project_z : 面に等高線を投影するか ''' fig.update_traces(contours_z=dict(show=True, usecolormap=True, highlightcolor='limegreen', project_z=True )) fig.show() 出力結果 Google Colab内でもインタラクティブに動きます。 まとめ matplolibで作成した2次元ヒートマップを3次元で表示する方法を紹介しましたが、2次元ヒートマップの良さもあるので、状況によって使い分けましょう。 3次元は角度によって見えない領域があるので、番外編で紹介したPlotlyが3次元を表示するのに便利です。 参考資料 matplotlib mplot3d tutorial Plotly 3D Surface Plots in Python
- 投稿日:2021-12-30T23:17:26+09:00
asyncioを非同期処理知識0から勉強してみた
こちらの記事は非同期処理の知識が0のわたしが https://qiita.com/everylittle/items/57da997d9e0507050085 こちらの記事やその他の情報をまとめたものになります。 私自身細かいコードを書くわけではないので、この記事ではざっくりとした理解を目的とし、他の記事をスラスラ読めるようになることができればいいというポジションです。 asyincio とは 並列処理には「マルチスレッド」、「マルチプロセス」、「ノンブロッキング」の3種類ありますが、asyncioは「ノンブロッキング」に相当します。 詳細な説明は https://qiita.com/icoxfog417/items/07cbf5110ca82629aca0 こちらに委ねます。 「ノンブロッキング」とは、1スレッドで複数のリクエストを処理するもので、スレッドが増えてメモリ不足にならないというメリットがあります。 asyncioのサンプルコード まずは次の例を見てみましょう。 async, awaitなどが見慣れないですね。 import asyncio async def func1(): print('func1() started') await asyncio.sleep(1) print('func1() finished') async def func2(): print('func2() started') await asyncio.sleep(1) print('func2() finished') async def main(): task1 = asyncio.create_task(func1()) task2 = asyncio.create_task(func2()) await task1 await task2 asyncio.run(main()) # こちらpython3.7以降の書き方らしいです func1() started func2() started func1() finished func2() finished func1の処理の最中にfunc2の処理が割り込まれていますね。 なぜこの挙動になるのか少しずつ紐解いていきましょう。 知らなければならない前提知識が多数あるので、遠回りですが頑張りましょう。 前提知識1:コルーチン(co-routine) まずはコルーチンというものを知ってもらいます。 サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できます。 接頭辞 co は協調を意味します。「他の処理と協調します」ということでしょうね。 pythonにおける最小のコルーチンは次のようになります。 async def f(n): return n asyncを関数定義の先頭に置くことで、「これがコルーチンだ!」というのをpyhonに教えることになります。 しっかりと、asyncを書いていない関数内で、非同期処理をしようとするとエラーになります。 例えば次のコードはエラーになります。 import asyncio def f(): await asyncio.sleep(1) asyncio.run(f()) エラー文として'await' outside async functionと言われてしまいます。 asyncとawaitはセットだということを覚えておきましょう。 また、コルーチン関数は普通の関数と挙動が異なります。 import asyncio async def f(): return 100 print(f) <function f at 0x0000025479C403A0> 普通の関数であれば、100が返ってきますが、この例ではコルーチンオブジェクトという謎のオブジェクトが返ってきます。 このコルーチンオブジェクトを実行するには、asyncioのイベントループに放り込む必要があります。 前提知識2:イベントループ 調べるとイベントループはかなり複雑なことをしているようで、理解しきれなかったのですが、どうやら初学者レベルでは「複数のコルーチンをいい感じに管理・処理してくれる、複雑な処理のループ」くらいの理解で十分みたいです。 ただ、イベントループの機能をすべて理解しなくとも、1つくらいは知っておきましょう。 import asyncio async def f(): print("fが実行されたよ") return 100 coro = f() # コルーチンオブジェクトを作成 loop = asyncio.get_event_loop() # イベントループを作成 print(loop) task = loop.create_task(coro) # イベントループにコルーチンを登録 print(task) <ProactorEventLoop running=False closed=False debug=False> <Task pending name='Task-1' coro=<f() running at c:/Users/username/Desktop/program.py:3>> このように、コルーチンオブジェクトが登録され、それらを管理するというのがイベントループの機能の一端です。だいたいイメージできたかと思います。(そういうことにします笑) (補足:コルーチンオブジェクトはコルーチン関数を呼び出すと返ってくるオブジェクトであり、コルーチン関数ではないです。) print(task)で<Task pending name='Task-1'...と出力されていますが、こちらはタスクオブジェクトといい、コルーチンオブジェクトの実行状態を管理するオブジェクトです。 一旦、「ふ~ん」くらいのままでOKです。タスクオブジェクトについては、後でまたでてきます。 このtaskを実際に走らせるにはloop.run_until_complete()というメソッドを使います。 import asyncio async def f(): print("fが実行されたよ") return 100 coro = f() # コルーチンオブジェクトを作成 loop = asyncio.get_event_loop() # イベントループを作成 task = loop.create_task(coro) # イベントループにコルーチンを登録 loop.run_until_complete(task) # タスクが終了するまでイベントループを実行 # result = loop.run_until_complete(task) # このようにすればresultに100が格納される fが実行されたよ こんな、長ったらしいのを書いてようやく実行されましたね。 はっきり言ってこんなに書くのめんどくさいですよね。ということで、python3.7から使えるようになったasyncio.run()が便利です。 次のように短くなります。 coro = coro_func() # コルーチンオブジェクトを作成 asyncio.run(coro) # イベントループを作成し、コルーチンオブジェクトが終了するまで実行 これなら書く気になりますね。 asyncio.run()は次の処理を行うようです。 1. あたらしくイベントループを作成します。 2. 指定されたコルーチンオブジェクトをイベントループに登録し、タスクオブジェクトを作成します。 3. タスクオブジェクトが完了するまでイベントループを実行し続けます。 便利ですね。これでイベントループのざっくりした理解を終えます。 最後にもう一度いうと、イベントループは「複数のコルーチンをいい感じに管理・処理してくれる、複雑な処理のループ」です。 前提知識3:Future Futureは処理の結果を格納するためのオブジェクトです。 JavaScriptでいうPromiseに相当するらしいです。 Futureには「結果」と「ステータス」という2つの情報を持っています。 次のコードを見てください。 >>> future = loop.create_future() >>> future.set_result(100) >>> future <Future finished result=100> futureがfinishedというステータスと100という結果を持っている事がわかりますね。 ステータスには「pending」「finished」「cancelled」の3種類があり、 初期状態は「pending」で、終了状態は「finished」か「cancelled」のいずれかです。 なお、通常ではこの例のようにわざとFutureオブジェクトが作られることはありません。このオブジェクトはただ単に「状態と結果を保存するための箱」です。この「箱」をうまく使ってイベントループは処理の状態を把握します。 前提知識4:Future/Coroutine/Taskの違い 個人的に気になったので、これまでのまとめも兼ねて、ここで整理しておきます。 公式ドキュメント1,公式ドキュメント2を見るとざっくり理解できます。 タスクは、イベントループ内のコルーチンオブジェクトの実行を担当する。 タスクはFutureのサブクラスである。 と公式ドキュメントに書いてありました。 あくまで私の理解ですが、TaskはFutureという処理の状態を記述できる用紙を片手に、実行命令をCoroutineに出したり、処理結果や処理状況をFutureを通してCoroutineから受け取り、上司であるイベントループにFutureを通して報告するのが役割だと思います(かなり不正確ですがこの記事ではイメージがつかめればOKです)。 相関図は次の通りです。これだけの情報だと「タスクいらない子」になるので、実際のTaskはもっと細かいことをやっているのだと思います。 await やっとずっとモヤモヤしていたawaitについて解説できますね。 awaitにはルールがあります。 await文に指定できるオブジェクトは、 CoroutineとFuture、Taskの3つです(これらをAwaitableオブジェクトと言います)。 ただ、Futureを直接使うことは無いので await taskオブジェクト await Coroutineオブジェクト のようにして使うみたいです。 「CoroutiineやTaskを実行させる行では、書類Futureのやり取りが必要になるため、そのような行ではwaitを先頭につけるといる決まりになっている」といってしまったほうがわかりやすいかもしれませんね。 asyncio.sleep(1)とtime.sleep(1)の違いからわかる全体像 ここで衝撃の事実をお伝えするのですが、asyncioは一つのタスクしか実行できません。 「あれ?並列処理じゃないの?」と思いますが、asyncioは「CPUを使わない待ち」が生じてる間に次に優先順位の高いタスクにCPUのリソースを割り当てることしかできません。 (決して、CPUのリソースを要する処理を2つ同時に実行できるわけではありません) 結局のところ、asyncio.sleep(1)とtime.sleep(1)の違いは「この処理はCPUを使わない待ちだよー」というのを、イベントループに伝える能力の有無にあります。 ここで、冒頭のサンプルコードとの違いに注意しながら次のコードを見てください。 import asyncio import time async def func1(): print('func1() started') await asyncio.sleep(1) # 上に「CPUを使わない待ち」ということを伝え,CPUの制御をイベントループに渡す print('func1() finished') async def func2(): print('func2() started') time.sleep(2) # 上に「CPUを使わない待ち」ということを伝えられず終わるまで待つしか無い print('func2() finished') async def main(): task1 = asyncio.create_task(func1()) task2 = asyncio.create_task(func2()) await task1 # タスクもawaitがないと、イベントループに「CPUを使わない待ち」ということを伝えられない await task2 asyncio.run(main()) func1() started func2() started func2() finished func1() finished 冒頭のサンプルコードとは異なり、func1()とfunc2()の終了タイミングが逆転していますね。これは、func2()の「待ち」の間にfunc1()に制御が戻らなかったことを意味しています。 「func2() started」が表示された直後までの流れのざっくりとしたイメージ図は次のようになります。 time.sleep()はコルーチンではなくサブルーチンなので、イベントループに「CPUを使わない待ち」ということを伝えられず、素直に終わるまで待つしか無いです。 また、別の捉え方をすると、asyncio.sleep()で他人に順番を譲ることはできても、自発的に割り込むことはできないということになります。 なお、「CPUを使わない待ち」が実際に生じる場面は、「webへの問い合わせを行った際に、結果が帰ってくるまで待つ」などが挙げられます。確かに、web関連のライブラリでよくasyncioを見ますね。 以上、ざっくりとした説明でしたが、ご指摘等あればコメントいただけると幸いです。 勉強しながら記事を書いたので正確ではない部分もありますが、用語を解説しながら説明したので初心者に優しい文章にはなっているはずです。(しかし文章力。。) ここまで理解すれば、他の記事の情報もスムーズに理解できると思います。(私自身が)
- 投稿日:2021-12-30T23:17:26+09:00
pythonのasyncioを非同期処理知識0から勉強してみた
この記事は非同期処理の知識が0の私が こちらの記事やその他の情報をまとめたものになります。 私自身細かいコードを書くわけではないので、この記事ではざっくりとした理解を目的とし、他の記事をスラスラ読めるようになることができればいいというポジションです。 asyincio とは 並列処理には「マルチスレッド」、「マルチプロセス」、「ノンブロッキング」の3種類ありますが、asyncioは「ノンブロッキング」に相当します。 詳細な説明はこちら に委ねます。 「ノンブロッキング」とは、1スレッドで複数のリクエストを処理するもので、スレッドが増えてメモリ不足にならないというメリットがあります。 デメリットや使い所、書き方の基本はこの記事を読むことでわかるようになります。 asyncioのサンプルコード まずは次の例を見てみましょう。async, awaitなどが見慣れないですが、二つの非同期関数があり、それぞれにスリープの処理が入っているだけです。 import asyncio async def func1(): print('func1() started') await asyncio.sleep(1) print('func1() finished') async def func2(): print('func2() started') await asyncio.sleep(1) print('func2() finished') async def main(): task1 = asyncio.create_task(func1()) task2 = asyncio.create_task(func2()) await task1 await task2 asyncio.run(main()) # こちらpython3.7以降の書き方らしいです func1() started func2() started func1() finished func2() finished func1の処理の最中にfunc2の処理が割り込まれていますね。 なぜこの挙動になるのか少しずつ紐解いていきましょう。 知らなければならない前提知識が多数あるので、遠回りですが頑張りましょう。 前提知識1:コルーチン(co-routine) まずはコルーチンというものを知ってもらいます。 サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できます。 接頭辞 co は協調を意味します。「他の処理と協調します」ということでしょうね。 pythonにおける最小のコルーチンは次のようになります。 async def f(n): return n asyncを関数定義の先頭に置くことで、「これがコルーチンだ!」というのをpyhonに教えることになります。 しっかりと、asyncを書いていない関数内で、非同期処理をしようとするとエラーになります。 例えば次のコードはエラーになります。 import asyncio def f(): await asyncio.sleep(1) asyncio.run(f()) エラー文として'await' outside async functionと言われてしまいます。 asyncとawaitはセットだということを覚えておきましょう。 また、コルーチン関数は普通の関数と挙動が異なります。 import asyncio async def f(): return 100 print(f) <function f at 0x0000025479C403A0> 普通の関数であれば、100が返ってきますが、この例ではコルーチンオブジェクトという謎のオブジェクトが返ってきます。 このコルーチンオブジェクトを実行するには、asyncioのイベントループに放り込む必要があります。 前提知識2:イベントループ 調べるとイベントループはかなり複雑なことをしているようで、理解しきれなかったのですが、どうやら初学者レベルでは「複数のコルーチンをいい感じに管理・処理してくれる、複雑な処理のループ」くらいの理解で十分みたいです。 ただ、イベントループの機能をすべて理解しなくとも、1つくらいは知っておきましょう。 import asyncio async def f(): print("fが実行されたよ") return 100 coro = f() # コルーチンオブジェクトを作成 loop = asyncio.get_event_loop() # イベントループを作成 print(loop) task = loop.create_task(coro) # イベントループにコルーチンを登録 print(task) <ProactorEventLoop running=False closed=False debug=False> <Task pending name='Task-1' coro=<f() running at c:/Users/username/Desktop/program.py:3>> このように、コルーチンオブジェクトが登録され、それらを管理するというのがイベントループの機能の一端です。だいたいイメージできたかと思います。(そういうことにします笑) (補足:コルーチンオブジェクトはコルーチン関数を呼び出すと返ってくるオブジェクトであり、コルーチン関数ではないです。) print(task)で<Task pending name='Task-1'...と出力されていますが、こちらはタスクオブジェクトといい、コルーチンオブジェクトの実行状態を管理するオブジェクトです。 一旦、「ふ~ん」くらいのままでOKです。タスクオブジェクトについては、後でまたでてきます。 このtaskを実際に走らせるにはloop.run_until_complete()というメソッドを使います。 import asyncio async def f(): print("fが実行されたよ") return 100 coro = f() # コルーチンオブジェクトを作成 loop = asyncio.get_event_loop() # イベントループを作成 task = loop.create_task(coro) # イベントループにコルーチンを登録 loop.run_until_complete(task) # タスクが終了するまでイベントループを実行 # result = loop.run_until_complete(task) # このようにすればresultに100が格納される fが実行されたよ こんな、長ったらしいのを書いてようやく実行されましたね。 はっきり言ってこんなに書くのめんどくさいですよね。ということで、python3.7から使えるようになったasyncio.run()が便利です。 次のように短くなります。 coro = coro_func() # コルーチンオブジェクトを作成 asyncio.run(coro) # イベントループを作成し、コルーチンオブジェクトが終了するまで実行 これなら書く気になりますね。 asyncio.run()は次の処理を行うようです。 1. あたらしくイベントループを作成します。 2. 指定されたコルーチンオブジェクトをイベントループに登録し、タスクオブジェクトを作成します。 3. タスクオブジェクトが完了するまでイベントループを実行し続けます。 便利ですね。これでイベントループのざっくりした理解を終えます。 最後にもう一度いうと、イベントループは「複数のコルーチンをいい感じに管理・処理してくれる、複雑な処理のループ」です。 前提知識3:Future Futureは処理の結果を格納するためのオブジェクトです。 JavaScriptでいうPromiseに相当するらしいです。 Futureには「結果」と「ステータス」という2つの情報を持っています。 次のコードを見てください。 >>> future = loop.create_future() >>> future.set_result(100) >>> future <Future finished result=100> futureがfinishedというステータスと100という結果を持っている事がわかりますね。 ステータスには「pending」「finished」「cancelled」の3種類があり、 初期状態は「pending」で、終了状態は「finished」か「cancelled」のいずれかです。 なお、通常ではこの例のようにわざとFutureオブジェクトが作られることはありません。このオブジェクトはただ単に「状態と結果を保存するための箱」です。この「箱」をうまく使ってイベントループは処理の状態を把握します。 前提知識4:Future/Coroutine/Taskの違い 個人的に気になったので、これまでのまとめも兼ねて、ここで整理しておきます。 公式ドキュメント1,公式ドキュメント2を見るとざっくり理解できます。 タスクは、イベントループ内のコルーチンオブジェクトの実行を担当する。 タスクはFutureのサブクラスである。 と公式ドキュメントに書いてありました。 あくまで私の理解ですが、TaskはFutureという処理の状態を記述できる用紙を片手に、実行命令をCoroutineに出したり、処理結果や処理状況をFutureを通してCoroutineから受け取り、上司であるイベントループにFutureを通して報告するのが役割だと思います(かなり不正確ですがこの記事ではイメージがつかめればOKです)。 相関図は次の通りです。これだけの情報だと「タスクいらない子」になるので、実際のTaskはもっと細かいことをやっているのだと思います。 await やっとずっとモヤモヤしていたawaitについて解説できますね。 awaitにはルールがあります。 await文に指定できるオブジェクトは、 CoroutineとFuture、Taskの3つです(これらをAwaitableオブジェクトと言います)。 ただ、Futureを直接使うことは無いので await taskオブジェクト await Coroutineオブジェクト のようにして使うみたいです。 「CoroutiineやTaskを実行させる行では、書類Futureのやり取りが必要になるため、そのような行ではwaitを先頭につけるといる決まりになっている」といってしまったほうがわかりやすいかもしれませんね。 asyncio.sleep(1)とtime.sleep(1)の違いからわかる全体像 ここで衝撃の事実をお伝えするのですが、asyncioは一つのタスクしか実行できません。 「あれ?並列処理じゃないの?」と思いますが、asyncioは「CPUを使わない待ち」が生じてる間に次に優先順位の高いタスクにCPUのリソースを割り当てることしかできません。 (決して、CPUのリソースを要する処理を2つ同時に実行できるわけではありません) 結局のところ、asyncio.sleep(1)とtime.sleep(1)の違いは「この処理はCPUを使わない待ちだよー」というのを、イベントループに伝える能力の有無にあります。 ここで、冒頭のサンプルコードとの違いに注意しながら次のコードを見てください。 import asyncio import time async def func1(): print('func1() started') await asyncio.sleep(1) # 上に「CPUを使わない待ち」ということを伝え,CPUの制御をイベントループに渡す print('func1() finished') async def func2(): print('func2() started') time.sleep(2) # 上に「CPUを使わない待ち」ということを伝えられず終わるまで待つしか無い print('func2() finished') async def main(): task1 = asyncio.create_task(func1()) task2 = asyncio.create_task(func2()) await task1 # タスクもawaitがないと、イベントループに「CPUを使わない待ち」ということを伝えられない await task2 asyncio.run(main()) func1() started func2() started func2() finished func1() finished 冒頭のサンプルコードとは異なり、func1()とfunc2()の終了タイミングが逆転していますね。これは、func2()の「待ち」の間にfunc1()に制御が戻らなかったことを意味しています。 「func2() started」が表示された直後までの流れのざっくりとしたイメージ図は次のようになります。 time.sleep()はコルーチンではなくサブルーチンなので、イベントループに「CPUを使わない待ち」ということを伝えられず、素直に終わるまで待つしか無いです。 また、別の捉え方をすると、asyncio.sleep()で他人に順番を譲ることはできても、自発的に割り込むことはできないということになります。 なお、「CPUを使わない待ち」が実際に生じる場面は、「webへの問い合わせを行った際に、結果が帰ってくるまで待つ」などが挙げられます。確かに、web関連のライブラリでよくasyncioを見ますね。 以上、ざっくりとした説明でしたが、ご指摘等あればコメントいただけると幸いです。 勉強しながら記事を書いたので正確ではない部分もありますが、用語を解説しながら説明したので初心者に優しい文章にはなっているはずです。(しかし文章力。。) ここまで理解すれば、他の記事の情報もスムーズに理解できると思います。(私自身が)
- 投稿日:2021-12-30T22:39:26+09:00
ディープラーニングを使って声優さんの声になろうとしてみている
はじめに この記事ではYukarinライブラリというボイスチェンジができる機械学習モデルをお借りして、Windows10で自分の声を声優さんの声に変換してみることに挑戦しています。 色々な方法を模索した記録としての、メモ書き感が強いです。また、先駆者様の手順に乗っかって進めている部分が多いので、新しい情報は少ないです。 また、この記事はある程度のYukarinライブラリの仕組みを前提知識として書いています。 Yukarinライブラリについて簡単に知りたいという方はこの動画をご覧になるのがわかりやすくて良いと思います。 スペック ・OS:Windows10 ・cuda:10.0 ・GPU:GTX1080 ・RAM:24GB ・CPU:i7-7700 音声処理的なこと はじめに このセクションでは音声の準備や環境などに関して書きます。プログラム実行に関する話はプログラムについてを見てください。 目標 声優統計コーパスという公開音声データセットの土谷麻貴さん(喜び)の声に自分の素の声を変換することを目標とする。 土谷麻貴さん(喜び)を選んだのは一番アニメっぽいと思ったので。 スペック(筆者の声) ・すごく声が小さい ・ぼそぼそ喋る とりあえず学習してみる 解説動画にもあるように、リップシンクなどの雑音は天敵らしい。またハキハキ喋らないと変換が難しいのも理解できる。 しかし、音声変換に良い条件を満たすには筆者的には無理した喋り方をする必要があり、ひいては普段使いしづらい。 ということで、ダメもとで筆者の素の声から声優さんの音声への変換に挑戦してみた。 データの準備 データの録音、編集にはAudacity(https://audacity.softonic.jp/)を使う。 声優統計コーパスで公開されているデータは、音素バランス文という音声特徴のバランスが良い100文を読み上げた100個の音声ファイルである。 やることは音声ファイルに合わせて自分の声を録音することである。 ポイント 1.タイミング 「音声ファイルに合わせて」というのは発声のタイミングもそろえるということで、つまりは同時再生したときハモるようにすることらしい。 ただ「この調整はプログラム側でもある程度行ってくれる」らしいので、割と妥協したところもある。 具体的な手順は ・声優さんの声を聴きながら、かぶせるように同時に喋る ・マイクの入力遅延があるので、録音音声の頭の部分を少し切り取る で行った。 2.発音 単純に文章が難しい。ちゃんと正しく発音するだけでも難しい。アナウンサー試験? 脳も舌もついていかないので、繰り返し聞いて覚えることで対応した。 3.声量 マイクが悪いのか、喉が悪いのか、全然声が入らない。 (上:声優さんの声、下、筆者の声) これでは学習に悪影響が出そうなので、Audacityの増幅機能を使って音量を上げた。 (上:声優さんの声、下、増幅フィルタをかけた筆者の声) これで声量を同程度にすることができた。ある程度波形も一致しているように見える。 しかしこの手法だとノイズも増幅されてしまうので、悪影響があると思われる。 結局、ノイズキャンセリングマイクを買うかボイトレをするのが良いのだろう。 データ量 当然多ければ多いほど良いが、なかなか骨の折れる作業なのでとりあえず30本分を録音して学習に使う。 このとき録音したファイル名を元データのファイル名と同じにしておくと後が楽。 学習結果 1stStage150000epoch,2ndStage20000epochの学習パラメータを使って変換を行った。 結果、月から話してんのかってくらい無茶苦茶回線悪い人の声に変換された。 単語の聞き取りも危うい。 ダメもとでやってみてダメだったという話なので、今後データや環境を改善してもうちょっとトライしてみたい。 プログラムについて(試した手法) はじめに このセクションではプログラムの苦難やエラーについて書きます。音声の用意や楽しい話については音声処理的なことを見てください。 目標 Windowsでディープラーニングのリアルタイムボイスチェンジをしたい(ゲームやりながらDiscordとかしたい) 結論 先に今の時点で出ている結論を紹介します。 ・絶対Linuxでやったほうが楽 ・現在(2021年12月)のWindows10で「Yukarinライブラリ」を動かすのは非常に大変である ・もしWindowsで動かしたいならこの記事とこの記事を見ながらやるといい ・どの手法でもcupyのインストールが大変 Anacondaを使う(断念) あらまし Anacondaは主にPythonの仮想環境構築に使うライブラリで、いろいろなモデルを一つのPC上で動かしたいときによくお世話になる。 環境の作成も手軽で、何か失敗しても外部に影響を与えることが少ない。 しかしその分pip installが使えないという難点もある。 手順 基本的にこの記事と同じような経過をたどった。 Anacondaでつまづき、諦めた後に見つけたのでいろいろショックだった。 大変だったこと 1.condaでinstallできないライブラリがある pysptkなど。githubなどから引っ張ってくればインストールは可能。 2.リポジトリのPythonバージョン(3.6)だとcondaでinstallできないライブラリがある pyworld。先駆者様方は3.7にバージョンアップしていたしそれで問題なさそう。 3.cupyのインストール ここで断念。 cupyのバージョン指定をしてもインストールに失敗する。 正着手はこの方も書かれている通り、 pip install cupy-cuda(cudaバージョン)==5.4.0 とすることだった。(conda installでも行けるのかは未検証) ここを踏まえてAnacondaに再トライすれば、動かすことができるかもしれない。(たぶんやらない) Docker Desktopを使う(失敗) 色々試したが、要するにNvidia-Dockerがwin10では使えなかったのが敗因。 win11にアップグレードする、CPU学習で妥協するなどの手法を使えば実行できるかもしれない。 pipでルート環境に構築する(成功) ルート環境に色々インストールするのは嫌なのだが、仕方ないのでこの方法を試した。(というか、これで駄目なら打つ手はない) 手順 こちらの記事の通りに行った。 cuda関連は既にinstallしてあったので、become-yukarinの環境整備から。 大変だったこと 1.cupyのインストール 記事の参考文献を読みつつcupyのインストールに苦戦する。ここでcupy-cuda100を使うことに気づく。 pip install cupy-cuda100==5.4.0 つまり、結局これで行けた。環境変数などもこの記事を参考に変えたが効果があったのかはわからない。 ライブラリのバージョン調整や追加インストール スクリプトを実行するとエラーが出て止まることがある。 そのたびエラーの内容を調べて、ヒットしたgithubのissueなどに従いライブラリのバージョンを変えたり追加インストールを行ったりソースコードをちょこっと書き換えたりして解決。 全体通して、そういった比較的軽い問題は何度か起こった。 2.pickleのエラーについて condaの方もwindowsの方も言及しているのがpickleの問題である。これは学習時(train.py,train_sr.pyの実行時)に発生するエラーである。 解決法はリンク先に示されている。 C:\Usersユーザ名\Anaconda3\pkgs\python-3.6.5-h0c2934d_0\Lib\multiprocessing\http://reduction.py/ の15行目にある import pickleを import dill as pickleに変更 これで問題なく動くのだが、実は別の問題を引き起こすことが試しているうちに分かった。 それが特徴量抽出がうまくいかないという問題である。 なんでこうなるのかよくわからないが、マルチプロセッシングで並列関数呼び出しをする際になんやかんやあって参照が外れてしまっているようだ。 この問題の対処は簡単である。 import dill as pickleに変更した部分をimport pickleに戻すのである。 つまり、 ・学習プログラムを動かすときは「import dill as pickle」に。 ・特徴抽出プログラムを動かすときは「import pickle」に編集するということである。無様な解決法だお 3.メモリリーク 最初の学習実行時、急にPCが重くなってブラウザがクラッシュした。学習もエラーで止まってしまった。 なんでかと調べたら仮想メモリ(ページングファイル)がCドライブを圧迫していた。その当時のCドライブの空き容量は10GB程度だった。つまり、Cドライブの空き容量が0になってPCが異常に重くなったらしい。 このプログラムの学習は異常に仮想メモリを使う。しかも物理メモリは使わず仮想メモリばかり使うので、ドライブの空き容量が少ないとすぐ異常終了する。 学習中のタスクマネージャーである。このうち大体35GBくらいをこのプログラムが使っている。 仮想メモリは50GBくらいだと止まることもあったので、大幅増設してこの数値になっている。 解決法がわからないので現状は大量の仮想メモリで対抗している。 そもそもこれで仕様通りなのかもしれない。 まだできていないこと リアルタイム音声変換 試したら追記予定(今のクオリティでやっても……) 参考文献 大変参考にさせていただきました。ありがとうございます。
- 投稿日:2021-12-30T21:35:55+09:00
コラッツの問題 Collatz problem を python で描画してみる。
はじめに 12/28~30 PythonBootCamp内で作成した関数を少しブラッシュアップして記事にしました。 コラッツの問題 Collatz problem とは コラッツの問題は非常に単純な問題ながら、いまだ自然数 n に対しての証明がなされていない。 2の68乗まではコンピューターで計算をして正しいことは確認ができているらしい。 任意の正の整数 n をとり、 n が偶数の場合、n を 2 で割る n が奇数の場合、n に 3 をかけて 1 を足す という操作を繰り返すと、どうなるか というものである。「どんな初期値から始めても、有限回の操作のうちに必ず 1 に到達する(そして 1→4→2→1 というループに入る)」という主張が、コラッツの予想である。 正確な定義などはWikipediaを参照してください。 コラッツの問題(コラッツのもんだい、Collatz problem)は、数論の未解決問題のひとつである。1937年にローター・コラッツが問題を提示した。問題の結論の予想を指してコラッツの予想と言う。固有名詞に依拠しない表現としては3n+1問題とも言われ、初期にこの問題に取り組んだ研究者の名を冠して、角谷の問題、米田の予想、ウラムの予想、他にはSyracuse問題などとも呼ばれる。数学者ポール・エルデシュは「数学はまだこの種の問題に対する用意ができていない」と述べ、解決した人に500ドルを提供すると申し出た。 ジェフリー・ラガリアスは2010年に、コラッツの予想は「非常に難しい問題であり、現代の数学では完全に手が届かない」と述べた。 コンピュータを用いた計算により、2の68乗までには反例がないことが確かめられている。 また、2011年度大学入試センター試験数学IIB第6問に題材として取り上げられた。 コラッツの問題を描画する Plotly で import plotly.graph_objects as go import plotly.express as px def collatz(num): n = num k = 0 # 引数を変数に格納。 lst = [n] # リストを準備 引数を最初に入れておく。 while n > 1: # While文をまわす。1になったら終了。 if n % 2 == 0: # 偶数・奇数を判定 n = n /2 lst.append(int(n)) k += 1 # 計算が1回終わったらリストに格納していく。 elif n % 2 == 1: n = n * 3 + 1 lst.append(int(n)) k += 1 print("試行回数:",k) fig = px.line(y=lst,) fig.update_layout(title="コラッツ問題をPlotlyで描画してみる", width=1000, height=400, template="plotly_dark", font={ "family":"Meiryo", "size":10 } ) fig.update_xaxes(title_text='試行回数') fig.update_yaxes(title_text='数値') fig.show() print(lst) collatz(126) 126をコラッツの問題の通り数字を計算していくと、最大値 9232 を通り、計算108回行うと最後は1に収束していきます。 指定数間の試行回数を描画する import pandas as pd def collatz_num(n, m): lst = list(range(n,m+1)) k_lst = [] for i in lst: k = 0 while i > 1: if i % 2 == 0: i = i / 2 k += 1 elif i % 2 == 1: i = i * 3 + 1 k += 1 k_lst.append(k) df = pd.DataFrame(k_lst, columns=["試行回数"]) df["指定数"] = lst print("試行回数 最大値:",df["試行回数"].max()) fig = px.line(y = k_lst, x = lst) fig.update_layout(title="必要試行数", width=1000, height=400, template="plotly_dark", font={ "family":"Meiryo", "size":10 } ) fig.update_xaxes(title_text='指定数') fig.update_yaxes(title_text='試行回数') fig.show() collatz_num(1, 50000) 初期値が大きければ試行回数が増えていくわけでもない。 1~50,000の間では、x = 35,655 で試行回数がMAXになることがわかる。 確認をする collatz(35655) 35,000台ではじまった計算がとちゅうで 410,000超 まで増加していることがわかる。 その後一気に減ったかと思うとまた400,000に迫るところまで数を増やしながら、最終的にはもちろん1に収束。 こどもと遊ぶ 我が家では、小6・小3の子どもに紙とえんぴつを渡し、下記の数値で計算をしてみました。 親 初期値 128 試行回数 7回 子 初期値 129 試行回数 121回 速攻計算が終わる親。 初期値129からはじまったのに、途中で1万に迫る数になりビビる子ども。 紙とえんぴつでなんとか計算しきりました。なんだかんだ盛り上がりました。 懸賞金ついてます! 解いたら1億2千万円!
- 投稿日:2021-12-30T21:25:12+09:00
登山好きプログラミング素人が衛星データで登りたい山を探してみた!〜①日本の山リスト作成編〜
はじめに ・登山が好きプログラミング初心者が衛星データを利用して山探しする企画です。 ・こう書いた方がより良いなどありましたら、勉強になりますのでぜひコメントください。 ・人工衛星のデータはオープンデータとして無料で利用できるものが多くあり、初心者でも比較的簡単に楽しめるため、実際に使ってみて初学者のプログラミング学習にもおすすめだと感じました。 ・本企画は以下の4部構成を予定しており、随時リリース予定です。 ①日本の山リスト作成編 ②衛星データで周囲で一番高い山を探してみる編 ③衛星データで山周辺の地域の晴天率を調べてみる編 ④衛星データで眺望の良い、山頂がひらけている山を探してみる編 ・この企画を参考に、自分の趣味と衛星データを繋げてみてもらえたら嬉しいです。 Google Earth Engine(GEE)とGoogle Colabを使った衛星データ活用 今回利用するGoogle Earth Engine(GEE)とGoogle Colabについて紹介します。 GEEは、Googleが提供する人工衛星画像をサーバー上で解析をすることが出来るサービスです。研究・教育・非営利目的であれば無償で利用することができ、複雑なデータフォーマットへの対応や前処理が不要となり、初学者でも簡単に衛星データを扱うことができます。 Google ColabはGoogleが提供するブラウザからPython を実行できるサービスです。Google ColabからGEEのデータを扱うこともできます。 GEEとGoogle Colabを用いた衛星データの解析に関する記事は多数出ていますが、筆者は下記の記事を参考にしました。手順通り実行していけば、簡単に衛星画像が取得でき楽しい上、少し書き換えることで自分の扱いたい衛星データも取得できるため、初学者の方はまずはこちらを参考に遊んでみることをオススメします。 Google Earth EngineとGoogle Colabによる人工衛星画像解析 〜無料で始める衛星画像解析(入門編)〜 https://qiita.com/takubb/items/518e860a1d7c336d8f3d 【続編】Google Earth EngineとGoogle Colabによる人工衛星画像解析 〜無料で始める衛星画像解析(実践編)〜 https://qiita.com/takubb/items/a98e2cab6f66d2f9c507 衛星データとプログラミング 筆者は衛星データに関する知識もPythonによるプログラミングも初心者でした。 衛星データに関する知識は「宙畑」というサイトでたくさんの事例が紹介されています。そ宙畑で紹介されているのは、GEEではなく、Tellusという衛星データプラットフォームのため、公開されているコードをそのまま使うことはできませんが、衛星データでどのようなことができるのかイメージが湧きやすくなるのでオススメです。 宙畑 https://sorabatake.jp/ Pythonによるプログラミングは、個人的には以下のサイトがオススメです。 Pythonの基本〜画像処理まで一通り勉強することができます。 初心者向けTellus学習コース https://tellusxdp.github.io/start-python-with-tellus/index.html 登りたい山の条件を考えてみる 筆者らは登山ガチ勢ではありません。筆者らのうちの一人は普通にスニーカーで岩山を登ってしまう人でしたし、もう一人もウルトラライトダウンのみで登山に向かうような人でした。さすがに危険なので今はそれぞれ最小限のアイテムは持っていますが…笑 そんな私たちが登りたい山の条件を挙げてみた結果、4つに絞られました。 ①標高2000m以上 まずは、登山においてその山の標高はとても大事です。 一概には言えませんが、眺望も登る大変さも標高は大きく影響してきます。 あまり深い理由はありませんが、個人的にはやはり「2000m以上」は一つ大きな指標の一つなので、これを条件にします。 ②周辺で最も高い山 山を決める際に最も重要視しているのが、「眺望」です。 苦労して頂上に着いたときの、「見渡せば広がる自然や街並み、見上げれば無限の青い空」は格別です。 せっかく登ったのに、近くに自分がいる位置より高い山があったらなんか悔しいですよね。 そのため、「周辺で最も高い山」を登りたい山の条件にします。 ③天気が崩れにくい せっかく立てた登山の予定が、雨で中止になることほど悲しいことはありません。 そのため、なるべく雨が降らないような場所にいく予定を立てれば、予定が中止になりにくいのではないでしょうか。 天気が崩れにくいに越したことはないので、これも条件にします。 ④山頂に木がない(森林限界となっている) 低山ではよくあるのですが、せっかく頂上に着いたのに高い木がたくさん生えていて、景色が見えないなんてことも… 良い眺望を求めているので、これも条件にします。 扱う衛星データと本企画の概要 上で決めたように4つの条件を基に4部構成で記事を書いていこうと思います。 ①登りたい山の条件を考えてみる〜日本の山リスト作成 ②衛星データで周囲で一番高い山を探してみる。 ③衛星データで山周辺の地域の晴天率を調べてみる。 ④衛星データで眺望の良い、山頂に木がない山を探してみる。 それぞれの記事で扱うデータを下の表にまとめます。 記事 衛星データ ②周囲で一番高い山を探してみる ALOS/DSM:標高データ(*1) ③山周辺の地域の晴天率を調べてみる。 GSMaP:降水量データ(*2) ④山頂に木がない山を探してみる Sentinel-2:植生指数(*3) スクレイピングによる日本の山リスト作成 本企画では、事前に作成した日本の山リストから、衛星データを使って登りたい山の条件で絞っていき、最終的に登りにいく山を決めます。 本記事ではまずスクレイピングによって日本の山リストを作成します。 国土地理院に日本の主な山岳一覧が公開されているため、ここから日本の山リストを取得します。 https://www.gsi.go.jp/kihonjohochousa/kihonjohochousa41140.html 筆者は下記サイトを見ながら、BeautifulSoupでスクレイピングをしてみました。 下記サイトではJリーグの順位をスクレイピングしていますが、同じ要領で進めていくと山のリストを取得でき、それをcsvファイルに出力しました。 https://atmarkit.itmedia.co.jp/ait/articles/1910/18/news015_2.html from urllib.request import urlopen from bs4 import BeautifulSoup import ssl import csv ssl._create_default_https_context = ssl._create_unverified_context # URLの指定 html = urlopen("https://www.gsi.go.jp/kihonjohochousa/kihonjohochousa41140.html") Obj = BeautifulSoup(html, "html.parser") # テーブルを指定 table = Obj.findAll("div", {"class":"base_txt"})[0] rows = table.findAll("tr") # スクレイピング with open("/content/drive/My Drive/mountain_list.csv", "w", encoding='utf-8_sig') as file: writer = csv.writer(file) for row in rows: csvRow = [] for cell in row.findAll(['td', 'th']): csvRow.append(cell.get_text()) writer.writerow(csvRow) 取得したcsvはこの様になっています。 無事リストを取得出来ましたが、標高のセルに「m」の文字が含まれていたり、緯度経度が同じセルが含まれていたり、いらない情報が多かったり、扱いづらい状態になっているので、ここから少しいじって必要な情報を扱いやすいように整えます。 まずは以下のコードでいらない列を削除します。 このようなcsvデータを扱う際にはpandasというライブラリがとても便利です。 山名、標高、緯度経度があれば問題ないので、今回はpandasを用いてそれ以外の情報の列を削除します。 import pandas as pd df = pd.read_csv("/content/drive/My Drive/mountain_list.csv") # いらない列の削除 df.drop("備考", axis=1, inplace=True) df.drop("都道府県", axis=1, inplace=True) df.drop("所在等", axis=1, inplace=True) df.drop("三角点名等", axis=1, inplace=True) 次に以下の標高列に含まれている「m」の文字列を削除します。この後の解析で標高値を参照する際に文字列が入っていると厄介なためです。 # 標高セルに含まれる"m"を削除 df['標高'] = df['標高'].str.strip("m") 次に、緯度経度のセルの編集をしていきます。 まずは緯度経度が同じセルに入っているため、「秒」の文字の後ろ側で分割し、それぞれ緯度経度列に代入します。 その後、元々の緯度経度列は削除します。 # 緯度経度の分割 df['緯度'] = df['緯度経度'].str.split(pat='秒', expand=True)[0] df['経度'] = df['緯度経度'].str.split(pat='秒', expand=True)[1] df.drop("緯度経度", axis=1, inplace=True) 次に、度分秒表記になっている緯度経度を10進法に変換します。GEEなどでは緯度経度は10進法で扱われているので、この変換は覚えておくと良いかもしれません。 変換の流れは、まず「度」「分」「秒」を「。(ピリオド)」に直します。次に「。」で分割して、「度」「分」「秒」で扱われていた数値を以下の式を基に10進法に変換します。 10進経緯度 = 度 + (分 ÷ 60) + (秒 ÷ 3600) # 度分秒をピリオドに置換 df['緯度'] = df['緯度'].str.replace("度", ".") df['緯度'] = df['緯度'].str.replace("分", ".") df['緯度'] = df['緯度'].str.replace("秒", ".") df['経度'] = df['経度'].str.replace("度", ".") df['経度'] = df['経度'].str.replace("分", ".") df['経度'] = df['経度'].str.replace("秒", ".") # 度分秒を10進法に変換 lat_list = df['緯度'].values lon_list = df['経度'].values lat_2 = [] lon_2 = [] for i, lat in enumerate (lat_list): lat_2 = lat.split('.') lat = float(lat_2[0]) + float(lat_2[1]) / 60 + float(lat_2[2]) / 60 / 60 df.at[i, "緯度"] = lat for i, lon in enumerate (lon_list): lon_2 = lon.split('.') lon = float(lon_2[0]) + float(lon_2[1]) / 60 + float(lon_2[2]) / 60 / 60 df.at[i, "経度"] = lon 最後にcsvとして出力します。 この後csvの結果をGoogleMapに表示する際に、”<>”などの特殊文字があると読み込みできませんと言われてしまうので、列名を変更しておきます。 # 列の名前に<>があってはいけないらしいので変更 # Google Mapにマッピングしない場合は不要 df.rename(columns={'山名<山頂名>': '山名'}, inplace=True) # 保存 df.to_csv("/content/drive/My Drive/mountain_list_fin.csv") 出力されたcsvは以下のようになっています。 無事に必要な情報がまとまったcsvが作れました。 このcsvの結果を試しにGoogle Mapに表示してみました。コードで行う方法もあると思いますが、今回は手動で行っています。方法を以下の記事で紹介しておきます。 https://qiita.com/oz_oz/items/e796bb1a66e883e4fa3c 日本に山ってこんなにたくさんあるんですね… 次回はこのたくさんの山から、自分たちの登りたい山を絞っていきたいと思います! まとめ 今回は企画の説明と、前準備として国土地理院のwebサイトからスクレイピングによって日本の山リストを取得するまでを紹介しました。 次回からはGEEの衛星データを使って、自分たちの登りたい山を探していきます。 ※2022/01/09に次の記事を公開予定です。 参考文献 *1:ALOS DSM Global 30m https://developers.google.com/earth-engine/datasets/catalog/JAXA_ALOS_AW3D30_V3_2 *2:GSMaP Operational: Global Satellite Mapping of Precipitation https://developers.google.com/earth-engine/datasets/catalog/JAXA_GPM_L3_GSMaP_v6_operational?hl=en *3:Sentinel-2 MSI: MultiSpectral Instrument, Level-2A https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR [Python入門]Beautiful Soup 4によるスクレイピングの基礎 https://atmarkit.itmedia.co.jp/ait/articles/1910/18/news015_2.html Google Earth EngineとGoogle Colabによる人工衛星画像解析 〜無料で始める衛星画像解析(入門編)〜 https://qiita.com/takubb/items/518e860a1d7c336d8f3d 【続編】Google Earth EngineとGoogle Colabによる人工衛星画像解析 〜無料で始める衛星画像解析(実践編)〜 https://qiita.com/takubb/items/a98e2cab6f66d2f9c507
- 投稿日:2021-12-30T21:18:46+09:00
証明書ファイル変換ではまった話
REST apiをpythonのrequestsで叩く際に、クライアント認証が必要でしたが、 クライアント認証に使う証明書ファイル作成ではまりました。 数ヶ月前の話なので、結構適当に書きます。 環境 OS: windows 10 経緯 サーバ管理者から証明書インストール手順書と、証明書ファイルが送られてくる。 手順書に従い、証明書をインストール?する。(用語が適切かわかりません) OpenSSL を使ってpfxファイル(証明書)をクライアント認証で使えるpemファイル(証明書情報が平文で入ってる)に変換する。 -> ここではまりました。よくわからないエラーが出てて原因が分かりませんでした。 (確か変換できません?みたいなやつだったかと....) 原因 原因は、手順書に記載の方法だと,OpenSSLでpem ファイルを生成する際にエクスポートを禁止してしまうためでした。 下画像の状態。 回避策は簡単で、「このキーのエクスポート可能にする」のチェックボックスをチェックして、 pfxファイルを生成した後OpenSSL でpenファイルに変換するだけです。
- 投稿日:2021-12-30T20:03:38+09:00
【画像処理】NumpyでMedianフィルター
NumpyでMedianフィルターを実装してみます。 Medianフィルターはある画素について、その画素と近傍画素の中央値を出力するフィルターです。ノイズ除去などに利用されます。 まず、使用する画像を読み込み、グレースケール画像に変換しておきます。 original_image = plt.imread(image_name) if np.issubdtype(original_image.dtype, np.integer): original_image = original_image / np.iinfo(original_image.dtype).max gray_image = 0.2116 * original_image[:,:,0] + 0.7152 * original_image[:,:,1] + 0.0722 * original_image[:,:,2] plt.imshow(gray_image, cmap='gray') ノイズを付与します。 from numpy import random noise_image = gray_image.copy() noise_image[random.choice([True, False], size=noise_image.shape, p=[0.05, 0.95])] = 1.0 plt.imshow(noise_image, cmap='gray') ノイズを付与した画像にMedianフィルターを適用します。 def median_filter(image, size=(3, 3), boundary='edge'): pad_image = np.pad(image, ((int(size[0] / 2),), (int(size[1] / 2),)), boundary) shape = (image.shape[0] - size[0] + 1, image.shape[1] - size[1] + 1) + size strides = image.strides * 2 strided_image = np.lib.stride_tricks.as_strided(image, shape, strides).reshape(shape[0], shape[1], size[0] * size[1]) return np.apply_along_axis(lambda a: np.median(a), 2, strided_image) median_image = median_filter(noise_image) plt.imshow(median_image, cmap='gray') 実装したコードはGoogle Colaboratoryに置いてあります。
- 投稿日:2021-12-30T19:25:39+09:00
blackで始める妥協のないPythonコードフォーマッティング
タイトルで言いたいことは終わりました。Pythonのコードフォーマッターであるblackの紹介です。 Any code style you like, as long as it's black. コードのスタイリングは十人十色になりがちです。コードフォーマッターを導入してみたものの、ちょっと自分が使いやすいようにカスタマイズするとか。このblackは妥協のないただ一つのコードフォーマットスタイルを手に入れようというフォーマッターです。PEP8にのっとり、カスタマイズできない(独自ルールが作成できない)フォーマット機能を提供するようで、さまざまなプロジェクトで使用されている様子です。潔い。悩みたくないからこれで良い感があります。 The following notable open-source projects trust Black with enforcing a consistent code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, and many more. The following organizations use Black: Facebook, Dropbox, KeepTruckin, Mozilla, Quora, Duolingo, QuantumBlack, Tesla. https://github.com/psf/black#note-this-is-a-beta-product より。 Visual Studio Codeで利用する まずはpipでインストールする。 > pip install black コマンドラインで実行するとこんな感じ。 > black main.py All done! ✨ ? ✨ 1 file left unchanged. 毎度コマンドをたたくのも面倒なので、Visual Studio Code でファイル保存時に、自動的にフォーマットが行われるようにします。ユーザー設定から。 python.formatting.provider を black に設定する。 editor.formatOnSave をチェックする。 これでOK!コードフォーマット自体の設定が無いので、ここから自分に合うようなカスタマイズをする必要はもちろんありません。潔くて良い!(2回目)
- 投稿日:2021-12-30T19:07:48+09:00
【Project Euler】Problem 94: 正三角形に近いヘロンの三角形
本記事はProjectEulerの「100番以下の問題の説明は記載可能」という規定に基づいて回答のヒントが書かれていますので、自分である程度考えてみてから読まれることをお勧めします。 問題: 正三角形に近いヘロンの三角形 原文 Problem 94:Almost equilateral triangles 問題の要約: 三辺の長さが整数$a,b,c$で$a=b=c\pm 1$で、面積も整数になる三角形で、三辺の和が$10^9$を超えないもの三辺の和の合計を求めよ 3辺の長さと面積が全て整数となる三角形をヘロンの三角形と言うみたいですが、この問題では正三角形に近い$a=b=c\pm 1$のものを探す必要があります。以下のような手順で解いていきます。 ヘロンの公式(三辺から面積を求める)使って方程式を作る 【ペル方程式を解く(その1)】平方根の連分数展開 【ペル方程式を解く(その2)】連分数からペル方程式の解を発生させる ペル方程式の解から元のa,b,cを求める ヘロンの公式(三辺から面積を求める)使って方程式を作る まずヘロンの公式です。 S=\sqrt{s(s-a)(s-b)(s-c)} \\ s=\frac{a+b+c}{2} \\ \\ $s$も整数なのでcは偶数。したがって$c=2k$とおいて変形して行きます。 c = 2k、a=b=2k \pm 1\\ したがって\\ s=3k\pm 1 \\ S=\sqrt{(3k\pm 1)\times k \times k \times (k \pm 1)} \\ = k \times \sqrt{3k^2 \pm 4k + 1} \\ Sが整数なので、ルートの中が平方数\\ 3k^2 \pm 4k + 1=y^2 \ \ とおく \\ これを変形すると \\ (3k \pm 2)^2-1=3y^2 \\ すると以下のようにペル方程式となります。\\ x^2-3y^2=1 \ \ ここで x = 3k \pm 2 【ペル方程式を解く(その1)】平方根の連分数展開 ペル方程式の解を生成していく方法には「ペル型方程式をsympyのdiop_DNで解く」で紹介したようにsympyのdiop_DNを使う方法もありますが。今回は「【Project Euler】Problem 66: ディオファントス方程式」の中の「ペル方程式の解を平方根の連分数展開から求める」でやったように平方根の連分数から求める方法で作ったコードをもとにして作って行きます。 まず平方根を連分数展開する関数cfsqrtで3の平方根の連分数$[1, [1, 2]]$が求まります。 def cfsqrt(m): n0 = int(m**(1/2)) if n0*n0 == m: return [n0] n,a,b,cf = n0,1,0,[] while True: b = n*a -b a = (m-b*b)//a n = (n0+b)//a cf.append(n) if a == 1: break return [n0,cf] cfp = cfsqrt(3) print(cfp) # [1, [1, 2]] 【ペル方程式を解く(その2)】連分数からペル方程式の解を発生させる 次にこの連分数からペル方程式の解を発生させる関数cfp2pellを作ります。このコードは「【Project Euler】Problem 65: ネイピア数eに収束する分数」で作った連分数から収束する分数を求める関数cf2fracを変更しました。 循環部分(今回は[1,2]の部分)の長さが偶数か奇数かで処理が変わってきます。奇数の場合には変数cntを使って2回に1回出力するようにしました。yieldを使っているのでnext関数を呼ぶごとに次のペル方程式の解が出てくる仕様です。 def cfp2pell(cfp): if len(cfp) == 1: yield (cfp[0],1) p, prd = 0, cfp[1] n0, l = cfp[0], len(prd) cnt = 0 if l>1 else 1 # alternate counter for l is odd case pn_2, pn_1, qn_2, qn_1, an_1 = 1, n0, 0, 1, prd[p] while True: pn, qn = an_1 * pn_1 + pn_2, an_1 * qn_1 + qn_2 #print(p, cnt, pn, qn) if p==max(l-2,0) : # if p = kl-1 if l%2==1: cnt = 1-cnt # anternate count if cnt == 0: yield (pn, qn) p = (p+1)%l an_1, pn_1, pn_2, qn_1, qn_2 = prd[p], pn, pn_1, qn, qn_1 ペル方程式の解から元のa,b,cを求める 2つの関数cfsqrtとcfp2pellを使ってメインを作ります。cfp2pellから生成されたペル方程式の解$(x,y)$を$k$に変換します。$\pm$があるので二つの値の各々の整数チェックをして周囲の長さ($6k \pm 2$)が上限になるまで足して行きます。 以下のプログラムでは$10^3$まで走らせた結果を表示しました。 cfp = cfsqrt(3) xyPell = cfp2pell(cfp) peri_sum, cont = 0, True while cont: (x,y) = next(xyPell) for i in [-1,1]: k = x+2*i if k%3==0: # check k is multiple of 3 k = k // 3 if k == 0: continue S, peri = k*y, 6*k-2*i if peri > 10**3: cont = False break print(f"a,b={2*k-i}, c={2*k}, peri={peri}, S={S}") peri_sum += peri print(F"answer = {peri_sum}") # a,b=5, c=6, peri=16, S=12 # a,b=17, c=16, peri=50, S=120 # a,b=65, c=66, peri=196, S=1848 # a,b=241, c=240, peri=722, S=25080 # answer = 984
- 投稿日:2021-12-30T18:27:51+09:00
カメラ映像をマインクラフト内でリアルタイム描画
はじめに 前回マイクラ内に任意の画像をピクセルアート化するプログラムを作成しました。 そこで今回は描画する対象をカメラ映像に置き換えて、カメラ映像をリアルタイムでマイクラ内に再現しようと思います。 開発環境 Windows 11 Mincraft JE 1.12.2 Forge 1.12.2 Raspberry Jam Mod 0.92 Python 3.9.9 カメラ映像の取り込み 自分のPCにはカメラが付いていないため、Android端末をつないでその映像を取り込みます。 Androidカメラ映像の取り込みはDroidCamAppを使いました。 マイクラへの映像受け渡し 映像の取り込みはOpenCVのVideoCaptureを使います。取り込んだ映像の1フレームは1枚の画像となるため、それをマイクラのピクセルアート化プログラムに渡し、マイクラ内で描画します。 import cv2 #カメラの起動 camera = cv2.VideoCapture(1) while True: ret, frame = camera.read() # フレームを取得 frame = Image.fromarray(frame) #pillowで操作できるように変換 #各ピクセルに対応したマイクラ内ブロックに変換 im = pic2map.resize_picture(frame, map_size, brightness) im_color = pic2map.to_color(im, color_convertor) im = np.array(pic2map.patting(im_color)) #ブロックの配置 for i in range(map_size): for j in range(map_size): #余白は白のウール if im[i,j] == 255: mc.setBlock(corner[0] + j, corner[1], corner[2] + i, block.WOOL.id, 0) #ウールなどブロックidと種類idを持つブロックの設置 elif im[i, j] > 1000: id1, id2 = divmod(im[i, j], 100) mc.setBlock(corner[0] + j, corner[1], corner[2] + i, int(id1), int(id2)) #その他ブロックの設置 else: mc.setBlock(corner[0] + j, corner[1], corner[2] + i, int(im[i, j])) #qの入力で終了 if cv2.waitKey(1) & 0xFF == ord('q'): break # 撮影用オブジェクトとウィンドウの解放 camera.release() cv2.destroyAllWindows() 結果 以下のようにカメラので取り込んだ映像をマイクラ内でピクセルアート化することができました。 終わりに カメラ映像をリアルタイムでマイクラ内に描画するプログラムを作成しました。以前に作った任意の画像をマイクラ内にピクセルアート化するプログラムを流用できたため、その画像をカメラで取り込んだに置き換えるだけで完成しました。これにより短時間でできました。機能を小分けにして汎用性を高める便利さを感じました。
- 投稿日:2021-12-30T18:21:58+09:00
Pythonでランダム文字列を数値化する
背景・目的 とある作業で、文字列を数値化する必要があったので作業ログとして残します。 厳密にはIDのような文字列を元にクラスタ化したかったので、数値化して余りを求めるところまで。 結論 一度バイト配列に変換してから、int型に直す。 内容 文字列をバイト配列に変換 コード string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890@`:*/?" print("byte[] {0}".format(string.encode('utf-8'))) 結果 byte[] b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890@`:*/?' バイト配列から数値に変換 コード int.from_bytesを利用する。 パラメータには、バイト配列と、byteorderを渡す。 byteorderの説明は以下の通り。 byteorder 引数は、整数を表すのに使われるバイトオーダーを決定します。 byteorder が "big" なら、最上位のバイトがバイト配列の最初に来ます。 byteorder が "little" なら、最上位のバイトがバイト配列の最後に来ます。ホストシステムにネイティブのバイトオーダーを要求するには、 sys.byteorder をバイトオーダーの値として使ってください。 bigとlittleが指定可能らしい。 これらはエンディアンの指定で、ビッグエンディアンは、左から大きい値(8桁なら128、64、32・・・1のように)。リトルエンディアンは、左から小さい値(8桁なら、1、2、4・・・128)となるようだ。 string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890@`:*/?" print("numeric {0}".format(int.from_bytes(string.encode('utf-8'), byteorder='big'))) 結果 numeric 3758001799418605191197857118323574174346338316370081010955161214851720350686724878846949519162920740126535318039925868075538283434967887497638995678921760193831907135 コード全体 import os import sys LIST =["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890@`:*/?"] def generate_string2numberic(string): print("byte[] {0}".format(string.encode('utf-8'))) print("numeric {0}".format(int.from_bytes(string.encode('utf-8'), byteorder='big'))) def main(): for line in LIST: print("string {0}".format(line)) generate_string2numberic(line) if __name__ == '__main__': main() 考察 他にもっと良い方法があるかもしれないが、現時点で思いつく方法をメモしておきます。 参考
- 投稿日:2021-12-30T18:19:22+09:00
FastAPIでBearerトークンを抽出する
小ネタ。 FastAPIでAuthorization: Bearer {token}のようにヘッダで送られてくるトークンを使って認証したい場合に、トークン取得処理の自前実装が不要になる。 コード Dependsを使ってDIチックに処理できるので嬉しい dependencies.py from fastapi import Request, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials async def get_bearer_token( request: Request, bearer: HTTPAuthorizationCredentials = Depends(HTTPBearer) ) -> str: credentials = await bearer(request) token = credentials.credentials return token await bearer(request)としている部分は、HTTPBearerクラスの__call__が該当処理。 ソースコード: fastapi/http.py at master · tiangolo/fastapi
- 投稿日:2021-12-30T18:15:32+09:00
『エラトステネスの篩』各言語記述例まとめ
タイトル通りです.筆者過去まとめ記事と趣旨は同じなのですが,一説に『ネット上のエラトステネスの篩のプログラムの大半がエラトステネスの篩じゃない』というのがあるらしく,その割合を少しでも減らす,という目論見もあったりします.なにそれ. 初版で用意した各言語記述例については,とりあえずWikipediaの説明に沿っているつもりですが,上記の記事の趣旨から,それちげーよというツッコミ大歓迎.また,最初にPython版を作成してそれを基に各言語に書き換えたという経緯から,各言語の特徴を活かした,よりシンプルでわかりやすい記述例も大募集.ただし,次の条件を満たすプログラムとします. アルゴリズムが『エラトステネスの篩』であること. 1から一千万までの範囲の素数を一旦求め,その中で最大の素数(9999991)を出力すること. 差し替える際には,当方の次の環境で実行を確認することにしています.なお,非力なハードウェアなのはわざとです(^^;).この環境で実行できる限り,他のプログラミング言語の記述例も募集中. ASUS Zenfone 5: Qualcomm Snapdragon 636 1.8GHz, 6GBメモリ Android 9 + Termux + Debian GNU/Linux 10 + 各言語処理系 $ uname -a Linux localhost 4.4.141-perf+ #1 SMP PREEMPT Fri Jul 10 17:48:58 CST 2020 aarch64 GNU/Linux 各言語記述例 Python sieve.py from numpy import ones, sqrt, arange n = int(input()) tbl = ones(n+1, dtype=bool) tbl[0:2] = False for i in range(2, int(sqrt(n))+1): if tbl[i]: tbl[2*i::i] = False print((arange(n+1)[tbl])[-1]) Python3.7.3+NumPy1.16.2 $ time python3 sieve.py <<< 10000000 9999991 real 0m2.507s user 0m1.250s sys 0m0.300s Ruby sieve.rb x = gets.to_i a = Array.new(x, true) a[0] = false (x**0.5).to_i.times { |i| if a[i] ((i+1)**2-1).step(x,i+1) { |j| a[j] = false } end } r = [] x.times { |i| if a[i] then r.push(i+1) end } p r[-1] Ruby2.5.5 $ time ruby sieve.rb <<< 10000000 9999991 real 0m6.161s user 0m5.330s sys 0m0.250s JavaScript sieve.js let x; if (process.argv.length < 3) x = 10000000; else x = Number(process.argv[2]); let a = new Array(x).fill(true); a[0] = false; s = x**0.5; for (i = 0; i < s; i++) if (a[i]) for (j = (i+1)**2-1; j < x; j += i+1) a[j] = false; let r = []; for (i = 0; i < x; i++) if (a[i]) r.push(i+1); console.log(r.slice(-1)[0]); Node.js10.24.0 $ time node sieve.js 10000000 9999991 real 0m3.397s user 0m3.090s sys 0m0.150s C言語 sieve.c #include <stdio.h> #include <stdlib.h> int main(void) { char buf[20], *a; unsigned int x, c = 0, *r; fgets(buf, sizeof(buf), stdin); sscanf(buf, "%u", &x); a = malloc(x); for (int i = 0; i < x; i++) a[i] = 1; a[0] = 0; for (int i = 0; i*i < x; i++) if (a[i]) for (int j = (i+1)*(i+1)-1; j < x; j += i+1) a[j] = 0; for (int i = 0; i < x; i++) c += a[i]; r = (unsigned int*)malloc(sizeof(unsigned int) * c); for (int i = 0; i < x; i++) if (a[i]) { r[c-1] = i+1; c--; } printf("%u\n", r[0]); return 0; } gcc8.3.0 $ gcc -o sieve sieve.c $ time ./sieve <<< 10000000 9999991 real 0m0.557s user 0m0.520s sys 0m0.010s Scheme sieve.scm (define (sieve x) (let ((a (make-vector x #t)) (s (sqrt x))) (vector-set! a 0 #f) (let loop1 ((i 0)) (if (< i s) (if (vector-ref a i) (let loop2 ((j (- (expt (+ i 1) 2) 1))) (cond ((< j x) (vector-set! a j #f) (loop2 (+ j (+ i 1)))) (else (loop1 (+ i 1))))) (loop1 (+ i 1))))) (let loop ((i 0) (r '())) (if (< i x) (if (vector-ref a i) (loop (+ i 1) (cons (+ i 1) r)) (loop (+ i 1) r)) r)))) (display (car (sieve (read)))) (newline) Chibi-Scheme0.10.0 $ time chibi-scheme -m chibi sieve.scm <<< 10000000 9999991 real 0m7.144s user 0m6.880s sys 0m0.090s 備考 更新履歴 2021-12-30:Python版を変更(コメントより) 2021-12-30:初版公開(Python,Ruby,JavaScript,C言語,Scheme)
- 投稿日:2021-12-30T17:49:38+09:00
【Python3】@propertyで変数を疑似的にプライベート変数化して隠ぺいする
Pythonには変数をプライベート化(外部から参照・代入できなくする)する機能がありません。 ただしpropertyデコレータを利用するとそれっぽいものを実装することはできます。 環境 Ubuntu20.04 Python3.8.10 propertyとは Pythonの組み込みクラスで、オブジェクトの属性を操作する関数を実装しています。 定義 property(fget=None, fset=None, fdel=None, doc=None) fget: 属性の値を取得する関数 fset: 属性の値を設定する関数 fdel: 属性の値を削除する関数 doc : 属性のdocstringを作成する 基本的な使用方法は下記の通りです。(Pythonの公式ドキュメントから丸写し) class C: def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.") 組み込み関数 ― Python 3.10.0b2 ドキュメント より デコレータを利用するとより可読性が高くなります。(同じくPythonの公式ドキュメントから丸写し) class Parrot: def __init__(self): self._voltage = 100000 @property def voltage(self): """Get the current voltage.""" return self._voltage 組み込み関数 ― Python 3.10.0b2 ドキュメント より このような関数を実装すると_voltage変数を読み取り専用の変数にすることができます。 動作確認 実際に動かしてみます。 hoge.py class Hoge(object): def __init__(self, hoge: str): self._hoge = hoge @property def hoge(self): return self._hoge hoge = Hoge("fuga") print(hoge.hoge) #hoge.hoge = "aa" $ python3 hoge.py fuga Hogeクラスインスタンスのhoge変数を参照することができました。 では、#hoge.hoge = "aa" のコメントアウトを外して再度実行するとどうなるでしょうか hoge.py class Hoge(object): ```(省略)``` hoge = Hoge("fuga") print(hoge.hoge) hoge.hoge = "aa" $ python3 hoge.py Traceback (most recent call last): File "hoge.py", line 12, in <module> hoge.hoge = "aa" AttributeError: can't set attribute 期待通り_hoge変数への代入が失敗しました。これで意図しない値が代入されるリスクを減らすことができます。 注意点 1. デコレートする関数名と変数名が同じだとインスタンスが初期化できない hoge.py class Hoge(object): def __init__(self, hoge: str): self.hoge = hoge @property def hoge(self): return self.hoge hoge = Hoge("fuga") #hoge.hoge = "aa" print(hoge.hoge) このコードを実行すると失敗します。 $ python3 hoge.py Traceback (most recent call last): File "hoge.py", line 11, in <module> hoge = Hoge("fuga") File "hoge.py", line 5, in __init__ self.hoge = hoge AttributeError: can't set attribute 代入を禁止しているので初期化も失敗するという当たり前といえば当たり前の話ですが... 「変数名の頭にアンダースコアを1つつける」というルールを設けておくのが良いでしょう。(アンダースコアを2つつけるとマングリングされてしまうのであまりよくないと思われます。) 2. 変数に直接代入することはできる(できてしまう) @propertyを利用したインスタンス変数の隠蔽は変数それ自体をプライベート化するというよりは、関数でラッピングして隠蔽するというのが実態に近いです。(多分、おそらく) したがって、クラスの外部から変数名を直接指定して代入すると値が上書きされてしまいます。 hoge.py class Hoge(object): def __init__(self, hoge: str): self._hoge = hoge @property def hoge(self): return self._hoge hoge = Hoge("fuga") # _hogeフィールドに直接再代入 hoge._hoge = "aa" print(hoge.hoge) $ python3 hoge.py # 値が上書きされている aa チーム開発の際はラッピングされた変数に直接アクセスしないというルールが必要になるでしょう。 (こんなめんどくさいことしたくないから早くプライベート変数を言語でサポートしてほしい) まとめ なんとか(見た目上は)インスタンス変数を隠蔽することができました。 @propertyを使えばクラス変数も同じように不変にできますし、定数のようなものも作成できそうです。 また、裏技のような方法が普通に公式ドキュメントに書いてあったのも意外です。 一次情報をきっちり読み込むのが大事だと思い知りました
- 投稿日:2021-12-30T17:36:00+09:00
Python#2
Python入門その2 条件分岐と繰り返し文 今回は3つの文法に触れていきます。 その前におさらいです。 ・条件分岐 if文のことです。もし~~だったらだったらどうする、という処理を書きたいときに使います。 ・繰り返し文 for文、while文のことです。ある処理を繰り返して行いたい時に使います。 条件分岐 それでは、実際にif文をpythonで書くとどうなるのかをみてみます。 #pythonの場合 var1 = 10 if var1 == 10: print "このva1の値は10です。" このようになります。 それでは、これをunity(c#)で書こうとすると、次のようになります。 //C#の場合 int var1 = 10; if(var1 == 10){ Debug.Log("このvar1の値は10です。"); } 重要な異なる点は3つです 1.条件を囲む()がいらない 2.if文全体を取り囲む{}もいらない 3.ただし、条件の後に:を記述する。 インデントのお話 ここでpythonについてとても重要な話をします。 上のif文では始まりと終わりの{}がありませんでした。 では、どのように始まりと終わりを判断するのでしょうか? pythonでは、if文やfor文の中身を一段下げます。 例えば、↓をみてください。 var1 = 20 if var1 == 10: print "このva1の値は10です。"#① print"ここはif文の外です。"#② 上の文は、①のprintは表示されません。なぜなら、var1は10ではなく、20だからです。 しかし、②のprintは表示されます。なぜなら、これはif文より下の改行の外にいるからです。 このように、文章の行頭に空白を挿入して先頭の文字を右に押しやることをインデントと言います。 このインデントがpythonを書く上で重要なカギになります。 話を条件分岐(if文)に戻します。 if文にはif...else文とif...else if文もありました。 else文とは、if文の条件を外れていた時に実行するモノです。 pythonの場合とc#の場合を比較してみましょう。 #pythonの場合 var1 = 350 if var1 == 0 : MyLayerColor = 'red' else : MyLayerColor = 'blue' print MyLayerColor //c#の場合 int var1 = 350; if(var1 == 0){ MyLayerColor = "res"; }else{ MyLayerColor = "blue"; } Debug.Log(MyLayerColor); 今回もインデントが大事になります。 else文の中身はインデントを下げて、else文から出る時はインデントを戻します。 else...if文は以下のように記述します。 var1 = 0 if var1 == 0 : print "number is 0" elif var1 == 1 : print "number is 1" elif var1 == 2 : print "number is 2" else : print "others" ex2-1 ・ある数字を入力して、その数字が10より大きければ'big'、小さければ'small'と出力するプログラムを書いてみましょう。 n = input() でnに数字が代入できます。 ・ある数字を入力して、その数字が2で割り切れれば偶数、割れなければ奇数と出力するプログラムを書いてみましょう。 繰り返し文 ここではfor文とwhile文を紹介します。 どちらもc#にあった文法なので、書き方さえわかればできるようになると思います。 ただし、for文はc系統の言語と全然違うので、そっちに慣れていると書くのが若干めんどくさいです笑 for文 pythonでは、for文を次のように記載します。 #python for x in range(0,10): print x これで0から9までの数字が出力されます。 ここでもインデントを一つ下げることでfor文の中身を作っています。 C#だと次のように書きます //c# for(int i = 0;i <= 10:i++){ Debug.Log(i); } pythonでは、カウンタ変数 in 範囲のように書きます。 上では、Xが0から10まで、という意味です。 pythonでは、ループが一回終わると自動でカウンタが1増えます. なので、i++みたいなのを書いていません。 2ずつ増やしたいときは以下のようにします。 for x in range(0,10,2): print x これだと、2こ飛ばしで加算されます。 ex2-2 ・0から100までを出力するプログラムを書いてみましょう。 ・次は、100から0に向かって、100,99,98,97,,,とカウントするプログラムを書いてみましょう。(ヒント:range(開始,終了,ループ終了時にカウンタに加える数)と書きます。 ・上のループを改造して、100から2こ飛ばしで0までカウントするプログラムを書いてみましょう。 while文 while文は、実はそんなに使われません。でも重要なのでやります。 こんなふうに書きます。 #python var1 = 0 while var1 < 10: var1 = var1 + 1 print var1 print "ループ終了" これで、上記のfor文と同じように0から9までカウントします。 みて分かる通り、for文より書くのに手間がかかります。 C#だとこんな感じです。 //c# int var1 = 0; while(var1 < 10){ var1 = var1 + 1; Debug.Log(var1); } Debug.Log("ループ終了"); while文は、特に永久にループさせたい時に使用されます。 下のように永久ループは使います。 while True: n = input() if n == 10: break print "end" 変数名=input()で数字を変数名の中に入れることができます。 上のプログラムでは、10と入力するまで永久にループし続け、10が入力されると、breakによってwhileの外に出ます。 ネストについて ネストとは、あるものの中に、それと同じ種類か似たものが入っている構造のことを言います。 こう書くと、非常にむずかしそうですが、例えば、二重になっったfor文や、for文のなかにif文が入っている状態のことを指します。 上の永久ループの例がそれです。 入力について 後々詳しくはやりますが、入力は変数名=input()で数字が入力できると言ってきました。 それでは、文字を入力したいときはどうすれば良いでしょうか? 文字を入力したいときは、変数名=raw_input()で入力させます。実際に確かめてみましょう。 n = raw_input() s = input() print(type(n)) print(type(s)) 以下のように出力されると思います。 <type 'str'> <type 'int'> strとは文字型、intとは整数型のことです。 ex2-3 ・永久ループを使うプログラムを書きます。'hello'と入力されるまで終わらないプログラムを書いてください。 ・while文を用いて、先ほどのex2-2の100から0までカウントするプログラムを書いてみてください。 ex2-4 ・0から100まで、2で割り切れる数のみ出力するプログラムを書きましょう。ただし、for x in range(0,100,2)は使わず、for文(またはwhile)とif文を組み合わせて書いてみましょう。 ・掛け算九九を出力してみましょう。(ヒント:for文を二重に回します。) 引用 [https://developer.rhino3d.com/guides/rhinopython/]
- 投稿日:2021-12-30T17:30:29+09:00
[Python]循環importになるケースでも型アノテーションができるケースがある、という話
TL;DR Pythonで2つのモジュール間でお互いにimportしており、且つその中でのクラスなどの型アノテーションをエラーにならずに対応できるケースがあるよ、という話です。 使う環境 OS: Debian GNU/Linux 10 Python 3.6 ※個人で趣味で作っているPythonライブラリの最低バージョンの3.6に合わせているので新しいPythonバージョンではもしかしたら話が変わってくるかもしれません(そろそろPython 3.6のサポートを切っても良いかもですしね・・・)。 悩ましいPythonの循環importのエラーの話 他の静的型付け言語だと特に気になったりしないのですが、Pythonだと2つのモジュールでお互いにimportしあう形になっているとImportErrorになったりすることがあります。例えば以下のようにsample_A.pyというモジュールとsample_B.pyというモジュールがあるとして、お互いに各モジュール内のクラスを型アノテーションなどでimportしている・・・といったケースです。 sample_A.py from sample_B import SampleB class SampleA: def sample_method(self) -> SampleB: ... sample_B.py from sample_A import SampleA class SampleB: def sample_method(self) -> SampleA: ... この状態でsample_Aモジュールを読み込んだりすると以下のようなエラーになったりします。 ... from sample_B import SampleB ImportError: cannot import name 'SampleB' こうなるケースは稀ではあり基本的には意識することはあまり無いのですが、コード量が多いプロジェクトなどだとたまに型アノテーション目的などでこのような循環importをしたくなるケースが発生しています。 今まではどうしても循環importにするのが自然な場合には片側の型アノテーションでAnyを使ったりもしくは型エイリアスを使って「なんの型なのか?」ということを分かりやすいように調整したりしていました。 sample_B.pyでのAnyを使った例 from typing import Any class SampleB: def sample_method(self) -> Any: ... sample_B.pyでのAnyを使った例 from typing import Any class SampleB: def sample_method(self) -> Any: ... sample_B.pyでの型エイリアスを使って型が分かりやすい名前を設定した例 from typing import Any _SampleA = Any class SampleB: def sample_method(self) -> _SampleA: ... しかしこれらの対応の場合AnyなどではmypyやPylanceなどでの型チェックはスルーされてしまいます。出来たら正確な型アノテーションをしたいところです。 解決策1: ローカルスコープであればそのスコープ内でimportを行う 1つ目の解決策として世の中のネット記事でも良く触れられているものとして、もし片側がローカルスコープ(関数やメソッド内など)であればその中でimportするという方法があります。例えばメソッド内の変数で対象の型のアノテーションが必要なのであればメソッド内でimportすることで循環importとならずに対応が効きます。例えば以下のような感じになります。 sample_B.py class SampleB: def sample_method(self) -> None: from sample_A import SampleA sample_variable: SampleA = SampleA() これであればsample_A側のモジュールが読み込まれた際にトップレベルのスコープでsample_Bモジュールがimportされますが、sample_B側ではこのメソッドが呼び出されるまでsample_A側のimportはされないので循環importにはなりません。 ただしこの書き方ではトップレベルの要素での型アノテーションなどができません。例えばグローバル変数であったりクラスのメソッドの引数や返却値などへの型アノテーションができません(そもそもローカルスコープでしかimportしていないのでそのローカルスコープのもので型アノテーションをすることができません)。 解決策2: モジュールだけimportして且つ文字列を使って型アノテーションをする 2つ目の解決策として、モジュールのみのimportと文字列による型アノテーションの方法があります。 まずモジュールのみのimportの場合ですが、これは循環importでエラーになったりはしません。各モジュールが以下のようなコードになっている状態で実行してみてもImportError無く通ります。 sample_A.py import sample_B class SampleA: def sample_method(self) -> None: ... sample_B.py import sample_A class SampleB: def sample_method(self) -> None: ... ただしこの状態で対象のモジュールを参照する形でクラスを使った型アノテーションなどをしてしまうとこの時点でエラーになります(ImportErrorではないのが少々紛らわしい・・・)。 sample_A.py import sample_B class SampleA: def sample_method(self) -> sample_B.SampleB: ... def sample_method(self) -> sample_B.SampleB: AttributeError: module 'sample_B' has no attribute 'SampleB' このエラーを回避する方法として、型アノテーション部分を文字列でやっておく・・・とエラーを回避できるという対策法があります。もともとはPython 3.7~3.9などでは自身のクラスをそのクラスのメソッドに対して型アノテーションをする場合from __future__ import annotationsなどの指定が必要(Python 3.10以降からはデフォルトの挙動となったので不要)、且つ3.6などの場合にはfutureのものも使えないため文字列で型アノテーションすると対応ができる・・・という言語仕様のものを利用した形となります。 例えば以下のように'sample_A.SampleA'といったようにクォーテーションで囲って書きます。 sample_A.py import sample_B class SampleA: def sample_method(self) -> 'sample_B.SampleB': ... sample_B.py import sample_A class SampleB: def sample_method(self) -> 'sample_A.SampleA': ... このように書くことでImportErrorなどにもならずに且つ両方のモジュールでお互いのクラスの型アノテーションの指定ができるようになります。また、この書き方でもVS Codeでのシンタックスハイライトはクラスなどの色としてちゃんと認識してくれますし、Pylanceなどの型チェックや入力補完なども正しく動作します。 解決策3: typing.TYPE_CHECKING を使う 前述までの対策でも大体解決できるのですが、まだこれだと対応が効かないケースがあります。たとえば継承用のクラスとして使い、且つ循環importになっているケースです。 継承用のクラス指定ではメソッドの引数や返却値などへの型アノテーションと異なり文字列での型の指定が効きません(エラーになります)。例えば以下のように片方のモジュールでは継承として使い、もう片方では型アノテーションとしてお互いのクラスを使用しているケースを考えます。 sample_A.py from sample_B import SampleB class SampleA(SampleB): def sample_method(self) -> None: ... sample_B.py import sample_A class SampleB: def sample_method(self) -> 'sample_A.SampleA': ... 上記だとエラーになります。 ... from sample_B import SampleB ImportError: cannot import name 'SampleB' メソッドの引数や返却値への型アノテーションと異なり、クラスの継承に文字列を指定してみてもこれはサポートされていないため動きません。 sample_A.py import sample_B class SampleA('sample_B.SampleB'): def sample_method(self) -> None: ... ... class SampleA('sample_B.SampleB'): TypeError: str() argument 2 must be str, not tuple ではどうするのか・・・という感じですが、この記事を書いていて調べていたら知ったのですが(いまだに知らない型アノテーションの言語仕様が出てきます・・・)型チェック時のみTrueになるtypingパッケージのTYPE_CHECKINGという値も存在します。 このTYPE_CHECKINGの値がTrueの時のみ対象の型アノテーション用のものをimportする・・・と設定すると、 実際の実行時 -> importはされないので循環importのエラーにはならない mypyなどによる型チェック時 -> importはされるものの、mypyやPylance上では循環importによるエラーになったりしない といった挙動になるためエラーを回避しつつmypyやPylanceの恩恵を得る・・・ということができます。例えば先ほどの例でいうと継承が必要なモジュール側では普通にimportを行って、もう片方の型アノテーションでのみ必要なmodule側ではTYPE_CHECKINGを使った分岐を加えておく(且つ、こちらでは文字列で型アノテーションをしておく)・・・という対応が効きます。 コードは以下のような形になります。 from sample_B import SampleB class SampleA(SampleB): def sample_method(self) -> None: ... from typing import TYPE_CHECKING if TYPE_CHECKING: from sample_A import SampleA class SampleB: def sample_method(self) -> 'SampleA': ... 使うケースは稀ではありますが、とりあえずこれらを把握しておくことでAnyを使わなければいけない・・・といったケースを軽減できそうです。 参考文献・参考サイト
- 投稿日:2021-12-30T17:18:10+09:00
Numpyで画像の平滑化フィルター
Numpyで画像の平滑化フィルターを実装してみます。 まず、画像の読み込みを行います。 original_image = plt.imread(image_name) if np.issubdtype(original_image.dtype, np.integer): original_image = original_image / np.iinfo(original_image.dtype).max plt.imshow(original_image) 画像の畳み込みを行う関数を定義しておきます。詳細は以下の記事を参照してください。 Numpyで画像の畳み込み演算 - Qiita def _convolve2d(image, kernel): shape = (image.shape[0] - kernel.shape[0] + 1, image.shape[1] - kernel.shape[1] + 1) + kernel.shape strides = image.strides * 2 strided_image = np.lib.stride_tricks.as_strided(image, shape, strides) return np.einsum('kl,ijkl->ij', kernel, strided_image) def _convolve2d_multichannel(image, kernel): convolved_image = np.empty((image.shape[0] - kernel.shape[0] + 1, image.shape[1] - kernel.shape[1] + 1, image.shape[2])) for i in range(image.shape[2]): convolved_image[:,:,i] = _convolve2d(image[:,:,i], kernel) return convolved_image def _pad_singlechannel_image(image, kernel_shape, boundary): return np.pad(image, ((int(kernel_shape[0] / 2),), (int(kernel_shape[1] / 2),)), boundary) def _pad_multichannel_image(image, kernel_shape, boundary): return np.pad(image, ((int(kernel_shape[0] / 2),), (int(kernel_shape[1] / 2),), (0,)), boundary) def convolve2d(image, kernel, boundary='edge'): if image.ndim == 2: pad_image = _pad_singlechannel_image(image, kernel.shape, boundary) if boundary is not None else image return _convolve2d(pad_image, kernel) elif image.ndim == 3: pad_image = _pad_multichannel_image(image, kernel.shape, boundary) if boundary is not None else image return _convolve2d_multichannel(pad_image, kernel) 平均化フィルター 近傍画素での平均をとるようにカーネル内の重みが同じで合計が1になるフィルターを平均化フィルターと呼びます。 例えば、3x3の場合および5x5の場合はそれぞれ次のようになります。 \begin{bmatrix} \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \end{bmatrix} \begin{bmatrix} \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} \\ \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} \\ \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} \\ \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} \\ \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} & \frac{1}{25} \\ \end{bmatrix} 平均化フィルターのカーネルを作成する関数は次のようになります。 def create_averaging_kernel(size = (5, 5)): return np.full(size, 1 / (size[0] * size[1])) パラメータを変えて平均化フィルター適用してみます。 averaging_kernel_3x3 = create_averaging_kernel(size=(3, 3)) averaging_image_3x3 = convolve2d(original_image, averaging_kernel_3x3) plt.imshow(averaging_image_3x3) averaging_kernel_5x5 = create_averaging_kernel() averaging_image_5x5 = convolve2d(original_image, averaging_kernel_5x5) plt.imshow(averaging_image_5x5) averaging_kernel_11x11 = create_averaging_kernel(size=(11, 11)) averaging_image_11x11 = convolve2d(original_image, averaging_kernel_11x11) plt.imshow(averaging_image_11x11) 縦方向だけで平均化してみます。 averaging_kernel_17x1 = create_averaging_kernel(size=(17, 1)) averaging_image_17x1 = convolve2d(original_image, averaging_kernel_17x1) plt.imshow(averaging_image_17x1) ガウシアンフィルター 中心画素に近いほど影響が大きくなるように、以下のガウス関数に基づき重みづけをしたフィルターをガウシアンフィルターと呼びます。 f(x, y) = \frac{1}{2\pi\sigma^2}exp(-\frac{x^2+y^2}{2\sigma^2}) ガウシアンフィルターのカーネルを作成する関数は次のようになります。重みの合計が1になるように最後に正規化しています。 def create_gaussian_kernel(size=(5, 5), sigma=1): center = ((size[0] - 1) / 2, (size[1] - 1) / 2) sigma2 = 2 * sigma * sigma kernel = np.fromfunction(lambda y, x: np.exp(-((x - center[0]) ** 2 + (y - center[1]) ** 2) / sigma2), size) kernel = kernel / np.sum(kernel) return kernel パラメータを変えてガウシアンフィルターを適用してみます。 gaussian_kernel1 = create_gaussian_kernel() gaussian_image1 = convolve2d(original_image, gaussian_kernel1) plt.imshow(gaussian_image1) gaussian_kernel2 = create_gaussian_kernel(size=(9, 9), sigma=3) gaussian_image2 = convolve2d(original_image, gaussian_kernel2) plt.imshow(gaussian_image2) gaussian_kernel3 = create_gaussian_kernel(size=(15, 15), sigma=5) gaussian_image3 = convolve2d(original_image, gaussian_kernel3) plt.imshow(gaussian_image3) 実装したコードはGoogle Colaboratoryに置いておきました。
- 投稿日:2021-12-30T17:01:26+09:00
MediaPipeを使って手から取得した骨格座標の情報をCSVに保存する
MediaPipe MediaPipeはGoogleが公開しているクロスプラットフォームで実行可能なMLソリューションです。 顔認証や手の骨格推定などのソリューションを少ないコード記述で利用することができ、動作も軽快です。 ※使用するプラットフォームによって利用可能なソリューションは異なります。 詳細は以下の公式ドキュメントをご参照ください。 今回はPythonにて、 MediaPipeを用いて手の画像からランドマークの座標情報を取得 取得した座標情報をCSVに保存 ついでにランドマークを描画した座標を保存 という流れを作成したので備忘録として残しておきます。 インストール pip installでインストールします。 $ pip install mediapipe データの用意 今回は./data以下に手の画像を何枚か用意しておきます。 コード ひとまず先にコード全体を記載します。 参考:MediaPipe Hands import cv2 import glob import os import csv import mediapipe as mp mp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solutions.drawing_styles mp_hands = mp.solutions.hands def fields_name(): # CSVのヘッダを準備 fields = [] fields.append('file_name') for i in range(21): fields.append(str(i)+'_x') fields.append(str(i)+'_y') fields.append(str(i)+'_z') return fields if __name__ == '__main__': # 元の画像ファイルの保存先を準備 resource_dir = r'./data' # 対象画像の一覧を取得 file_list = glob.glob(os.path.join(resource_dir, "*.jpg")) # 保存先の用意 save_csv_dir = './result/csv' os.makedirs(save_csv_dir, exist_ok=True) save_csv_name = 'landmark.csv' save_image_dir = 'result/image' os.makedirs(save_image_dir, exist_ok=True) with mp_hands.Hands(static_image_mode=True, max_num_hands=1, # 検出する手の数(最大2まで) min_detection_confidence=0.5) as hands, \ open(os.path.join(save_csv_dir, save_csv_name), 'w', encoding='utf-8', newline="") as f: # csv writer の用意 writer = csv.DictWriter(f, fieldnames=fields_name()) writer.writeheader() for file_path in file_list: # 画像の読み込み image = cv2.imread(file_path) # 鏡写しの状態で処理を行うため反転 image = cv2.flip(image, 1) # OpenCVとMediaPipeでRGBの並びが違うため、 # 処理前に変換しておく。 # CV2:BGR → MediaPipe:RGB image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image.flags.writeable = False # 推論処理 results = hands.process(image) # 前処理の変換を戻しておく。 image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) if not results.multi_hand_landmarks: # 検出できなかった場合はcontinue continue # ランドマークの座標情報 landmarks = results.multi_hand_landmarks[0] # CSVに書き込み record = {} record["file_name"] = os.path.basename(file_path) for i, landmark in enumerate(landmarks.landmark): record[str(i) + '_x'] = landmark.x record[str(i) + '_y'] = landmark.y record[str(i) + '_z'] = landmark.z writer.writerow(record) # 元画像上にランドマークを描画 mp_drawing.draw_landmarks( image, landmarks, mp_hands.HAND_CONNECTIONS, mp_drawing_styles.get_default_hand_landmarks_style(), mp_drawing_styles.get_default_hand_connections_style()) # 画像を保存 cv2.imwrite( os.path.join(save_image_dir, os.path.basename(file_path)), cv2.flip(image, 1)) あまり書くことがないですが、ポイントだけ。 with mp_hands.Hands(static_image_mode=True, max_num_hands=1, # 検出する手の数(最大2まで) min_detection_confidence=0.5) as hands mp.solutions.hands.Handsにて手のランドマーク推定のインスタンスを生成しておきます。 image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image.flags.writeable = False # 推論処理 results = hands.process(image) # 前処理の変換を戻しておく。 image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) OpenCVとMediaPipeでは色の順番が異なっています。 - OpenCV:BGR - MediaPipe:RGB そのため、MediaPipeでの推論処理前にcv2.cvtColor(image, cv2.COLOR_BGR2RGB)にて色の順番を変換させる必要があります。 ※画像のオブジェクトを再利用する場合、反対にcv2.cvtColor(image, cv2.COLOR_RGB2BGR)出戻しておきます。 その後、hands.process(image)にて推定処理を実行します。 # csv writer の用意 writer = csv.DictWriter(f, fieldnames=fields_name()) writer.writeheader() # -- 省略 -- # ランドマークの座標情報 landmarks = results.multi_hand_landmarks[0] # CSVに書き込み record = {} record["file_name"] = os.path.basename(file_path) for i, landmark in enumerate(landmarks.landmark): record[str(i) + '_x'] = landmark.x record[str(i) + '_y'] = landmark.y record[str(i) + '_z'] = landmark.z writer.writerow(record) 取得したランドマークの座標情報をCSVに書き込みします。 座標情報は以下の画像の通り、各関節の位置がindexで定められています。 landmarksには以下のような形で各ランドマークの座標が設定されています。 ...省略... landmark { x: 0.6645678281784058 y: 0.6872593760490417 z: -0.061526086181402206 } landmark { x: 0.722145676612854 y: 0.690102219581604 z: -0.07218331098556519 } landmark { x: 0.7754498720169067 y: 0.688714325428009 z: -0.08117059618234634 } ...省略... 気をつけるポイントとしては、 xとyについては0~1の間で正規化された値になっています。 そのため、実際の画像上の座標に落としたい場合は以下の様に画像サイズ(横幅、縦幅)と掛け合わせて変換する必要があります。 image_height, image_width, _ = image.shape x = landmark.x * image_width y = landmark.y * image.height zについても-1~1の間で正規化されているようです。 ※詳細はわからず... 上記でresult/csvの下に次のようなCSVが保存されるかと思われます。 file_name 0_x 0_y 0_z ... 20_x 20_y 20_z hogehoge.jpg 0.xxxx 0.xxxx 0.xxxx ... 0.xxxx 0.xxxx 0.xxxx ついでにランドマークを元画像上に描画して保存しています。 mp_drawing.draw_landmarks( image, landmarks, mp_hands.HAND_CONNECTIONS, mp_drawing_styles.get_default_hand_landmarks_style(), mp_drawing_styles.get_default_hand_connections_style()) cv2.imwrite( os.path.join(save_image_dir, os.path.basename(file_path)), cv2.flip(image, 1)) 座標情報を元に自力でラインを描画することも勿論可能ですが、MediaPipeの中に描画用の関数が用意されているので、簡単に試すことができます。 以下のような画像が作成できます。 おわりに 今回は手の画像に対して推定処理を行いましたが、カメラを繋いで取得した映像からリアルタイムに座標取得が行えるほど軽量に動作します。 手の推定以外にも姿勢制御等もあり、めちゃくちゃ弄るのが楽しいので、おすすめです。
- 投稿日:2021-12-30T16:37:50+09:00
Raspberry Piを用いた疑似ドライブレコーダー(物体検出,自動起動)に関するメモ
きっかけ Rasberry Pi 3Bをドローンに組み込み,行動監視や動体追跡,ベイズ最適化による行動最適化など遊んでいたところ, ふと,持ち運びしやすいパソコンとしてRasberry Pi 3Bを購入したはずなのに,このドローンごと持ち運んでいては意味がないのでは,との哲学的な疑問に行き着いた. そこでRaspberry Pi 4B 8GBモデルを買い足した. メモリが増えどこまでできるようになったか知りたかったため,物体検知モデル( TFLite_detection_webcam)を動かし,ついでドライブレコーダっぽく使えるか試すために,自動起動(systemd)など試みてみたのであった. 参考・利用 Raspberry Piで、オリジナルの学習モデルを使った物体検出(Raspberry Piの環境構築編)[3/4] https://dream-soft.mydns.jp/blog/developper/smarthome/2021/02/2881/ EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi Raspberry Pi 4Bの初期設定 日本語ツールインストール sudo apt update sudo apt install ibus-mozc カメラテスト sudo rpi-update raspistill -o image.jpg フォルダ名を英語表記に LANG=C xdg-user-dirs-update --force 物体検知モデルのインストールから試運転 ・TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi pip3 install tensorflow==1.14.0 sudo pip3 install opencv-python==4.1.0.25 git clone https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi.git mv TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi tflite cd tflite/ sh get_pi_requirements.sh wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip unzip coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip -d coco_ssd_mobilenet_v1_1.0_quant cd tflite/ python3 TFLite_detection_webcam.py --modeldir=./coco_ssd_mobilenet_v1_1.0_quant systemdによる自動起動の設定 ・Xserver errorが起きる,tensorflowがimportできない,オプションが使えない,などいくらか悩んだ.試行錯誤した結果動いた内容を下記記載する.最適解ではないであろうがとりま. 1.shファイル作成 sudo nano /opt/object_detection.sh object_detection.sh #!/bin/sh sleep 20; cd /home/pi/tflite /usr/bin/python3 TFLite_detection_webcam.py --modeldir=./coco_ssd_mobilenet_v1_1.0_quant sudo chmod a+x /opt/object_detection.sh 2.service作成 sudo nano /etc/systemd/system/object_detection.service object_detection.service [Unit] Description=autoboot detection [Service] ExecStart=/opt/object_detection.sh [Install] WantedBy=graphical.target 3.プログラム本体の改変 ・TFLite_detection_webcam.py ・systemdではパスが通っていなかったため追加(shで対応しても良い) import sys sys.path.append("/home/pi/.local/lib/python3.7/site-packages") ・一部絶対パスに書き直し(serviceで対応しても良い) CWD_PATH = '/home/pi/tflite/' ・モニタなしのヘッドレスで動かすためコメントアウト #cv2.imshow('Object detector', frame) ・frameをjpgとしてUSBに保存してゆく.適当に while True: ~省略 cv2.imwrite('/media/pi/RASP4DATA/save_traffic_jpg/'+str(jpgfiles_max)+'.jpg',frame) jpgfiles_max += 1 if jpgfiles_max>5000: jpgfiles_max = 0 など 4.service起動とテスト sudo systemctl daemon-reload sudo systemctl enable object_detection.service sudo systemctl start object_detection.service sudo systemctl status object_detection.service ● object_detection.service - autoboot detection Loaded: loaded (/etc/systemd/system/object_detection.service; enabled; vendor pre Active: active (running) since Thu 2021-12-30 10:48:08 JST; 5s ago Main PID: 4234 (object_detection.sh) Tasks: 3 (limit: 4915) CGroup: /system.slice/object_detection.service ├─4234 /bin/sh /opt/object_detection.sh └─4235 /usr/bin/python3 TFLite_detection_webcam.py --modeldir=./coco_ 12月 30 10:48:08 raspberrypi systemd[1]: Started autoboot detection. 12月 30 10:48:09 raspberrypi open-browser.sh[4234]: 2021-12-30 10:48:09.876082: lines 1-11/11 (END) ・以降,再起動するたびに自動起動する. 自動車に設置 ・Raspberry Pi 4Bは3Aほど要求する.電源問題は地味に厄介. 定期的にUSBからjpgを吸い出し動画に加工 ・こんな感じとなるはずです. https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi/blob/master/doc/BSR_demo.gif (https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi より引用) 今後やりたいこと ・CLIPを用いたより高度な物体認識.メモリ問題がなくなったのでできるかも・・・ CLIPを用いた画像のテキストによる説明と、その発展可能性に関する試行 (オントロジー・知識グラフの代替可能性?.画像を介する文章ベクトルの適正化とラベルなし解像度変換、婉曲表現変換の可能性? ) https://qiita.com/kzuzuo/items/e35e0c0535f0d32b135e *とりま,pytorchインストールまで sudo apt install libopenblas-dev libblas-dev m4 cmake cython python3-dev python3-yaml python3-setuptools sudo apt-get install libavutil-dev libavcodec-dev libavformat-dev libswscale-dev #https://github.com/sungjuGit/PyTorch-and-Vision-for-Raspberry-Pi-4B cd /home/pi/Downloads sudo pip3 install torch-1.8.0a0+56b43f4-cp37-cp37m-linux_armv7l.whl sudo pip3 install torchvision-0.9.0a0+8fb5838-cp37-cp37m-linux_armv7l.whl ・・・ その後 Raspberry Pi 4B 8GBはドライブレコーダーとして自動車に固定され, 持ち運びしやすいパソコンは,やはり無いままなのであった. 年末年始は普段乗りなれていたに方々が運転するためか,危ない運転をする方々が増えます.お気をつけて!
- 投稿日:2021-12-30T16:37:50+09:00
Raspberry Piを用いた疑似ドライブレコーダー(物体検出,記録,自動起動)に関するメモ
きっかけ Rasberry Pi 3Bをドローンに組み込み,行動監視や動体追跡,ベイズ最適化による行動最適化など遊んでいたところ, ふと,持ち運びしやすいパソコンとしてRasberry Pi 3Bを購入したはずなのにドローンごと持ち運んでいては意味がないのでは,との高度に哲学的な疑問に行き着いた. そこでRaspberry Pi 4B 8GBモデルを買い足した. メモリが増えどこまでできるようになったか知りたかったため,物体検知モデル(TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi)を動かし,ついでドライブレコーダっぽく使えるか試すために,自動起動(systemd)など試みてみたのであった. 参考・利用 Raspberry Piで、オリジナルの学習モデルを使った物体検出(Raspberry Piの環境構築編)[3/4] https://dream-soft.mydns.jp/blog/developper/smarthome/2021/02/2881/ EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi Raspberry Pi 4Bの初期設定 ありふれた設定はおいておいて忘れやすいものを 日本語入力インストール sudo apt update sudo apt install ibus-mozc カメラテスト sudo rpi-update raspistill -o image.jpg フォルダ名を英語表記に LANG=C xdg-user-dirs-update --force 物体検知モデルのインストールから試運転 ・TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi pip3 install tensorflow==1.14.0 sudo pip3 install opencv-python==4.1.0.25 git clone https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi.git mv TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi tflite cd tflite/ sh get_pi_requirements.sh wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip unzip coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip -d coco_ssd_mobilenet_v1_1.0_quant cd tflite/ python3 TFLite_detection_webcam.py --modeldir=./coco_ssd_mobilenet_v1_1.0_quant systemdによる自動起動の設定 ・Xserver errorが起きる,tensorflowがimportできない,オプションが使えない,などいくらか悩んだ.試行錯誤した結果を下記記載する.最適解ではないであろうがとりま. 1.shファイル作成 sudo nano /opt/object_detection.sh object_detection.sh #!/bin/sh sleep 20; cd /home/pi/tflite /usr/bin/python3 TFLite_detection_webcam.py --modeldir=./coco_ssd_mobilenet_v1_1.0_quant sudo chmod a+x /opt/object_detection.sh 2.serviceファイル作成 sudo nano /etc/systemd/system/object_detection.service object_detection.service [Unit] Description=autoboot detection [Service] ExecStart=/opt/object_detection.sh [Install] WantedBy=graphical.target 3.プログラム本体の改変 ・TFLite_detection_webcam.py ・systemdではパスが通っていなかったため追加(shで対応しても良い) import sys sys.path.append("/home/pi/.local/lib/python3.7/site-packages") ・一部絶対パスに書き直し(serviceで対応しても良い) CWD_PATH = '/home/pi/tflite/' ・モニタなしのヘッドレスで動かすためコメントアウト #cv2.imshow('Object detector', frame) ・frameをjpg形式でUSBに保存してゆく.条件は適当に while True: ~省略 cv2.imwrite('/media/pi/RASP4DATA/save_traffic_jpg/'+str(jpgfiles_max)+'.jpg',frame) jpgfiles_max += 1 if jpgfiles_max>5000: jpgfiles_max = 0 など 4.service起動とテスト sudo systemctl daemon-reload sudo systemctl enable object_detection.service sudo systemctl start object_detection.service sudo systemctl status object_detection.service ● object_detection.service - autoboot detection Loaded: loaded (/etc/systemd/system/object_detection.service; enabled; vendor pre Active: active (running) since Thu 2021-12-30 10:48:08 JST; 5s ago Main PID: 4234 (object_detection.sh) Tasks: 3 (limit: 4915) CGroup: /system.slice/object_detection.service ├─4234 /bin/sh /opt/object_detection.sh └─4235 /usr/bin/python3 TFLite_detection_webcam.py --modeldir=./coco_ 12月 30 10:48:08 raspberrypi systemd[1]: Started autoboot detection. 12月 30 10:48:09 raspberrypi open-browser.sh[4234]: 2021-12-30 10:48:09.876082: lines 1-11/11 (END) ・以降,Raspberry Piを起動するたびにTFLite_detection_webcam.pyが自動起動し,物体検出した画像をjpg形式でUSBに保存し続ける. 自動車に設置 ・Raspberry Pi 4Bは3Aほど要求する.電源問題は地味に厄介. 定期的にUSBからjpgを吸い出し動画に加工 ・こんな感じとなるはずです. https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi/blob/master/doc/BSR_demo.gif (https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi より引用) 今後やりたいこと ・CLIPを用いたより高度な物体認識.メモリ問題がなくなったのでできるかも・・・ CLIPを用いた画像のテキストによる説明と、その発展可能性に関する試行 https://qiita.com/kzuzuo/items/e35e0c0535f0d32b135e *とりま,pytorchインストールまで sudo apt install libopenblas-dev libblas-dev m4 cmake cython python3-dev python3-yaml python3-setuptools sudo apt-get install libavutil-dev libavcodec-dev libavformat-dev libswscale-dev #https://github.com/sungjuGit/PyTorch-and-Vision-for-Raspberry-Pi-4B cd /home/pi/Downloads sudo pip3 install torch-1.8.0a0+56b43f4-cp37-cp37m-linux_armv7l.whl sudo pip3 install torchvision-0.9.0a0+8fb5838-cp37-cp37m-linux_armv7l.whl ・センサーをつけて野生動物撮影用トレイルカメラ代わりとし,物体検出を利用して検出動物の記録csv自動作成・送信などしても面白いかもしれない.未知データ含め実用とするには・・・分類が限定されやすい画像分類モデルでなく,深層距離学習が必要かな・・・ArcFaceなど現在の技術はどうなっているのだっけか・・・ ・・・ その後 Raspberry Pi 4B 8GBはドライブレコーダーとしてND5RCに固定され, 持ち運びしやすいパソコンは,やはり無いままなのであった. 年末年始は普段乗り慣れていない方々が運転するためか危ない運転をする方々が増えます.お気をつけて!
- 投稿日:2021-12-30T15:30:25+09:00
【Python Flask】初心者プログラマーのWebアプリ#3 【Javascript導入】【画像表示】【CSS適用】
今回のPython Flaskフレームワークで作るもの 今回はFlaskで静的ファイルを使用する方法について書きます。 前回はHTMLテンプレートを使って表示を作りました。簡単なデータ埋め込みです、表示を動的に作成していました。 今日扱うのは、「静的ファイル」を使ってページ作成です。静的ファイルは 画像ファイル CSS JavaScript こんなものがあります。WEBページを作るときには必須の要素ですがファイルそのものを配置して、Flaskではとくに処理しないで表示するだけという、中身のデータが変化しないでそのまま使うようなものを静的ファイルって言います。 Pythonで作るFlaskアプリ記事一覧 内容 part1 簡単なページ作成 part2 HTMLテンプレート表示 part3 "画像" "CSS" "Javascript"実装← ココ part4 フォーム送信 予定 part5 データベースの値取得・更新 予定 ソースコード この章のコードは以下です。確認やコピペでどうぞ 0. 静的ファイルをFlaskで扱うための準備 では、静的ファイル入れるためにstaticフォルダを作ってください。 その後、今回は画像、CSS、JavaScriptを使うので画像を入れるためのimagesフォルダを作成しました。 $ mkdir static $ cd static $ mkdir images そして、今回私はimagesフォルダの中にflask-logo.pngと言う名前のファイルを入れました。 別に画像ならなんでもいいので、好きな画像ファイルを配置してください。 1. 画像の表示をPython Flaskで 第一引数に'static'を指定、filenameに先ほど作成したstaticディレクトリ以降を記述すればOKです。 /Users/dev/flask/testapp/templates/testapp/index.html . . <div class="container"> <h1>index.html表示してます</h1> <img src="{{ url_for('static', filename='images/flask-logo.png') }}"> . . 表示されました。 url_forはリンクとか、ファイルのディレクトリ先を作成する関数。なので直に以下のURL http://127.0.0.1:5000/static/images/flask-logo.png アクセスすると普通に画像だけ表示されます。 http://127.0.0.1:5000/static/images/flask-logo.png こんなURLをFlaskで作ってHTMLに埋め込んでいるだけと言うことです。 2. FlaskでCSS適用 前回すでにbootstrap使っているので不要かもしれませんが、追加の方法を書きます。 手順は画像の追加方法とほぼ同じです。 まずcssファイルを配置するためのディレクトリを作成して、さらにCSSファイルをその中に作成します。 $ mkdir css $ touch css/style.css /Users/dev/flask/testapp/static/css/style.css h1 { color: blue; border-bottom: double 6px #87CEFA; } お試しなのでわかりやすいようにh1の色と線を変更してみました。 このCSSファイルを読み込めばいいだけです。 /Users/dev/flask/testapp/templates/layout.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"/> <title>テストWebアプリ</title> </head> 読み込んでいるのは {{ url_for('static', filename='css/style.css') }} 参照先をurl_for()で作成しているだけです。 前回共通で使用するテンプレートを作成しました。 今回のCSSは今後作成するどのページでも使用するようにしてみたいので、layout.htmlに読み込みを追加しました。 画像の時の違いはlinkタグにしてhrefに変えただけです。 ここら辺はPythonでなくHTMLの話なのでいまいちわからない方はHTMの話ですね。 cssを指定したh1がちゃんとダサくなりましたのでちゃんとスタイルが適用されていますね。 3. FlaskでJavaScriptを配置 これもurl_for()を使って該当ファイルを参照すればいいので同じです。 フォルダを作成して、JavaScriptの処理を記述するのはsample.jsにしました。 $ mkdir javascript $ touch javascript/sample.js JavaScriptの学習はメインでないので以下をコピペで良いと思います。 /Users/dev/flask/testapp/static/javascript/sample.js window.onload = function() { var changeColor = function() { var e = document.getElementById('test'); e.style.color = 'red'; console.log("書き換えテスト") } setTimeout(changeColor, 5000); } idが「test」の要素を5秒後に赤くしてるだけです。 追加するのは 読み込みの記述 id="test" idはJavaScriptの処理で今回は使っているので 読み込みはlayout.htmlで行っても良いですが、全てのページで処理が起こって欲しくはないのでindex.htmlに追加しました。 /Users/dev/flask/testapp/templates/testapp/index.html {% extends "layout.html" %} {% block content %} <div class="container"> <h1>index.html表示してます</h1> <img src="{{ url_for('static', filename='images/flask-logo.png') }}"> <p id="test">{{ my_dict.insert_something1 }}</p> <p>{{ my_dict.insert_something2 }}</p> ... </div> <script src="{{ url_for('static', filename='javascript/sample.js') }}"></script> {% endblock %} scriptタグを読むのはHTMLのルールと同じ。どこでもいいです。 今回は{% endblock %}の前だとbodyタグ終わり直前なので(layout.htmlも参照)ここで読み込むようにしました。 もしくは、layout.htmlの<head>の中でJSファイルを参照しても同じ結果は得られるはずです。 画面を確認してみましょう。 5秒後に文字が赤くなったらOKです。 まとめ 今回URLのリンク作るだけなので中身が薄い気がしますが、、、 簡単すぎるからか、公式のQuickstartにさらっとしか書いてなかったのでお役に立てればなと思います。 Pythonで作るFlaskアプリ記事一覧 内容 part1 簡単なページ作成 part2 HTMLテンプレート表示 part3 "画像" "CSS" "Javascript"実装← ココ part4 フォーム送信 予定 part5 データベースの値取得・更新 予定 ソースコード この章のコードは以下です。確認やコピペでどうぞ
- 投稿日:2021-12-30T15:20:40+09:00
Python3エンジニア認定基礎試験
意図 受験して合格できましたので、これから受験される方への一助になればと思い雑記を残します。 私のバックグラウンド ・受験前からPythonの経験は多少あり ・ただ使う場面が偏っていたので、一度体系的に勉強してみたかったので受験を決意 ・多言語の経験も多少あり ・職業プログラマではない ・飯を食っているのはインフラエンジニアとして ・仕事があるので、勉強に避けるのは土日メイン 受験当日:メモ用紙等は配布されません コードを追ったり、リストや文字列を数えたりするのにメモ用紙等があれば便利ですが、試験時にはそのようなものは配布されませんでしたし、持ち込みも不可でした。よって、問題はすべて目で追い、脳内ワーキングメモリで処理せねばなりません。勉強時に問題を解くときもメモ用紙等を使わずに回答できるようにしておきましょう。 模擬テストと本番の違い おそらく多くの方は、DIVE INTO CODE様と、PRIME STUDY様の無料模擬テストのお世話になると思います。そしてこれらは大変有用です。本番の問題形式としても参考になるのですが、本試験を受験してみての感想です。 一択問題だけではない 「正しいものを二つ選べ」という問題が出題されました。上記2つの試験ではそのような出題はありませんでしたので、本番では注意してください。試験時間が残っている限り、戻って見直したり回答しなおしたりすることが可能です。 難易度 私も、DIVE INTO CODE < 本試験 < PRIME STUDY (https://hirahira.blog/python3-basic-exam-method/) と同感です。しかし、当たり前ですがそのままずばりの問題が出るわけではありません。本試験の出題元がPythonチュートリアル第4版ですので、その対策である模擬試験も同じドキュメントを参照して作成されることから、ほぼ同じ問題もありました。くれぐれも深層学習で言うところの過学習wにならないように注意してください。問題覚えちゃって、模試の点数が上がってきても実力がついていることにはなりませんので。 勉強法 多くの方がWebで体験記を公開されているので、大変参考になると思います。多くの方と同意見で、私も「Pythonチュートリアル第4版」は最初に取り組むべき本ではないと思いますし、教科書として良書とも思えません。もちろん出題元なので外せませんし、言語の作者が書いたものとして価値はあります。しかし、いきなりこれを読むと面食らうのではないでしょうか。Webで読めるとは言っても、結局私も購入しましたが。 書籍 ・プログラミング経験者のためのPython最速入門 これであらましをつかみました。恥ずかしながら、自分で使うツールではエラー処理とかちゃんと書いていませんでしたので、勉強になりました。 ・Pythonチュートリアル第4版 文句を言っておいてなんですが、ここから出題されますので手元にあった方が良いと思います。前述した模擬試験の問題を解いていても、「こんなこと書いてあったっけ?」と思っても、絶対この本に載っていますw だけでこの本では勉強できないんです。うまく伝えられないんですが、この試験の勉強を続けていけばご理解いただけると思いますwww 演習 ・DIVE INTO CODE 受験は無料ですが、月額プランに入らないと、問題の解説を見ることができません。よって1か月だけ入会しました。問題の解説だけでなく技術に関する動画や資料も閲覧できますので、一ヵ月だけ入会して集中的に取り組んでみました。 ・PRIME STUDY 受験も、解説も無料です。メールで回答の正誤は送られてくるものの、問題の選択肢等は模擬試験実施中しか見られないため、回答中に気になることがあればメモしながら取り組むと良いと思います。 Amazonには問題集もあるようですが、私はこのWeb模擬試験のみ使用しました。 勉強期間 多少の経験があったので、純粋にこの試験のためのみに費やしたのは一ヵ月分の土日数時間だと思います。そもそも集中力がないのもあって、土日といっても数時間ずつくらいしか取り組めませんでした。その数時間の内容は、模擬試験やって、知らないこと理解できないことを調べて、コードをjupyterで動かしてみるといった感じです。 感想 個人的にプログラミング言語についてちょっとコンプレックスがありました。私はプログラマではないので、学生時代C言語でつまずいたこと、だましだましプログラムを書いてきたこと、体系的に勉強したことがなかったことと、使い方が偏っていたこと、いくら「やったことある」といったも証明するものがないことなどなどです。もちろん基礎試験でそれほど難易度が高いものではありませんが、ちょっとは自信になりました。これで「一応資格もっているよ」とは言えますので(^^;)。勉強していく中で知らないことも多々ありましたし、いままでうやむやにしていたこともちゃんと知識を整理する機会にもなりました。受験を検討されている方の参考になれば幸いです。
- 投稿日:2021-12-30T15:11:40+09:00
KaggleのTitanicチュートリアルをやってみた
やったこと kaggleの最初の一歩の課題にある、難破したタイタニック号の生存者を予測するモデルを作成して、コンペティションに参加するところまでを、Kaggleのチュートリアルのとおりやってみました。 コンペティション概要 タイタニック号の沈没は、歴史上最も悲惨な沈没事故のひとつで、1912年4月15日、処女航海中、「不沈船」と広く考えられていたRMSタイタニック号は氷山に衝突し沈没し、救命ボートが乗客全員分なく、乗客・乗員2224人のうち1502人が死亡した事件です。 生存には運の要素もあり、ある集団は他の集団よりも生き残る可能性が高かったようです。 乗客のデータ(名前、年齢、性別、社会経済階級など)を使用して、「どのような種類の人々が生き残る可能性が高いか」という質問に答える予測モデルを構築して、生存者予想精度が高いものがランク上位になります。 コンペティションへの参加 とりあえず https://www.kaggle.com/ でログインIDは取得はできている前提で進めます。 コンペティションへの参加 まずはコンペティションに参加します。 新しいウィンドウでTitanicコンペティションページを開き、「Join Competition」(コンペティションに参加する)ボタンをクリックします。 (「Submit Predictions」「予想を投稿する」ボタンが表示されている場合は、すでに大会に参加していますので、再度参加する必要はありません) コンペティションデータ コンペデータを見るには、コンペページの上部にある「Data」タブをクリックします。その後、下にスクロールすると、ファイルのリストがあります。 データには3つのファイルがあります。 (1) train.csv (2) test.csv (3) gender_submission.csvです。 (1) train.csvファイル train.csvには、乗客の一部(正確には891人)の詳細が含まれています。 画面の左側にあるファイル名をクリックするとすべてのデータが表示されます。 2列目の("Survived")「生存」列から、各乗客が生存しているかどうかを判断することができます。 "1 ":生存 "0 ":死亡 例えば、train.csvの最初の行の乗客Braund,Owen Harris氏は、「0」なので死亡したことになります。 (2) test.csvファイル train.csvで見つけたパターンを使って、test.csvにある他の乗客418人が助かったかどうかを予測します。 画面左のtest.csvをクリックすると、その中身を見ることができます。 test.csvには("Survived")「生存」列の列はありません。この情報は隠されており、この隠された値をいかにうまく予測するかがモデル作成の腕の見せ所となります。 (3) gender_submission.csvファイル このファイルは、仮に、女性乗客は全員生存し、男性乗客は全員死亡したとした場合の成果物提出のサンプルファイルです。 実際は作成したモデルから以下のように各乗客ID毎の生存、非生存のデータを作成して最終成果物として提出します。 test.csv の各乗客の IDである 「PassengerId」(パッセンジャーID) 行と Survived"(生存者)行に カラムに乗客が生存していると予測した行に "1" を、乗客が死亡したと予測した行 "0" を入れ、「submisson.csv」ファイルとして最終的にコンペティションへ提出します。 モデル構築 ノートブックの準備 まず最初に、Kaggle Notebookを作成します。 Kaggleノートブックを使えば、コンピュータに何もインストールすることなく、コードを書き始めることができます。 コンペティションページで「Code」(コード)タブから、「New Notebook」をクリックします。 画面左上のタイトル「notebookcc8261ca00)」は、「Titanic model」等好きな名前に変更できます。 最初のコードは自動で入力されていおり、numpay関数およびpandas関数のインポートとファイルを読み込めるようにディレクトリ設定をしてくれます。 矢印ボタンをクリックするか、[Shift]+[Return]で実行します。しばらく待つとファイルパスが表示されます。 データロード 以下の2行のコードを2番目のコードセルに入力して、訓練データをロードします。 train_data = pd.read_csv("/kaggle/input/titanic/train.csv") train_data.head() 以下の2行のコードを2番目のコードセルに入力して、テストデータをロードします。 test_data = pd.read_csv("/kaggle/input/titanic/test.csv") test_data.head() 男女別の生存率 最初に男女別の生存率を計算してみます。 men = train_data.loc[train_data.Sex == 'male']["Survived"] women = train_data.loc[train_data.Sex == 'female']["Survived"] rate_men = sum(men)/len(men) rate_women = sum(women)/len(women) print("% of men who survived:", rate_men) print("% of women who survived:", rate_women) 結果、 男性19% 女性74% の生存率となっており、基本女性の生存率のほうが高そうであり、性別は生存率に強い関係を持っていそうです。 これは性別のみであり、複数の行の関係を考慮するために機械学習をしてみます。 機械学習モデル ここでは、ランダムフォレストモデル(決定木)を利用してみます。 from sklearn.ensemble import RandomForestClassifier y = train_data["Survived"] features = ["Pclass", "Sex", "SibSp", "Parch"] X = pd.get_dummies(train_data[features]) X_test = pd.get_dummies(test_data[features]) model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=1) model.fit(X, y) predictions = model.predict(X_test) output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions}) output.to_csv('submission.csv', index=False) print("Your submission was successfully saved!") メッセージ (Your submission was successfully saved!) が出力されることを確認できたら、ノートブックの右上にあるSave Version(バージョンを保存する)ボタンをクリックして保存します。 結果の提出 Save Versionの右に表示されている番号をクリックして、Version履歴を表示させて、最新のバージョンの右側にある省略記号(...)をクリックし、「Open in Viewer」(ビューアで開く)を選択します。 画面右上の省略記号(...)から、「Submit to competition」をクリックして、Submitファイルが問題なければ、「Submit」ボタンをクリックして、結果を送信します。 結果、スコアとして77.511%との回答がKaggleから返ってきました。 Leaderboardに順位が表示されました!! 大体8000位弱くらいでしょうか。 これからモデルの改善をして順位を上げていく感じになります。
- 投稿日:2021-12-30T14:45:44+09:00
【Python】propertyとついでにclassを完全に理解する
はじめに propertyを使った書き方を丸暗記だけしてよくわからないままclassを使っている人が多いですね。元をたどればclassをなんとなく使っているからです。ということで、中で何が起きているのかを徹底的に解説します! 目次 そもそもclassとは __get__メソッドについて __set__メソッドについて propertyとは よく見るpropertyの使い方 そもそもclassとは propertyを理解するには、まずclassが何なのかを知る必要があります。 ほとんどの解説記事では、classは次のような構成で紹介されます。 class A: def __init__(self, value=0): """インスタンスを作成""" self.value = value def show(self): """値を表示""" print(self.value) これによりshow関数は謎の「メソッド」というものとして定義され、classの定義はメソッドの集合体として認識されがちです。しかしこれはいったん忘れてください。 名前空間としてのclass classの定義もあくまで1行(または1ブロック)ずつ実行しているにすぎません。次の例を見てください。 class A: a = 1 def print_value(x): print("value =", x) print_value(a) classの中であること以外、地の文と変わりません。これを実行すると、 value = 1 とちゃんとprintされます。違いは定義した変数aや関数print_valueにアクセスするときにAを介する必要があることです。 A.a # [Out]: 1 A.print_value # [Out]: <function __main__.A.print_value(x)> このおかげで、classの外でa = 10のように同じ名前の変数を新しく定義しても混同されません。「ただのa」と「クラスAのa」で区別できるので「名前空間を分ける」というふうに言われるわけです。 インスタンスの作成 上で定義したAは関数呼び出し可能です。 callable(A) # [Out]: True Aを関数呼び出ししたときの返り値は「Aのインスタンス」と呼ばれます。typeで型を確認するとAになっているので、型を定義したと解釈することもできます。 ins = A() ins # [Out]: <__main__.A at 0x...> type(ins) # [Out]: __main__.A Aのインスタンスは、ピリオド.を介してAの中で定義した変数・関数すべてにアクセス可能です。しかし、このとき、アクセス先の変数によって挙動が異なります。もう一度Aの定義に戻りましょう。 class A: a = 1 def print_value(x): print("value =", x) インスタンスからアクセスできるものはaとprint_valueです。前者は ins.a # [Out]: 1 となり、とても直観的ですが、後者は ins.print_value(0) # TypeError: print_value() takes 1 positional argument but 2 were given ins.print_value() # [Out]: value = <__main__.A object at 0x0000021CC6533DC0> からわかるように、引数が1つ減っている代わりに、1つめの引数xにins自身が渡されていることが分かります。このことはclassの基本として紹介されていますが、考えてみればPythonにおいては整数も文字列も関数もすべてobject型に属します。 isinstance(1, object) # [Out]: True isinstance(A.print_value, object) # [Out]: True なぜこのような違いが出てくるのでしょうか。 __get__メソッドについて classA内の変数に、Aとは全く別物である、Aのインスタンスinsからアクセスすることは未定義です。これがPythonでどう定義されているのかがポイントです。 AのインスタンスinsはどのようにAから変数をもらっているのか。結論から言うと デフォルトでは浅いコピーをもらう 変数に__get__メソッドがある場合は__get__メソッドを呼び出す の2パターンです。 1. デフォルトでは浅いコピーをもらう intは__get__メソッドを持ちません。 hasattr(0, "__get__") # [Out]: False したがって、ins.aとアクセスされたとき、 A.aをコピー(仮にa_copyとする)がつくられる。 ins.a = a_copyで渡される。 というようなことが起きます。つまり、ins.aを介して値を変更しようとしても、A.aに影響はありません。 ins = A() ins.a += 10 ins.a # [Out]: 11 A.a # [Out]: 1 ただし、これは浅いコピーです。listの中身は共有されます。 class B: x = [0] ins_b = B() ins_b.x # [Out]: [0] ins_b.x[0] = -1 ins_b.x # [Out]: [-1] B.x # [Out]: [-1] <-- 上書きされる! 2. 変数に__get__メソッドがある場合は__get__メソッドを呼び出す まずは__get__が何なのかを知る必要があります。最も大事な、関数を例に説明します。 (1) 関数の__get__メソッド Pythonでの関数はfunction型です。関数は、どこで定義されようとfunctionのインスタンスであり、__get__メソッドを持ちます。 def f(x): ... type(f) # [Out]: function hasattr(f, "__get__") # [Out]: True type(A.print_value) # [Out]: function hasattr(A.print_value, "__get__") # [Out]: True Python内部での関数の__get__メソッドはPython実装ではないですが、真似すれば次のようになっています。 class function: def __get__(self, obj, objtype=None): def method(*args, **kwargs): return self(obj, *args, **kwargs) # selfが関数自身であることに注意 return method ( これはいろいろな意味で完全ではありません。あくまで説明のためです) 少しわかりづらいですが、自身の1つめの引数のみobjにした新しい関数methodを作成して返しています。 functionクラスを無理やりtypeで取得してこれを実行してみましょう。足し算する関数とfunctionクラスを用意します。 def add(x, y): return x + y function = type(add) 整数1のaddメソッド1.addのようなものを用意します。 add_method = function.__get__(add, 1) add_methodはaddの1つめの引数に1が入った状態ですので、もう片方を指定すれば足し算が計算されます。 add_method(2) # [Out]: 3 (2) class内での__get__の挙動 以上を念頭に考えます。さきほどから使っているclassAとそのインスタンスを使います。 class A: a = 1 def print_value(x): print("value =", x) ins = A() 関数print_valueがclassAで定義され、それがAのインスタンスinsからins.print_valueとアクセスされたとき、次のような作業が行われます。 method = function.__get__(A.print_value, ins)によりメソッド関数methodが作成される。 ins.print_valueの返り値がそのままmethodになる。 つまり、print_valueをそのままコピーする代わりに、print_valueには__get__が定義されているので、__get__の返り値(すなわち1つめの引数が指定された関数)をコピーの代わりにしているということです。 (3) __get__を使ってみる よりシンプルに、"Hello!"を常にコピーとして渡してくる無能な型を作ってみましょう。 class Munou: def __get__(self, obj, objtype=None): return "Hello!" この無能オブジェクトをclass内で定義すれば、アクセスしたときに常に"Hello!"が得られます。 class B: m = Munou() ins = B() ins.m # [Out]: 'Hello!' 値へのアクセスが完全に制御できました。 __set__メソッドについて __get__は値を受け取るときに呼び出されました。値を渡す(代入する)ときはどうでしょうか。これも同じような構成になっています。 デフォルトではそのまま値を上書きする。 上書きしようとしている変数に__set__メソッドがある場合は__set__メソッドを呼び出す intには__set__メソッドがないので、普通に上書きされます。 class B: i = 0 ins = B() ins.i # [Out]: 0 ins.i = 1 ins.i # [Out]: 1 <-- 上書きされている 先ほどのMunouも__set__を定義していないので、新しい値を与えると上書きされます。 class B: m = Munou() ins = B() ins.m = 0 ins.m # [Out]: 0 <-- 上書きされている そこで、上書きしないように、エラーを返すように書き換えます。 class Munou: def __get__(self, obj, objtype=None): return "Hello!" def __set__(self, obj, value): raise AttributeError この無能オブジェクトを使うと、上書きins.m = 0の際にMunou.__set__(B.m, ins, 0)が呼び出されます。この例では、無能なうえに上書きが阻止されます。 class B: m = Munou() ins = B() ins.m = 0 # [Out]: AttributeError ins.m # [Out]: 'Hello!' propertyとは 以上のように__get__や__set__は値の取得・上書きを制御するのに使えることが分かりました。これをうまく使えば、たとえば 格納された値を使いやすい形で返してくれるように__get__を定義する。 値の更新の際、型や値をチェックしてから更新するように__set__を定義する。 値の更新を禁止するために__set__でエラーを吐くようにする。 のようなことができます。 しかし問題は、毎回新しいクラスを定義するのは面倒だということです。メインのクラスであるBとは別に、__get__と__set__を有したMunouクラスを定義しましたが、そもそもBにしか使わないのにわざわざMunouを別で用意するといった大掛かりな作業は避けたいです。 ここで出てくるのが、標準実装のpropertyクラスです。 propertyクラスの実装 propertyクラスはPythonで真似れば次のような実装になっています。 class property: def __init__(self, fget=None, fset=None): self.fget = fget # ゲッター関数(なくてもよい) self.fset = fset # セッター関数(なくてもよい) def getter(self, fget): """ゲッター関数を更新した新たなpropertyを返す""" return property(fget=fget, fset=self.fset) def __get__(self, obj, objtype=None): if self.fget is None: raise AttributeError("unreadable attribute") # ゲッター関数がなければエラー return self.fget(obj) # ゲッター関数を呼び出す def setter(self, fset): """セッター関数を更新した新たなpropertyを返す""" return property(fget=self.fget, fset=fset) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") # セッター関数がなければエラー return self.fset(obj, value) # セッター関数を呼び出す ( これもいろいろな意味で完全ではありません。あくまで説明のためです) つまり、propertyのインスタンスpropを何かのクラスに定義し、適宜propにゲッター関数やセッター関数を与えているとすると、インスタンスinsからpropにアクセスするとき、 ins.propでは、ゲッター関数があればそれを呼び出し、なければ書き込み専用ということでエラーを返す。 ins.prop = 0では、セッター関数があればそれを呼び出し、なければ読み取り専用ということでエラーを返す。 というルールに従うということです。したがって「__get__や__set__を持ったクラスを定義する」代わりに「ゲッター関数とセッター関数を定義する」ことでほぼ同じ仕組みが実現できることになります。 propertyの利用 それでは、自己紹介と、セットしようとした値の説明をするだけのpropertyを作ります。ゲッター関数とセッター関数を定義します。 def getter_func(self): print(f"I am {self}") def setter_func(self, value): print(f"This is {value}") これらをもちいてpropertyインスタンスを作成します。 prop = property(getter_func, setter_func) prop # [Out]: <property at 0x...> ゲッター関数とセッター関数を別々でセットするのであれば次のコードで同じpropが得られます。 prop0 = property(getter_func) prop = prop0.setter(setter_func) prop # [Out]: <property at 0x...> このpropertyのインスタンスpropをclassに渡します。 class C: prop = prop すると値のアクセス/代入をするたびに対応する関数が呼び出されます。 c = C() c.prop # [Out]: I am <__main__.C object at 0x...> c.prop = 0 # [Out]: This is 0 よく見るpropertyの使い方 さて、ようやくいつもの使い方が出てきます。ポイントは ゲッター関数・セッター関数はどうせクラス外部から使わないので、クラス内で定義する。 propertyのメソッド、__init__とsetterは単一の関数を引数にとれるので、できるだけデコレータを使う。 の2点です。 デコレータのとは 関数またはクラスを引数にとり、引数1つで動く関数は、デコレータ表記ができます。たとえばlistに関数を追加するとき、 def func(): return 0 function_list.append(func) と @function_list.append def func(): return 0 はほぼ等価です。後者ではfuncにはfunction_list.append(func)の返り値が格納されるので、funcがNoneになっているという違いだけです。 デコレータを使ったpropertyの定義 以上から、propertyの__init__, getter, setter関数はすべてデコレータ表記できます。したがって def getter_func(self): print(f"I am {self}") def setter_func(self, value): print(f"This is {value}") prop0 = property(getter_func) prop = prop0.setter(setter_func) と @property def getter_func(self): print(f"I am {self}") @getter_func.setter def setter_func(self, value): print(f"This is {value}") はほぼ等価です。そして、getter_funcはゲッター関数だけ定義された中間産物で不要なので、すべて同じ変数名propで問題ないです。 @property def prop(self): print(f"I am {self}") @prop.setter def prop(self, value): print(f"This is {value}") このように、はじめの方法で出てきた不要な変数名getter_func, setter_func, prop0を書かずにpropを定義できたので、デコレータを使った方法が良いとされています。これをクラス内でやればOKです。 class C: @property def prop(self): print(f"I am {self}") @prop.setter def prop(self, value): print(f"This is {value}") まとめ 以上のようにpropertyというのは、Pythonでのクラスとインスタンスのアクセス関係を利用して、読み取り専用・書き込み専用のような変数を可読性高く簡単に定義するためのものでした。この仕組みが分かっていれば無用なトラブルシューティングが避けられますね。 また、__get__/__set__はPythonで魔法のような(直観的で使いやすいが一見Pythonの範疇を超えているかのように動作する)クラスを作るときに非常に有用です。ぜひ使えるようにしたいメソッドです。
- 投稿日:2021-12-30T13:12:33+09:00
Pythonでソースコードファイルのディレクトリ位置を指定して外部ファイルをロードする方法
Pythonの開発の際、私はVisual Studio Code を使うことが多いのですが、その際、デバッグ実行しようとすると、実行時のカレントディレクトリの位置のせいで、外部ファイルが読み込めないということがたまに発生します。 (私の場合、特に、ワークスベース内にディレクトリ分けしてソースを作っているからですねー?) そういうことにならないために、ソースコード上に明示的に、ソースコードのディレクトリ上の位置を書きたいとこなのですが、ベタでフルパスで書くという方法はファイルの場所が変わったときや配布することを考えると望ましくはありません。なので、動的にその場所を取得する方法を紹介しておきます。 こんな感じで、テキストファイルを読み込んで出力するだけのプログラムを適当に書いてみます。 Visual Stukio Codeの実行ボタン(または、デバッグ実行ボタン)をクリックすると案の定、こんな感じで「FileNotFoundErrr(ファイルが存在しないことを示すエラー)」になります。 そりゃそうですよね。実行時のカレントディレクトリの場所に読み込むファイルがないためです。 そこで、ソースを↓のように書き換えるわけです。 testBase.py import os dirname = os.path.dirname(__file__) test_data = open(dirname + "/hoge.txt", "r") for i in test_data: print(i) test_data.close() これは何をしているのかというと、ソースコードファイルの場所を取得して、それを基準にファイルを指定するようにしているのです。 「__file__」は、ソースコードのファイルを示す予約後で、「os.path.dirname(str)」関数を使うことで、ソースコードファイルのあるディレクトリを取得するという意味になります。 これで、問題なく、ファイルが見つかってファイルの内容が表示できるわけですね。 めでたしめでたしー 【補足】 ついでに補足ですが、、 ↓のソースを実行してみると。。 test01.py import os # カレントディレクトを取得 print('getcwd: ', os.getcwd()) # ソースコードファイルのパスを取得 print('__file__: ', __file__) # ソースコードファイルのファイル名のみを取得 print('basename: ', os.path.basename(__file__)) # ソースコードファイルのディレクトリのみを取得 print('dirname: ', os.path.dirname(__file__)) 実行結果 getcwd: H:\Project\PythonTest __file__: h:\Project\PythonTest\Base\test01.py basename: test01.py dirname: h:\Project\PythonTest\Base 以上です。こういうのって割と便利なので、覚えておきましょー。 何かの参考にしていただければ幸いです。
- 投稿日:2021-12-30T12:51:58+09:00
【Python】Selenium ChromeDriverでディズニーレストランのキャン待ちツールを作ってみた
内容 TDRレストランの予約ツールです. レストランの予約はぴったり1ヶ月前の午前11時から可能になります. ただ,予約しようと思った時には既に空きがなくキャンセルによる空き待ちをする方がほとんど…
- 投稿日:2021-12-30T12:45:44+09:00
エンジニアとして一年と少し経ったので、やらかした事をまとめてみる。
やっぱり失敗しちゃいますよね? stackoverflow, Qiita記事のコピぺ エラーを解決できずに、数時間死にたくなる ゴミコードを書いてしまい、デバックで苦労する。などなど。 こんにちは!新米エンジニアのりゅうです。今日から、新年の抱負の一つである、エンジニアとしてのスキル、知名度をあげる。それを達成するためのto doとして、キータ記事の週一更新をやっていこうと思います(まだ、新年ではないのにフライングしている事はすみません笑) 記念すべき、第一投稿としましては、僕がエンジニアとして働いていた中での失敗点を列挙させていただきます。 技術的な失敗 1. チュートリアルやstackoverflow記事のコピぺ エンジニアなら一度は経験があると思う コピペ 問題点 + コードを説明できない。 + エラーが発生した時に、なぜエラーになったのかわからない。 + コピペしたので、コードの書き方、意味を憶えない。同じ課題に直面した時にまた調べないといけない。 改善点 + ライブリー、フレームワークを使用する時は一度公式のdocumentをしっかりと読む。 + エラーが発生した時にstackoverflowなどで解決策をしらべるのはいいが、なぜ、それがエラーになったのか、どのように解決したのかをしっかりと理解する。 2. 雑なエラーハンドリング、ログを残さない。 独自エラーの作成が一番めんどくさくないですか? 問題点 + ログを書かかないと、コードが長くなった時にどのコードの前後にエラーが発生して処理が止まったのかがわからない。 + エラーをしっかり書かないと、チーム開発の際に、発生したエラーが無視していいものなのか、そうでないのかがわからず無駄に時間をかけてしまう。 改善点 + 複雑or大事な処理を行う際はlogを残すようにする。 + 発生したエラーに対して、どのようなエラーなのかを簡単にざっくりと理解できるようなエラーメッセージを残す。 + Not implementedを使用する。 3. 型を指定しない、docsを書かない docsを書くのめんどくさいですよね。 問題点 + 型を指定せず、関数の引数を定義する。 + docsを書かかないで関数を定義すると、どのようなデータを入力して、何が帰って来るのかがわからないので、エラーになった時、解決に時間がかかる。 + 複雑な関数の場合、理解するのに時間がかかる。 改善点 + 常に型を指定する(Typescriptの使用など) + Docs generatorを使用して、短時間にわかりやすいdocsを書く。 精神的な失敗 1. 質問を怖がる 質問するのって勇気がいると思いませんか? 問題点 + エラーが発生した時など、質問するのを躊躇って、無駄に時間を消費してしまう。 改善点 + とにかく、具体的でわかりやすく、解決方を提示しやすい質問を作成して、質問をする相手が嫌がらないような質問をする。 + 勇気を出す。 2. 100点のものを最初から作ろうとする。 完璧を目指したいですよね...。 問題点 + 時間がかかってしまう。 + 自分の視点で完璧なものは他の視点から見た時に完璧でないことが多い。 改善点 + 70点のものができたら、一度feedbackをもらうようにする。 + 一度に完璧なものを作るのではなく、徐々に完璧にしていくようにする。 最後に 拙い文章ですが、以上で締めたいと思います。最後まで読んでいただきありがとうございました! 本当に僕個人の反省点のまとめになったのですが、僕の失敗が誰かのやく立つことを祈っています。