- 投稿日:2021-12-24T23:15:50+09:00
細かすぎて伝わらないpandasテクニック集
LIFULLアドベントカレンダーのトリは、クリスマスよりも明日の有馬記念が大事、そんな@kazuktymがお届けします。 パンダジスタへの道 機械学習によるデータの前処理など、大量のデータ操作を簡単に実現するためにpandasをよく利用します。なるべくならPythonプログラミングに頼らず、pandasのDataFrame上でデータ処理を完結したいとパンダジスタの誰もが思っていることでしょう。(パンダジスタという言葉は一般用語ではありませんので、用法・用量を守って正しくお使いください) 今回は、利用するケースがそうそう無いかもしれない、しかし知っているときっと助かるはず、という細かすぎて伝わらないpandasの活用例を紹介します。 今回使用するDataFrame import pandas as pd df = pd.DataFrame({ 'レース番号': [1, 1, 1, 1, 1, 2, 2, 2, 2], '馬名': ['アアアウィーク', 'イイイテイオー', 'ウウウブラック', 'エエエキャップ', 'オオオシャトル', 'カカカワンダー', 'キキキグルーヴ', 'クククブルボン', 'ケケケシャワー'], '生年月日': [20180311, 20180301, 20180221, 20180211, 20180201, 20180131, 20180121, 20180111, 20180101], '走破タイム': [2000, 1580, 2005, 1585, 1590, 2010, 1595, 2015, 2015], '着順': [4, 1, 5, 2, 3, 2, 1, 3, 4] }) レース番号 馬名 生年月日 走破タイム 着順 1 アアアウィーク 20180311 2000 4 1 イイイテイオー 20180301 1580 1 1 ウウウブラック 20180221 2005 5 1 エエエキャップ 20180211 1585 2 1 オオオシャトル 20180201 1590 3 2 カカカワンダー 20180131 2010 2 2 キキキグルーヴ 20180121 1595 1 2 クククブルボン 20180111 2015 3 2 ケケケシャワー 20180101 2015 4 競馬の予想モデルを作るイメージで、上記のDataFrameを使用していきます。 グループ化したデータをゴニョゴニョしたい時 pandasではgroupbyを活用する場面が何かと多いと思います。今回の例で言うと、レースごとにグループ化してデータを加工する際に重宝します。groupbyしたデータをもっと柔軟に操作できたらいいのにという場面によく出くわすので、そんな時に役立つ事例をまとめてみました。 グループごとにソート順で先頭n件抽出する 早くもちょっと何言ってるかわからないですが、実際にサンプルコードで見てみましょう。 df = df.sort_values('走破タイム', ascending=True) df.groupby('レース番号').head(3) 各レースごとに走破タイムの早い順に3頭抜き出したいケースがあったときに、上記のようにDataFrame全体をまずは走破タイムの昇順でソートして、groupbyの結果をheadして先頭n件取り出します。 レース番号 馬名 生年月日 走破タイム 着順 1 イイイテイオー 20180301 1580 1 1 エエエキャップ 20180211 1585 2 1 オオオシャトル 20180201 1590 3 2 キキキグルーヴ 20180121 1595 1 2 カカカワンダー 20180131 2010 2 2 クククブルボン 20180111 2015 3 ただしこの方法には一つ問題があります。指定したn件のデータがインデックス順にきっちり抽出されてしまうため、同じ走破タイムの馬が複数いた場合、拾えないレコードが存在します。(今回の例で言うとケケケシャワーが該当します) 上記の方法は、ソート対象の値が重複しないと分かっているケースで使用するとよいでしょう。重複するレコードも抽出したい場合は、以下の方法で実現することが可能です。 indexes = df.groupby('レース番号')['走破タイム'].nsmallest(3, keep='all').index.values df[df.index.isin(map(lambda x: x[1], indexes))] レース番号 馬名 生年月日 走破タイム 着順 1 イイイテイオー 20180301 1580 1 1 エエエキャップ 20180211 1585 2 1 オオオシャトル 20180201 1590 3 2 キキキグルーヴ 20180121 1595 1 2 カカカワンダー 20180131 2010 2 2 クククブルボン 20180111 2015 3 2 ケケケシャワー 20180101 2015 4 nsmallest関数のkeep='all'オプションを使うことで、重複したレコードを全て抽出できます。ただし、戻り値が走破タイムのSeriesになるので、インデックスを取り出して、後続処理で元のDataFrameと突合します。中々複雑ですね。 グループごとに条件にマッチする割合を算出する 続いてもちょっと何言ってるかわからないですが、実際にサンプルコードで見てみましょう。 df.groupby(['レース番号'])['走破タイム'].apply(lambda x: (x < 2000).sum() / x.count()).to_frame() レース番号 走破タイム(割合) 1 0.60 2 0.25 こちらは、レースごとに走破タイムが2000(競馬では2分00.0秒を表します)を切る馬の割合を算出しています。apply関数を使用して、groupbyした結果に独自の関数を適用しています。 グループにデータがn件以上存在しない場合は、DataFrameから除外する こちらもなかなかマニアックな処理ですね。 df.groupby(['レース番号']).filter(lambda d: len(d) >= 5) レース番号 馬名 生年月日 走破タイム 着順 1 アアアウィーク 20180311 2000 4 1 イイイテイオー 20180301 1580 1 1 ウウウブラック 20180221 2005 5 1 エエエキャップ 20180211 1585 2 1 オオオシャトル 20180201 1590 3 filter関数を使って、指定レコード数を満たすグループのみ抽出しています。 グループごとに特定カラムを区切り文字で結合する 例として、グループ内の馬名を連結します。 df.groupby('レース番号')['馬名'].apply('/'.join).to_frame() レース番号 馬名(連結文字列) 1 アアアウィーク/イイイテイオー/ウウウブラック/エエエキャップ/オオオシャトル 2 カカカワンダー/キキキグルーヴ/クククブルボン/ケケケシャワー CSVファイルからDataFrameを生成する際に、任意の関数で値を変換する CSVファイルを読み込んで、DataFrameを生成することは往々にしてあります。その読み込み処理と同時に任意の関数を実行して値を変換することができます。 data.csv レース番号,馬名,生年月日,走破タイム,着順 1,アアアウィーク,20180311,2000,4 1,イイイテイオー,20180301,1580,1 1,ウウウブラック,20180221,2005,5 1,エエエキャップ,20180211,1585,2 1,オオオシャトル,20180201,1590,3 2,カカカワンダー,20180131,2010,2 2,キキキグルーヴ,20180121,1595,1 2,クククブルボン,20180111,2015,3 2,ケケケシャワー,20180101,2015,4 # 競馬表記の走破タイムを実際の秒に変換 def cnv_second(val): min_to_sec = int(val[:1]) * 60 decimal_sec = int(val[1:]) / 10 return min_to_sec + decimal_sec df = pd.read_csv('data.csv', converters={'生年月日':lambda x : str(x), '走破タイム': cnv_second}) レース番号 馬名 生年月日 走破タイム 着順 1 アアアウィーク 20180311 120.0 4 1 イイイテイオー 20180301 118.0 1 1 ウウウブラック 20180221 120.5 5 1 エエエキャップ 20180211 118.5 2 1 オオオシャトル 20180201 119.0 3 2 カカカワンダー 20180131 121.0 2 2 キキキグルーヴ 20180121 119.5 1 2 クククブルボン 20180111 121.5 3 2 ケケケシャワー 20180101 121.5 4 read_csv関数のconvertersオプションを使用します。サンプルでは、生年月日を文字列型に変換し、走破タイムを実際の秒数に変換しています。lambdaを使った無名関数でも、実際に定義した関数のどちらでも使用可能です。 特定の条件にマッチするレコードにフラグ値(True)を追加する DataFrameで複雑な条件を扱いたい場合、先にフラグ用のカラムを作っておいた方が処理が楽になる場面もあります。フラグ用のカラムは以下のようにすると簡単に作れます。例では、走破タイムが2分未満かつ着順が3着以内のレコードにフラグ値(True)を追加します。 df['要チェックフラグ'] = False df.loc[(df['走破タイム'] < 2000) & (df['着順'] <= 3), '要チェックフラグ'] = True レース番号 馬名 生年月日 走破タイム 着順 要チェックフラグ 1 アアアウィーク 20180311 2000 4 False 1 イイイテイオー 20180301 1580 1 True 1 ウウウブラック 20180221 2005 5 False 1 エエエキャップ 20180211 1585 2 True 1 オオオシャトル 20180201 1590 3 True 2 カカカワンダー 20180131 2010 2 False 2 キキキグルーヴ 20180121 1595 1 True 2 クククブルボン 20180111 2015 3 False 2 ケケケシャワー 20180101 2015 4 False 特定カラムのソート順でDataFrameを指定の割合に分割する 最後までちょっと何言ってるかわからないですが、実際にサンプルコードで見てみましょう。 df = df.sort_values('生年月日', ascending=True) ratio = round(len(df) * 0.7) df_major = df[:ratio] # 7割 df_minor = df[ratio:] # 3割 レース番号 馬名 生年月日 走破タイム 着順 2 ケケケシャワー 20180101 2015 4 2 クククブルボン 20180111 2015 3 2 キキキグルーヴ 20180121 1595 1 2 カカカワンダー 20180131 2010 2 1 オオオシャトル 20180201 1590 3 1 エエエキャップ 20180211 1585 2 レース番号 馬名 生年月日 走破タイム 着順 1 ウウウブラック 20180221 2005 5 1 イイイテイオー 20180301 1580 1 1 アアアウィーク 20180311 2000 4 例では、生年月日順に7:3の割合でDataFrameを分割しています。機械学習では、学習データと評価データに分割して、モデル構築することが一般的ですが、時系列の概念がある場合は、時間を軸に上記の方法で分割すると簡単です。 さいごに 以上が現在の自分の引き出しにある、細かすぎて伝わらないpandasの活用例でした。私のように、データ加工処理をpandasで頑張って完結したい人の参考になれば幸いです。またノウハウが溜まったら第2弾を投稿したいと思います。
- 投稿日:2021-12-24T23:08:58+09:00
うぉ!Pythonで引数のデフォルト値をミュータブルな型にした結果
Pythonで初心者ネタを投稿します。 タイトル見てニヤニヤしながら結果を予想して来た皆様、ご想像の通りなのでそっと閉じてください。 Pythonにはミュータブル(変更可能)な型と、イミュータブル(変更不能)な型があります。 bool、int、float、strなどは、イミュータブルな型。 list、dict、setなどはミュータブルな型です。 以下のように、Pythonの引数にデフォルト値を指定したとします。 ショッピングカートの配列に商品を入れる関数です。 デフォルト値には空の配列、つまりミュータブルな型の値を指定しました。 def add_to_cart(product: str, *, cart: list = []) -> list: cart.append(product) return cart taro_shopping_cart = add_to_cart("Playstation5") print(f"太郎さんのカート :{taro_shopping_cart}") hanako_shopping_cart = add_to_cart("みかん") print(f"花子さんのカート :{hanako_shopping_cart}") 太郎さんはプレステ5をカートに入れ、花子さんはみかんをカートに入れました。 自然ですよね。 この関数を使った結果はどのようになるでしょう。 太郎さんのカート :['Playstation5'] 花子さんのカート :['Playstation5', 'みかん'] 花子さんのカートにプレステ5が入ってしまいました。 ワンクリックで決済してたら、卒倒しますね。 Pythonでは、引数のデフォルト値が評価されるのは、関数が定義された時だけです。 なので最初の定義でcartに空の配列が入ってからは、その空の配列がずっと使いまわされます。 解消方法としては、単純でデフォルト値にミュータブルな値を設定しないことです。 ここではNoneを指定しました。 def add_to_cart(product: str, *, cart: list = None) -> list: if cart is None: cart = [] cart.append(product) return cart taro_shopping_cart = add_to_cart("Playstation5") print(f"太郎さんのカート :{taro_shopping_cart}") hanako_shopping_cart = add_to_cart("みかん") print(f"花子さんのカート :{hanako_shopping_cart}") 実行結果 太郎さんのカート :['Playstation5'] 花子さんのカート :['みかん'] 理由は知っている人にとっては当たり前のことですし、PyCharm先生もちゃんとハイライトして"Default argument value is mutable"って注意してくれるので、ハマる人はたぶんいないと思います。 ただただ、adventカレンダーにちょっと参加したかっただけです。 そしてまだこれを知らなかった人が検索してこの記事を読んで理解してもらえたら嬉しいです。
- 投稿日:2021-12-24T23:03:31+09:00
LINE公式垢でボットとチャット両立させてみた!
はじめに こんにちは、42tokyo Advent Calendar 2021の24日目を担当する、42tokyoの在校生のtharaです。今回は、私が個人的に行っているTeenMakersというプロジェクトのメールフォームを撤廃し、公式LINEアカウントを導入するお話です。 TeenMakersは「中高生が社会人に取材をする」サービスで、42の方にも数名お世話になっています。大変ありがとうございます。TeenMakersのウェブサイトはこちら! LINE公式垢にはBotモードとチャットモードがあります。Botモードでは、Webhookなどが利用でき、イベントに対する処理のカスタマイズがかなり出来ます。チャットモードでは手動での返信 + 簡単な自動応答ができるみたいです。そして、現状その両立が出来ないという。。。 しかし、僕が42でいつもお世話になっているkkonishiさんがSlack APIを活用し、Webhookを用いたイベント処理と手動でのチャット対応を両立させてくれました!(お前じゃないんかいっていうツッコミは無しでお願いします笑) 完成品はこちら。(42でお世話になっている二人の方をNoにするという最悪のデモです。時間が無かったんです。許してください笑) レポジトリーはこちら QRコードはこちら (デプロイが完了したら上げます!少々お待ちください...泣) Botモードのオススメ機能と記事作成機能 今回Botモードでは2つの機能を実装しました! 記事作成機能 誰かと話した時にそれの振り返りをしたい思ったことはないですか?これは、その振り返りをLINEボットと会話をすることで簡単にできる機能です。 社会人おすすめ機能 TeenMakersには現在20人ほどの社会人にご登録頂いており、中高生はそのプロフィールを見て取材を申し込むという形になっています。今後もっとプロジェクトが大きくなるかもしれません!その時に、中高生が社会人のプロフィールを全て見て自分にマッチした人を探すのは大変なのではないか?ということで、いくつかの質問に答えるだけでオススメの社会人を教えてくれる機能です! どうやって会話するの...? LINEボットの簡単な仕組みとしては↓です。 友だち追加やメッセージの送信のようなイベントが発生すると、LINEプラットフォームからWebhook URL(ボットサーバー)にHTTPS POSTリクエストが送信されます。 (Messaging APIリファレンスより) このHTTPS POSTリクエストの種類に応じて、メッセージの返信やその他のアクションを行うサーバーを用意する形です。単体のメッセージに関して特定の返信をすることは簡単ですが、実際にやりたいことはこんな感じのフローチャートでした。 今回は、こちらの投稿を参考にさせて頂き、ユーザー毎にどの種類の会話なのか、またどのステージまで行っているのか保存することで会話を実現しました!ソースコードに関しても上記の投稿のものを少し変えて使わせて頂きました! # status.py class Status: def __init__(self): # 何番めの会話まで行ったか self.context = 0 # 会話の種類 self.type = None # 会話の種類 class Type(Enum): BN_CREATE = auto() BN_CREATE_TRACK1 = auto() BN_CREATE_TRACK2 = auto() BN_CREATE_TRACK3 = auto() BN_CREATE_TRACK5 = auto() SELF_REF = auto() CATCH_REC = auto() CONTACT = auto() # session.py class Session: # ユーザーIDとStatusの辞書 _status_map = dict() こちらを用いることで、1の「記事作成機能」に関しては完成出来ました。 推薦アルゴリズム こちらの記事によると推薦アルゴリズムにも色々な種類あるようです。協調フィルタリングなんてすごいワクワクしますが、今回は制約が甘いのもあり、単純に全探索です。別の良い方法をご存知の方がいらっしゃったら、是非教えてください! 事前に社会人の候補を全て持っておきます。TeenMakersのサイトはWordPressで作られているのですが、自動でREST APIが出来ているので、そちらから取得する形にしました。 各社会人毎にその人の職種や経験などからTagと言われるものがついています。事前にそのTagに基づいた質問を用意しておき、その質問への’Yes’ ‘No’の回答で選択肢を絞っていきます。 社会人の方で付いている全てのタグが’Yes’になった方がいれば、その方を推薦します。 これにも’Yes’ ‘No’で回答できるようにし、’Yes’であれば終了。’No’であれば、2に戻って質問を続けていきます。 Botモードで手動チャット機能を実装 今回、手動チャットで行いのは、マニュアルでのお問い合わせ対応です。 以下kkonishiさんの寄稿文です。(笑) LINEボットに送られてきたお問い合わせの内容をSlackのスレッドを使って管理したら楽なのでは?と考え、実装してみました。 まず、こちらの記事を参考にSlack API周り(Slack Botに対してのアクセス権限やトークンの取得など)、PythonのSlack SDKの導入など、SlackとPythonを連携させる準備を行いました。 この準備ができた時点で、Python → Slackにメッセージを送る処理はほぼできました。 次に、Slack → Pythonにメッセージを送る処理をこちらの記事 を参考に実装しました。 お問い合わせがあった時点でスレッドを作成し、運営側からの返信やユーザーからのお問い合わせ内容の送信などを一つのスレッドで管理できるようにしました。 簡単に処理の手順を説明します。 お問い合わせしたユーザーとSlackのスレッドを紐づけるためにContactクラスを用意する # contact.py class Contact: def __init__(self) -> None: self._thread_map = dict() self._user_id_map = dict() def register(self, user_id, thread_ts): self._set_thread(user_id, thread_ts) self._set_user(user_id, thread_ts) def _set_thread(self, user_id, thread_ts): self._thread_map[user_id] = thread_ts def _set_user(self, user_id, thread_ts): self._user_id_map[thread_ts] = user_id def get_thread(self, user_id): return self._thread_map.get(user_id) def get_user(self, thread_ts): return self._user_id_map.get(thread_ts) ユーザーが「お問い合わせ」と送信した際に、Slack Botが「 user さんからのお問い合わせがありました!」というメッセージをSlackのチャンネルに投稿する。同時に、このお問い合わせをスレッドとして作成するために、メッセージのthread_idとuser_idを紐づけておく。 # app.py con = contact.Contact() profile = line_bot_api.get_profile(user_id) res = slack.start_contact(profile.display_name) con.register(user_id, res['message']['ts']) お問い合わせの具体的な内容をuserが送信すると、手順1で作成したスレッドの返信にそのメッセージが送られる。 # app.py profile = line_bot_api.get_profile(user_id) slack.send_msg_to_thread(profile.display_name, text, con.get_thread(user_id)) 手順2で送られてきたスレッドの内容に対して、Slackのお問い合わせチャンネルに参加している運営が(チャンネルに参加している人なら誰でも可)返信する。 手順3で運営が返信した内容がLINEbotに送られ、ユーザー側に表示される。 # app.py # Slackからメッセージが送られてくるイベントをキャッチする @app.route('/', methods=["POST"]) def index(): data = request.data.decode('utf-8') data = json.loads(data) # for challenge of slack api if 'challenge' in data: token = str(data['challenge']) return Response(token, mimetype='text/plane') # for events which you added if 'event' in data: event = data['event'] app.logger.info(event) reply_contact(event) return 'OK' # LINEbotにSlackのメッセージを送る def reply_contact(event): if 'bot_id' not in event: thread_ts = event['thread_ts'] user_id = con.get_user(thread_ts) msg = event['text'] line_bot_api.push_message(user_id, TextSendMessage(text=msg)) ユーザーが「終了」と入力するとお問い合わせが終了する。 細かい実装についてはリポジトリを覗くか、僕に連絡ください。 LINE側からは、運営によるチャット対応は全てLINE botがやっているように見え、Slack側からは、ユーザーからのお問い合わせは全てSlack botがやっているように見える、っていう感じですね。 このように、LINEbot, Slack API, Pythonを組み合わせることによって、お問い合わせに対してはチャット対応、他の機能に関しては自動応答みたいに、LINEbotの2つのモードを切り替えずに両立することができました。(厳密に両立というには、ちょっと甘いかもですが。。。) 現実的に、担当者1人が解決できるケースは少ないと思いますし、Slackのスレッドにお問い合わせの履歴が残っていれば、似たようなお問い合わせが来た時にもすぐ過去のやりとりを引っ張ってこれたりできて便利かなぁ〜と思います。 LINEボットを自分で作ってみたい方向け 自分達が参考にしたサイトなどを読んで行った順にご紹介いたします。 【入門用】PythonによるLINEbot作り方 まずはここに書いてあることを全て順番に行いました。LINEボットのイメージが掴めます。 flask-kitchensink Repository LINE公式の実装例です。kitchensinkに「出来得る限りすべてのもの」という意味があるらしく、「こんなことも出来るんだー!」と思いながら、気になるところを写経していきました。 PythonでLINE BOT開発で2ターン以上の会話の作り方がわからない。 Pythonの実装例まであり、大変ありがたかったです。 Slack APIをPython SDKで使う SlackとPythonを連携させるための手順から実装例まで丁寧に解説してくださってます。 SlackからPythonサーバーにメッセージを送信する Slack→Pythonにメッセージを送るためのイベントフックの設定などわかりやすく解説してくださってます。 [LINEBOT] Herokuへデプロイ ③ deploy自体はこの記事を参考にさせて頂きました。 まとめ LINEボットはローカルから動かすのがDiscordボットなどに比べて少し面倒くさかったですが、普段みんなが慣れ親しんだアプリのUIを使えるで、プログラミング学習の教材として割と良いものかもしれません。 42では年中入学者を募集しています!気になる方はこちらをチェック! 明日は、hiroinさんが「tracerouteの出力を42生が調べたら」について書いてくれる予定ですので、そちらの記事もお楽しみに!
- 投稿日:2021-12-24T22:54:27+09:00
SvelteKit + FastAPI + vercel + heroku でやる気があれば誰でも簡単フルスタックエンジニア (後半戦)
はじめに 本稿は、 SvelteKit + FastAPI + vercel + heroku でやる気があれば誰でも簡単フルスタックエンジニア (前半戦) の続きです。 前稿でやったこと svelteKitで、フロントエンドを実装した 実装したフロントのソースを、vercelにあげてネットに公開した。 後半戦では、サーバーの実装に触れていきます。 (そして、毛色がわかったら、それをフロントに繋いてみよう !) 準備 サンプルをダウンロード こちらをクローンしてください! もしくは、上級者の方は、 tiangoloさん (fastapi作者のヒゲ親父) が公開している、Full-Stack用のプロジェクトテンプレートを使った方が、いいかもしれません! この記事では、サンプルを元に説明します。 ダウンロードしたら、まず、README.mdの指示に従って、ローカルの環境構築を行なってください!。 (サーバはまだ実行しなくていいです。$ uvicorn ....の部分) herokuに登録 前半戦での説明通り、herokuにサーバの実装をデプロイします。 ここで行うことは3つで、 herokuに登録 herokuで「アプリケーション」を作成 herokuで、PostgreSQLのデータベースを無料で貸与 です。 ↑から、アカウントを作成 & ログインからの、 ◆ Create new appを押して、 適当なアプリケーション名とサーバの置き場所を指定して、[Create app] これで、herokuの母体の準備はOK。 次に、Resource > Add-onの検索窓から、「Heroku Postgres」を探す。 > 決定 Hobbyプランが無料です。 登録すると、Heroku Postresのコンソールに行けるようになるので、 コンソール移動 > Setting > Database Credentials > View credential で、 DBへの接続情報をGETできます。ここのURIを、サンプルソースコードの、/config/db_config.pyの、「ここにURI入れて!」部分に入れてください。 注意として、URIの先頭はpostgres: ですが、今回使うソースでは、postgresq;:と書き換えてください。 from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker engine1 = create_engine( "postgresql://...残りはそのまんま..." ) pgadminのインストールと利用 DBの中身を覗くことは実は結構難儀します。そこで、pgadminを使って、可視化します。 この辺を参考にしつつ、pgadmin4をインストール Serverを右クリック > Create > Server すると、設定画面が出てくるので、先ほどHeroku Postgresで出てきたCredentialの情報を入れていきます。 Generalタブ Nameに、好きな名前を設定(pgadmin4上での、表示名になる) Connectionタブ Host name/address に、「Host」の記載を入力(ほとんどの場合、ec2-から始まるやつ) Portに、「Port」の記載を入力 (ほとんどの場合5432) Maintenance databaseに、「Database」の記載を入力 Usernameに、 「User」の記載を入力 Passwordに、「Password」の記載を入力。そしてセキュリティ意識高い系でもない僕たちのような人は、特別な理由がない限り、こんなクソ長いパスワード忘れるので下のチェックマークをONにして、入力を省略できるようにしとこう Saveを押すと、左メニューバーに作ったDB接続が表示されるので、プルダウンしていきます。 {? 作った名前} > Databases > {Maintenance databaseの名前までスクロール} > Schemas > Tables > 何も表示されないが正解 ちなみに、Databases移行に死ぬほどDBが並んでいるのは、Herokuが一つの接続に、複数のDBを登録して、その1つ1つをユーザに対して間借りする形でやってるからだと思われます。課金したら変わるのかな? ここで、DB関連で、注意事項をいくつか。 低料金プランでは、最大接続数が決まっています。Hobbyはたった20接続しか確保されません。 「間借り」しているので、ちょっと重たいです。 登録できるレコードは、Hobbyで上限10000レコードです。上限を超えると、メール通知ののちに、1週間程度の猶予期間が始まり、そのあとは、消えるかなんらかの対処がなされます。 次に、用意しましたサンプルデータを突っ込みます。 ./createtable.pyを実行すれば、あなたの用意したHeroku Postgres DBに勝手にサンプルデータを流し込みます。少量なので帯域とかは気にしなくて大丈夫。 python createtable.py or python3 createtable.py そうすると、DBにデータを入れられるので、 早速、pgadmin4で確認してみましょう。 Table > どれかのテーブル(3つ入っているはず) > 右クリック > View/Edit Data > All Rowsで、 ユーザらしきものが2つ出れば成功です。 最後に、README.mdにも記載しております、起動シーケンスを実行して、サーバをあなたのPCで立ててみましょう。 uvicorn main:app --reload を実行して、 INFO: Will watch for changes in these directories: ... INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: Started reloader process [27454] using watchgod INFO: Started server process [27456] INFO: Waiting for application startup. INFO: Application startup complete. Application startup complete が出たら、事前準備はこれにて終了です! Fast API実装 お待たせしました。インフラ整備は、さっきのでほとんど終わりです。 いよいよサーバの実装です。 流し込んだデータを見てもらえればわかる通り、サンプルでは、Twitterっぽいシステムを構築しようとしています。 FastAPIの魅力はなんといっても、その早さ (実装のシンプルさ) と速さ (処理速度的な) です。 ヒゲ作者の方が、「Flaskを意識して作った」と言うとおり、コードが直感的でわかりやすいです。 では、それらの機能を、実装とともにみていきます。 サンプルを見れば、初心者の方も実装のイメージがつくと思いますので、頑張ってみていきましょう。 リクエスト/レスポンスの実装 リクエスト/レスポンス は、いわばAPIの窓口です。 窓口をしっかりしないと、APIというのはどんどん使いづらくなっていきます。 その点、FastAPIはかなり楽にリクエストの窓口を定義できます。 /api/twitter_modoki/__init__.pyの、このコードをみていきましょう。 /api/twitter_modoki/__init__.py twitterModokiRouter = APIRouter() #一旦ここは気にしない # .(メソッド名) ('{パス}', response_model={レスポンスの形式}) @twitterModokiRouter.get('/tweet/list', response_model=List[TweetResponseModel]) def 全てのツイートを取得するAPI( # APIの関数名が、自動生成ドキュメントのAPIの説明文になります。 user_id: UUID = None, # クエリパラメータ。 型チェック(もしくは変換)・軽いバリデーションもしてくれる offset: int = 0, # クエリパラメータ, デフォルト値を設定すると自動的にOptionalになる。 limit: int = 100, # クエリパラメータ session: Session= Depends(get_session) # APIの開始時にget_sessionが呼び出され、終了時にはget_sessionのfinallyを実行する。 ): try: ....... このコードで、以下の機能が実装されました http://****/tweet/list に窓口開設 窓口ではuser_id、limit、offsetの3つのパラメータを任意で受け付けるし、型チェックや、軽いバリデーションなども行う。(user_idはUUID形式出ないとエラーを吐く。limit, offsetは、数値型になりうるならパラメータから勝手に数値型に変形する。) 3つのパラメータは、任意である。(= *** と書くと、その値がデフォルトになり、パラメータがOptional・任意になる) 窓口は、レスポンスの形式をTweetResponseModelの配列で返すように約束。 get_session関数(DBの接続を確保する関数、自作)を、レスポンスを返すまで取り回せるようにする。この例では、「APIが終わったら、DBの接続をきる」という実装がこれだけで済む。(詳しくは、get_sessionの実装を参照) どうでしょうか。 これらの実装が、数行で済んでいることがまず驚きである。 では、サーバを起動して、 uvicorn main:app --reload APIを呼んでみよう。 curl -X GET http://127.0.0.1:8000/twitter-modoki/tweet/list?user_id=12341234-1234-1234-1234-123412341234&offset=1&limit=10 or (↑これは、Talend API Tester. 便利だよ.) すると、こんなのが帰ってくる。 パラメータを指定しなければ、 curl -X GET http://127.0.0.1:8000/twitter-modoki/tweet/list こんなふうにリストが帰ってくる。 これらは、データを「取得」するAPI、たいていHTTPメソッドは「GET」で定義される。 では、「POST」の実装はどうだろう。 # api/twitter-modoki/__init__.py @twitterModokiRouter.post('/tweet') def ツイートを1つ登録するAPI( body: TweetRequestBody, # POSTメソッドの場合、このようにbodyに格納される予定のjsonオブジェクトの定義と結びつけると、 # 勝手に必要な情報を構造体として抽出してくれる session: Session= Depends(get_session) ): try: ----- # schema/request.py class TweetRequestBody(BaseModel): text: str = Field(.... user_id: UUID = Field(... reply_tweet_id: Optional[UUID] = Field(.... GETと同じく、引数部分に何やらものを書くと、POSTメソッドで頻繁に使われる「BODYパラメータ」を取得できる。 POSTメソッドは、以下のように、BODYに構造体を定義してリクエストを送出する。 通信経路を通る際は、BODYはただの文字列と化すので、サーバ側はその文字列を構造体に直すのに難儀するはずだったが、この実装は、TweetRequestBodyの定義にそう構造をBODYから勝手に作り出し、bodyに代入する。あれまー サンプルでは、GET, POSTメソッドを使ったパターンを紹介したが、基本的にリクエスト/レスポンスのすべての実装は、これだけで賄える。 ドキュメント生成 fastapiの真なる強みとして、ドキュメントの自動生成機能があります。 やるべきことをやっていれば、ドキュメントの生成自体は非常に簡単です。 /docs をつける。これだけ。 生成されたドキュメントの、/twitter-modoki/tweet/listの記述がこちら。 さっきの、リクエスト/レスポンスの実装をやっていれば、実務レベルにも耐えうる立派なドキュメントが完成します。これで残業しなくて済むね! これには、見た目以上にたくさんのメリットがあります。 色々言い換えながら説明するなら、こんな感じでしょうか↓ ドキュメントをわざわざ別で作らなくていい。 サーバーの実装ができれば、即座にドキュメントが生成される フロントの実装者は、サーバと同じドメインにアクセスすればドキュメントが手に入る ドキュメントと実装にズレが生じることがない!!!!! (神) デフォだと、swagger形式のドキュメントが生成されますが、 redocが好きな人は、/docs じゃなくて、 /redoc とすればそうなります。スゴイネー また、ドキュメント自動生成機能は、もちろんOFFにすることも可能です。 app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None) #オフにする ルーティング apiのルーティングが簡単な方が、実装にも幅が出るし、バージョニングも楽ちんです。 FastAPIの、APIRouter、include_router()は、本当に楽ちんです。 main.py from fastapi import FastAPI from api.twitter_modoki.v1 import twitterModokiRouter as v1 from api.twitter_modoki.v2 import twitterModokiRouter as v2 app = FastAPI() app.include_router(v1, prefix="/twitter-modoki", tags=['TwitterModoki']) app.include_router(v2, prefix="/twitter-modoki/v2", tags=['TwitterModoki2']) ちなみにmain.pyは、このサーバー実装のスタートポイントです。appというのが全ての母体で、appに紐付ける形で、routerというのをどんどん繋げていきます。 prefixの定義もできるので、APIをお手軽にグルーピングしたい場合は、 このサンプルみたく、バージョンでAPIを分けたい時にも役立ちます。 tagsは、自動生成ドキュメントのグループ名になります。 Dependenciesによる依存性注入 では、APIのv2バージョンで、v1よりセキュリティを強化したり、リクエストをしやすくしたりしましょう。依存性注入(DI)で。 DIってなに?って人も、多分実装を見れば言いたいことがわかります。 さて、v2では、簡単なセキュリティ要件に対応したようです。 フロントは、HTTPヘッダーに、user_idと、認証キー(authorization)が必ず入れる。 user_idがDBに存在するか、存在チェックをしないといけなくなった authorizationが違うなら、APIにアクセスできなくなった。 これらを、DIで実装しよう。 実は、ここまでにDIっぽい実装が一つ登場しています。 session: Session = Depends(get_session) 此奴です。この、Dependsというfastapiのモジュールが不可思議奇奇怪怪な存在で、DIの魔法に簡単に誘ってくれます。 /api/twitter-modoki/v2.py @twitterModokiRouter.get( '/tweet/list', response_model=List[TweetResponseModel], #これにより、2つの関数がAPI処理実行前に必ず行われるようになった。 dependencies=[Depends(required_header), Depends(required_authorization)] ) def 全てのツイートを取得するAPI( #user_idはHTTP Headerからとる。 # A = Header(...)で、ヘッダーからカラムAを勝手に探して取る。 user_id: UUID = Header(...), offset: int = 0, limit: int = 100, session: Session= Depends(get_session) ): try: ....... 次に、dependenciesに紐付けた2つの関数の実装をみてみよう。 /api/common/authorization.py from fastapi import Header, HTTPException from sqlalchemy.orm.session import Session from model.UserModel import UserModel from config.db_config import SessionLocal from uuid import UUID def required_authorization( authorization: str = Header(...) ): if authorization == 'fast-api-token-barebare': pass else: #キーが違うなら、リクエストをRejectする。 raise HTTPException(status_code=432, detail="Access invalid") def required_header( user_id: UUID = Header(...) ): session: Session = SessionLocal() try: #get()によって、該当するidをもつユーザが0つ、あるいは2つ以上取れた時、エラーを吐く。 session.query(UserModel).get(user_id) except: #ので、リクエストをRejectできる。 raise HTTPException(status_code=432, detail="Access invalid") finally: session.close() こんな感じ、単純に、要件にそうチェックを走らせている。 おそらく上級者の方は、「Header(...)ってこんなとこまで来ても中身引っ張れるんだ。。。」 と驚愕かもしれないが、できてしまうものはできてしまうのである。 これらが通れば、API関数の↓の部分 # A = Header(...)で、ヘッダーからカラムAを勝手に探して取る。 user_id: UUID = Header(...), でHeaderからuser_idをとってもOKという感じである。 Headerに必要情報を記載して送信。これで、v2の要件達成である。 そして、この構文なら、 dependencies=[Depends(required_header), Depends(required_authorization)] この記述を他のAPI関数にペチペチコピペするだけで、認証をかけたいAPIに同じ処理を実装することができるのです。 補足説明 初心者向けに、このサンプルが構成についての説明欄を設けました。 /api 窓口です。窓口の説明や機能は、おそらく今までに紹介した機能で十分だと思います。 /service /apiから、1:1対応で呼び出します。いわばAPIのメイン処理部分。 /repositoryと組み合わせて、DBから欲しい情報を整理して、レスポンスの型に沿ったデータを作り出します。 /model サーバでいうModelとは、データベースに入ってるテーブルの定義の写し鏡です。 SQLAlchemyというORMマッパ(写し鏡をやってくれるライブラリ)を使っているので、ORMマッパのルール通りにModelを実装しています。 /schema リクエスト、レスポンスの型を定義しています。/apiでも頻繁に使っているので、おなじみかと。 /repository /DBにやらせたい操作を、汎用性持たせつつ書いてます。 /serviceから共通パーツを切り分けた・と捉えてもいいでしょう。 /config 各種設定。 やってみよう追加課題 フォロー・フォロワーを実装しよう /tweet/listが、フォロワーの内容しか出てこないように実装しよう Twitter Modoki アプリのフロントを実装して、結合してみよう。 OAuth2を使って、ログインの実装をしよう デプロイ さて、最後の仕事です。実装をデプロイしましょう。 フロントエンドの時よろしく、自分のリポジトリにソースをあげたら、herokuのDeployメニューから、 リポジトリを検索してConnect あとはよしなにDeployの設定を行います。 とりあえずAutomatic deployの設定をして、 初回だけは、Manual deployを実行。 Procfile (heroku用のコマンドソース)に、デプロイに必要な定義をしておいたので、あとはデプロイを待つだけ。 終わったら、 Setting > ちょっとスクロール >Domains に、ドメインが貼られてます。 http://localhost:8000/の代わりです!!!!!! やったぜ!!!!! 最後に いかがでしたでしょうか! sveltekit + fastapi 、2つのフレームワークの強力さを知らせることはもちろん、昨今のデプロイサービスは恐ろしいほど簡単に世界に配信をかけられてしまうので戦々恐々です。 ==================== 2021年、エンジニアを志す若者にとって、すごくいい時代です。 膨大なソースコード資産、俗にいう「巨人」の「肩の上」に成り立つツールで、 こんなにも快適で、簡単で、実現できる物事の幅も広い開発体験ができるのですから。 そして何より、簡単なフレームワークというのは、HTTP/TCPや、ブラウザや、その他ベースの基幹技術を理解する上での補助剤でもあります。 なので、誰に罵倒されようと、簡単で、楽で、面白くて、いいのです。 遠慮なく巨人の肩の上で、やりたいことやっちまいましょう。 最後に、ありがとうおまえたち!
- 投稿日:2021-12-24T22:38:12+09:00
政府統計e-Stat APIを使ってデータ分析しましょう
はじめに 自分は人事給与ERP製品を開発する会社で、統計帳票の開発を担当しまいます。 統計帳票というのは、毎月勤労統計表や賃金台帳、公務員調査表など、全国調査にあってはその全国的変動を毎月、毎年明らかにすることを、地方調査にあってはその都道府県別の変動を毎月、毎年明らかにすることを目的とした調査です。 日々大量なデータを見る私は、これらのデータを分析することで、社員の退職や昇給などの原因がわかるではないかを気づきました。 ビッグデータ活用が近年話題になり、国や民間企業等が提供している主要な統計データを分析し、提供しているWebサービスもあります。 https://dashboard.e-stat.go.jp/ ただ、分析した内容と画像がややシンプルであり、データをもっと深堀りできるではないかを考えています。 もし、これらのデータを手には入れたら。。。と思いつつ、e-State APIがあることに気づきました。 e-State APIというのは、政府統計の総合窓口(e-Stat)で提供している統計データを機械判読可能な形式で取得できるAPI機能です。今回e-StateのAPIを使って簡単なデータ分析をやってみます。 https://www.e-stat.go.jp/api/ e-stateのAPIの使い方 基本以下の流れです。 アプリケーションID取得→統計対象データの選択とAPIクエリー取得 アプリケーションID取得 API経由でデータを取得する際に、アプリケーションIDというパラメータを指定しないとデータが取得できません。ですのでユーザー登録して、マイページで発行ボタンをクリックすると、アプリケーションIDを取得します。 https://www.e-stat.go.jp/mypage/user/preregister アプリケーションIDは以下のようなハッシュコードです。 "appId" : "6tt320sfjagagiadkghijgjngdkgoda" 統計対象データの選択とAPIクエリー取得 指定した統計表ID又はデータセットIDに対応する統計データ(数値データ)を取得します。 e-State APIから提供できるデータは以下のようになります。国勢調査や人口推計、家計調査などいろいろあります! https://www.e-stat.go.jp/stat-search/database?page=1&layout=normal 統計したい調査を開いて、APIボタンをクリックします。 APIボタンをクリックすると、データ取得の時に使われるAPIリクエストのURLが表示されます。 クエリーは三つの形式があり、今回JSON形式でデータを取得します。 JSONを解析 Pythonのrequests.get(url, params)でデータを取得します。 urlはAPIクエリーの一番左側から?までのurlです。 paramsに、ユーザー登録時にもらったアプリケーションIDとAPIクエリーに書いたstatsDataIdを記入します。 statsDataIdは統計表IDです、異なる統計表のIDは違います。 今回JSON形式のクエリーをもらいましたので、json.loads()でデータを取得します。 簡単ですね!! numpyでデータを分析する 今回の毎月勤労統計表のデータは8個統計項目がありますので、それぞれ取得して2年度の平均値をnumpyで計算します。 matplotlibで可視化する 最後に ふたんデータ分析をしたく、データがない場合、e-State APIを活用すれば、いろいろなデータ分析ができ、楽しいですね。
- 投稿日:2021-12-24T22:35:03+09:00
エンジニアに転向して1年で開発チームのリーダーになるまでに勉強したことをまとめる
これはなに? 自分は2020年8月ごろにプロダクトマネージャーからエンジニアに転向し、この1年半でバックエンド、フロントエンド、インフラなど色々やっているうちに気付いたらいちチームのリーダーを任されるまでになりました。なのでこの記事ではその間にどんなことを勉強したのかをまとめておこうと思います。 エンジニアになったばかりの人やこれからなる人の一つの参考になれば幸いです。 担当プロダクトの技術スタック バックエンド:Python, flask フロントエンド:JavaScript, Vue.js DB:MySQL インフラ:AWS 勉強したこと メインとしてバックエンドのapi開発をやっていたため、フロントエンドは薄めになっています。 とりあえず入門 Progate 受講コース:Git/Command Line/SQL/Python/HTML&CSS/JavaScript とりあえず入門するのに手軽でよかったと思います。 キタミ式イラストIT塾 ITパスポート とりあえず基礎中の基礎をさらうために勉強しました。資格自体にめっちゃ意味があるとは思ってないですが、資格勉強をすることで一旦広く知識が得られるのはとても有益だと思っています。 コンピュータの基礎 コンピュータはなぜ動くのか~知っておきたいハードウエア&ソフトウエアの基礎知識 プログラムはなぜ動くのか 第3版 知っておきたいプログラミングの基礎知識 この2つはプログラミングの勉強をするにあたって、まず最初に読みました。いきなり言語の勉強をし始めるより基礎をちゃんとさらったことは良かったと思っています。 Python 独学プログラマー Python言語の基本から仕事のやり方まで マジでこれから入ってよかった。オライリーから入ったら死んでたと思う Pythonチュートリアル 入門Python3 ガチ初学者には向いてないです。一回プログラミングを通った人がPythonを学ぶのには○ flaskアプリの自作 flaskに関しては恥ずかしながら体系的なインプットをする前に、実務で教えてもらいながら見様見真似で少しずつ理解していきました。(というか1ヶ月くらいで実務に放り込まれたので時間がなかった) 代わりと言ってはなんですが、少し時間が経った頃にこの記事や関連記事を参考に、自分でflaskを使ったslackアプリをつくって遊んでいました。結構勉強になったと思います。 SQL SQL 第2版 ゼロからはじめるデータベース操作 select文は死ぬほど書いたことがあったのですが、それ以外は全くだったので基本をさらうために。 HTML/CSS これからWebを始める人のHTML&CSS,JavaScriptのきほん この辺はある程度はわかっていたのですが、改めてちゃんとエンジニアになるにあたって基本は押さえておこうと思って一応。 JavaScript Udemy 【JS】ガチで学びたい人のためのJavaScriptメカニズム JS学ぶのにいい本ないですか?と聞いたら、これが一番わかりやすくておすすめ、と教えてもらったのがこちら。 結構基礎から非同期処理とかまで一通り扱ってくれて結構助かりました。 Vue.js Vue.js入門 基礎から実践アプリケーション開発まで とりあえず一番基本っぽいものをやりました。 プログラミング基礎 リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック 括りかたわからなかったけど、言わずもがなの必読本。一番お世話になってる気がする。 Linux linux標準教科書 Linuxコマンドポケットリファレンス ポケットリファレンスは、とりあえず通しでパラパラっと読んで、辞書的に使ってます。 最初はMacOSとLinuxの違いが分かってなかったので、オプションの違いとかで結構戸惑いました。 Git GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus) 実際にチームで開発する中での細かいルールとかは実務で教えてもらいながらにはなりますが、Gitに関する最低限の知識として絶対読むべきなやつ。 セキュリティ 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践 めちゃくちゃ分厚くて心折れかけましたが、セキュリティに関する最低限の知識としてがんばりました。 オブジェクト指向/デザインパターン オブジェクト指向でなぜつくるのか 第3版 知っておきたいOOP、設計、アジャイル開発の基礎知識 デザインパターンとともに学ぶオブジェクト指向のこころ オブジェクト指向型の言語を使うにあたって最低限理解しておく必要があるだろうということで。 デザインパターンは正直ある程度実装に慣れた頃に読んで初めて意味があるかな、という感じです。 アルゴリズム プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問 珠玉のプログラミング 本質を見抜いたアルゴリズムとデータ構造 実務でめちゃくちゃアルゴリズムに気を使わないといけないようなことがあるか、と言われたらそんなにないんですが、一通りやっておくことで普段の実装でも計算量とかを意識できるようになると思います。 AWS (模擬問題付き)改訂新版 徹底攻略 AWS認定 ソリューションアーキテクト − アソシエイト教科書[SAA-C02]対応 Udemy これだけでOK! AWS 認定ソリューションアーキテクト – アソシエイト試験突破講座(SAA-C02試験対応版) Udemy AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問) 実務でちょいちょいawsに触れる機会があったのですが、一回体系的にインプットしたいな、と思ってとりあえず資格を目標に勉強しました。Udemyの講座が、試験対策だけでなく一通りハンズオン形式で触らせてくれたのでとてもありがたかったです。 一通り最低限の知識を入れたことで、その後の公式ドキュメントとかも読みやすくなった気がします。 ネットワーク 3分間ネットワーキング 全然3分じゃ無理ですが、とりあえずネットワークについて最低限の知識を抑えるのにはおすすめだと思います REST Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus) API設計で必要な考え方の基本が抑えられます。 テスト テスト駆動開発 この本読んでからテストを書かないと気持ち悪くなりました。テスタブルなコードを意識すると、自然とコードがきれいに分割されていく気がするので早いうちに読んでおいて正解だったなと思います。 開発プロセス LeanとDevOpsの科学[Accelerate] テクノロジーの戦略的活用が組織変革を加速する (impress top gear) アジャイルな見積りと計画づくり ~価値あるソフトウェアを育てる概念と技法 SCRUM BOOT CAMP THE BOOK【増補改訂版】 スクラムチームではじめるアジャイル開発 チーム開発をより効率的に進めるために、DevOpsやアジャイル開発に関するインプットもしました。 チーム作り エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで How Google Works(ハウ・グーグル・ワークス) 私たちの働き方とマネジメント Team Geek ―Googleのギークたちはいかにしてチームを作るのか チームリーダーを任されるということで、改めて開発組織をどう作っていくか、というのを考えるために読みました。 プロダクトマネジメント プロダクトマネジメントのすべて PMやデザイナーも含め、チームで一つのプロダクトを作るために目線を揃えよう、ということでチームみんなで読みました。とてもよかった。 おわりに こうやって見てみると、改めてとりあえず広く基本を抑えた感じでやってきたなと思いました。 来年は求められる役回り的にも、もう少しDBの深いところやミドルウェア関連の知識とかもつけていきたいなと思っています。 またここにはわざわざ書いてないですが、各種公式ドキュメントには大変お世話になりました。 ↓の記事にも書きましたが、日々色々とググる中でちゃんと公式に立ち戻ってインプットしていたことがとても良かったなと思います。
- 投稿日:2021-12-24T22:03:38+09:00
Pandas 入門
matplotlib に引き続き、python のライブラリPandasについて学んでいきたいと思います。 Pandas とは pythonのライブラリで、データ分析用に表形式のデータや行列を扱う事が可能です。 集計処理、計算処理をPandasにて置き換えることが出来ます。 pip 等でインストールも可能ですが、Anacondaに含まれていますので今回はそちらを使用します。 結果をすぐに確認するため、今回も Jupyter Notebook を使用します。 Pandas用語 いくつか専門の用語がありますので、先に理解をします。 Series データ形式の一つで一次元の配列から単一の列から成す表 DataFrame 行列からなる表形式の配列データ。Pandas の中心となるデータ形式です。 index SeriesやDataFrameの行データに付与できるラベルです。 columns DataFrameの列データに付与できるラベルです。 integer-location DataFrame は行(列)形式のため、番号指定でデータにアクセス出来ます。 この方式をinteger-locationと呼びます。 よく使用されるpandasのデータタイプ pandasで使用されるデータタイプとして、標準にint以外にNumpyを使用したfloatや、文字列等が挙げられます。 DataType Description bool 真理値 np.int64 64bit整数(intと同等) np.float64 64bit浮動小数点(floatと同等) pd.StringDtype() pandasの文字列 object pythonのオブジェクト Series Series はpd.Series()メソッドにて定義します。 この例では、a,b,c,d,eという列データに対して 1,2,3,4,5 というindexが付与されます。 sample1.py import pandas as pd s = pd.Series(["a", "b", "c","d","e"], index=["1", "2", "3","4","5"]) print(s) 特定のデータの表示を行う場合、indexを指定して表示する事も可能です。 データタイプを表示するにはdtypeを指定します。 sample2.py import pandas as pd s1 = pd.Series([1, 2, 3], index=["a", "b", "c"]) # s1のindex=aの列データを表示 print(s1.a) s2 = pd.Series([1, 2, 3], index=["a", "b", "c"], dtype=int) # s2のデータタイプを表示 print(s2.dtype) DataFrame DataFrame は表形式データで、Seriesのindex以外にcolumnsという列データにラベルを付与します。pd.DataFrame()メソッドを呼び出して表形式のデータを生成します。 リストの場合、pd.DataFrame(2次元配列データ, columns=columnsのリスト, index=indexのリスト) が基本になります。 sample3.py import pandas as pd df = pd.DataFrame([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], columns=['col1', 'col2'], index=["a", "b", "c", "d", "e"]) print(df) 辞書の場合、data = { columns1 : データ配列 ..... , columns2 : データ配列 .... } pd.DataFrame(data , index=indexのリスト) が基本になります。 sample4.py import pandas as pd data = {'col1': [1, 3, 5, 7, 9], 'col2': [2, 4, 6 ,8,10]} df = pd.DataFrame(data, index=["a", "b", "c","d","e"]) print(df) ファイルの読み書き CSVからデータを読み込む場合 pd.read_csv()メソッドを使用します。 pd.read_csv(ファイルパス) が基本になりますが、index のラベルは任意で割り振りされます。 index_col を指定する事で、対象列をindexとして割り振る事も可能です。 sample5.py import pandas as pd df= pd.read_csv("C:/Users/user-name/data.csv",index_col="col1") print(df) 定義された DataFrameをCSVへ書き込む場合、df.to_csv()を使用します。 df.to_csv()が基本になりますが、index=Falseを指定する事で、indexを書き込みしない指定が可能です。 sample6.py import pandas as pd df = pd.DataFrame([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], columns=['col1', 'col2'], index=["a", "b", "c", "d", "e"]) df.to_csv("C:/Users/user-name/data2.csv") data2.csv の出力結果 data3.csv(index=False)の出力結果 演算・統計 DataFrameで同士では、カラムが一致している場合に四則演算を受け付けます。 四則演算に用いる 演算子は +,-,*,/ となります。 sample7.py import pandas as pd df1 = pd.DataFrame([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], columns=['col1', 'col2'], index=["a", "b", "c", "d", "e"]) df2 = pd.DataFrame([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], columns=['col1', 'col2'], index=["a", "b", "c", "d", "e"]) sum =df1 + df2 print(sum) 統計処理を行う場合、以下のメソッドを使用します。 Method Description df.count() データ件数 df.mean() 平均 df.max() 最大値 df.min() 最小値 df.var() 分散値 df.describe() 一括取得 df.sample() ランダムに行の値を取得 sample8.py import pandas as pd df = pd.DataFrame([[1,2], [3,4], [5,6], [7,8], [9,10]], columns=['x','y']) # データ件数の取得 c = df.count() print(c) 置換・ソート・集計 データの置換を行う場合、df.replace() メソッドを使用します。 df.replace(置換したいデータ,置換後の値) を指定します。 sample9.py import pandas as pd df1 = pd.DataFrame([["yuko","tomohisa"], ["eriko","yosure"]], columns=['Women', 'Men'], index=["a", "b"]) print(df1) # yosure を yosuke へ置換 df2 = df.replace('yosure', 'yosuke') print(df2) ソートは、df.sort_values()メソッドを使用します。 df.sort_values('ソートするcolumns',ascending=True[昇順]/False[降順])が基本になります。 複数の columnsを指定する事は可能ですが、先頭にソートしたindexが優先されます。 sample10.py import pandas as pd df = pd.DataFrame([[5, 4], [2, 6], [7, 8], [1, 10], [3, 9]], columns=['col1', 'col2'], index=["a", "b", "c", "d", "e"]) print(df) df2 = df.sort_values(['col1'], ascending=[True]) print(df2) 集計は df.groupby()メソッドを使用します。 Method Value df.groupby([columns]).max() 最大値 df.groupby([columns]).min() 最小値 df.groupby([columns]).var() 分散値 df.groupby([columns]).sum() 合計値 df.groupby([columns]).mean() 平均値 df.groupby([columns]).std() 偏差値 以下は、index=team a,b,c に対してheight/weight のデータが入っています。これらteam a,b,c単位に各集計処理を実施します。 sample10.py import pandas as pd team = ["a", "b", "c", "b", "c", "a"] height = [181, 180, 171, 167, 188, 159] weight = [72, 82, 70, 67, 90, 59] data = {'team': team, 'height': height, 'weight': weight} df = pd.DataFrame(data) m = df.groupby("team").max() print(m) フィルター フィルターはindexと同じサイズのbool型のシーケンスを定義します。 True であるリストのみ表示されます。 以下の例では、0行目、2行目、4行目 が表示されます。 sample11.py import pandas as pd data = {'A': [1, 2, 3, 4 ,5], 'B': ["a", "b", "c", "d", "e"]} df = pd.DataFrame(data) condition = [True, False, True, False,True] print(df[condition]) Pandas は他にも多くのメソッドが存在するため一括りは紹介できそうにありませんでした・・・・。 次回は pandas + matplotlib を組み合わせた記事を書こうと思います。 以上です。
- 投稿日:2021-12-24T21:51:10+09:00
リッチなLiDARでスキャンするボッチな世界 ~Velodyne VLS-128点群処理事始め~
この記事は、NTTドコモ R&D Advent Calendar 2021 25日目の記事です。 今年もNTTドコモ R&D Advent Calendarにお付き合いいただき、ありがとうございます。いよいよ12/25ということで、最後の記事になります。 はじめまして(orお久しぶりです)、ドコモの北出と申します。 昨年は都会の車窓からフォトグラメトリ 〜LiDAR無しスマホでもできる!3Dスキャン〜 - Qiitaというタイトルで、LiDARを使わないことをウリにした記事を書かせていただきましたが、今年は完全に手のひらを返してLiDARフル活用で記事を書きたいと思います。 こんな感じの(点群的には密だけど人口密度としては疎な)3Dモデルを作っていきます。 点群の色合いでクリスマス感を出してみました(ちょっと無理があるか)。 何をする記事? とっても高級なLiDARを使って自社ビル(横須賀のR&Dセンタ)をスキャンします。 得られる(といいな)情報としてはおおよそ下記の通りです。 Velodyne LiDAR(VLS-128)の特徴 Open3D 0.14のコンパイル方法 Open3D+GPUを使ったMulti-Scale ICPの動作 LiDARデータ統合による3Dモデル作成 ちなみに、VLS-128は個人で購入するにはなかなか厳しい価格感ですが、宝くじでも当たればお家に一台欲しいなぁと思いました。 VLS-128とは? VLS-128は、Velodyne社が開発した回転式LiDARのうち、最高級のモデルになります。Alpha Primeとも呼ばれます。この別名は、設定ファイルを探す際に地味に重要になってきます。 回転式のLiDARでは、垂直方向に16~128本のレーザー光を発射し、LiDARに戻ってくるまでの時間を測定することで三次元距離測定を実現します(ToF方式)。 垂直レーザーの本数は、モデルによって異なりますが、今回使うLiDAR「VLS-128」は名前の通り128本の垂直レーザを回転させ、スキャンを行います。 おおよそのスペックは下記の通りです。 レーザー+検出器:128本(垂直) FOV:水平360度、垂直+15度~-25度 測定(回転)周期:5〜10Hz 最大距離:300m 日本では、株式会社アルゴ様より購入ができます。 点群データの収集 前項で紹介したVLS-128を使って、実空間をスキャンしていきます。 今回は、ドコモR&Dセンタの端から端まで歩いてスキャンすることにしました。 横須賀にある(のどかな?)拠点です。 機材 利用するLiDARに比べて、突然低コストな感じになります。 カインズで買ったコンテナ台車に下記のような物品を詰め込んで、手押し走行を行いました。不安定でヒヤヒヤしました。 VLS-128(with 三脚) Anker PowerHouse II 400 Macbook Pro(M1) VLS-128はには100V電源がついてきましたので、100V出力可能なバッテリーを使っています。 データの保存PCとしてMBPを用いました。VLS-128とはイーサネットケーブル直結です。 ちなみに、VLS-128は車載を前提とした作りになっているようで、裏面にはM8のネジ穴が4つあります。今回はこれを利用して、1/4インチナットマウンターを3Dプリンタで自作して無理やり三脚にマウントしました。 VeloViewを使ったキャプチャ Velodyneを使ったデータ収集は色々な方法があると思いますが、今回は可視化と保存含めて最もお手軽と思われるVeloViewを使うことにしました。 VeloView公式サイトより、4.1以降のバージョンをダウンロードしてきて利用します。macOS12,Windows10,Ubuntuそれぞれpcap形式でのキャプチャが動作しました。 ※pcapファイルは、イーサネット経由でVelodyneセンサから入ってくるセンサストリームをdumpしたファイルです。ROSを経由したrosbag形式でもキャプチャ可能ですが、純粋にセンサストリームを保存する用途であればこのpcapがおすすめです。 センサストリームはこんな感じで可視化できます。 intensity(信号強度)で色を付けるモードにすると、材質によって見え方が変わって楽しい感じです。 Tools -> Recordを選択すると、pcap形式でデータが蓄積されていきます。 30分ほどで10GB弱のファイルになるので、ストレージが強めのPCを使うのが良さそうです(そのほかのスペックはM1なMacboocProでも問題ありませんでした)。 点群処理環境環境準備 キャプチャされた3Dな点群を扱うには、Open3Dが便利です。難しいことを考えずに使いたい方は、こちらのGetting Startedにある通り、下記コマンドで一発です。 pip install open3d M1なMacでもRTXなUbuntuでも問題なく動きました。 以下Open3Dのコンパイルは、下記のような人におすすめです。 Open3D-MLを使いたい KinectやRealSenseなどのデバイスを使いたい C++なサンプルをビルドしたい 純粋な点群処理だけであれば読み飛ばしていただいてOKです。 Open3Dのコンパイル 基本的な手順は、特にOpen3D-MLに関するところはこちらのドキュメントを参考にしてください。 前提環境 Pytorch1.8.1+CUDA11.1で動作を確認しました。 (私の方では、現時点ではTensorflow系の機能をONにできていません) ただし、公式ドキュメントに記載されているPytorchのバージョンが異なるため、注意が必要です。 (細かいところで動作不良があるかもしれません。) また、CXX ABIのバージョンについても注意が必要です。 これまたドキュメントにある通り、pipで取得可能なPytorchやTensorflowは古いABIにてコンパイルされているので、今回のOpen3Dコンパイルにおいても-DGLIBCXX_USE_CXX11_ABI=OFFオプションをつけてあげる必要があります。 もしくは、Open3D公式からビルド済みPytorchなどが配布されているので、そちらを使ってABIバージョンを合わせる方法もあります。 ソースからOpen3Dをコンパイル 下記手順に従って作業を進めます。おおよそ下記のようなセッテイングです。 Python及びJupyterモジュール有効化 CUDA有効化 サンプルのコンパイル GPU機能有効化 Open3D-ML有効化 Pytorchモジュール有効化(基本的にはOpen3D-MLとセット) cd /usr/local sudo git clone --recursive https://github.com/intel-isl/Open3D sudo git clone https://github.com/intel-isl/Open3D-ML.git sudo chown -R ${USER} /usr/local/Open3D cd Open3D mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX=/data/workspace/open3d_build \ -DPYTHON_EXECUTABLE=$(which python3) \ -DBUILD_PYTHON_MODULE=ON \ -DBUILD_SHARED_LIBS=ON \ -DBUILD_EXAMPLES=ON \ -DBUILD_UNIT_TESTS=OFF \ -DBUILD_BENCHMARKS=ON \ -DBUILD_CUDA_MODULE=ON \ -DBUILD_COMMON_CUDA_ARCHS=ON \ -DBUILD_CACHED_CUDA_MANAGER=ON \ -DBUILD_GUI=ON \ -DBUILD_JUPYTER_EXTENSION=ON \ -DWITH_OPENMP=ON \ -DWITH_IPPICV=ON \ -DENABLE_HEADLESS_RENDERING=OFF \ -DSTATIC_WINDOWS_RUNTIME=OFF \ -DGLIBCXX_USE_CXX11_ABI=OFF \ -DUSE_BLAS=ON \ -DBUILD_FILAMENT_FROM_SOURCE=OFF \ -DWITH_FAISS=OFF \ -DBUILD_LIBREALSENSE=ON \ -DUSE_SYSTEM_LIBREALSENSE=ON \ -DBUILD_AZURE_KINECT=ON \ -DBUILD_TENSORFLOW_OPS=OFF \ -DBUILD_PYTORCH_OPS=ON \ -DBUNDLE_OPEN3D_ML=ON \ -DOPEN3D_ML_ROOT=../../Open3D-ML \ .. make -j$(nproc) sudo make install sudo make install-pip-package 点群の結合 今回キーとなるのはmulti-scale ICPというアルゴリズムです。 ICPは点群同士の位置合わせを行う代表的なアルゴリズムとなりますが、multi-scale ICPとしては密な点群の位置合わせを扱えるという特徴があります。 Open3DのPipeline(Tensor)から扱うことで、cuda処理を可能としています。 流れは以下の通りです。 Velodyne pcapデータのフレーム毎読み出し 直前の点群との位置合わせ(multi-scale ICP) うまくフィットしない場合、全体の点群との位置合わせを実施 点群のマージ、保存 次のようなソースコードで、実際のpcapデータを使って点群を繋ぎ合わせ、3Dモデルを作っていきます。 あいだに入っているコメント(コードブロック外)を抜いてコピペすれば、動かせると思います。 #coding:utf-8 from tqdm import tqdm import numpy as np import argparse import os import open3d as o3d import datetime import time import velodyne_decoder as vd import copy import math 点群のデコードには、velodyne_decorderを利用します。 Open3Dについても正しくimportできるか確認します。 parser = argparse.ArgumentParser( prog='pcap_multi-scale_ICP', usage='create pointcloud from pcap', description='', epilog='end', add_help=True, ) parser.add_argument('-i', '--input') parser.add_argument('-o', '--output') parser.add_argument('-s', '--start', type=float) parser.add_argument('-e', '--end', type=float) parser.add_argument('-m', '--icp_interval', type=float) parser.add_argument('-v', '--voxel_size', type=float) parser.add_argument('-c', '--cuda', action='store_true') parser.add_argument('--save_interval', type=int) parser.add_argument('--merge_interval', type=int) args = parser.parse_args() voxel_resol = 0.1 icp_interval = 0.1 save_interval = 100 merge_interval = 10 outdir = os.path.dirname(args.input) + '/' + os.path.splitext(os.path.basename(args.input))[0] + '/integration/' start_time = 0.0 end_time = time.time() if args.output: outdir = args.output if not os.path.isdir(outdir): os.makedirs(outdir) if args.voxel_size: voxel_resol = args.voxel_size if args.start: start_time = args.start if args.end: end_time = args.end print(str(start_time) + " to " + str(end_time)) if args.icp_interval: icp_interval = args.icp_interval if args.save_interval: save_interval = args.save_interval if args.merge_interval: merge_interval = args.merge_interval 入力はpcapファイル、出力は点群(ply)ファイルとなります。また、Open3DのGUI画面についても、スクリーンショットとして保存することにしました。 各フレームのマージは毎回行うのではなく、merge_intervalで指定した頻度で、voxel_resolで指定した解像度でボクセル化した上で繋ぎ合わせることにしました。 また、メモリーいっぱいでプログラムが落ちたりもするので(最初からやり直しで悲しい)、save_intervalで指定した頻度でplyファイルに保存することにします。 boundary = {'minX':-30.0, 'minY':-30.0, 'minZ':-5.0, 'maxX':30.0, 'maxY':30.0, 'maxZ':20.0} def maxDist(): minXYZ_len = math.sqrt(boundary['minX']**2 + boundary['minY']**2 + boundary['minZ']**2) maxXYZ_len = math.sqrt(boundary['maxX']**2 + boundary['maxY']**2 + boundary['maxZ']**2) return max(minXYZ_len, maxXYZ_len) VLS-128は最大300mの測位が可能ですが、点群を結合していくためには大きすぎるため点群を球状のエリアでクロップすることにしました。boundaryで指定した範囲のうち最長の長さを半径として、点群を切り取っていきます。 def process_m_ICP(s_pcd, t_pcd): #ICP process # Setting Verbosity to Debug, helps in fine-tuning the performance. # o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug) s = time.time() source_pcd = o3d.t.geometry.PointCloud.from_legacy(s_pcd) source_pcd.estimate_normals() target_pcd = o3d.t.geometry.PointCloud.from_legacy(t_pcd) target_pcd.estimate_normals() if args.cuda: source_pcd = source_pcd.cuda(0) target_pcd = target_pcd.cuda(0) registration_ms_icp = o3d.t.pipelines.registration.multi_scale_icp(source_pcd,target_pcd, voxel_sizes, criteria_list, max_correspondence_distances, init_source_to_target, estimation, save_loss_log) ms_icp_time = time.time() - s print("Time taken by Multi-Scale ICP: ", ms_icp_time) print("Inlier Fitness: ", registration_ms_icp.fitness) print("Inlier RMSE: ", registration_ms_icp.inlier_rmse) return registration_ms_icp こちらのチュートリアルほぼそのままなのですが、cpu側で作成した点群をcuda処理を行う改造をしております。 config = vd.Config() config.model = 'VLS-128' config.rpm = 600 config.calibration_file = 'calibrations/Alpha Prime.yml' config.timestamp_first_packet = start_time config.max_range = maxDist() pcap_file = args.input cloud_arrays = [] velodyne_decoder側の設定になります。 config.modelをVLS-128に設定するだけではcalibration_fileが読み込めなかったため、こちらにあるAlpha Prime.ymlを指定しました。 なお、処理を行うタイムスタンプをconfig.timestamp_first_packetで指定してみましたが、これはうまく動作しませんでしたので、別で処理をしています(後述)。 if args.cuda: device = o3d.core.Device("CUDA:0") print('Using CUDA') else: device = o3d.core.Device("CPU:0") vis = o3d.visualization.Visualizer() vis.create_window() pcd = o3d.geometry.PointCloud() geom_added = False pcd_prev = None ## ICP settings voxel_sizes = o3d.utility.DoubleVector([0.1, 0.05, 0.025]) # List of Convergence-Criteria for Multi-Scale ICP: criteria_list = [ o3d.t.pipelines.registration.ICPConvergenceCriteria(relative_fitness=0.0001, relative_rmse=0.0001, max_iteration=50), o3d.t.pipelines.registration.ICPConvergenceCriteria(0.00001, 0.00001, 15), o3d.t.pipelines.registration.ICPConvergenceCriteria(0.000001, 0.000001, 10) ] # `max_correspondence_distances` for Multi-Scale ICP (o3d.utility.DoubleVector): max_correspondence_distances = o3d.utility.DoubleVector([0.3, 0.14, 0.07]) # Initial alignment or source to target transform. init_source_to_target = o3d.core.Tensor.eye(4, o3d.core.Dtype.Float32) # Select the `Estimation Method`, and `Robust Kernel` (for outlier-rejection). estimation = o3d.t.pipelines.registration.TransformationEstimationPointToPlane() # Save iteration wise `fitness`, `inlier_rmse`, etc. to analyse and tune result. ここも、サンプルそのままです。 なお、大規模な点群でうまくマッチしないケースがあったので、max_iterationを50まで増やしています。 また、multi-scale ICPでは三段階の解像度(voxel_sizes)でマッチング処理を行いますが、この解像度を上げすぎると時間がかかるだけではなくメモリー不足で落ちてしまいます。。 save_loss_log = True stamp_prev = 0 trans_prev = None trans_list = [] pcd_prev = None count = 0 prev_save_count = 1 prev_merge_count = 1 fitness_threshold = 0.65 for stamp, points in vd.read_pcap(pcap_file, config): if stamp < start_time: continue if stamp > end_time: break str_timestamp = str(datetime.datetime.fromtimestamp(stamp)).replace(' ', '-') pcd_cur = o3d.geometry.PointCloud() if stamp-stamp_prev >= icp_interval: pcd_cur.points = o3d.utility.Vector3dVector(points[:,:3]) else: continue 指定されたタイムスタンプ内でVelodyne点群をOpen3D形式の点群に変換します。 registration_ms_icp = None if pcd_prev is None or pcd is None: pcd_prev = pcd_cur pcd = pcd_cur else: print(stamp) registration_ms_icp = process_m_ICP(pcd_prev, pcd_cur) stamp_prev = stamp if registration_ms_icp.fitness < fitness_threshold: print("global registration") registration_ms_icp = process_m_ICP(pcd, pcd_cur) pcd_prev = copy.deepcopy(pcd_cur) 一定周期でmulti-scale ICPを行います。一致度が一定(ここでは0.65としました)を下回った時、点群全体との位置合わせを行います。 if registration_ms_icp is not None: pcd.transform(o3d.core.Tensor.numpy(registration_ms_icp.transformation)) if prev_merge_count > merge_interval: pcd_cur_np = np.asarray(pcd_cur.points) pcd_global_np = np.asarray(pcd.points) pcd_tmp_np = np.concatenate((pcd_cur_np,pcd_global_np), axis=0) pcd.points = o3d.utility.Vector3dVector(pcd_tmp_np) pcd.points = pcd.voxel_down_sample(voxel_size=voxel_resol).points print("Merge pointcloud") print(pcd) prev_merge_count = 0 else: prev_merge_count += 1 multi-scale ICPの結果を用いて、点群の位置や向きを変えていきます。 また、一定周期でダウンサンプルした上で、全体の点群にマージしていきます。 if prev_save_count > save_interval: o3d.io.write_point_cloud(outdir + '/' + os.path.splitext(os.path.basename(args.input))[0] + '.ply', pcd) prev_save_count = 0 else: prev_save_count += 1 if geom_added == False: vis.add_geometry(pcd) geom_added = True vis.update_geometry(pcd) vis.poll_events() vis.update_renderer() time.sleep(0.01) vis.capture_screen_image(outdir + '/' + 'screen_{:07d}'.format(count) + '.jpg', True) count += 1 点群の保存とともに、スクリーンショットも保存していきます。 ちなみに、環境によってはcapture_screen_imageをupdate_rendererの直後に呼び出すと真っ白な画像しか保存されませんでしたので、sleepを入れています。 結果 上記のプログラムで処理した、点群位置合わせの様子です。 Z軸方向の高さで色をつけています。 点群の位置合わせやSLAM処理では、Loop Closureや最適化処理を行うのが一般的ですが、今回のようなICPベースの手法のみでもそれなりの完成度に見えます。 VLS-128の点群が非常に高密度であったことも影響していると思われます。 所々段差で台車を持ち上げたのですが、しっかり追従できていますね。 タイトルで「ぼっち」と書かせていただいたように、この点群には人や車がほとんど写っておりません。 (ドコモR&Dセンタ拠点に人がいなかったのもありますが)点群の結合処理の段階でダウンサンプルを行なっている都合上で、動くものが消えたようです。 まとめ Open3Dを使って、Velodyne LiDAR点群を繋ぎ合わせ、3Dモデルを作っていきました。 今回は単色の点群をもとにしてモデルを作りましたが、機会があれば今後カメラとの組み合わせによるカラー点群や点群認識なども紹介したいと思います。 それでは、良いお年を!!
- 投稿日:2021-12-24T21:21:04+09:00
関数の可変長引数
タプルの可変長引数 tuple.py def tupleArgument(*arg): tupleArgument('value1','value2','value3') *ひとつでタプルの可変長引数であることを示す ディクショナリの可変長引数 dictionary.py def dictionaryArgument(**kwargs) dictionaryArgument(key1='value1',key2='value2',key3='value3') *ふたつでディクショナリの可変長引数であることを示す
- 投稿日:2021-12-24T20:13:44+09:00
「Python Advent Calendar 2021」集計(1/3)
Python Advent Calendar 2021 20日に、「dockerでpython」を書きかけで投稿した。 これ以外に、 「Rust Advent Calendar 2021」集計してみる 「IT資格取得をテーマに学びをシェアしよう!【PR】Udemy Advent Calendar 2021」を振り返る(前段) の2つの記事を書き始めて、この間の参加者数をそれぞれ集計してみて比較する。 date title gods stocks others goods 12月1日 日系大手企業のパワポエンジニア事情をPythonで無理やり改善する https://qiita.com/c60evaporator/items/f92dff60a46317b7d967 81 73 Python の asyncio は超便利 https://qiita.com/waterada/items/1c03a7c863faf9327595 86 78 【WSL】Python+VSCode環境構築 https://qiita.com/satto_sann/items/4c754595cf9119330c45 5 2 PyTorch向けの深層強化学習ライブラリPFRLを試してみた https://qiita.com/HGS_Naofumi/items/9cac3f4eb08b79efd072 3 0 12月2日 マインクラフト上に迷路をPythonから自動生成して遊ぶ https://qiita.com/Supu/items/d90175ddd72f0f3e9f11 17 7 DeepL API x PySimpleGUIで翻訳支援アプリを作成 https://qiita.com/NSsystems_DX/items/18a01f195c31f1d1db8c 5 3 スマートなPythonistaになりましょう。 https://qiita.com/tasuren/items/2999f5c662c846260b36 8 10 12月3日 とあるゲームにてバトルが始まるときのフェードアウト表現(黒い線が画面を回りながら暗転する)を簡易実装してみた https://qiita.com/seigot/items/1e455e5b65284eddc4d0 【図解】8行で機械学習【Python】 https://zenn.dev/nekoallergy/articles/sklearn-nn-mlpclf01 zenn 32 selenium小技 (プロキシ/ヘッドレス) https://ryuichi1208.hateblo.jp/entry/2021/12/01/234631 hatenablog 12月4日 pythonでCSVレコードをモデルに変換 https://qiita.com/hoshi_kouki/items/b932710304df3e6cc65a 7 5 SageMaker Studio Lab 使ってみた [Python] https://nakagami.blog.ss-blog.jp/2021-12-04 ss-blog Python Boot Camp テキストで始める Python https://d.nishimotz.com/archives/2405 12月5日 Python clickの案外知られていない機能 https://qiita.com/cvusk/items/fcda5e77350f248d39e5 85 72 PyQt6の基本の使い方からのまとめとPyQt5との違い https://qiita.com/phyblas/items/d56003904c83938823f2 30 22 Flask + SQLAlchemy でよさそうなトランザクション管理 https://qiita.com/ijufumi/items/52abed9ce4205fd0eff3 1 0 12月6日 SVGベースのPythonフロントエンドライブラリを作っているという話 https://qiita.com/simonritchie/items/26f7da93ac85f60b2972 9 3 Google App Engine(GAE)のスタンダード環境でFFmpegを使う方法 https://zenn.dev/sikkim/articles/3be1841603a06619fa2c zenn 8 Unix環境でもPythonランチャーを使いたい https://qiita.com/3w36zj6/items/cf1bd929c538082d5db3 3 1 12月7日 PythonでUnicodeDecodeErrorが発生した時の対応 https://qiita.com/ijufumi/items/3609a983cd0673383f69 4 0 TLS1.3 のラッパー書いた [Python] https://nakagami.blog.ss-blog.jp/2021-12-07 ss-blog [Python]デフォルト値を持つ引数の前にはアスタリスクを設けておくと堅牢で良いかも、という話 https://qiita.com/simonritchie/items/7d8382e821dd1e9bd98b 14 10 12月8日 python 3.10からの新機能 match - case を使ったIF文置き換えの紹介 https://qiita.com/Intel0tw5727/items/6988c62ce4aaa681b151 33 18 決定木、ランダムフォレストのアルゴリズムを徹底解説! https://qiita.com/hydrogen_0330/items/59dfa37ceb3bb19b6372 5 0 poetry install で setup command: use_2to3 is invalid が発生した時の対応 https://qiita.com/ijufumi/items/d1149f3d4c0034235799 3 0 合計 399 304 40
- 投稿日:2021-12-24T19:53:43+09:00
Linuxインストール後の設定 〜 kivyの環境構築 〜 Androidのapkを作るまで
自分用の備忘録です。これで少しでもLinux使いが増えると嬉しい。 OS ... Linux Mint 20.2 MATE Edition (Ubuntu 20.04 LTS 派生) PC ... Intel NUC5i3RYH (かろうじて1080pのYoutube動画が観られる程度の弱いPC。gameはもってのほか。) RAM ... 16GB (最低8GBは無いとAndroidアプリを作る時にきつい) OS篇 swapfileを無効化 まずは$ freeでswapfileが現在有効になっているかの確認。 $ free total used free shared buff/cache available Mem: 16280664 550208 14756408 78572 974048 15356640 Swap: 2097148 0 2097148 swapfileをまだ使ってはいないものの何時いつでも使えるように備えてある事が分かります。swapfileはSSDが主流となった今日こんにちにおいては利より害のほうが大きいと思われるので$ sudo swapoff -aで現在有効になっているswapfileを無効化します。 $ sudo swapoff -a $ free total used free shared buff/cache available Mem: 16280648 1457856 13248092 456244 1574700 14062524 Swap: 0 0 0 次はswapfileを永遠に無効化します。linuxの設定fileの中にOS起動時にswapfileを作るよう指示している者が居るのでそれを次のように弄ってあげます。$ sudo xed /etc/fstab(xedはLinux Mint MATE Editionに標準装備されているtext editor)と打ってfileを開いたら (#から始まる行はcommentなので省略) UUID=略 / ext4 errors=remount-ro 0 1 UUID=略 /boot/efi vfat umask=0077 0 1 /swapfile none swap sw 0 0 三列目がswapである行を全てcomment out。 # /swapfile none swap sw 0 0 そして残った/swapfileはもう無用なので消しておきます ($ sudo rm /swapfile)。 作業directoryとしてRAM Diskが使われるようにする 続いてlinuxの一時file置き場である/tmpにRAM Diskが使われるようにしてあげます。これを internetから落とすファイルの保存先 アプリ開発 等とにかく色んな物に利用してSSDの負担を減らすのが目的です。 起動時に自動でRAM Diskが作られるようにする 先程のように$ sudo xed /etc/fstabと打って/etc/fstabを開いたら tmpfs /tmp tmpfs defaults,noatime,size=8G 0 0 をfileの最後に加えます。size=8GがRAM Diskの容量を指定している部分で、google playで自分のアプリを公開する事を考えているなら5G、公開せずに自分用としてしか使わない場合でも最低4Gは欲しいところです。 このファイルを弄り損ねるとOSが起動しなくなるので注意。仮にそうなったとしてもLive USBで別にOSを立ち上げてファイルを直せばいいだけなので大した問題ではないですが。 直ちにRAM Diskを作る 今は再起動せずに直ちにRAM Diskを利用したいので $ sudo mount -a と打って/etc/fstabの編集結果を直ちに反映させます。 OSを更新 画面右下のタスクトレイにあるアップデートマネージャを用いてOS更新、すると再起動を促されるので従います。 日本語名のdirectoryを英語名にする 「ドキュメント」「ミュージック」といった日本語名のdirectoryがあるので「Documents」「Music」といった英語名にします。$ LANG=C xdg-user-dirs-gtk-update windowをdrag/resize中に中身を描画させない これが有効だとKivyのwindowをresizeした時に劇的に重くなってしまうので無効にしておきます。screenshotは英語になっていますがOSの言語設定を日本語にしていればちゃんと日本語になります。(できればcommand lineでこの設定を弄る方法が知りたいです)。 Alt key + mouseによるwindowの移動を防ぐ Visual Studio Codeで複数cursorを置くときの操作とかぶってしまってそれが出来なくなってしまうので無効化あるいはAlt以外のkeyを割り当てます。(できればcommand lineでこの設定を弄る方法が知りたいです)。 Visual Studio Codeを入れる 公式サイトより.debファイルを落として $ sudo apt install .debファイル と打つだけです。入れた後の設定に関しては幾らでも解説が見つかるので省きます。 Python篇 私は pyenv + pipenv もしくは pyenv + poetry の組み合わせで使っているのでそれらを入れます。 pyenv 初期状態ではgitが入ってなかったので入れたら(sudo apt install git)その後はBasic GitHub Checkoutの手順に従ってpyenvを入れます。入れたらpyenvを有効にする為にloginし直し、install可能なpythonのversionを列挙。 $ pyenv install --list Available versions: 2.1.3 2.2.3 2.3.7 2.4.0 . . その中で使いたい物を好きなだけinstall。 $ pyenv install 3.8.10 # CPython 3.8.10 $ pyenv install 3.8.12 # CPython 3.8.12 $ pyenv install 3.9.6 # CPython 3.9.6 PCにはpyenvのgit repositoryがまるごと入るので、pyenvを更新したい時は cd ~/.pyenv git pull origin master と打つ。 poetry $ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - $ echo "export PATH=\$HOME/.local/bin:\$PATH" >> ~/.bashrc $ poetry config virtualenvs.in-project true 実際に打ったcommandを載せはしましたが、これをcopy&pasteするような真似はせずにその都度公式のdocを読んで従って下さい。 pipenv $ pyenv which python3 /usr/bin/python3 でsystemのpythonが有効な事を確かめたら $ python3 -m pip install --user pipenv と打って入れます。その後 $ echo "export PIPENV_VENV_IN_PROJECT=1" >> ~/.bashrc と打ってproject内に.venvが作られるようにしたら、これまでの.bashrcへの編集を反映させるためにterminalを再起動。その後 $ poetry --help $ pipenv --help などと打って各道具が使えるようになっている事を確かめて終わりです。 Kivy篇 Kivyを自分でcompileしたりandroidのapk/aabを作るのに必要な物を入れていきます。自分でせずともcompile済みの物がPyPIには上がってはいるのですができた方が便利なので入れます。Installation Componentsに書いてある物に加えてxclipとxselのどちらか好きな方を入れたら完了。 $ apt install xclip $ apt install xsel xclipやxselは無くてもKivyは動きますが無いとTextInputを使った時に警告が出ます。もしかしたら無いとcopy&pasteができないのかもしれません(未確認)。 次は以下のlinkにあるAndroidのapk/aabを作るのに必要な物を入れていくのですが、ここではaptを用いる物だけに留め、pipを用いる物は各アプリのプロジェクトの仮想環境内で入れる事にします。 アプリ開発篇 ここからがアプリの開発を始める度に行う手順となります。 projectのroot directoryを作る # SSDの負担を減らすためにRAM Disk下に作っています。 $ mkdir /tmp/myproject $ cd /tmp/myproject 仮想環境を作り各種install pipenv派 # 使いたいversionのpythonをpyenvで入れた物の中から選ぶ $ pipenv --python 3.9.6 # 仮想環境に入る $ pipenv shell # kivyを入れる # 開発版 $ pipenv install "kivy[base] @ https://github.com/kivy/kivy/archive/master.zip" # 安定版 $ pipenv install "kivy[base] @ https://github.com/kivy/kivy/archive/stable.zip" poetry派 $ poetry initと打つと始まる問答に答えたら以下のようにしてkivyを入れます。 # 開発版 $ poetry add https://github.com/kivy/kivy/archive/master.zip # 安定版 $ poetry add https://github.com/kivy/kivy/archive/stable.zip $ poetry install # 仮想環境に入る $ poetry shell poetryに訊かれるpackage名はPyPIに上げる時には使われますがandroidアプリのpackage名とは関係ないです。 PC上で開発 installが終わったらPC上でアプリを開発し十分にtestします。 buildozerを入れる 今からbuildozerを入れていくのですが、もしplay storeで公開するつもりなのならaab形式で出力できるmisl6氏の物が要ります。そうじゃなければ公式の物でも構いません。 pipenv派 # aab対応版 (play storeで公開予定ならこっち) $ pipenv install --dev Cython==0.29.19 virtualenv git+https://github.com/misl6/buildozer@feat/aab-support#egg=buildozer # 開発版 (安定版は長らく更新されていないのでこっちを推奨) $ pipenv install --dev Cython==0.29.19 virtualenv https://github.com/kivy/buildozer/archive/master.zip # 安定版 $ pipenv install --dev Cython==0.29.19 virtualenv buildozer poetry派 # aab対応版 (play storeで公開予定ならこっち) $ poetry add --dev Cython==0.29.19 virtualenv git+https://github.com/misl6/buildozer@feat/aab-support # 開発版 (安定版は長らく更新されていないのでこっちを推奨) $ poetry add --dev Cython==0.29.19 virtualenv https://github.com/kivy/buildozer/archive/master.zip # 安定版 $ poetry add --dev Cython==0.29.19 virtualenv buildozer buildozer.specを編集 $ buildozer initと打つとbuildozer.specが作られるので適切に設定します。 # アプリに付ける名前。日本語も使える。 title = Test Application # Package name (これとPackage domainを繋げた物がandroidのpackage名となる) package.name = testapp # Package domain package.domain = jp.gottadiveintopython # main.pyのあるdirectory source.dir = . # 画像や音声、フォントなどのアプリに含めたいファイルの拡張子を全て指定 source.include_exts = py,png,ogg,mo,ttf # アプリに含めたくないdirectory source.exclude_dirs = .venv, venv, .git, tests, bin # 求める権限 android.permissions = # recipe及び依存module (開発で使ったpythonのversionと揃える) requirements = python3==3.9.6,kivy==master,hostpython3==3.9.6,android,plyer # アプリが対応する端末の向き landscape(横持ち) sensorLandscape(横持ち) portrait(縦持ち) all(全方向) orientation = all # 使用するpython-for-androidを開発版(develop)に p4a.branch = develop # 自分のアプリが吐くlogだけが出力されるようにする。これがFalseだと大量の無関係なlogに自分のlogが埋もれて # しまって見づらいので最初はTrueにしておく。もし何もlogが吐かれないようならアプリの起動に失敗している可能 # 性があるのでFalseに戻し原因を探る。 android.logcat_pid_only = True # logのlevel。これは必ずに最大にしておく。 log_level = 2 apkを作成 android端末を開発者モードでPCに繋いだら $ buildozer android debug deploy run logcat と打って実機上でtest。最初はかなり時間がかかる(20-30分)ので何か別の作業をしながら時々エラーが起きていないか画面を覗くのがお薦めです。今回は一つだけエラーが出ましたがすんなり解決できました。 最適なABI(CPU命令セット)の選択 自分の端末は32bitのABIにしか対応してないのでABIの値は初期値のarmeabi-v7aのままで良いですが、64bitのABIに対応した端末を持っている人はbuildozer.specの以下の行を弄って最適なマシンコードを吐かせたほうが良いと思われます。 # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 android.arch = armeabi-v7a # aab対応版では複数の命令セットを選べます。複数選ぶとbuildにかかる時間がその分倍増するので開発中は一つだけにし、release buildの際に複数選ぶのがお薦めです。 android.archs = armeabi-v7a,arm64-v8a 端末が対応しているABIを調べるにはPCに開発者モードで端末を繋げた状態で$ adb shell getprop | grep cpu.abiと打ちます。以下が私の端末での出力で [ro.product.cpu.abi]: [armeabi-v7a] [ro.product.cpu.abi2]: [armeabi] [ro.product.cpu.abilist]: [armeabi-v7a,armeabi] [ro.product.cpu.abilist32]: [armeabi-v7a,armeabi] [ro.product.cpu.abilist64]: [] 64bitのABIには対応していないのが分かります。 release build apkによる実機testが終わったら次は公開用aabの作成です。buildozer.specに以下のように複数のABIを書いて # play storeで公開するには32bitと64bitのABI各一つ以上が必要となる。 android.archs = armeabi-v7a,arm64-v8a,x86_64 署名用のkeystoreファイルを用意して環境変数を設定したら $ buildozer android release と打ってaabが出来上がるのを待ちます。その後は一般的なandroid開発と手順は変わらないので手順は省きます。 参考資料 Guhan Sensam氏の資料(aabを作るのに役立った) Working on Android(python-for-androidのdocは全て重要ですが中でもこの部分が特に)
- 投稿日:2021-12-24T17:45:00+09:00
有料級なPythonの開発レシピが載っているWebアプリ
概要 AI/DX活用を学習教材として提供するプラットフォームです。 その名も、「Axross」 Axrossは「学んだが活用できない人」を減らしたい想いでソフトバンクの社内起業制度(SBイノベンチャー)で立ち上げた、現役エンジニアのノウハウを "レシピ" として提供するプラットフォームです。 使用してみて ・現役エンジニアのレシピがありとても興味深い ・かなり有料級の学習教材が多い ・Pythonを書いている人はとても嬉しいWebアプリ ・学習教材を開いてやってみて、解説や回答の流れまでシンプルで使いやすいかつ自主性を求めている所が「見て終わり」ではなくて「書いて終わり」ができるので素晴らしいWebアプリだと思った。 ・クリアした後には、修了証などゲーム感覚や友達や世間に認めてもらえる遊びシステムがあるところもAxrossの魅力だと思いました。 参考文献 Axross https://axross-recipe.com/recipes
- 投稿日:2021-12-24T17:38:34+09:00
k近傍法をsklearnなしで実行する方法について!!
こんにちは!初投稿させていただきます. 今回はk近傍法をsklearnを使わず行う方法を解説します. 使うデータとしてはsklearnに内蔵されているdigitデータです. k.py import numpy as np import pandas as pd import collections from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, accuracy_score, confusion_matrix # データのロード digits = datasets.load_digits() # 特徴量 image = digits.images total , x , y = image.shape image = np.reshape( image , [total,x*y] ) # 目的変数 label = digits.target # 学習データ,テストデータ train_data, test_data, train_label, test_label = train_test_split(image, label, test_size=0.5, random_state=None) # テストデータ数 testcount = test_label.shape[0] # 予測結果を格納する配列 all_label=[] #k近傍法のkの指定 mk=int(input()) #テストデータの繰り返しの準備 for i in range(len(test_data)): #あるテストデータにおける最近傍を求めるためのデータフレームの準備 output=pd.DataFrame(columns=['index','ans']) #学習データの繰り返しの準備 for j in range(len(train_data)): #あるテストデータと学習データの距離を格納するための変数ans ans=0 #あるテストデータと学習データの距離を調べるfor文 for k in range(len(train_data[0,:])): ans+=(train_data[j,k]-test_data[i,k])**2 #outputに各学習データのindexとあるテストデータとの距離を格納 number = pd.Series([int(j),ans],index=output.columns) output = output.append(number, ignore_index=True) #outputを距離で昇順に並べる output = output.sort_values('ans') #距離で昇順に並べたあとのindexを格納するリストkkk kkk=[] #距離で昇順に並べたあとのindexから学習データの中のラベルを調べるfor文 for l in range(mk): kkk.append(train_label[int(output.iat[l,0])]) #ラベルを調べた後にその中にどの値が最も多いのか(最頻値)を調べてkkkに格納 kk = collections.Counter(kkk).most_common()[0][0] #ラベルを結果に格納する all_label.append(kk) #念の為の正解率や混合行列の結果 #予測結果 print( classification_report(test_label, all_label) ) #正解率 print( accuracy_score(test_label, all_label) ) #混同行列 print( confusion_matrix(test_label, all_label) ) ぜひ,ご自分でも加工して作ってみてください!! これを学ぶだけでもpythonの中身がわかっていい勉強になると思います.
- 投稿日:2021-12-24T17:21:43+09:00
[py2rb] dict の for
はじめに 移植やってます for (Python) h = { 1: 2, 3: 4, 5: 6 } for x in h: print(x) # 1 # 3 # 5 知っている人にとって当たり前かもしれませんが、dictのforで変数が1つの場合、キーのみを返します。 知らなかったため、人生初のstack level too deepを頂きました。 とんでもないクリスマスイブになるところでしたが、デバッグの甲斐あって普通のクリスマスになりそうです。 each_key (Ruby) h = { 1 => 2, 3 => 4, 5 => 6 } h.each_key do |key| puts key end h.keys.map{ puts _1 } mapやselectのときは、keysを使用することも可能です。 メモ Python の for を学習した 道のりは遠そう
- 投稿日:2021-12-24T17:11:51+09:00
LINE WORKSのAPI 2.0 (Beta) を使って画像メタ情報取得Bot作ってみた
はじめに 2021/10/28よりLINE WORKSのAPI 2.0 (Beta) が提供開始された。 上のリリースページにもあるように、全体的にシンプルな作りとなったり、OAuth2.0による認可機能が実装されたり等、多くのアップデートが入った。 今回はこのAPI 2.0 (Beta)を使ったBotの作成や画像ダウンロード周りを試すため、送られてきた画像のメタ情報を返すシンプルなトークBotを作成してみる。 関連: 従来のAPIを使ったトークBot作成 以前従来のAPIでトークBotを作成した時の過去記事 注意 本記事ではベータ版の情報を扱っています。今後のアップデートや正式版では一部異なる仕様となる可能性があります。 Bot作成 0. 前準備 Developer Consoleでアプリを登録 従来のAPIでは、Developer Consoleでテナント単位の各種キーや証明書を管理・発行するようになっていたが、API 2.0 (Beta)では「アプリ」という概念が追加され、アプリ単位に認証情報を管理するようになった。 アプリ設定画面 アプリ画面で各種キーの発行 従来のAPIには「サービスAPI」と「サーバーAPI」の2種類があり、トークBot関連はサーバーAPIに区分されていた。それぞれで必要なキー情報も異なっていたが、API 2.0 (Beta)ではその2種類が1つに統合され区別はなくなった。 アプリを作成すると、アプリ単位に持っている「Client ID」と「Client Secret」が生成される。 Client ID アプリを識別するID Client Secret アプリごとに持っている、Client IDに紐づくシークレット値 これらは、APIの認証で利用するAccess Tokenの取得のために利用される。 また、Access Tokenの取得のために加えて「Service Account」と「Private Key」も取得する。 Service Account アプリ専用の仮想管理者アカウント。ユーザーアカウントの代わりにAccess Tokenを取得することが可能となる。 Private Key 上記Service Accountを使用してAccess Tokenを取得する際に必要な認証キー。 最後に、「OAuth Scopes」で。そのアプリの権限範囲を設定する。 OAuth Scope アプリが利用するリソースとその操作権限。 https://developers.worksmobile.com/jp/reference/scopes?lang=ja 今回はトークBotの利用のため、Scopeは bot, bot.read を設定しておく。 1. チャットボットサーバーの構築・実装 トークBotの本体となるチャットボットサーバーを構築・実装する。 従来のAPIと同様に、Bot設定のCallback URLに指定したURLに対してHTTP POST リクエストとしてメッセージイベントが送られる (Botの設定については後述)。 構成 今回は、Google Cloudの「Cloud Run」をサーバーとして置いた構成で作成した。 LINE WORKS側から送られてくるメッセージイベントをCloud Runで受信し、それに応じた返答をメッセージ送信用のAPIで返す。また、APIの利用で必要なAccess Tokenの発行・更新についてもCloud SchedulerをトリガーとしてCloud Runで行うようにした。関連する秘匿情報はSecret Managerで管理することとした。 アプリケーション 今回は、アプリケーションはPythonで実装し、サーバーにはFast APIを利用した。 Python: 3.8 Fast API: 0.70.0 Access Tokenの取得 APIを利用するため、まずはAccess Tokenを取得する必要がある。 Service Account (上記で作成したアプリ専用の仮想管理者アカウント) を利用するため、Service Account認証 (JWT) を利用してAccess Tokenを取得する。 まず、Client IDとService Account IDとPrivate KeyでJWTを取得する。 def get_jwt(client_id, service_account_id, privatekey): """ LINE WORKS アクセストークンのためのJWT取得 """ current_time = datetime.now().timestamp() iss = client_id sub = service_account_id iat = current_time exp = current_time + (60 * 60) # 1時間 jwstoken = jwt.encode( { "iss": iss, "sub": sub, "iat": iat, "exp": exp }, privatekey, algorithm="RS256") return jwstoken 取得したJWTと、Client ID, Client Secretを使ってAccess Tokenを取得する。 その際、Scopeを指定する (今回は bot, bot.read) def get_access_token(client_id, client_secret, scope, jwttoken): """アクセストークン取得""" url = 'https://auth.worksmobile.com/oauth2/v2.0/token' headers = { 'Content-Type': 'application/x-www-form-urlencoded' } params = { "assertion": jwttoken, "grant_type": urllib.parse.quote("urn:ietf:params:oauth:grant-type:jwt-bearer"), "client_id": client_id, "client_secret": client_secret, "scope": scope, } form_data = params r = requests.post(url=url, data=form_data, headers=headers) body = json.loads(r.text) return body Access Tokenの有効期限は24時間固定となっている。 API 2.0 (Beta)では、Access Tokenの取得時のレスポンスに含まれるRefresh Tokenを使ってAccess Tokenの再発行ができる。 def refresh_access_token(client_id, client_secret, refresh_token): """アクセストークン更新""" url = 'https://auth.worksmobile.com/oauth2/v2.0/token' headers = { 'Content-Type': 'application/x-www-form-urlencoded' } params = { "refresh_token": refresh_token, "grant_type": "refresh_token", "client_id": client_id, "client_secret": client_secret, } form_data = params r = requests.post(url=url, data=form_data, headers=headers) body = json.loads(r.text) return body チャットボットの実装 LINE WORKSから送られてくるイベントを受けてその内容に応じて返答するチャットボット部分について実装する。 今回のチャットボット 今回は、送信された画像のサイズや種類、メタ情報を返すチャットボットを作る。 流れ ① メッセージ受信 ↓ ② 画像を取得 ↓ ③ 画像分析 (メタ情報等取得) ↓ ④ 結果を返信 (通知) ① メッセージ受信 POSTリクエストで受信し、リクエストボディに以下のドキュメントにあるフォーマットでメッセージについての情報が格納されている。 (補足) 改ざんチェックについて 既存のAPIでは、送られてくるリクエストの改ざんチェックを行うために、署名検証の仕組みが用意されている。 API 2.0 (Beta)では現在利用できないが、正式版では実装される予定となっている。 Beta版ではメッセージの改ざん検証は利用できません。 正式版リリース時に対応を予定しております。 ② 画像の取得 トークから画像が送信された際にサーバーが受信するメッセージの形式は以下のようになっている。 { "type":"message", "source":{ "userId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, "createdTime":1640281225908, "content":{ "type":"image", "fileId":"jp1.1640281225928569422.1640367625.2.3022841.10353252.125185387.11" } } 参考 ここには画像データは含まれておらず、含まれているcontent.fileIdを使って、以下のコンテンツダウンロードAPIから画像をダウンロードする。 def get_attachments(bot_id, file_id, access_token): """コンテンツダウンロード""" url = "https://www.worksapis.com/v1.0/bots/{}/attachments/{}".format(bot_id, file_id) headers = { 'Authorization' : "Bearer {}".format(access_token) } r = requests.get(url=url, headers=headers) r.raise_for_status() return r.content 実際は、まずはリダイレクトURLが取得され、そこに転送することでファイルデータを取得することができる。 使っているrequestsモジュールでは自動的にリダイレクトがされるため上記のような書き方となる。 ③ 画像分析 (メタ情報等取得) 上記で取得した画像データから、送信された画像のメタ情報などを取得する。今回はpillowモジュールを利用した。 コンテンツダウンロードAPIでバイナリデータとして取得されるため、これをioモジュールと組み合わせて読み込ませて情報を取得する。 from PIL import Image import io ... img = Image.open(io.BytesIO(img_data)) msg = "Size: {}\nFormat: {}\nMode: {}\nInfo: {}".format(img.size, img.format, img.mode, img.info) ④ 結果を返信 (通知) 画像の情報をトークに返信する。 テキストメッセージを返すため、contentのフォーマットは以下の通り。 { "content": { "type": "text", "text": msg } } 返信はメッセージ送信APIで行う。 BotIDや、受信メッセージに含まれているUser ID、Access Tokenを使って送信する。 def send_message(content, bot_id, user_id, access_token): """メッセージ送信""" url = "https://www.worksapis.com/v1.0/bots/{}/users/{}/messages".format(bot_id, user_id) headers = { 'Content-Type' : 'application/json', 'Authorization' : "Bearer {}".format(access_token) } params = content form_data = json.dumps(params) r = requests.post(url=url, data=form_data, headers=headers) r.raise_for_status() デプロイ 実装後、Cloud Runへデプロイし、また、その他認証に必要な情報についてもSecret Managerに登録する。 2. Botの登録と公開 従来通り、Developer Consoleの「Bot」で登録する。 Bot画面 その際に、「API Interface」の設定項目で、「従来のAPI」または「API 2.0 (beta)」を選択できるが、今回は、「API 2.0 (beta)」を選択する。 また、Callback URLにCloud RunのURLを登録し、チャットボットサーバーへメッセージイベントが送信されるようにする。 登録後、LINE WORKSのテナントの管理画面でBotを追加し、公開となる。 3. できたもの PNG画像を送った場合の結果。画像のサイズや種類等をテキストメッセージとして返答する。 (サイズにもよるが) 画像のダウンロードに時間がかかるようで、1分後に返信がくる。 4. ソースコード 以下、本チャットボットアプリのソースコード まとめ API 2.0 (Beta) となってAPIの全体の構成や認証周りが特に変わったが、従来のAPIと変わらずチャットボットを作る上で必要そうなAPIはしっかり揃っており、よりシンプルになったと感じた。 Beta版としてまだ十分に安定していないところもあるようなので、こちらもフィードバックを送りつつ正式版を期待したいと思う。
- 投稿日:2021-12-24T17:05:10+09:00
Azure Databricks で 高品質データを抽出してみました
概要 SEが日々登録している工数データが、そのSEの月々の出勤日数の8割以上であれば、そのSEの工数登録データは正しいとみなす、つまりこのSEが登録しているデータは高品質であると仮定するための ベーステーブルを作成するための手順を説明します。なお、上記8割以上のところを、7割、6割、5割、、、、と判断値を変えることにより最終的な分析データの品質確認も行うことを想定しています。 その高品質データ登録SEを抽出するためのテーブル作成の手順イメージは以下となります。 1.対象となるデータを各々のメタデータからデータを抽出 ・工数登録データ : shima ・出勤データ : kintai ・等級データ : grade 2.出勤データを整形 ・SEの出勤データの必要項目の選択と等級データからのName情報の連結 ・その後、SE毎の月毎の出勤回数を取得 : ikentai 3.工数登録データのデータ登録日の取得 ・SE毎の日毎の工数登録回数を取得 ・その後、SE毎の月毎の工数登録回数を取得 : imonth 4.高品質データを登録しているSEを判断するためのテーブルの新規作成 ・SE単位で月毎の工数登録回数と出勤回数のテーブルを作成 : trustse ローカル環境 macOS Monterey 12.0.1 python 3.8.3 Azure CLI 2.28.0 前提条件 Azure環境がすでに用意されていること(テナント/サブスクリプション)。 ローカル環境に「azure cli」がインストールされていること。 事前準備 Azure 環境の準備 ### ローカル環境変数の定義(Databricks関連) export RG_NAME=rg_ituru_bricks01 export SUBS_NAME=PSG2-01 export DB_WORKSPACE_NAME=dbw_ituru_workspace01 ### ローカル環境変数の定義(Gen2関連) export RG_GEN2=rg-DUPdatalake-01 export STORAGE_ACCOUNT=dupdatalake01 export STORAGE_CONT_BRONZE=bronze export STORAGE_CONT_SILVER=silver export STORAGE_CONT_GOLD=gold ## 使用するテナントへのログイン $ az login --tenant <tenant_id> ## 使用サブスクリプションの定義 $ az account set --subscription $SUBS_NAME ## 使用サブスクリプションの確認(IsDefault=True) $ az account list --output table ## Azure Databricks 用のリソースグループ作成 $ az group create --name $RG_NAME --location japaneast 既存ストレージアカウント情報の確認 ## ストレージアカウントの認証情報の取得 $ az storage account keys list --account-name $STORAGE_ACCOUNT --subscription $SUBS_NAME --resource-group $RG_GEN2 --output table CreationTime KeyName Permissions Value -------------------------------- --------- ------------- --------------------------------------------- 2021-12-10T00:42:19.082575+00:00 key1 FULL xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 2021-12-10T00:42:19.082575+00:00 key2 FULL zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz ## ストレージアカウントのコンテナ一覧の取得 $ az storage container list --account-name $STORAGE_ACCOUNT --output table Name Lease Status Last Modified -------------- -------------- ------------------------- bronze 2021-12-10T05:42:47+00:00 bronze-parquet 2021-12-20T06:19:47+00:00 rawdata 2021-12-10T05:05:02+00:00 silver 2021-12-13T08:14:08+00:00 silver-parquet 2021-12-21T07:12:27+00:00 ## コンテナー内の BLOB の一覧表示 $ az storage blob list --account-name $STORAGE_ACCOUNT --container-name $STORAGE_CONT_BRONZE --output table Azure Data Bricks の作成 14日間無料のトライアル版で構成します ## Azure Databricks Workspace の作成・・・完了まで数分かかります az databricks workspace create --resource-group $RG_NAME --name $DB_WORKSPACE_NAME --location japaneast --sku trial Spark クラスタの作成 Azure portal で、作成した Databricks サービスに移動し、「ワークスペースの起動」 を選択します Azure Databricks ポータルにリダイレクトされます。 ポータルで [New Cluster] を選択します 以下のパラメータ入力後、「Create Cluster」ボタンを押します。クラスターが作成され、起動されます 項目 値 Cluster Name db_ituru_cluster01 Cluster Mode Single Node Databrickes Runtime Version 9.1 LTS (Scala 2.12, Apache Spark 3.1.2) AutoPilotOprions Terminate after 45 minutes of inactivity Node Type Standard_DS3_v2 (14GB Memory 4Cores) Notebook の作成 Azure Databricks ポータルで [New Notebook] を選択します 以下のパラメータ入力後、「Create」ボタンを押します。新規の Notebook 画面が表示されます 項目 値 Name InputTrustSE01 Default Language Python Cluster db_ituru_cluster01 Notebook Notebook の実装 この記事 の内容を一部ほぼそのまま活用させていただきました。 Blobコンテナーのマウント cmd_1 # コンテナーのマウント # 一度マウントすると、Clusterを停止、変更してもマウント状態が維持されます # マウントされた状態で再度操作を実行するとエラーが発生するため、マウント状態をチェックする # Blob Storage情報(データ抽出用) storage = { "account": "dupdatalake01", "container": "bronze", "key": "<storage account key>" } # マウント先DBFSディレクトリ(データ抽出用) mount_point = "/mnt/bronze" try: # マウント状態のチェック mount_dir = mount_point if mount_dir[-1] == "/": mount_dir = mount_dir[:-1] if len(list(filter(lambda x: x.mountPoint == mount_dir, dbutils.fs.mounts()))) > 0: print("Already mounted.") mounted = True else: mounted = False # Blob Storageのマウント if not mounted: source = "wasbs://{container}@{account}.blob.core.windows.net".format(**storage) conf_key = "fs.azure.account.key.{account}.blob.core.windows.net".format(**storage) mounted = dbutils.fs.mount( source=source, mount_point = mount_point, extra_configs = {conf_key: storage["key"]} ) except Exception as e: raise e "mounted: {}".format(mounted) Blob Storageをマウントした ディレクトリ と ファイル の確認 cmd_2 # ディレクトリの確認 display(dbutils.fs.mounts()) # ファイルの確認(ファイルが存在しないとエラーが発生) # mount_point = "/mnt/{マウント先ディレクトリ}" display(dbutils.fs.ls(mount_point)) 対象ファイルの読込(データの抽出) cmd_3 # Blob から 工数登録データファイルの読込 --> PySpark Dataframes 型式 sdf = spark.read\ .option("header", "true").option("inferSchema", "true")\ .json('/mnt/bronze/tech_shima.json') display(sdf) # クエリとして扱うときのテーブルを作成 sdf.createOrReplaceTempView("shima") # Blob から 勤怠データファイルの読込 --> PySpark Dataframes 型式 sdf = spark.read\ .option("header", "true").option("inferSchema", "true")\ .json('/mnt/bronze/tech_kintai.json') display(sdf) # クエリとして扱うときのテーブルを作成 sdf.createOrReplaceTempView("kintai") # Blob から 等級データファイルの読込 --> PySpark Dataframes 型式 sdf = spark.read\ .option("header", "true").option("inferSchema", "true")\ .json('/mnt/bronze/tech_grade.json') display(sdf) # クエリとして扱うときのテーブルを作成 sdf.createOrReplaceTempView("grade") 出勤データの整形 cmd_4 # SEの出勤データの必要項目の選択と等級データからのName情報の連結 query_01 = """ SELECT kintai.J_Name, grade.Name, Kintai.K_Year, kintai.K_Month, kintai.K_Days From kintai Inner Join grade On kintai.J_Name = grade.J_Name """ qdf = spark.sql(query_01) display(qdf) # SE毎の月毎の出勤回数を取得する # その「月毎」は新たにカラムを追加しは毎月1日のdatatime型とする import pandas as pd import datetime # PySpark Dataframes から Pandas への変換 pdf = qdf.toPandas() # カラム['K_Year', 'K_Month']を連結させて毎月1日のカラムを新たに作成する(datetime型) pdf['K_Date'] = pd.to_datetime(pdf['K_Year'].astype(str)+'-'+pdf['K_Month'].astype(str)+'-01', format='%Y-%m-%d') # 不必要なカラムの削除後、Pandas から PySpark Dataframes への変換 df = spark.createDataFrame(pdf.drop(['K_Year', 'K_Month'], axis = 1)) display(df) # クエリとして扱うときのテーブルを作成 df.createOrReplaceTempView("ikintai") 工数登録データのデータ登録日の取得 cmd_5 # SE毎の日毎の工数登録回数を取得する query_01 = """ SELECT shima.userid, shima.date, count(shima.date) as input_cnt From shima Group by shima.userid, shima.date Order by shima.userid, shima.date """ qdf = spark.sql(query_01) display(qdf) # クエリとして扱うときのテーブルを作成 qdf.createOrReplaceTempView("inputse") # SE毎の月毎の工数登録回数を取得する query_02 = """ SELECT inputse.userid, date_trunc('MONTH', inputse.date) as dt_month, count(inputse.date) as input_cnt From inputse Group by inputse.userid, dt_month Order by inputse.userid, dt_month """ qdf = spark.sql(query_02) display(qdf) # クエリとして扱うときのテーブルを作成 qdf.createOrReplaceTempView("imonth") 高品質データを登録しているSEを判断するためのテーブルの新規作成 cmd_6 # SE単位で月毎の工数登録回数と出勤回数のテーブルを作成する query_01 = """ SELECT imonth.userid, ikintai.J_Name, imonth.dt_month, imonth.input_cnt, ikintai.K_Days From imonth Inner Join ikintai On imonth.userid = ikintai.Name and imonth.dt_month = ikintai.K_Date Order by imonth.userid, imonth.dt_month """ qdf = spark.sql(query_01) display(qdf) # クエリとして扱うときのテーブルを作成 qdf.createOrReplaceTempView("trustse") Azure Data Bricks リソースのクリーンアップ ## リソースをクリーンアップするには、ワークスペースを削除します az databricks workspace delete --resource-group $RG_NAME --name $DB_WORKSPACE_NAME ## ついでにリソースグループを削除する場合は以下を実行 az group delete --name $RG_NAME まとめ データ分析を実施するにあたり、データの品質が大事ということに気が付き、今回の記事を記載。ただ、かなり社内よりの内容なので、忘備録のような扱いになってしまいました、、、、、 参考記事 以下の記事を参考にさせていただきました。感謝申し上げます Azure Databricks: 3-1. DBFSにBlob Storageをマウント Spark DataframeのSample Code集
- 投稿日:2021-12-24T16:59:49+09:00
Djangoでマイグレーション時にrelated_nameの設定でエラー出た時の解消法
概要 PythonのフレームワークDjangoでmodels.pyの変更を行い、 マイグレーションを実行した際にrelated_nameの設定でエラーとなってしまった際の解消法を書きました 事象 このようにmodels.pyにデータベース設計したモデルを記載した後に、マイグレーションのコマンドを実行した。 models.py class Message(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='message_owner') ... class Group(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friend_owner') ... class Friend(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friend_owner') ... エラーメッセージ SystemCheckErrorが発生 qiita_app % python3 manage.py makemigrations sns SystemCheckError: System check identified some issues: ERRORS: sns.Friend.owner: (fields.E304) Reverse accessor for 'Friend.owner' clashes with reverse accessor for 'Group.owner'. HINT: Add or change a related_name argument to the definition for 'Friend.owner' or 'Group.owner'. sns.Friend.owner: (fields.E305) Reverse query name for 'Friend.owner' clashes with reverse query name for 'Group.owner'. HINT: Add or change a related_name argument to the definition for 'Friend.owner' or 'Group.owner'. sns.Group.owner: (fields.E304) Reverse accessor for 'Group.owner' clashes with reverse accessor for 'Friend.owner'. HINT: Add or change a related_name argument to the definition for 'Group.owner' or 'Friend.owner'. sns.Group.owner: (fields.E305) Reverse query name for 'Group.owner' clashes with reverse query name for 'Friend.owner'. HINT: Add or change a related_name argument to the definition for 'Group.owner' or 'Friend.owner'. 対応内容 models.pyのGroupとFriendのクラスのrelated_nameが「friend_owner」の同じ設定をしていたため、 適切な名称に変更 models.py class Message(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='message_owner') ... class Group(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='group_owner') ... class Friend(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friend_owner') ... 修正後実行 上記のようにmodels.pyを編集し、再度マイグレーションのコマンドを実行 qiita_app % python3 manage.py makemigrations sns Migrations for 'sns': sns/migrations/0001_initial.py - Create model Group - Create model Message - Create model Friend マイグレーション成功しました!
- 投稿日:2021-12-24T16:46:03+09:00
エンジニアの悪夢、緊急コールを実装する
はじめに こんにちは。 エンジニアの卵のなりかけです。 緊急コールとかやめて欲しいですよね(夜勤するから夜勤手当てください)。 「実装しようとしたんですが、工数めちゃくちゃかかるので無理です!」って言いたかったのに、簡単に作れちゃった.... 注) ここでの緊急コールは「エラー通知飛んでるから携帯見ろや」的なものを指します。 要するに、この電話がかかってきたら夜間だろうが休暇中だろうがSlackのエラー関連スレッド開いてねってことです。 作り方 設定ファイルを作る setting.json { "on":[0, 1, 2, 3, 4], "from":22, "end":7, "to":"XXXXXXXXXX" } on:緊急コールを行う曜日(0から月曜日で、6が日曜日) from:緊急コール機能開始時刻(午後10時からであれば22) end:緊急コール機能終了時刻(午前7時までであれば7) to:電話かける先(e.164フォーマット) 定数を設定する const.py """定数記述ファイル""" ACCOUNT_SID = "XXXXXXXXXX" AUTH_TOKEN = "YYYYYYYYYY" FROM_NUMBER = "+00000000000" 関数を作る main.py """緊急コールを仕込む""" # 定数ファイル読み込み import const from datetime import datetime, timedelta, timezone import json from twilio.rest import Client setting_file_path = "./setting.json" def isOnToday(day_number, on_days_list): """ 動作する日か確認 """ if day_number in on_days_list: return True return False def isWorkignHour(current_hour, start_hour, end_hour): """稼働時間か確認""" if current_hour >= start_hour or current_hour <= end_hour: return True return False def emergecyCall(setting_file_path): """緊急時にコールする""" with open(setting_file_path) as f: settings = json.load(f) JST = timezone(timedelta(hours=+9), 'JST') dt_now = datetime.now(JST) if not isOnToday(dt_now.weekday(), settings['on']): return if not isWorkignHour(dt_now.hour, settings['from'], settings['end']): return account_sid = const.ACCOUNT_SID # Your Account SID from www.twilio.com/console auth_token = const.AUTH_TOKEN# Your Auth Token from www.twilio.com/console client = Client(account_sid, auth_token) client.calls.create( to=settings['to'], from_=const.FROM_NUMBER, url="http://demo.twilio.com/docs/voice.xml" ) return if __name__ == "__main__": emergecyCall(setting_file_path) このemergecyCall関数をトライキャッチ文に仕込むとか、監視Botから呼び出すとかすれば緊急コールの完成です 最後に なぜ悪夢を現実化してしまったのか...
- 投稿日:2021-12-24T16:23:07+09:00
Youtube liveのコメントで安価出来るソフト作った話
簡易自己紹介 一般メカトロ高専生のKazuryuです ロボコンで制御を少しかじってました Twitter TL;DR Youtube liveのコメントで安価出来るソフト(AnkaChan☆)を作ったよ! Python Onlyで書いたよ! AnkaChan☆紹介 配布ページはこちら(GitHub) 機能紹介 Youtube liveのURLから簡単に安価が出来る! 安価の範囲選択や、再安価の機能もある! 絵師さんとかどうでしょう(ういママとか使ってくれないかな(ハナホジ 安価について 安価スレとは2ch用語で、簡単に言うとスレ主のこれからの行動・展開をほかのユーザーの書き込みに決めてもらうというものである 参考記事 なんj用語「安価スレとは?」 技術的要素 使用したライブラリ Threading(コメント取得の並列処理) Tkinter(GUI作成) --Qtのほうがよかったかな? Pytchat(コメント取得用ライブラリ) GitHub Pyinstaller(exe化) PyGithub(アップデート機能) ソースコード GitHubにあります こちら 苦労点 Threadingの定期実行や並列処理の理解が甘く、実装に時間がかかった。まだ理解が甘い点がある。 再安価や削除用にデータとWindowオブジェクトの紐づけがややこしくなり、実装に時間がかかった(今回はdatetimeのsha256で紐づけ) おわりに Outputが大事って偉い人が言ってた
- 投稿日:2021-12-24T16:21:42+09:00
Flask + SQLAlchemy でよさそうなトランザクション管理
よくあるやつ Transactions using Flask-SQLAlchemy にあるように from contextlib import contextmanager @contextmanager def transaction(): try: yield db.session.commit() except Exception: db.session.rollback() raise を作成して、それを def hogehoge(): with transaction(): ... と処理を書く。 記述ところが少しなら良いけど、色んなところにあると、少しめんどくさい。 よさそうなやつ from functools import wraps def transactional(func): @wraps(func) def decorator(*args, **kwargs): if db.session.info["in_transaction"]: return func(*args, **kwargs) else: db.session.info["in_transaction"] = True try: result = func(*args, **kwargs) db.session.commit() return result except Exception as e: db.session.rollback() raise e finally: db.session.info["in_transaction"] = False return decorator @transactionalアノテーションを用意して、それをトランザクション管理したいところに付与する。 @transactional def hogehoge(): ... 基本的に、トランザクション管理する時ってfunction単位でやると思うので、 個人的にはこっちの方が良いなって思う。
- 投稿日:2021-12-24T14:26:30+09:00
strftime / strptimeのフォーマット
良く忘れるのでジャンルごとに分けたよ、というだけのもの。 公式はこちら https://docs.python.org/ja/3/library/time.html#time.strftime ジャンル 表現 内容 年 %y 上 2 桁なしの西暦年を表す 10 進数 [00,99]。 年 %Y 上 2 桁付きの西暦年を表す 10 進数。 月 %b ロケールにおける省略形の月名。 月 %B ロケールにおける省略なしの月名。 月 %m 月を表す 10 進数 [01,12]。 日 %j 年の初めから何日目かを表す 10 進数 [001,366]。 日 %d 月の始めから何日目かを表す 10 進数 [01,31]。 日 %x ロケールにおける適切な日付の表現。 曜日 %a ロケールにおける省略形の曜日名。 曜日 %A ロケールにおける省略なしの曜日名。 曜日 %w 曜日を表す 10 進数 [0(日曜日),6]。 時 %H (24 時間計での) 時を表す 10 進数 [00,23]。 時 %I (12 時間計での) 時を表す 10 進数 [01,12]。 分 %M 分を表す 10 進数 [00,59]。 秒 %S 秒を表す 10 進数 [00,61]。 週 %U 年初からの週数 (日曜始まり) を表す 10 進数 [00,53]。年が明けてから最初の日曜日までのすべての曜日は 0 週目。 週 %W 年初からの週数 (月曜始まり) を表す 10 進数す [00,53]。年が明けてから最初の月曜日までの全ての曜日は 0 週目。 他 %c ロケールにおける適切な日付および時刻表現。 他 %p ロケールにおける AM または PM に対応する文字列。 他 %X ロケールにおける適切な時刻の表現。 他 %Z タイムゾーンの名前 (タイムゾーンがない場合には空文字列)。 他 %% 文字 “%” 自体の表現。
- 投稿日:2021-12-24T13:00:46+09:00
テイラー展開
はじめに 理想波形をテイラー展開によって近似し,プログラムを用いてグラフに表します. 今回はeを底とする指数関数とsinとcos関数をテイラー展開します. 目次 1. 求めたい関数のテイラー展開を計算 ・テイラー展開に必要なライブラリ ・求めたい理想波形 ・結果 2.求めた関数のグラフを描画 ・グラフ描画に必要なライブラリ ・グラフの設定 ・グラフの描画 3. 参考 求めたい関数のテイラー展開を計算 テイラー展開に必要なライブラリ 求めたい関数のテイラー展開を計算するのに必要なライブラリを入れます. ライブラリの導入 from sympy import * import numpy as np 今回はsympyとnumpyを使ってテイラー展開を計算します. 求めたい理想波形 求めたい理想波形を生成します. 理想波形を生成 # 理想波形を生成 cal_x = np.arange(-10, 10.1, 0.1) exp_cal_y = np.exp(cal_x) sin_cal_y = np.sin(cal_x) cos_cal_y = np.cos(cal_x) numpy.arange(start,stop,step) グラフはx軸を-10から10.1の間に0.1間隔で点を生成します. 理想波形のテイラー展開 # シンボルを定義 x = Symbol("x") # 任意点周りのテイラー展開を精度違いで計算する exp_sol = [] # 結果を入れるリスト sin_sol = [] cos_sol = [] a = 0 # 任意点を指定 print("e^xのx="+str(a)+"周りのテイラー展開は") for i in range(6): exp_taylor = series(exp(x), x=x, x0=a, n=i+3).removeO() # removeO()で剰余項を除去 print("n="+str(i+2)+"のとき、") print(exp_taylor) # 式を表示させて確認 exp_taylor_y = lambdify(x, exp_taylor, "numpy") # numpyの関数に変換 exp_sol.append(exp_taylor_y(cal_x)) # 関数に値を入れて曲線を計算 print() print("sinxのx="+str(a)+"周りのテイラー展開は") for i in range(6): sin_taylor = series(sin(x), x=x, x0=a, n=i+3).removeO() print("n="+str(i+2)+"のとき、") print(sin_taylor) sin_taylor_y = lambdify(x, sin_taylor, "numpy") sin_sol.append(sin_taylor_y(cal_x)) print() print("cosxのx="+str(a)+"周りのテイラー展開は") for i in range(6): cos_taylor = series(cos(x), x=x, x0=a, n=i+3).removeO() print("n="+str(i+2)+"のとき、") print(cos_taylor) cos_taylor_y = lambdify(x, cos_taylor, "numpy") cos_sol.append(cos_taylor_y(cal_x)) ここではa=0まわりのテイラー展開(マクローリン展開)をしていますが,別の点でもできます. ▼入力 底がeである指数関数を理想波形としたテイラー展開 sin関数を理想波形としたテイラー展開 cos関数を理想波形としたテイラー展開 ▼出力 テイラー展開した形 e^xのx=0周りのテイラー展開は n=2のとき, x*2/2 + x + 1 n=3のとき, x3/6 + x2/2 + x + 1 n=4のとき, x4/24 + x3/6 + x2/2 + x + 1 n=5のとき, x5/120 + x4/24 + x3/6 + x2/2 + x + 1 n=6のとき, x6/720 + x5/120 + x4/24 + x3/6 + x2/2 + x + 1 n=7のとき, x7/5040 + x6/720 + x5/120 + x4/24 + x3/6 + x*2/2 + x + 1 sinxのx=0周りのテイラー展開は n=2のとき, x n=3のとき, -x*3/6 + x n=4のとき, -x3/6 + x n=5のとき, x5/120 - x3/6 + x n=6のとき, x5/120 - x3/6 + x n=7のとき, -x7/5040 + x5/120 - x*3/6 + x cosxのx=0周りのテイラー展開は n=2のとき, 1 - x*2/2 n=3のとき, 1 - x2/2 n=4のとき, x4/24 - x2/2 + 1 n=5のとき, x4/24 - x2/2 + 1 n=6のとき, -x6/720 + x4/24 - x2/2 + 1 n=7のとき, -x6/720 + x4/24 - x*2/2 + 1 結果 プログラムを用いて関数をテイラー展開し,理想波形を多項式に展開(変換)しました. 求めた関数のグラフを描画 上でテイラー展開して求めた関数をグラフに描画します. グラフ描画に必要なライブラリ グラフに描画するのに必要なライブラリを入れます. ライブラリの導入 import matplotlib.pyplot as plt 今回はmatplotlibを使ってグラフを描画します. グラフの設定 どのようなグラフを作りたいか,フォントやスケールの大きさなどを設定します. グラフの設定 # フォントのサイズと種類を設定 plt.rcParams["font.size"] = 10 plt.rcParams["font.family"] = 'Times New Roman' # 目盛を内側に変更 plt.rcParams["xtick.direction"] = "in" plt.rcParams["ytick.direction"] = "in" #グラフのサイズを変更 fig = plt.figure(figsize = (15,5)) #グラフの位置を配分 exp_ax = fig.add_subplot(131) sin_ax = fig.add_subplot(132) cos_ax = fig.add_subplot(133) # 軸のラベルとスケールを設定 exp_ax.set_xlabel('x') exp_ax.set_ylabel('y') exp_ax.set_xlim(-10, 10) exp_ax.set_ylim(-10, 10) sin_ax.set_xlabel('x') sin_ax.set_ylabel('y') sin_ax.set_xlim(-10, 10) sin_ax.set_ylim(-10, 10) cos_ax.set_xlabel('x') cos_ax.set_ylabel('y') cos_ax.set_xlim(-10, 10) cos_ax.set_ylim(-10, 10) #グラフのタイトルを設定 exp_ax.set_title("e^x") sin_ax.set_title("sinx") cos_ax.set_title("cosx") グラフの描画 上で決めたグラフの設定を用いて,テイラー展開した関数を描画します. グラフの描画 # データをプロット #理想波形をプロット exp_ax.plot(cal_x, exp_cal_y, label="Theory", lw=3, color="black") #テイラー展開の結果をプロット for j in range(len(exp_sol)): exp_ax.plot(cal_x, exp_sol[j], label="Series:n=" + str(j+2)) sin_ax.plot(cal_x, sin_cal_y, label="Theory", lw=3, color="black") for j in range(len(sin_sol)): sin_ax.plot(cal_x, sin_sol[j], label="Series:n=" + str(j+2)) cos_ax.plot(cal_x, cos_cal_y, label="Theory", lw=3, color="black") for j in range(len(cos_sol)): cos_ax.plot(cal_x, cos_sol[j], label="Series:n=" + str(j+2)) # グラフを表示 plt.legend() plt.show() plt.close() ▼出力 参考 今回参考にさせて頂いたサイト(https://watlab-blog.com/2020/05/05/sympy-taylor-series/ )
- 投稿日:2021-12-24T12:45:52+09:00
AWS Lambda layersを作成/管理できるツールlamblayerを作った
AWS Lambda layersを作成/管理できるツールlamblayerを作りました。 現在はPythonにのみ対応しています。 できること pipでinstallできるpakcageのビルド済みlayerを作成する(←これがとっても便利) ローカルのディレクトリからlayerを作成する Lambdaにlayerを設定する jsonの設定ファイルからパラメータを読み取って内部でLambdaのAPIを叩いています。 Quick Start 現在はpipでのみインストール可能です。 pip install git+https://github.com/YU-SUKETAKAHASHI/lamblayer.git@v0.1.0 lamblayer initで既存のLambdaを取り込みます。 $ mkdir quick_start $ cd quick-start $ lamblayer init --function-name quick_start 2021-12-24 10:13:41,225: [INFO]: lamblayer : v0.1.0 2021-12-24 10:13:42,132: [INFO]: starting init quick_start 2021-12-24 10:13:42,318: [INFO]: createing function.json 2021-12-24 10:13:42,319: [INFO]: completed するとfunction.jsonが生成されるので、以降はfunction.jsonを編集してlamblayer setでLambdaにlayerをセットすることができます。 $ lamblayer set 2021-12-24 10:23:34,041: [INFO]: lamblayer : v0.1.0 2021-12-24 10:23:35,312: [INFO]: starting set layers to quick_start 2021-12-24 10:23:35,723: [INFO]: completed Create layer lamblayer createでlayerを作成することもできます。 pipでインストール可能なパッケージのビルド済みlayerの作成と、 ローカルのディレクトリからlayerを作成することの2種類ができます。 1. pipでインストール可能なパッケージのビルド済みlayerの作成 こんな感じにパッケージ名とバージョンをjsonで書けば、 lamblayer creage --packages packages.jsonでLambdaで実行可能なビルド済みのlayerが作成されます。 これがとっても便利で、Dockerを立ち上げてpip installしてzipつくって...という作業から解放されます。最高! 内部ではこちらの記事のAPIを叩いています。 packages.json { "Arch": "x86_64", "Runtime": "py38", "Packages": [ "numpy==1.20.2", "requests" ] } 作成するlayerの名前や実行環境などは別にjsonで書いてください。--layerオプションでファイルを指定できます。 layer.json { "LayerName": "numpy_requests", "Description": "numpy==1.20.2, requests", "CompatibleRuntimes": [ "python3.7", "python3.8", "python3.9" ], "LicenseInfo": "" } ※まだこちらの記事のAPIが公開されていないため、現在はこの機能は利用できません? Coming soon! 2. ローカルのディレクトリからlayerを作成する ローカルのディレクトリを指定して、自作パッケージのlayerを作成することもできます。 lamblayer create --src my_package layerを作成するときの地味な沼ポイントとして、ファイルたちをpythonディレクトリ以下に配置する必要がありますが、それもlamblayerがやってくれます。 --wrap-dirオプションでディレクトリ名を指定してあげれば、--src以下のファイルをラップしたlayerを作成することができます。 これで自分でpythonフォルダに配置し直す手間はありません。 lamblayer create --src my_package --wrap-dir1 python Set Layer lamblayer setでLambdaにlayerをセットすることができます。こちらは--functionオプションで設定ファイルを指定できます。 function.json { "FunctionName": "quick_start", "Layers": [ "arn:aws:lambda:ap-northeast-1:249908578461:layer:AWSLambda-Python38-SciPy1x:29", "numpy_requests", "my_package" ] } function.jsonでlayer名のみを指定した場合は自動で最新のバージョンのARNに補完してくれます。これもとっても便利で、lamblayer createで作成した最新バージョンのlayerをセットするには、function.jsonにlayer名のみを指定してlamblayer setすればOKです。 その他のcommand 現在は、init, create, set, listコマンドがあります。 最後に GitHub Actionsでも利用できるようにlamblayerをインストールするactionも作りました。 詳細はREADMEに記してあるのでご覧ください。 どうぞご利用ください!
- 投稿日:2021-12-24T12:45:00+09:00
GoogleAPIの使用について
GoogleAPIの使用についてちょっと嵌ったところがあったのでメモ書きと簡単なコードを紹介します。 目的 同じような問題で詰まっている方がいれば解決方法をシェアしてハッピーになってもらえればと GoogleAPIってどんなことが出来るの?が少しでもシェアできればと それでは GoogleAPIの説明やサービスアカウント等の話は省きます。 ドキュメント読めば書いてあるのでドキュメント読んでください。 本記事では、GoogleWorkSpaceの特定ユーザーがメンバーとなっているGroupを一覧化する方法を紹介します。 嵌ったこと ドキュメント通りにGCPでProjectを立ち上げ、サービスアカウントを作成、鍵(JSON)をダウンロードしてコードを実行したのですがうまくいかず、認可されていないとのエラーに悩んでました。 message': 'Not Authorized to access this resource/api 結論 GoogleWorkSpace管理上でロールを割り当ててなかったことが原因でした。 ロールを作成して、今回はグループ操作をしたので、グループのみに権限を与え(チェックを入れ)、サービスアカウントに割り当てて解決 サンプルコード qiita.py #Googleクライアントライブラリ from googleapiclient.discovery import build from google.oauth2.service_account import Credentials SECRETS = '[秘密鍵].json' SCOPES = ['https://www.googleapis.com/auth/admin.directory.group'] def Get_GoogleGroup(): #クレデンシャルインスタンスの作成 credentials = Credentials.from_service_account_file(SECRETS) #スコープの追加 scoped_credentials = credentials.with_scopes(SCOPES) service = build('admin', 'directory_v1', credentials=scoped_credentials) results = service.groups().list(domain='hoge.com', userKey='test@hoge.com').execute() results = results['groups'] groups = [] for i in range(len(results)): groups.append(results[i]['email']) print(groups)
- 投稿日:2021-12-24T11:47:43+09:00
ニュースをTF-IDFで解析
はじめに 自然言語について勉強を始めたばかりの初心者です。 ニュース記事の文章を、いくつか用意して、TF-IDFで類似性を解析しました。 TF-IDFについては、詳しいサイトが沢山あるのでここでの説明は省略します。 なぜ形態素解析しないのか 自然言語処理では、形態素解析するのが一般的のようです。 しかし、ニュースの記事は「公選法違反容疑」のような熟語と熟語の組み合わせが 多くあります。こういった場合、「公選法」と「違反」と「容疑」に分割されてしまうので 別途、自前でユーザ辞書作成など処理が必要です。 色々試しましたが、うまいことするには、まだ私のレベルが不足しているので 一旦は、漢字だけ抜き出して解析することにしました。 環境 Google Colaboratory 使用するデータについて ニュース記事は、最初の文章に記事の要約が記載されているのが特徴です。 その最初の文章を解析することにしました。 ですが、そのまま利用するわけにもいかないため 実際にあったニュース記事を参考にし、架空のニュース記事を作成しました。 ・日付も重要なので漢数字に手で修正 例) 10月31日 ⇒ 十月三十一日 ・年齢なども解析対象としたいので漢数字に手で修正 import regex import numpy as np a = '投開票の衆院選で山口三区から出馬して当選した民自党の田中外相の後援会入会を神奈川県職員に勧めたとして、県警は、公選法違反容疑で、同県の松早川副知事らを書類送検した。' b = '衆院選(十月三十一日投開票)の比例代表で、憲法立民主党と日本民自主党が同一の党名略称である「主民党」を使用したことで、県内の約二万票が投票先の判然としない案分票となったことが十五日、県選管への取材で分かった。' c = '憲法立民主党の金本呂治参院議員(七十三)=北海道選挙区=は二十一日、改選を迎える来年夏の参院選道選挙区(改選数三)に立候補をしない意向を表明した。' d = '憲法立民主党の金本呂治参院議員(七十三)=道選挙区=は二十一日、来年夏の参院選道選挙区(改選数三)に立候補しない意向をフェイスブックで明らかにした。' e = '英国では四二十万人の子供が貧困に苦しんでいる、ほぼ三人に一人だ。衝撃的な数字だ。' f = '憲法立民主党の金本呂治参議院議員が、来年行われる参院選に立候補しない意向を表明しました。' docs = [a, b, c, d, e, f] cとdとfが同じニュースについて、別々の文章で記載された記事となります。 ですので、cとdとfが類似性が高いようになれば成功です。 aとb、eは、まったく別のニュースとなります。 TF-IDFの計算 TF-IDFの関数 def tfidf(word, sentence): # term frequency #文章sentenceの中で指定した単語wordが #どの程度出現するのか頻度を計算します。 tf = sentence.count(word) / len(sentence) #ある単語の文書間でのレア度を計算します。 # inverse document frequency idf = np.log10(len(docs) / sum([1 for doc in docs if word in doc])) return round(tf*idf, 4) TF-IDFの関数を動かしてみる ニュース記事aの中で、「憲法立民主党」TF-IDFを確認します。 tfidf('憲法立民主党', a) 結果 0.0 0となりました。'憲法立民主党'という単語はニュース記事aには ありませんので、想定した通りの結果となりました。 続いて、ニュース記事b,c,d,eについてもそれぞれ確認していきます。 ニュース記事bの中で、「憲法立民主党」のTF-IDFを確認します。 tfidf('憲法立民主党', b) 結果 0.0017 ニュース記事cの中で、「憲法立民主党」のTF-IDFを確認します。 tfidf('憲法立民主党', c) 結果 0.0024 ニュース記事dの中で、「憲法立民主党」のTF-IDFを確認します。 tfidf('憲法立民主党', d) 結果 0.0024 ニュース記事eの中で、「憲法立民主党」のTF-IDFを確認します。 tfidf('憲法立民主党', e) 結果 0.0 ニュース記事eは、関係のない記事です。想定どおり、結果が0となりました。 最後にニュース記事fの中で、「憲法立民主党」のTF-IDFを確認します。 tfidf('憲法立民主党', f) 結果 0.004 TF-IDFベクトルの計算 すべての文書に含まれるすべての単語の集合を、リストvocabとして作成します。 次に、すべての文書のすべての単語について、TF-IDFスコアを計算します。 全記事のボキャブラリーリスト作成 ニュース記事aから漢字のみを取り出して、リスト化します。 la = [] #a用リスト la = regex.findall("\p{Han}+", a) print(la) 実行結果 ['投開票', '衆院選', '山口三区', '出馬', '当選', '民自党', '田中外相', '後援会入会', '神奈川県職員', '勧', '県警', '公選法違反容疑', '同県', '松早川副知事', '書類送検'] このようにして、リストvacabに全単語を登録します。 lb = [] #b用リスト lb = regex.findall("\p{Han}+", b) lc = [] #c用リスト lc = regex.findall("\p{Han}+", c) ld = [] #d用リスト ld = regex.findall("\p{Han}+", d) le = [] #e用リスト le = regex.findall("\p{Han}+", e) lf = [] #f用リスト lf = regex.findall("\p{Han}+", f) vocab = set(la+lb+lc+ld+le+lf) print(vocab) 実行結果 {'分', '金本呂治参院議員', '道選挙区', '山口三区', '十月三十一日投開票', '判然', '苦', '七十三', '約二万票', '勧', '三人', '英国', '四二十万人', '子供', '民自党', '貧困', '松早川副知事', '参院選', '二十一日', '来年行', '使用', '党名略称', '投票先', '取材', '比例代表', '同県', '同一', '田中外相', '一人', '公選法違反容疑', '県選管', '書類送検', '当選', '出馬', '十五日', '北海道選挙区', '意向', '主民党', '衝撃的', '明', '金本呂治参議院議員', '衆院選', '表明', '後援会入会', '県警', '投開票', '日本民自主党', '県内', '改選', '参院選道選挙区', '改選数三', '迎', '立候補', '来年夏', '数字', '案分票', '憲法立民主党', '神奈川県職員'} TF-IDFベクトルの計算 各ニュース記事について、TF-IDFベクトルを計算します。 # initialize vectors vec_a = [] vec_b = [] vec_c = [] vec_d = [] vec_e = [] vec_f = [] for word in vocab: vec_a.append(tfidf(word, a)) vec_b.append(tfidf(word, b)) vec_c.append(tfidf(word, c)) vec_d.append(tfidf(word, d)) vec_e.append(tfidf(word, e)) vec_f.append(tfidf(word, f)) 結果確認 結果を確認します。print()で値を表示させるだけだとわかりにくいので エクセルで、結果をまとめて表にしました。 まとめ 同じ内容を扱っているニュース記事cdfとそれ以外で、異なる傾向が見られます。
- 投稿日:2021-12-24T11:17:12+09:00
[Python][img2pdf] 画像ファイルを PDF に一括変換(コマンドライン)
前回の逆をコマンドラインにて。 ディレクトリ内の全ての画像ファイルを一つの PDF ファイルに出力。 PDF ファイルに画像ファイルを埋め込んでいるので、PDF ファイルサイズは画像ファイル依存。したがって、バイナリレベルは非可逆的。見た目は拡大しなければ、実用的に可逆。 png のアルファチャンネル削除処理にて、OpneCV を使用。破壊的処理なので、テンポラリディレクトリにて処理。 natsort にて"自然順"アルゴリズムで画像ファイルをソート。 -s オプションで画像ファイル個別に PDF ファイルを出力可。 verbose に拘ったら、ダラダラと冗長になってしまった。 imgtopdf.py import sys import os import glob import tempfile import shutil import argparse import img2pdf from natsort import natsorted import cv2 def create_parser(): parser = argparse.ArgumentParser() parser.add_argument( "source", type=str, help="This is pdf source file or dir. (Specify a file or directory. Wildcards cannot be used.)" ) parser.add_argument( "destination", type=str, nargs="?", default=None, help="This is destination file or dir." ) parser.add_argument( "-s", "--split", action="store_true", help="Output file is a separate file. Pdf filename follows image file." ) parser.add_argument( "-v", "--verbose", action="store_true", help="Give more output." ) return parser def convert_pdf(input, output, source, destination, split, verbose): input_list = glob.glob(input + "/*") if split: for file in natsorted(input_list): origin_source = os.path.join(source, os.path.basename(file)) single_convert_pdf(file, output, origin_source, destination, verbose) else: if verbose: print(source + "*", end=" > ") if output == "": output = os.path.join(destination, "new.pdf") try: with open(output, "wb") as f: f.write(img2pdf.convert([str(i) for i in natsorted(input_list)])) except Exception as e: print(e) sys.exit(1) if verbose: print(output) def single_convert_pdf(input, output, source, destination, verbose): if verbose: print(source, end=" > ") if output == "": root, ext = os.path.splitext(input) file_name = os.path.basename(root) output = os.path.join(destination, file_name + ".pdf") try: with open(output, "wb") as f: f.write(img2pdf.convert([input])) except Exception as e: print(e) sys.exit(1) if verbose: print(output) def alphachannel_erase(filename): try: img = cv2.imread(filename,cv2.IMREAD_UNCHANGED) if img.shape[2] == 4: img2 = cv2.imread(filename,cv2.IMREAD_COLOR) cv2.imwrite(filename, img2) except Exception as e: print(e) sys.exit(1) def makedir(path): try: os.makedirs(path, exist_ok=True) except FileExistsError as e: print("ERROR: Destination is " + e.filename) sys.exit(1) def main(): parser = create_parser() args = parser.parse_args() source = args.source split = args.split verbose = args.verbose output = "" destination = "." if args.destination is None else args.destination if split: if destination.endswith(".pdf"): makedir(os.path.dirname(destination)) destination = os.path.dirname(destination) else: makedir(destination) else: if destination.endswith(".pdf"): makedir(os.path.dirname(destination)) output = destination destination = os.path.dirname(destination) else: makedir(destination) if verbose: print("**** Start ****") if os.path.isfile(source): root, ext = os.path.splitext(source) file_name = os.path.basename(root) if ext == ".png": try: with tempfile.TemporaryDirectory() as dname: shutil.copy(source, dname) input = os.path.join(dname, file_name + ext) alphachannel_erase(input) single_convert_pdf(input, output, source, destination, verbose) except Exception as e: print(e) sys.exit(1) else: single_convert_pdf(source, output, source, destination, verbose) elif os.path.isdir(source): png_files = glob.glob(source + "/*.png") if png_files == []: convert_pdf(source, output, source, destination, split, verbose) else: try: with tempfile.TemporaryDirectory() as dname: shutil.copytree(source, dname, dirs_exist_ok=True) png_files = glob.glob(dname + "/*.png") for file in png_files: alphachannel_erase(file) convert_pdf(dname, output, source, destination, split, verbose) except Exception as e: print(e) sys.exit(1) else: print("ERROR: Source does not exist.") sys.exit(1) if verbose: print("**** Complete! ****") if __name__ == '__main__': main() github はこちら。 参考 https://phst.hateblo.jp/entry/2021/05/28/120000
- 投稿日:2021-12-24T11:04:11+09:00
魚眼ステレオカメラの距離推定(実装編)
はじめに ※これはRICORA Advent Calendar 12/23 の記事になります。 本記事はカメラのキャリブレーションから始めて距離推定を行うまでの実装について説明を行います。 コードの全体は私のgithubレポジトリにあります。⇒Fisheye-Depth-Estimation もし理論編の方を読んでいなければそちらから読むことをおススメします。前提知識がある方はこちらからでも大丈夫です。 目次 ステレオキャリブレーション ステレオ平行化 2Dルックアップテーブル 正距円筒変換 視差推定 距離推定 実行結果 さっそくコードを見ていきましょう。クラスで書いているので少し見づらいですがご了承ください。m(__)m 1. ステレオキャリブレーション まずはカメラのキャリブレーションからです。カメラ画像の歪みを補正するためのパラメーターを求めます。 FisheyeCalibrate.py def stereo_calibrate(self, images, image_size, board=(6,9), square_size=0.027, detail=False): # images -> calibrate params(left K, right K, left D, right D) self._image_size = image_size[::-1] self._board = board self._square_size = square_size print(f"Board type: rows{board[0]}, cols{board[1]}") assert isinstance(images, list), "images must be list" # Corners detection print("detecting corners ...") left_corners = [] right_corners = [] object_point = self._create_object_point() object_points = [] for image in tqdm(images): left_corner, right_corner = self._detect_corners(image) # All corners must be detected. n_corners = (rows * cols) assert len(left_corner)==len(right_corner)==board[0]*board[1], "Corners detection failed." left_corners.append(left_corner.reshape(1,-1,2)) right_corners.append(right_corner.reshape(1,-1,2)) object_points.append(object_point) print("corners detection was successful.") print("stereo calibrating ...") self._rvec = [np.zeros((1, 1, 3), dtype=np.float64) for i in range(len(images))] self._tvec = [np.zeros((1, 1, 3), dtype=np.float64) for i in range(len(images))] self._ret, self._left_K, self._left_D, self._right_K, self._right_D, self._rvec, self._tvec = cv2.fisheye.stereoCalibrate( objectPoints=object_points, imagePoints1=left_corners, imagePoints2=right_corners, K1=self._left_K, D1=self._left_D, K2=self._right_K, D2=self._right_D, imageSize=self._image_size, flags=cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC ) print("stereo calibration was successful.") if detail: self.show_stereo_params() stereo_params_dict = { "left_K": self._left_K, "right_K": self._right_K, "left_D": self._left_D, "right_D": self._right_D, "rvec": self._rvec, "tvec": self._tvec } return stereo_params_dict imagesにはチェッカーボードの映った画像を20枚ほど渡します。image_sizeには(2560,960)のように画像サイズを渡します。boardはチェッカーボードの形状(h,w)です。square_sizeはマス目の一辺の長さ(メートル)です。 object_pointは各コーナーが左上を(0,0)としてどの座標にあるかをメートル単位で保持している配列です。 まずは各チェッカボード画像のコーナー検出を行っていきます。その作業を行っている関数が_detect_cornersです↓ FisheyeCalibrate.py def _create_object_point(self): # _ -> object points grid refered board object_point = np.zeros((1, self._board[0]*self._board[1], 3), np.float32) object_point[0,:,:2] = np.mgrid[0:self._board[0], 0:self._board[1]].T.reshape(-1, 2) object_point = object_point*self._square_size return object_point def _detect_corners(self, image): # image -> left half corners point, right corners point left_image, right_image = self._split_image(image) flag = cv2.CALIB_CB_ADAPTIVE_THRESH+cv2.CALIB_CB_FAST_CHECK+cv2.CALIB_CB_NORMALIZE_IMAGE criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) left_ret, left_corner = cv2.findChessboardCorners(left_image, self._board, flag) if len(left_corner)>0: for points in left_corner: cv2.cornerSubPix(left_image, points, winSize=(3, 3), zeroZone=(-1,-1), criteria=criteria) right_ret, right_corner = cv2.findChessboardCorners(right_image, self._board, flag) if len(right_corner)>0: for points in right_corner: cv2.cornerSubPix(right_image, points, winSize=(3, 3), zeroZone=(-1,-1), criteria=criteria) return left_corner, right_corner OpenCVのfindChessboardCorners関数を使うことでコーナーを検出しています。さらに検出したコーナー座標(整数)をcornerSubPixという関数で小数点まで求めます。これをサブピクセル処理と言います。 後は求まったコーナー座標をfisheye.stereoCalibrateに渡すと補正に必要なパラメーターが求まります。 ここでKはカメラマトリックス、Dは歪み係数ベクトルです。 K = \begin{pmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{pmatrix} \qquad D = (k_1,k_2,k_3,k_4) ここでfは焦点距離cは中心座標を表しています。OpenCVにはこのパラメーターを使って画像を補正してくれる関数が存在しますがここでは使いません。補正に関する式なども含めて詳しくはOpenCV公式リファレンスを参照してください。 2.ステレオ平行化 FisheyeCalibrate.py def stereo_rectify(self, detail=False): # _ -> rectify params(left R, right R, left P, right R, Q) print("stereo rectify...") flags = cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC self._left_R, self._right_R, self._left_P, self._right_P, self._Q = cv2.fisheye.stereoRectify( K1=self._left_K, D1=self._left_D, K2=self._right_K, D2=self._right_D, imageSize=self._image_size, R=self._rvec, tvec=self._tvec, flags=cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC, newImageSize=(int(self._image_size[0]*self._mag), int(self._image_size[1]*self._mag)), fov_scale=self._mag ) print("stereo rectification was successful.") if detail: self.show_rectify_params() rectify_params_dict = { "left_R": self._left_R, "right_R": self._right_R, "left_P": self._left_P, "right_P": self._right_P, "Q": self._Q } return rectify_params_dict 先ほどと違ってこちらの操作は非常にシンプルです。ステレオキャリブレーションで求めたパラメーターをfisheye.stereoRectify関数に渡すだけです。説明をしていませんでしたがRとtvecはそれぞれカメラ間の変換を行うための行列とベクトルですステレオキャリブレーションの際に求まります。 fov_scaleについてですが、これはどれぐらい広く画角を取るかを決めるための値です。(fov=Field of view) 今回はここに2を渡して2倍の画角で平行化画像を取得します。 この関数によって平行化変換の回転行列R(3*3)と平行座標系に投影を行うための行列P(3*4)が求まります。これらを使って平行化画像を生成することができます。 3. 2Dルックアップテーブル 理論編で登場していない単語が出てきました。2Dルックアップテーブルについて解説します。 2Dルックアップテーブルとは計算を高速化するためのマッピング行列です。マッピングとは点と点を結びつけることです。 先ほど求めたパラメーターからそれぞれの画素がどの座標に移動するかを計算しておいて、それを適用することで素早く画像の変換を行うことができます。 パラメーターを使って変換をしようとすると、各画素に対して毎回行列の計算を行う必要があり時間がかかってしまいます。 FisheyeCalibrate.py def create_rectify_map(self): # _ -> rectify maps(left x, left y, right x, right y) self._left_rectify_map_x, self._left_rectify_map_y = cv2.fisheye.initUndistortRectifyMap( K=self._left_K, D=self._left_D, R=self._left_R, P=self._left_P, size=(int(self._image_size[0]*self._mag), int(self._image_size[1]*self._mag)), # size=self._image_size, m1type=cv2.CV_32FC1 ) self._right_rectify_map_x, self._right_rectify_map_y = cv2.fisheye.initUndistortRectifyMap( K=self._right_K, D=self._right_D, R=self._right_R, P=self._right_P, size=(int(self._image_size[0]*self._mag), int(self._image_size[1]*self._mag)), # size=self._image_size, m1type=cv2.CV_32FC1 ) print("rectify lookup table ceraeted.") rectify_map_dict = { "left_map_x": self._left_rectify_map_x, "left_map_y": self._left_rectify_map_y, "right_map_x": self._right_rectify_map_x, "right_map_y": self._right_rectify_map_y } return rectify_map_dict fisheye.initUndisortedRectifyMapという関数にこれまで求めたパラメーターK,D,R,Pを渡してx座標のマップ、y座標のマップをそれぞれ取得することができます。 このマッピングはインデックス番号と要素が直感的に逆になっていることに注意してください。どういうことかというと、元画像の座標を(x , y)、平行化画像の座標を(i , j)としたときに map_x[j][i] = x, \qquad map_y[j][i] = y となっています。つまり平行化画像⇒元画像というような座標変換です。なぜこうなのかはよくわかっていません。 このマップにremap関数を使うと画像変換を行うことができます。 4. 正距円筒変換 次に正距円筒変換です。これはOpenCVに関数が無いので自分で直接2Dルックアップテーブルを作成します。具体的には平行化画像から正距円筒画像に変換するマッピングを計算します。特に上で説明したインデックス番号と要素の関係に注意します。 FisheyeCalibrate.py def create_equirectangular_map(self, axis="vertical", mag_x=1.0, mag_y=1.0): # _ -> eqrec maps(left x, left y, right x, right y) assert axis in ["horizontal", "vertical"], "Select horizontal or vertical as axis." w,h = self._image_size w = int(w * mag_x) h = int(h * mag_y) self._left_eqrec_map_x = np.zeros((h,w), dtype=np.float32) self._left_eqrec_map_y = np.zeros((h,w), dtype=np.float32) self._right_eqrec_map_x = np.zeros((h,w), dtype=np.float32) self._right_eqrec_map_y = np.zeros((h,w), dtype=np.float32) # left map fx = self._left_P[0,0] fy = self._left_P[1,1] cx = self._left_P[0,2] cy = self._left_P[1,2] for y in range(h): for x in range(w): if axis=="vertical": lamb = (1.0 - y/(h/2.0)) * (math.pi/2.0) phi = (x/(w/2.0) - 1.0) * (math.pi/2.0) vs_y = math.tan(lamb) vs_x = math.tan(phi) / math.cos(lamb) rec_x = cx + vs_x*fx rec_y = cy - vs_y*fy self._left_eqrec_map_x[y,x] = rec_x self._left_eqrec_map_y[y,x] = rec_y elif axis=="horizontal": lamb = (1.0 - x/(w/2.0)) * (math.pi/2.0) phi = (1.0 + y/(h/2.0)) * (math.pi/2.0) vs_x = math.tan(lamb) vs_y = math.tan(phi) / math.cos(lamb) rec_x = cx - vs_x*fx rec_y = cy + vs_y*fy self._left_eqrec_map_x[y,x] = rec_x self._left_eqrec_map_y[y,x] = rec_y # right map fx = self._right_P[0,0] fy = self._right_P[1,1] cx = self._right_P[0,2] cy = self._right_P[1,2] for y in range(h): for x in range(w): if axis=="vertical": lamb = (1.0 - y/(h/2.0)) * (math.pi/2.0) phi = (x/(w/2.0) - 1.0) * (math.pi/2.0) vs_y = math.tan(lamb) vs_x = math.tan(phi) / math.cos(lamb) rec_x = cx + vs_x*fx rec_y = cy - vs_y*fy self._right_eqrec_map_x[y,x] = rec_x self._right_eqrec_map_y[y,x] = rec_y elif axis=="horizontal": lamb = (1.0 - x/(w/2.0)) * (math.pi/2.0) phi = (1.0 + y/(h/2.0)) * (math.pi/2.0) vs_x = math.tan(lamb) vs_y = math.tan(phi) / math.cos(lamb) rec_x = cx - vs_x*fx rec_y = cy + vs_y*fy self._right_eqrec_map_x[y,x] = rec_x self._right_eqrec_map_y[y,x] = rec_y print("equirectangular lookup table ceraeted.") eqrec_map_dict = { "left_map_x": self._left_eqrec_map_x, "left_map_y": self._left_eqrec_map_y, "right_map_x": self._right_eqrec_map_x, "right_map_y": self._right_eqrec_map_y } return eqrec_map_dict 正距円筒画像にすると少し画像が縦につぶれ気味になってしまっていたので、マッチングがしやすくなるように縦横の大きさを定数倍できるようになっています。またエピポーラ線がどの方向に歪む変換を行うかのオプションも用意してあります。今回は縦方向のエピポーラ線が変化する変換なのでverticalです。計算式を以下に示します。 \lambda = (1 - \frac{y}{h/2})\frac{\pi}{2}, \quad \phi = (\frac{x}{w/2} - 1)\frac{\pi}{2} \qquad (正距円筒の座標系) vs_y = \tan{\lambda}, \quad vs_x = \frac{\tan{\phi}}{\cos{\lambda}} \qquad (z=1スクリーン上での座標) rec_y = c_y - vs_y * f_y \quad rec_x = c_x + vs_x * f_x \qquad (ピクセル座標) 緯度経度といいう意味でλ, Φをという記号を使っています。rec_x, rec_yが平行化画像の座標です。 マッピングの事情で正距円筒画像⇒平行化画像という順番で計算を行っています。もし逆の計算を行うとどうなるでしょうか? 一応座標は求まるのでマッピングを計算することはできます。しかし、インデックス番号は整数でなくてはならないので必然的に正距円筒画像の座標をintに丸めることになります。こうなってしまうと画像の一部が欠けるようなマッピングができてしまいます。なので正距円筒画像⇒平行化画像の順で計算を行う必要があります。 5. 視差推定 画像の変換が完了したのでマッチングを適用して視差を求めましょう。 DepthEstimation.py def stereo_matching(self, save=None, show=True): # image -> disparity map assert self._image is not None, "Plz set image first." print("stereo matching ...") stereo = cv2.StereoSGBM_create( numDisparities = self._num_disparities, blockSize = self._window_size, P1 = 8 * 3 * self._window_size**2, P2 = 32 * 3 * self._window_size**2, disp12MaxDiff = 1, uniquenessRatio = 10, speckleWindowSize = 150, speckleRange = 32, mode=cv2.StereoSGBM_MODE_HH ) blur = 7 self._left_image = cv2.GaussianBlur(self._left_image,(blur,blur),blur) self._right_image = cv2.GaussianBlur(self._right_image,(blur,blur),blur) self._disparity_map = stereo.compute(self._left_image, self._right_image).astype(np.float32) self._disparity_map = self._disparity_map / 16.0 if save: fig = plt.figure(figsize=(15,15)) plt.imshow(self._disparity_map) plt.axis("off") fig.savefig(save) if show: self._show_images(images=[cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB), self._disparity_map], titles=["Original", "Disparity"], figsize=(30,20), subplot=(1,2)) return self._disparity_map OpenCVのStereoSGBMというクラスを使います。numDisparitiesに計算を行う視差の最大値を、blockSizeに探索ウィンドの大きさを、P1,P2には動的計画法で使用するペナルティーをそれぞれ渡します。今回は最大視差100、ウィンドサイズ7で実行しています。 この値を変えることで画像の遠くと近くのどっちを正確に計算するか調整することができます。P1,P2については公式ドキュメントに記載してある式をそのまま使っています。 計算を行う前に画像に軽いぼかしを入れておくとマッチングがきれいに計算されやすいです。 最後に視差を16で割っていることに気を付けてください。理由はよくわかりませんがOpenCVの仕様で求めたい値の16倍になっているそうです。(4bit分左にずれていると考えられる) 6. 距離推定 ようやくここまで来ました。あとは視差画像から距離を計算するだけです。 DepthEstimation.py def depth_estimation(self, axis="vertical",threshold=(0.0,2.0), save=None, show=True): assert self._disparity_map is not None, "Plz run stereo matching first." height = self._left_image.shape[0] width = self._left_image.shape[1] self._depth_map = np.zeros(self._disparity_map.shape , np.float32) if axis == "vertical": for y in range(height): for x in range(width): lamb = (1.0 - y/(height/2.0)) * (math.pi/2.0) phi = (x/(width/2.0) - 1.0) * (math.pi/2.0) delta_phi = self._disparity_map[y,x] / (width/2.0) * (math.pi/2.0) distance = self._baseline * math.sin(math.pi/2.0 - phi) / (1e-7+math.sin(delta_phi) * math.cos(lamb)) self._depth_map[y,x] = distance *math.cos(phi) * math.cos(lamb) elif axis == "horizontal": for y in range(height): for x in range(width): lamb = (1.0 + y/(height/2.0)) * (math.pi/2.0) phi = (1.0 - x/(width/2.0)) * (math.pi/2.0) delta_lamb = -(self._disparity_map[y,x] / (width/2.0)) * (math.pi/2.0) distance = self._baseline * math.sin(math.pi/2.0 - lamb) / (1e-7+math.sin(delta_lamb) * math.cos(phi)) self._depth_map[y,x] = distance * math.cos(phi) self._depth_map = np.where((self._depth_map < threshold[0]) | (self._depth_map > threshold[1]), 0.0, self._depth_map) if save: fig = plt.figure(figsize=(15,15)) plt.imshow(self._depth_map) plt.axis("off") fig.savefig(save) if show: self._show_images(images=[cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB), self._depth_map], titles=["Original", "Depth"], figsize=(30,20), subplot=(1,2),color_bar=True) return self._depth_map こちらもverticalとして計算を行います。式はシンプルに見えて意外と複雑です。視差をdとしています↓ \lambda = (1 - \frac{y}{h/2})\frac{\pi}{2}, \quad \phi = (\frac{x}{w/2} - 1)\frac{\pi}{2} \qquad (正距円筒の座標系) \Delta\phi = \frac{d_{x,y}}{w/2}\frac{\pi}{2} \qquad (視差を角度に変換) \qquad\qquad distance = \frac{B\sin{(\frac{\pi}{2} - \phi)}}{\epsilon + \sin{(\Delta\phi)}\cos{(\lambda)}} \qquad (カメラからの距離を計算,\epsilon=10^{-7}) depth_{x,y} = distance * \cos{\phi} * cos{\lambda} \qquad (奥行きを計算) カメラからの距離と奥行きは異なることに注意してください。今回求めたい距離というのは奥行きのことなのでカメラとの距離にcosを掛けています。 7. 実行結果 1mと2m地点に分かりやすい目標を置いて実験をしてみたいと思います! 今回実験に使う画像です。手前のボードが1m奥のボードが2mとなっています。 まずは平行化変換です。fov_scaleを2としているので画角が十分に得られている反面、端が相当引き伸ばされてしまっています。 次に正距円筒変換です。平行化画像のエピポーラ線を保ったまま引き延ばしを解消できていることが分かると思います! そして視差推定です。目的のボードの部分がはっきりととれているのが分かります。マッチングアルゴリズムはテクスチャの無いものを正しく推定するのが困難なので、テーブルや天井が汚くなっているのは大目に見てください。思いのほか椅子もきれいにとれてそうです。 最後は距離推定です。距離推定では視差画像とほぼ同じような見た目となります。視差は手前ほど大きいのに対して、距離は手前ほど小さくなります。なので視差画像と距離画像では色が逆転します。 この画像の画素値を見ると誤差±3cm程度でボードの位置を計測できていることが確認できます!かなり高精度と言えるでしょう。 まとめ ここまで読んでいただき、ありがとうございました! 理論編と実装編の2つに分けて魚眼ステレオカメラによる距離推定を解説しました。記事の投稿は初なので読みずらいところなどがあったら指摘していただけると幸いです。 GitHubの実装も見ていただけると嬉しいです。 この技術の応用先として、例えば車やロボットに搭載するというタスクが最近増えています。こここでは誤差が数cmだったのとテクスチャが無いと計測が困難という点で実践に耐えうる性能は出せませんでした。 現場ではディープラーニングを使ってより高精度な計測を行えるモデルの開発が行われています。距離推定の今後に期待です! 参考 OpenCV
- 投稿日:2021-12-24T10:18:23+09:00
英語の発音練習に活用:Azure Pronunciation assessmentの使用方法を解説
本記事では英語の発音に関して自動で評価を行う、Azure Cognitive Services Speech SDKの「発音評価(Pronunciation assessment)」の使用方法と実装を解説します。 本記事の内容 0. Azureの「発音評価(Pronunciation assessment)」とは 1. リソース「Speech service」の作成 2. SDKを利用した開発環境の用意(Python) 3. まずは音声認識からはじめてみましょう 4. 発音評価を実施する 5. さいごに 0. Azureの「発音評価(Pronunciation assessment)」とは Azureの「発音評価(Pronunciation assessment)」はAzure Cognitive ServicesのSpeechサービスの一機能です [link1] , [link2] 。 Speechサービスでは音声認識(文字起こし)や音声翻訳などの機能が用意されています。 「発音評価(Pronunciation assessment)」機能の公式ドキュメント(日本語)はこちらになります [link]。 発音評価ではスピーチの発音を評価し、話された音声の正確性と流暢性に関するフィードバックを話者に提供します。 言語学習者は、発音評価を使用して練習を行い、即座にフィードバックを得て、発音を改善することができます。そのため、自信を持って話し、発表することができます。 教師がこの機能を使用すれば、複数の話者の発音をリアルタイムに評価することができます。 評価される主な指標を上記のサイトより引用紹介します。 出力パラメータ 説明 AccuracyScore 音声の発音精度。 精度は、音素がネイティブ スピーカーの発音とどれだけ厳密に一致しているかを示します。 単語およびフル テキスト レベルの精度スコアは、音素レベルの精度スコアから集計されます。 FluencyScore 音声の流暢性。 流暢性は、音声がネイティブ スピーカーによる単語間の間の取り方にどれだけ厳密に一致しているかを示します。 CompletenessScore 音声の完全性。テキスト入力を参照するために発音された単語の比率を算出することで判断されます。(※小川注釈:入力文の単語のうち、何単語をきちんと発音したかを示します) PronunciationScore 音声の発音品質の良さを表す総合スコア。これは、重み付きの AccuracyScore、FluencyScore、および CompletenessScore から集計されます。 1. リソース「Speech service」の作成 AzureのCognitive ServicesではSpeechやTextなどを包括的にまとめて扱えるリソースと、そこからSpeech関連だけなど、特定機能のCognitive Serviceだけを取り出したリソースを作ることができます。 今回は、Cognitive Services リソースを作成(=プロビジョニング)することにします。 (1)Auzreポータルにサインインします (2)+「リソースの作成」 ボタンを選択し、Cognitive Services と検索し、Cognitive Services リソースを作成します(価格レベルはStandard S0とします。その他は自由に。) (3)Cognitive Services リソースのデプロイが完了したら、リソースに移動し、左側メニューから「キーとエンドポイント」をクリックします。このページに表示されている「キー 1」、「場所/地域」、「エンドポイント」の値をそれぞれどこかにコピーして記録しておきます(後ほど使用します)。 2. SDKを利用した開発環境の用意(Python) 続いて、Azure Cognitive ServicesをSDKで開発する環境を用意します。 SDKは様々なプログラミング言語で開発できますが、今回はPythonを使用することにします。 (1)開発環境は自分のローカルPCに用意しても良いのですが、さくっと試すだけであれば、AzureのVMのAI用のイメージである「Data Science VM(DSVM)のWindows版」を立ち上げてしまうのがおススメです [link]。 AI系の必要なライブラリやVS codeやその拡張機能などが全部用意済みなので、RDPで入って、すぐに開発を始められます(マシンはNC6かNC6 promoあたりにし、GPUも必要に応じて使えるようにしておくと便利です)。 本記事ではさらに簡便に済ませるために、Google Claboを使用します。 なお実装プログラムはGitHubにも置いております。 実装プログラムのGitHubリポジトリ: https://github.com/YutaroOgawa/example_of_azure_pronunciation_assessment (2)PythonのSpeech用のSDKをpip installします。 pip install azure-cognitiveservices-speech Installing collected packages: azure-cognitiveservices-speech Successfully installed azure-cognitiveservices-speech-1.19.0 記事執筆時の最新の1.19.0がインストールされました [link pypi] 3. まずは音声認識からはじめてみましょう 英語の発音評価に向けて、まずは簡単なところからスタートするのが良いです。 そこで音声認識を実施してみるところからはじめましょう。 その後、その音声の発音評価へと進化させていきます。 (1)まず音声認識の対象ファイルを用意します。 マイク入力ももちろん可能ですが、ローカル環境ではなく、仮想マシンやGoogle Colabの場合はファイルの方が扱いやすいため、本記事では音声入力には.wav形式のファイルを使用します。 音声ファイルには、 に用意されている、time.wavというファイルを使用します。 このファイルでは「what time is it?」という文章が発音されています。 まずはこの音声ファイルから”what time is it?”を文字起こし(音声認識)できるかを試します。 (2)以下、Google ColabでPythonを使用し、アップロードしたtime.wavファイルを読み込ませて、音声認識をします。なおJupyter Notebookのプログラム本体はGitHubにも置いております。 実装プログラムのGitHubリポジトリ: https://github.com/YutaroOgawa/example_of_azure_pronunciation_assessment (3)以下のようにGoogle Colabでtime.wavファイルをアップロードしておきます。 それではGoogle Colabでの実装に移ります。 [3-1] AzureのCognitive Servicesのspeech SDKをインストールします。 # [1] AzureのCognitive Servicesのspeechのインストール !pip install azure-cognitiveservices-speech [3-2] 使用するパッケージをimportします # [2] 使用するパッケージなどのimport import json import azure.cognitiveservices.speech as speech_sdk [3-3] Azure Cognitive Servicesのリソースを作成(プロビジョニング)した際に、リソースに用意された「キー 1」、「場所/地域」の値を設定します。 今回はプログラム内に直書き(ハードコーディング)しており、セキュリテイ面を無視しているので、その点はご注意ください。 以下のyour_hogehogeをご自身のリソースの値に書き換えてください。 # [3] keyとregionの設定 # 今回はセキュリテイを無視していますが、本番環境では重要情報なのでご注意を COG_SERVICE_KEY="your_cognitive_services_key" COG_SERVICE_REGION="your_cognitive_services_location" [3-4] SpeechConfigを設定します。この設定により、Azure Cognitive ServicesのSpeech機能が使えるようになります。speech_config.regionをprintしてみて、きちんと設定できているか確かめておきます。 # [4] SpeechConfigの設定 # 今回はセキュリテイを無視していますが、本番環境では重要情報なのでご注意を speech_config = speech_sdk.SpeechConfig(COG_SERVICE_KEY, COG_SERVICE_REGION) print('speech serviceのregionはこちらに設定しました:', speech_config.region) [3-5] 使用する音声ファイルのパスを設定します。Google Colabに左側のメニューからtime.wav(さきほど説明したファイルです)をアップロードしておき、それを使用します。 # [5] 使用するファイルの設定 audioFile = '/content/time.wav' [3-6] 続いてAudioConfigを設定します。このAudioConfigがSpeech SDKで使用する音声関連の設定となります。ここでは単に[5]で指定した音声ファイルをAudioとして使用してください、と設定しているのみです。 # [6] AudioConfigの設定 audio_config = speech_sdk.AudioConfig(filename=audioFile) [3-7] SpeechRecognizerを作成します。SpeechRecognizerは先ほど作成したSpeechConfigとAudioConfigに従い、音声処理をするためのオブジェクト、元となる要素です。 # [7] SpeechRecognizerの設定(SpeechConfigとAudioConfigを設定時に使用します) speech_recognizer = speech_sdk.SpeechRecognizer(speech_config, audio_config) [3-8] 作成したSpeechRecognizerで音声処理(音声認識=文字起こし)を実施します。 音声ファイルの内容通り、What time is it?と認識してくれました。 # [8] SpeechRecognizerで音声処理(音声認識) speech_result = speech_recognizer.recognize_once_async().get() print("次の音声と認識しました:", speech_result.text) 結果 次の音声と認識しました: What time is it? 以上で音声ファイルに対する音声認識は完了です。 続いてはここに発音評価の要素を付け加えていきます。 4. 発音評価を実施する [4-1] まずは発音しているスクリプトを用意します。今回は音声ファイルがネイティブスピーカーの方の短い文章なので、スクリプトを変更しないと(当然ですが)満点が出てしまうので、実際の音声とスクリプトを少し異なるものにします。 音声はWhat time is it?ですが、'What time is it now in Japan ?'と発音していることにしましょう。 # [1] 発音している単語のスクリプトを用意します script = 'What time is it now in Japan ?' [4-2] PronunciationAssessmentConfigを設定します。 以下、設定の詳細説明のリンクを掲載します。 ・参考記事 ・SDK解説:azure-cognitiveservices-speech Package ・SDK解説2 ・プログラム参考 # [2] AudioConfigの設定 pronunciation_config = speech_sdk.PronunciationAssessmentConfig(reference_text=script, grading_system=speech_sdk.PronunciationAssessmentGradingSystem.HundredMark, granularity=speech_sdk.PronunciationAssessmentGranularity.Word) [4-3] SpeechRecognizerを設定します。さきほど[3-7]で設定したものと同じ内容です。 # [3] SpeechRecognizerの設定(SpeechConfigとAudioConfigを設定時に使用します) speech_recognizer = speech_sdk.SpeechRecognizer(speech_config, audio_config) # 先ほど[7]で設定したものと同じ内容です [4-4] 発音評価を実施します。[4-1]で設定したPronunciationAssessmentConfigにSpeechRecognizerを適用します。そしてSpeechRecognizerで音声処理を実施して結果resultを取得します。 # [4] 発音評価:Pronunciation Assessmentの実施 pronunciation_config.apply_to(speech_recognizer) result = speech_recognizer.recognize_once() [4-5] 発音評価の結果を扱いやすいようにオブジェクト化するPronunciationAssessmentResultクラスに変更します。 # [5] 発音評価:Pronunciation Assessmentの結果をまとめたオブジェクトを作成 pronunciation_result = speech_sdk.PronunciationAssessmentResult(result) [4-6] スクリプト全文に対する発音評価の結果を出力します。 # [6] 発音評価の結果を表示(全文での) print('Accuracy score: {}, fluency score: {}, completeness score : {}, pronunciation score: {}'.format( pronunciation_result.accuracy_score, pronunciation_result.fluency_score, pronunciation_result.completeness_score, pronunciation_result.pronunciation_score )) 結果 Accuracy score: 69.0, fluency score: 53.0, completeness score : 86.0, pronunciation score: 62.8 得点は100点満点です。Scriptが「What time is it now in Japan ?」に対して、「What time is it ?」という音声ファイルを評価させているので当然点数が低くなっています。 [4-7] スクリプトの各単語に対する発音評価の結果を出力します。 # [7] 発音評価の結果を表示(単語ごとに) for word_result in pronunciation_result.words: print('単語:{}, Accuracy score:{}'.format(word_result.word, word_result.accuracy_score)) 結果 単語:What, Accuracy score:100.0 単語:time, Accuracy score:100.0 単語:is, Accuracy score:100.0 単語:it, Accuracy score:100.0 単語:now, Accuracy score:40.0 単語:in, Accuracy score:40.0 単語:Japan, Accuracy score:0.0 あとから勝手にScriptに付け足した、発音していない部分(now in Japan)の単語で点数が低いことが分かります。 [4-8] 最後に異なる結果取得の方法、JSONで全ての情報を取得する方法を解説します。 resultに対して、以下のように実行します。 # [8] まとめてJsonで取得・表示 json_result = result.properties.get(speech_sdk.PropertyId.SpeechServiceResponse_JsonResult) jo = json.loads(json_result) print(json.dumps(jo, indent=2)) 結果のJSONは以下のような構造です。 { "Id": "b19370d415794e5bb53919f179ffa1df", "RecognitionStatus": "Success", "Offset": 5000000, "Duration": 23500000, "DisplayText": "What time is it now in Japan?", "SNR": 27.69381, "NBest": [ { "Confidence": 0.9015003, "Lexical": "What time is it now in Japan", "ITN": "What time is it now in Japan", "MaskedITN": "what time is it now in japan", "Display": "What time is it now in Japan?", "PronunciationAssessment": { "AccuracyScore": 69.0, "FluencyScore": 53.0, "CompletenessScore": 86.0, "PronScore": 62.8 }, "Words": [ { "Word": "What", "Offset": 5000000, "Duration": 3500000, "PronunciationAssessment": { "AccuracyScore": 100.0, "ErrorType": "None" } }, { "Word": "time", "Offset": 8600000, "Duration": 2900000, "PronunciationAssessment": { "AccuracyScore": 100.0, "ErrorType": "None" } }, { "Word": "is", "Offset": 11600000, "Duration": 1300000, "PronunciationAssessment": { "AccuracyScore": 100.0, "ErrorType": "None" } }, { "Word": "it", "Offset": 13000000, "Duration": 3100000, "PronunciationAssessment": { "AccuracyScore": 100.0, "ErrorType": "None" } }, { "Word": "now", "Offset": 25900000, "Duration": 500000, "PronunciationAssessment": { "AccuracyScore": 40.0, "ErrorType": "None" } }, { "Word": "in", "Offset": 26500000, "Duration": 500000, "PronunciationAssessment": { "AccuracyScore": 40.0, "ErrorType": "None" } }, { "Word": "Japan", "Offset": 27100000, "Duration": 1400000, "PronunciationAssessment": { "AccuracyScore": 0.0, "ErrorType": "Mispronunciation" } } ] } ] } 以上で発音評価を行うことができました。 8. さいごに 以上、本記事では英語の発音に関して自動で評価を行う、Azure Cognitive Services Speech SDKの「発音評価(Pronunciation assessment)」の使用方法と実装を解説しました。 英語の発音練習やそのアプリ作成ぐらいしか使い道が私には思い浮かばないのですが、なかなか面白い機能です。 本記事のJupyter Notebookのプログラム本体はGitHubにも置いております。 実装プログラムのGitHubリポジトリ: https://github.com/YutaroOgawa/example_of_azure_pronunciation_assessment 以上、ご一読いただき、ありがとうございました。 【記事執筆者】 電通国際情報サービス(ISID)AIトランスフォーメーションセンター 製品開発Gr 小川 雄太郎 主書「つくりながら学ぶ! PyTorchによる発展ディープラーニング」 自己紹介(詳細はこちら) 【情報発信】 Twitterアカウント:小川雄太郎@ISID_AI_team IT・AIやビジネス・経営系情報で、面白いと感じた記事やサイトを、Twitterで発信しています。 【免責】 本記事の内容そのものは執筆者の意見/発信であり、執筆者が属する企業等の公式見解ではございません
- 投稿日:2021-12-24T10:02:42+09:00
【RestAPI?】初心者向けにざっくり解説
目的 最近流行のpython, djangoを触ってAPIを実装しようとしたときに、django_restframeworkなるもので簡単にRESTAPIが実装できると言うことを知ったものの、RestfulなAPIとはどのようなものか完全には理解できていなかったためここで同じように曖昧なイメージを持っている方々に共有してみむとせん。 前提 API自体の知識は多少ある方。 RestfulAPIと言う名前を聞いたことがある方 概要 REST APIは「REST」と呼ばれるAPIの設計思想に基づいたweb用のapiのこと。 RESTには以下の4原則がある。 ①ステートレス ②統一インターフェース ③接続性 ④アドレス可能性 以上の四つの原則に従って実装されたAPIのことをRESTAPIと呼ぶ。 具体的に4原則について説明する 詳細 1. ステートレス サーバーにユーザーセッションなどの「状態」(ステート)を持たない(レス)前提で情報のやり取りを行う。 → やり取りが一回ごとに完結する。 → 2度目のアクセスをしてもサーバー側は何も記憶がない 2. 統一インターフェース 例えば ①HTTPメソッドでサーバーにリクエストを送るよ ②JSON形式のデータを送るよ などのようにブラウザとサーバのインターフェースの定義を統一する 3. 接続性 情報の内部に、別の情報の状態へのリンクを含めることができる。 4. アドレス可能性 全ての情報が唯一かぶる事のない識別子を持っていて、提供する情報をその識別子で表現可能 具体例で言うとメンバーの情報は一意のidやusernameを持っているため、 「/member/pk/」このようにして指定したメンバーに対して編集などの動作を行うことができる。 具体的には 一番大きなポイントとしては以下のように、同じuriでも異なるHTTPメソッドでリクエストを投げる事で異なる動作を行うことができると言う事。 このことによりシンプルでわかりやすいAPIが作成できると言うことだ。 HTTPメソッド リソース 概要 GET /member/ メンバーの一覧を取得 GET /member/pk/ pkで指定したメンバーの情報を取得 POST /member/ 新規のメンバーを登録する PUT /member/pk/ pkで指定したメンバーの情報を更新する/pkにメンバーがなければ追加する DELETE /member/pk/ pkで指定したメンバーの情報を削除 参考文献
- 投稿日:2021-12-24T09:55:55+09:00
Python を用いてGUIアプリケーションの配布用実行ファイルを作る
はじめに Python を利用して実行ファイルを作る記事は様々ありましたが,難読化や利用したライブラリをまとめるなどの操作がまとまった記事はなかなか見つからなかったので書いておきます. やりたいこと Python で書いたプログラムを実行ファイルにし,難読化を行って配布する. 環境 python = 3.8 パッケージ pip install pyinstaller==4.3(記事執筆時の最新版は4.8) pip install pyarmor pip install pip-licenses サンプルコード 下記リンクのコードを元にしています. https://qiita.com/seisantaro/items/74ed83fec3d126553245 https://qiita.com/y-tsutsu/items/a8cc1578dd2f930e5439 実行ファイルにアイコン画像を設定する際,エクスプローラなどの上で表示されるアイコン設定と,タスクバーとウィンドウに表示されるアイコンは別々に指定する必要があることに注意が必要です. 前者は実行ファイル生成時にオプションをつけるのみで良いですが,後者は実行ファイル生成時にファイルをバンドルし,プログラム内でも設定すると記述する必要があります. main.py # -*- coding: utf-8 -*- import tkinter as tk import os, sys #アイコン画像取得用の関数 def resource_path(relative): if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative) return relative class App(tk.Tk): # 呪文 def __init__(self, *args, **kwargs): # 呪文 tk.Tk.__init__(self, *args, **kwargs) # ウィンドウタイトルを決定 self.title("Tkinter change page") # ウィンドウの大きさを決定 self.geometry("800x600") # ウインドウのサイズ変更可否指定 self.resizable(width=False, height=False) # タイトルバーやタスクバーのアイコンを設定 self.iconbitmap(resource_path("icon.ico")) # ウィンドウのグリッドを 1x1 にする # この処理をコメントアウトすると配置がズレる self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) #-----------------------------------main_frame----------------------------- # メインページフレーム作成 self.main_frame = tk.Frame() self.main_frame.grid(row=0, column=0, sticky="nsew") # タイトルラベル作成 self.titleLabel = tk.Label(self.main_frame, text="Main Page", font=('Helvetica', '35')) self.titleLabel.pack(anchor='center', expand=True) # フレーム1に移動するボタン self.changePageButton = tk.Button(self.main_frame, text="Go to frame1", command=lambda : self.changePage(self.frame1)) self.changePageButton.pack() #-------------------------------------------------------------------------- #-----------------------------------frame1--------------------------------- # 移動先フレーム作成 self.frame1 = tk.Frame() self.frame1.grid(row=0, column=0, sticky="nsew") # タイトルラベル作成 self.titleLabel = tk.Label(self.frame1, text="Frame 1", font=('Helvetica', '35')) self.titleLabel.pack(anchor='center', expand=True) # フレーム1からmainフレームに戻るボタン self.back_button = tk.Button(self.frame1, text="Back", command=lambda : self.changePage(self.main_frame)) self.back_button.pack() #-------------------------------------------------------------------------- #main_frameを一番上に表示 self.main_frame.tkraise() def changePage(self, page): ''' 画面遷移用の関数 ''' page.tkraise() if __name__ == "__main__": app = App() app.mainloop() 実行ファイル生成 コンソールでコードを置いたディレクトリまで移動し,下記のコマンドを入力します. # ライセンス期限の設定,今回は 2021/3/31 pyarmor licenses --expired 2022-03-31 code # pack で実行ファイル生成,-with-license のオプションで期限の設定,-e のオプションで pyinstaller のオプション設定 pyarmor pack --with-license licenses/code/license.lic -e " --onefile --noconsole --add-data .\\icon.ico;. --icon icon.ico" main.py pyinstaller のオプションを見ていきます.--onefile のオプションで実行ファイルを1つにまとめています.--noconsole のオプションでコンソールを非表示にしています.--add-data のオプションでアイコンファイルをバンドルしています.--icon のオプションで実行ファイルのアイコン画像を設定しています. ライセンスファイル生成 配布時は利用したライブラリのライセンスをまとめておく必要があるので,コンソールに下記の記述を行うことでファイルにまとめることができます. # 利用したライブラリとライセンスをまとめてファイル出力 pip-licenses --format=rst --output-file=LICENSE.rst 参考 https://www.delftstack.com/ja/howto/python-tkinter/how-to-set-window-icon-in-tkinter/ https://qiita.com/y-tsutsu/items/a8cc1578dd2f930e5439 https://qiita.com/Dr_Thomas/items/f3b2eff79cba10eb42a3 https://lets-hack.tech/programming/languages/python/pyinstaller/ https://qiita.com/seisantaro/items/74ed83fec3d126553245 https://pyarmor.readthedocs.io/en/latest/man.html?highlight=pack#pack https://pypi.org/project/pip-licenses/