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

データの加工と可視化のスクリプトは分けようよという話。

最近、PythonやRではどんどん便利なパッケージが開発されている。例えば、Pandasなんかではテーブルデータを整形をするためのメソッドだけでなく、可視化のためのメソッドまで用意されている。なので、Pandasを使えばデータの整形から可視化まで1つのスクリプトで行える!。。。。お願いだからやめてくれ。いや、書き捨てのスクリプトだったり、Jupyter notebook上でそういうことするのは分かる。でもお願いだから再利用する可能性のあるスクリプトで、そういうことするのはやめてくれ。便利だからってこういうスタイルを続けていると後で大変なことになる。そして、自分と他人を大変苦しめる羽目になるのである。なぜ、駄目なのか。ここでは一つ例をあげて説明したい。 Seabornのclustermapとか使うのやめようよ。 個人的に、データの整形から可視化までをやってくれる最もありがた迷惑な関数として、seabornのclustermapがある(Rのheatmapやheatmap.2も同様)。 これはその名の通りclusteringされたheatmapを作成してくれる関数である。複数サンプルの遺伝子発現データなんかを派手に見せるためにデンドログラム付きで、カラーラベル付きのデデーンとしたheatmapを作成してくれる。今や、生命科学界隈の論文では当たり前といっていいほど一般的な図である(参考図)。 Seabornのclustermap等を使えばインフォマティクスに明るくない人もたった一行のコードでなんかクラスタリングされた図を生成できる。うん素晴らしいね。しかし、最近ある人にこんな感じの質問をされた。 質問者 「Pythonを使って、clustering heatmapを作ったんですがちょっと分からないことがあっていいですか?」 筆者 「うん?いいよー。」 質問者 「heatmapの行や列の順序が分からないんですが、どうすればわかりますか?」 最初、私は意味がわからなかった。なのでこう聞き返した。 筆者 「え、クラスタリングされた後のテーブルをcsvとかの形式で書き出せばいいんじゃない?」 質問者 「??クラスタリングされた後のテーブル?は手元にありません。」 筆者 「え、じゃあどうやってheatmap作ったのさ?。クラスタリングした後のテーブルを入力にしたんじゃないの?」 質問者 「???、ちょっと何言ってるか分からないですが、入力は持っているテーブルデータです。」 何を言ってるのか分からないのはこっちである。目の前の人間は、クラスタリング後のテーブルデータを持っていないのにクラスタリングされたheatmapのPDFファイルはあるという謎のことを言っているのである。このことについて「え、そういうもんじゃないの?」と思う人はマジで反省してほしい。話をつづけると、どうやらseabornのclustermapを使うと、クラスタリングから可視化までを全てやってくれるということらしかった。そして、heatmapの図しか出力されないから、クラスタリング後のテーブルデータなんて無いということらしい。 私は、もう何がなんだかわからなかった。そして心のなかで呟く。 「クラスタリングされた後のテーブルデータを得られなくて、heatmapだけ出力されて何が嬉しいの??。まさか、なんか綺麗な図を作ることを目的にクラスタリングしたってこと?。クラスタリングは別にかっこいい図をつくるための前処理じゃないぞ?。アホの子なの?」 しかし、こういう言い方は最近では許されていない、そこでこう聞き返す。 筆者 「そういう関数を使って作ったのはわかった。でもさ、じゃあヒートマップの色だけ変えたい時はどうしてるの?」 質問者 「???質問の意図がわからないですが、cmapのパラメータを変えます」 筆者 「それ、データが大きいと毎回時間かからない?」 質問者 「!!そうなんですよ!。今回もこの図をつくるのに10分ぐらいかかるんですよ!」 その返答を聞いて、私は心がおれかけた。そして、こんな調子でデータ解析をしている生命科学の研究者がほとんどであるという事実に気づき絶望した。。。。。 なぜ、データの加工と可視化のスクリプトは分けるべきなのか。 ここまでの話で、すでにデータの加工と可視化のスクリプトを分けた方がいい理由に気づいた人も多いだろう。スクリプトを分けたほうが理由は大きく2つある。 1. 加工過程で作成されるデータを他人も編集可能な形式で残すため。 2. 毎回データを加工するという処理を繰り返さないため。 1については、さっきの例でいうとクラスタリング後のテーブルデータを残しておこうということだ。これが残ってないと結局どの要素同士がクラスタリングされたのかよく分からないし(まさかラベルも画像に出力して確認するわけにもいかないし、まじでやめてくれ。)、詳細な数値情報も分からない。他人にデータを渡す時共有するときだって不便だ。 研究や受注解析とかしてると頻繁に解析結果を他者に共有することがある。人によっては綺麗な図だけ渡せば喜んでくれる人もいるが、多くの場合、データを貰った相手もデータの信頼性を確かめるために色々と聞いてくるし、図の見た目や、ちょっとした追加解析は自分の手でしたいという人もいる。なので、基本的に他人にデータを渡す時は生データだけでなく、エクセルでも論文の図と同じ図がつくれるようにまで加工したデータも共有するべきだ。今回の例であれば、クラスタリング後のテーブルデータをラベル付きで共有してあげれば、受け取り手はエクセルでもエクセルのヒートマップ機能を使って、seabornで出力するheatmapと同じ図を作ろうと思えば作れるし、フィルタリングの機能を使えば特定の要素だけでheatmapを作り直すのだっておちゃのこさいさいだ。一方で、画像データだけもらってもできることはない。「うん、綺麗な図だねー。」程度の感想しかでてこないだろう。だって画像データだけもらっても追加の解析等はできないのだから、、、。まぁベクター図ならな。数値データ取り出すこともできるけどな。うん。 2については、入力データのサイズが大きくなるほど問題になる。基本的にはクラスタリングはその手法にもよるが計算オーダーが大きい。なのでデータサイズが数倍になるだけで計算時間は何十倍にもなっていく。最近の生命科学では普通に、1000x1000とか10000x10000とかのサイズのクラスタリングを扱ったりするわけだが、世の中にはヒートマップの色調をかえるためだけにクラスタリングからやり直しているひとがほとんどだというのがどうやら現実らしい。1. で書いたようにクラスタリング後のテーブル残しておけば色調だけ変えるなんて一瞬なんだぜ。 うん、まぁな。最近のCPU速いもんな。でもさ、1CPUの速度は結構頭打ちしてるんだぜ。「俺のPCはラップトップだけど8コア16スレッドおおおおおお。だから計算負荷とか関係ないからぁああ!」みたいに思ってるかもしれないけど、clustermapとかみたいなメソッドは並列化できるように実装されてないからさ。Pythonは基本シングルスレッドでしか走らないしさ。うん。毎回クラスタリングするのマジ人生の無駄だからさ。やめようぜ。 ということで、結局質問者には上記の話を伝え、クラスタリングをするための関数を伝授し、クラスタリングされた後のデータをheatmapとして可視化するための方法を伝授したのであった。そこらへんの話は、次の記事で。しかし、あんまり重要性が伝わってなかった気がするんだよなぁ。。。。泣
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Twitter API】特定キーワードを含むツイートの件数推移が知りたい!無料でどこまで調べられる??

はじめに とあるメーカー。 上司 「今度、山登りをテーマにした商品を企画しようと思っててさ」 上司 「今、山登りが流行ってるじゃん?」 上司 「流行っているっていう根拠になる数字が欲しいんだよね」 上司 「例えばSNSでこれだけ山登りに関するキーワードの検索数が増えています!とか」 上司 「そういう数字、集めてくれない?」 うめ 「了解です!」 ・・・(検索検索)・・・ うめ 「うーん…、twimpは3か月前まで(Twitter)、かつ調査に時間がかかるし...」 うめ 「Yahooのリアルタイム検索は30日前まで(Twitter)のデータだし...」 うめ 「InstantTrendsは直近の11日(Instagram)...」 うめ 「もっと長期的な推移を無料で見る方法って無いの??」 ・・・(検索検索)・・・ うめ 「あった!Twitter API??APIって、聞いたことはあるけど何が何だかわからないな」 ・・・見つけた記事=【難解!?】Twitter APIを使って投稿データを収集する方法・・・ うめ 「会員登録が必要?無料/有料版があるのか。無料版でも「Premium/Sandboxプラン」なら、フルアーカイブを一部無料で使うことができるみたい!」 うめ 「このプラン登録して、どこまでできるか試してみたいな。よし、やってみよ!」 ・・・(検索検索)・・・ うめ 「ちょっと待った!このプランはv1のもので、今はv2っていう新バージョンが登場しているみたい。」 うめ 「登録フォームにも、"you can begin to use our new Twitter API v2, or our v1.1 standard and premium APIs."ってあるよ。」 うめ 「最新の情報はどれなんだろ?Twitter API利用のための会員登録の解説記事も色々で回っているみたい。どれが最新情報?わからんーー!!」 うめ 「とにかく、Twitter Developerの公式サイトから会員登録を進めながら調べていくのがよさそう。よし、やろう!」 やりたいこと ・「山登り」を含むツイートが日ごとに何件あるか、推移を調べたい。 (直近の半年分集められたら嬉しい。年毎の数も10年分とか集められたらなお嬉しい。…できるのか!?) ・「山登り」をキーワードに検索された件数を調べたい。 ・「山登り+〇〇」と、どのワードと組み合わせて検索されたのか、知りたい。 環境 Windows10 スタート時点の私の状況 ・Twitter API なるものがあるらしい。APIって何ぞや??どうやって使うの?? ・Python触ったこと無いよ。 結論 ・結局、今の私の力量では欲しい情報をAPIから取得することはできず、業務ではYahooのリアルタイム検索のツイート数グラフから30日分の1日の件数(「山登り」を含むツイートの件数)を手作業で拾って、その数値を使った。 ・Pythonで欲しいデータを取得できるようだったが、Pythonを触ったことがない現段階で「直近7日のツイートから"山登り"を含むツイートの件数を取得する」というのは、難しい。Pythonの基本を学び、必要な情報は何なのかを整理し、その情報を集めて勉強してからチャレンジした方が良さそう。 【v2 スタンダード版(無料)について、Twitter公式ドキュメントの内容】 (ドキュメントから読みとった内容だが、正しく読み取れているか自信無し。) ・無料で利用できるのは直近7日分のデータのみ。 ・ひと月の最大検索ツイート数=50万ツイート ・レート制限=(Recent searchについて)450リクエスト/15分(アプリ毎)、180リクエスト/15分(ユーザー毎) ・リクエストごとに取得できるスイート数=最大100ツイート/1リクエスト 参考=エンドポイントの一覧(Comparing Twitter API’s Search Tweets endpoints) 1.Twitter Developer公式サイトで会員登録 1.「Twitter Developer」でグーグル検索→公式サイトトップページに到着。 2.会員登録については、こちらの記事にまとめました。  →【Twitter API v2.0】利用申請(デベロッパー登録)したので方法記録 2.特定キーワードを含むツイートの件数を調べる方法 2-1.下調べ ・30日以内であればYahooのリアルタイム検索で事足りそう。件数を取得するには、グラフから手作業で1日ずつメモするしか無い?? ・NTT DATAがTwitterデータ提供サービスを展開しているみたい。料金表はこんな感じ。  →NTT DATA料金表  ・無料版だと取得できるツイートは、過去7日前のものまで。 This endpoint gives you conversations from Twitter for the last 7 days. (このエンドポイントは、過去7日間のTwitterからの会話を提供します。)  引用元:Twitterドキュメント(Tutorials / Analyze past conversations<チュートリアル/過去の会話を分析する>) →年単位、月単位の長期的なツイート数推移を調べたいと思っていたけれど、課金が必要。 →Twitter API v2は「スタンダート/学術研究/ビジネス」の3段階のプロダクトトラックが存在する。学術研究/ビジネスについては、ステータスが「近日公開」となっていた。詳細は書かれていないけれど、過去の長期的なツイートを利用したい場合は、学術研究/ビジネスの契約が必要なようだ。対象者は、 ・学術研究=要件を満たした学術研究者。 ・ビジネス=Twitterの公式パートナーやエンタープライズデータカスタマーなど、Twitter API上でビジネスを構築する開発者。 エンタープライズ(ビジネス)版や学術研究版の利用料金は明記されていなかった。(私が見つけられなかっただけの可能性もあり) ・そもそも、無料でも30日以上前のツイートデータを取得できるかも!という期待が高まったのは、 日単位で取得すれば7日前までしか取得できない制限を突破できます untilには7日より前の日付を指定しても取得することができませんが、sinceとuntilで1日ずつ取得すれば7日より前の過去も取得できます。 引用元:TwitterAPIで期間指定してTweetを取得する方法-Qiita @areph さん2016.08.27記事 という記事を見つけたからでした。が、今はできないようです。(実は方法がある!のかもしれませんが、私には情報を見つけられませんでした。)変化し続けているのですね。今後、もっと過去のデータも無料版で触れるようになる可能性も、無きにしもあらず...。 2-2.v2で取得できるツイートの上限を知る ・ひと月の最大検索ツイート数=50万ツイート If you are using the Standard product track at the Basic access level, you will be limited to receiving 500,000 Tweets per month across the v2 endpoints noted earlier. (基本アクセスレベルで標準製品トラックを使用している場合 、 前述のv2エンドポイント全体で月に500,000ツイートを受信するように制限されます。) (引用元:Projects-Twitter公式ドキュメント) ・レート制限=(Recent searchについて)450リクエスト/15分(アプリ毎)、180リクエスト/15分(ユーザー毎) (右記文章以外に、引用元リンク先に詳細の表あり)Every day many thousands of developers make requests to the Twitter API. To help manage the sheer volume of these requests, limits are placed on the number of requests that can be made. These limits help us provide the reliable and scalable API that our developer community relies on. The maximum number of requests that are allowed is based on a time interval, some specified period or window of time. The most common request limit interval is fifteen minutes. If an endpoint has a rate limit of 900 requests/15-minutes, then up to 900 requests over any 15-minute interval is allowed. (毎日、何千人もの開発者がTwitterAPIにリクエストを送信しています。これらのリクエストの膨大な量を管理しやすくするために、実行できるリクエストの数に制限が設けられています。これらの制限は、開発者コミュニティが依存する信頼性が高くスケーラブルなAPIを提供するのに役立ちます。 許可されるリクエストの最大数は、時間間隔、指定された期間、または時間枠に基づいています。最も一般的なリクエスト制限間隔は15分です。エンドポイントのレート制限が900リクエスト/ 15分である場合、15分間隔で最大900リクエストが許可されます。 ) (引用元:Rate limits-Twitter公式ドキュメント 2021/05/05現在) ・リクエストごとに取得できるスイート数=最大100ツイート/1リクエスト (引用元:Search Tweets-Twitter公式ドキュメント 2021/05/05現在) ・料金 v1.1は、リクエスト数に応じて変則的な料金体系になっているよう。 プライス表=https://developer.twitter.com/en/products/twitter-api (v1.1の欄参照) 自分が取得したいデータを得るにはリクエストを何件送る必要があるのか、それを知る必要がありそう。 ただ、それを知るためにはレート制限のことをもっと理解する必要がありそう。今の私には、「山登り」を含むツイートを過去フルアーカイブから取得したい場合に、料金がいくらになるのか、判断する力は無い。 v2の料金については、該当ページを見つけられず。 ・エンドポイントの一覧 Comparing Twitter API’s Search Tweets endpoints ・その他一覧 カテゴリー 商品名 サポートされている履歴 クエリ機能 エンドポイントをカウント データの忠実度 Twitter API v2 Recent search(最近の検索) 7日 Twitter APIv2オペレーター まだ利用できない フル Twitter API v2 Full-archive search(フルアーカイブ検索) アーカイブ全体(AcademicResearch製品トラックでのみ利用可能) Twitter APIv2オペレーター まだ利用できない フル (引用元:Twitter's search endpoints-Twitter公式ドキュメント 2021/05/05現在) 2-3.どうするか ・実際の業務で使う数字は、おとなしくYahooのキーワード検索から拾ってこようと思います。現段階では自動で取得する方法がわからないので、手作業で数字をうつします。 ・せっかくAPIのことを知ったので、今回は直近7日間のツイートで「山登り」が含まれているツイートの数を7日間の推移として調べたいと思います。 →調べていくと、7日分の該当する全ツイートを抽出するのは、PCへの負荷のことや、取得できるツイート数の制限のことを考えると、自分の浅い理解のまま実行するのはPCを壊しそうで怖い...と感じました。なので、今回は実行までは手を付けず、まずPythonの基本を勉強してからTwitter APIの操作に移りたいと思います。 参考 【Twitter公式ドキュメント】 ・Twitter API v1.1 と v2の詳細説明 ・検索できるツイート上限 ・アクセスレベルと製品トラックの説明 ・商用/非営利目的の説明 ・プロジェクト ・エンターブライズとは ・エンタープライズのレート制限 ・「Tutorials / Analyze past conversations(チュートリアル / 過去の会話を分析する)」 【その他】 ・TwitterAPIで期間指定してTweetを取得する方法-Qiita @areph さん 2016.08.27記事 ・500万件を超えるTwitter のリツイート データを取得・分析する方法 -Twitter Premium Search API を実際に使ってみてわかった嵌りポイントとその対策--Morning Girl 2019.02.01記事 ・プログラミング歴10日の人がTwitterAPIに負けた話 #2-Qiita @johnsakagami さん 2019.10.07記事 ・大量ツイートの収集・分析を個人で手軽に実現可能にする方法の提案-デジタルプラクティス Vol.11 No.1(Jan. 2020)推薦投稿論文-※1松浦 智之、※1當仲 寛哲、※2大野 浩之  (※1=(有)ユニバーサル・シェル・プログラミング研究所、 ※2=金沢大学総合メディア基盤センター/慶應義塾大学SFC研究所 ) ・【GetOldTweets-python】無料でTwitterの過去7日より前のツイートを取得してみる-Qiita @jinto さん 2020.05.09更新記事 ・【難解!?】Twitter APIを使って投稿データを収集する方法-GaaaOn 2020.06.21記事 ・TwitterAPIから特定のキーワードを含むツイート数を調べる-Qiita @tobiuo0203 さん 2020.07.23更新記事 ・Pythonでツイートを取得する方法【Twitter分析】-Your 3D 2020.10.31更新記事 【Pythonの基礎勉強してから試したい記事】 ・TwitterAPIから特定のキーワードを含むツイート数を調べる-Qiita @tobiuo0203 さん 2020.07.23更新記事 →APIで直近7日のツイートから「山登り」が含まれるツイート数を調べたい...と思って見つけたこちらの記事。7日分ではありませんが、「特定の1日に20分ごとに1秒間72回取得し、件数を"corona.xlsx"に入力。これを元に1日の件数を推定」という方法が取られていました。PCへの負荷もかかりそうだし、コード内容を理解しきれない今の状態で実行するのは怖い...と感じたので、一度Pythonの基本を学び、その後試してみたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyTorchのCrossEntropyLossクラスの挙動

はじめに シングルラベルタスクにおける損失関数としてよく用いられる交差エントロピーですが、PyTorchの実装クラスの挙動がややわかりにくいものだったため苦労しました。備忘録を兼ねて記事を書きます。 交差エントロピーとは 主にシングルクラスラベルで用いられる損失関数で、以下の数式で定義されます(PyTorchの公式リファレンス)。 loss(x, class) = -log(\frac{exp(x[class])}{\sum_j exp(x[j])}) = -x[class] + log(\sum_j exp(x[j])) 交差エントロピー自体の説明については、この記事がわかりやすいです。GLUEタスクに代表されるようなシングルラベルタスクにおいては、この交差エントロピー関数を用いると損失が上手に収束して高い精度が達成されます。 torchのCrossEntropyLossクラスについて PyTorchには、nnモジュールの中に交差エントロピーの損失関数が用意されています。PyTorchの公式リファレンスによると、使い方は以下の通りです。 PyTorchの公式リファレンスより >>> from torch import nn >>> import torch >>> >>> loss = nn.CrossEntropyLoss() >>> input = torch.randn(3, 5, requires_grad=True) >>> target = torch.empty(3, dtype=torch.long).random_(5) >>> output = loss(input, target) >>> output.backward() ターゲット変数の型について 上述のコードにおいて、入力変数であるinputの型はLogitsをシグモイド関数に通したものであるため、float型と考えて問題ないかと思います。他方、ターゲット変数であるtargetの型はint型で、変数のインデックスが1.0になっているone-hotベクトルがターゲット変数として設定されるようです。つまり、torch.tensor(1)はtorch.tensor([0., 1.])と扱われるようです。 >>> from torch import nn >>> import torch >>> >>> loss = nn.CrossEntropyLoss() >>> input = torch.tensor([0.8, 0.1, 0.1]).unsqueeze(0) >>> target = torch.tensor(0).unsqueeze(0) >>> output = loss(input, target) >>> output tensor(0.6897) CrossEntropyLossクラスの内部で、整数インデックスからone-hotベクトルが生成されているとは思いもよらなかったため、Huggingface inc.のコードを解読している時にハマってしまいました。 他方、マルチラベルタスクで用いられるBCEWithLogitsLossというクラスでは、ターゲット変数にfloat型のmulti-hot型ベクトルを入力する必要があるようです。 参考文献 PyTorchの公式リファレンス 交差エントロピー誤差をわかりやすく説明してみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyTorchのCrossEntropyLossクラスについて

はじめに シングルラベルタスクにおける損失関数としてよく用いられる交差エントロピーですが、PyTorchの実装クラスの挙動がややわかりにくいものだったため苦労しました。備忘録を兼ねて記事を書きます。 交差エントロピーとは 主にシングルクラスラベルで用いられる損失関数で、以下の数式で定義されます(PyTorchの公式リファレンス)。 loss(x, class) = -log(\frac{exp(x[class])}{\sum_j exp(x[j])}) = -x[class] + log(\sum_j exp(x[j])) 交差エントロピー自体の説明については、この記事がわかりやすいです。GLUEタスクに代表されるようなシングルラベルタスクにおいては、この交差エントロピー関数を用いると損失が上手に収束して高い精度が達成されます。 PyTorchのCrossEntropyLossクラスについて PyTorchには、nnモジュールの中に交差エントロピーの損失関数が用意されています。PyTorchの公式リファレンスによると、使い方は以下の通りです。 PyTorchの公式リファレンスより >>> from torch import nn >>> import torch >>> >>> loss = nn.CrossEntropyLoss() >>> input = torch.randn(3, 5, requires_grad=True) >>> target = torch.empty(3, dtype=torch.long).random_(5) >>> output = loss(input, target) >>> output.backward() ターゲット変数の型について 上述のコードにおいて、入力変数であるinputの型はLogitsをシグモイド関数に通したものであるため、float型と考えて問題ないかと思います。他方、ターゲット変数であるtargetの型はint型で、変数のインデックスが1.0になっているone-hotベクトルがターゲット変数として設定されるようです。つまり、torch.tensor(0)はtorch.tensor([1., 0.])と扱われるようです。 >>> from torch import nn >>> import torch >>> >>> loss = nn.CrossEntropyLoss() >>> input = torch.tensor([0.8, 0.1, 0.1]).unsqueeze(0) >>> target = torch.tensor(0).unsqueeze(0) >>> output = loss(input, target) >>> output tensor(0.6897) CrossEntropyLossクラスの内部で、整数インデックスからone-hotベクトルが生成されているとは思いもよらなかったため、Huggingface inc.のコードを解読している時にハマってしまいました。 他方、マルチラベルタスクで用いられるBCEWithLogitsLossというクラスでは、ターゲット変数にfloat型のmulti-hot型ベクトルを入力する必要があるようです。 参考文献 PyTorchの公式リファレンス 交差エントロピー誤差をわかりやすく説明してみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python機械学習プログラミング備忘録 3.3

はじめに オンライン機械学習講義の復習用。(2021/05/05現在) Python機械学習プログラミング 達人データサイエンティストによる理論と実践 の以下を取り扱います。 第3章 分類問題 (3.3) ここでは、数式による理論の理解と sklearn を使った実装による理解を目指します。 対象データ: Iris データセット 目次 開発環境 パーセプトロンの問題点 事象の起こりやすさを表すオッズ比 ロジット関数の定義 ロジット関数の逆関数がロジスティック関数 シグモイド関数の実装 クラスの所属関係の確率を見積もる 重みの更新 Irisデータをロード ロジスティック回帰の学習 学習結果の可視化 まとめ 参考文献 開発環境 MacBook Air 2017 macOS Catalina 10.15.16 Google Colaboratory sklearn: 0.22.2.post1 numpy: 1.19.5 matplotlib: 3.2.2 パーセプトロンの問題点 前回、パーセプトロンを実装し、 その学習アルゴリズムが 実社会で使われない理由が見えました。 最大の問題点は、 完全に線形分離できないクラスがあると 収束しない点です。 エポックごとに誤分類されるサンプルが 一つでもあった場合、重みが絶えず更新されてしまうのです。 この点を踏まえ、線形分類問題に対して、 より柔軟性を持つロジスティック回帰を見ていきます。 ※ その名前とは裏腹に、回帰ではなく分類のためのモデルになります。 ロジスティック回帰は、 産業界で広く利用されている分類アルゴリズムの一つで、 線形分類可能なクラスに対して、高い性能を発揮します。 ※ 二値分類のための線形モデルですが、多値分類モデルとして拡張できます。 事象の起こりやすさを表すオッズ比 ロジスティック回帰の概念には、 オッズ比(odds ratio)の理解が欠かせません。 odds\,ratio : \frac{p}{1-p} オッズ比は事象の起こりやすさを表したもので、 $p$ には正事象の確率が入ります。 ※ 正事象は予測したい事象のことで、通常 $y = 1$ として考えます。 ロジット関数の定義 オッズ比の対数をとったものは対数オッズと呼ばれ、 これを関数とみなしたものがロジット関数になります。 logit(p) = \log \frac{p}{1-p} \,(0 < p < 1)\\ \\ ロジット関数は、 0より大きく、1より小さい範囲の入力値を受け取り、 実数の全範囲に変換します。 ここで、注目すべきは入力値です。 上式では確率 $p$ が「分かったもの」として扱っていましたが、 実際には、その確率 $p$ を求めなければなりません。 なぜなら最終的にやりたいことは、二値分類であり、 そのクラスに属する確率 $p$ を出力したいからです。 この関数は後ほど使います。 ロジット関数の逆関数がロジスティック関数 ここで、 一般的な線形分離の決定境界 $z$ は以下の式で表現できますが、 z = w_0x_0 + w_1x_1 + \,... + \,w_mx_m = \sum_{i=0}^{m}w_ix_i = \boldsymbol{w^Tx}\\ \\ これでは、$z$ の取りうる値が $-\infty$ から $\infty$ になります。 しかし、予測したい $p$ は確率なので、 出力値は、0 から 1 の間に収まっていなければなりません。 上式では $p \neq z$ となるため、 $z$ の取りうる範囲が 0 から 1 の値に収まるように、 リンク関数として、ロジット関数を導入します。 logit(p\,(y = 1 \,| \,\boldsymbol{x}) = z \\ ※ $logit(p\,(y = 1 \,| \,\boldsymbol{x})$ は、特徴量 $\boldsymbol{x}$ が与えられたときに、 サンプルクラスが1に属する条件付き確率を表しています。 ※ 確率 $p$ の変換に使う関数をリンク関数と呼んでいます。 今回はリンク関数が対数であるため、 右辺を指数と見立て、その逆関数を導き出します。 \\ \log \frac{p}{1-p} = z\\ まずは対数の性質から式変形です。 e^{z} = \frac{p}{1-p}\\ e^{z} - e^{z}p = p\\ あとは単純な式変形です。 \begin{align} p &= \frac{e^{z}}{e^{z}+1}\\ &= \frac{1}{1+e^{-z}}\\ \\ \end{align} 上式はロジット関数の逆関数から得られた結果です。 $z$ は総入力となっており、 取りうる値の「$-\infty$ から $\infty$」を「0 から 1」に変換します。 これで、 サンプルクラスが1に属する確率 $p$ を計算できるようになりました。 このようにして導かれた関数は、ロジスティック関数と呼ばれ、 改めて関数として記載しますと、以下の通りになります。 \phi(z)= \frac{1}{1+e^{-z}} $z$ は総入力を表しています。 z = \boldsymbol{w^Tx} = w_0x_0 + w_1x_1 + \,... + \,w_mx_m\\ \\ ところで、このロジスティック関数は 他の分野では標準シグモイド関数と呼ばれ、 機械学習の人たちだけが、ロジスティック関数と呼ぶらしいです。 シグモイド関数は、上記の式において、 本来ならば $z$ の前に $a$ (ゲイン)を伴いますが、 標準シグモイド関数は、そのゲインが $a=1$ になるため、 「標準」と呼ぶのだそうです。 ※ ここでは今後も、この標準シグモイド関数を ロジスティック関数、もしくはシグモイド関数と呼び続けることにします。 シグモイド関数の実装 collaboratory 上でシグモイド関数を実装します。 ipynb import matplotlib.pyplot as plt import numpy as np def sigmoid(z): return 1.0 / (1.0 + np.exp(-z)) # 0.1間隔で-7以上7未満のデータを生成 z = np.arange(-7, 7, 0.1) # 生成したデータでシグモイド関数を実行 phi_z = sigmoid(z) # 元のデータとシグモイド関数の出力をプロット plt.plot(z, phi_z) # 垂直線を追加 plt.axvline(0.0, color='k') # y軸の上限/下限を設定 plt.ylim(-0.1, 1.1) # 軸のラベルを設定 plt.xlabel('$\phi (z)$') # y軸の目盛りを追加 plt.yticks([0.0, 0.5, 1.0]) # Axesクラスのオブジェクトの取得 ax = plt.gca() # y軸のメモリに合わせて水平グリッド線を追加 ax.yaxis.grid(True) # グラフを表示 plt.show() これを実行すると、以下の図のようなシグモイド曲線が得られます。 ポイントは以下の通りです。 $z$ が無限大に向かう場合($z \to \infty$)は $\phi(z)$ が 1 に近づく $z$ が負の無限大に向かう場合($z \to -\infty$)は $\phi(z)$ が 0 に近づく そのため、シグモイド関数は実数値を入力として受け取り、 $\phi(z)= 0.5$ を切片として、それらを $[0, 1]$ の範囲の値に出力します。 またグラフの特徴的な形(S字)からシグモイドと呼ぶのだそうです。 クラスの所属関係の確率を見積もる 次はシグモイド関数の出力を見ていきます。 サンプルが 1 に属している確率は以下の式で解釈できます。 \phi(z) = P(y=1\,| \,\boldsymbol{x};\boldsymbol{w})\\ \\ 上式はロジスティック回帰の重み $\boldsymbol{w}$ が所与のとき、 特徴量 $\boldsymbol{x}$ が与えられた条件のもとで $y=1$ になる条件付き確率を表しています。 例えば、Iris データセットの2つの品種の Iris-Versicolor ( $y=1$ ) と Iris-Setosa ( $y=0$ ) を分類するとき、 $\phi(z)= P(y=1\,| \,\boldsymbol{x};\boldsymbol{w})=0.8$ が算出される場合、 このサンプルが Iris-Versicolor である確率が 80% であることを意味します。 同様にこのサンプルが Iris-Setosa である確率は、 $P(y=0\,| \,\boldsymbol{x};\boldsymbol{w})= 1 - P(y=1\,| \,\boldsymbol{x};\boldsymbol{w}) = 0.2$ (20%)として計算できます。 クラスに属する確率を求めた後は、 量子化器(単位ステップ関数)を使って予測された確率を二値の値に変換するだけです。 \hat{y} = \left\{ \begin{array}{ll} 1 & \phi(z) \geq 0.5 \\ 0 & \phi(z) \lt 0.5 \end{array} \right.\\ \\ また総入力に着目した場合、これは以下と等価になります。 \hat{y} = \left\{ \begin{array}{ll} 1 & z \geq 0.0 \\ 0 & z \lt 0.0 \end{array} \right.\\ \\ このように、予測されるクラスラベルは、 所属クラスのみに関心があるだけでなく、 どのくらいの確率で所属しているのかを調べる際に役立ちます。 例えば、以下の例ではロジスティック回帰は力を発揮するでしょう。 気象予報における降水確率(雨が降るかどうかの確率に加えて) 患者が特定の疾患にかかっている確率(特定の症状である確率に加えて) 重みの更新 次は、ロジスティック回帰のコスト関数を見ていきます。 まずは予測の信頼度として、 以下のように尤度 $L$ (結果から見た条件のもっともらしさ)を定義します。 L(\boldsymbol{w}) = P\,(\boldsymbol{y}\,| \,\boldsymbol{x};\boldsymbol{w}) = \prod_{i=1}^{n}P\,\Bigl(y^{(i)}\,| \,x^{(i)};\boldsymbol{w}\Bigr) = \prod_{i=1}^{n}\Bigl(\phi\Bigl(z^{(i)}\Bigr)\Bigr)^{y^{(i)}}\Bigl(1 - \phi\Bigl(z^{(i)}\Bigr)\Bigr)^{1-y^{(i)}}\\ \\ \\ 第3項目は、各サンプルのクラスラベルの条件付き確率を表しており、 それを第4項目では積の形として、計算することを意味します。 このように、一つの式で クラスラベル(0 or 1)の確率分布を同時に満たせるようになりました。 しかし、より簡単に計算できるよう対数を取り、和の形で表現できるようにします。 l(\boldsymbol{w}) = \log L(\boldsymbol{w}) = \sum_{i=1}^{n}\Bigl[y^{(i)} \log \Bigl(\phi \Bigl(z^{(i)}\Bigr)\Bigr) + \Bigl(1 - y^{(i)}\Bigr) \log \Bigl(1 -\phi\Bigl(z^{(i)}\Bigr)\Bigr)\Bigr]\\ \\ \\ $l(\boldsymbol{w})$ は対数尤度(log-likelihood)関数を表しており、 先ほどの積の形より尤度の最大化を容易にします。 また、一般にモデルの性能を評価する際には、 モデルの「良さ」ではなく、「悪さ」を指標にして、 その最小値を求めるのが普通です。 そのため、対数尤度 $l(\boldsymbol{w})$ をコスト関数 $J(\boldsymbol{w})$ として書き直すと 以下のようになります。 J(\boldsymbol{w}) = - \log L(\boldsymbol{w}) = \sum_{i=1}^{n}\Bigl[- y^{(i)} \log \Bigl(\phi \Bigl(z^{(i)}\Bigr)\Bigr) - \Bigl(1 - y^{(i)}\Bigr) \log \Bigl(1 -\phi\Bigl(z^{(i)}\Bigr)\Bigr)\Bigr]\\ \\ \\ この関数について、 例えばサンプルが一つ($n=1$)の場合、 上式は以下のようになります。 \begin{align} J\bigl(\phi(z), y; \boldsymbol{w}\bigr) = -y \log\bigl(\phi(z)\bigr) - \bigl(1 - y\bigr)\log\bigl(1 - \phi(z)\bigr)\\ \end{align}\\ \\ $y=0$ の場合、一項目が 0 になり、 $y=1$ であれば二項目が 0 になります。 J\bigl(\phi(z), y; \boldsymbol{w}\bigr) = \left\{ \begin{array}{ll} - \log \bigl(\phi(z)\bigr) & (y = 1)\\ - \log \bigl(1 - \phi(z)\bigr) & (y = 0) \end{array} \right.\\ \\ 次に、この関数を可視化します。 cost_function def cost_1(z): return - np.log(sigmoid(z)) def cost_0(z): return - np.log(1 - sigmoid(z)) z = np.arange(-10, 10, 0.1) phi_z = sigmoid(z) c1 = [cost_1(x) for x in z] plt.plot(phi_z, c1, label='J(w) if y=1') c0 = [cost_0(x) for x in z] plt.plot(phi_z, c0, linestyle='--', label='J(w) if y=0') plt.ylim(0.0, 5.1) plt.xlim([0, 1]) plt.xlabel('$\phi$(z)') plt.ylabel('J(w)') plt.legend(loc='best') plt.tight_layout() plt.show() 以下のグラフは一つのサンプルに対するコスト関数です。 ここで、コスト関数の勾配を計算するために、 ロジスティック関数 $\phi(z)$ を $z$ で微分しておきます。 \begin{align} \frac{\partial}{\partial z} \phi(z) &= \frac{\partial}{\partial z} \frac{1}{1+e^{-z}}\\ \\ &= \frac{e^{-z}}{(1+e^{-z})^2} \\ \\ &= \frac{1}{1+e^{-z}}\Big(1 - \frac{1}{1+e^{-z}}\Big) \\ \\ &= \phi(z)\big(1 - \phi(z)\big)\\ \\ \end{align} 続いて、コスト関数 $L(\boldsymbol{w})$ を $j$ 番目の重み $w_j$ で微分します。 式変形、少し大変です。 \begin{align} \frac{\partial}{\partial w_j} J(\boldsymbol{w}) &= - \frac{\partial}{\partial w_j}\sum_{i=1}^{n}\Bigl[y^{(i)} \log \Bigl(\phi \Bigl(z^{(i)}\Bigr)\Bigr) + \Bigl(1 - y^{(i)}\Bigr) \log \Bigl(1 -\phi\Bigl(z^{(i)}\Bigr)\Bigr)\Bigr]\\ \\ &= - \sum_{i=1}^{n}\Bigl[y^{(i)} \frac{\partial}{\partial \,\phi\bigl(z^{(i)}\bigr)} \log \Bigl(\phi \Bigl(z^{(i)}\Bigr)\Bigr) + \Bigl(1 - y^{(i)}\Bigr) \frac{\partial}{\partial \,\phi \bigl(z^{(i)}\bigr)} \log \Bigl(1 -\phi\Bigl(z^{(i)}\Bigr)\Bigr)\Bigr]\\ \\ &= - \sum_{i=1}^{n}\Biggl[y^{(i)} \frac{\partial \log \Bigl(\phi \Bigl(z^{(i)}\Bigr)\Bigr)}{\partial \,\phi\bigl(z^{(i)}\bigr)}\frac{\partial \,\phi \bigl(z^{(i)}\bigr)}{\partial \,z^{(i)}} \frac{\partial \,z^{(i)}}{\partial \,w_j} + \Bigl(1 - y^{(i)}\Bigr) \frac{\partial \log \Bigl(1 -\phi\Bigl(z^{(i)}\Bigr)\Bigr)}{\partial\,\phi \bigl(z^{(i)}\bigr)} \Biggr]\\ \\ &= - \sum_{i=1}^{n} \Biggl[y^{(i)} \frac{\phi\bigl(z^{(i)}\bigr)}{\phi\bigl(z^{(i)}\bigr)} \Big(1 - \phi\Bigl(z^{(i)}\Bigr)\Bigr)x_{ij} + \Bigl(1 - y^{(i)}\Bigr) \frac{\partial \log \Bigl(1 -\phi\Bigl(z^{(i)}\Bigr)\Bigr)}{\partial\,\phi \bigl(z^{(i)}\bigr)}\frac{\partial \,\phi \bigl(z^{(i)}\bigr)}{\partial \,z^{(i)}} \frac{\partial \,z^{(i)}}{\partial \,w_j}\Biggr] \\ \\ &= - \sum_{i=1}^{n} \Biggl[y^{(i)} \Big(1 - \phi\Bigl(z^{(i)}\Bigr)\Bigr)x_{ij} - \frac{\Bigl(1 - y^{(i)}\Bigr)}{\Bigl(1 - \phi\bigl(z^{(i)}\bigr)\Bigr)}\phi \Bigl(z^{(i)}\Bigr)\Big(1 - \phi\Bigl(z^{(i)}\Bigr)\Bigr)x_{ij}\Biggr] \\ \\ &= - \sum_{i=1}^{n} \Bigl[y^{(i)} \Big(1 - \phi\Bigl(z^{(i)}\Bigr)\Bigr)x_{ij} - \Bigl(1 - y^{(i)}\Bigr)\phi \Bigl(z^{(i)}\Bigr)x_{ij}\Bigr] \\ \\ &= - \sum_{i=1}^{n} \Bigl(y^{(i)} - \phi \Bigl(z^{(i)}\Bigr)\Bigr)\,x_{ij} \\ \\ \end{align} ※ 途中式が長くなるのを防ぐために一項づつ計算しているところがあります。 上式がコスト関数の勾配です。 これに学習率 $\eta $ をかけ、重み $w_j$ を更新します。 \Delta w_j = - \eta \Bigl(- \sum_{i=1}^{n} \Bigl(y^{(i)} - \phi \Bigl(z^{(i)}\Bigr)\Bigr)\,x_{ij}\Bigr)\\ \\ \\ w_j \leftarrow w_{j} + \Delta w_j \\ ここまで、一通りロジスティック回帰を見てきました。 次は実装です。 Irisデータをロード colab notebook に入力して sklearn のバージョンを確認します。 ipynb import sklearn print(sklearn.__version__) # 0.22.2.post1 2021/05/05 現在、colab notebook ではデフォルトで 上記のバージョンが使用されるようです。 ※ バージョンによっては、これより下のコードの整合性が取れないことがあるかもしれません。 続いて、以下のコードで学習に必要なデータをロードし、 X に特徴量、y にラベルを格納します。 ipynb from sklearn import datasets import numpy as np iris = datasets.load_iris() X = iris.data[:, [2, 3]] # 2: petal length, 3: petal width y = iris.target 前回同様、データをグラフにプロットしたいので、 考慮する特徴量は、 花びらの長さ(petal length)と 花びらの幅(petal width)のみになります。 以下のコードでラベルの確認をします。 ipynb print("Class labels: ", np.unique(y)) # [0 1 2] Iris-Setosa: 0, Iris-Versicolor: 1, Iris-Virginica: 2 として、与えられています。 ロジスティック回帰の学習 次にモデルの汎化性能を確保するために、 データ全体から トレーニングデータセットとテストデータセットに 分割します。 以下のコードで、 テストデータの割合を30%(45個のサンプル)に指定します。 ipynb from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) 続いて特徴量のスケーリングを行います。 各特徴量の取りうる値を揃えるために、 StandardScalerを使います。 以下のコードで、標準化できます。 ipynb from sklearn.preprocessing import StandardScaler sc = StandardScaler() # fitメソッドで平均と標準偏差を計算 sc.fit(X_train) X_train_std = sc.transform(X_train) # transformメソッドで標準化 X_test_std = sc.transform(X_test) StandardScalerのfitメソッドを使うと 特徴量ごとの平均 μ と標準偏差 σ を計算できます。 これらのパラメータを使い、 transform メソッドでトレーニングデータを標準化します。 テストデータにも 同じスケーリングパラメータを適用したのは、 相互の基準を揃え、比較できるようにするためです。 これでロジスティック回帰をトレーニングできる状態になりました。 以下のコードで、 ロジスティック回帰のインスタンスを作成し、 トレーニングを実行します。 ipynb from sklearn.linear_model import LogisticRegression # インスタンスの生成 lr = LogisticRegression(random_state=0) # トレーニングデータをモデルに適合させる lr.fit(X_train_std, y_train) ※ 正則化パラメータはデフォルト(C=1.0)の値が適用されます。 学習結果の可視化 続いて、 トレーニングしたモデルを可視化します。 モデルの決定領域をプロットし、 未知のデータに対してその程度識別できるか見ていきます。 以下のコードで、描画の定義をします。 ipynb from matplotlib.colors import ListedColormap import matplotlib.pyplot as plt def plot_decision_regions(X, y, classifier, test_idx=None, resolution=0.02): # マーカーとカラーマップの準備 markers = ('s', 'x', '^', 'v') colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan') cmap = ListedColormap(colors[:len(np.unique(y))]) # 決定領域のプロット x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1 x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1 # グリッドポイントの生成 xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution)) # 各特徴量を1次元配列に変換して予測を実行 Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T) # 予測結果を元にグリッドポイントのデータサイズに変換 Z = Z.reshape(xx1.shape) # グリッドポイントの等高線をプロット plt.contourf(xx1, xx2, Z, alpha=0.5, cmap=cmap) # 軸の範囲を設定 plt.xlim(xx1.min(), xx1.max()) plt.ylim(xx2.min(), xx2.max()) # クラスごとにサンプルをプロット for idx, cl in enumerate(np.unique(y)): plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1], alpha=0.6, color=cmap(idx), marker=markers[idx], label=cl) # テストサンプルを目立たせる(黒で描画) if test_idx: X_test, y_test = X[test_idx, :], y[test_idx] plt.scatter(X_test[:, 0], X_test[:, 1], color=(0, 0, 0), alpha=0.6, linewidths=1, marker='o', s=55, label='test set') この関数で、 トレーニングデータとテストデータをプロットします。 以下のコードで、実際に描画します。 ipynb # トレーニングデータとテストデータの特徴量を行方向に結合 X_combined_std = np.vstack((X_train_std, X_test_std)) # トレーニングデータとテストデータのクラスラベルを結合 y_combined = np.hstack((y_train, y_test)) # 決定境界をプロット plot_decision_regions(X_combined_std, y_combined, classifier=lr, test_idx=range(105, 150)) plt.xlabel('petal length [standardized]') plt.ylabel('petal width [standardized]') plt.legend(loc='upper left') plt.show() これを実行すると、以下の図ような出力が得られるはずです。 テストデータは黒色でプロットしており、 微妙に分類できていないサンプルも存在しますが、 ある程度は学習できていそうです。 以下のコードで実際のクラスを予測してみます。 ipynb print(lr.predict_proba(X_test_std[0, :].reshape(1, -1))) # [[4.57718485e-05 4.30250370e-02 9.56929191e-01]] これは、テストデータの先頭のサンプルに対して、 Iris-Virginica である確率が 95.7% であることを示しています。 (クラス2: Iris-Virginica の値が最も高い) ここまで理論を数式で理解しようと試みました。 今回メインで使用したPython機械学習の本では、 理論のさわりしか取り扱っておらず、数式の導出が意外にも省略されていたため、 分野をまたいで調べる必要がありました。 追加で正則化についても述べたい。 数式のTex入力疲れる。 まとめ 以下に今回のまとめを記載します。 ロジスティック回帰は産業界で広く利用されている分類アルゴリズムの一つ。 ロジット関数の逆関数がロジスティック関数。 クラスの所属関係の確率を出力。 次回は CNN かサポートベクトルマシンを取り扱う予定です。 参考文献 ロジット関数とロジスティック関数 1からみなおす線形モデル (2) - 一般化線形モデル 一般化線形モデル ロジスティック関数とシグモイド関数 Sebastian Raschka (2015). Python Machine Learning-1st Edition, Packt Publishing. (セバスチャン・ラシュカ, 株式会社クイープ(訳) (2016). Python機械学習プログラミング達人データサイエンティストによる理論と実践, 株式会社インプレス, pp.54-66) Qiitaの数式チートシート Qiitaでの数式記述のベクトル表記 ロジスティック回帰 Python Machine Learning - Code Examples
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python

出力 略記号を入力:FE 基本情報技術者試験です 略記号を入力:QUIT #このQUITが出たら終了 を表示したいんですが id = {"IP":"ITパスポート試験","SG":"情報セキュリティマネジメント試験", "FE":"基本情報技術者試験"} s = int(input("略記号を入力:")) print(id{s}) while True: if id == "QUIT" else: print() エラーがでて表示されないので間違っている所を教えてくれませんか、、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習の学習完了をslackに通知したい

はじめに 機械学習って学習時間長くないですか? いつ終わるんだろうとか、クラウドの場合は、終了時間を覚えておかないとインスタンスの時間かかって高額の使用量が請求されてしまうなんでこともあると思います。(経験談) そこで、今回は学習結果と、完了通知をslackに通知します。 1. Incoming webhookの利用 まずは、slackとアプリの連携を行いましょう。 Incoming webhookというAPIを使用します。 先人の方がとても細かくまとめてくださっているのでこちらを参考にしました。 そのため、Incomingwebhookの登録方法は省きます。 2. サンプルコード こちらです。 notificaton.py import slackweb slack = slackweb.Slack(url="Your IncomigWebhooks URL") slack.notify(text="通知したい文字") 3.使用方法 学習するモジュール上に先ほどのコードを埋め込みます。 以下はpytorchを想定しています。 train.py import slackweb slack = slackweb.Slack(url="Your IncomigWebhooks URL") def run_training(): #省略 for epoch in range(Config.EPOCHS): loss, acc = train_fn(model, trainloader, optimizer, scheduler, epoch) torch.save(model.state_dict(),'modelname_epoch.pt') slack.notify(text="学習が完了しました") おわり いかがでしたでしょうか? 非常に簡単に学習完了通知ができるので、皆さんも是非試してみてください。 よい機械学習ライフを!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python boot camp day11

Mission>> Black Jack game Rule1:1枚ずつカードを引いて、その和が21より小さいかつより近い方が勝者 Rule2:カードの和が21を超えた時点で自分の負け Rule3: J,Q,Kは10として扱い、1は都合に応じて1or11どちらかとして扱うことが可能。 →まずは1だけ特別扱いするのは難しいので、1は11として扱うことにして以下作成。 BJ0.py from art import logo import random cards = {1:11,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,10:10,"J":10,"Q":10,"K":10} you = 0 pc = 0 def blackjack(): print(logo) key1 = random.choice(list(cards.keys())) #key2 = random.choice(list(cards.keys())) #最初2枚いっぺんに引いたけど、1枚ずつに変更 value1 = cards[key1] #value2 = cards[key2] print(f"Your cards: {key1}") total = you + value1 return total def PC_blackjack(): key3 = random.choice(list(cards.keys())) value3 = cards[key3] print(f"Computer cards: {key3}") PC_total = pc + value3 return PC_total def add(): key_add = random.choice(list(cards.keys())) value_add = cards[key_add] return key_add,value_add total = blackjack() #上記関数でreturnした値をその名前の変数に格納(you) PC_total = PC_blackjack() #上記関数でreturnした値をその名前の変数に格納(PC) k,v = add() #add()だけ値が2つreturnされるので、各々変数に格納 pk,pv = add() #PCのkey,PCのvalueってことでpk,pv deal = True while deal: if total == 21: #you=21 print("You win!!") deal = False elif total > 21 or PC_total == 21: #you>21 or PC=21 print("You lose...") deal = False elif total < 21 and PC_total < 21: #you<21 and PC<21 while not input("Another card? [yes/no] ") == "no": #no以外でインクリメント total += int(v) #add newcard to you PC_total += int(pv) #add newcard to PC print(f"You added {k}, now your total: {total}") print(f"Computer added {pk}, now PC total: {PC_total}") if (total == 21 and PC_total < 21) or (total < 21 and PC_total > 21): #追加後 print("You win!!") deal = False break elif (total < 21 and PC_total == 21) or (total > 21 and PC_total < 21): print("PC wins!!") deal = False break elif (total > 21 and PC_total > 21) or (total < 21 and PC_total < 21) and total == PC_total: print("draw!!") deal = False break else: #no選択し、インクリメントしない場合 sa = int(21-total) - int(21-PC_total) #int(-2)=2。絶対値にしてくれる if sa < 0 or PC_total > 21: #sa="差" print("You win!") deal = False break elif sa == 0: #絶対値が同値だった場合 print("draw") break else: print("PC wins...!!") break # deal = True # while deal: # if not input("Are you ready? [deal/finish] ") == "finish": # blackjack() # else: # print("Bye...") # deal = False 何も見ずに一から自分で書いてみた。 若干スパゲッティ感あるけど、達成感あり!場合分けのところ難しい。 もっとシンプルに書けるはず。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python boot camp by Dr.Angela day11

Mission>> Black Jack game Rule1:1枚ずつカードを引いて、その和が21より小さいかつより近い方が勝者 Rule2:カードの和が21を超えた時点で自分の負け Rule3: J,Q,Kは10として扱い、1は都合に応じて1or11どちらかとして扱うことが可能。 →まずは1だけ特別扱いするのは難しいので、1は11として扱うことにして以下作成。 BJ0.py from art import logo import random cards = {1:11,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,10:10,"J":10,"Q":10,"K":10} you = 0 pc = 0 def blackjack(): print(logo) key1 = random.choice(list(cards.keys())) #key2 = random.choice(list(cards.keys())) #最初2枚いっぺんに引いたけど、1枚ずつに変更 value1 = cards[key1] #value2 = cards[key2] print(f"Your cards: {key1}") total = you + value1 return total def PC_blackjack(): key3 = random.choice(list(cards.keys())) value3 = cards[key3] print(f"Computer cards: {key3}") PC_total = pc + value3 return PC_total def add(): key_add = random.choice(list(cards.keys())) value_add = cards[key_add] return key_add,value_add total = blackjack() #上記関数でreturnした値をその名前の変数に格納(you) PC_total = PC_blackjack() #上記関数でreturnした値をその名前の変数に格納(PC) k,v = add() #add()だけ値が2つreturnされるので、各々変数に格納 pk,pv = add() #PCのkey,PCのvalueってことでpk,pv deal = True while deal: if total == 21: #you=21 print("You win!!") deal = False elif total > 21 or PC_total == 21: #you>21 or PC=21 print("You lose...") deal = False elif total < 21 and PC_total < 21: #you<21 and PC<21 while not input("Another card? [yes/no] ") == "no": #no以外でインクリメント total += int(v) #add newcard to you PC_total += int(pv) #add newcard to PC print(f"You added {k}, now your total: {total}") print(f"Computer added {pk}, now PC total: {PC_total}") if (total == 21 and PC_total < 21) or (total < 21 and PC_total > 21): #追加後 print("You win!!") deal = False break elif (total < 21 and PC_total == 21) or (total > 21 and PC_total < 21): print("PC wins!!") deal = False break elif (total > 21 and PC_total > 21) or (total < 21 and PC_total < 21) and total == PC_total: print("draw!!") deal = False break else: #no選択し、インクリメントしない場合 sa = int(21-total) - int(21-PC_total) #int(-2)=2。絶対値にしてくれる if sa < 0 or PC_total > 21: #sa="差" print("You win!") deal = False break elif sa == 0: #絶対値が同値だった場合 print("draw") break else: print("PC wins...!!") break # deal = True # while deal: # if not input("Are you ready? [deal/finish] ") == "finish": # blackjack() # else: # print("Bye...") # deal = False 何も見ずに一から自分で書いてみた。 若干スパゲッティ感あるけど、達成感あり!場合分けのところ難しい。 もっとシンプルに書けるはず。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonによるcsvファイル読み込みとグラフ化

今回は自分用のメモとしてpandasを用いてcsvファイルを読み込み,読み込んだデータをグラフ表示することをまとめる. pandasによるcsvファイルの読み込み 今回は相対パスでプログラムとcsvファイルが同じディレクトリ内にある場合で示す.csvファイルについては気象庁のデータから月の日照時間データを使うことにした. 気象庁 過去の気象データダウンロード pandasは日本語をうまく読み込めないので,csvファイルの日本語データを英語に直す必要がある.とりあえず今回はデータのうち日付と日照時間の合計のみを用いることにする.またデータを折れ線グラフでグラフ表示してみる. read_csv.py import pandas as pd data1 = pd.read_csv('data.csv') #データの確認 print(data1) #折れ線グラフで表示 data1.plot() データの確認をすると一応の表の値は以下のように得られる. さらに得られた折れ線グラフは以下のようになる. これ以外にもプロット方法については以下のようなことがコマンドがある. pandas_graph_command data.plot() #折れ線グラフ data.plot.bar(stacked=True) # 積み上げ棒グラフ data.plot.scatter('date','time(hour)') # 列を指定して散布図 data['time(hour)'].plot.hist() # 列を指定してヒストグラム とりあえずは以上です. 後は時間があれば内容を充実していこうと思います. 参考文献 interface 2021年6月号 京大 python 演習
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SkPy ― Python で Skype に投稿

概要 SkPy を使用して、Python から簡単に Skype へ投稿できます。 投稿のみにしか利用していないので、メッセージの送信のみまとめます。 Microsoft のオフィシャルなライブラリではないので、信頼性を求める場合は Microsoft Bot Framework の使用を検討すると良いかもしれません。 インストール pip install skpy ※ Anaconda を使用の場合は、仮想環境を使用して環境構築後、最後に pip install が良いようです。 詳しくは、以下、参照してください。 Using Pip in a Conda Environment Installing non-conda packages ログイン from skpy import Skype sk = Skype(user, pwd) # user: Skype username, pwd: Skype account password chat.id 取得 メッセージの送信時に、使用する id を取得します。 for chat in sk.chats.recent(): print(chat) SkPy ドキュメント「Iterating chats」に実行結果があるので参照してください。 メッセージ送信 channel = sk.chats.chat('19:*****.skype') # 取得した id を指定 channel.sendMsg("Test") ID で指定したグループチャットに 「Test」が表示されます。 補足 24時間 でセッションが切れるようです 24時間で、token が無効になるようなので、毎日同じ時刻に再接続しています。 試していませんが、Skype() の3番目の引数で path を指定するとセッション情報が保持されるようです。 token が無効になると再接続という動作になるようです。 詳しくは、「Rate limits and sessions 」を参照してください。 旧 Skype ID でのログイン失敗 昔から Skype で使用しているアカウントでログインができなくなったことがあります。 Microsoft の仕様変更が原因だったようで「skpy GitHub Issue」を参考に試行錯誤しました。 旧Skype ID でサインインして、アカウントエイリアス(***@outlook.jp)を追加することで、ログインできるようになりました。 (他にもいくつか変更していますが、最終的に解決したときに変更したものがこれです。) ログインできないときは、SkPy Guides 「Logging in」も参照してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Atcoder ABC186 E - Throne : CRT 中国剰余定理

初めてのCRT入門。 この問題は次のように言い換えられます。 $s, k, n$が与えられたとき、$s + kx \equiv 0 (\bmod n)$となる$x$を答えよ。存在しない場合、$-1$を答えよ。 CRTを使います。CRTは、ある数$a$を考えたときに、$b_1, b_2, m_1, m_2$を与えることで(これは、2つでなくて複数個与えることもできます) $$a \equiv b_1 (\bmod m_1) \\ a \equiv b_2 (\bmod m_2) $$となるすべての$a$および、$\bmod m$を求めます。ここで、今回の問題について次の2式を考えます。 $kx \equiv 0 (\bmod k) $: これはひどく当たり前の式ですが、1つ目の式として考えます。$k$が整数であることから明らかです。 $kx \equiv (n-s) (\bmod n) $: まず、$s + kx \equiv 0 (\bmod n) $がもとめたかった式であることを思い出します。ここで、$s$を右辺に移し、$kx \equiv - s (\bmod n) $となります。定義より、$kx \equiv n - s (\bmod n) $となります。言葉でいえば、$kx$の$mod n$をとると$n-s$になるもの。なので、つまり、最初の座席から王座に到達できるだけ(modの世界で)動ける$kx$になります。 ということで、 $$ kx \equiv 0 (\bmod k) \\ kx \equiv (n-s) (\bmod n) $$ をCRTして$a,m$を得ればよいです。ここで、$a$と$m$の扱いですが、$a$を答えとすればよいです。もしこれが、最初の最小の値ではなくて、$c$回目に座る回数なら、$a + (c-1)m$を答えればよいです。 実装 def egcd(a, b): if a == 0: return b, 0, 1 else: g, y, x = egcd(b % a, a) return g, x - (b // a) * y, y # https://qiita.com/drken/items/ae02240cd1f8edfc86fd # a = b1 (mod m1) # a = b2 (mod m2) # となるような、 a = r (mod m) を返す # m=-1のとき解なし。 def crt(bList, mList): r, m = 0, 1 for i in range(len(bList)): d, x, y = egcd(m, mList[i]) if (bList[i] - r) % d != 0: return [0, -1] tmp = (bList[i] - r) // d * x % (mList[i] // d) r += m * tmp m *= mList[i] // d return [r, m] for _ in range(int(input())): n,s,k = map(int, input().split()) r, m = crt([0, n-s], [k, n]) if m == -1: print(-1) else: print(r // k)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

因数分解するゲーム「Wallprime」をAIで解いてみた

はじめに Wallprime(ワルプライム)という素因数分解をして壁をどんどん破壊していくスマホのゲームを、AIの物体検出を使用して解いてみました。 こんなゲームです。 このゲームの画面をPythonのOpenCVを使用して映像を読み取り、Microsoft Azure Cognitive ServicesのCustom Visionを使用して物体検出して、その結果から素因数分解を行い、表示を行います。 作ったもの 作ったものはこのようなものになってます。 (打つのが遅くてあまりスコアは出せてません。。) どうやって作ったか Custom Visionを使用することで、「画像分類」や「物体検出」を行うことができます。また、クラウド上で、「アノテーション」、「学習」、「推論」の一連の流れを行うことができます。 作成したアプリケーションは、以下を行います。 wallprimeで表示された数値の認識(Custom Vision) 認識した数値を素因数分解 認識した数値と素因数分解した数値を画面に表示 Custom Visionを使用して、アプリケーションを構築する流れは、一般的な物体検出を行うディープラーニングと同様で以下の流れになります。 データ収集(wallprimeのアプリケーション画像を集める) アノテーション(数値の0~9のタグ付けを行う) 学習 推論 詳しい作り方と使用したソースコードはブログに記載しています。 https://remix-yh.net/1156/ さいごに AzureのCustom Visionを使うことで、AIを詳しくなくても簡単に作成することができました。(アノテーション含めて、半日くらい) AIで物体検出をする全体像(アノテーションから推論まで)を知るという意味でも、面白くて良い題材かと思いますので、やってみてはいかがでしょうか。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Educational Codeforces Round 102 B. String LCM: Z-アルゴリズムを使った繰り返し文字列の判別

Zアルゴリズムは不要ですが、初めて実戦で使ったのでメモ その文字列がある文字列を何回繰り返してできているのか?の判定が必要です。制約的に愚直に計算できますが、Zアルゴリズムを使って解きました。 題意 ある文字列s,tが与えられる ここで、ある文字列strを考えたときに、別の文字列aをb回繰り返してstrが得られるとき、strはaで割り切れるとする。 例えば、abababはab x 3回なので、abで割り切れ、 ababab x 1回なので、abababで割り切れる 例えば、abcはabc x 1回でしか割り切れないので、abcでのみ割り切れる s,tで割り切れる最小の文字列を答えよ。存在しない場合は-1を出力せよ。 こう考えた 考察は解説に任せます、この問題は文字列のLCM(一般的なものではなく、この問題の範囲の中での言葉です)を求める問題です。 上記例のようにこの問題は、整数の約数のように考えられます。文字札s,tに共通に含まれる共有の約文字列(約数のようなもの)を求め、それぞれの個数のLCMを求めた回数繰り返す。 ポイント:ある文字列の約文字列(約数)の求め方 さて、この動作に必要なのは、以下のような情報を列挙することです。 abababはab x 3回なので、abで割り切れ、 ababab x 1回なので、abababで割り切れる abcはabc x 1回でしか割り切れないので、abcでのみ割り切れる 以下のように考えます。 あるn文字の文字列の1文字目からi文字目($1\leq i \leq n$)文字目を使って、元の文字列が作れるかを判定する この際、nがiで割り切れなければ、その文字列をつなげて作成できるものではないので候補にはならない abababはababをマッチングの方法によっては2回含みますが、この問題の題意では、1回含むなら、abab 2回含むならababababになるので、ababからabababを作ることはd系ません i文字の文字だけで構成される可能性があるなら、元の文字列の1~i文字目, i+1 ~ 2i文字目 , ... , ki+1 ~ n 文字目にその文字が含まれるかを確認する ここで、Zアルゴリズムを使います。 iをnまでインクリメントさせながら、1-i文字目までと元の文字列を連結した文字列にZアルゴリズムを適応する 得られた配列を元の文字列の1文字目からstep iで回し、i以上であることを確認する すべてのforでi以上なら、その文字は1からi文字目までの繰り返しでできている。失敗すれば、1からi文字目までの繰り返しでその文字列を作ることはできない 実装 class zAlgorithm(): def __init__(self, s): self.sdat = list(map(lambda x: ord(x), s)) self.sl = len(s) self.res = [0] * self.sl self.res[0] = self.sl i, j = 1, 0 while i < self.sl: while (i+j) < self.sl: if self.sdat[j] != self.sdat[i+j]: break j += 1 self.res[i] = j if(j == 0): i += 1 continue k = 1 while (i+k) < self.sl and (k+self.res[k] < j): self.res[i+k] = self.res[k] k += 1 i += k j -= k def stringDivisors(s): z = zAlgorithm(s) res = dict() for i in range(len(s) // 2): curTryLen = i+1 if len(s) % curTryLen != 0: continue target = s[:i+1] t = s[:i+1] + ":" + s z = zAlgorithm(t) can = True for j in range(0, len(s) , curTryLen): if z.res[len(target) + 1 + j] >= len(target): continue can = False break if can: res[target] = len(s) // curTryLen res[s] = 1 return res import math def lcm(x, y): return (x * y) // math.gcd(x, y) q = int(input()) for _ in range(q): s, t = input(), input() sdiv = stringDivisors(s) tdiv = stringDivisors(t) reslen = 0 resval = "" for k in list(sdiv.keys()): if k not in tdiv: continue tmps = str(k) * lcm(sdiv[k], tdiv[k]) if len(tmps) > reslen: reslen = len(tmps) resval = tmps if resval == "": print(-1) else: print(resval)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

inputで入力した数字がうまく反映してくれない? (今日のPython Day5)

0. はじめに  参考書を読んで理解できてもいざ自分でコードを書こうとすると上手く行かないことがよくあります。一緒にアウトプットしませんか?  毎日1問、Pythonの問題を出題します。出題範囲は特に定めていませんがはじめの1ヶ月くらいは『入門Python3 第2版』の第1~11章までのことが分かれば解ける問題にしたいと思います。「こういう問題を作って欲しい」などのリクエストがございましたら初心者ながら頑張って作問します。また「別解を思いついた」、「間違えを見つけた」などがありましたら遠慮なくコメント欄にて教えて下さい。記事を執筆している当人もこの記事を読んでくださった方も新たなことを学ぶことができるので。 1. 問題  以下のように、数字の1を入力すると「私も!」が、数字の0を入力すると「そいういときもあるよ」と表示されるプログラムを太郎君は作成した。しかし1と0を入力しても「正しい数を入力してください」と表示されてしまう。太郎くんに代わってプログラムを直そう! print("pythonが楽しいと思う人は1を、楽しくないと思う人は0を入力してください。") ans = input() if ans == 1: print("私も!") elif ans == 0: print("そいういときもあるよ") else: print("正しい数を入力してください") 2. ヒント ansの型がどうなっているか確認してみましょう。 3. 解答 print("pythonが楽しいと思う人は1を、楽しくないと思う人は0を入力してください。") ans = int(input()) # int()を追加する。 if ans == 0: print("私も!") elif ans == 1: print("そいういときもあるよ") else: print("正しい数を入力してください") 4. 解説  input()関数を用いて入力された値は文字列になります。今回の問題では入力された値が整数の0か1かを判断したいのでansを文字列から整数に変換するためにint()関数を追加する必要があります。 5. おまけトーク  プログラミング習いたての人に説明するのって難しいですよね。例えば今回の記事も最初は「input関数の戻り値は文字列になります。」のように書こうとしたのですが、「戻り値って何?」となりそうだなと思いやめました。「戻り値なんて分かって当然だろ。初心者だからといってバカにするな!」と言われそうですが、念には念を入れました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

にゃんしぶるを支える技術

Ansible界隈の人たちとの会話で何気なく「にゃんしぶる」という単語を使ったら、Ansible実行時にネコチャンが出てくるモジュールとか作りそうな流れになってきたので、ひとまずメッセージ出力でにゃんしぶるできないかどうか試してみましたにゃーん。。。 環境 [zaki@manager nyan]$ ansible --version ansible 2.8.5 config file = /home/zaki/work/ansible/nyan/ansible.cfg configured module search path = [u'/home/zaki/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python2.7/site-packages/ansible executable location = /usr/bin/ansible python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] [zaki@manager nyan]$ [zaki@manager nyan]$ cat /etc/redhat-release CentOS Linux release 7.7.1908 (Core) [zaki@manager nyan]$ なお、本記事の内容は、cowsayの使い方・アレンジ方法と、Ansible Callback Pluginの簡単な作り方という構成になってます。 解説はいいからちょっと試したい 詳細は後述してるけど、/usr/local/bin/cowsayに以下のスクリプトをコピペ。(スクリプトの引数を元に整形してascii artをprintしてるだけです) /usr/local/bin/cowsay #!/usr/bin/perl my $border = "-" x ((length $ARGV[-1]) + 2); print <<"__EOL__"; $border < $ARGV[-1] > $border  \\ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ __EOL__ そしてchmod 755 /usr/local/bin/cowsayで実行権限を付与して、Ansibleを実行すればOK 元に戻したくなったら、/usr/local/bin/cowsayを削除すればOKです。 cowsay でにゃんしぶる Ansible実行時に、システムにcowsayコマンドがインストールされている場合、このコマンドを使ってtask名を出力する機能が備わっている。 Ansibleからcowsayを呼び出している部分はこの箇所 ansible/ansible https://github.com 自前の cowsay を用意してにゃんしぶる Ansible実行時にcowsayがインストールされているかどうかは、以下のリストに実行ファイルがあるかどうかを見て判断している。 b_COW_PATHS = ( b"/usr/bin/cowsay", b"/usr/games/cowsay", b"/usr/local/bin/cowsay", # BSD path for cowsay b"/opt/local/bin/cowsay", # MacPorts path for cowsay ) これらのパスのどこかに、実行可能なcowsayスクリプト等を配置すればOK。 逆に言えば、Ansible実行時にパスが通っている場所(~/local/bin/cowsayとか)にcowsayを置いていても、これらのパス以外であれば反応しない。 なので、yumなどでcowsayをインストールしなくても、/usr/local/bin/cowsayを自分で用意してしまえば、Ansibleでにゃんしぶるが可能。 (※ chmod 755で実行権限を与えるのをお忘れなく) 実装例 例えばPerlスクリプト /usr/local/bin/cowsay #!/usr/bin/perl my $border = "-" x ((length $ARGV[-1]) + 2); print <<"__EOL__"; $border < $ARGV[-1] > $border  \\ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ __EOL__ オリジナルのcowsayがインストール済みで、それ以外は特に追加設定を行わない場合、Ansibleからはcowsay -W 60 -f default "メッセージ"という形式でcowsayが実行される。 def banner_cowsay(self, msg, color=None): if u": [" in msg: msg = msg.replace(u"[", u"") if msg.endswith(u"]"): msg = msg[:-1] runcmd = [self.b_cowsay, b"-W", b"60"] if self.noncow: thecow = self.noncow if thecow == 'random': thecow = random.choice(list(self.cows_available)) runcmd.append(b'-f') runcmd.append(to_bytes(thecow)) runcmd.append(to_bytes(msg)) cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() self.display(u"%s\n" % to_text(out), color=color) この関数の runcmd = [self.b_cowsay, b"-W", b"60"] [...] runcmd.append(b'-f') runcmd.append(to_bytes(thecow)) の部分。 なので、自作のcowsayを作る場合は、この呼び出し仕様を踏まえて、引数の一番最後を取り出して出力すれば、にゃんしぶるが実行できる。 (Perlにおいてスクリプトの引数は@ARGV配列で受け取ることができ、配列の末尾を参照するには配列インデックスに-1を指定すれば取り出せる) 実行例 [zaki@manager nyan]$ ansible-playbook playbook.yml [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' ------------------ < PLAY [localhost] > ------------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ ------------------------ < TASK [Gathering Facts] > ------------------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ ok: [localhost] ------------- < TASK [ping] > -------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ ok: [localhost] ------------ < PLAY RECAP > ------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 [zaki@manager nyan]$ cowsay に自前のテンプレートを追加してにゃんしぶる cowsay自体はパッケージインストールし、cowsayの出力テンプレートを用意することでもにゃんしぶるできる。 cowsayはデフォルトで牛のAAでメッセージを表示するが、表示形式はいくつかサンプルがあり、オプションで表示を変えることができる。 女子エンジニアはcowsayで女子力をupさせよう - Qiita cowsay の設定 [zaki@manager nyan]$ cowsay Ansible _________ < Ansible > --------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || 選択できるテンプレート [zaki@manager nyan]$ cowsay -l Cow files in /usr/share/cowsay: beavis.zen blowfish bong bud-frogs bunny cheese cower default dragon dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters head-in hellokitty kiss kitty koala kosh luke-koala mech-and-cow meow milk moofasa moose mutilated ren satanic sheep skeleton small sodomized stegosaurus stimpy supermilker surgery telebears three-eyes turkey turtle tux udder vader vader-koala www [zaki@manager nyan]$ tuxを選んでみる [zaki@manager nyan]$ cowsay -f tux Ansible _________ < Ansible > --------- \ \ .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/ [zaki@manager nyan]$ じゃあこのテンプレートってどこにあるのかというと、リストの1行目Cow files in /usr/share/cowsay:の通り、/usr/share/cowsay以下にある。 [zaki@manager nyan]$ ls -F /usr/share/cowsay/ DragonAndCow.pm default.cow luke-koala.cow stimpy.cow Example.pm dragon-and-cow.cow mech-and-cow.cow supermilker.cow Frogs.pm dragon.cow meow.cow surgery.cow MechAndCow.pm elephant-in-snake.cow milk.cow telebears.cow Stegosaurus.pm elephant.cow moofasa.cow three-eyes.cow TextBalloon.pm eyes.cow moose.cow turkey.cow TuxStab.pm flaming-sheep.cow mutilated.cow turtle.cow beavis.zen.cow ghostbusters.cow ren.cow tux.cow blowfish.cow head-in.cow satanic.cow udder.cow bong.cow hellokitty.cow sheep.cow vader-koala.cow bud-frogs.cow kiss.cow skeleton.cow vader.cow bunny.cow kitty.cow small.cow www.cow cheese.cow koala.cow sodomized.cow cower.cow kosh.cow stegosaurus.cow 中身はPerlスクリプトで、$the_cow変数に出力するAAをセットしている。 /usr/share/cowsay/tux.cow ## ## TuX ## (c) pborys@p-soft.silesia.linux.org.pl ## $the_cow = <<EOC; $thoughts $thoughts .--. |o_o | |:_/ | // \\ \\ (| | ) /'\\_ _/`\\ \\___)=(___/ EOC [zaki@manager nyan]$ ということは、あとはわかるな? /usr/share/cowsay/cat.cow ## ## A cow wadvertising the World Wide Web, from lim@csua.berkeley.edu ## $the_cow = <<EOC;  \\ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ EOC ちなみに、日本語(多バイト文字)を含む場合、UTF-8では文字化けするため、UTF-16 LEで保存してある。 [zaki@manager ~]$ cowsay -l Cow files in /usr/share/cowsay: beavis.zen blowfish bong bud-frogs bunny cat cheese cower default dragon dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters head-in hellokitty kiss kitty koala kosh luke-koala mech-and-cow meow milk moofasa moose mutilated ren satanic sheep skeleton small sodomized stegosaurus stimpy supermilker surgery telebears three-eyes turkey turtle tux udder vader vader-koala www ↑catが追加されている。 catを指定して実行。 [zaki@manager ~]$ cowsay -f cat Ansible _________ < Ansible > ---------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ [zaki@manager ~]$ なお、パッケージで配置されるファイルの置き場である/usr/share/cowsayに自前のファイルを紛れさせるのが宗教上の理由等により不可能な場合は、/usr/local/share/cowsay/cat.cowに保存し、実行時に環境変数COWPATHで指定しても動く。(詳細はmanかcowsayのソース) [zaki@manager ~]$ COWPATH=/usr/local/share/cowsay cowsay -l Cow files in /usr/local/share/cowsay: cat [zaki@manager ~]$ COWPATH=/usr/local/share/cowsay cowsay -f cat aaa _____ < aaa > -----  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ [zaki@manager ~]$ 複数パス設定 [zaki@manager ~]$ COWPATH=/usr/local/share/cowsay:/usr/share/cowsay cowsay -l Cow files in /usr/local/share/cowsay: cat Cow files in /usr/share/cowsay: beavis.zen blowfish bong bud-frogs bunny cheese cower default dragon dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters head-in hellokitty kiss kitty koala kosh luke-koala mech-and-cow meow milk moofasa moose mutilated ren satanic sheep skeleton small sodomized stegosaurus stimpy supermilker surgery telebears three-eyes turkey turtle tux udder vader vader-koala www [zaki@manager ~]$ Ansible の設定 cowsayで自前のテンプレートでメッセージ出力する準備ができたので、Ansibleから指定のテンプレートを使う設定を行う。 といっても、ansible.cfgに記述するだけ。 (自前のテンプレートが/usr/local/share/cowsayなどデフォルト以外にある場合はCOWPATH=/usr/local/share/cowsayが必要) ANSIBLE_COW_SELECTION ansible.cfg [defaults] cow_selection = cat ちなみにここにtuxなどの、cowsay -lで表示されるリストの項目を記述すれば、その内容でAnsibleの出力が行われる。 また、randomにすれば、使用可能なテンプレートから出力のたびにランダムで一つ選ばれる。 実行例 [zaki@manager nyan]$ ansible-playbook playbook.yml [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' __________________ < PLAY [localhost] > ------------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ ________________________ < TASK [Gathering Facts] > ------------------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ ok: [localhost] _____________ < TASK [ping] > -------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ ok: [localhost] ____________ < PLAY RECAP > ------------  \ ∧_∧   .ミ,,・_・ミ ヾ(,_uuノ localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 [zaki@manager nyan]$ cowsay を無効にする ansible.cfg [defaults] nocows = True あるいはyum erase cowsayで削除する。 callback plugin でにゃんしぶる ここまでのcowsayを使ったにゃんしぶるだと、play名やtask名を出力しているだけなので、正常時はかわいいネコチャン、エラー時は毛が逆立ったネコチャン、みたいなことはできない。 この辺りをイジる場合は、callback plugin(普段使う時も「デフォルトのエラー見づらいわー、あ、でもyamlのstdout_callback使うと見やすくなるらしいで」ってアレ)を使えば可能。 「にゃんしぶる用のcallback pluginを自作」することになるが、「何も設定しないときに使用されるdefaultcallback plugin」をベースに作れば、表示をイジる程度であれば、コピペで簡単にできる。 Ansibleのcallback pluginを使って突然の死(echo-sd)を表現する - Qiita callback pluginはAnsible本体と同じくPython製。 CentOS7でepelからパッケージインストールしたansible 2.8の場合は/usr/lib/python2.7/site-packages/ansible/plugins/callback/*.pyにプラグインのソースがある。 にゃんしぶる callback plugin を作る まずはベースとなるdefault.pyを、プレイブックのあるディレクトリにcallback_pluginsディレクトリを作成し、その下へnyansible.pyという名前でコピーする。 [zaki@manager nyan]$ mkdir callback_plugins/ [zaki@manager nyan]$ cp /usr/lib/python2.7/site-packages/ansible/plugins/callba ck/default.py callback_plugins/nyansible.py [zaki@manager nyan]$ ll callback_plugins/ 合計 20 -rw-r--r--. 1 zaki zaki 17488 11月 4 12:36 nyansible.py [zaki@manager nyan]$ defaultをベースにした最も簡単なコピペコード default callback pluginの、表示に部分のメッセージを変更してあげればOK. ただし、日本語(多バイトコード)をそのまま書いてしまうと文字化けしてしまうので、対応が必要。 日本語対応 ERROR! Unexpected Exception, this is probably a bug: Non-ASCII character '\xce' in file /home/zaki/work/ansible/nyan/callback_plugins/nyansible.py on line 89, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details (nyansible.py, line 89) スクリプト内に日本語を使用する場合、スクリプトの1行目(普段のPythonスクリプトでshebang(#!/usr/bin/env python3とか書くアレ)が1行目にある場合は2行目)にエンコードを指定する。 UTF-8であれば # -*- coding: utf-8 -*- と書く。 [WARNING]: Failure using method (v2_runner_on_ok) in callback plugin (<ansible.plugins.callback.nyansible.CallbackModule object at 0x7fda2cacc410>): 'ascii' codec can't decode byte 0xe0 in position 0: ordinal not in range(128) また、日本語を使用する各箇所でUnicode指定を表すu" ... "という記述が必要。(okやchangedのメッセージ出力はuがないため、付け加える) self._display.display("fatal: [%s]: FAILED! => %s" ... を self._display.display(u"エラー: [%s]: FAILED! => %s" ... と書く。 変更点サンプル [zaki@manager nyan]$ diff -u /usr/lib/python2.7/site-packages/ansible/plugins/c allback/default.py callback_plugins/nyansible.py --- /usr/lib/python2.7/site-packages/ansible/plugins/callback/default.py 2019-09-13 06:12:55.000000000 +0900 +++ callback_plugins/nyansible.py 2019-11-04 12:59:50.520091324 +0900 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> # (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -48,7 +49,7 @@ CALLBACK_VERSION = 2.0 CALLBACK_TYPE = 'stdout' - CALLBACK_NAME = 'default' + CALLBACK_NAME = 'nyansible' def __init__(self): @@ -86,11 +87,11 @@ else: if delegated_vars: - self._display.display("fatal: [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'], + self._display.display(u"Σ(;Φ ω Φ): [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'], self._dump_results(result._result)), color=C.COLOR_ERROR, stderr=self.display_failed_stderr) else: - self._display.display("fatal: [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)), + self._display.display(u"Σ(;Φ ω Φ): [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)), color=C.COLOR_ERROR, stderr=self.display_failed_stderr) if ignore_errors: @@ -107,9 +108,9 @@ self._print_task_banner(result._task) if delegated_vars: - msg = "changed: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) + msg = u"ฅ/ᐠ。ᆽ。ᐟ \: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) else: - msg = "changed: [%s]" % result._host.get_name() + msg = u"ฅ/ᐠ。ᆽ。ᐟ \: [%s]" % result._host.get_name() color = C.COLOR_CHANGED else: if not self.display_ok_hosts: @@ -119,9 +120,9 @@ self._print_task_banner(result._task) if delegated_vars: - msg = "ok: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) + msg = u"ฅ(^・ω・^ฅ): [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) else: - msg = "ok: [%s]" % result._host.get_name() + msg = u"ฅ(^・ω・^ฅ): [%s]" % result._host.get_name() color = C.COLOR_OK self._handle_warnings(result._result) [zaki@manager nyan]$ 実行例 playbook.yml --- - hosts: localhost tasks: - ping: - shell: date - yum: name: ruby [zaki@manager nyan]$ ansible-playbook playbook.yml [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' PLAY [localhost] *************************************************************** TASK [Gathering Facts] ********************************************************* ฅ(^・ω・^ฅ): [localhost] TASK [ping] ******************************************************************** ฅ(^・ω・^ฅ): [localhost] TASK [shell] ******************************************************************* ฅ/ᐠ。ᆽ。ᐟ \: [localhost] TASK [yum] ********************************************************************* Σ(;Φ ω Φ): [localhost]: FAILED! => {"changed": false, "changes": {"installed": ["ruby"]}, "msg": "You need to be root to perform this command.\n", "rc": 1, "results": ["Loaded plugins: fastestmirror\n"]} PLAY RECAP ********************************************************************* localhost : ok=3 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0 [zaki@manager nyan]$ エラーが見やすいyamlプラグインを継承したにゃんしぶる defaultプラグインでなく、エラー時のメッセージをyaml形式で出力してくれるyamlプラグインをベースに作ってみる。 yamlプラグインは/usr/lib/python2.7/site-packages/ansible/plugins/ca llback/yaml.pyにソースがある。ただしこのソースを見ても、okやchanged・fatalを出力しているコードがない。 じゃあどうなっているのかというと、Ansibleのcallback pluginはCallbackModuleクラスを作るようになっていて、yamlプラグインはdefaultプラグインを継承して作られている。 つまり、基本的な処理はすべてdefaultプラグインの処理をそのまま使用し、出力処理のみyaml.dump()を使うような実装になっている。 /usr/lib/python2.7/site-packages/ansible/plugins/callback/yaml.py if abridged_result: dumped += '\n' dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False)) というわけなので、yamlプラグインを継承し、ok/changed/fatalなどを出力する処理をオーバーライドするような実装にしてやればOK yamlプラグインのクラスを継承したにゃんしぶる用CallbackModuleクラスを作成する まずは自前の処理を何も実装していない、yamlプラグインをそのまま継承しただけのクラスを作成。 こんな感じ。 名前はnyamlにしました。 callback_plugins/nyaml.py # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' callback: nyaml type: stdout short_description: nyaml-ized Ansible screen output ''' from ansible.plugins.callback.yaml import CallbackModule as CallbackModule_yaml class CallbackModule(CallbackModule_yaml): """ nyansible and nyaml /ᐠ。ꞈ。ᐟ\ """ CALLBACK_VERSION = 2.0 CALLBACK_TYPE = 'stdout' CALLBACK_NAME = 'yaml' ※ この時点でansible.cfgにstdout_callback = nyamlと書けば(ただのyamlプラグインと同じように)動く。 メッセージ出力処理を実装する defaultプラグインのソースからメッセージ出力処理の関数を拝借してくる。 対象はv2_runner_on_ok()やv2_runner_on_failed()など。 これらの関数をまずはコピペして動かしてみる。 すると、いくつか定義が足りないとwarningが出力される。 TASK [Gathering Facts] ********************************************************* [WARNING]: Failure using method (v2_runner_on_ok) in callback plugin (<ansible.plugins.callback.nyaml.CallbackModule object at 0x7fc66f796c50>): global name 'TaskInclude' is not defined TASK [ping] ******************************************************************** TASK [shell] ******************************************************************* TASK [yum] ********************************************************************* [WARNING]: Failure using method (v2_runner_on_failed) in callback plugin (<ansible.plugins.callback.nyaml.CallbackModule object at 0x7fc66f796c50>): global name 'C' is not defined TaskIncludeやCが無いと言われている。 ので、これをdefaultプラグインのソースからさらに拝借する。(基本的にこの作業の繰り返し) というわけで、全体のソースはこんな感じ。 callback_plugins/nyaml.py # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' callback: nyaml type: stdout short_description: nyaml-ized Ansible screen output ''' from ansible.plugins.callback.yaml import CallbackModule as CallbackModule_yaml from ansible import constants as C from ansible.playbook.task_include import TaskInclude from ansible.utils.color import colorize, hostcolor class CallbackModule(CallbackModule_yaml): """ nyansible and nyaml /ᐠ。ꞈ。ᐟ\ """ CALLBACK_VERSION = 2.0 CALLBACK_TYPE = 'stdout' CALLBACK_NAME = 'yaml' def v2_runner_on_failed(self, result, ignore_errors=False): delegated_vars = result._result.get('_ansible_delegated_vars', None) self._clean_results(result._result, result._task.action) if self._last_task_banner != result._task._uuid: self._print_task_banner(result._task) self._handle_exception(result._result, use_stderr=self.display_failed_stderr) self._handle_warnings(result._result) if result._task.loop and 'results' in result._result: self._process_items(result) else: if delegated_vars: self._display.display(u"Σ(;Φ ω Φ): [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'], self._dump_results(result._result)), color=C.COLOR_ERROR, stderr=self.display_failed_stderr) else: self._display.display(u"Σ(;Φ ω Φ): [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)), color=C.COLOR_ERROR, stderr=self.display_failed_stderr) if ignore_errors: self._display.display("...ignoring", color=C.COLOR_SKIP) def v2_runner_on_ok(self, result): delegated_vars = result._result.get('_ansible_delegated_vars', None) if isinstance(result._task, TaskInclude): return elif result._result.get('changed', False): if self._last_task_banner != result._task._uuid: self._print_task_banner(result._task) if delegated_vars: msg = u"ฅ/ᐠ。ᆽ。ᐟ\: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) else: msg = u"ฅ/ᐠ。ᆽ。ᐟ\: [%s]" % result._host.get_name() color = C.COLOR_CHANGED else: if not self.display_ok_hosts: return if self._last_task_banner != result._task._uuid: self._print_task_banner(result._task) if delegated_vars: msg = u"ฅ(^・ω・^ฅ): [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) else: msg = u"ฅ(^・ω・^ฅ): [%s]" % result._host.get_name() color = C.COLOR_OK self._handle_warnings(result._result) if result._task.loop and 'results' in result._result: self._process_items(result) else: self._clean_results(result._result, result._task.action) if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result: msg += " => %s" % (self._dump_results(result._result),) self._display.display(msg, color=color) nyamlプラグインでにゃんしぶる このnyamlプラグインを使ってansible-playbookを実行すると [zaki@manager nyan]$ ansible-playbook playbook.yml [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' PLAY [localhost] *************************************************************** TASK [Gathering Facts] ********************************************************* ฅ(^・ω・^ฅ): [localhost] TASK [ping] ******************************************************************** ฅ(^・ω・^ฅ): [localhost] TASK [shell] ******************************************************************* ฅ/ᐠ。ᆽ。ᐟ\: [localhost] TASK [yum] ********************************************************************* Σ(;Φ ω Φ): [localhost]: FAILED! => changed=false changes: installed: - ruby msg: |- You need to be root to perform this command. rc: 1 results: - |- Loaded plugins: fastestmirror PLAY RECAP ********************************************************************* localhost : ok=3 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0 [zaki@manager nyan]$ 参考資料 ねこのAA 猫 | ねこ - 可愛い顔文字 | 顔文字まとめサイト 2chのかわいいAA/顔文字まとめ: 10/10更新 猫
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pandasの行のマスクはbool型を明示する

言いたいことはタイトルのままだけど、ちょっとだけ整理したのでまとめておく。 発生したトラブル 毎日値が変動する数百のデータがあり、その監視を行う必要ができた。 ただし、一度値が0になったらその後は監視しなくても良かったため、pandasで次のような表を作ってgit管理することにより、監視状況の確認をできるようにした。 設定 チェック完了フラグ 2 True 1 False 0 None 例えば最新のログが上記のような形式になっていた場合、次に監視するのはチェック完了フラグがFalse(まだ条件を満たしていない)とNone(今まで監視していなかった)のした2つの行になる。 これを抜き出そうとして、if None:がif False:と同じになるからと横着したらエラーを吐いた。 コード 出たエラーと対処法もまとめて表記。 import pandas as pd f = pd.DataFrame([[2, True], [1, False], [0, None]], columns=["value", "checked"]) print(f) print("-----") # print(f[~f["checked"]]) # TypeError: bad operand type for unary ~: 'NoneType' # print(f[f["checked"]]) # ValueError: Cannot mask with non-boolean array containing NA / NaN values # print(f[f["value"]]) # KeyError: "None of [Int64Index([2, 1, 0], dtype='int64')] are in the [columns]" print(f[~f["checked"].astype(bool)]) 一番最初のコメントアウトが横着した結果。「~はNoneを含む入力には使えない」とのことだった。 二番目のコメントアウトはそれを踏まえた確認。「NA(None)を含むようなbool型ではない配列はマスクには使えない」とのこと。 なお、だからといって3つめのコメントアウトのようにbool型じゃない配列を入れたら今度は列のマスク用だと受け取るようだ。 (これを踏まえると、2番目の「マスク」って「列のマスク」である可能性がある) 正しくは、一番最後の行のようにastype関数で変換してやることだった。 それであれば下記のような結果が出力された value checked 0 2 True 1 1 False 2 0 None ----- value checked 1 1 False 2 0 None
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像のコーナー検出

はじめに OpenCVでコーナーの検出を行います。方法は主にHarris Corner Detection MethodとgoodFeaturesToTrack Detection Methodの2つがあります。 使用する画像は次の画像です。Rectangles.pngとして保存して使用します。 Harris Corner Detection Method はじめにHarris Corner Detection Methodを試します。 harris.py import cv2 import numpy as np image= cv2.imread('Rectangles.png') gray= cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) gray= np.float32(gray) harris_corners= cv2.cornerHarris(gray, 3, 3, 0.05) kernel= np.ones((7,7), np.uint8) harris_corners= cv2.dilate(harris_corners, kernel, iterations= 2) image[harris_corners > 0.025 * harris_corners.max()]= [255,127,127] cv2.imwrite('Corner_detected_rectangles.png', image) cv2.imshow('Harris Corners', image) cv2.waitKey(0) cv2.destroyAllWindows() 処理後 goodFeaturesToTrack Detection Method 次にgoodFeaturesToTrack Detection Methodを試します。 goodReatures.py import cv2 img= cv2.imread('Rectangles.png') gray= cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) corners= cv2.goodFeaturesToTrack(gray, 100, 0.01, 50) for corner in corners: x,y= corner[0] x= int(x) y= int(y) cv2.rectangle(img, (x-10,y-10),(x+10,y+10),(255,0,0),-1) cv2.imwrite('Corner_detected_rectangles.png', img) cv2.imshow("goodFeaturesToTrack Corner Detection", img) cv2.waitKey() cv2.destroyAllWindows() 処理後 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeetCode 1000000本ノック【200. Number of Islands】

目次 No. 項目 1 概要 2 問題 3 解法 4 メイキング 概要 ●発端  ・競プロ初心者、コーディング面接でズタボロにされ、  ・最低限のアルゴリズム学習とコーディング力強化を習慣化するため記事化 ●環境  ・LeetCodeブラウザ上で実装(vscodeに拡張機能にするかもシレナイ)  ・言語はpython3 ●その他ルール  ・google検索は自由に(直接的なLeetCodeの問題解説ページはNG)   (60分実装して実装出来なければ解説ページ参照)  ・コーディング面接対策のために解きたいLeetCode 60問から問題選出  ・「1000000」は任意の2進数とする 問題 200. Number of Islands Given an m x n 2D binary grid grid which represents a map of '1's (land) and '0's (water), return the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water. ⇨「0を海、1を陸に見立てたグリッド図で、島は何個あるのか?  ちなみに島ってのは上下左右を海で囲われた陸の集まりだよ」  と書いてあります。知らんけど ex Input: grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ]Output: 3 ※島は下記のように3つあるってことですね。 ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] 解法 実施時間:60分オーバー class Solution: def numIslands(self, grid: List[List[str]]) -> int: def adjoining_land_check(grid,i,j): #確認前陸(1)かそれ以外(確認後陸(2)か海(0))か判別 if grid[i][j] == "1": #確認済み(2)に変更 grid[i][j] = "2" #座標の上下左右が1なら2にしてその上も確認を繰り返す #上が存在するエリア if i-1>=0: adjoining_land_check(grid,i-1,j) #右が存在するエリア if j-1>=0: adjoining_land_check(grid,i,j-1) #下が存在するエリア if i+1<=len(grid)-1: adjoining_land_check(grid,i+1,j) #左が存在するエリア if j+1<=len(grid[0])-1: adjoining_land_check(grid,i,j+1) island_count = 0 #gridを左上〜右下まで横順で確認ループ for i,inner_grid in enumerate(grid): for j,area in enumerate(inner_grid): if grid[i][j] == "1": #引数 グリッド grid 座標 var,hor (ij) adjoining_land_check(grid,i,j) #大陸数に+1 island_count +=1 return island_count ※色々と余裕がなかったので汚いままですがご容赦を メイキング class Solution: def numIslands(self, grid: List[List[str]]) -> int: #gridを左上〜右下まで横順で確認ループ #確認前陸(1)かそれ以外(確認後陸(2)か海(0))か判別 #確認前陸(1)なら大陸数に+1 #確認前陸(1)なら確認済み(2)に変更 #上下左右が確認前陸(1)があれば確認済み(2)に変更してその上下左右もループ確認 #2or0なら次を確認しに行く とりあえずはざっと設計してみる。 肝は #上下左右の確認〜 のところだなと思ったけども、 とりあえず詳細は後回しにしてコーディングしていくことに。 class Solution: def numIslands(self, grid: List[List[str]]) -> int: island_count = 0 for i,inner_grid in enumerate(grid): for j,area in enumerate(inner_grid): if area == "1": island_count +=1 grid[i][j] = "2" #上下左右が確認前陸(1)があれば確認済み(2)に変更してその上下左右もループ確認 #2or0なら次を確認しに行く return island_count 25分程度で全体的には出来ました。 (この時点では island_count は単純に "1" の数を返します。) それでは問題の箇所に入っていきます。まずは仮コーディングしてみる。 #上は1か?   #2にする     #さらに上は1か?                  #...... #右があるか? #同じ感じ #下があるか? #同じ感じ #左があるか? #同じ感じ 際限なく確認せなあかんやろ。。。となるところです。1ヶ月前の自分では。 今は「再帰的」という言葉を知っているので余裕の予感がしましたが問題は時間。 (残り20分) class Solution: def numIslands(self, grid: List[List[str]]) -> int: island_count = 0 #gridを左上〜右下まで横順で確認ループ for i,inner_grid in enumerate(grid): for j,area in enumerate(inner_grid): #確認前陸(1)かそれ以外(確認後陸(2)か海(0))か判別 if area == "1": #大陸数に+1 island_count +=1 #確認済み(2)に変更 grid[i][j] = "2" #上下左右が確認前陸(1)があれば確認済み(2)に変更してその上下左右もループ確認 #引数 グリッド grid 座標 var,hor (ij) def adjoining_land_check(grid,i,j): #座標の上下左右が1なら2にしてその上も確認を繰り返す if grid[i-1][j] == "1": grid[i-1][j] == "2" adjoining_land_check(grid,i-1,j) if grid[i][j+1] == "1": grid[i][j+1] == "2" adjoining_land_check(grid,i,j+1) if grid[i+1][j] == "1": grid[i+1][j] == "2" adjoining_land_check(grid,i+1,j) if grid[i][j-1] == "1": grid[i][j-1] == "2" adjoining_land_check(grid,i,j-1) adjoining_land_check(grid,i,j) print(grid) #2or0なら次を確認しに行く return island_count 慣れない再帰処理とインナークラス実装で手こずりながらも完成しましたが。。。 Runtime Error RecursionError: maximum recursion depth exceeded in comparison 再帰エラー。。。だと。。。 と色々確認しているうちにタイムアップで、下記の記事を参考にさせて頂きました。 LeetCode「200. Number of Islands」解説 一番問題の箇所(エラー)は再帰の中の条件ですね。 早い話が確認範囲の際限がないため無限ループになっていたよう。 誤: if grid[i-1][j] == "1": 正: if i-1>=0: 後は、細々したところを直して完成。 今回は難易度Mediumってのもあったけど、再帰慣れないとだなぁ。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blender Python APIによるUIレイアウト、Workspaceの制御方法の調査

はじめに AddOnを作っていると、AddOn側からユーザに想定している使いやすいGUI構成を強制、提案できないか、と考えることがあると思います。BlenderのGUI要素(つまり、Areaの幅や高さの変更、Workspaceの作成など)のPython APIの制御方法についての調査結果をまとめたものです。 環境 Blender 2.91, Windows 10で検証しています。 Window, Screen, Area, Region 2.80以降Workspaceという概念が追加されています。 ユーザマニュアルではScreenの概念についてはWorkspaceに置き換わっています。 しかし、Python API側ではScreen Operatorはそのまま残っている点に注意してください。 2.80以降からBlenderを触り始めた人も多いと思いますので、Screen何それという人は2.79と2.80以降のドキュメントを見比べるとよいと思います。 Window System 2.79 Window System 2.91 Areaの操作はScreen Operatorで定義されています。 Blender Screen Operator 結論 Workspaceの個別追加は容易に行うことができます。(実質blendファイルからのappend)Python APIでの追加も簡単にできます。 標準機能でのWorkspaceの追加機能はApplication Templateと紐づいている。 WorkspaceをゼロからPythonスクリプトのみで作っていくのは難しそうです。 Areaデータの位置、サイズのプロパティはReadOnlyなので直接値を変更することができません。 Area操作用のOperatorはScreen Operatorとして定義されています。 現状のScreen Operatorで、Areaの位置、サイズ調整をすることはお勧めできません。それらのOperatorは標準機能としてマウス操作するために特化しているので、他から流用するのは、困難または不便な作りになっています。(また、Areaデータ自体が名前を持たないので、簡単に識別する方法もありません。そのこともAPIでの操作に不便さに拍車をかけています) area_split Operatorは使えなくはないです。(それでも、workspaceのAppendで済ました方がよいでしょう) ops.screen.area_move, ops.screen.area_optionはpythonからは実質使用不可。area_joinは辛うじて使用可能です。 今回調査してみて、workspaceのメリットとApplication Templateについて理解が進みました。 Blenderの環境構築、環境の配布という観点で、Application Templateを掘り下げていくのは面白そうです。 (CGの制作会社などで複数人が統一した環境で使うには、現状のAddOnシステムは不便な仕様があるので) それについては、いずれ検証、考察してみたいです。 以下、Workspaceについて掘り下げていきます。その後、Area用のOperaterについて検証した内容についてまとめています。 最後に、Blenderの実装コードを確認してドキュメント化されていないOperatorの動作仕様について調査しています。 Operatorの名前がCのソースコードにそのまま記述されているので、動作仕様を結構簡単に調べることができました。C言語が読める人は、Operatorの動作検証をブラックボックスとして試行錯誤するより、直接実装を見に行ったほうが早いかもしれません。 Screen Operatorの実装調査 Workspaceについて Workspaceはデータとしては、Screenをwrapして、シーンデータとして取り扱うことができるようにしたものと思うのがよさそうです。 Dataとしてはscreenのデータのみ参照しています。 >>> bpy.context.workspace bpy.data.workspaces['Scripting'] >>> bpy.context.workspace.screens[0] bpy.data.screens['Scripting'] シーンデータ扱いなのでMenu > File > Appendでworkspaceが現在のシーンに個別で追加ができます。 手動でWorkspaceを追加し、手動でAppendする例 手動でTest Workspaceを追加します。 D:\scenes\test_workspace.blendとして保存し、File > Newでシーンを初期化します。 その後、File > Appendで先ほど保存したファイルを選択します。 blendファイルのWorkspaceが一覧されるので、先ほど追加した「Test」workspaceを選択します。 このようにAppendでの個別追加ができるので、Materialライブラリと同様に、Workspaceのライブラリ運用が可能です。 PythonでのWorkspaceの追加 Pythonを使ってもWorkspaceのAppendが可能です。Python APIでのAppendには2つの手段があります。 WindowManagerのAppend Operatorからの追加 import bpy fpath = r"D:\scenes\test_workspace.blend" bpy.ops.wm.append(directory=fpath+"\\WorkSpace\\", filename="Test") Workspace Operatorのappend_activateによる追加 このOperatorは、実質1のappend処理と追加されたWorkspaceのactive化を一括で行うだけです。 bpy.ops.workspace.append_activate(idname="Test", filepath=fpath) おまけ WorkspaceのActive化はContextのWindowのWorkspaceにそのまま設定すればよいようです。 >>> bpy.context.window.workspace = bpy.data.workspaces['Layout'] >>> bpy.context.workspace = bpy.data.workspaces['Layout'] # ちなみにContextのworkspace直設定は不可 Traceback (most recent call last): File "<blender_console>", line 1, in <module> AttributeError: bpy_struct: attribute "workspace" from "Context" is read-only Workspaceの「+」ボタンからの追加について 「+」ボタンを押すと、「General」, 「2D Animation」, ...「Video Editing」のメニューが表示されます。 この一覧は、Aplication Templateというものの一覧です。File > Newの一覧とも同じになります。 Application Templateについて 公式のドキュメントがあります。英語しかなさそうですが、自動翻訳で十分に分かります。 非常にシンプルな仕様でした。 Application Templateについての公式ドキュメント ここも参考になりそう 簡単にいうと、下記の4点のファイルをまとめて、Blenderの初期化の仕様をパッケージにしたものです。 startup.blend (File > Defaults > Save Startup Fileで保存されるFile > Newの時に開かれるシーンファイル) userpref.blend (Preferenceの設定保存ファイル) 初期化pythonスクリプト Splash Screen画像 いずれ掘り下げたいですが、workspaceに関連するところだけ簡単にまとめます。 Application TemplateとしてAddOnを提供している例がいくつか見られます。 Blender to Unreal tools Blender Pro (2.91でインストール試したところAplication Templateとして認識はされても、スクリプト初期化でエラーになりました) Application Templateとして公開されているデータをDownloadすると、zipファイルが取得できます。そのzipファイルのまま下図のBlender Iconのメニューから読み込んでください。表示されるファイル選択ダイアログからzipそのまま選択で問題ありません。 Installの結果、ユーザのBlenderコンフィグが配置されているフォルダにzipファイルが解凍されます。 ただzipが解凍されてデータがそのまま配置されるだけのようです。 %USERPROFILE%\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\startup\bl_app_templates_user File > Newにある「General」, 「Video Editing」などの組み込みのApplication Templateは下記のフォルダに配置されています。 C:\Program Files\Blender Foundation\Blender 2.92\2.92\scripts\startup\bl_app_templates_system このフォルダを複製したり、リネームしてApplication Templateの検証ができます。 フォルダ複製などしてもBlenderは再起動の必要がありませんでした。File > New, Workspaceの+ボタンで即座に反映されています。 workspaceだけ必要な場合は、startup.blendだけフォルダに置いておけば、エラーなく「File > New」および, Workspaceの「+」 ボタンは機能しました。 他にScriptの実行などもできるので、そのあたりの動作検証はいずれしてみようと思います。 「Blender Pro」では、Blender Pro専用のAddOnの登録なども初期化スクリプトで行っているようです。 あと、templateフォルダを列挙するpython APIがあるようです。 for p in bpy.utils.app_template_paths(): print(p) Area用Screen Operator 以下は、Screen Operatorの動作検証についてまとめています。 Screen Operatorのarea関連はあまり使いようがないのですが、検証結果を捨ててしまうのはもったいないので、簡単にまとめています。 Window, Screen, Areaのデータアクセス 前提としてwindow, screen, areaのデータのアクセスについて簡単にまとめます bpy.dataからのアクセス Workspace >>> bpy.data.workspaces[0] bpy.data.workspaces['Animation'] >>> len(bpy.data.workspaces) 11 >>> for ws in bpy.data.workspaces: ... print(ws.name) ... Animation Compositing Layout Modeling Rendering Scripting Sculpting Shading Test Texture Paint UV Editing >>> bpy.data.workspaces[0].screens[0] bpy.data.screens['Animation'] Window, Screen, Area >>> len(bpy.data.window_managers['WinMan'].windows) 1 >>> bpy.data.window_managers['WinMan'].windows[0] bpy.data.window_managers['WinMan']...Window >>> window = bpy.data.window_managers['WinMan'].windows[0] >>> window.screen bpy.data.screens['Scripting'] >>> for scr in bpy.data.screens: ... print(scr) ... <bpy_struct, Screen("Animation") at 0x000002A68B81D028> <bpy_struct, Screen("Compositing") at 0x000002A68B81BEA8> <bpy_struct, Screen("Default") at 0x000002A68B81E928> <bpy_struct, Screen("Layout") at 0x000002A68B81C4E8> <bpy_struct, Screen("Modeling") at 0x000002A68B81D2A8> <bpy_struct, Screen("Rendering") at 0x000002A68B81D528> <bpy_struct, Screen("Scripting") at 0x000002A68B81D668> <bpy_struct, Screen("Sculpting") at 0x000002A68B81D7A8> <bpy_struct, Screen("Shading") at 0x000002A68B81C268> <bpy_struct, Screen("Texture Paint") at 0x000002A68B81D8E8> <bpy_struct, Screen("UV Editing") at 0x000002A68B81C3A8> Areaの列挙 >>> window = bpy.data.window_managers['WinMan'].windows[0] >>> screen = window.screen >>> for area0 in screen.areas: ... print("type:{} x:{} y:{} width:{} height:{}".format(area0.type, area0.x, area0.y, area0.width, area.height)) ... type:PROPERTIES x:1587 y:20 width:333 height:594 type:OUTLINER x:1587 y:833 width:333 height:594 type:INFO x:0 y:20 width:562 height:594 type:OUTLINER x:1587 y:615 width:333 height:594 type:VIEW_3D x:0 y:646 width:562 height:594 type:CONSOLE x:0 y:188 width:562 height:594 type:TEXT_EDITOR x:563 y:20 width:1023 height:594 Contextからのアクセス 現在アクティブになっているWorkspace, Areaなどが欲しい場合はこちらの方法で取得 >>> bpy.context.workspace bpy.data.workspaces['Scripting'] >>> bpy.context.window <bpy_struct, Window at 0x000002A692404B78> >>> bpy.context.screen bpy.data.screens['Scripting'] >>> bpy.context.area bpy.data.screens['Scripting']...Area >>> bpy.context.region bpy.data.screens['Scripting']...Region Screen Operatorの検証 以下はScriptingワークスペースの初期状態で検証しています。 コードはython Consoleでの実行を想定しています。 Areaの配置依存で動作が変わるので、下記のコードを再実行する際はその違いを適宜調整してください。 AreaのプロパティはRead Only Areaのx, y, width, heightはDocumentにもある通りReadOnlyで直接変更することはできません。 >>> C.window.screen.areas[0].x = 10 Traceback (most recent call last): File "<blender_console>", line 1, in <module> AttributeError: bpy_struct: attribute "x" from "Area" is read-only area_splitの実行 area_splitOperatorは簡単に使用することができます。 引数を指定しないと、アクティブなコンテキストのAreaが水平に分割されます。 分割したいAreaのWindow, screen, areaの辞書を引数として渡すと、そのareaを分割することができます。 おそらく、引数のcursorはAPIからは特に参照されていないと思われます。 area_splitの実行 >>> bpy.ops.screen.area_split() # 何も指定しないとアクティブなコンテキスト(この場合Python Console)のAreaが水平に分割) {'FINISHED'} >>> bpy.ops.screen.area_split(direction='VERTICAL', factor=0.5, cursor=(100, 100)) # パラメータの指定 {'FINISHED'} >>> window = bpy.data.window_managers['WinMan'].windows[0] >>> screen = window.screen >>> area = screen.areas[5].type # type: VIEW_3Dを想定 >>> region = area.regions[0] >>> ctx = {'window': window, 'screen':screen, 'area':area, 'region':region} >>> bpy.ops.screen.area_split(ctx, direction='VERTICAL', factor=0.3, cursor=(0,0)) {'FINISHED'} area_join area_splitと比べると大分面倒なAPIになります。 area_split Operatorと異なり、Areaの上下左右どの境界をJoinするかを指定する必要があります。 cursor引数の座標示すarea境界がJoinの対象となります。 無効なarea境界上の座標を指定した場合や境界でない座標を指定した場合は、OperatorがCANCELLEDを返します。 areaの座標、サイズ情報を使うことで、area_joinを正常終了することができます。 joinされたAreaの境界は即座に消えません(よく見ると若干細くなっている)。適当な境界をドラッグしてリフレッシュするとエッジが消えます。typeプロパティに別の値を入れて、戻すとscriptだけれもリフレッシュできました。 area_splitとは異なり、Python ConsoleがActiveなコンテキストの状態で、他のAreaのJoinを行うことはできました。 area_joinの実行例 >>> bpy.ops.screen.area_join(cursor=(100, 100)) {'CANCELLED'} >>> bpy.ops.screen.area_join(cursor=(C.area.x, C.area.y+100)) {'FINISHED'} >>> C.area.x, C.area.y (225, 245) area_joinについて議論しているforumがありました。 https://devtalk.blender.org/t/join-two-areas-by-python-area-join-what-arguments-blender-2-80/18165 area_move, area_option area_joinと同様に引数でのcursor位置に加えて、実際にcursorの位置がその境界上に存在する必要があります。 timerを使用して、スクリプト実行から遅延してcursor移動とarea_moveを実行している例がありました。 area_optionは、右クリックした際に表示されるpopupを表示するためのOperatorのようです。 以上のことを鑑みると、これらのAPIが、GUI上のマウス操作もOperatorとして呼び出されているようです。 Addon開発時に標準のOperatorを流用する際には、このようなGUIに深く密接したOperatorもあることを覚えておくとよいかもしれません。 Python APIドキュメントにはこれらも一緒くたに記載されているので、実際にOperatorについて詳しく調査しないと、なかなかそのようなGUIに密接したAPIかどうかを判断することはできませんが… おまけとして、このcursor位置に依存したoperatorというのが、一体どのような実装になっているのか気になったので、勉強も兼ねてソースコードを確認してみました。 Screen Operatorの実装調査 Blenderのgitについては下記のリンクを参照してください。 git clone git://git.blender.org/blender.git コンパイルするつもりがなければ、submoduleなどは無視で、上記のコマンドだけ実行(30分ぐらいかかります)し、grepなどでoperator名を適当に検索すれば、結構簡単に該当コードが見つかります。 screen_ops.c static void SCREEN_OT_area_move(wmOperatorType *ot) { /* identifiers */ ot->name = "Move Area Edges"; ot->description = "Move selected area edges"; ot->idname = "SCREEN_OT_area_move"; ot->exec = area_move_exec; ot->invoke = area_move_invoke; ot->cancel = area_move_cancel; ot->modal = area_move_modal; ot->poll = ED_operator_screen_mainwinactive; /* when mouse is over area-edge */ /* flags */ ot->flag = OPTYPE_BLOCKING | OPTYPE_INTERNAL; /* rna */ RNA_def_int(ot->srna, "x", 0, INT_MIN, INT_MAX, "X", "", INT_MIN, INT_MAX); RNA_def_int(ot->srna, "y", 0, INT_MIN, INT_MAX, "Y", "", INT_MIN, INT_MAX); RNA_def_int(ot->srna, "delta", 0, INT_MIN, INT_MAX, "Delta", "", INT_MIN, INT_MAX); } ... /* when mouse is over area-edge */ bool ED_operator_screen_mainwinactive(bContext *C) { if (CTX_wm_window(C) == NULL) { return false; } bScreen *screen = CTX_wm_screen(C); if (screen == NULL) { return false; } if (screen->active_region != NULL) { return false; } return true; } ED_operator_screen_mainwinactiveはarea_move Operatorのpoll関数(Operatorが実行可能か判定する関数)として呼び出されている関数で、コメントに書かれている通り、area-edge上にマウスカーソルがあるかどうか判定しているようです。 screen->active_regionがそのフラグのようですが、bScreen *screenがbpy.types.Screenに対応すると思います。 python上でScreenのアトリビュートを確認しても、active_regionは見つかりませんでした。 contextにも対応する項目はなさそうなので、やはりarea_move Operatorのカーソル位置依存はどうしようもない仕様と結論づけられます。 Workspace Operatorの実装調査 ついでに、workspace operatorの実装も覗いてみました。実装は下記ファイルにありました。 blender/source/blender/editors/screen/workspace_edit.c append_active Operator workspace_edit.c static int workspace_append_activate_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); char idname[MAX_ID_NAME - 2], filepath[FILE_MAX]; if (!RNA_struct_property_is_set(op->ptr, "idname") || !RNA_struct_property_is_set(op->ptr, "filepath")) { return OPERATOR_CANCELLED; } RNA_string_get(op->ptr, "idname", idname); RNA_string_get(op->ptr, "filepath", filepath); WorkSpace *appended_workspace = (WorkSpace *)WM_file_append_datablock( bmain, CTX_data_scene(C), CTX_data_view_layer(C), CTX_wm_view3d(C), filepath, ID_WS, idname); if (appended_workspace) { /* Set defaults. */ BLO_update_defaults_workspace(appended_workspace, NULL); /* Reorder to last position. */ BKE_id_reorder(&bmain->workspaces, &appended_workspace->id, NULL, true); /* Changing workspace changes context. Do delayed! */ WM_event_add_notifier(C, NC_SCREEN | ND_WORKSPACE_SET, appended_workspace); return OPERATOR_FINISHED; } return OPERATOR_CANCELLED; } static void WORKSPACE_OT_append_activate(wmOperatorType *ot) { /* identifiers */ ot->name = "Append and Activate Workspace"; ot->description = "Append a workspace and make it the active one in the current window"; ot->idname = "WORKSPACE_OT_append_activate"; /* api callbacks */ ot->exec = workspace_append_activate_exec; RNA_def_string(ot->srna, "idname", NULL, MAX_ID_NAME - 2, "Identifier", "Name of the workspace to append and activate"); RNA_def_string(ot->srna, "filepath", NULL, FILE_MAX, "Filepath", "Path to the library"); } 分かりやすい。fileのappendと読み込んだworkspaceのactive化がそのまま記述されているだけですね。 WM_file_append_datablockがblendファイルをappendする関数のようです。 BLO_update_defaults_workspaceがworkspaceをactiveにしているコードでしょうか。 add Operator add Operatorも呼び出しても特に反応がなかったので、実装を確認してみました。 popup menuがどうのこうのとあるので、Workspace追加の「+」ボタンを押したときのMenu表示のためのOperatorのようです。 workspace_edit.c static int workspace_add_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) { uiPopupMenu *pup = UI_popup_menu_begin(C, op->type->name, ICON_ADD); uiLayout *layout = UI_popup_menu_layout(pup); uiItemMenuF(layout, IFACE_("General"), ICON_NONE, workspace_add_menu, NULL); ListBase templates; BKE_appdir_app_templates(&templates); LISTBASE_FOREACH (LinkData *, link, &templates) { char *template = link->data; char display_name[FILE_MAX]; BLI_path_to_display_name(display_name, sizeof(display_name), template); /* Steals ownership of link data string. */ uiItemMenuFN(layout, display_name, ICON_NONE, workspace_add_menu, template); } BLI_freelistN(&templates); uiItemS(layout); uiItemO(layout, CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Duplicate Current"), ICON_DUPLICATE, "WORKSPACE_OT_duplicate"); UI_popup_menu_end(C, pup); return OPERATOR_INTERFACE; } static void WORKSPACE_OT_add(wmOperatorType *ot) { /* identifiers */ ot->name = "Add Workspace"; ot->description = "Add a new workspace by duplicating the current one or appending one " "from the user configuration"; ot->idname = "WORKSPACE_OT_add"; /* api callbacks */ ot->invoke = workspace_add_invoke; } その他のOperatorはGUI操作と一対一で対応するので省略。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RNNを理解し、PyTorchで実装/予測してみる

0. はじめに DeepLearningを使用した時系列/自然言語処理に関して調べたことをシェアしていく記事の第1弾です。第1回はRNN(Recurrent neural network)を扱います。 RNNの日本語記事はかなりありましたが、Pytorchを使ったコードを記載しているサンプルが少なかったりKerasを使用したRNN記事と比べても圧倒的にPyTorchが少ないと思ったので、私なりに初心者でもわかるような直感的な表現を使いつつこの記事ではまとめていこうと思います。 RNNは今や古いので誰にも使われていませんが、いろんな分野の基礎になっているので理解しておくことは重要だと考えています。 例)テキスト解析、感情分析、文章翻訳、チャットボット、動画分析・・・etc なお、本記事では自然言語ではなく「時系列処理」に関してRNNを適応した例を記載します。 1. RNNって何? RNNは「時間方向」につながりを持たせられるニューラルネットである。 では時間方向って何?ということだが、まず通常のニューラルネットは下図のように複数の入力に対して中間層で特徴量を抽出して何かしらの出力を得る仕組みである。 しかし、時間や並びの順番に重要な意味を持つデータ(これをシーケンシャルデータと呼ぶ)に関しては通常のニューラルネットでは扱えないのである。 ※1の次は2で、その次は3・・みたいな順番をニューラルネットでは学習できない そこでRNN(再帰型ニューラルネットワーク)が考えられた。 ではRNNではどうやってシーケンシャルデータを学習するか?というと、各時刻で学習した特徴量を次の時刻の入力データとしても使うのである。 下図はイメージ図だが、時刻tのタイミングの入力データをRNNへ入れる時に時刻t-1の特徴量も一緒に入力させることで、t-1までに得た特徴も加味させることができる。 ちなみに、上図のRNN_cellの中身は以下を足し合わせたものをtanhの活性化関数に通したものになっている。 ①時刻tの入力$x_t$に重み$W_x$をかけたもの ②時刻t-1の隠れ層$h_{t-1}$に重み$W_h$をかけたもの ③x、hそれぞれのバイアス($b$) ※Pytorchではxとhでバイアスが分かれているが教科書的には1個 時刻tにおけるRNNの式: h_{t} =tanh (W_{x}x_{t}+b_{x}+W_{h}h_{t-1}+b_{h}) なお、上図を見るとわかるがRNN_cellの出力が「上方向への出力と次のRNN_cellへの入力」と2分岐していることもわかるが、この2個は用途が違うだけで全く同じ出力である。 その他「RNNの逆伝番の仕組み(BPTT(backpropagation through time)」や「ステートフル」等の知識も知る必要があるかもしれないが、この記事では複雑になりすぎるので割愛する。本記事を読み終わった後に別の方の記事を確認してみていただきたいです。 2. RNNの実装と予測(Pytorch) 2-1. データの確認とRNNへの入力イメージ 今回はairpasengerを題材にPyTorchを使って実装したので以下で解説していく。 なおライブラリのインポートには触れないので、必要に応じてpipコマンドで入れるようにしてください。 #各種のインポート import torch from torch import nn,optim from torch.utils.data import DataLoader, TensorDataset, Dataset from torchvision import transforms from torchinfo import summary #torchinfoはニューラルネットの中身を見れるのでおすすめ import numpy as np import matplotlib.pyplot as plt %matplotlib inline import pandas as pd import os import random #乱数固定用の処理 seed = 10 torch.manual_seed(seed) torch.cuda.manual_seed(seed) #データをPandasで読み込み df = pd.read_csv("AirPassengers.csv") #データを3行だけ表示 df.head(3) Month #Passengers 0 1949-01 112 1 1949-02 118 2 1949-03 132 AirPassengersはこのように月別にどれくらいの乗客が乗ったか?を示すデータなことがわかる。 #Monthカラムは解析に不要なので排除 df = df.iloc[:,1].values #乗客数の1次元データとする df = df.reshape(-1,1) df = df.astype("float32") #ニューラルネットの入力データは0~1へ正規化する必要があるので乗客数を正規化する from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range = (0, 1)) df_scaled = scaler.fit_transform(df) #正規化された乗客数の推移を図示して確認する plt.plot(df) plt.xlabel("time") plt.ylabel("Number of Passenger") plt.title("international airline passenger") plt.show() すると、左下の図が出力される。 これをRNNへの学習用とテスト用に分ける必要があるが、キリがいいので右下のように分けることにする。 ここでどうやってRNNへデータを渡して予測させるか?のイメージも載せておく。 まずは上図の学習期間の中のt=0~10を抜き出したのが左下図になっている。なお今回は連続した10個のシーケンシャルデータを使って、11個目の時刻のデータを求めるようにする。(これは任意のパラメータなので何個の固まりでやってももいい) そしてt=0~9をRNNへ入力し、t=10の値を予測させるRNNのイメージ図が右下図になる。 時刻t=9のRNN_cellには、時刻t=9の入力データ以外に時刻t=0から受け継がれてきた特徴量hiddenも加わって学習が行われる。 また今回RNNに求めたいのは、時刻t=10の数字(乗客数)なので特徴量(hidden)の次元を1次元にする必要があり、時刻t=9の出力(hidden)を全結合層に入れることで次元削減をさせている。 そしてt=10の予測が終わったら、次は1個ずらして学習させる。 つまりt=1~10をRNNに入れて、t-11を予測・・のように徐々に学習させていくイメージである。 これを学習範囲の中で繰り返すのがRNNの時系列予測の基本的なやり方になる。 2-2. 入力データの加工 先程述べたように学習とテストを分割する。 train_size = int(len(df_scaled) * 0.70) #学習サイズ(100個) test_size = len(df_scaled) - train_size #全データから学習サイズを引けばテストサイズになる train = df_scaled[0:train_size,:] #全データから学習の個所を抜粋 test = df_scaled[train_size:len(df_scaled),:] #全データからテストの個所を抜粋 print("train size: {}, test size: {} ".format(len(train), len(test))) 実行結果 train size: 100, test size: 44 time_stemp = 10 #今回は10個のシーケンシャルデータを1固まりとするので10を設定 n_sample = train_size - time_stemp - 1 #学習予測サンプルはt=10~99なので89個 input_data = np.zeros((n_sample, time_stemp, 1)) #シーケンシャルデータを格納する箱を用意(入力) correct_input_data = np.zeros((n_sample, 1)) #シーケンシャルデータを格納する箱を用意(正解) print(input_data.shape) #10×1のシーケンシャルデータが89個ある print(correct_input_data.shape) 実行結果 (89, 10, 1) (89, 1) 今回の設定の場合、RNNへの入力として最低限10個のシーケンシャルデータが必要なので学習予測スタート地点が0ではないことに注意。 #空のシーケンシャルデータを入れる箱に実際のデータを入れていく for i in range(n_sample): input_data[i] = dataset_scaled[i:i+time_stemp].reshape(-1, 1) correct_input_data[i] = dataset_scaled[i+time_stemp:i+time_stemp+1] input_data = torch.tensor(input_data, dtype=torch.float) #Tensor化(入力) correct_data = torch.tensor(correct_input_data, dtype=torch.float) #Tensor化(正解) dataset = torch.utils.data.TensorDataset(input_data, correct_data) #データセット作成 train_loader = DataLoader(dataset, batch_size=4, shuffle=True) #データローダー作成 ここのバッチサイズは4としているが何でもいい。また、shuffle=Trueとしているが、Falseでもいい。 なお、ここに違和感を感じる方もいるかと思う(事実私は最初困惑した)が、実は先程説明を省いた「ステートフル」を理解するとより理解が深まる。簡単に言うと「t=0~9のRNN」と「t=1~10のRNN」自体の繋がりは学習対象である重みとバイアスの数字以外に実はなくて、互いに独立して学習させている。 つまり、hiddenは各シーケンシャルデータ毎に毎回0からスタートさせている為に学習順番をshuffleしても関係ないし、バッチサイズに関してもそこまで気にしなくていいのである。 2-3. RNNクラスの作成 class My_rnn_net(nn.Module): def __init__(self, input_size, output_size, hidden_dim, n_layers): super(My_rnn_net, self).__init__() self.input_size = input_size #入力データ(x) self.hidden_dim = hidden_dim #隠れ層データ(hidden) self.n_layers = n_layers #RNNを「上方向に」何層重ねるか?の設定 ※横方向ではない """ PyTorchのRNNユニット。batch_first=Trueでバッチサイズを最初にくるように設定 また、先程示した図ではRNN_cellが複数あったがここではRNNが1個しかない。      つまりこの「nn.RNN」は複数のRNN_cellをひとまとめにしたものである。      ※シーケンシャルデータと初期値のhiddenだけ入れてあげれば内部で勝手に計算してくれる      ※出力部は各時刻毎に出力されるが、下で述べているように最後の時刻しか使用しない """ self.rnn = nn.RNN(input_size, hidden_dim, n_layers, batch_first=True) self.fc = nn.Linear(hidden_dim, output_size) #全結合層でhiddenからの出力を1個にする def forward(self, x): #h0 = torch.zeros(self.n_layers, x.size(0), self.hidden_dim).to(device) #y_rnn, h = self.rnn(x, h0) y_rnn, h = self.rnn(x, None) #hidden部分はコメントアウトした↑2行と同じ意味になっている。 y = self.fc(y_rnn[:, -1, :]) #最後の時刻の出力だけを使用するので「-1」としている return y #RNNの設定 n_inputs = 1 n_outputs = 1 n_hidden = 64 #隠れ層(hidden)を64個に設定 n_layers = 1 net = My_rnn_net(n_inputs, n_outputs, n_hidden, n_layers) #RNNをインスタンス化 print(net) #作成したRNNの層を簡易表示 #おすすめのtorchinfoでさらに見やすく表示 batch_size = 4 summary(net, (batch_size, 10, 1)) 実行結果 My_rnn_net( (rnn): RNN(1, 64, num_layers=2, batch_first=True) (fc): Linear(in_features=64, out_features=1, bias=True) ) なお、torchinfoで示される上図のパラメータ数は上で示したRNNの数式に当てはめれば計算できる $W_{x}x_{t}+b_{x}+W_{h}h_{t-1}+b_{h}$ ⇒ 64*1 + 64 + 64*64 + 64 = 4288 ※1次元のxを64層のhiddenに分けるのでxの重み数も64となる 2-4. RNNで学習 loss_fnc = nn.MSELoss() #損失関数はMSE optimizer = optim.Adam(net.parameters(), lr=0.001) #オプティマイザはAdam loss_record = [] #lossの推移記録用 device = torch.device("cuda:0" if torch.cuda. is_available() else "cpu") #デバイス(GPU or CPU)設定 epochs = 200 #エポック数 net.to(device) #モデルをGPU(CPU)へ for i in range(epochs+1): net.train() #学習モード running_loss =0.0 #記録用loss初期化 for j, (x, t) in enumerate(train_loader): #データローダからバッチ毎に取り出す x = x.to(device) #シーケンシャルデータをバッチサイズ分だけGPUへ optimizer.zero_grad() #勾配を初期化 y = net(x) #RNNで予測 y = y.to('cpu') #予測結果をCPUに戻す loss = loss_fnc(y, t) #MSEでloss計算 loss.backward() #逆伝番 optimizer.step() #勾配を更新 running_loss += loss.item() #バッチごとのlossを足していく running_loss /= j+1 #lossを平均化 loss_record.append(running_loss) #記録用のlistにlossを加える """以下RNNの学習の経過を可視化するコード""" if i%100 == 0: #今回は100エポック毎に学習がどう進んだか?を表示させる print('Epoch:', i, 'Loss_Train:', running_loss) input_train = list(input_data[0].reshape(-1)) #まず最初にt=0~9をlist化しておく predicted_train_plot = [] #学習結果plot用のlist net.eval() #予測モード for k in range(n_sample): #学習させる点の数だけループ x = torch.tensor(input_train [-time_stemp:]) #最新の10個のデータを取り出してTensor化 x = x.reshape(1, time_stemp, 1) #予測なので当然バッチサイズは1 x = x.to(device).float() #GPUへ y = net(x) #予測 y = y.to('cpu') #結果をCPUへ戻す """ もっと綺麗なやり方あるかもですが、次のループで値をずらす為の部分。 t=0~9の予測が終了 ⇒ t=1~10で予測させたいのでt=10を追加する・・・を繰り返す """ if k <= n_sample-2: # input_train.append(input_data[k+1][9].item()) predicted_train_plot .append(y[0].item()) plt.plot(range(len(df_scaled)), df_scaled, label='Correct') plt.plot(range(time_stemp, time_stemp+len(predicted_train_plot )), predicted_test2, label='Predicted') plt.legend() plt.show() これを実行すると、最後の200Epoch目が以下のように出力されてて、うまく学習できていることがわかる。 ※途中のEpoch出力は省略 #最後にlossの推移を確認 plt.plot(range(len(loss_record)), loss_record, label='train') plt.legend() plt.xlabel("epochs") plt.ylabel("loss") plt.show() 一応lossも確認しておくと、Epoch毎に下がっていることも確認できた。 2-5. RNNで未学習部分を予測 学習に使用してない部分を予測させる⇒t=100以降となるのでそのデータを準備する つまり今回のテストではまず「t=90~99を使ってt=100を予測」から始まるのでt=90~133を先頭としたシーケンシャルデータを抜粋すればいい。 ※正解データはt=143で終わるため、比較させる為に最後の入力はt=133~142となる #学習の時と同じ感じでまずは空のデータを作る time_stemp = 10 n_sample_test = len(dataset_scaled) - train_size #テストサイズは学習で使ってない部分 test_data = np.zeros((n_sample_test, time_stemp, 1)) correct_test_data = np.zeros((n_sample_test, 1)) #t=90以降のデータを抜粋してシーケンシャルデータとして格納していく start_test = 90 for i in range(n_sample_test): test_data[i] = df_scaled[start_test+i : start_test+i+time_stemp].reshape(-1, 1) correct_test_data[i] = df_scaled[start_test+i+time_stemp : start_test+i+time_stemp+1] #以下は学習と同じ要領 input_test = list(test_data[0].reshape(-1)) predicted_test_plot = [] net.eval() for i in range(n_sample_test): x = torch.tensor(predicted_test_plot [-time_stemp:]) x = x.reshape(1, time_stemp, 1) x = x.to(device).float() y = net(x) y = y.to('cpu') input_test.append(y[0].item()) predicted_test_plot .append(y[0].item()) plt.plot(range(len(dataset_scaled)), dataset_scaled, label='Correct') plt.plot(range(start_test+time_stemp, start_test+time_stemp+len(predicted_test_plot )), predicted_test_plot , label='Predicted') plt.legend() plt.show() こんな感じで学習未使用データもきちんと予測できていそうである。 なお省略するが、元のスケールにはinverse_transformを使えば戻すことが出来る 3. おわりに かなり長くなってしまったが、実は今回やっていないことがある。 本来の時系列予測でやりたいことは予測結果をさらに入力データとしてどんどん積み重ねてもいい具合の予測結果になっていることなのだが、結論から言うと今回のRNNでは全くうまくいかない。 下図が実際に予測期間において予測結果を次の時刻の入力データとして使用していった予測結果だが、うまく予測できていないことがわかる。 その理由は簡単で、予測結果を次の入力データに入れる・・を繰り返すと徐々に予測誤差の分だけずれていってしまう為である。なので私もあまり試せていないが、RNNは実際には使えないのでは?と考えている。 そこでGRUやLSTM等の次世代の技術が出てきたと思うので、次回はLSTMをやってみることにする。 それでは今回はここまで。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2直線の交点と連立方程式の解

2直線の交点と連立方程式の解をPythonとJupiter Notebookを使って解きました ソースはこちらです。 x = sp.Symbol('x') y = sp.Symbol('y') expr1 = sp.Eq(y,70*x) expr2 = sp.Eq(y,100*x-600) #交点の座標を求める場合 ans = sp.solve([expr1,expr2]) ans #グラフを描く場合 sp.plot(70*x,100*x-600,(x,0,30)) Pythonを使った数学とデータ分析は便利ですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoの本番公開に挑戦!!

デプロイに挑戦してみます。。 Djngoでは、APサーバーを介して本番環境って流れみたいです。 まず、requirements.txtというやつとprachileの2つが必要 requirements.txtというファイルをつくり(プロジェクトの直下) 中身を↑の記述をする たぶん使ったライブラリを記述するんだと思われる。 procfileを作成 同じようにファイルを作成して中身を↓のように記述 gunicornでdjango_website.wsgiを起動するという意味らしい・・・ これからGitHubにアクセスしていく まずはstatusで確認 赤文字のやつは含まれてないという意味らしくそれを許可していく。 緑になったのでOKらしい・・・ そしてcommitするとなんかメールアドレスの許可を求められ。。今回は個人的なやつなので適当でいいらしい。 ↓みたいにコピペ そしてもう一回commit 今度はGitHubに移動して、トップページの右上にある「+」をおして「New repository」を選択して ↓のところに名前をつける何でもいいらしい。 Publicは公開 Privateは非公開です 下の緑ボタンをクリック そして次の画面のURLをコピー Paizaを使って開発していたので、Paizaにしらせる これでエラーでたら git remote add origin 自分のurl を試してみる そしてアプロード git push origin master するとGithubのusernameとpasswordを入力します。。 しかし でたよ〜エラー(´;ω;`) しかしこちらの記事に救われた!! でけた〜。。  上がってます!! なるほど〜。。こーすればPaizaで開発したやつをGithubに追加できるのか〜!! 感激〜!! 今度はHerokuへ移動 creat newappを選択 そして好きな名前。かぶる名前はNGなので赤字になりますので、番号いれたり・・ しまった。画面をとるのわすれた〜 一応進むと下の画面にいき、GitHubをクリックしてHerokuとつなげる・・つなげたら、あげたいアプリ名を入力して検索してでてきたのをコネクトしたら↓の画面 そして、灰色のボタンは自動的に更新してくれるみたいなやつなのでクリックした。今は白くなっている 手動でデプロイ 完了して右上のアイコンをクリックすると できました!!デプロ〜イ!! ちゃんと自己紹介ページにも移動できました!! しかし、この自己紹介は嘘なのはお伝えします。(笑) いや〜できるか不安でしたができた・・・PaizaでもGithubをくっしれば無料で使えるし、そのあとデプロイもできるということを学んだだけでも嬉しいですわ〜。 今回も成功したぞ〜!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NVIDIA Jetson Nano 開発者キットでソフトウェア無線(動作確認編)

はじめに セットアップ編 で、Jetson Nano でソフトウェア無線(SDR: Software Defined Radio)を利用するための環境(cuSignal + SoapySDR)構築について紹介しました。受信機には RTL-SDR BLOG V.3 を使います。今回の動作確認編では、構築した環境上で、FM 放送の受信を例に、動作確認してみます。 cuSignal に付属する Jupyter ノートブック cuSignal には、その使い方を示したいくつかの Jupyter ノートブックが付属し、ソフトウェア無線関連のノートブックもあります。 ノートブック 内容 Jetson Nano で動作 online_signal_processing_tools.ipynb cuSignal をソフトウェア無線で利用する際の基本的なサンプル sdr_integration.ipynb rtlsdr を利用した FM 放送受信サンプル 部分的(設定を変更すれば動作) sdr_wfm_demod.ipynb SoapySDR を利用した FM 放送受信サンプル 動作確認を解説 cuSignal 付属のノートブックもとても役に立ちましたが、自分自身の理解のためにノートブックを作成したので、それを以下に解説します。なお、作成したノートブックは Gist で公開 しています。 ライブラリのインポート import SoapySDR from SoapySDR import * #SOAPY_SDR_ constants import matplotlib.pyplot as plt import numpy import importlib sample_rate は RTL-SDR がデータをサンプルするサンプリングレートです。FM 放送音声のサンプリングレートである audio_fs との違いに注意。復調後にダウンサンプリングするための比率を resample_factor で定義しています。 sample_rate = int(2.4e6) audio_fs = int(48e3) resample_factor = sample_rate // audio_fs read_elements = 1024 channel = 0 fm_freq は FM 放送局の周波数です。ここでは、80.0MHz の TOKYO FM を設定しています。 fm_freq = 80.0e6 # FM Station Frequency gain = 40 sample_time = 1 # Sampling time in sec メモリ消費は大きいですが、動作が分かりやすいので、まず、受信データを sample_time 秒間蓄えてから、変調します。total_elements はそのサンプル数です。SoapySDR の readStream 関数は(私のやり方が間違っている可能性もありますが)一度に多くのサンプルを読み出すと動作が不安定だったので、複数回に分けて読み出し、num_reads がその回数。 total_elements = int(sample_rate * sample_time) num_reads = total_elements // read_elements 受信デバイス(ここでは RTL-SDR)の初期設定を行います。このページ を参考にしました。 #enumerate devices results = SoapySDR.Device.enumerate() for result in results: print(result) #create device instance #args can be user defined or from the enumeration result args = dict(driver="rtlsdr") sdr = SoapySDR.Device(args) #query device info print(sdr.listAntennas(SOAPY_SDR_RX, channel)) print(sdr.listGains(SOAPY_SDR_RX, channel)) freqs = sdr.getFrequencyRange(SOAPY_SDR_RX, channel) for freqRange in freqs: print(freqRange) #apply settings sdr.setSampleRate(SOAPY_SDR_RX, channel, sample_rate) sdr.setFrequency(SOAPY_SDR_RX, channel, fm_freq) sdr.setGain(SOAPY_SDR_RX, channel, gain) {driver=rtlsdr, label=Generic RTL2832U OEM :: 00000001, manufacturer=Realtek, product=RTL2838UHIDIR, serial=00000001, tuner=Rafael Micro R820T} ('RX',) ('TUNER',) 2.3999e+07, 1.764e+09 受信データ読み込み用のバッファ・メモリを用意します。 buff_len = total_elements cpu_buff = numpy.array([0] * buff_len, numpy.complex64) print('buff_len={}'.format(buff_len)) buff_len=2400000 受信機からデータを読み込みます。先ほど述べたように、readStream 関数で一度に多くのデータを読み出すと動作が不安定だったため、複数回に分けて読み出しています。最初の読み出しに失敗することもあったので、空読みしてから、本当の読み出しを行っています。 #setup a stream (complex floats) rxStream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32) sdr.activateStream(rxStream) #start streaming while(True): sr = sdr.readStream(rxStream, [cpu_buff], 1024) if sr.ret == 1024: break offset = 0 for i in range(num_reads): sr = sdr.readStream( rxStream, [cpu_buff[offset:offset + read_elements]], read_elements, timeoutUs=int(8e12)) if sr.ret != read_elements: print('readStream error sr.ret={}'.format(sr.ret)) break offset += read_elements #shutdown the stream sdr.deactivateStream(rxStream) #stop streaming sdr.closeStream(rxStream) パワースペクトラム密度を求める関数。sig を cusignal の別名と、scipy.signal の別名に切り替えることで、両方のライブラリでの実行に対応します。 def welch(buff, sample_rate): return sig.welch(buff, sample_rate, nfft=1024, scaling='density', return_onesided=False) FM 復調を行う関数。こちらも、cupy と numpy に対応。受信機から読み出せるデータは I-Q 表現と呼ばれる複素数形式で、np.angle 関数で位相角が求まり、np.unwrap 関数で位相角のシフトを求めます。np.diff 関数で微分します。次に、ダウンサンプリングで、受信機のサンプリングレートから、FM 放送の音声サンプリングレートへ変換。最後に正規化を行って完了です。 def demodulate(buff, resample_factor): b = buff b = np.diff(np.unwrap(np.angle(b))) b = sig.resample_poly(b, 1, resample_factor, window='flattop') b /= np.pi return b 私は、説明できる程の知見がないので、FM 変調の仕組みはこの辺でご勘弁を。もう少し詳しく知りたい方は、Interface 2021年5月号 の特集「Pythonで無線信号処理」をお勧めします。 CuPy + cuSignal まずは、CuPy と cuSignal で、FM 復調を実行します。 cupy の別名を np へ、cusignal の別名を sig に設定します。 np = importlib.import_module('cupy') print(np.__name__) sig = importlib.import_module('cusignal') print(sig.__name__) cupy cusignal GPU が利用できるメモリを割り当て、そこへ、先程、受信機から読み出したデータをコピーします。 gpu_buff = sig.get_shared_mem(buff_len, dtype=np.complex64) gpu_buff[:] = cpu_buff パワースペクトラム密度を計算。 f, Pxx_den = welch(gpu_buff, sample_rate) 計算結果をグラフ表示 plt.semilogy(np.asnumpy(np.fft.fftshift(f/1e4)), np.asnumpy(np.fft.fftshift(Pxx_den))) plt.show() なぜ中心周波数に凹みがあるのか?不明。私のコードに問題があるのかも知れません。 FM 復調を行います。CUDA による実行は、CUDA プログラムロード後、最初の呼び出し時にオーバーヘッドがあるので、2回実行して処理時間を計測。 %%time b = demodulate(gpu_buff, resample_factor) CPU times: user 76 ms, sys: 20 ms, total: 96 ms Wall time: 180 ms %%time b = demodulate(gpu_buff, resample_factor) CPU times: user 12 ms, sys: 4 ms, total: 16 ms Wall time: 157 ms 音声波形を表示。 b = np.asnumpy(b).astype(np.float32) x = [i for i in range(len(b))] plt.plot(x, b, label="test") [<matplotlib.lines.Line2D at 0x7f311a5550>] NumPy + SciPy Signal 次に、NumPy と SciPy Signal で、FM 復調を実行します。 numpy の別名を np へ、scipy.signal の別名を sig に設定します。 np = importlib.import_module('numpy') print(np.__name__) sig = importlib.import_module('scipy.signal') print(sig.__name__) numpy scipy.signal パワースペクトラム密度を計算。 f, Pxx_den = welch(cpu_buff, sample_rate) 計算結果をグラフ表示 plt.semilogy(np.fft.fftshift(f/1e4), np.fft.fftshift(Pxx_den)) plt.show() データが同じなので、CuPy + cuSignal で計算した場合と同じ結果です。 FM 復調を行います。 %%time b = demodulate(cpu_buff, resample_factor) CPU times: user 512 ms, sys: 1.61 s, total: 2.12 s Wall time: 2.13 s 音声波形を表示。 x = [i for i in range(len(b))] plt.plot(x, b, label="test") [<matplotlib.lines.Line2D at 0x7f311da5e0>] こちらも、データが同じなので、CuPy + cuSignal で復調した場合と同じ結果です。 最後に音声データをファイルに保存します。 from scipy.io import wavfile wavfile.write('demod.wav', rate=audio_fs, data=1e2*b) 最後に 前回の セットアップ編 と、今回の動作確認編で、Jetson Nano でソフトウェア無線を試す環境が整いました。しかし、まだ、FM 復調を試しただけで、ソフトウェア無線の目的である、ハードウェアを変更せずに、いろいろな通信方式に対応することには、ほぼ遠いです。Jetson Nano を使うからには、ディープラーニング推論も組み合わせて、面白い使い方を探ってみたいです。例えば、AI ノイズ除去など。私の勉強不足で、そこまで行くのは大変ですが、もし、何かできたら、続きの Qiita 記事を作成したいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NVIDIA Jetson Nano 開発者キットでソフトウェア無線(セットアップ編)

はじめに ソフトウェア無線(SDR: Software Defined Radio)をご存知でしょうか?ソフトウェア無線とは、ハードウェアを変更せずに、制御ソフトウェアにより、無線通信方式を変更できる技術と説明されています。送信側は、法令で規制されているため、気安く試すわけには行きませんが、受信側でソフトウェア無線を試すのは比較的容易です。USBドングル形状の、ソフトウェア無線用受信機は安価で販売され、ARM 64-bit 対応のデバイスドライバも提供されているので、Jetson Nano でも利用することができます。おまけに、Jetson Nano 上の GPU で、ソフトウェア無線のデジタル信号処理も高速に実行できます。 ハードウェア ソフトウェア無線用受信機とアンテナが必要です。私は、RTL-SDR BLOG V.3 とアンテナのセットを購入しました。アマゾンにて5千円程度の価格で購入できます。 この受信機はUSBドングルとしては少し大きいので、他の USB ポートと干渉します。そのため、この写真のとおり、USB エクステンダーケーブルを介して、Jetson Nano と接続しています。 ソフトウェア Python でプログラミングできるようにするため、以下のパッケージを利用しました。 RAPIDS cuSignal GPU を利用したデジタル信号処理ライブラリ Scipy Signal 互換 API SoapySDR ベンダーに依存しないソフトウェア無線サポート・ライブラリ ライブラリ内部で、RTL-SDR 用ライブラリ librtl-sdr を呼び出している ソフトウェアのインストール cuSignal, SoapySDR の順で、インストールを行います。この2つに依存関係は存在しないのですが、cuSignal は Anaconda 環境が標準なので、その環境構築が最初という意味で、この順序でのインストールに帰結します。 両方共に、配布元のドキュメントに沿ってインストールできますが、少し面倒なところもあったので、ここに記録します。 cuSignal のインストール cuSignal の GitHub リポジトリ に Jetson 用のインストール手順 が記載されているので、基本的にはこの手順に従います。 注意点は、CuPy のビルド時に、GPU の Compute Capability を指定することです。これを行わないと、いつまでたってもビルドが完了しません。 Anaconda 環境の構築 まず最初に、Anaconda 環境を構築します。ドキュメントに記載どおり、Miniforge をインストールします。Linux aarch64 (arm64) Miniforge3-Linux-aarch6 を利用しました。インストーラーを実行するだけでインストールできます。最後に conda init を実行するかどうか問われますので、この後の作業のために、そこで Yes とします。 cuSignal のインストール export CUSIGNAL_HOME=$(pwd)/cusignal git clone https://github.com/rapidsai/cusignal.git $CUSIGNAL_HOME cd $CUSIGNAL_HOME 以下の環境変数設定が非常に重要です。Jetson Nano の Compute Capability は 5.3 なので以下のように設定します。これを行わないとすべての Compute Capability に対応できるよう CuPy がビルドされるので、いつまでたってもビルドが完了しません。(私は半日放置してビルドが終わらなかったので諦め、Compute Capability を指定してのビルドへ切り替えました。) export CUPY_NVCC_GENERATE_CODE="arch=compute_53,code=sm_53" conda env create -f conda/environments/cusignal_jetson_base.yml conda activate cusignal-dev CUDA Toolkit へパスを通しておかないと、CuPy のビルドに失敗します。 PATH="$PATH":/usr/local/cuda/bin ./build.sh cd python cuSignal が正しくインストールできたかテストします。いくつかのワーニングが出ましたが、エラーは出なかったので、良しとしました。 pytest -v SoapySDR のインストール 基本的には、SoapySDR BuildGuide と Building Soapy RTL-SDR のとおり。 RTL-SDR と オーディオ関連のライブラリをインストール sudo apt install rtl-sdr librtlsdr-dev portaudio19-dev pyrtlsdr と pyaudio は conda install で見つからなかったので、pip でインストールしました。 pip install pyrtlsdr pyaudio ほとんどの RTL-SDR デバイスで libdvb ドライバをブラックリスト登録してロードをされないようにする必要があるらしいです。Please note, for most rtlsdr devices, you'll need to blacklist the libdvb driver in Linux. sudo vi /etc/modprobe.d/blacklist.conf 最後の行に、blacklist dvb_usb_rtl28xxu を追加。それを反映させるため、リブート。 sudo reboot swig をインストールしておかないと、SoapySDR の Python パッケージがビルドされないので。 conda install swig 後々、サンプルの Jupyter Notebook を実行するので。 conda install jupyter SoapySDR のインストール git clone https://github.com/pothosware/SoapySDR.git cd SoapySDR mkdir build cd build Anaconda 環境に関する理解が不足しているため、ここが苦労したところ。普通に cmake .. とやると、通常の Python パスに SoapySDR の Python パッケージがインストールされて、Anaconda 環境からは見えなくなってしまう。そのため Anaconda 環境のパスを指定。PYTHON_INSTALL_DIR と PYTHON3_INSTALL_DIR の両方に指定したが、PYTHON3_INSTALL_DIR だけで良いのかも知れない。 cmake -DPYTHON_INSTALL_DIR=`python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"` -DPYTHON3_INSTALL_DIR=`python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"` .. make -j4 sudo make install sudo ldconfig 以下のコマンドで SoapySDR がインストールできたことを確認。 SoapySDRUtil --info 以下のように出力されれば成功。 ###################################################### ## Soapy SDR -- the SDR abstraction library ## ###################################################### Lib Version: v0.8.0-gab626068 API Version: v0.8.0 ABI Version: v0.8 Install root: /usr/local Search path: /usr/local/lib/SoapySDR/modules0.8 Module found: /usr/local/lib/SoapySDR/modules0.8/librtlsdrSupport.so (0.3.2-53ee8f4) Available factories... rtlsdr Available converters... - CF32 -> [CF32, CS16, CS8, CU16, CU8] - CS16 -> [CF32, CS16, CS8, CU16, CU8] - CS32 -> [CS32] - CS8 -> [CF32, CS16, CS8, CU16, CU8] - CU16 -> [CF32, CS16, CS8] - CU8 -> [CF32, CS16, CS8] - F32 -> [F32, S16, S8, U16, U8] - S16 -> [F32, S16, S8, U16, U8] - S32 -> [S32] - S8 -> [F32, S16, S8, U16, U8] - U16 -> [F32, S16, S8] - U8 -> [F32, S16, S8] SoapyRTLSDR のインストール git clone https://github.com/pothosware/SoapyRTLSDR.git cd SoapyRTLSDR mkdir build cd build cmake .. make sudo make install 最後に RTL-SDR デバイスが認識できているか確認。Jetson Nano 起動前から RTL-SDR デバイスが USB ポートに刺さっている必要があるようです。 SoapySDRUtil --probe 以下のように表示されれば、めでたし、めでたし。 ###################################################### ## Soapy SDR -- the SDR abstraction library ## ###################################################### Probe device Found Rafael Micro R820T tuner [INFO] Opening Generic RTL2832U OEM :: 00000001... Found Rafael Micro R820T tuner ---------------------------------------------------- -- Device identification ---------------------------------------------------- driver=RTLSDR hardware=R820T index=0 origin=https://github.com/pothosware/SoapyRTLSDR ---------------------------------------------------- -- Peripheral summary ---------------------------------------------------- Channels: 1 Rx, 0 Tx Timestamps: YES Time sources: sw_ticks Other Settings: * Direct Sampling - RTL-SDR Direct Sampling Mode [key=direct_samp, default=0, type=string, options=(0, 1, 2)] * Offset Tune - RTL-SDR Offset Tuning Mode [key=offset_tune, default=false, type=bool] * I/Q Swap - RTL-SDR I/Q Swap Mode [key=iq_swap, default=false, type=bool] * Digital AGC - RTL-SDR digital AGC Mode [key=digital_agc, default=false, type=bool] * Bias Tee - RTL-SDR Blog V.3 Bias-Tee Mode [key=biastee, default=false, type=bool] ---------------------------------------------------- -- RX Channel 0 ---------------------------------------------------- Full-duplex: NO Supports AGC: YES Stream formats: CS8, CS16, CF32 Native format: CS8 [full-scale=128] Stream args: * Buffer Size - Number of bytes per buffer, multiples of 512 only. [key=bufflen, units=bytes, default=262144, type=int] * Ring buffers - Number of buffers in the ring. [key=buffers, units=buffers, default=15, type=int] * Async buffers - Number of async usb buffers (advanced). [key=asyncBuffs, units=buffers, default=0, type=int] Antennas: RX Full gain range: [0, 49.6] dB TUNER gain range: [0, 49.6] dB Full freq range: [23.999, 1764] MHz RF freq range: [24, 1764] MHz CORR freq range: [-0.001, 0.001] MHz Sample rates: [0.225001, 0.3], [0.900001, 3.2] MSps Filter bandwidths: [0, 8] MHz 最後に Jetson Nano でもソフトウェア無線を気軽に試すことができることは、先人達の努力によるもので、感謝しかありません。但し、Windows 10 や x64 Linux プラットフォームの場合ほど、Jetson Nano で試す際の情報は充実していません。私自身、このインストール手順を見出すのに何日もかかり、それが、記事にした動機です。誰かのお役に立てば幸いです。 ところで、インストール作業をもっと簡単にするため、Dockerfile の作成を考えたのですが、Anaconda を使わないで CuPy をインストールすることが難しくて断念しました。(x64 向けは サポート されているようです。) 本記事は、動作確認編 へ続きます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超解像手法/FSRCNNの実装

概要 深層学習を用いた、単一画像における超解像手法であるFSRCNNの実装したので、それのまとめの記事です。 Python + Tensorflow(Keras)で実装を行いました。 今回紹介するコードはGithubにも載せています。 1. 超解像のおさらい 超解像について簡単に説明をします。 超解像とは解像度の低い画像に対して、解像度を向上させる技術のことです。 ここでいう解像度が低いとは、画素数が少なかったり、高周波成分(輪郭などの鮮鋭な部分を表す)がないような画像のことです。 以下の図で例を示します。(図は[論文]より引用) これは、超解像の説明をする時によく使われる画像です。 (a)は原画像、(b)は画素数の少ない画像を見やすいように原画像と同じ大きさにした画像、(c)は高周波成分を含まない画像の例です。 (b)と(c)は、荒かったりぼやけていたりしていると思います。 このような状態を解像度が低い画像といいます。 そして、超解像はこのような解像度が低い画像に処理を行い、(a)のような精細な画像を出力することを目的としています。 2. 論文の超解像アルゴリズム 超解像のアルゴリズムの概要図は以下の通りです。(図は論文から引用) 上の図がSRCNN、下の図がFSRCNNのモデルの概要図となっています。 SRCNNでは、最初にBicubic法で画像を補間してからニューラルネットワークに入力しており、画像サイズが大きい状態で特徴抽出や返還を行っていました。 そのため、CNNのサイズが大きくなったり、計算が非効率になっているという問題がありました。 そこで、FSRCNNではSRCNNの大まかなモデルの構造はそのままに、 Feature extraction Shrinking Mapping Expanding Deconvolution のいくつかの層に分けることで、各層のCNNのサイズを小さくすることに成功しました。 また、LR画像から直接ピクセルの特徴抽出・変換を行うことで、で効率良い演算ができるようにしているのも特徴です。 3. 実装したアルゴリズム 今回、実装したFSRCNNの構造のコマンドラインは以下の通りです。 _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, None, None, 1)] 0 _________________________________________________________________ conv2d (Conv2D) (None, None, None, 56) 1512 _________________________________________________________________ conv2d_1 (Conv2D) (None, None, None, 16) 928 _________________________________________________________________ conv2d_2 (Conv2D) (None, None, None, 16) 2336 _________________________________________________________________ conv2d_3 (Conv2D) (None, None, None, 16) 2336 _________________________________________________________________ conv2d_4 (Conv2D) (None, None, None, 16) 2336 _________________________________________________________________ conv2d_5 (Conv2D) (None, None, None, 16) 2336 _________________________________________________________________ conv2d_6 (Conv2D) (None, None, None, 56) 1008 _________________________________________________________________ conv2d_transpose (Conv2DTran (None, None, None, 1) 4537 ================================================================= Total params: 17,329 Trainable params: 17,329 Non-trainable params: 0 _________________________________________________________________ なお、mappingの回数は4回でモデルを制作しています。 活性化関数はPReLUを使用しました。 KerasのClass PReLUの状態だと、Input_shape = (None, None)の任意の自然数に対応していないので、 今回は独自のClass PReLUを作成しました。 コードは以下の通りです。 model.py class MyPReLU(Layer): #PReLUを独自に作成 def __init__(self, alpha_initializer = 'zeros', alpha_regularizer = None, alpha_constraint = None, shared_axes = None, **kwargs): super(MyPReLU, self).__init__(**kwargs) self.alpha_initializer = initializers.get('zeros') self.alpha_regularizer = regularizers.get(None) self.alpha_constraint = constraints.get(None) def build(self, input_shape): param_shape = tuple(1 for i in range(len(input_shape) - 1)) + input_shape[-1:] self.alpha = self.add_weight(shape = param_shape, name = 'alpha', initializer = self.alpha_initializer, regularizer = self.alpha_regularizer, constraint = self.alpha_constraint) self.built = True def call(self, inputs, mask=None): pos = K.relu(inputs) neg = -self.alpha * K.relu(-inputs) return pos + neg def compute_output_shape(self, input_shape): return input_shape def get_config(self): config = { 'alpha_initializer': initializers.serialize(self.alpha_initializer), 'alpha_regularizer': regularizers.serialize(self.alpha_regularizer), 'alpha_constraint': constraints.serialize(self.alpha_constraint), } base_config = super(MyPReLU, self).get_config() return dict(list(base_config.items()) + list(config.items())) stack over flowの質問を参考に、Kerasのドキュメントを変更して作成しました。 4. 使用したデータセット 今回は、データセットにDIV2K datasetを使用しました。 このデータセットは、単一画像のデータセットで、学習用が800種、検証用とテスト用が100種類ずつのデータセットです。 今回は、学習用データと検証用データを使用しました。 パスの構造はこんな感じです。 train_sharp - 0001.png - 0002.png - ... - 0800.png val_sharp - 0801.png - 0802.png - ... - 0900.png このデータをBicubicで縮小したりしてデータセットを生成しました。 5. 画像評価指標PSNR 今回は、画像評価指標としてPSNRを使用しました。 PSNR とは Peak Signal-to-Noise Ratio(ピーク信号対雑音比) の略で、単位はデジベル (dB) で表せます。 PSNR は信号の理論ピーク値と誤差の2乗平均を用いて評価しており、8bit画像の場合、255(最大濃淡値)を誤差の標準偏差で割った値です。 今回は、8bit画像を使用しましたが、計算量を減らすため、全画素値を255で割って使用しました。 そのため、最小濃淡値が0で最大濃淡値が1です。 dB値が高いほど拡大した画像が元画像に近いことを表します。 PSNRの式は以下のとおりです。 PSNR = 10\log_{10} \frac{1^2 * w * h}{\sum_{x=0}^{w-1}\sum_{y=0}^{h-1}(p_1(x,y) - p_2(x,y))^2 } なお、$w$は画像の幅、$h$は画像の高さを表しており、$p_1$は元画像、$p_2$はPSNRを計測する画像を示しています。 6. コードの使用方法 このコード使用方法は、自分が執筆した別の実装記事とほとんど同じです。 ① 学習データ生成 まず、Githubからコードを一式ダウンロードして、カレントディレクトリにします。 Windowsのコマンドでいうとこんな感じ。 C:~/keras_FSRCNN> 次に、main.pyから生成するデータセットのサイズ・大きさ・切り取る枚数、ファイルのパスなどを指定します。 main.pyの15~26行目です。 使うPCのメモリ数などに応じで、画像サイズや学習データ数の調整が必要です。 main.py train_height = 120 #HRのサイズ train_width = 120 test_height = 720 #HRのサイズ test_width = 1280 train_dataset_num = 10000 #生成する学習データの数 test_dataset_num = 10 #生成するテストデータの数 train_cut_num = 10 #一組の動画から生成するデータの数 test_cut_num = 1 train_path = "../../dataset/DIV2K_train_HR" #画像が入っているパス test_path = "../../dataset/DIV2K_valid_HR" 指定したら、コマンドでデータセットの生成をします。 C:~/keras_FSRCNN>python main.py --mode train_datacreate これで、train_data_list.npzというファイルのデータセットが生成されます。 ついでにテストデータも同じようにコマンドで生成します。コマンドはこれです。 C:~/keras_FSRCNN>python main.py --mode test_datacreate ② 学習 次に学習を行います。 設定するパラメータの箇所は、epoch数と学習率とかですかね... まずは、main.pyの28~31行目 main.py mag = 4 #拡大倍率 BATSH_SIZE = 32 EPOCHS = 1000 後は、学習のパラメータをあれこれ好きな値に設定します。81~94行目です。 なお、今回のFSRCNNではこちらが設定するパラメータがいくつかあります。 model.pyには書いているのですが、こちらにも記載しておきます。 まずは、FSRCNNのパラメータはこのようになっています。 model.py def FSRCNN(d, s, m, mag): """ d : The LR feature dimension s : The number of shrinking filters m : The mapping depth mag : Magnification """ main.pyのパラメータ設定はこのようになっています。 main.py train_model = model.FSRCNN(56, 16, 4, mag) optimizers = tf.keras.optimizers.Adam(learning_rate=1e-4) train_model.compile(loss = "mean_squared_error", optimizer = optimizers, metrics = [psnr]) train_model.fit(train_x, train_y, epochs = EPOCHS, verbose = 2, batch_size = BATSH_SIZE) train_model.save("FSRCNN_model.h5") optimizerはAdam、損失関数は平均二乗誤差を使用しています。 学習はデータ生成と同じようにコマンドで行います。 C:~/keras_FSRCNN>python main.py --mode train_model これで、学習が終わるとモデルが出力されます。 ③ 評価 最後にモデルを使用してテストデータで評価を行います。 これも同様にコマンドで行いますが、事前に①でテストデータも生成しておいてください。 C:~/keras_FSRCNN>python main.py --mode evaluate このコマンドで、画像を出力してくれます。 7. 結果 出力した画像はこのようになりました。 なお、今回は輝度値のみで学習を行っているため、カラー画像には対応していません。 対応させる場合は、modelのInputのchannel数を変えたり、データセット生成のchannel数を変える必要があります。 元画像 低解像度画像(4倍縮小) 生成画像 PSNR:25.15 分かりにくいので、低解像度画像を拡大にして生成画像と同じサイズにしたものも載せておきます。 低解像度画像(生成画像と同じサイズに拡大) 低解像度画像に比べると生成画像の粗さが取れていることがわかります。 最後に元画像・低解像度画像・生成画像の一部を並べて表示してみます。 並べて比べてみると、補間よりは高解像度化がされているのが分かります・ 流石に4倍だとなかなかうまくいかないのが現実です。 ついでに、大失敗した例も載せておきます。 低解像度生成の状態から既に線が分離しており、上手く高解像度化ができていません。 このことからも分かるように、超解像に適した画像とそうでない画像があります。 実際の動画像に処理をかける場合は、動画像をフレームに分解して、1枚ずつ処理を行う必要があります。 OpenCVで動画像をフレームごとに取得するとOKです。 8. コードの全容 前述の通り、Githubに載せています。 pythonのファイルは主に3つあります。 各ファイルの役割は以下の通りです。 data_create.py : データ生成に関するコード。 model.py : 超解像のアルゴリズムに関するコード。 main.py : 主に使用するコード。 9. まとめ 今回は、最近読んだ論文のFSRCNNを元に実装してみました。 FSRCNNは、拡大処理もニューラルネットワークの中に入っているのが特徴です。 超解像の失敗例も一緒に載せれたのでよかったです。 記事が長くなってしまいましたが、最後まで読んでくださりありがとうございました。 参考文献 ・Accelerating the Super-Resolution Convolutional Neural Network  今回実装の参考にした論文。 ・画素数の壁を打ち破る 複数画像からの超解像技術  超解像の説明のために使用。 ・DIV2K dataset  今回使用したデータセット。 ・Keras advanced_activations.py  KerasのPReLUが記載されている元コード。今回はこれをコピーして変更した。 ・stack overflowの質問  PReLUに関する質問。PReLUを採用しているモデルの入力次元数が指定されていない場合の対処法を記している。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoでカスタムユーザ認証

独自定義のユーザ認証を行いたい場合にみるドキュメント。 自分の中の腹落ち整理もかねて書き残す。 参考 Django の認証方法のカスタマイズ | Django ドキュメント | Django 目次 環境情報 前提 AbstractUser vs AbstractBaseUser 必要なもの 実装例 終わりに 環境情報 os, python, django のバージョン $ cat /etc/os-release PRETTY_NAME="Debian GNU/Linux 10 (buster)" $ python --version Python 3.8.5 $ python -m django --version 3.1.7 前提 AbstractUser vs AbstractBaseUser class use case AbstractUser Djangoがデフォルトで用意しているユーザ情報 + いくつかの追加属性で十分な場合に利用する AbstractBaseUser Djangoがデフォルトで用意しているユーザ情報では不十分な場合に利用する 不十分な場合って? username や email 以外の認証情報を使用したいとか、認証の識別子が重複しているとか、そういった理由がある場合はデフォルトでは不十分になるんだと思われる。 AbstractBaseUser を使うためには合わせて BaseUserManager も実装する必要がある。 Django の認証方法のカスタマイズ | Django ドキュメント | Django ここでいう username はユーザ名というよりかはユーザの識別子としての意味合いだと思われる。 必要なもの 今回はシステムの都合で独自に定義したIDを使って認証を行う方法を選択する。 必要なものは以下の二つ。 AbstractBaseUser を継承した会員情報 - Member クラスとして実装してみる BaseUserManager を継承した Member クラスで認証/認可を行うためのマネージャクラス - MemberManager として実装してみる その他 url や view の設定など 実装例 django-admin startproject sampleapp で作成したプロジェクトを例に書くことにする。 まずは一旦画面を出すため url や view の設定から行うことにする。 自動生成されないファイルやディレクトリは適宜作成する。 ログイン確認用のアプリケーションを作成して $ python manage.py startapp accounts urlなどの設定を追加して sampleapp/settings.py INSTALLED_APPS = [ + 'accounts.apps.AccountsConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] sampleapp/urls.py -from django.urls import path +from django.urls import path, include urlpatterns = [ + path('accounts/', include('accounts.urls')), path('admin/', admin.site.urls), ] accounts/urls.py from django.urls import path from . import views app_name = 'accounts' urlpatterns = [ path('', views.signin, name='login'), ] ログイン画面の 処理 && テンプレート と accounts/views.py from django.shortcuts import render def signin(request): list = {} return render( request, 'accounts/login.html', {'list': list}, ) accounts/templates/accounts/login.html <h1>Login</h1> {% if error_message %} <p><strong style="color: red;">{{ error_message }}</strong></p> {% endif %} <form action="{% url 'accounts:login' %}" method="post"> {% csrf_token %} <label for="member_id">member id</label> <input type="text" name="member_id" id="member_id" value=""> <br> <label for="password">password</label> <input type="password" name="password" id="password" value=""> <br> <input type="submit" value="Login"> </form> ログインに成功したときのテンプレートを追加する accounts/templates/accounts/login_success.html <h1>Login Success</h1> <p>Congratulations! You have successfully logged in</p> これで仮のログイン画面が表示できるはず。 python manage.py runserver して django を起動して、ブラウザから https://localhost:8000/accounts/ などにアクセスしてログイン画面を確認する。 ここまでで意味不明な場合は 公式のチュートリアル あたりをなぞって貰えばわかるはず。 次にモデルを作ってログイン処理を追加する。 今回はメンバーIDとパスワードを認証の要素に指定して使うことにする。 まずは会員を表す Member クラスを AbstractBaseUser を継承させて定義する。 accounts/models.py from django.contrib.auth.base_user import AbstractBaseUser class Member(AbstractBaseUser): member_id = models.CharField(primary_key=True, max_length=8, null=False, unique=True) password = models.CharField(max_length=1024, null=True) last_login = models.DateTimeField(null=True) USERNAME_FIELD = 'member_id' objects = None objects には BaseUserManager を継承した MemberManager を設定するので一旦 None を代入しておく。 次に MemberManager を追記して objects に MemberManager のインスタンスを代入する。 accounts/models.py -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager +class MemberManager(BaseUserManager): + pass class Member(AbstractBaseUser): member_id = models.CharField(primary_key=True, max_length=8, null=False, unique=True) password = models.CharField(max_length=1024, null=True) last_login = models.DateTimeField(null=True) USERNAME_FIELD = 'member_id' - objects = None + objects = MemberManager() 本来は MemberManager に create_user, create_superuser を定義してユーザを作成するんだろうけど、一旦手動でユーザを作成するので pass にしておく。 ログイン認証用のカスタムユーザモデルを作成したので、認証対象になるよう設定を追加する。 sampleapp/settings.py +# CustomUser +AUTH_USER_MODEL = 'accounts.Member' views のロジックに認証処理を追加する。 GETの場合はそのまま返却して、POSTの場合は認証処理を実行する。 認証に成功した場合はログイン成功ページに遷移させて、認証に失敗した場合はエラーメッセージとともにログイン画面を再表示する。 accounts/views.py from django.shortcuts import render +from django.contrib.auth import authenticate, login, logout def signin(request): - list = {} - return render( - request, - 'accounts/login.html', - {'list': list}, - ) + if request.method == "GET": + return render(request, 'accounts/login.html') + user = None + if request.method == "POST": + member_id = request.POST['member_id'] + password = request.POST['password'] + user = authenticate(request, username=member_id, password=password) + + if user is not None: + login(request, user) + return render(request, 'accounts/login_success.html') + else: + return render(request, 'accounts/login.html', {'error_message': 'login failed'}) 次にユーザ認証をするためにユーザデータを作成する。 $ python manage.py makemigrations $ python manage.py migrate $ python manage.py shell >>> from accounts.models import Member >>> member = Member(member_id=1) >>> member.set_password("1q2w3e") >>> member.save() 先ほどのログイン画面から member_id:1, password:1q2w3e でログインしてみる Congratulations! You have successfully logged in が表示されれば成功。 終わりに 当然もっとDjangoに寄り添って作れば綺麗で簡単にできると思うんだけど、公式ドキュメントを見ながらわかる範囲で寄り道する旅路も悪くはないはず。 非効率の先に高効率が成り立つ場合もきっとある。 おまけに 遭遇したエラーについて Message Manager' object has no attribute 'get_by_natural_key' 原因 モデルクラス内のマネージャを代入しておく objects 変数が空なのでエラーになっている。 対応 モデルの objects 変数に独自で定義したマネージャを代入しておく。 class Employee(AbstractBaseUser): shain_id = models.CharField('社員ID', primary_key=True, max_length=8, null=False, unique=True) # ... last_login = models.DateTimeField('最終ログイン日時', null=True) USERNAME_FIELD = 'shain_id' + objects = EmployeeManager()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djabgoで静的ファイルを使う。。

プログラミング初心者である私の挑戦・・・ 今回は、前回作ったやつを本番環境でも表示できるようにしよう・・って事なのかなぁ?? 前回のやつはこちら 多分、本番環境であげるさいセキュリティー面でsettingの所の DEBUGをfalseにしないといけないみたいです。 しかしそれをすると、画像、CSSが適用されないということがおきる こんな感じで これを解決するにはAWSのS3を使う方法があります。ど〜やら、殆どはこちらになるということ・・・ 私もやったことあるんですが、、アマゾン嫌いなためイライラしながらやってました(笑) 就職したら、おそらくきりたくても切れない縁になるんだろうと思いながらも・・・・ あと、これは金もかかる。。。 そこで今回は、whitenoiseという別のやつをつかってやってみよう挑戦!! 果たして・・・できるか・・・・ まずは、「staticfiles」というディレクトリを作成 そしてwhitenoiseをインストール pip install whitenoise をターミナルで実行 そして全体設定の最後に以下を追記setting.py STATIC_URL = '/assets/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' そして、MIDDLEWAREの所にwhitenoiseを追加 52行目 これで、whitenoiseの設定は完了らしい・・・ そして今度は「static」のファイルを集めないといけないらしくそのコマンドが python manage.py collectstatic 上書きしますか?と聞かれるのでyesと答える ↑ そしてサーバーをたちあげると お〜!!! 画像ある!!CSSも適用できてる!! しかしそもそも、何故本番環境では画像が適用できなかったりするのか??? それはど〜やら、DjangoやRailsなどで作ったアプリを本番サーバーですると、キャパオーバーになる。 開発環境であれば自分一人なのでOK。。 そこで、静的ファイルは外部で担当してもうという役割分担的な感じってことかなぁ?? と自分は解釈しました。 最後にsetting.pyの最後につけたやつのメモ STATIC_URL Amazon S3など外部サーバーを使う時はそのURL djangoの内部から配信する時は自由に決めれる STATIC_ROUT collectstaticで集めるファイルを指定している 一応、今回もクリアできたぞぉ〜!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[LINEbot] Pythonを使い知識0から4日で既読管理ボットを作った

こんにちは、Rayです。 今回はPythonを使ってLINE botに関する知識0から4日で既読管理ボットを開発したことについてまとめたいと思います。Pythonの知識は0ではありません。 このボットの特徴はグループラインに既読を付けずに重要な知らせのみを受け取ることができます。 プログラムすべては取り扱わず一部のみ解説します。プログラム全文はこちら LINE botは以下からどうぞ! それではいきましょう! 目次 はじめに 動機 開発環境と使用したもの 既読管理ボットの機能 それぞれの機能のプログラムを作成 最後に はじめに 私はPythonを始めて2か月経ってません。Pythonの基礎とWeb系の本2冊読み終え、Djangoと深層学習を勉強しようとした矢先でした。 (深層学習はすでに手を出してましたが笑。これに関しても後日記事を書きたい) YouTubeで偶然見たのです!「PythonでLINEボットの作り方」というサムネを!サムネ見た瞬間もうウキウキで目移りしちゃって作りました。 [東大卒エンジニア] YTS高橋 様の動画でLINE botの作り方を勉強しましたが、作ったものは完全オリジナルです。 動機 上記で言ったように面白そうと思ったのもありますがもう一つ理由がありまして、それは先日学校の授業で知らない人たちとチームを組まされた時のことです。 私が率先してメンバーのやることを決めたり進捗の確認をしているのに誰も返信してくれないのです!!どこまでいってるのかも分からないし、本当にやっているのかも分からない。それで私の怒りはMAX。 でも私ももう大人、怒りを表して接しても今後のチーム活動には支障がでてしまうと考え怒りは抑え込みました。(でも送信直前まではいった笑) そんなこんなで何とか授業の発表は成功。 でも今後のチームコミュニケーションはもっと円滑にしたい!そんな思いで作りました。 環境と使ったもの ・Python3 ・Flask ・LINE Developers ・ngrok LINE Developersとngrokの登録、使い方に関しては取り上げません。興味があれば他の記事や、私が参考にした[東大卒エンジニア]YTS高橋 様の動画を参照してください。非常に分かりやすく取り扱っています。 既読管理ボットの機能 基本はコマンドを打って動かします。コマンドは以下5つ、 ・よろしく:直前のチャット内容を取得し、確認ボタンを返す。 ・点呼:確認ボタンを押した人と残り人数を表示。 ・登録:登録ボタンを返す。登録した者は今後、「よろしく」で取得されるチャット内容を個別ラインに送信する ・解除:登録を解除する ・撤退:グループ退出 これらの機能は友達登録をしてないと使えません。 そしてこの登録こそが、グループラインに既読を付けずに重要な内容のみ見ることができる機能です。 なぜこんなものをって?グループラインだと既読を付けたがらない人がいます。その人達のせいでチーム活動が進まないのです。でも既読を付けずに確認できるならすぐ確認してくれるはず!ということでこの機能を付けました。 それぞれの機能のプログラムを作成 前置き これらの機能はほぼ辞書とリスト(配列)を駆使して作っています。多少見苦しいところがあるかもしれませんが、しっかりと動作はするので自分はとりあえず満足しました笑 これらのプログラムは3つのイベントから構成されます。JoinEvent,MessageEventとPostBackEventです。 app.py @handler.add(JoinEvent) def join_event(event): pass @handler.add(MessageEvent, message=TextMessage) def handle_message(event): pass @handler.add(PostbackEvent) def handle_postback(event): pass JoinEventはグループ入出時にプログラムを実行します。 MessageEventはユーザーが何かチャットを送るとプログラムを実行します。 PostBackEventはこのアクションが関連付けられたコントロールがタップされると、dataプロパティに指定された文字列を含むPostBackEventがwebhookを介して返されます。 つまりMessageEventで返されるボタンをタップしたらプログラムを実行します。 これからはこの3つのpassををいじっていきます。 入出時, JoinEvent app.py #group_boxはJoinEventの外に書いて、グルーバル変数にしてください。 group_box = {} #以下全てJoinEvent内 groupId = event.source.group_id group_summary = line_bot_api.get_group_summary(groupId) #もしグループIdがgroup_boxにないなら if not groupId in group_box: group_box[groupId] = {} group_box[groupId]['users_profile'] = {} group_box[groupId]['user_check'] = {} #よろしくの確認を押した人の辞書 if not groupId in group_box[groupId]: #グループの名前を取得 group_box[groupId]['groupName'] = group_summary.group_name #入出時にメッセージを返す line_bot_api.reply_message( event.reply_token, TextSendMessage( text=f"招待ありがとう!\n{group_box[groupId]['groupName']}への参加を確認しました。" ) ) ここで重要なのは、グループ入出すると同時にグループIdを取得してこのグループ専用の辞書を作成することです。これを図に表すとこんな感じです。 これがグループの数だけgroupIdが増えます。もしこれをしないと、グループで取得した情報がグループ同士で境界線を持つことなく共有されてしまいます。今後はこの図の末端の辞書を使っていきます。 「よろしく」 app.py #直前のチャットを格納します if not userId in text_box: text_box[userId] = [] if text_message != "よろしく": text_box[userId].append(text_message) if len(text_box[userId]) > 1: text_box[userId].pop(0) if text_message == "よろしく": detail_text = text_box[userId][0] buttons_template = ButtonsTemplate( title='確認したらボタンを押してね', text=detail_text, actions=[ PostbackAction(label="確認しました", data="check-txt") ] ) # チェックリストを初期化する group_box[groupId]['user_check']['userIds'] = [] group_box[groupId]['user_check']['user_name'] = [] template_message = TemplateSendMessage(alt_text="スマートフォンからボタンを押してね", template=buttons_template) line_bot_api.reply_message(event.reply_token, template_message) if group_box[groupId]['users_profile']: for user in group_box[groupId]['users_profile']: line_bot_api.push_message( user, TextSendMessage(text=f"{group_name}から{user_name}より\n\n{detail_text}") ) 「よろしく」と打たれたら、直前のチャット内容を含んだボタンを返します。それと同時にuser_check内にuserIdsとuser_nameの空のリストを作ります。その後、確認ボタンをユーザが押すと、そのユーザのuserIdと名前を格納します。これはコマンド「点呼」で使います。 そしてfor文で登録しているユーザに個別で同じ内容のものを返します。これがグループラインに既読を付けずに重要なチャットのみを見る機能です。 「点呼」 app.py number = line_bot_api.get_group_members_count(groupId) rest_number = number - len(group_box[groupId]['user_check']['user_name']) name_list = ', '.join(group_box[groupId]['user_check']['user_name']) if name_list == '': line_bot_api.reply_message( event.reply_token, TextSendMessage(text="誰の連絡も確認できません。") ) elif rest_number == 0: line_bot_api.reply_message( event.reply_token, TextSendMessage(text="全員の確認が取れました。ご協力ありがとうございました。") ) else: line_bot_api.reply_message( event.reply_token, TextSendMessage(text=f"{name_list}の連絡を確認。残り{rest_number}人です。") ) 「よろしく」と同様「点呼」とメッセージが来たら上の処理を行います。 難しくはありません。確認ボタンを誰も押してなければ最初の処理、残りの人数が0になれば2番目の処理、最後は残りの人数を返す処理です。 「登録」 app.py #MessageEevnt内 buttons_template = ButtonsTemplate( title='個別チャットへの連絡', text='登録ボタンを押してね', actions=[ PostbackAction(label="登録", data="register-chat") ] ) template_message = TemplateSendMessage(alt_text="スマートフォンからボタンを押してね。", template=buttons_template) line_bot_api.reply_message(event.reply_token, template_message) #PostBackEvent内 if not userId in group_box[groupId]['users_profile']: group_box[groupId]['users_profile'][userId] = user_profile.display_name line_bot_api.reply_message( event.reply_token, TextSendMessage(text=f"{group_box[groupId]['users_profile'][userId]}の登録を確認しました。") ) 「登録」とメッセージが来たら処理を行います。 MessageEvent内の処理は、登録を促すPostBackActionを返します。 PostBackEvent内はボタンが押されたとき、users_profileにuserIdとその名前を格納します。 解除は登録した者を消すだけ、退出処理は簡単なので省略します。 最後に 少し長くなってしまいましたね。わかりずらかったとこもあると思います。なのでプログラム全文はGitHubに挙げているのでそちらを見てください。 ここまでお付き合いくださってありがとうございました。まだまだ未熟者ですが、もっときれいで高度なプログラムを書けるように今後とも精進していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[LINEbot] Pythonで知識0から4日で既読管理ボットを作った

こんにちは、Rayです。 今回はPythonを使ってLINE botに関する知識0から4日で既読管理ボットを開発したことについてまとめたいと思います。Pythonの知識は0ではありません。 このボットの特徴はグループラインに既読を付けずに重要な知らせのみを受け取ることができます。 プログラムすべては取り扱わず一部のみ解説します。プログラム全文はこちら LINE botは以下からどうぞ! それではいきましょう! 目次 はじめに 動機 開発環境と使用したもの 既読管理ボットの機能 それぞれの機能のプログラムを作成 最後に はじめに 私はPythonを始めて2か月経ってません。Pythonの基礎とWeb系の本2冊読み終え、Djangoと深層学習を勉強しようとした矢先でした。 (深層学習はすでに手を出してましたが笑。これに関しても後日記事を書きたい) YouTubeで偶然見たのです!「PythonでLINEボットの作り方」というサムネを!サムネ見た瞬間もうウキウキで目移りしちゃって作りました。 [東大卒エンジニア] YTS高橋 様の動画でLINE botの作り方を勉強しましたが、作ったものは完全オリジナルです。 動機 上記で言ったように面白そうと思ったのもありますがもう一つ理由がありまして、それは先日学校の授業で知らない人たちとチームを組まされた時のことです。 私が率先してメンバーのやることを決めたり進捗の確認をしているのに誰も返信してくれないのです!!どこまでいってるのかも分からないし、本当にやっているのかも分からない。それで私の怒りはMAX。 でも私ももう大人、怒りを表して接しても今後のチーム活動には支障がでてしまうと考え怒りは抑え込みました。(でも送信直前まではいった笑) そんなこんなで何とか授業の発表は成功。 でも今後のチームコミュニケーションはもっと円滑にしたい!そんな思いで作りました。 環境と使ったもの ・Python 3.8.5 ・Flask ・LINE Developers ・ngrok LINE Developersとngrokの登録、使い方に関しては取り上げません。興味があれば他の記事や、私が参考にした[東大卒エンジニア]YTS高橋 様の動画を参照してください。非常に分かりやすく取り扱っています。 既読管理ボットの機能 基本はコマンドを打って動かします。コマンドは以下5つ、 ・よろしく:直前のチャット内容を取得し、確認ボタンを返す。 ・点呼:確認ボタンを押した人と残り人数を表示。 ・登録:登録ボタンを返す。登録した者は今後、「よろしく」で取得されるチャット内容を個別ラインに送信する ・解除:登録を解除する ・撤退:グループ退出 これらの機能は友達登録をしてないと使えません。 そしてこの登録こそが、グループラインに既読を付けずに重要な内容のみ見ることができる機能です。 なぜこんなものをって?グループラインだと既読を付けたがらない人がいます。その人達のせいでチーム活動が進まないのです。でも既読を付けずに確認できるならすぐ確認してくれるはず!ということでこの機能を付けました。 それぞれの機能のプログラムを作成 前置き これらの機能はほぼ辞書とリスト(配列)を駆使して作っています。多少見苦しいところがあるかもしれませんが、しっかりと動作はするので自分はとりあえず満足しました笑 これらのプログラムは3つのイベントから構成されます。JoinEvent,MessageEventとPostBackEventです。 app.py @handler.add(JoinEvent) def join_event(event): pass @handler.add(MessageEvent, message=TextMessage) def handle_message(event): pass @handler.add(PostbackEvent) def handle_postback(event): pass JoinEventはグループ入出時にプログラムを実行します。 MessageEventはユーザーが何かチャットを送るとプログラムを実行します。 PostBackEventはこのアクションが関連付けられたコントロールがタップされると、dataプロパティに指定された文字列を含むPostBackEventがwebhookを介して返されます。 つまりMessageEventで返されるボタンをタップしたらプログラムを実行します。 これからはこの3つのpassををいじっていきます。 入出時, JoinEvent app.py #group_boxはJoinEventの外に書いて、グルーバル変数にしてください。 group_box = {} #以下全てJoinEvent内 groupId = event.source.group_id group_summary = line_bot_api.get_group_summary(groupId) #もしグループIdがgroup_boxにないなら if not groupId in group_box: group_box[groupId] = {} group_box[groupId]['users_profile'] = {} group_box[groupId]['user_check'] = {} #よろしくの確認を押した人の辞書 if not groupId in group_box[groupId]: #グループの名前を取得 group_box[groupId]['groupName'] = group_summary.group_name #入出時にメッセージを返す line_bot_api.reply_message( event.reply_token, TextSendMessage( text=f"招待ありがとう!\n{group_box[groupId]['groupName']}への参加を確認しました。" ) ) ここで重要なのは、グループ入出すると同時にグループIdを取得してこのグループ専用の辞書を作成することです。これを図に表すとこんな感じです。 これがグループの数だけgroupIdが増えます。もしこれをしないと、グループで取得した情報がグループ同士で境界線を持つことなく共有されてしまいます。今後はこの図の末端の辞書を使っていきます。 「よろしく」 app.py #直前のチャットを格納します if not userId in text_box: text_box[userId] = [] if text_message != "よろしく": text_box[userId].append(text_message) if len(text_box[userId]) > 1: text_box[userId].pop(0) if text_message == "よろしく": detail_text = text_box[userId][0] buttons_template = ButtonsTemplate( title='確認したらボタンを押してね', text=detail_text, actions=[ PostbackAction(label="確認しました", data="check-txt") ] ) # チェックリストを初期化する group_box[groupId]['user_check']['userIds'] = [] group_box[groupId]['user_check']['user_name'] = [] template_message = TemplateSendMessage(alt_text="スマートフォンからボタンを押してね", template=buttons_template) line_bot_api.reply_message(event.reply_token, template_message) if group_box[groupId]['users_profile']: for user in group_box[groupId]['users_profile']: line_bot_api.push_message( user, TextSendMessage(text=f"{group_name}から{user_name}より\n\n{detail_text}") ) 「よろしく」と打たれたら、直前のチャット内容を含んだボタンを返します。それと同時にuser_check内にuserIdsとuser_nameの空のリストを作ります。その後、確認ボタンをユーザが押すと、そのユーザのuserIdと名前を格納します。これはコマンド「点呼」で使います。 そしてfor文で登録しているユーザに個別で同じ内容のものを返します。これがグループラインに既読を付けずに重要なチャットのみを見る機能です。 「点呼」 app.py number = line_bot_api.get_group_members_count(groupId) rest_number = number - len(group_box[groupId]['user_check']['user_name']) name_list = ', '.join(group_box[groupId]['user_check']['user_name']) if name_list == '': line_bot_api.reply_message( event.reply_token, TextSendMessage(text="誰の連絡も確認できません。") ) elif rest_number == 0: line_bot_api.reply_message( event.reply_token, TextSendMessage(text="全員の確認が取れました。ご協力ありがとうございました。") ) else: line_bot_api.reply_message( event.reply_token, TextSendMessage(text=f"{name_list}の連絡を確認。残り{rest_number}人です。") ) 「よろしく」と同様「点呼」とメッセージが来たら上の処理を行います。 難しくはありません。確認ボタンを誰も押してなければ最初の処理、残りの人数が0になれば2番目の処理、最後は残りの人数を返す処理です。 「登録」 app.py #MessageEevnt内 buttons_template = ButtonsTemplate( title='個別チャットへの連絡', text='登録ボタンを押してね', actions=[ PostbackAction(label="登録", data="register-chat") ] ) template_message = TemplateSendMessage(alt_text="スマートフォンからボタンを押してね。", template=buttons_template) line_bot_api.reply_message(event.reply_token, template_message) #PostBackEvent内 if not userId in group_box[groupId]['users_profile']: group_box[groupId]['users_profile'][userId] = user_profile.display_name line_bot_api.reply_message( event.reply_token, TextSendMessage(text=f"{group_box[groupId]['users_profile'][userId]}の登録を確認しました。") ) 「登録」とメッセージが来たら処理を行います。 MessageEvent内の処理は、登録を促すPostBackActionを返します。 PostBackEvent内はボタンが押されたとき、users_profileにuserIdとその名前を格納します。 解除は登録した者を消すだけ、退出処理は簡単なので省略します。 最後に 少し長くなってしまいましたね。わかりずらかったとこもあると思います。なのでプログラム全文はGitHubに挙げているのでそちらを見てください。 ここまでお付き合いくださってありがとうございました。まだまだ未熟者ですが、もっときれいで高度なプログラムを書けるように今後とも精進していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む