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

【AWS】lambdaでKMSを用いて環境変数の暗号化・復号化

概要 lambda使っててAPIを叩きたい時にシークレットキーなどの情報を環境変数に平文で置きたくないですよね. その時に用いるサービスがKMS(Key Management Service)です. この記事ではKMSを用いてlambda_functionの中で使用する環境変数の暗号化からプログラム中で復号化するところまでご紹介します(私自身AWS初心者で嵌ってしまったポイントがあるので記事にしました). lambdaで使用する言語はPythonで,コンソール上で操作を行います. 流れ ざっくりした流れは以下のようになります. KMSのカスタマー管理型キーの作成 対象のLambda関数に KMS:Decrypt 権限を与える 環境変数の作成・暗号化 関数上で暗号化された環境変数の復号化 1. KMSのカスタマー管理型キーの作成 KMSの管理コンソールへ行き,キーの作成を行います. キーのタイプの選択がありますが,対称でいいと思います. 今回は「user/kms-test」というエイリアスをつけました. 説明やタグは適当に設定しましょう. 次のキーの管理アクセス許可の定義やキーの使用アクセス許可の定義は各々の設定に合わせます. ここでlambda関数にのみ権限を与えるのが本来いいとは思います(が面倒なので私はAdminに設定しました). たとえばこちらの記事ではこの時点でlambda関数にロールを設定しています. 2. 対象のLambda関数に KMS:Decrypt 権限を与える まず実行ロールの設定します.IAMコンソールのこちらのページにアクセスしてポリシーの作成を行います. 下の図のように設定を行います.サービスにKMS,アクションにDecypt,リソースに先程作成したKMSのカスタマー管理キーのARNを記述します. 名前はlambda-kms-decrypt-policyとしました. 次にlambda関数に今作成したポリシーをアタッチします. こちらのページにアクセスして対象のlambda関数のロールを選択します.するとポリシーをアタッチしますというボタンがあると思うのでクリックしてください.そこで先程作成したポリシーを選択します. 3. 環境変数の作成・暗号化 続いてlambdaでの作業になります.コンソールのlambdaのページから対象の関数のところに行きます. 設定→環境変数で環境変数の作成をクリックします. 今回はTEST_ENCRYPTという環境変数を試しに作ってみます. 暗号化の設定のところで転送時の暗号化に使用するヘルパーの有効化にチェックを入れると暗号化できるようになります. そこで先程作成したKMSキーを選択します.すると下の図のように値が暗号化されました. これで環境変数の設定は以上です. 4. 関数上で暗号化された環境変数の復号化 lambda_functionの例を示します. 以下のコードをテストするとログに暗号化された文字列と復号化された文字列が表示されるはずです. lambda_function.py import json import boto3,os from base64 import b64decode def lambda_handler(event, context): ENCRYPTED = os.environ['TEST_ENCRYPT'] print(ENCRYPTED) DECRYPTED = boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED),EncryptionContext={'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']})['Plaintext'].decode('utf-8') print(DECRYPTED) return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') } 最後に 私もAWS初心者のためこれで正しいのか正直わかりません.有識者の方,助言などぜひよろしくお願いします. あと,転送時の暗号化に使用するヘルパーの有効化についてですが,こちらはCDKからは操作できないのでしょうか?コンソール上だけからの設定だと面倒くさいなと思いました... 参考 https://dev.classmethod.jp/articles/decrypt-sensitive-data-with-kms-on-lambda-invocation/ https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-console https://aws.amazon.com/jp/premiumsupport/knowledge-center/kms-invalidciphertextexception/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】lambdaでKMSを用いて環境変数の暗号化・復号化【Python】

概要 lambda使っててAPIを叩きたい時にシークレットキーなどの情報を環境変数に平文で置きたくないですよね. その時に用いるサービスがKMS(Key Management Service)です. この記事ではKMSを用いてlambda_functionの中で使用する環境変数の暗号化からプログラム中で復号化するところまでご紹介します(私自身AWS初心者で嵌ってしまったポイントがあるので記事にしました). lambdaで使用する言語はPythonで,コンソール上で操作を行います. 流れ ざっくりした流れは以下のようになります. KMSのカスタマー管理型キーの作成 対象のLambda関数に KMS:Decrypt 権限を与える 環境変数の作成・暗号化 関数上で暗号化された環境変数の復号化 1. KMSのカスタマー管理型キーの作成 KMSの管理コンソールへ行き,キーの作成を行います. キーのタイプの選択がありますが,対称でいいと思います. 今回は「user/kms-test」というエイリアスをつけました. 説明やタグは適当に設定しましょう. 次のキーの管理アクセス許可の定義やキーの使用アクセス許可の定義は各々の設定に合わせます. ここでlambda関数にのみ権限を与えるのが本来いいとは思います(が面倒なので私はAdminに設定しました). たとえばこちらの記事ではこの時点でlambda関数にロールを設定しています. 2. 対象のLambda関数に KMS:Decrypt 権限を与える まず実行ロールの設定します.IAMコンソールのこちらのページにアクセスしてポリシーの作成を行います. 下の図のように設定を行います.サービスにKMS,アクションにDecypt,リソースに先程作成したKMSのカスタマー管理キーのARNを記述します. 名前はlambda-kms-decrypt-policyとしました. 次にlambda関数に今作成したポリシーをアタッチします. こちらのページにアクセスして対象のlambda関数のロールを選択します.するとポリシーをアタッチしますというボタンがあると思うのでクリックしてください.そこで先程作成したポリシーを選択します. 3. 環境変数の作成・暗号化 続いてlambdaでの作業になります.コンソールのlambdaのページから対象の関数のところに行きます. 設定→環境変数で環境変数の作成をクリックします. 今回はTEST_ENCRYPTという環境変数を試しに作ってみます. 暗号化の設定のところで転送時の暗号化に使用するヘルパーの有効化にチェックを入れると暗号化できるようになります. そこで先程作成したKMSキーを選択します.すると下の図のように値が暗号化されました. これで環境変数の設定は以上です. 4. 関数上で暗号化された環境変数の復号化 lambda_functionの例を示します. 以下のコードをテストするとログに暗号化された文字列と復号化された文字列が表示されるはずです. lambda_function.py import json import boto3,os from base64 import b64decode def lambda_handler(event, context): ENCRYPTED = os.environ['TEST_ENCRYPT'] print(ENCRYPTED) DECRYPTED = boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED),EncryptionContext={'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']})['Plaintext'].decode('utf-8') print(DECRYPTED) return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') } 最後に 私もAWS初心者のためこれで正しいのか正直わかりません.有識者の方,助言などぜひよろしくお願いします. あと,転送時の暗号化に使用するヘルパーの有効化についてですが,こちらはCDKからは操作できないのでしょうか?コンソール上だけからの設定だと面倒くさいなと思いました... 参考 https://dev.classmethod.jp/articles/decrypt-sensitive-data-with-kms-on-lambda-invocation/ https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-console https://aws.amazon.com/jp/premiumsupport/knowledge-center/kms-invalidciphertextexception/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GCPのCompute EngineにPython実行環境を整える

はじめに(この記事に書いていること) データエンジニアをしている私は、趣味でpythonの競馬予想プログラムを作成しています。 プログラミングが未熟なせいもありますが、過去20年(現在約93万頭)のデータを用いてプログラムを実行すると、現在16GBの自前ローカルマシン(※)のメモリ使用率は99%になっており、将来の継続性に不安を感じるようになりました。 そうした中、クラウドサービスの高性能コンピュータを用いることが出来れば、将来の不安を取り除くことが出来ることから、Google Cloud Platform(GCP)の$300無料クレジット枠でpythonの実行環境を整えることにチャレンジしました。 なお、Google Colabの方が安価で手っ取り早いことが分かったのですが、そうしたコストパフォーマンスの記事は別に書きたいと思っています。 (※)ローカルマシンの環境:(OS)Windows10 この記事は以下の項目で整理しています。 - VMインスタンスの作成 - ssh-keyの作成と登録およびwindowsマシンからVMへの接続 - WindowsマシンからVMへのファイルの送受信とAnacondaのインストール - pythonの実行 VMインスタンスの作成 googleアカウントのGCP無料クレジット枠への登録は留意事項をよく読んで承認してください。 GCPへの登録が済みましたら、Compute Engineに入り、インスタンスの作成に進んでください。 次に設定は以下の通りです。 インスタンス名 任意に命名します。 リージョン:us-west1、us-central1、us-east1 のいずれか (この3つのリージョンだとf1-microを無料クレジット枠が無くなっても無料で使えます。) マシンの構成: 8CPU以下のマシン (無料クレジットで選択できる上限) (https://cloud.google.com/compute/vm-instance-pricing?hl=ja#general-purpose_machine_type_family)を参照してください。 このマシン構成はいつでも変更可能です。 以下の図で選択しているマシンは($0.10848/h 約12円/h)です。 ブートディスク  ここでの注意事項はこの容量を途中で変更はできないので、予想されるファイルサイズの2.5倍ぐらいを選択しておくのが良いです。30GB以下なら無料クレジット後も無料です。  マシンはデフォルトのDebian GNU/Linux 10 を選択します。 (Linuxに不慣れな方も以下の通り実行すれば大丈夫!) 以上を設定したら作成をクリックします。 「必ず無料トライアルクレジットが適用されているかを確認してください」 ssh-keyの作成と登録およびwindowsマシンからVMへの接続 windowsマシンからVMインスタンス(以下VM)にはsshという方式で接続します。 その際に、誰でも固有のVMを操れるようでは問題なので、ローカルマシンで暗号鍵を作成し、VMに登録します。 Puttyのインストール 暗号鍵はPuTTyというアプリケーションで作成するので、まずはググってPuTTyをインストールしてください。 ssh-keyの作成 PuTTygenを起動させ、 1.generate  マウスをクルクル動かしてください。暗号key(ssh-key)が作成されます。 2.use-nameとパスワードの登録  ここでは<google.account>で登録する例を示します。  入力すると暗号keyの最後に=google.accountと追加されます。 その下に任意のパスワードをconfirmとともに入力してください。 3. 保存 ローカルマシンの適当なディレクトリに任意の名前で拡張子ppkのファイルを保存します。 4. keyのコピー  この後にVMに設定するためにkey全体をコピーします。 VMにssh-keyの登録 1.鍵の登録 GCPのCompute EngineのページでVMを編集から、下の方へ進み、 以下のSSH鍵のところを編集します。その際、先ほどコピーしたものをペーストします。 するとgoogle.accountが反映されるのが確認できます。 2.IPアドレスのコピー VMの外部IPアドレスは起動のたびに割り当てられます。 現在のIPアドレスを次の接続用にコピーしておいてください。 SSH接続 先ほどインストールしたPuTTyのファミリーからPuttyを起動させます。 1.IPアドレスの入力 google.account@コピーしたIPアドレス 2.SSH鍵のppkファイルを指定して接続 3.パスワードを入力してログイン 下のようなshellが現れ、PuTTygenで2回入力したパスワードを入力してログインします。 成功したら次のようなShellが現れます。 (画像では次のステップで行うAnacondaインストーラーをアップロードしている) WindowsマシンからVMへのファイルの送受信とAnacondaのインストール ファイルの送受信方法は色々ありますが、Guiで分かりやすい、WinSCPを用いる方法がおすすめです。ですので、WinSCPをダウンロード、インストールしてください。 WinSCPの設定 転送プロトコルにSCPを選択し、IPアドレス、google.account、上と同じパスワードを入力し、設定をクリックします。 次にppkファイルを指定して、ログインします。 成功するとVMインスタンスのhome/google.accountのディレクトリーが表示されます。 WinSCPはいろいろ試した中で、アップロードとダウンロードが最速で最も簡易にできると感じています。 Linux用のAnacondaインストーラーをSCPでVMにアップロード https://www.anaconda.com/products/individual からLinux用のインストーラー(拡張子sh)をローカルマシンにダウンロードします。 ダウンロード後、先ほど設定したWinSCPからhome/google.accountにアップロードします。 Anacondaのインストール PuTTyのShellにて以下のコマンド(bash以下)を入力してください。 $ bash Anaconda3-2020.11-Linux-x86_64.sh (Anacondaのバージョンは適宜変更) 同意確認に入るので、しばらく[enter]を押してください(下にMOREと表示されます) 3回ほど、Yes/Noの入力要求があるので、すべてYesで入力します。 VMインスタンスの再起動と再ログイン 無事インストールが終了したら、Anacondaを認識させるためにVMを再起動させます。 1. shellにexitと入力して閉じる。 2. ブラウザーからCompute Engineを停止し、VMインスタンスを再起動。 3. 外部IPアドレスをコピー(新しいIPアドレスが割り当てられる。同じ場合もある) 4. PuTTyからVMに再度ログイン(新しいIPアドレス) 5. Anacondaの仮想環境が(base)でログインされる。 (base)が表示されたらAnacondaがインストールされた証です。 (PuTTyの外観(カラーやフォント)はchenge settingから変更できます。) 任意の仮想環境の設定とactivate condaのupdateをしておく。   $conda update -n base -c defaults conda と入力。 任意の仮想環境設定(例は任意の仮想環境「qiita」でpython3.7を用いる)    $conda create --name qiita python=3.7 任意の仮想環境に入る $conda activate qiita (qiita)google.account@インスタンス名 になっているのを確認 Pythonの実行 ローカル環境で作成したpythonファイル(sample.py)をWinSCPでVMにアップロードし、 $python3 sample.pyと入力すると実行できるはずです。 ただし、必要なモジュールをinstallする必要がありますので、エラーが出るたびにインストールされていないモジュールをinstallします。 例えば $conda install pandas あとがき 以上です。画像が入り乱れてしまっており、乱雑な記事で失礼します。 なお、VMインスタンスは都度シャットダウンか削除しておかないと、起動中は料金が発生します。無料クレジット枠は約30,000円分あり、承諾しない限りは課金モードに入らないので、知らずに課金されることはないと思いますが注意が必要です。 また、作成したファイルの中に大容量のファイルなどがあるとストレージの料金がかかることもありますので、都度ダウンロードし、インスタンスは削除するのが良いと思います。 インスタンスの削除、再作成は最初に苦戦した場合などは億劫に感じますが、何回か繰り返すと慣れます。 皆様のクラウドコンピューティングライフを始める一助になればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

量子力学 自由粒子の確率密度分布の時間発展をプロットしてみた

はじめに 量子力学を考える上で、粒子の波動関数を考えることは非常に重要なテーマです。ここではプロパゲータを用いた波動関数の表現から波動関数を考えていきます。 今回は波動関数が ・ガウス分布(正規分布)の初期状態(無限井戸型ポテンシャルの基底状態など) ・ハミルトニアンがある時刻($ t_0 $)で自由粒子になった場合 の確率密度分布の時間発展をプロットしてみます。(一次元) 具体的な条件式 初期状態 $$ \Psi(t_0,x)= \left( \frac{1}{\sqrt{\pi} \Delta} \right)^\frac{1}{2} \exp \left[-\frac{x^2}{2\Delta^2} \right] $$ $$ (分散:\Delta^2) $$ ハミルトニアン $$ \hat{H} = \frac{\hat{P}^2}{2m} $$ プロパゲータ $$ \left(\frac{m}{2\pi i\hbar T}\right)^\frac{1}{2} \exp \left[\frac{im(\Delta x)^2}{2\hbar T} \right] $$ $$ (T=t-t_0,\Delta x = x-x_0) $$ 確率密度分布 $$ \frac{m\Delta}{\sqrt{\pi}\left(m\Delta^2 + i\hbar T\right)} \exp\left[ -\frac{m\Delta^2x^2}{(m\Delta^2)^2+(\hbar T)^2} \right] $$ コーディング 定数は形に依存しないので $$ \hbar = \Delta =\pi = m =1 $$ と考えてコードを書く import numpy as np from matplotlib import pyplot as plt from matplotlib import animation def _updateAnimation(frame): plt.cla() plt.ylim(-0.1,1.01) t = frame x = np.arange(-1*10,10,0.1) y = function(x,t) xline = [0 for i in x] plt.plot(x,y) plt.plot(x,xline,color="black") t = round(t, 2) def function(x,t): a = 1/(1+t**2) b = np.power(a,1/2) f = b*np.exp(-a*np.power(x,2)) return f def main(): fig = plt.figure() params = { 'fig':fig, 'func':_updateAnimation,#グラフ更新する関数 'interval': 100,#更新間隔 'frames': np.arange(0,6, 0.05),#フレーム番号を生成するイテレータ 'repeat': True,#繰り返しの有無 } anime = animation.FuncAnimation(**params) anime.save("plot.gif", writer = 'imagemagick') plt.show() if __name__ == '__main__': main() 出力結果 考察 当然ながら時間経過とともに確率密度分布の裾ひろがって行きます。 このようにディラックのブラケットを用いた議論で考えた量子力学が系の状態変化をよく記述できていることが確認できました。 また、このような考え方は柏太郎さんの「経路積分法」から学びました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで株価の東証の銘柄コード一覧を取得して、東証1部、2部、TOPIXに整理

東証の銘柄コードの取得と市場ごとの整理 JPXに一覧のexcelがあるため、これをダウンロード pandasで読み出して市場や指標ごとに取り出します. 株関連の前提 大丈夫かと思いますが、大事なポイントを書き下しておきます. 4桁の銘柄コードになります. 何番代かで業界が異なります. このExcelに東証1部、2部、TOPIXで選ばれるもの、当たり前ですが日経平均225のものも全て含まれます. (しかし日経平均225に含まれるものは別途取得必要) TOPIX100はCore 30とLarge 70、TOPIX500はそれにMid 400、TOPIX 1000はそれにSmall 1、TOPIXはSmall 2を加えたものとなります 環境 Google Colaboratoryにて確認. コード import pandas as pd ##東証から上場企業の一覧を取得 !wget 'https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls' codelist = pd.read_excel("./data_j.xls") ## 各市場を整理 firstSectionCodeList = codelist.loc[codelist["市場・商品区分"] == "市場第一部(内国株)"] secondSectionCodeList = codelist.loc[codelist["市場・商品区分"] == "市場第二部(内国株)"] mothersCodeList = codelist.loc[codelist["市場・商品区分"] == "マザーズ(内国株)"] jasdaqStandardCodeList = codelist.loc[codelist["市場・商品区分"] == "JASDAQ(スタンダード・内国株)"] jasdaqGrowthCodeList = codelist.loc[codelist["市場・商品区分"] == "JASDAQ(グロース・内国株)"] ## TOPIXを整理 topix100CodeList = codelist.loc[codelist["規模区分"].isin([ "TOPIX Core30" , "TOPIX Large70" ])] topix500CodeList = codelist.loc[codelist["規模区分"].isin([ "TOPIX Core30" , "TOPIX Large70" , "TOPIX Mid400" ])] topix1000CodeList = codelist.loc[codelist["規模区分"].isin([ "TOPIX Core30" , "TOPIX Large70" , "TOPIX Mid400", "TOPIX Small 1"])] topixCodeList = codelist.loc[codelist["規模区分"].isin([ "TOPIX Core30" , "TOPIX Large70" , "TOPIX Mid400", "TOPIX Small 1", "TOPIX Small 2"])] 実際に値を確認すると以下のようになる firstSectionCodeList[["コード", "銘柄名"]]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はちみーのうたオートマトン書いてみた

こんにちは!バイオインフォマティクス系オタク会社員のroadricefieldです!今日はGW暇なので以下の?????????さんのTweetのトーカイテイオーの「はちみーのうた」オートマトンをPythonで書いてみました. はちみーのうた pic.twitter.com/aBgprfFVFQ— ????????? (@aventador_770_4) May 3, 2021 import sys class AutoUmaton: kashi = ["はちみー", "をなめるとー", "あしがー", "はやくーなる"] state = 0 def transition(self, ip): print(self.kashi[self.state]) if self.state == 0: if(ip): self.state = 1 else: pass elif self.state == 1: if(ip): self.state = 0 else: self.state = 2 elif self.state == 2: if(ip): self.state = 3 else: pass elif self.state == 3: if(not ip): self.state = 0 else: pass def main(): try: S = list(map(int,list(input()))) except: print("トレーナー!入力がちがうよ!!", file=sys.stderr) Teiou = AutoUmaton() for i in S: Teiou.transition(i) main() 入力例 000100011 出力例 はちみー はちみー はちみー はちみー をなめるとー あしがー あしがー あしがー はやくーなる クラスを作るいい練習になったと思います. おしまい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

やりがちなif文でのエラーに気をつけよう! (今日のPython Day3)

0.はじめに  いよいよ3日目ですね。今日と明日頑張れば3日坊主にはなりません。GW中、何もやることなくYouTubeでダラダラ動画を見続けているそこの大学生もPythonを一緒に学ぼう!  毎日1問、Pythonの問題を出題します。出題範囲は特に定めていませんがはじめの1ヶ月くらいは『入門Python3 第2版』の第1~11章までのことが分かれば解ける問題にしたいと思います。「こういう問題を作って欲しい」などのリクエストがございましたら初心者ながら頑張って作問します。また「別解を思いついた」、「間違えを見つけた」などがありましたら遠慮なくコメント欄にて教えて下さい。記事を執筆している当人もこの記事を読んでくださった方も新たなことを学ぶことができるので。 1. 問題  太郎君は得点が60点以上なら「合格」, 60点未満なら「不合格」と表示するプログラムを作ろうとした。しかし、エラーが出てしまった。太郎君に代わってプログラムを直そう。 2. 解答 # 太郎君がつくったプログラム score = 60 if score >= 60 print("合格") else: print("不合格") 3. ヒント # 吐き出されたエラー if score >= 60 ^ SyntaxError: invalid syntax 4. 解答 # 太郎君がつくったプログラム score = 60 if score >= 60: print("合格") else: print("不合格") 5. 解説  末尾のコロン「:」が必要です。自分はよくコロンを打ち忘れるかセミコロン「;」を誤って打ちます (JISキーボードだと「;」と「:」が隣り合っている上に押す指がどちらも右手小指だから)。「くれぐれも気をつけましょう。」と言いたいところですが、エラーが出たら諦めてその都度対処しましょう。 6. まとめ ・if文ではコロンの打ち忘れに気をつけよう! 7. おまけトーク  GW中、何もやることなくYouTubeでダラダラ動画を見続けているそこの大学生とは自分のことです。最近はエンジニアチャンネルの「あるある動画」にハマっています。 エンジニアチャンネル
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sys.modulesに登録しないでモジュールをロードする方法

sys.modulesに登録しないでモジュールをソースファイルからロードし、そのモジュールを変数にアサインして、そのモジュールの利用をその変数を通してのみの利用に限定したい場合、importlibのクラスや関数を駆使した下記の方法があるようです。参考リンク先に紹介されていました。モジュール名は識別子として利用できない文字列も付けられるようです。 import importlib.util from importlib.machinery import SourceFileLoader loader = SourceFileLoader("<unnamed module>", path="samplemodule.py") spec = importlib.util.spec_from_loader(loader.name, loader) mod = importlib.util.module_from_spec(spec) loader.exec_module(mod) >>> mod <module '<unnamed module>' from 'samplemodule.py'> 参考 * https://stackoverflow.com/questions/19009932/import-arbitrary-python-source-file-python-3-3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

__setattr__よりpropertyのsetterを優先させたい場合

__setattr__は、属性への代入をカスタマイズするためのスペシャルメソッドですが、propertyのsetterよりも優先順位が高いです。したがって、下記のように、xにsetterが定義されていても、xへの代入文では__setter__が呼ばれており、メンバ変数_xは作成されていません。 class Foo: @property def x(self): return self._x @x.setter def x(self, value): self._x = value def __setattr__(self, key, value): pass >>> f = Foo() >>> f.x = 3 >>> f.x AttributeError: 'Foo' object has no attribute '_x' x propertyのsetterを機能させたい場合、__setattr__の中で、次のようにすることができます。 class Foo: @property def x(self): return self._x @x.setter def x(self, value): object.__setattr__(self, "_x", value) def __setattr__(self, key, value): if hasattr(type(self), key): attr = getattr(type(self), key) if isinstance(attr, property): if hasattr(attr, 'fset'): attr.fset(self, value) else: raise AttributeError("%s is read-only" % key) else: raise AttributeError("%s is not a property" % key) このように__setattr__を定義すると、下記のようにsetterが機能します。 >>> f = Foo() >>> f.x = 3 >>> f.x 3 f.x = 3の文で、__setattr__では、 hasattr(type(self), key)で、Fooが属性xを持つかどうか、 isinstance(attr, property)で、Foo.xがプロパティオブジェクトであるか、 hasattr(attr, 'fset')で、そのプロパティオブジェクトがsetterをもつかがチェックされていて、 attr.fset(self, value)で実際にsetterを呼んでいます。 注意しなければならないのは、xのsetterの定義中で、単純にself._x = valueとしてしまうと、カスタマイズした__setter__が呼ばれてしまうので、デフォルトの代入動作を行うため、objectの__setattr__を明示的に呼ぶようにしています。 参考 * https://stackoverflow.com/questions/15750522/class-properties-and-setattr
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラウド上に構築した新型仮想ネットワークとWebAPIでJSONメッセージをやり取りする画面

# -*- coding: utf-8 -*- from PyQt5 import QtWidgets, uic import signal from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.Qt import * import sys from PyQt5 import QtCore, QtGui, QtWidgets from collections import deque import datetime import threading import time import json import urllib.request import sys import codecs import webbrowser #from lib import functions #from lib import gl app = QtWidgets.QApplication([]) win = uic.loadUi("ui/metatronsight.ui") #specify the location of your .ui file def GetValue(self, row, col): return self.data[row][col] def openurl(self): #obj = event.GetEventObject() #url1=obj.GetCellValue(event.GetRow(),1) item = self.table.itemAt(row, 2) win.label.setText(item) #print(currentQTableWidgetItem.row(), currentQTableWidgetItem.column(), currentQTableWidgetItem.text()) oid="608be70b8a6e8c0f51a65b46" url="http://192.168.0.15:3000/meta/"+oid webbrowser.open(url) def cell_was_clicked( row, column): #print("Row %d and Column %d was clicked" % (row, column)) #item = win.tableWidget.itemAt(row, 2) #win.label.setText(str(item.text())) win.label.setText("Row %d and Column %d was clicked" % (row, column)) item = win.tableWidget.item(row, column) oid=str(item.text()) url="http://192.168.0.15:3000/meta/"+oid #クラウド上に構築した組織内仮想ネットワークに接続するWebAPI webbrowser.open(url) def setjson(): url = 'http://192.168.0.15:3000/jsapi.json' res = urllib.request.urlopen(url) # json_loads() でPythonオブジェクトに変換 data = json.loads(res.read().decode('utf-8')) #s=res.read().decode('utf-8') i = 0 j = 0 win.tableWidget.setRowCount(100) # set column count win.tableWidget.setColumnCount(2) #win.tableWidget.itemDoubleClicked.connect(openurl) win.tableWidget.cellClicked.connect(cell_was_clicked) #win.tableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) for item in data: win.label.setText(str(item["title"])) Data = QTableWidgetItem(str(item["title"])) j = 0 win.tableWidget.setItem(i, j, Data) Data2 = QTableWidgetItem(str(item["oid"])) j = 1 win.tableWidget.setItem(i, j, Data2) i = i + 1 win.tableWidget.resizeColumnToContents(0) win.tableWidget.resizeColumnToContents(1) def setTable(win): headers = ["内容", "重要度", "優先度"] # ファイルをオープンする # test_data = open("data/tabledata.json", "r") # すべての内容を読み込む # contents = test_data.read() # ファイルをクローズする # test_data.close() #f = open('data/test2.txt', 'r') #jsonData = json.load(f) #f.close() # tableData0=contents d = data print(d) i = 0 j = 0 # for i, (key, value) in enumerate(d["events"].items()): # set row count win.tableWidget.setRowCount(10) # set column count win.tableWidget.setColumnCount(4) win.tableWidget.itemDoubleClicked.connect(show_hensyu) # currentQTableWidgetItem.row() for item in d: # print(str(i)+":"+item["title"]) Data = QTableWidgetItem(str(item["title"])) j = 0 win.tableWidget.setItem(i, j, Data) j = 1 Data2 = QTableWidgetItem(str(item["body"])) win.tableWidget.setItem(i, j, Data2) j = 2 if not item.get('date'): print('NULL') else: Data2 = QTableWidgetItem(str(item["date"])) win.tableWidget.setItem(i, j, Data2) j = 3 Data2 = QTableWidgetItem(str(item["amount"])) win.tableWidget.setItem(i, j, Data2) i = i + 1 win.tableWidget.resizeColumnToContents(0) win.tableWidget.resizeColumnToContents(2) win.resize(1024,750) win.show() setjson() #gl.main() sys.exit(app.exec())
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pickleの動作をカスタマイズするためのメソッドをテストする

pickleは、ファイルなどに保存するために、Python オブジェクトをバイト列にシリアライズするための標準ライブラリです。Pickle可能なオブジェクトはpickable、シリアライズされたバイト列からオブジェクトを再生成することをUnpickleと呼んでいます。 Pickleは、デフォルトでは__dict__に登録されているオブジェクトの属性をシリアライズしようとしますが、pickle用のスペシャルメソッドを定義することにより、その挙動をカスタマイズすることができます。特に、pickleするオブジェクトが、pickle出来ないオブジェクトをメンバーとして持っている場合や、そのオブジェクトが引数をとるコンストラクタで生成される場合などに使用します。 メソッド 使用目的 __getnewargs__() Unpickleする際にコンストラクタに渡す引数を指定する。 __getstate__() __dict__ではなく、このメソッドが返すオブジェクトをpickleする。 __setstate__(state) pickleされていたオブジェクトをstateとして受け取り、unpickle時の処理を行う。 次のコードはこれらのメソッドの挙動を確認するためサンプルスクリプトです。 import pickle class Foo: def __new__(self, a): print('in __new__:', a) self = object.__new__(self) # self.a = a return self def __init__(self, a): print('in __init__:', a) self.a = a def __getstate__(self): print('in __getstate__:', self.a) state = self.__dict__.copy() return state def __getnewargs__(self): print('in __getnewargs__:', self.a) return (2 * self.a,) def __setstate__(self, state): self.__dict__.update(state) print('in __setstate__:', self.a) foo = Foo(3) bar = pickle.dumps(foo) baz = pickle.loads(bar) print(baz.__dict__) このスクリプトの実行結果は次のようになります。 in __new__: 3 in __init__: 3 in __getnewargs__: 3 in __getstate__: 3 in __new__: 6 in __setstate__: 3 {'a': 3} pickleのマニュアルに書いてあるとおりですが、下記のことが分かります。 __getnewargs__は、Pickleする時に呼ばれる。つまり、Unpickleされるときに__new__に渡す引数の情報は、Pickleされている。 __new__には、__getnewargs__が返した値が渡されている。 Unpicleでは__init__は呼ばれない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Django / python] 関連テーブルの複数の属性を条件にしてgroup_byする(ORMで)

やりたいこと 複数のテーブルを結合し、その結果できた表に対してGROUP_BY、SUMを適用したい。 GROUP_BYの条件とするのは、関連テーブル側に存在するカラム。 どういうこと? 以下のようなテーブルがあると考えます。 テーブル 1. Orders(注文) カラム名 型 id integer date date 2. Items(商品) カラム名 型 id integer name string 3. OrderedItems(注文された商品) カラム名 型 id integer order_id integer item_id integer count integer このようなテーブルがあるとして、それぞれの日(date)にどの商品(name)がどれだけ(count)注文されたのかを集計したい。 そこで次のようなSQLが思い浮かぶ.... SQL ※このSQLは実際に実行したわけではないので、このSQLには誤りが含まれている可能性がかなり高いです。 あくまでもイメージです。 SQLのイメージ SELECT orders.date, items.name, SUM(ordered_items.count) AS total FROM ordered_items LEFT JOIN orders ON ordered_items.order_id = orders.id LEFT JOIN items ON ordered_items.item_id = items.id GROUP BY orders.date, items.name; こんな感じのSQLをDjangoのORM経由で実行したい。 これでできた 1 python from django.db.models import Sum OrderedItem.objects \ .values('order__date', 'item__name') \ .annotate(total=Sum('count')) ポイント? どうやら、 GROUP_BYしたいとき valuesメソッドの引数に、GROUP_BYする際の条件とするカラム名を文字列で指定する 他のテーブルのカラムを条件にしてGROUP_BYしたい場合 valuesに指定するカラム名の前に、{テーブル名}__をつける => '{テーブル名}__{カラム名}'のようになる 複数条件でGROUP_BYしたい場合 valuesメソッドの引数として、それらの条件を全て列挙する で良さそう。 感想 このあたりの情報をはっきりと記載している記事がなかなか見つからなかった、、、 特にポイントの3つ目については、記載されている記事を見つけられず、仕方なく勧で書いたらたまたま動いてしまった、という状況でした...ԅ(º﹃ºԅ) そんなわけでメモ化。 (次回スムーズに同じことができる自信がない...) 参考にした記事 Django データベース操作 についてのまとめ Django 1対多(one2many)でリレーションされた子テーブルの集計値を取得する Django ORMでGROUP BY ... COUNTまたはSUMを実行する方法 Djangoで、集計処理 より役に立ったと感じた順に記載しています そしてなぜDjangoの公式サイトがヒットしない......? 公式サイトが全然頼りにならない... 実際には、もっとわけのわからない名前のテーブルに対してこの処理実行しており、今回はわかりやすくするためにテーブル名やカラム名を変えているので、完全にこの通りに書いても動かない可能性もあります。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Dockerfile】DockerにConda環境を構築し、仮想環境をActivateする

CUDAイメージ上に Miniconda or Anaconda 環境を構築し、conda or pip を仮想環境にインストール、アクティベートするところまでをDockerfileで完結させるためのDockerfileを記載します。 実現したいこと CUDAイメージ上にMiniconda(Anaconda)環境を構築 Dockerfile内で、任意の仮想環境を作成し、パッケージをインストール Docker run (attach)した際に、Dockerfileに記載した仮想環境下に入る(conda activateする必要がない) Dockerfile Dockerfile FROM nvidia/cuda:11.2.1-devel-ubuntu20.04 RUN apt-get update && apt-get install -y \ sudo \ wget \ vim WORKDIR /opt RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \ sh Miniconda3-latest-Linux-x86_64.sh -b -p /opt/miniconda3 && \ rm -r Miniconda3-latest-Linux-x86_64.sh ENV PATH /opt/miniconda3/bin:$PATH COPY <env_file_name>.yml . RUN pip install --upgrade pip && \ conda update -n base -c defaults conda && \ conda env create -n <env_name> -f <env_file_name>.yml && \ conda init && \ echo "conda activate <env_name>" >> ~/.bashrc ENV CONDA_DEFAULT_ENV <env_name> && \ PATH /opt/conda/envs/<env_name>/bin:$PATH WORKDIR / CMD ["/bin/bash"] NVIDIA Containers / Miniconda NVIDIA CUDA Image 一覧はこちらから Miniconda 一覧はこちらから
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外側の一時的なローカル変数を参照する際の注意点

PyQtでウィジットを作成するときなど、関数やメソッドをコールバックとして利用するために気軽にlambdaを作成してパラメータを減らしたものをコールバックとしてセットするというようなことをすることがあります。 この場合、次の例のように、コールバックを生成するメソッド内の一時的なローカル変数を コールバックの中で参照すると、その変数は自由変数となり、意図しない結果となる場合があるので注意が必要です。 class Parent: def __init__(self): self.children = {"foo": Child(), "bar": Child()} for name, child in self.children.items(): child.get_name = lambda: self.get_child_name(name) def get_child_name(self, name): return name class Child: pass p.children["foo"].get_name()は、"foo"を返すことを期待しますが、実際は"bar"を返します。 >>> p = Parent() >>> p.children["foo"].get_name() 'bar' __init__の中で、nameはforループで使用する一時的なローカル変数として使っていますが、2つのChildオブジェクトのget_name属性に代入されるlambdaの中で使用されているため、自由変数になります。 forループの中で最初のChildオブジェクトにlambdaをセットするときはnameには"foo"がセットされていますが、その後もそのlambdaは、同じname変数を参照し続けるため、nameにはその後"bar"がセットされて意図しない動作となるわけです。下記のとおり、p.children["foo"].get_name()とp.children["bar"].get_name()が同じ変数を参照していることが分かります。 >>> p.children["foo"].get_name() is p.children["bar"].get_name() True 関連記事 * 自由変数の名前やデータを、関数オブジェクトの中で調べる * ネストした関数定義のユースケース
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOS Big Surでgrpcioのインストールに失敗したらSYSTEM_VERSION_COMPAT=1をつける

tl;dr SYSTEM_VERSION_COMPAT=1 pip install grpcio 問題が生じた環境 macOS Big Sur (11.2.3) Python 3.8.0 pip 20.2.4 grpcio 1.37.0 経緯 Pythonのパッケージ解決をしていたら grpcio だけインストールに失敗した。 原因 原因は、macOSのバージョンとその判別処理のせいらしい。 macOSのバージョンは長らく10系だったが、Big Surになりバージョンが11系になった。 grpcioは11系のサポートをしていないためにエラーが返り、インストールに失敗するのだ。 解決方法 今回はgrpcioが引っかかったが、実はgrpcio以外のパッケージやPython以外のソフトウェアでも似たような問題は起こりうる。 Appleもそれは見越していて、このような問題の対処のために SYSTEM_VERSION_COMPAT という環境変数を用意している。 SYSTEM_VERSION_COMPAT=1 という環境変数を定義しておくと、Big Sur(11系)であってもOSのバージョンを10系列であるかのように反応してくれる。 今回の場合であれば、 SYSTEM_VERSION_COMPAT=1 pip install grpcio として実行すればよい。 pipenvやpoetryを使っている場合も同様で、 SYSTEM_VERSION_COMPAT=1 pipenv install grpcio SYSTEM_VERSION_COMPAT=1 pipenv install SYSTEM_VERSION_COMPAT=1 poetry add grpcio SYSTEM_VERSION_COMPAT=1 poetry install などなど、とにかくコマンドの頭に SYSTEM_VERSION_COMPAT=1 をつけておけばよい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

picoCTF Practice Compress and Attack Writeup

python で ソケット通信 を組み立て,総当たり攻撃で解く問題。 こういうの,おしいところまではいくけど自力で成功することはめったにないので,ものすごく嬉しい。 この記事を自分で何度も見直すので picoCTF Practice Writeup 6 から独立させた。 Compress and Attack Category: Cryptography Description: Your goal is to find the flag. compress_and_attack.py nc mercury.picoctf.net 50899 Hints: 1. The flag only contains uppercase and lowercase letters, underscores, and braces (curly brackets) compress_and_attack.py #!/usr/bin/python3 -u import zlib from random import randint import os from Crypto.Cipher import Salsa20 flag = open("./flag").read() def compress(text): return zlib.compress(bytes(text.encode("utf-8"))) def encrypt(plaintext): secret = os.urandom(32) cipher = Salsa20.new(key=secret) return cipher.nonce + cipher.encrypt(plaintext) def main(): while True: usr_input = input("Enter your text to be encrypted: ") compressed_text = compress(flag + usr_input) encrypted = encrypt(compressed_text) nonce = encrypted[:8] encrypted_text = encrypted[8:] print(nonce) print(encrypted_text) print(len(encrypted_text)) if __name__ == '__main__': main() 重要なのは flag + user_input を圧縮し,encrypted_textの長さを出力している点 nc $ nc mercury.picoctf.net 50899 Enter your text to be encrypted: 適当な文字 abcdefghi $ nc mercury.picoctf.net 50899 Enter your text to be encrypted: abcdefghi b'\\y\xf9^6\xa1:v' b'\x0c\xa2H1y\xd6\x16b\x03tf|\x9c\x11\xa7-\xe5\xc9\x15\x1a\xa4\xc8N\x1d9\x8a\xbc\x8c\xc7V\xba\xd3\xe4\x7f7\xcf\x91\xeeD\x9d\x8a\x13\xff\xb9p\xcbm:o\x95\xca\x86+;' 54 picoCTF Enter your text to be encrypted: picoCTF b'~TJ{#.N ' b"\x15>\x0b\x07\x8d\n\xc6\x8d\x07'l\x8e\xfa\r\x11\xa3NcM\x0cv\xd3Y\xb7\xf3\x07< \x06Xl\xa6\xe3\x95\xaf\x92L:\xf8s\x9b\x1exm\x82\xa7\x86\x92" 48 encrypted_text の長さに注目 flag + "abcdefghi" --> 54 flag + picoCTF --> 48 flagは picoCTF{任意文字}なので,user_inputがflagと同じなら,圧縮効率が高い=encrypted_textの長さが短くなることがわかる Enter your text to be encrypted: picoCTF{a b"\xc5vTn\xc4F'N" b'\t}\xff(\xbc\x0b\xcd\x02\xeffD\tjfm\xca\xadN\xaf\\\xb3\xb2\xb7\x04?\x1c@\xbbd\xc1\x80u\xdcJ\x1c5]\xa8\xabW\xc2Y\xbbe\xaflg\xe5\xad' 49 Enter your text to be encrypted: picoCTF{b b'\x14\x10o6N\xc3_\x92' b'\xa4\x7fJ\x07\xbfOe\x04\x84\x9a\x16\x07\x97\xa5\xc8\t\x80ZXb\x86\x12\xb0E\xca\x0b\x87\xf6\xd5\xf0\x04\x8aZ\x12WM\xb3g\xa0\xf25\xb7 \xb9\xddJ\xb7\x7f9' 49 (中略) Enter your text to be encrypted: picoCTF{s b'0u\xab\xfbN\n\x87\xdd' b'KQ\xbc\x06\x14\xd4\xfc\xec\xd2@\\\x03\x02\x08W\x08rpi\xd0\x02\xd7\xe2\x82J\xba\xefd\xe4qHG\x93T\xe2\xfc\xecT\x8fe\x1c*\xf6\x19\xd7G!O' 48 picoCTF{a --> 49 picoCTF{b --> 49 picoCTF{s --> 48 ビンゴ あとは印字可能な文字で総当たりする python を書く。 solver.py # python 2.7 import socket import string import time start = time.time() def recvuntil(s, tail): data = '' while True: if tail in data: return data data += s.recv(1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('mercury.picoctf.net', 50899)) # 最初のプロンプトを受信,出力 data = recvuntil(s, 'encrypted:') print(data) # 基準となる encrypted_text の長さを取得する ans = "picoCTF{" s.sendall(ans + '\n') data = recvuntil(s, 'encrypted:') print(data) min_size = data.split('\n')[2] print("min_size") print(min_size) # ここからブルートフォース while True: # 終了判定 "}" を送信した結果が min_size と同じなら終了(フラグGet) s.sendall(ans + '}' + '\n') data = recvuntil(s, 'encrypted:') print(data) chk = data.split('\n')[2] #print("chk") #print(chk) if (chk==min_size): print(ans + '}' + '\n') break #for c in ("_" + string.ascii_letters): for c in ("_abcdefghijklmnopqrstuvwxyz"): s.sendall(ans + c + '\n') data = recvuntil(s, 'encrypted:') print(data) chk = data.split('\n')[2] #print("chk") #print(chk) if (chk==min_size): ans = ans + c print(ans + '\n') break elapsed_time = time.time() - start print ("speed:{0}".format(elapsed_time) + "[sec]") 実行結果
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerからTensorBoardXをつかう

TensorboardXはTensorflowの可視化ライブラリであるTensorboardをPyTorchから使用できるようにしたパッケージです。 こちらの環境ではdocker環境からjupyter notebookを使っていましたが、TensorBoardXも使えるようにした際のメモです。 行ったこと TensorBoardX も使えるようなdocker環境を修正した グラフファイルを作成し、tensorboardXの基本動作を確認した 動作環境 OS $ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=16.04 DISTRIB_CODENAME=xenial DISTRIB_DESCRIPTION="Ubuntu 16.04.6 LTS" dockerの環境 こちらで用いている環境をベースにしています。 jupyter notebookに加えてTensorBoardXも使えるようにしました。 1. TensorBoardX も使えるようにdocker環境を修正 tensorboardXでは、ニューラルネットワークの共通フォーマットであるONNX(Open Neural Network Exchange)形式を可視化するので、まずはPytorchで作成されたモデルをONNX形式に変換する必要があります。 TensorBoardXを使えるようにするためには、tensorflowとtensorboardxをインストールする必要があるため、Dockerfileに追加しました。 Dockerfile FROM nvcr.io/nvidia/pytorch:20.03-py3 ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y python3-tk && \ ## GUIに関するライブラリ pip install --upgrade pip && \ apt-get install -y libsm6 libxext6 libxrender-dev && \ ## OpenCVに必要なライブラリ pip install opencv-python && \ pip install pytest && \                    ## テスト環境に必要 pip install nose && \                     ## テスト環境に必要 pip install ipywidgets && \                 ## UI pip install tensorflow --ignore-installed --user && \  pip install tensorboardx RUN echo 'export QT_X11_NO_MITSHM=1' >> ~/.bashrc && \ source ~/.bashrc 上記Dockerfileを用いて、docker imageを作成します。 $cd **/work/ [Dockerfileのあるフォルダ] $docker build -t [repository名]:[tag名] . 例)$docker build -t test:1 . 作成したimageを用いてdockerのcontainerを立ち上げます。 (web cameraの設定はこちらを参照ください。) $xhost + $docker run --gpus all -it --rm \ --device /dev/video0:/dev/video0:mwr \ #web camera -p 10000:8888 \ #jupyter用のport forwarding -p 6006:6006 \ #tensorboardx用のport forwarding -v /[working directory path]/:/work \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -e DISPLAY=$DISPLAY test:1 $cd /work コンテナが起動したら、tensorboardxを立ち上げる準備ができました。 2. グラフファイルの作成とtensorboardの基本動作 可視化したいネットワークモデルとして、pytorchに入っているvgg19のモデルファイルを作成します。 graph import torch import torchvision net = torchvision.models.vgg19(pretrained=True) net.train() vgg19 VGG( (features): Sequential( (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU(inplace=True) (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): ReLU(inplace=True) (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (6): ReLU(inplace=True) (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (8): ReLU(inplace=True) (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU(inplace=True) (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (13): ReLU(inplace=True) (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (15): ReLU(inplace=True) (16): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (17): ReLU(inplace=True) (18): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (19): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (20): ReLU(inplace=True) (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (22): ReLU(inplace=True) (23): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (24): ReLU(inplace=True) (25): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (26): ReLU(inplace=True) (27): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (29): ReLU(inplace=True) (30): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (31): ReLU(inplace=True) (32): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (33): ReLU(inplace=True) (34): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (35): ReLU(inplace=True) (36): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (avgpool): AdaptiveAvgPool2d(output_size=(7, 7)) (classifier): Sequential( (0): Linear(in_features=25088, out_features=4096, bias=True) (1): ReLU(inplace=True) (2): Dropout(p=0.5, inplace=False) (3): Linear(in_features=4096, out_features=4096, bias=True) (4): ReLU(inplace=True) (5): Dropout(p=0.5, inplace=False) (6): Linear(in_features=4096, out_features=1000, bias=True) ) ) 次に、上記モデルをグラフファイルに出力する。グラフ以外にも様々なものが出力できる(参考資料)。 from tensorboardX import SummaryWriter batch_size = 5 dummy_input = torch.rand(batch_size, 3, 368, 368) with SummaryWriter("./tbx/", comment='test example vgg19') as w: w.add_graph(net, (dummy_input, ), True) こうしてカレントディレクトリの下にtbxというフォルダが作成され、その中にグラフファイルevents.out.tfevents.*** というファイルが生成されます。1章で起動したdocker内で、tensorboardを立ち上げます。ここで、グラフファイルがあるフォルダまでのパスを以下のように指定します。 #tensorboard --logdir="./tbx/" --bind_all ホスト側でブラウザを立ち上げ, URLとして http://localhost:6006 を入力すると、ブラウザ上にtensorboardxの画面が立ち上がります。 グラフ内のブロックをダブルクリックすると、より詳細な情報が提示されます。 最後に、Tensorbordxを終了するには、ターミナルでCtrl+Cを入力します。 参考にさせていただいた記事 1.tensorboardx 2.graph_addのサンプルコード 3.PyTorchによる発展ディープラーニング 4.Dockerでポートフォワーディング解説
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

markdown で書いたテキストを Medium に投稿する を iPad だけで完結させた話

Medium に投稿した記事の流用です。 なぜこんな事をしでかしたのかはこちらをご覧ください。 少しステップがいりますがタイトル通り iPad ですべて完結させるやり方です。 Github に上げています。 Github - post-md-medium 以下、解説と使い方です。 使用環境 iPad Air 第4世代 iPadOS 14.5 Python 3.9 (後述の a-shell のデフォルトがそうでした) 事前準備 Medium の Integration を使う事前準備をします。 Integration token まず Medium の Settings から integration token を発行します。 description には任意ですが何に使うトークンかわかる説明を入れて Get を押すと下にトークンが発光されます。 トークンの画面は Web 版にしか現れない様なので注意です。 このトークンは後で使用するのでコピーしておきましょう。 Download apps 次に使用するアプリをダウンロードします。 a-shell と Working Copy を使いますのでダウンロードしてください。(App Store で検索しても出てきます。) a-shell は iPad で使えるターミナルです。後で触れますが Python が触れる環境が整っています。普通に Python のプログラミングの勉強に持ってこいなめちゃつよアプリです。 Working Copy は iPad の Git クライアントアプリです。push が有料ですが、pull は無料でできます。とても使いやすいめちゃつよアプリです。 ちなみに自分はマークダウンエディタは Joplin を使っています。(今まで Boostnote を使っていたのですが、方向性の不一致から最近乗り換えようとしています?) なので本ツールは Joplin がはきだすテキストファイルの形式に合わせて作成しているので、もしこだわりのエディタが無ければ合わせてダウンロードしておくと少しだけ幸せになれるかもしれません。 How to use Git clone それでは本題に入っていきます。一応使い方を Github の README に書いてあるので、わかる方はここからはそっちを見ていただいても大丈夫だと思います。 まず a-shell を起動して mkdir medium としてディレクトリを作成してください。名前はなんでも大丈夫です。 次に Working Copy を立ち上げ以下のリポジトリを clone してください。Repository とある横の + を押すとリポジトリが追加できます。URL を以下の様にして DONE するとクローンされます。 ssh キーは Github のアカウントにログインすると自動で登録してくれているので SSH に切り替えて URL 指定するといけます。 Github - post-md-medium もしできそうになければ普通に zip をダウンロードしてきても大丈夫です。 クローンできたら右上のシェアから Setup folder sync を選択してください。リポジトリと同期するディレクトリを選ぶ事になるので、先ほど作成したディレクトリが a-shell の下にあるので選択して DONE です。 同期しなくても Save to file するだけでも問題はありません。 最終的に a-shell に戻って cd medium して ls した時に以下の様になっていると成功です。 Setup ↑の状態から python etc/init.py を実行してください。Integration token を入力させられますので一番最初に準備したトークンをコピペして Enter 。トークンデータが入ったファイルが作成されます。 ここまででセットアップは完了です。 Post だいたいのエディタにはテキストファイルのエクスポート機能が付いているはずなので、右上のシェアから Save to file で ↑ で準備したディレクトリにエクスポートしましょう。 エクスポートした時にタグの設定がどこにもなかったので、もしタグも合わせてポストしたい場合は本文の先頭に #Medium みたいに入力してみてください。 テキストファイルが用意できたら python post_medium.py を実行してください。ポストするファイルの名前を入力させられるので用意したファイル名を入力してください。 ここで draft か public か選択できます。デフォルトで draft にしているので何も入力しなければ draft になります。public と入力すれば直で公開されます。 posted nantara~~ みたいに表示されれば完了です。 お疲れ様でした!これで iPad からマークダウンで書いたテキストをそのまま Medium に投稿できる様になりました! オシャなカフェでドヤり放題です? 実装について init も全て Python にしているのは、一応 PC からでも使える様に init.sh を置いていますが、これが a-shell で実行できなかったからです。 ここが一番試行錯誤しました。subprocess で無理やり run しても一度にやりすぎみたいな感じでターミナルに怒られてクラッシュする始末...? 最終的に Python なら実行できたので妥協しました。ちなみに PC なら pipenv で実行できる様になっています。たぶん。 コンテンツタイプのお話ですが、最初は HTML ならみたいな話を見かけたので markdown を HTML にコンバートしてから投げるつもりでしたが、 Medium API のドキュメント によると markdown が指定できるので最強かよ...となった次第です。 まぁ独自スタイルにコンバートされるので思ったレイアウトではないですが... タグの扱いだけまだ悩んでいます。使っていくうちにアップデートしていくかもしれません。 とりあえず動く、を優先したので今後色々整備していくかもしれません。 少しだけ API の解説を。 まず Integration token から json のポストに必要な User ID の取得です。 user_id_res = requests.get('https://api.medium.com/v1/me', headers={ 'Authorization': f'Bearer {integration_token}' }) user_id = user_id_res.json()['data']['id'] このユーザ ID がリクエスト URL に必要です。 実際のリクエストは以下。contentFormat に本文のフォーマットが指定できます。今回はマークダウンですが、HTML も指定できます。 post_url = f'https://api.medium.com/v1/users/{user_id}/posts' headers = { 'Authorization': f'Bearer {integration_token}', 'Content-Type': 'application/json' } json = { "title": title, "contentFormat": 'markdown', "content": content, "tags": tags, "publishStatus": publish_status } requests.post(post_url, headers=headers, json=json) レスポンスで投稿した記事の URL なんかも取れるのでハンドリングするといいかもしれませんね。 Conclusion 普通にはてブとかにしとけばよかったんじゃ??? 僕の GW はこれに費やされました。。。つらみ。。。 誰がこんなニッチな事やりたがるんや。。。 こだわりが強いのも考えものですね... でも上手くいった時の達成感はクルものがあるので楽しくやれました。 普段仕事で Python 触らないのでこうやってたまに何か書くと楽しいです? やっぱり Integration が用意されてるサービスは僕みたいな人にはいいですね!用途に合わせて自動化したり便利に使えるので。 ただまぁ、Note が公式でサポートされたらそっちに移行するかもしれません。 少し長くなってしまいましたが同じ悩みを持つ人がいれば(いるのか...?)手助けになれば幸いです。 もし上手くいかないとかよく分からないとなればサポートしますのでコメントください。 それではみなさん、よい iPad ライフを✋ 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

セカント法(割線法)

任意の方程式(関数)に対してセカント法(割線法)で1つの解を求めるプログラムを1から書いてみた。 欠陥あるかもしれません。 セカント法(割線法) $$x_{new}=x_{old_2}-\frac{f(x_{old_2})}{\frac{f(x_{old_2})-f(x_{old_1})}{x_{old_2}-x_{old_1}}}$$ 基本的にはニュートン法と同じ。 $$x_{new}=x_{old}-\frac{f(x_{old})}{f(x_{old})^{\prime}}$$ $f(x_{old})^{\prime}$が分からない・計算したくない時はセカント法で微分計算を差分近似できる。 セカント法の場合は初期値が2つ必要になる。 設定するもの ・解きたい方程式(関数) ・初期値2つ ・許容誤差 def newton_method(func, initial_x1, initial_x2, r_error): こういう書き出しで作ってみます。 更新の流れ $$x_{new}=x_{old_2}-\frac{f(x_{old_2})}{\frac{f(x_{old_2})-f(x_{old_1})}{x_{old_2}-x_{old_1}}}$$ に基づいて、 $|f(x_{初期値1})| >$ 許容誤差 $|f(x_{初期値2})| >$ 許容誤差 ならば更新に入る $$x_{初期値2}-\frac{f(x_{初期値2})}{\frac{f(x_{初期値2})-f(x_{初期値1})}{x_{初期値2}-x_{初期値1}}}=x_{更新値1}$$ $|f(x_{更新値1})| >$ 許容誤差  ならば $x_{初期値1}$ の部分を $x_{初期値2}$ $x_{初期値2}$ の部分を $x_{更新値1}$ と改めた上で 次の 更新値2 を求める。 $$x_{更新値1}-\frac{f(x_{更新値1})}{\frac{f(x_{更新値1})-f(x_{初期値2})}{x_{更新値1}-x_{初期値2}}}=x_{更新値2}$$ 繰り返していけばそのうち $|f(x_{更新値n})| <$ 許容誤差 となり許容誤差の範囲に入る。 関数次第では収束しない場合もある。 実装1 # 実装 # 実装 def secant_method(func, initial_x1, initial_x2, r_error): count = 0 while True: count += 1 # 初期値1での許容誤差 y_f1 = func(initial_x1) if abs(y_f1) < r_error: break # 初期値2での許容誤差 y_f2 = func(initial_x2) if abs(y_f2) < r_error: break # この2つで許容誤差を満たしていない場合にアルゴリズムに入る # 更新値を求める # ニュートン法の微分の部分が差分近似される y_d = (y_f2-y_f1)/(initial_x2-initial_x1) x_new = initial_x2 - y_f2/y_d print("{}回目:x_new = {}".format(count,x_new)) # 更新 initial_x1 = initial_x2 initial_x2 = x_new 関数次第で無限ループします。 実装2で離脱する設定を追加しています。 試運転 $\sqrt{7}の近似$ 方程式(関数):$x^2-7=0$ 初期値は適当に30と28 許容誤差:0.001 # 関数部分はlambda関数で指定する secant_method(lambda x : x**2 - 7, 30, 28, 0.001) 実装2 ・countと誤差の推移のグラフを追加 ・100回で収束しない場合の撤退を追加 import matplotlib.pyplot as plt import numpy as np # 実装 def secant_method(func, initial_x1, initial_x2, r_error): """ funcの部分はlambda関数で設定する。 func = lambda x : x**7 - 7 """ count = 0 # 折線表示のために誤差結果の保存 y_f_value_list = [] while True: count += 1 # 初期値1での許容誤差 y_f1 = func(initial_x1) if abs(y_f1) < r_error: break # 初期値2での許容誤差 y_f2 = func(initial_x2) # この値から誤差リストに計上していく y_f_value_list.append(abs(y_f2)) if abs(y_f2) < r_error: break # この2つで許容誤差を満たしていない場合にアルゴリズムに入る # 更新値を求める # ニュートン法の微分の部分が差分近似される y_d = (y_f2-y_f1)/(initial_x2-initial_x1) x_new = initial_x2 - y_f2/y_d print("{}回目:x_new = {}".format(count,x_new)) # 更新 initial_x1 = initial_x2 initial_x2 = x_new # 100回tiralで収束しない場合、計算を止める if count == 100: print("近似解見つからず。") break # countと誤差の推移 x = np.arange(1, count+1, 1) # 1 ~ n回 y = np.array(y_f_value_list) # 1 ~ n回の各誤差 plt.plot(x, y) # 横軸と縦軸のラベルを追加 plt.xlabel('trial') plt.ylabel('r_error') # 許容誤差の横線 plt.hlines(r_error, 1, count,color="red",linestyles='dashed') # 折れ線と許容誤差のボーダーを一括で表示 plt.show() $x^2-7=0$ を初期値30,28から近似する。 secant_method(lambda x : x**2 - 7, 30, 28, 0.001) 実装3 実装1,2は許容誤差を高さに設定して、近似している。 横の動きの更新が浅くなったら近似解にする感じのプログラムも書いてみた。 今回、関数は最初に外に出して定義。 x_ini1:初期値1 x_ini2:初期値2 JJ:繰り返し回数 EPS:許容誤差 $\frac{|x_{new}-x_{old}|}{|x_{old}|} > EPS $ で評価する。 $|x_{new}-x_{old}|$が小さくなればなるほど収束していると考えられる。 値が極端に大きい時にも対応できるように$|x_{old}|$で正規化したものを使った。 def ff(x): return x**2-7 # 微分の差分近似を返す def df(x1,x2): return (ff(x2) - ff(x1)) / (x2-x1) def secant_method(x_ini1, x_ini2, JJ, EPS): for i in range(JJ): x_new = x_ini2 - ff(x_ini2)/df(x_ini1, x_ini2) if (abs(x_new-x_ini2)/abs(x_ini2)) < EPS: break else: x_ini1 = x_ini2 x_ini2 = x_new if i == JJ-1: print("Do not CONVERGE") else: return x_new if (__name__=='__main__'): JJ = 100 EPS = 0.001 x_ini1 = 30 x_ini2 = 28 x_sol = secant_method(x_ini1, x_ini2, JJ , EPS) print("solution:{}".format(x_sol)) まとめ 思い出してみると、以前のニュートン法でも微分は中心差分公式を使っていたので、なんか今回のはあまり意味がないかも。 手計算だと微分するかしないかで違いはあるはず。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TensorFlowのDatasetを知る

時間のない方むけ 学習データセットの設定は下記が良い。 ds = ds.shuffle(count) ds = ds.batch(batch_size, drop_remainder=True) ds = ds.prefetch(buffer_size=AUTOTUNE) テスト用はshuffleをぬいて、drop_remainderをFalseにする。 (drop_remainderはpytorchで言えばdrop_lastと同じものです) やりたいこと 大抵のニューラルネットワークの学習時には1エポックで、下記のようにバッチを取得して学習したいのではないでしょうか。下記は全20データを3バッチずつとってくるイメージ。これをtensorflowのdsではどうやったらできるか?というのが今回の目的。2021年に書くことじゃないだろ、と思うんですが、最近知ったこともあるので備忘録として・・ この記事が機能の詳細は、かなり細かく載せていますので、参考に。 TensorFlowで使えるデータセット機能が強かった話 Tutorialに従ってみる Flowerのデータセットを例にとったTutorialでのデータセットのコードを見てみましょう。 このshuffle_and_repeatというやつが、一体なにをしてくれるんでしょうか。 ds = image_label_ds.apply( tf.data.experimental.shuffle_and_repeat(buffer_size=image_count)) ds = ds.batch(BATCH_SIZE) ds = ds.prefetch(buffer_size=AUTOTUNE) 具体的に100データに対して、batch_size=16でこれを動かしてみます。 (無限ループになるので10ループでbreakしています) import tensorflow as tf AUTOTUNE = tf.data.experimental.AUTOTUNE ds = tf.data.Dataset.from_tensor_slices(tf.range(100)) ds2 = ds.apply(tf.data.experimental.shuffle_and_repeat(buffer_size=100)) ds2 = ds2.batch(16) ds2 = ds2.prefetch(buffer_size=AUTOTUNE) for i, data in enumerate(ds2): print(i) print(data) if i > 10: break あっという間に同じデータが繰り返されているのがわかります。 tensorflowの公式tutorialt通りにやると、挙動が欲しいものと違う気がします。なんでtensorflowはこの方法を一番シンプルな公式tutorialに載せてるんだろう。と少しだけ感じますが、考察は下の方に書いています。 0 tf.Tensor([49 46 72 99 1 17 78 71 89 97 48 12 55 94 35 61], shape=(16,), dtype=int32) 1 tf.Tensor([29 52 79 84 4 2 87 44 56 42 16 33 86 6 14 80], shape=(16,), dtype=int32) 2 tf.Tensor([38 70 11 25 20 5 68 81 58 9 53 24 34 63 91 90], shape=(16,), dtype=int32) 3 tf.Tensor([13 30 92 18 67 0 8 74 65 40 47 15 43 85 28 22], shape=(16,), dtype=int32) 4 tf.Tensor([88 7 96 83 31 36 41 23 37 3 51 64 19 54 62 73], shape=(16,), dtype=int32) 5 tf.Tensor([57 93 21 59 98 69 95 32 75 60 66 26 10 82 45 50], shape=(16,), dtype=int32) 6 tf.Tensor([76 27 39 77 36 45 73 99 12 11 0 57 42 51 23 9], shape=(16,), dtype=int32) 7 tf.Tensor([98 39 4 17 91 50 15 24 65 86 52 81 35 18 3 26], shape=(16,), dtype=int32) 8 tf.Tensor([33 70 89 88 67 31 6 53 55 14 97 64 84 43 54 37], shape=(16,), dtype=int32) 9 tf.Tensor([25 32 5 92 75 21 90 29 19 40 28 79 69 48 66 46], shape=(16,), dtype=int32) 10 tf.Tensor([58 74 93 82 20 96 8 95 47 30 76 85 7 94 22 2], shape=(16,), dtype=int32) 11 tf.Tensor([83 16 68 62 1 41 13 78 38 44 10 87 63 49 34 59], shape=(16,), dtype=int32) 解決策 すごくシンプルに下記を書けばOK。 これは冒頭にも書きました。シャッフルとバッチだけでいいんです。 ds = ds.shuffle(count) ds = ds.batch(batch_size, drop_remainder=True) ds = ds.prefetch(buffer_size=AUTOTUNE) なぜrepeat書いてあるのか?問題 では公式tutorialではそもそもなぜrepeatをしているのでしょう。 これは推測ですが、1epochって別に厳密な定義がなくて、1epochで何ステップ回すか、というのは自由に考えれば良いという意味だと思います。例えばaugmentationをする場合、1epochで全画像を1周することにどれだけ意味があるか?という問題があります。画像がloadされるたびに、RandomにAugmentationされるとしたら、データセット1周ってなんでしょう。 そういう観点から言っても、1epochで何step学習するかまで好きに設計して学習をしたほうが良いという、tensorflowのtutorial開発者の少し深い考え方かなぁ、と。下記のkerasのfit関数も、steps_per_epochは設定しなければcount/batch_sizeで計算されますが、設定してあげれば自由に設定が可能です。 model.fit(ds, epochs=1, steps_per_epoch=100) ちなみにrepeatを使わないdsを引き渡して、count/batch_size以上のsteps_per_epochを設定すると下記のようなエラーがでます。 29/36 [=======================>......] - ETA: 11s - loss: 4.4318 - accuracy: 0.2248WARNING:tensorflow:Your input ran out of data; interrupting training. Make sure that your dataset or generator can generate at least `steps_per_epoch * epochs` batches (in this case, 36 batches). You may need to use the repeat() function when building your dataset. 36/36 [==============================] - 53s 1s/step - loss: 4.0827 - accuracy: 0.2321 <tensorflow.python.keras.callbacks.History at 0x10c9b3750> augmentationを使わないような例では、むしろわかりづらいので、今回書いたshuffleとbatchだけを使ったほうがシンプルで良いかなぁ、と考えます。他にもなにか情報や考え方をご存じの方は是非教えて下さい。datasetを使って良いニューラルネットワークライフを!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WaniCTF'21-spring Easy Writeup

python力が問われる問題。 力づく( 総当たり × 総当たり )で解いた。 問題 Easy 166pt Easy 手始めに encrypt.pyとoutput.txtが渡される encrypt.py with open("flag.txt") as f: flag = f.read().strip() A = REDACTED B = REDACTED plaintext_space = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}" assert all(x in plaintext_space for x in flag) def encrypt(plaintext: str, a: int, b: int) -> str: ciphertext = "" for x in plaintext: if "A" <= x <= "Z": x = ord(x) - ord("A") x = (a * x + b) % 26 x = chr(x + ord("A")) ciphertext += x return ciphertext if __name__ == "__main__": ciphertext = encrypt(flag, a=A, b=B) print(ciphertext) 文字の置換に使用されているパラメータ 変数A , 変数B が消されている。 output.txt HLIM{OCLSAQCZASPYFZASRILLCVMC} FLAG{ を暗号化した結果が HLIM{ なので, 総当たりで A , B を求めた後, ciphertextに一致するplaintextを総当たりで求めるソルバーを作成する solver.py def encrypt(plaintext: str, a: int, b: int) -> str: ciphertext = "" for x in plaintext: if "A" <= x <= "Z": x = ord(x) - ord("A") x = (a * x + b) % 26 x = chr(x + ord("A")) ciphertext += x return ciphertext plaintext_space = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}" flag = "FLAG" A=0 B=0 i = 0 while i < 26: j = 0 while j < 26: ciphertext = encrypt(flag, i, j) if ciphertext == "HLIM": print("bingo!") print("A="+str(i)) print("B="+str(j)) A = i B = j j = j + 1 i = i + 1 plaintext = "" ciphertext = "HLIM{OCLSAQCZASPYFZASRILLCVMC}" for i in ciphertext: for j in plaintext_space: c = encrypt(j, A, B) if c == i: plaintext += j print(plaintext) 実行結果 >python sol_cry-easy.py bingo! A=5 B=8 FLAG{WELCOMETOCRYPTOCHALLENGE} ビンゴ! 作問者writeupみたら数学的に解いてる。       作問者writeup
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自然言語処理 Seq2Seq&TransFormer(Sequence to Sequence & TransFormer)

本書は筆者たちが勉強した際のメモを、後に学習する方の一助となるようにまとめたものです。誤りや不足、加筆修正すべきところがありましたらぜひご指摘ください。継続してブラッシュアップしていきます。 © 2021 NPO法人AI開発推進協会 本書は時系列データを別の時系列データに変換するSeq2Seqについて、RNN、LSTMからAttentionまで説明します。また、Attentionを用いた最新の様々な自然言語モデルのベースとなっているTransFormerについても説明します。(CNNの基礎を理解している前提で記載しています。まだ理解していない方は別冊のCNNの基礎を先に読んでください) Seq2Seqを基礎から理解するために、本書では以下の順番で説明を行います。最初に時系列データを扱うシンプルな構造であるRNN(Recurrent Neural Network)からはじめ、RNNを性能改善したLSTM(Long Shot Term Memory)、Encoder-Decoderモデル、そして本書の目的であるSeq2Seqの順に説明を行います。さらにSeq2Seq に劇的な進化を起こすディープラーニングにおける重要なアーキテクチャの1つであるAttentionについて説明を行います。そして、昨今の大幅に性能を向上させた自然言語処理モデルであるBERTやGTP2/GTP3のベースとなっているAttentionを活用したTransFormerモデルについて説明を行います。 【参考文献、サイト】 ゼロから作るDeep Learning2(自然言語処理編) LSTMネットワークの概要 作って理解するTransFormer/Attention 自然言語処理の必須知識TransFormerを徹底解説! Google AI Blog Transformer: A Novel Neural Network Architecture for Language Understanding Attention Is All You Need 1.RNN/LSTM (1) RNN(Recurrent Neural Network) RNNは、その名(Recurrent)の通りループする経路を持ち、時系列データであるxtを入力すると隠れ状態であるht(htには過去からの情報が記録される)を出力します。RNNの特徴は、このように1つ前の時刻の隠れ状態ht-1を利用して過去の情報を引き継げることにあります。 図 1 RNNの概要図と概要展開図   RNN層の中身は以下の図2に示すように、行列の積と和、そして活性化関数であるtanhで構成されています。 図 2 RNNの仕組み (2) RNNでの勾配消失および勾配爆発について RNNでは勾配消失や勾配爆発が内在しています。これは何層ものレイヤの逆伝播計算時に、重み(w)や活性化関数(tanh)の微分を掛け合わせるからです。 図 3 活性化関数の微分 上図3の活性化関数の微分のグラフから分かるようにtanhの微分は「0~1.0」の範囲であり、xが0から遠ざかるとその値は小さくなります。したがって逆伝播において勾配がtanh活性化を通過するたびに勾配の値はどんどん小さくなっていくため、勾配消失が発生します。 一方全結合の逆伝播(WX、WH)では、以下の図ように(この例では隠れ層であるhの勾配を示す)、上流から伝わってきた勾配dhが重みWとの行列積で伝わっていきます。 図 4 RNNの逆伝播   RNNでも数十ステップの短期依存(short-term dependencies)には対応できますが、数千ステップ以上のような長期の系列になると、活性化関数にtanhを使っているとはいえ(sigmoidよりは勾配消失・爆発が起こりにくい)、無視できないくらい勾配が小さく(もしくは大きく)なってしまい、勾配消失(爆発)が発生します。 (3) 勾配爆発への対策 勾配爆発への対策として、定番の方法として勾配クリッピング(gradients clipping)があります。 [Pascanu+ 12]において、BengioらのチームはRNNにおける勾配爆発問題が起こる必要条件がリカレント重み行列w_recの最大の特異値にあることを証明し、その明快な回避方法として以下のアルゴリズムに従う勾配クリッピングを提案しています。     If ||g^|| ≧ threshold(しきい値);     g^ = { threshold / ||g^|| } × g^ ここで「g^」は、ニューラルネットワークで使われるすべてのパラメータに対する勾配を一つにまとめているものです。上記の式はシンプルに言えば、     -threshold < ||gradient|| < threshold を実施すことを意味しています。 (4) 勾配消失への対策 勾配消失に対処するには、RNNレイアのアーキテクチャを根本から変える必要があります。この対応アーキテクチャが「ゲート付きRNN」で、様々なアーキテクチャが提案されていますが、その代表的なものが「LSTM(Long Short Term Memory)」です(他に代表的なものとしてLSTMをもう少しシンプルにしたGRU(Gated Recurrent Unit)があります)。 (5) LSTMの入出力 RNNとLSTMとの違いは、下図に示すように記憶セル(c)の入出力有無の違いです。記憶セルはLSTM専用の記録部に相当しLSTM層間のみで入出力します(上下層には渡さない)。この記憶セルが勾配消失を防ぐ鍵となっています。 図 5 RNNとLSTMの入出力   LSTMレイヤ内での記憶セルは以下のように、一種のコンベア・ベルトのようにLSTMの層(時間軸)をまっすぐに走り、情報を連鎖します。 ではなぜこの記憶セルによる勾配消失を抑止できるのか、それは学習時の逆伝播を見ればわかります。 図 6   記憶セルの逆伝播では、上図のように「+」と「×」ノードだけを通ることになります。「+」の逆伝播(微分)では上流から伝わる勾配をそのまま流すだけです。残る「×」ノードは、RNNと違い「行列の積」ではなく「アダマール積(要素ごとの積)」で、毎時系列で異なるゲート値によって要素ごとの積の計算が行われるため、勾配消失を起こさない・起こしにくくなっています。勾配消失は多層でシグモイド関数やtanh関数が重なることで起こる (例えばシグモイド関数の値域は 0 ~ 1/4 なので掛ける回数が増えると勾配が 0 に近づく) ので、非線形変換を持たない記憶セルは勾配を残しやすくなります。 記憶セルを含め、LSTMのモデル中身は下図のようになっています。 図 7   ① f(forgetゲート)   記憶セルに対して「何を忘れるのか」を明示的に支持します。   f = σ(xtWfx + ht-1Wfh + bf) ② g(新しい記憶セル)   forgetゲートだけでは忘れることしかできないため、あたらしく覚えるべき情報を記憶セルに追加します。   g = tanh(xtWgx + ht-1Wgh + bg) ③ i(Inputゲート)   g(新しい記憶セル)の各要素が新たに追加する情報としてどれだけ価値があるのかを判断(追加する情報の取捨選択)します。   i = σ(xtWix + ht-1Wih + bi) ④ O(Outputゲート)   隠れ状態(ht)は記憶セル(Ct)に対してtanh関数を適用して算出されるが、これが次の時刻の隠れ状態としてどれだけ重要かを判断します。   o = σ(xtWox + ht-1Woh + bo) 注意)f、i、oはゲートですが、gはゲートではないことに注意してください 2.Encoder-Decoderモデル Inputデータ(画像やテキスト、音声等)を何かしらの特徴ベクトル(固定長)に変換する機能をEncoderといいます。また、Encoderで生成された特徴ベクトルをデコードして新しいデータ(Inputと同じである必要はなく画像やテキスト、音声等)を生成する機能をDecoderといいます。 Encoder-Decoderモデルは文字通りEncoderとDecoderを繋げたものであり、画像 -> テキスト、音声 -> テキスト、英語 -> 日本語等、様々な生成が行えます(いわゆる生成モデル)。 図 8 3.Seq2Seqモデル Seq2Seqは「Encoder-Decoderモデル」を使って、系列データを別の系列データに変換するモデルである。適用例として、翻訳や対話モデル(チャット)の実装が可能になります。 (1) Encoder側 LSTMで最後の隠れ層のベクトルを生成するのみです。なお、LSTMでなくRNNやGRU等のモデルが使われる場合があります。 図 9   ここで、「h」はEncoderの最後の層が出力する(固定長ベクトル)隠れ状態です。これは、入力データの情報を出力データで変換するのに必要な情報が詰まった固定長ベクトルです。 (2) Decoder側 Encoderで生成された隠れ層と文字生成開始を知らせる特殊文字(下図では(他、、’_(アンダースコア)’、等何でも良い)を与え、以降は出力された文字を次のLSTM層へのインプットと生成すべき文字の数だけ繰り返します。 図 10   上記は推論時のモデルであり学習時は以下のようになります。(正解ラベルを与え損失計算が加わる) 図 11 (3) Seq2Seqの工夫 Seq2Seqの学習の効率化に向けたモデルの改善として「Reverse(入力データの反転)」と、「Peeky(特徴ベクトルの覗き見)」があります。 Reverse(入力データの反転) 単純に入力データの順序を反転させるものです。多くの場合学習の進みが早くなり精度も改善されます(この理由について論文上はソースセンテンスとターゲットセンテンスの間に多くの短期的な依存関係が導入され、最適化問題が容易になるためとされています)。 https://papers.nips.cc/paper/5346-sequence-to-sequence-learning-with-neural-networks> Peeky(特徴ベクトルの覗き見) これまでのSeq2Seqのモデルでは、Encoderが生成した固定長の隠れベクトル「h」(=Decoderにとって必要な情報がすべて詰まっている)をDecoderの最初のLSTM層のみが入力として使っていました。この隠れベクトル「h」をDecoderの各層にも与え、学習の進みと精度を改善します。これを「Peeky Seq2Seq」といいます(モデルネットワークを以下参照)。 図 12 4.Attention 「Attention」はSeq2Seqに劇的な進化を起こすディープラーニングにおける重要なアーキテクチャです。これは大雑把に言うと、Seq2SeqのEncoderに時系列での特徴ベクトルを保持し、Decoderの各層に入力データと相対するどの特徴ベクトルに注意を払うかの「Attention層」を入れ込むようなイメージです。 (1) Encoderで生成する特徴ベクトルの改良 Seq2Seqでは、Encoderが出力する固定長の隠れベクトルでは、入力データの長さにかかわらず常に同じベクトルに変換しなければなりません。これでは長い入力データではその特徴をうまく詰め込むことができません。このため入力されるデータ長に応じて生成される特徴ベクトルの長さを変える、すなわち各LSTM層で生成されるベクトルをすべて利用するように改善し、各時系列の特徴ベクトルを蓄積(hs)するようにします(下図13参照)。 図 13 (2) Decoder側での処理(重み付きベクトルとコンテキストベクトル) Decoder側では、Attention層を加え上記Encoderで生成した各時刻の特徴ベクトルを入力します。このとき、Decoder側の各層では自層の入力と対応関係にあるベクトルを選び出します(これをアライメントといいます)。 この選び出す操作は、各入力の重要度を表す重み「a」を別途計算することで実施します。この重み「a」と特徴ベクトルとの和を「コンテキストベクトル」といいます(このベクトルはその層での入力データと関係のある成分が多く含まれたベクトルになります)。 図 14   この重み「a」はデータから自動で学習します。これはDeCoder側でのLSTM層で出力された隠れベクトルhと、Encoderで生成された入力データ各層の隠れベクトルhsから求めます。 図 15   (3) 双方向RNN/LSTM これまでのRNNやLSTMは左から右へ時系列に処理しています。そのため、時系列の途中では左端から該当時間までの入力データの情報がエンコードされています。これを逆に右から左へ時系列に処理してバランスよい隠れ状態ベクトルを生成するのが双方向RNN/LSTMです。各時刻での隠れ状態のベクトルは「連結」したり、「和」や「平均」をとる方法があります(以下に連結した場合の例を示します) 図 16 (4) Attentionレイヤの配置場所 Attentionレイヤの配置はこれまでの例に示したLSTMと全結合の間で固定ではなく様々な実装のやり方があります(詳細はそれぞれのモデルの説明を参照ください。)。 (5) LSTM層の深層化とSkipコネクション LSTMの層も1層ではなく深く重ねることでより高い表現力が生成されます。この場合、EncoderとDecoderの層を同一にするのが一般的です。 なお、層を深くするときに使われるテクニックとして「skipコネクション」が様々なモデルで多用されています。層をまたいで隠れベクトルを連携することで、勾配の消失(もしくは爆発)を抑止し学習精度を上げることができます。具体的には準方向の層の跨ぎは2つの隠れ層ベクトルhの出力を加算するため、勾配は何の影響を受けることなく前層へ伝えることができます。 図 17 5.そして、Transformerの登場 Attentionの利用により精度が高まりましたが、LSTM/RNNで時系列データを逐次的に処理しているためデータの並列処理ができず高速化ができない課題は解決されていないままでした(Attention付きCNNを用いたモデルも考案されたが、長文の依存関係モデルを構築することが難しかった)。2017年にGoogle社が発表した「Attention is All You Need」という論文で、RNN/LSTMおよびCNNを用いず、「Attention」のみを用いた「Transformer」を提案し、従来のモデルで抱えていた「高速化ができない」、「精度の高い依存関係モデルを構築できない」という課題を解決しました。 Transformerは、昨今のBERTやGPT2/GPT3といった最新のNLPモデルの基礎モデルであり、またDETR(別冊で掲載予定)などの画像認識にも使われている重要なモデルです。 (1) Transformerの特徴  主なモデルの特徴としては以下になります。 RNNやCNNを使わずAttention層のみで構築 後述するSelf-Attention層とTarget-Source-Attention層のみで構築することで、並列処理が 可能となりました。 PositionalEncoding RNN/LSTMを利用しないことで失われる文脈情報を、入力する単語データの文全体での位置情報を埋め込みます。 Attention層におけるQuery-Key-Valueモデルの採用 より単語同士のアライメントをより正確に反映することができるようになりました。 (2) Tansformerの構造 TransformerはSeq2Seqと同様Encoder-Decoderモデルであり、Encoder-Decoderで異なる時系列データが入力されています。 図 18 ●Encoderの構造 ①Embedding層によって入力文章を512次元のベクトルに圧縮 ②Positional Encoder層によって位置情報を付加 ③Multi-Head Attention層でSelf-Attentionを計算し、データ内アライメントを付加 ④各種Normalizationを行う ⑤Point-wise Feed-Forward networks(PFFN:位置単位順伝搬ネットワーク)で活性化関数を適用する ⑥各種Normalizationを行う ③~⑥をN回繰り返す   ●Decoderの構造 ①Embedding層によって入力文章を512次元のベクトルに圧縮 ②Positional Encoder層によって位置情報を付加 ③Masked Multi-Head Attention層でSelf-Attentionを計算し、データ内アライメントを付加 ④各種Normalizationを行う ⑤ここまでの出力をQueryに、Encoderの出力をKeyとValueにして、Multi-Head AttentionでAttentionを計算し、異なる時系列データのアライメントを獲得 ⑥各種Normalizationを行う ⑦PFFNで活性化関数を適用 ⑧各種Normalizationを行う ③~⑧をN回繰り返す (3) 予測のための構造 Decoderの出力結果をLiner(線形変換)+softmaxで各ラベルの予測確率を計算して、予測した結果を出力します。 図 19 (4) モデルのポイントとなる各層の説明 a) PositionalEncoding層 Transformerでは、RNNやCNNを使わないので文脈情報(単語同士の長期依存関係)を取得できないため単語列の語順(位置情報)を明示的に付与する必要がある。PositionalEncodingは、各要素に「n番目」というような文中の位置が一意に定まる情報を付与する。実際には、周波数が異なるSin関数・Cos関数の値をEmbedding後の文(input, output)の行列に加算することで、各単語と文脈(位置)情報を考慮した情報を取得する。    PE(pos,2i) = sin(pos/100002i/dmodel) …①    PE(pos,2i+1) = cos(pos/100002i/dmodel) …②    ※posはセンテンス中の出現位置 (0,1,...,T)、iは次元数(単語の分散表現の次元数) 図 20     出展:How to code The Transformer in Pytorchを参照に加筆 b) Self-Attention層 Self-Attentionは、Seq2Seqなどで実装されていた異なる時系列データ間で実施されていたAttention処理を入力データ内の単語同士で実施することで、個々のデータがデータ全体のアライメントを並列的に参照するため、より広範囲でのアライメントを把握するようにしたもの。  ex.   ・従来のAttention    I have a cat. ←→ 私は猫を飼っています    -> このとき、例えば「I」は、特に「私」や「飼っています」とのアライメントを獲得する   ・Self-Attention    I have a cat. ←→ I have a cat.    -> このとき、例えば「I」は、特に「I」や「have」とのアライメントを獲得する Self-Attentionを行うことで同一センテンス内の類似度が獲得され、特に多義語や代名詞などが実際に何を指しているのかを正しく解釈できるようになった。  ex.   ・The animal didn’t cross the street because it was too tired.    → このときの「it」は「animal」   ・The animal didn’t cross the street because it was too wide.    → このときの「it」は「street」 図 21     出展 Transformer: A Novel Neural Network Architecture for Language Understanding 実装の基本となるのは、queryとmemory(key, value)である。 queryによってmemoryから必要な情報を選択的に引っ張ってくるが、queryはkeyによって取得するmemoryを決定し対応するvalueを取得する。一方、学習は元データ(memory)と検索をかけたい入力(input)の関連度を学習するだけである。一般的には下記のフローとなる。 図 22 ① Input、memory input(query)は入力時系列、memoryは引いてくる情報の時系列。 ベクトルのlengthは文章中のトークンの長さ(例えば1文の中の最大単語数等)、depthは各単語をEmbeddingしたときの次元数。  ex.   input  :「好き」「な」「動物」「は」   memory:「猫」「が」「すき」   このとき、input(query)のlengthは4、memoryのlengthは3となる。 また、「depth」はdepth次元の単語の分散表現(n次元のベクトル、Transformerのデフォルトでは512次元)にしたものとなる。 ② Query-Key-Value inputデータは全結合層(Dense)でquery(ベクトル)に、memoryは2つの全結合層(Dense)でそれぞれkey(ベクトル)、value(ベクトル)に変換されます。このとき入力データをα、depthの次元をn、重みをW(それぞれWq、Wk、Wv)とすると以下のように計算されます。     Qn = Wq × αn     Kn = Wk × αn     Vn = Wv × αn ★Key-Valueモデルについての詳細は補足を参照ください   ③ attention weight queryは、memoryのどこから情報を引いてくるのかを決めるためにkeyを使います。例えば、「動物」というqueryに対して、「「猫」がxx%、「が」がyy%、「好き」がzz%」というような計算をします。具体的には、queryとkeyとの行列積(matmul)を行います。行列積をとった後はsoftmaxにより確立に落としていきます(★この辺りは従来のAttentionと同じ考え方)。 図 23   ・Additive Attention 重みをかけたQueryとKeyを加算したものを活性化関数で出力することで求めます。メリットとしてQueryとKeyとの次元が異なっていても問題がないことです。 ・Dot-Product Attention(Multiplicative Attention) QueryとKeyとの内積(Dot-Product)で求める方法です。一般的にAdditive Attentionと比べパラメータが必要なく高速です。ただし、depthの次元数が大きくなりすぎると内積が大きくなりすぎて、逆伝搬することがうまくできなくなります。このためTransformerでは、depthの次元数の平方根でスケーリングする「Scaled Dot-product Attention」が用いられています。 ※この式は図22の③、④を実装したものになります   ④ attention weightに従ってvalueから情報を引き出す attention weightとvalueの行列積を取ることで、重みに従ってvalueの情報を取得します。 図 24 最後に得られたベクトルを全結合(Dense)で変換したものがoutput(出力ベクトル)となります。 c) Multi-Head Attention層 各単語に対してヘッド数分だけのQuery、Key、Valueの組をつくり、それぞれのヘッドで異なる組を用いて潜在表現を計算する方法。最終的にそれらを1つのベクトルに落とすことで、ある単語のもつ潜在表現とします(アンサンブル学習のようなもの)。 具体的には、それぞれのheadの潜在表現を結合(concat)し、それを重みで掛け算することで元の次元に戻してその層のOutputとします。 一般的にはMultiHeadの方がSingleHeadよりも高い性能が出るが、これはSingleHeadで深く潜在変数を処理するよりも、ヘッドが異なれば処理している潜在表現空間も異なるという事実からMultiHeadで複数の潜在表現を処理してまとめる方がより広範囲に豊かな情報を取ってくることが可能なためです。 図 25     出展:Attention Is All You Need d) Masked Muiti-Head Attention層 訓練時のデコーダでは、全ターゲット単語を同時に入力し全ターゲット単語を同時に予測します。このため、入力した単語が先読みを防ぐために情報を遮断するマスクをするオプションがあります(評価/推論時は、逐次的に単語列を生成するので必要はありません)。 例えば3番目の単語を予測する際には最初と2番目のみ使用、4番目の単語を予測する際には1番目、2番目、3番目の単語のみ使用するようにします。 具体的には、maskは特定のkeyに対してattention_weightを0(重みゼロ)にすればその項目はマスクされます(ただし、attention_weightはsoftmaxの出力なので、その入力であるlogitに対しては-∞に値を置き換えることになる)。 なお、マスクは先読みを防ぐ目的以外にも、PADを無視するためにも使います。以下に例を示します。  ex. Keyの文章が以下の場合、長さがバラバラではベクトルに変換できないのでPAD(パディング)を追加する。 このとき、PADを無視させるためマスクする。   Key_length = 3のときのPADの例    [      おはよう /<PAD>  / <PAD>      猫    /が    /すき      お腹   /すいた  / <PAD>    ] 図 26    出展:Attention Is All You Need e) Source-Target-Attention層 Self-Attentionと同様にQuery-Key-Value構造を持ちますが、Source-Targer-Attentionは、異なるデータ間のアライメントを獲得します(従来のAttention)。KeyとValueはEncoderの隠れ層(Source)からきて、QueryはDecoderの隠れ層(Target)からきます。 これはTansformerの構造図の⑤‘部分が該当します。 図 27 【補足】Key-Valueモデル これまでのAttentionでは、Target、Sourceという2つの変数からアライメントを見ていました。 Transformerでは前述のとおり、Target=Query、Source=Key、Valueという3つの変数からアライメントをより適切に反映させています。これはアライメントをただ持たせるだけでなく辞書表現のように探索用(Key)と内実用(Value)に分離することで、より適切な表現力が得られるという考えに基づいています。 図 28   出展:FRUSTRATINGLY SHORT ATTENTION SPANS IN NEURAL LANGUAGE MODELING 従来のAttentionで行われていたアライメントの探索の仕方は、  ①EncoderとDecoderで獲得された各々の固定長ベクトル同士の類似度を算出し  ②類似度が高いものをより強いアライメントであると判定 します。 このとき、「固定長ベクトルとは各単語の特徴を表すもとである」ことが成立していることが前提となっています。これは厳密に単語同士を分離することは言語学的にも不可能であるというのが理由になっています。例えば、「Tom」とは何か、という説明をするためには他の単語(「human」など)の助けが必要、つまり各単語そのものがほかの単語と共有している何かがあるために成り立っています(このような発想のもと分散表現などが考案されている)。 しかし、それでもある単語は他の単語とは異なるものとして存在している以上、それらを分離することのできる単語の核ともいうべき特徴があるはずであり、この微妙な関係性を表現したのが「Key」と「Value」という辞書オブジェクトの組み合わせであり、このことがより正確に単語同士のアライメントを参照することを可能にしました。 以下に、各単語の類似度からアライメントを判定することの問題点、Key-Valueに分類することの利点を説明します。 「直接単語の特徴を参照する」ことで生じている問題をイメージするために、例えば質疑応答に関する学習について考えてみるとわかりやすくなります。  情報: Yoshi is 18years-old.  質問: Who is 18yeass-old in fdit-team?  答え: Yoshi. このとき、正しい答えを導くために2つの動作が行われていると考えることができます。  ① 質問に対する探索  ② (質問に正しく応答している)答えに対する探索 重要なことは「質問に答えは含まれていない」ということです。このとき、従来のKeyとValueが一致している場合、質問に含まれる単語(18years-oldなど)の特徴に類似しているものを探索して答えを見つけ出していました。この場合、Tomと18years-oldの間には強いアライメントがあるため、18years-oldだけの特徴を用いて質問を探し答えたとしても多くの場合は正解することができます。 しかし、本来18years-oldとYoshiは別の概念であり、単語同士の類似性のみを根拠にして答えとすることは、全体データが大きくなればなるほど不安定になってきます(例えば、類似性だけであれば19years-oldのような表現の方が近いかもしれない)。このときYoshiの内実の特徴(Yoshi自身を示す特徴)だけでなく、18years-oldとの繋がりを示す特徴があれば、それを参照することでより正確にYoshiにたどり着くことができます。加えて、whoなどとの繋がりを示す特徴を持つことでより正確に近づいていくことができます。 図 29   出展:Key-Value Memory Networks for Directly Reading Documents つまり、Keyとは他の単語との関係性を示す(分散表現のベクトルもの)、Valueはその単語そのものを示す(分散表現のベクトル)ものといえます。無鉄砲に荒野から自分と似ているというValueを探すよりも、Keyという目印に従ってValueという答えを得る方が、より早く・より適切な答えを見つけることができるようなイメージであり、KeyとValueを設定することで高い表現力を獲得できます。Key-Valueモデルを提唱した研究チームは「Keyは質問に一致させるのを助ける特徴を持つべきであり、Valueは応答に一致させるのを助ける特徴を持つべきである」と述べています。   (ⅰ)自分のタスクに関する事前知識を符号化するためのより大きな柔軟性を   (ⅱ)keyとValueの間の非自明な変換を介してモデルのより効果的なパワーの両方を得る ことができるとしています。 このようにKeyとValueを明確に分離することでより高い表現力を獲得することが可能となりました。 おわりに Seq2SeqやAttentionを応用した様々なモデルが存在しますが、基本的な内容は本説明で記載したとおりです。Transformerは物体検出にも適用され始めているので(DETR:DEtection TRnsformer)、今後調べて執筆予定です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

第3回 今更だけどしっかり基礎から強化学習を勉強する 価値推定編(TD法、モンテカルロ法、GAE)

今回はモデルフリーの環境における価値の推定(評価)手法の話がメインとなります。 第2回 ディープラーニング編(Q学習、方策勾配法/REINFORCE、A3C/A2C) ※ネット上の情報をかき集めて自分なりに実装しているので正確ではない可能性がある点はご注意ください コード全体 本記事で作成したコードは以下です。 GoogleColaboratory 環境(CartPole-v0) 第2回と同じ CartPole-v0を使っていきます。 学習コード概要 第2回のA3C/A2Cアルゴリズムをベースに価値関数の評価(推定)手法を見ていきます。 まずはメインとなるコードを作成し、手法に影響ある部分だけを変えて実装していきます。 今回のモデルは以下です。 Actor側の出力を softmax にしています。 そのままだと学習がうまくいかなかったので前段階にDense層をはさみました。 学習のメインコードは以下です。 import gym import numpy as np import tensorflow as tf from tensorflow.python import keras from tensorflow.keras.optimizers import Adam # softmaxでアクションを決定 def SoftmaxPolicy(model, state, nb_actions): action_probs, _ = model(state.reshape((1,-1))) return np.random.choice(list(range(nb_actions)), size=1, p=action_probs[0].numpy())[0] with gym.make("CartPole-v0") as env: nb_actions = env.action_space.n obs_shape = env.observation_space.shape # モデルの定義 c = input_ = keras.layers.Input(shape=obs_shape) c = keras.layers.Dense(10, activation="relu")(c) c1 = keras.layers.Dense(10, activation="relu")(c) actor_layer = keras.layers.Dense(nb_actions, activation="softmax")(c1) critic_layer = keras.layers.Dense(1, activation="linear")(c) model = keras.Model(input_, [actor_layer, critic_layer]) model.compile(optimizer=Adam(lr=0.01)) # 経験バッファ用 experiences = [] # 学習ループ for episode in range(300): state = np.asarray(env.reset()) done = False total_reward = 0 # 1episode while not done: # アクションを決定 action = SoftmaxPolicy(model, state, nb_actions) # 1step進める n_state, reward, done, _ = env.step(action) n_state = np.asarray(n_state) total_reward += reward # 経験を追加 experiences.append({ "state": state, "action": action, "reward": reward, "n_state": n_state, "done": done, }) state = n_state ※(a)1step毎に学習する場合の処理を記載 ※(b)1episode毎に学習する場合の処理を記載 print("episode: {}, reward: {}".format(episode, total_reward)) 各手法において、毎ステップ学習する手法の場合は(a)に、1エピソード毎に学習する手法は(b)にコードを記載します。 次にモデル学習側のメインコードです。 (c)に各手法での推定価値の計算を記述します。 その後、推定価値は正規化をおこなっています。 正規化は強化学習の話ではなく機械学習全般の話ですね。 ここでは平均0、分散1になるように正規化しています。 def train(model, experiences): ※(c) 各手法で、推定価値(v_val)を計算 # 推定価値の正規化(平均0,分散1) std = v_val.std() if std != 0: v_val = (v_val - v_val.mean()) / std # データ整形 states = np.asarray([e["state"] for e in experiences]) actions = np.asarray([e["action"] for e in experiences]) # actionをonehot化 onehot_actions = tf.one_hot(action_batch, nb_actions) # 勾配を計算 with tf.GradientTape() as tape: action_probs, v = model(state_batch, training=True) # π(a|s)を計算 selected_action_probs = tf.reduce_sum(onehot_actions * action_probs, axis=1, keepdims=True) # log(π(a|s)) * Q(s,a) を計算 selected_action_probs = tf.clip_by_value(selected_action_probs, 1e-10, 1.0) # 0にならないようにclip policy_loss = tf.math.log(selected_action_probs) * advantage # Value loss value_loss = tf.reduce_mean((v_val - v) ** 2, axis=1, keepdims=True) #--- 方策エントロピー entropy = tf.math.log(selected_action_probs) * selected_action_probs entropy = tf.reduce_sum(entropy, axis=1, keepdims=True) #--- total loss value_loss_weight = 1.0 entropy_weight = 0.1 loss2 = -policy_loss + value_loss_weight * value_loss - entropy_weight * entropy # ミニバッチ処理(平均をlossに) loss = tf.reduce_mean(loss2) # 勾配を元にoptimizerでモデルを更新 gradients = tape.gradient(loss, model.trainable_variables) model.optimizer.apply_gradients(zip(gradients, model.trainable_variables)) 価値関数の推定の仕方 第1回でも話しましたが強化学習は現在の価値を推定し、その差分を価値関数または方策関数という形で学習していく手法となります。 現在の価値の推定にはベルマン方程式が使われますが、未来の報酬 $V_\pi(s_{t+1})$ がありそのままでは計算できません。 今回はこの価値の推定方法についてまとめてみました。 TD法 1step進めた結果を元に価値を予測する手法です。 ExperienceMemoryと相性がよく、Q学習でよく使われる手法ですね。 現在の価値は以下の式で推定されます。 $$ V(s_t) = r_{t+1} + \gamma V(s_{t+1}) $$ 次の状態の価値 $V(s_{t+1})$ が推定値となり、現在の価値関数から予測した値となります。 コードは以下です。 batch_size = 16 # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (a) batchサイズたまったら学習 if len(experiences) >= batch_size: train_TD(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_TD(model, experiences): gamma = 0.9 # 次の状態の推定価値を取得 _, n_v = model(np.asarray([e["n_state"] for e in experiences])) n_v = n_v.numpy() # 各経験毎に価値を推定 v_vals = [] for i, e in enumerate(experiences): if e["done"]: # 終了時は次の状態がないので報酬のみ r = e["reward"] else: r = e["reward"] + gamma * n_v[i][0] v_vals.append(r) v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 (以下、学習コード) 学習結果です。 On-policy 手法とは相性が悪く学習できていませんね。 モンテカルロ法 1エピソードの結果を元に価値を推定する手法です。 推定といっていますが、実際に観測した報酬を使って推定しています。 TD法であった次の状態価値の推定値 $V(s_{t+1})$ が無くなっているのが特徴です。 $$ V(s_t) = r_{t+1} + \gamma r_{t+2} ... + \gamma^{T-t-1} r_{T-t} $$ # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (b) 1episode毎に処理する if len(experiences) >= batch_size: train_MC(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_MC(model, experiences): gamma = 0.9 # 各経験毎に価値を推定、後ろから計算 v_vals = [] r = 0 for e in reversed(experiences): if e["done"]: # 終了時は次の状態がないので報酬のみ r = e["reward"] else: r = e["reward"] + gamma * r v_vals.append(r) v_vals.reverse() # 反転して元に戻す v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 (以下、学習コード) 価値の計算は後ろから計算する事で計算しやすくなります。 学習結果です。 Multistep lerning モンテカルロ法では1エピソード全ての経験が対象でしたが、n-step先を対象に価値を推定する手法が Multistep lerning です。 1-stepの場合はTD法と同じになり、エピソード長と同じステップならモンテカルロ法と同じになります。 $$ V(s_t) = r_{t+1} + \gamma r_{t+2} ... + \gamma^{t+n-1} V(s_{t+n}) $$ n_step = 16 # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (a) N step毎に処理する if len(experiences) >= n_step: train_ML(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_ML(model, experiences): gamma = 0.9 # 各経験毎に価値を推定、後ろから計算 if experiences[-1]["done"]: # 最後が終わりの場合は全部使える r = 0 else: # 最後が終わりじゃない場合は予測値vで補完する _, n_v = model(np.atleast_2d(experiences[-1]["n_state"])) r = n_v[0][0].numpy() v_vals = [] for e in reversed(experiences): if e["done"]: r = 0 r = e["reward"] + gamma * r v_vals.append(r) v_vals.reverse() # 反転して元に戻す v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 (以下、学習コード) 学習結果です。 TD(λ)法 上記3つの手法における各stepでの推定価値ですが、本質的には同じ価値を推定しているので重みを使って平均化することができます。 どういうことかというとあるタイミングの価値はTD法だと (1-step価値)、モンテカルロ法だと (エピソード最後までの価値) ですが、全stepを足した平均 (1-step価値 + 2-step価値 + ... + エピソード最後までの価値) を使っても問題ないという考えです。 各step $n$毎の推定価値 $G_t^n$ は以下です。 \begin{align} 1step &: \ G_t^1 = r_{t+1} + \gamma V(s_{t+1}) \\ 2step &: \ G_t^2 = r_{t+1} + \gamma r_{t+2} + \gamma^2 V(s_{t+2}) \\ nstep &: \ G_t^n = r_{t+1} + \gamma r_{t+2} + ... + \gamma^{n-1} r_{t+n} + \gamma^n V(s_{t+n}) \\ episode end &: \ G_t^{T-n} = r_{t+1} + \gamma r_{t+2} + ... + \gamma^{T-t} r_{T-t} \\ \end{align} TD(λ)法ではこれら各stepに $\lambda^{n-1}$ に比例して重みづけをします。 $$ G_t^1 + \lambda G_t^2 + \lambda^2 G_t^3 ... $$ ここで $1 + \lambda + \lambda^2 + ... \to \frac{1}{1-\lambda}$ なので正規化するために $1-\lambda$ を掛けます。 すると以下となります。 $$ G_t^\lambda = (1-\lambda)(G_t^1 + \lambda G_t^2 + ... + \lambda^{T-n-2} G_t^{T-n-1}) + \lambda^{T-n-1} G_t^{T-n}$$ エピソード最後の $G_t^{T-n}$ に $1-\lambda$ がかかっていないのは、 $$(1-\lambda)(1 + \lambda + \lambda^2 + ... + \lambda^{T-t-2}) = 1- \lambda^{T-t-1}$$ になるからです。 各stepの重み $\lambda$ ですが $\lambda=0$ の場合TD法と同じになり、$\lambda=1$ の場合モンテカルロ法と同じになります。 強化学習について学んでみた。(その24) 強化学習理論の基礎2 コードは以下です。 # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (b) 1エピソード毎に処理する if len(experiences) >= n_step: train_TDh(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_TDh(model, experiences): gamma = 0.9 td_lambda = 0.9 # 次の状態の推定価値を出しておく _, n_v = model(np.asarray([e["n_state"] for e in experiences])) n_v = n_v.numpy() v_vals = [] for i in range(len(experiences)): # 各step r = 0 t = 0 G = 0 for j in range(i, len(experiences)-1): # 割引報酬を計算 r += (gamma ** t) * experiences[j]["reward"] t += 1 # 推定価値を計算 G = (td_lambda ** (t-1)) * (r + (gamma ** t) * n_v[j][0]) # G全体の計算とエピソード最後の報酬 G = (1 - td_lambda) * G + (r + (gamma ** t) * experiences[-1]["reward"]) v_vals.append(G) v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 (以下、学習コード) 学習結果です。 方策勾配法における価値関数の推定 方策勾配法では行動価値 $Q^{\pi_\theta}(s,a)$ を推定する必要があります。 $$ \nabla J(θ) \propto E_{\pi_\theta} [ log \pi_{\theta}(a|s) Q^{\pi_\theta}(s,a) ] $$ 基本的にこの推定方法はTD法等上記で紹介した方法で推定しても問題ありません。 ただ、より学習を安定するためにいくつか手法がるようです。 参考 Going Deeper Into Reinforcement Learning: Fundamentals of Policy Gradients Baseline 方策勾配法では分散を小さくすることが重要となります。(機械学習全般にいえますが) 分散が大きいと学習後の方策が大きく異なってしまい、性能が大きく向上する可能性もありますが大きく低下する可能性もあります。 分散を小さくする手法の1つがBaseline $b(s_t)$ の導入です。 $$ \nabla J(θ) \propto E_{\pi_\theta} [ log \pi_{\theta}(a|s) (Q^{\pi_\theta}(s,a) - b(s_t))] $$ Baseline $b(s_t)$ はどんな値をつかってもいいのですが、実装では価値の平均を採用しています。 強化学習 森村哲郎・著 シリーズ: 機械学習プロフェッショナルシリーズ 言語生成の強化学習をやっていく(手法紹介 REINFORCE編) コードは以下です。 経験の収集はモンテカルロ法で実装しています。 # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (b) 1エピソード毎に処理する if len(experiences) >= n_step: train_B(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_B(model, experiences): gamma = 0.9 # 各経験毎に価値を推定、後ろから計算 v_vals = [] r = 0 for e in reversed(experiences): if e["done"]: # 終了時は次の状態がないので報酬のみ r = e["reward"] else: r = e["reward"] + gamma * r v_vals.append(r) v_vals.reverse() # 反転して元に戻す v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 # ベースラインとして平均を引く v_vals -= np.mean(v_vals) (以下、学習コード) 学習結果です。 Advantage function Advantage functionは以下で定義されます。 $$ A^{\pi}(s,a) = Q^{\pi}(s,a) - V^{\pi}(s) $$ この Advantage function を用いて更新する手法となります。 $$ \nabla J(θ) \propto E_{\pi_\theta} [ log \pi_{\theta}(a|s) A^{\pi}(s,a)] $$ Baselineの $b(s)$ を $V^{\pi}(s)$ にした物と同じですね。 コードは以下です。 経験の収集はモンテカルロ法で実装しています。 # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (b) 1エピソード毎に処理する if len(experiences) >= n_step: train_Adv(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_Adv(model, experiences): gamma = 0.9 # 各経験毎に価値を推定、後ろから計算 v_vals = [] r = 0 for e in reversed(experiences): if e["done"]: # 終了時は次の状態がないので報酬のみ r = e["reward"] else: r = e["reward"] + gamma * r v_vals.append(r) v_vals.reverse() # 反転して元に戻す v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 # Vを引く _, v = model(np.asarray([e["state"] for e in experiences])) v_vals -= v.numpy() (以下、学習コード) 学習結果です。 GAE Advantage function でも未来の価値をどう推定するかは決まっていませんでした。(上記実装ではモンテカルロ法で推定しています) GAE(Generalized Advantage Estimator)では未来の価値をTD誤差を元に推定する方法となります。 TD誤差 $\delta_t^V$ は以下です。 $$ \delta_t^V = r_t + \gamma V(s_{t+1}) - V(s_t)$$ これを元にTD(λ)法のように各ステップをλで重みづけした手法がGAEとなります。 \hat{A}_t^{GAE(\gamma, \lambda)} = (1 - \lambda)(\delta_t^V + \lambda (\delta_t^V + \gamma \delta_{t+1}^V) + \lambda^2 (\delta_t^V + \gamma \delta_{t+1}^V + \gamma^2 \delta_{t+2}^V) + ... ) この式を解くと以下になります。 \hat{A}_t^{GAE(\gamma, \lambda)} = \sum^{\infty}_{l=0}( \gamma \lambda)^l \delta^V_{t+l} High-Dimensional Continuous Control Using Generalized Advantage Estimation(論文) Notes on the Generalized Advantage Estimation Paper 一般化報酬による高次元の強化学習の論文を読む OpenAIのSpinning Upで強化学習を勉強してみた その4 n_step = 16 # 学習ループ experiences = [] for episode in range(300): # 1episode while not done: (略) experiences.append(経験) # (a) n-step毎に処理する if len(experiences) >= n_step: train_GAE(model, experiences) # On-policyアルゴリズムは学習毎に方策が変わるので、経験は学習毎に初期化する experiences = [] (略) def train_GAE(model, experiences): gamma = 0.9 gae_lambda = 0.9 # 推定価値をだす _, v = model(np.asarray([e["state"] for e in experiences])) _, n_v = model(np.asarray([e["n_state"] for e in experiences])) v = v.numpy() n_v = n_v.numpy() # 逆から計算する last = 0 v_vals = [] for i in reversed(range(len(experiences))): e = experiences[i] if e["done"]: delta = e["reward"] - v[i] last = 0 else: delta = e["reward"] + gamma * n_v[i] - v[i] last = delta + gamma * gae_lambda * last v_vals.append(last) v_vals.reverse() v_vals = np.asarray(v_vals).reshape((-1, 1)) # 整形 (以下、学習コード) 学習結果です。 あとがき 結果ですが、環境側の報酬や使うアルゴリズムによってかなり性能が変わります。 参考程度ですね。 価値推定方法は各手法に既知として組み込まれている場合が多く理解するまでに苦労しました。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OCR モデル PGNet で SageMaker 上に日本語 OCR を構築

効率的な OCR モデル PGNet OCR モデル PGNet を解説し、SageMaker でエンドポイントを作ってみます。 とにかく使ってみたい方向けに gist にコードを上げています。 概要 Point Gathering Network (PGNet) は、Point Gathering という演算を使うことで、OCR を実現するための様々な認識モデルの組み合わせを回避し、高速化を図ったモデルです。AAAI-21 で採択されています。 P. Wang, C. Zhang, F. Qi, S. Liu, X. Zhang, P. Lyu, J. Han, J. Liu, E. Ding, G. Shi PGNet: Real-time Arbitrarily-Shaped Text Spotting with Point Gathering Network https://arxiv.org/abs/1507.05717 図1 にその成果が書かれていて分かりやすいですね。SOTAである ABCNet [Liu, et al., CVPR2020] よりも速く動作することが分かります。 従来研究との比較 図2が分かりやすいので書き加えてみました。 まず緑の枠で囲ったネットワークは、物体検出でもよく使われる ROI (Region of Interest) 系の演算を行ってテキスト領域を認識し、その後、実際にテキストを認識するため全体の計算時間が長くなります。論文中では two-stage の手法と呼んでいます。 一方、提案手法はテキスト領域の認識とテキストの認識を一括で行う方法を提案しています。これはオレンジで囲んだ CharNet [Xing et al., ICCV2019] と同じ枠組みです。しかし、CharNet は図中の GT: W, C のところに着目すると、文字単位 (C) でのアノテーションが必要であり、一方 PGNet は単語単位 (W) でのアノテーションのみで良いことからデータ準備の面で有利です。 PGNet の全体像 図3から説明していきましょう。まず図左にあるように画像から特徴抽出を行います。特徴抽出には物体検出によく使われるFPN(Feature Pyramid Networks)が利用されます。抽出した特徴 $F_{visual}$ は元の画像サイズの 1/4 のサイズになります。続いてこの $F_{visual}$ から以下の情報を求めるネットワークを学習します。図で上から順だと説明しにくいので、少し順番をかえて説明します。 Text Center Line (TCL) TCL はテキストの中心領域を表していて、テキストの中心領域かどうかの1次元スコアが各ピクセルに入っています(1チャネル) Text Border Offset (TBO) TCL の各ピクセルからテキスト領域の上側の点までの距離 (x方向とy方向)と下側の点までの距離(x方向とy方向) が、各ピクセルに入っています (4チャネル) Text Direction Offset (TDO) テキストの方向が入っています。2次元で方向を表すので2チャネルです。 Text Character Classification Map (TCC) 各ピクセルがどの文字を表しているかを示します。37文字の英数字なら37チャネルです。 これらを推定するネットワークは非常に簡単で、それぞれConv+Batch Normalization になっています。詳細は github のコード を見てみてください。 このうち TBO はそのままテキスト領域の検出に使われ、それ以外の部分はテキスト認識に利用されます。テキスト認識は次で説明する Point Gathering で行います。 Point Gathering (PG) あるテキスト領域に対する PG について、論文中では以下のような式で説明されています。 $$ P_\pi = gather(TCC, \pi) $$ $\pi$ は TCL で予測した N 個の点を TDO の方向に従って並べ替えたもの、TCC は上で求めた各文字の確率に関するマップです。これらを使って、N 個の点に対応する各文字の確率 $P_\pi$ を求めます。gather というのがよくわかりませんでしたが、 こちらのコードを参考にすると、N 個の点の確率分布を TCC から取ってくるだけのようです。TCC はConv+Batch Normalizationの結果をそのまま使う方法だけでなく、$F_{visual}$ と同時に Graph Convolution を使って精度を改善する方法も提案されています (3.5節)。 学習時にはこの$P_\pi$と正解のテキスト $L_i$ との比較でロスを計算します。M個のテキストが認識されたら、それぞれのロスの総和をとります。 L_{PG−CTC}=∑_{i=1}^M CTC\_loss(P_{\pi_i}, L_i) ここでは CTC (Connectionist Temporal Classification) ロスというのを使っています。CTC ロスの説明は towardsdatascience.com がわかりやすいと思います。なぜ CTC を使うかというと、実は学習データが文字単位でのアノテーションを行っていないので、文字を比較してロスを計算することができないからです。例えば、OCR という単語を認識しようとしたとき、"OCC R"みたいに認識されるかもしれません。CC のように同じ文字が連続して認識されたり、空白ができたりしてしまいます。CTC ロスは、重複する単語を除去したり、空白を除去したりする CTC 特有の操作 (CTC Decoding) を想定したロスになっています。この $L_{PG−CTC}$ と、それ以外に TCL, TBO, TDO のためのロスも計算して、それぞれを重み付けしたロスでネットワーク全体を学習しています。 推論するときは gather 関数を使って $P_\pi$ を求めたら、それに対して、重複する単語を除去したり、空白を除去したりする CTC Decoding をかけるだけです。 R_\pi = CTC\_Decoder(P_\pi) SageMaker へデプロイして日本語OCRを作る PGNet を開発している Baidu が日本語を含む多言語OCRモデルを公開しており、PaddleOCRという形で簡単に利用できるようにしています。 PaddleOCR https://github.com/PaddlePaddle/PaddleOCR 実はフレームワークは TensorFlow とかではなく、Baidu 独自の Paddle (PArallel Distributed Deep LEarning) とよばれるフレームワークです。とはいえコンテナイメージまで公開されており、python での API も用意されているので SageMaker に乗せることはできそうです。 以下では SageMaker ノートブックインスタンスで実行することを想定しています。インスタンスタイプは t2.medium で良いですが、コンテナイメージのビルドに容量を使うので、ディスクサイズを 50GB くらいにしておいてください。 例によって面倒な方は gist のノートブックをダウンロードしてそのまま実行できます。 PaddleOCR を動かす SageMaker コンテナイメージの準備 ビルドを home で行うように設定 これからビルドする Docker イメージはなかなか大きく、デフォルトの /var/lib/docker ではディスクサイズが足りずビルドに失敗します。そこで以下を最初に実行して、home でビルドするようにしましょう。 !sudo /etc/init.d/docker stop !sudo mv /var/lib/docker /home/ec2-user/SageMaker/docker !sudo ln -s /home/ec2-user/SageMaker/docker /var/lib/docker !sudo /etc/init.d/docker start コンテナイメージ作成に必要なファイル群 今回は Paddle というフレームワークを使うので、SageMaker で用意したコンテナではなく自作して使います。作り方は以下のサンプルが参考になります。 必要なファイルは以下のとおりです。 Dockerfile Paddle が使える環境を用意 Python からも利用できるよう pip で Paddle の Python ライブラリをインストール それ以外は上記のサンプルに従って、必要なデプロイ用のライブラリをインストール dockerd-entrypoint.py 特に編集不要 model_handler.py を実行するように実装されている model_handler.py 日本語の OCR モデルをロードする処理を実装する 画像を受け取ったら、ロード済みのモデルで推論して、結果を返す実装をする Dockerfile 長くなるので全体は gist を見てください。 今回は GPU を使って推論するイメージを作成します。PaddleOCR を GPU で動かすためには以下が必要です。 Cuda 10.2 cuDNN v7.6 glibc 2.2.3 (Ubuntu 16.04 で利用可能) そこで、これを利用可能なベースイメージを取得するよう変更しましょう。ubuntu:16.04のままだと CPU しか使うことができません。 FROM nvidia/cuda:10.2-cudnn7-devel-ubuntu16.04 Python のバージョンを上げておきます。 ARG PYTHON_VERSION=3.7.10 あとは python から利用できるよう paddle のライブラリを pip でインストールします。 # install paddleocr RUN pip3 install paddlepaddle-gpu==2.0.2 RUN pip3 install "paddleocr>=2.0.1" dockerd-entrypoint.py こちらは変更点がないので説明を省略します。 model_handler.py 実際にモデルで推論する際の挙動を記述します。その挙動は、グローバルの関数の handle(data, context) で書かれているように 初期化されていなければ initialize で初期化する (モデルをロードするなど) データがくればhandleで予測した結果を返す となっています。つまり、initialize と handle を実装すればOKです。initialize は以下のようにモデルをロードする処理を書きます。SageMaker は S3 にモデルをおいておけば、それを /opt/ml/model/ においてくれますので、そこに置かれた各種モデルを読むようにします。S3へのモデル配置はこのあと行います。 self.initialized = True model_dir = "/opt/ml/model/" # Load ocr model try: self.ocr = PaddleOCR(det_model_dir=os.path.join(model_dir,'model/det'), rec_model_dir=os.path.join(model_dir,'model/rec/ja'), rec_char_dict_path=os.path.join(model_dir,'model/dict/japan_dict.txt'), cls_model_dir=os.path.join(model_dir,'model/cls'), use_angle_cls=True, lang="japan", use_gpu=True) handle は preprocess と inference にかけます。 def handle(self, data, context): model_input = self.preprocess(data) model_out = self.inference(model_input) return model_out それぞれの実装は以下のとおりです。preprocess では base64 形式のデータを想定して、1枚の画像を numpy array に変換してリストにしています。これは PaddleOCR が numpy を受け取るのでそのようにしています。ocr.ocr で実行した予測結果 result は、[テキスト領域の座標群, (テキスト、スコア)] となります。このうち、スコアはfloat32で json に変換できないので float64に直してリスト形式にしておきます。 def preprocess(self, request): # Take the input data and pre-process it make it inference ready img_list = [] for idx, data in enumerate(request): # Read the bytearray of the image from the input img_arr = data.get('body') img_arr = base64.b64decode(img_arr) img_arr = Image.open(BytesIO(img_arr)) img_arr = np.array(img_arr) # Check the number of dimension assert len(img_arr.shape) == 3, "Dimension must be 3, but {}".format(len(img_arr.shape)) img_list.append(img_arr) return img_list def inference(self, model_input): res_list = [] # Do some inference call to engine here and return output for img in model_input: result = self.ocr.ocr(img, cls=True) for res in result: ## because float32 is not json serializable, score is converted to float (float64) ## However the score is in tuple and cannot be replaced. The entire tupple is replaced as list. string = res[1][0] score = float(res[1][1]) res[1] = [string,score] res_list.append(result) return res_list これを一発で書けるか?といわれると、そんな人は多くいないと思います。デプロイして画像を送ってエラーがないかを確認して、エラーが出れば上記を修正する、という作業を繰り返す必要が出てきます。ライブラリが必要であれば Dockerfile に追記、推論のスクリプトにバグがあればmodel_handler.py を修正します。初回のコンテナイメージのビルドには数十分の時間がかかりますが、一度ビルドするとキャッシュされるので、細々とした変更に多くの時間はかかりません。 OCR モデルの S3 へのアップロード PaddleOCR のライブラリは、モデルがローカルにない場合は自動でモデルをダウンロードする仕組みをもっています。しかし今回は、事前にモデルをダウンロードして S3 に保存しておき、デプロイするときは S3 からモデルをダウンロードして利用するようにします。自分で管理するので手間ではありますが、もしライブラリがモデルのダウンロードに失敗しても S3 からダウンロードして対応できます。 以下のテキストエリア検出、テキスト認識(認識モデルと日本語辞書)、テキスト分類(角度調整等に利用)の4つのファイルがあり、それぞれダウンロード・解凍して、以下に示すフォルダ構造で保存します。このフォルダ構造は、コンテナイメージをビルドする際に作成した model_handler.py の関数 initialize でモデルを読み込む際の階層構造と合わせる必要があります。また、モデルの URL はこちらのファイルから確認できます。テキスト認識にかかる2つのファイルは言語固有で、それ以外は、各言語共通のものを使います。 以下の階層構造でモデルなどを保存したら、それらを1つに圧縮して model.tar.gz にします。これは SageMaker が tar.gz で圧縮されていることを前提とするためです。エンドポイントを作成する際は、このmodel.tar.gzは解凍されて /opt/ml/model に展開されます(つまり /opt/ml/model/model/det/ 以下にテキストエリア検出モデルが展開されます)。 model ├── det ... (テキストエリア検出) ├── rec │   └── ja ... (テキスト認識モデル) ├── dict ... (日本語辞書) └── cls ... (テキスト分類) エンドポイントの作成 SageMaker の Model を作成して deploy を実行すればエンドポイントを作成できます。image_uriには ECR に push したコンテナイメージの URI を、model_data には先ほどアップロードした OCR モデル (model.tar.gz) へのパスを渡します。 from sagemaker.model import Model from sagemaker.predictor import Predictor ocr_model = sagemaker.model.Model(image_uri, model_data=model_uri, predictor_cls=Predictor, role=sagemaker.get_execution_role()) 今回は GPU を使いたいので ml.g4dn.xlarge をインスタンスに選んでデプロイします。ただし、これには時間がかかるので、もしデバッグがすんでいなければ、instance_type="local"と指定して、ローカル環境でまずはテストしてみましょう。以下でエラーが出る場合は、コンテナイメージに不具合があるはずなので、エラーを CloudWatch Logs で確認して、Dockerfile や model_handler.py を修正してコンテナイメージをもう一度ビルドします。 predictor = ocr_model.deploy(initial_instance_count=1,instance_type="ml.g4dn.xlarge") 画像を OCR にかけてみる 某所から持ってきた画像を試してみます。画像は model_handler.py で実装したように base64 でエンコードして送ります。 %%time import base64 from io import BytesIO from PIL import Image import PIL image = Image.open("test01.png") if isinstance(image,PIL.PngImagePlugin.PngImageFile): image = image.convert('RGB') buffered = BytesIO() image.save(buffered, format="JPEG") img_str = base64.b64encode(buffered.getvalue()) import json response = json.loads(predictor.predict(img_str)) response には結果が入っていますが、わかりやすくするため、検出された場所に赤枠で囲み、認識されたテキストと対応付けるための通し番号を付けます。 import numpy as np import cv2 x_offset = 20 y_offset = 0 for i, res in enumerate(response): box = np.reshape(np.array(res[0]), [-1, 1, 2]).astype(np.int64) image = cv2.putText(np.array(image), '('+str(i)+')', (box[0][0][0] -x_offset, box[0][0][1]-y_offset), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 0), 1, cv2.LINE_AA) image = cv2.polylines(np.array(image), [box], True, (255, 0, 0), 2) 最後に表示しましょう。 import matplotlib.pyplot as plt for i,res in enumerate(response): print('('+str(i)+'): '+res[1][0], end=', ') fig = plt.figure(dpi=200) plt.imshow(image) plt.show() 結果はこのような感じです。改行のある文章が各行ごとに検出されていますが、全体の日本語の精度はなかなか良さそうに見えます。一方、日本語にしているせいなのか、どうも英数字の検出精度がいまいちに見えます (Amazon EC2 -> AmaJnEC2 など)。また、数字は小数点が検出されていません。 また、検出速度ですが、前者の画像 (603 × 434 ピクセル)は概ね 130 msec での検出、後者の画像 (910 × 558 ピクセル) は 260 msec での検出となりました。 さいごに このノートブックを試した方はエンドポイントを削除するのを忘れないようにしましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python備忘録(1) 環境構築

Pythonの環境構築、バージョン管理、パッケージ管理と、IDEの基本的な使用方法をまとめておく。情報は2021年5月頭時点。使用環境は下記の通り。 Windows 10 HOME (64 bit) Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz 2.50 GHz RAM: 8.00 GB WindowsにPythonのexeを入れる 下のサイトを参照。 手順: 1. Pythonをダウンロード 2. インストール 3. PowerShellでポリシーを設定 以上。 手順の詳細 手順の1番「Pythonをダウンロード」について、上記の参考サイトからリンクされている非公式Pythonダウンロードリンク(https://pythonlinks.python.jp/ja/index.html )から欲しいバージョンのPythonをダウンロードする。複数のバージョンを入れても問題ない。 手順の2番「インストール」について、「Add Python 3.9 to PATH」にチェックを入れてインストールする。 手順の3番「PowerShellでポリシーを設定」について、PowerShellはデフォルトでスクリプトを実行しない設定(Restricted)になっているので、これを「信頼された発行元からのファイル」なら実行する設定(RemoteSigned)に変更する。ただし変更の対象範囲(スコープ)を現在のユーザーに限定する。すなわち下のコマンドを実行する。 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force 詳細は下のサイトを参照 使用方法 環境を入れるとPowerShellでpyコマンドが使用できるようになる。pyコマンドはバージョンを指定してpythonコマンドを実行する。 py -3.9 -m pip install --upgrade pip py -3.9 -m pip install flake8 py -3.9 -m pip install mypy py -3.9 -m pip install ipython py -3.9 -m pip install sphinx py -3.9 -m pip check py -3.9 myfile.py py -3.9 -m flake8 myfile.py py -3.9 -m mypy --strict myfile.py py -3.9 -m IPython sphinx-quickstart 備考 Sphinxはvenvを作って入れる方が良いかもしれない。下のサイトでもvenvを使用している。 Chocolateyを用いる場合はaptの感覚で管理することができるようだ。下記参照。 WindowsにAnacondaで入れる Anacondaは2020年に一部有償化された。 ここではIndividual Editionを想定する。 手順: 1. Anacondaのサイトからexeファイルをダウンロード 2. Anaconda Navigatorを起動 3. Environmentsタブで仮想環境一覧を開く 4. 仮想環境を作成 5. 仮想環境のパッケージ管理 6. condaのアップデート 7. 仮想環境にパッケージをインストール 以上。 手順の詳細 手順の1番「Anacondaのサイトからexeファイルをダウンロード」について、2021-05-02時点では、Anacondaのサイト(https://www.anaconda.com/ )のトップにタブがあり、「Products」タブから「Individual Edition」を選択すると、少しスクロールした位置に「Download」ボタンがある。それをクリックするとページ下までスクロールしてOS選択ができる。 手順の2番「Anaconda Navigatorを起動」について、正常にインストールされていればスタートメニュー内にAnaconda3グループが出来ているはずなのでそこからAnaconda Navigatorを起動する。起動直後は鬱陶しいコンソールウィンドウが何度か出現/消滅するが、しばらく待つとWindowが表示される。 手順の3番「Environmentsタブで仮想環境一覧を開く」について、左側の「Environments」タブを選択する。中央列に最初は「base (root)」だけがあるが、ここに順に仮想環境を追加していく。 手順の4番「仮想環境を作成」について、下の方にある「Create」ボタンを押す。Pythonのバージョンと仮想環境名を指定して環境を作成する。無事に作成されると、中央列に作成した仮想環境の項目が表示される。 あるいはcondaコマンドで作成しても良い。 conda create --name myenv python=3.8 手順の5番「仮想環境のパッケージ管理」について、仮想環境の項目の右側にある三角ボタンをクリックするとメニューが出現するので、そこで「Open Terminal」を選択する。コマンドプロンプトが現れる。コマンドプロンプトのプロンプトは(base)のように選択中の仮想環境名が表示された状態になっている。 あるいは conda activate myenv のようにcondaで仮想環境を切り替える。 手順の6番「condaのアップデート」について、conda自体をアップデートしておく。 conda update -n base -c defaults conda 手順の7番「仮想環境にパッケージをインストール」について、必要なパッケージをpipコマンドの代わりにcondaコマンドを使用してインストールする。 conda install numpy パッケージのインストールが完了したらコマンドプロンプトは閉じてよい。Anaconda Navigatorの表示を更新するにはAnaconda Navigator上で環境をもう一度クリックするか、「Update Index」ボタンをクリックする。 Anaconda Navigatorパッケージの一覧には、PythonアイコンのパッケージとAnacondaアイコンのパッケージの二種類ができることがある。Anacondaもパッケージリストを持っているらしく、リストに登録されていないパッケージの場合はpipで入れる必要がある。pipで入れたパッケージはPythonアイコンになる。 使用方法 Anacondaでは開発にSpyderというIDEを使用することができる。仮想環境が起動している状態でspyderコマンドを打つとSpyderエディタが開く。ひとたびSpyderの起動を行うと、Windowsのスタートメニューにその仮想環境用のSpyderへのリンクができるので、そこから起動ができるようになる。 Spyderのペイン構成はデフォルトでは左側がコード領域、右上がグラフなどのプロット領域、右下がipythonとなっている。コード領域に対して実行を行うと、右下のipython領域にrunfile()コマンドが打たれるという仕組みになっている。コマンドライン引数は下の例のように文字列で与える。 runfile('mycode.py', wdir='C:/path/to/work_dir', args='input1.txt input2.txt') PyCharm 有償版(Professional)と無償版(Community)がある。 詳細は下記のサイトを参照 ここでは無償版(Community版)を想定する。 手順: 1. インストール 2. プロジェクトを作成 3. パッケージをインストール 以上。 手順の詳細 手順1「インストール」について、インストール手順は特に悩むところはない。 手順2「プロジェクトを作成」について、インストールしたPyCharmを起動して「New Project」で新規プロジェクトを作成する。この時点でPythonインタープリタの選択ができるので、Anacondaなどで仮想環境を作成している場合は適宜使用したいものを選択する。 手順3「パッケージをインストール」について、「File」→「Settings」を選択し、左側メニューで「Project: {プロジェクト名}」の中の「Python Interpreter」を選択すると、右側にパッケージ一覧が表示される。左上にある+ボタンで必要なパッケージをインストールする。 使用方法 コマンドライン引数は、「Run」→「Edit Configurations」を選択し、対象ファイルを選択してから「Configuration」タブの「Parameters」欄に記入する。 「Code」→「Inspect Code」でコードをチェックする。左下のProblemsにも警告が表示される。 Visual Studio Code 使用経験なし。 下記のサイトに説明がある模様。 Jupyter Notebook Jupyter Notebookは、クライアント側に入力したコードをサーバー側で実行するという形のウェブアプリケーションと思われる。ここではWindowsにPythonのexeが入っている状態からの環境構築を想定する。 手順: 1. pipでnotebookをインストール 2. jupyter-notebookを起動 以上。 手順の詳細 手順1「pipでnotebookをインストール」について、PowerShell上のpipでnotebookパッケージを入れる。 py -3.9 -m pip install notebook これを実行するとPowerShell上でjupyter-notebookコマンドが使用できるようになる。 手順2「jupyter-notebookを起動」について、PowerShell上で以下のコマンドを打って起動する。 py -3.9 -m notebook 正常に起動するとブラウザが立ち上がりJupyterの画面が開く。 Pythonのバージョンを気にしない場合はjupyter-notebookコマンドで起動しても良い。 備考 今回初めて試したが、ざっと見た印象ではDjangoやFlaskのようなウェブアプリケーションと同じようなものと思われる。Anacondaをインストールした場合はAnacondaから起動することが出来るらしい。 VirtualBox上のUbuntuでもおそらく同じ手順で可能だが、ホスト側の画面に映すためにはネットワークの設定が必要になると思われる。Docker内のUbuntuでも同様。 Cygwin Cygwinに入れる。この方法は今回の動作環境では未検証。 Cygwinはsetup.exeというパッケージ管理ソフトでパッケージを一元管理しているので、そこのPythonメニューからPython関連のものを選択すればよい。いったんPython環境を入れればpipで追加していくこともできる。ただしCygwinのsetup.exeとpipの相性が良いかどうかは未調査。 WindowsのVirtualBoxにUbuntuを入れその中にPython環境を作る 手順: 1. VirtualBoxのインストール 2. UbuntuのISOイメージをダウンロード 3. Ubuntu環境を作成 4. Ubuntu上にPython環境を構築 以上。 手順の詳細 手順の1番「VirtualBoxのインストール」については特に悩むところはない。 手順の2番「UbuntuのISOイメージをダウンロード」について、Ubuntuには16.04LTSや18.04LTSや20.04LTSのような長期保証バージョンがあるので、使用したいバージョンのものを入手する。 手順の3番「Ubuntu環境を作成」について、途中の細かい設定項目の最適値は人・環境に依存するので適宜試行錯誤する。入手したISOイメージを仮想光学ディスクファイルに指定する1。Guest Additionsを忘れずインストールする2。Guest Addtionsインストール後に共有フォルダを設定する3。 手順の4番「Ubuntu上にPython環境を構築」は次の項目を参照。 Ubuntuでのやり方 Ubuntuでは普通Pythonが最初から入っている。Ubuntuのバージョンやaptに絡んでくるので、バージョン管理を慎重に行う必要がある(最初から入っているPythonにシステムが依存している可能性があるため、破壊しないように注意する)。 ここではUbuntu 18.04LTSを想定する。Ubuntu 18.04LTSの場合デフォルトでPython 3.6が入っていた。 手順: 1. システムが最新であることを確認 2. Pythonのバージョンを確認 以上。 手順の詳細 手順の1番「システムが最新であることを確認」について、以下のコマンドで最新にする。 sudo apt update sudo apt -y upgrade 手順の2番「Pythonのバージョンを確認」について、 python --version Python 2とPython 3が共存している場合、pythonコマンドではPython 2が呼ばれる場合がある。そういう場合、python3コマンドが別途用意されていることがある。type pythonまたはwhich pythonでパスを確認し、シンボリックリンクの状態を確認する。 パッケージ管理は前述のとおり慎重に行う必要がある。次項のvenvなどの仮想環境を作るのが良いだろう。 欲しいバージョンのPythonがシステムにない場合、自分でインストールする(これは今回は試していない)。 venv パッケージの環境を複数用意したい場合は、これを使用する。 手順: 1. 仮想環境用のディレクトリを用意 2. 仮想環境を作成 3. 仮想環境の起動/終了 以上。 手順の詳細 手順の1番「仮想環境用のディレクトリを用意」について、venvは一つ一つの仮想環境をそれぞれ一つのディレクトリに割り当てるのだが、通常は仮想環境を複数作るので、それらを格納する親ディレクトリを用意しておくのがわかりやすい。 mkdir ~/venvs/ cd ~/venvs/ 共有フォルダなどを指定するとうまくいかない場合がある。ホームディレクトリ直下に作るのがわかりやすい。 手順の2番「仮想環境を作成」について、例えばmyenvという名前の仮想環境を作る場合、以下のコマンドを使用する。 venv myenv myenvディレクトリが作成される。このディレクトリの中にパッケージがインストールされる。 仮想環境を削除するには単純にフォルダを削除すれば良い。 手順の3番「仮想環境の起動/終了」について、作成された仮想環境のディレクトリの中のbin/activateというファイルを読み込ませると仮想環境が起動した状態になる。Bashの場合、 source myenv/bin/activate deactivate 仮想環境を起動するとプロンプトに(myenv)のように環境名が表示された状態になる。この状態でpipで入れればパッケージは仮想環境内にインストールされる。いったんインストールすれば再起動(deactivate後にactivate)しても使用することができる。 Docker pythonイメージを使用する。 手順: 1. Docker作成用のファイルを準備 2. docker compose 3. うまく作れたか確認 4. docker execで接続 以上。 手順の詳細 手順の1番「Docker作成用のファイルを準備」について、 下記のフォルダ構成とする。 └── python ├── Dockerfile └── docker-compose.yml Dockerfileとdocker-compose.ymlには例えば下のファイルを用意する。 FROM python:3 USER root RUN apt update RUN apt -y install locales && \ localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 RUN pip install --upgrade pip && \ apt install vim -y && \ apt install graphviz -y && \ apt install cmake -y && \ apt install mypy -y && \ pip install ipython && \ pip install --upgrade ipython && \ pip install flake8 docker-compose.yml version: "3" services: python: build: . container_name: "python" working_dir: "/root/" tty: true volumes: - /home/share:/root/work volumeは、ホスト側の共有したいパスの後ろに共有フォルダ名を指定する。 手順の2番「docker-compose」について、上記のファイル群を置いたPythonディレクトリで以下のコマンドを叩く。 docker-compose up -d --build 手順の3番「うまく作れたか確認」について、例えば以下のコマンドで確認する。 docker ps -a コンテナID、イメージ名、ステータスなどが表示される。 他にdocker container lsやdocker image lsというコマンドもある。 イメージ名はフォルダ名などから自動で決定される。「python_python」となっていると想定する。 手順の4番「docker execで接続」について、以下のコマンドで接続する。 docker exec -it python_python bash オプションの -itは対話的に実行、 bashの部分はシェルを指定。 docker attachだとすでに起動しているDockerの本流のシェルに接続するが、docker execの場合はもう少し弱く接続する(ので、うっかり本流を殺してしまう心配がない)。 使用方法 いったんDockerを起動してしまえばあとは通常のUbuntuの感覚と変わらない。 備考 データをホスト側とやり取りする場合、共有フォルダを使用するのが楽。ただし、普段の入出力を共有フォルダにするとパフォーマンス(処理速度)が落ちることに注意。 WSL2とDocker Desktop Windows 10のWSL2にはLinuxカーネルが使用されているらしく、機能を有効化することでWindowsのちょっとしたアプリケーションの感覚でDockerが使用できる。 このDockerを管理するGUIアプリケーションがいくつか存在する。 ここではDocker Desktopを想定する。 手順: 1. Docker Desktopをインストール 2. 警告が出た場合、メッセージに従って対処 以上。 備考 GUIやGPUが使用できるかどうかは未検証。これらは例えば以下のサイトを参照。 pipのバージョン管理 pipの実行方法はいくつか存在する。 pip pip3 python -m pip python3 -m pip 環境やバージョンに合わせた実行方法にする必要がある。ここでは表記をpipで揃えて記述する。パッケージの管理には以下のコマンド群を使用する。 コマンド(Bashを想定) 処理 pip list パッケージ一覧を表示 pip check パッケージ整合性のチェック pip install numpy numpyパッケージをインストール pip install numpy==1.20.2 numpyパッケージ(バージョン1.20.2)をインストール pip uninstall numpy numpyパッケージをアンインストール pip install -r requirements.txt requirements.txt記述のパッケージをまとめてインストール pip freeze > requirementst.txt パッケージ一覧をrequirements.txtに保存 備考 Python単体で閉じているパッケージはこれでおおむね問題ないが、Python外のソフトと連携しているとややこしい場合がある4。 普通のPCにはUSBポートやCD挿入口があるが、仮想マシンの場合はこのようにホスト側からソフト的に入れるということなのだと理解している。 ↩ これをインストールしておかないと色々と不便である。 ↩ このあたりは若干挙動が不安定な場合がある。 ↩ 例えばaptと絡んでくる場合や、自分で用意したインストールフォルダが絡んでくる場合がある。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】Xリーグスタッツ自動抽出器を作ってみた(OCR、正規表現)

こんにちは。 ノジマ相模原ライズというアメフトチームでアナライズを担当している冨上と申します。 アメフトでデータを活用する取り組みをいくつか行っているので、紹介して参ります。 これまでの取り組みもぜひご覧ください。 https://qiita.com/ryojihimeno/items/73c3a201375aaa419492 https://qiita.com/ryojihimeno/items/6cb013f8b847a4864a87 https://qiita.com/KentaroTokami/items/96002901ee2d18fdad89 今回はXリーグの公式HPから試合のスタッツを抽出してみました。 背景 アメフトの試合の分析を行うにあたって必要となるものは、プレーの結果を数値化したスタッツです。 近年のスポーツ業界では、リーグとしてデータの活用を行ったり、外部の企業がデータをまとめて提供したりしています。 Xリーグではまだそういった仕組みはなく、PDF化されたデータがHPで公開されているだけです。 PDFデータのままでは分析を行ったり、可視化することはできません。 そこで今回は現状唯一入手できるPDFデータをOCRを用いてcsvファイルに変換し、分析できる形にすることを目指します。 スタッツのPDFファイルを取得→PDFを画像に変換→OCRでテキストへの変換→正規表現によるパターンマッチングでスタッツ部分の抽出→csvファイルへの変換 という流れになります。 実行環境 Windows10 python3.8.5 jupyter lab 2.2.6 今回はjupyter lab上で実行しました。 実装 まずはXリーグの公式HPから試合詳細をダウンロードします。 続いて、jupyter labを起動し、pythonでコードを書いていきます。 OCRの実行は以下のリンクを参考にしました。 https://qiita.com/Tak3315/items/4cf6bc0ee011048f1424 まず、使用するライブラリをインポートしていきます。 # 必要なライブラリのインポート import os import pathlib from pathlib import Path from pdf2image import convert_from_path from PIL import Image import sys import pyocr import pyocr.builders import pathlib import glob import pandas as pd import re import numpy as np 次にダウンロードしたPDFを画像に変換します。 def pdf_to_image(pdf_name): # poppler/binを環境変数Pathに追加する(一時的に) poppler_dir = pathlib.Path("__file__").parent.resolve() / "poppler/bin" os.environ["PATH"] += os.pathsep + str(poppler_dir) # PDFファイルのパスを取得する pdf_dir = pathlib.Path('./pdf_file') pdf_path = pdf_dir / pdf_name # PDF -> Imageに変換(200dpi) pages = convert_from_path(str(pdf_path)) # 画像ファイルを1ページづつ保存 if not os.path.exists("./image_file/{0}".format(pdf_name)): os.mkdir("./image_file/{0}".format(pdf_name)) image_dir = pathlib.Path("./image_file/{0}".format(pdf_name)) for i, page in enumerate(pages): # enumerate関数でpagesのpage数を取得 # .stemでpathの末尾を表示(pathlib) file_name = pdf_path.stem + "_{:02d}".format(i + 1) + ".jpeg" image_path = image_dir / file_name # JPEGで保存 page.save(str(image_path), "JPEG") 画像をテキストファイルに変換します。 def image_ocr(pdf_name): # tesseract-OCRのパスを通す tessera_path = "C:\Program Files\Tesseract-OCR" # pathsepは環境変数に追加するときの区切り; os.environ["PATH"] += os.pathsep + str(tessera_path) tools = pyocr.get_available_tools() if len(tools) == 0: print("No OCR tool found") sys.exit(1) # 引数1は終了ステータスで1を返す tool = tools[0] # ocr対象のファイルがあるディレクトリ image_dir = pathlib.Path("./image_file/{0}".format(pdf_name)) # globでディレクトリ内のjpegファイルをリストで取得 jpg_path = list(image_dir.glob('**/*.jpeg')) if not os.path.exists("./txt_file/{0}".format(pdf_name)): os.mkdir("./txt_file/{0}".format(pdf_name)) for i in jpg_path: # ocrした内容を変数txtにする txt = tool.image_to_string( Image.open(str(i)), lang="jpn", builder=pyocr.builders.TextBuilder(tesseract_layout=6) ) # 変数txtをtxt_fileディレクトリにtxtファイルで保存 with open("./txt_file/{0}/".format(pdf_name) + str(i.stem) + '.txt', mode='wt') as t: t.write(txt) ここからは正規表現を用いてデータの抽出を行います。 テキストファイルを一行ごとに処理して各スタッツを抽出していきます。 まずはどのようなファイルが生成されたか確認します。 path = './txt_file/rise_ibm_06.txt' with open(path) as f: lines = f.readlines() lines_strip = [line.strip() for line in lines] lines_stripの中身はこのようになっています。 ['IBM BigBlue(BB) vs ノジマ相模原ライズ(NR)', '2020/11/23(月) 会場 : 富士通スタジアム川崎', 'Play by Play Second Quarter', 'ノジマ相模原ライズ 12:00', '2&12-NR42 M RUN #6 KURT PALANDECH 0yラン(#35 Gamboa Herbert)- No Play', '+Penalty NR #67 ホールディング 10y 久退', '2&22-NR32 M PASS #6 KURT PALANDECH パス失敗', '3&22-NR32 M PASS #6 KURT PALANDECH パス失敗', リストにプレー内容と結果が格納されています。 このままでは会場情報等プレーには直接関係ない内容が含まれているので、プレー結果が含まれている要素にのみを抽出します。 &やPenalty等プレーに関係してそうな文字列を含む行を抽出しています。 l_XXX = [line for line in lines_strip if ('&' in line) or ('Penalty' in line) or ('Kick-off' in line) or ('Extra Point' in line) or ('TIMEOUT' in line) or (':' in line) or ('Quarter' in line)] ここでの要素抽出は、すべてのプレーを網羅できていない可能性があるのでアップデートする必要がありそうです。 続いて各要素からスタッツを入手します。 まずはフィールドポジションの抽出を試みます。 pythonのreモジュールを用いて求めている文字列が含まれているか調べます。 「NR32M」のような文字列を抽出できればNR陣32yard、ハッシュミドルにボールがあるとわかるので、アルファベット2文字、数字1文字以上、LMRのいずれかがつながった文字列を探します。 fp = re.search(r'\w\w\d+[LMR]', s) 情報を含む文字列を陣地、ヤード、ハッシュに分割します。 if fp is not None: fp_all = fp.group() yard = re.search(r'\d+', fp_all) y = yard.group() position = fp_all[0:2] Hash = fp_all[-1] 以上のコードを関数化すると以下のようになります。 def get_fieldposition(s): """ 陣地、ヤード、ハッシュを抽出する """ position = np.nan y = np.nan Hash = np.nan fp = re.search(r'\w\w\d+[LMR]', s) if fp is not None: fp_all = fp.group() yard = re.search(r'\d+', fp_all) y = yard.group() position = fp_all[0:2] Hash = fp_all[-1] return position, y, Hash 他のスタッツも同様に正規表現を用いて抽出します。 def get_downdistance(s): """ ダウンディスタンスを抽出する """ down = np.nan distance = np.nan dd = re.search(r'[1234]&\d+', s) if dd is not None: dd_split = re.split("&", dd.group()) down = dd_split[0] distance = dd_split[1] return down, distance def get_gain(s): """ 獲得ヤードを抽出する """ gain = np.nan yardy = re.search(r'-*\d+y', s) if yardy is not None: yard = re.search(r'-*\d+', yardy.group()) gain = yard.group() return gain def get_playtype(s): """ プレーのタイプを抽出する """ play_type = np.nan if "RUN" in s: play_type = "Run" elif "PASS" in s: play_type = "Pass" elif "FG" in s: play_type = "FG" elif "PUNT" in s: play_type = "Punt" elif "Kick-off" in s: play_type = "KO" elif "Extra" in s: play_type = "Extra Pt." else: pass return play_type def get_offense_team(s): """ 攻撃チームを抽出する """ offense_team = np.nan ot = re.search(r'\d+:\d+', s) team_dict = {"パナソニックインパルス": "PI", "東京ガスクリエイターズ": "TG", "ノジマ相模原ライズ": "NR", "IBMBigBlue": "BB"} if ot: offense_team = re.search(r'\D+', s).group() offense_team = offense_team.replace("Visitor", "") for k, v in team_dict.items(): if offense_team == k: offense_team = v return offense_team def get_quarter(s): """ クウォーターを抽出する """ quarter = np.nan q = re.search(r'Quarter', s) if q: fq = re.search(r'First', s) sq = re.search(r'Second', s) tq = re.search(r'Third', s) yq = re.search(r'Fourth', s) if fq: quarter = "1" elif sq: quarter = "2" elif tq: quarter = "3" elif yq: quarter = "4" return quarter スタッツを抽出する関数ができたら一つの関数にまとめます。 最終的にはヘッダーを持つDataFrameに変換したいのでここではdictをリターンするようにします。 def get_stats_dict(s): """ 正規表現によるスタッツの抽出をまとめる """ position, y, Hash = get_fieldposition(s) down, distance = get_downdistance(s) gain = get_gain(s) play_type = get_playtype(s) offense_team = get_offense_team(s) quarter = get_quarter(s) stats_list = [("position", position), ("YARD LN", y), ("HASH", Hash), ("DN", down), ("DIST", distance), ("GN/LS", gain), ("PLAY TYPE", play_type), ("offense team", offense_team), ("QTR", quarter),] stats_dict = dict(stats_list) return stats_dict PDFファイルのパスからDataFrameが入手できるよう、さらに関数をまとめます。 def get_data(path): """ PDFファイルからスタッツデータフレームに変換する """ with open(path) as f: lines = f.readlines() lines_strip = [line.strip() for line in lines] l_XXX = [line for line in lines_strip if ('&' in line) or ('Penalty' in line) or ('Kick-off' in line) or ('Extra Point' in line) or ('TIMEOUT' in line) or (':' in line) or ('Quarter' in line)] remove_space = [line.replace(' ', '') for line in l_XXX] df = pd.DataFrame(remove_space, columns=["test"]) stats_list = [get_stats_dict(df["test"][i]) for i in range(len(df["test"]))] stats_df = pd.DataFrame(stats_list) return stats_df 攻撃チーム、クウォーターの情報は各行には含まれていないので、切り替わったタイミングをもとに補完していきます。 def cleandata(pdf_name): """ 不必要なデータを削除、足りていない部分を補完 """ df = pd.DataFrame(columns=["position", "YARD LN", "HASH", "DN", "DIST", "GN/LS", "PLAY TYPE", "offense team", "QTR"]) path_list = glob.glob('./txt_file/{}/*'.format(pdf_name)) for path in path_list: df = pd.concat([df, get_data(path)]) df = df.dropna(how='all') df = df.reset_index(drop=True) for i in range(len(df["offense team"])): if df["offense team"][i] is np.nan: if i >= 1: h = i - 1 d = df["offense team"][h] while d is np.nan and h > 0: h -= 1 d = df["offense team"][h] df["offense team"][i] = d for i in range(len(df["QTR"])): if df["QTR"][i] is np.nan: if i >= 1: h = i - 1 d = df["QTR"][h] while d is np.nan and h > 0: h -= 1 d = df["QTR"][h] df["QTR"][i] = d df = df.dropna(subset=["QTR"]).reset_index(drop=True) df = df.dropna(subset=["position","YARD LN", "HASH", "DN", "DIST", "GN/LS", "PLAY TYPE"], how="all").reset_index(drop=True) team_list = df["offense team"].unique() df.loc[df["offense team"] == team_list[0], "defense team"] = team_list[1] df.loc[df["offense team"] == team_list[1], "defense team"] = team_list[0] return df 最後に今回作った関数をまとめて、保存したPDFファイル名からcsvファイルを出力できるようにします。 def stats_csv(pdf_name): """ 最後に関数をまとめ、PDFファイル名からCSVファイルを作成する """ pdf_to_image(pdf_name) image_ocr(pdf_name) df = cleandata(pdf_name) df.to_csv("./csv_file/" + pdf_name + '.csv', encoding="shift-jis") stats_csv("rise_ibm.pdf") 最終アウトプットは以下のようになります。 position YARD LN HASH DN DIST GN/LS PLAY TYPE offense team QTR defense team 0 17 KO NR 1 BB 1 BB 31 R 1 10 PASS BB 1 NR 2 BB 31 R 2 10 11 PASS BB 1 NR 3 BB 31 L 1 10 7 PASS BB 1 NR 4 BB 31 R 2 3 10 PASS BB 1 NR 使い道 扱いやすいcsv形式のデータがあれば、いろいろな使い方ができます。 Hudl(動画共有プラットフォーム)へのインポートが可能になったりcsvファイルをエクセルやpythonで分析にかけることができます。 今後の展望 正規表現での抽出はすべてを網羅できていない可能性があり、もっと良い抽出方法がありそうです。また、元のPDFファイルにある情報で使えるものもまだまだあるはずです。 本来ならば今回のような手間をかけずに、扱いやすい形式でのデータ提供があることが一番です。 扱いやすい形式のデータの要求を続けつつ、現状で入手可能なデータを扱うという2方向からのアプローチを進めていきたいと思います。 また、ノジマ相模原ライズでは一緒に活動してくれるエンジニアを募集中です! スタッフ申し込みはこちらのフォームからお願いします。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

manimの作法 その53

概要 manimの作法、調べてみた。 複素数、使ってみた。 サンプルコード from manimlib.imports import * class FourierCirclesScene(Scene): CONFIG = { "n_vectors": 10, "big_radius": 2, "colors": [BLUE_D, BLUE_C, BLUE_E, GREY_BROWN, ], "circle_style": { "stroke_width": 2, }, "vector_config": { "buff": 0, "max_tip_length_to_length_ratio": 0.35, "tip_length": 0.15, "max_stroke_width_to_length_ratio": 10, "stroke_width": 2, }, "circle_config": { "stroke_width": 1, }, "base_frequency": 1, "slow_factor": 0.25, "center_point": ORIGIN, "parametric_function_step_size": 0.001, "drawn_path_color": YELLOW, "drawn_path_stroke_width": 2, } def setup(self): self.slow_factor_tracker = ValueTracker(self.slow_factor) self.vector_clock = ValueTracker(0) self.vector_clock.add_updater(lambda m, dt: m.increment_value(self.get_slow_factor() * dt)) self.add(self.vector_clock) def get_slow_factor(self): return self.slow_factor_tracker.get_value() def get_vector_time(self): return self.vector_clock.get_value() def get_freqs(self): n = self.n_vectors all_freqs = list(range(n // 2, -n // 2, -1)) all_freqs.sort(key = abs) return all_freqs def get_coefficients(self): return [complex(0) for x in range(self.n_vectors)] def get_color_iterator(self): return it.cycle(self.colors) def get_rotating_vectors(self, freqs = None, coefficients = None): vectors = VGroup() self.center_tracker = VectorizedPoint(self.center_point) if freqs is None: freqs = self.get_freqs() if coefficients is None: coefficients = self.get_coefficients() last_vector = None for freq, coefficient in zip(freqs, coefficients): if last_vector: center_func = last_vector.get_end else: center_func = self.center_tracker.get_location vector = self.get_rotating_vector(coefficient = coefficient, freq = freq, center_func = center_func, ) vectors.add(vector) last_vector = vector return vectors def get_rotating_vector(self, coefficient, freq, center_func): vector = Vector(RIGHT, **self.vector_config) vector.scale(abs(coefficient)) if abs(coefficient) == 0: phase = 0 else: phase = np.log(coefficient).imag vector.rotate(phase, about_point = ORIGIN) vector.freq = freq vector.coefficient = coefficient vector.center_func = center_func vector.add_updater(self.update_vector) return vector def update_vector(self, vector, dt): time = self.get_vector_time() coef = vector.coefficient freq = vector.freq phase = np.log(coef).imag vector.set_length(abs(coef)) vector.set_angle(phase + time * freq * TAU) vector.shift(vector.center_func() - vector.get_start()) return vector def get_circles(self, vectors): return VGroup(*[self.get_circle(vector, color=color) for vector, color in zip(vectors, self.get_color_iterator())]) def get_circle(self, vector, color = BLUE): circle = Circle(color=color, **self.circle_config) circle.center_func = vector.get_start circle.radius_func = vector.get_length circle.add_updater(self.update_circle) return circle def update_circle(self, circle): circle.set_width(2 * circle.radius_func()) circle.move_to(circle.center_func()) return circle def get_vector_sum_path(self, vectors, color = YELLOW): coefs = [v.coefficient for v in vectors] freqs = [v.freq for v in vectors] center = vectors[0].get_start() path = ParametricFunction(lambda t: center + reduce(op.add, [complex_to_R3(coef * np.exp(TAU * 1j * freq * t)) for coef, freq in zip(coefs, freqs)]), t_min=0, t_max=1, color=color, step_size=self.parametric_function_step_size, ) return path def get_drawn_path_alpha(self): return self.get_vector_time() def get_drawn_path(self, vectors, stroke_width = None, **kwargs): if stroke_width is None: stroke_width = self.drawn_path_stroke_width path = self.get_vector_sum_path(vectors, **kwargs) broken_path = CurvesAsSubmobjects(path) broken_path.curr_time = 0 def update_path(path, dt): alpha = self.get_drawn_path_alpha() n_curves = len(path) for a, sp in zip(np.linspace(0, 1, n_curves), path): b = alpha - a if b < 0: width = 0 else: width = stroke_width * (1 - (b % 1)) sp.set_stroke(width = width) path.curr_time += dt return path broken_path.set_color(self.drawn_path_color) broken_path.add_updater(update_path) return broken_path def get_y_component_wave(self, vectors, left_x = 1, color = PINK, n_copies = 2, right_shift_rate = 5): path = self.get_vector_sum_path(vectors) wave = ParametricFunction(lambda t: op.add(right_shift_rate * t * LEFT, path.function(t)[1] * UP), t_min = path.t_min, t_max = path.t_max, color = color, ) wave_copies = VGroup(*[wave.copy() for x in range(n_copies)]) wave_copies.arrange(RIGHT, buff = 0) top_point = wave_copies.get_top() wave.creation = ShowCreation(wave, run_time = (1 / self.get_slow_factor()), rate_func = linear, ) cycle_animation(wave.creation) wave.add_updater(lambda m: m.shift((m.get_left()[0] - left_x) * LEFT)) def update_wave_copies(wcs): index = int(wave.creation.total_time * self.get_slow_factor()) wcs[:index].match_style(wave) wcs[index:].set_stroke(width = 0) wcs.next_to(wave, RIGHT, buff = 0) wcs.align_to(top_point, UP) wave_copies.add_updater(update_wave_copies) return VGroup(wave, wave_copies) def get_wave_y_line(self, vectors, wave): return DashedLine(vectors[-1].get_end(), wave[0].get_end(), stroke_width = 1, dash_length = DEFAULT_DASH_LENGTH * 0.5, ) def get_coefficients_of_path(self, path, n_samples = 10000, freqs = None): if freqs is None: freqs = self.get_freqs() dt = 1 / n_samples ts = np.arange(0, 1, dt) samples = np.array([path.point_from_proportion(t) for t in ts]) samples -= self.center_point complex_samples = samples[:, 0] + 1j * samples[:, 1] result = [] for freq in freqs: riemann_sum = np.array([np.exp(-TAU * 1j * freq * t) * cs for t, cs in zip(ts, complex_samples)]).sum() * dt result.append(riemann_sum) return result class test(FourierCirclesScene): CONFIG = { "n_vectors": 51, "center_point": ORIGIN, "slow_factor": 0.1, "n_cycles": 2, "tex": "\\pi", "start_drawn": False, "max_circle_stroke_width": 1, } def construct(self): self.add_vectors_circles_path() for n in range(self.n_cycles): self.run_one_cycle() def add_vectors_circles_path(self): path = self.get_path() coefs = self.get_coefficients_of_path(path) vectors = self.get_rotating_vectors(coefficients = coefs) circles = self.get_circles(vectors) self.set_decreasing_stroke_widths(circles) drawn_path = self.get_drawn_path(vectors) if self.start_drawn: self.vector_clock.increment_value(1) self.add(path) self.add(vectors) self.add(circles) self.add(drawn_path) self.vectors = vectors self.circles = circles self.path = path self.drawn_path = drawn_path def run_one_cycle(self): time = 1 / self.slow_factor self.wait(time) def set_decreasing_stroke_widths(self, circles): mcsw = self.max_circle_stroke_width for k, circle in zip(it.count(1), circles): circle.set_stroke(width=max(mcsw / k, mcsw, )) return circles def get_path(self): tex_mob = TexMobject(self.tex) tex_mob.set_height(6) path = tex_mob.family_members_with_points()[0] path.set_fill(opacity = 0) path.set_stroke(WHITE, 1) return path 生成した動画 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WaniCTF'21-spring Automaton Lab. Writeup

python等のプログラミング力と問題解決力が問われた問題。 一度は心を折られたが,あきらめずにチャレンジして解いた記憶に残る問題。 最近はやりの,English only の問題ですね。 Big welcome です。 問題 Automaton Lab. 262pt Normal Automaton Lab.で将来予測のお手伝いをしましょう <--日本語混じってる nc automaton.mis.wanictf.org 50020 reference: https://en.wikipedia.org/wiki/Rule_30 Solution ncしてみる $ nc automaton.mis.wanictf.org 50020 Welcome to Automaton Lab.! We study about automaton in there, here is the space of "Rule 30"[1] automaton. We breed 15 cells automaton now, they are ring-connected -- they are connected the first cell and the last cell. Our interest is what this cute automaton grow up in future, we want your help to expect their growth. [1]: https://en.wikipedia.org/wiki/Rule_30 For example, now automaton state is "100000100000001" ('1' is alive and '0' is dead) and in next generation they are "010001110000011". generation state 0 100000100000001 1 010001110000011 2 011011001000110 We give you initial state(init) and generation(gen). You write down the state of this automaton of the nth generation in binary. Are you ready? (press enter key to continue) ここで aaa と入力 aaa OK! Here we go! The first question is warming up. init = 010111100010000 gen = 7 > ルール に基づいて何世代か後のビットパターンを答える問題とわかった。 ソルバー書いてみる solver.py init = ”010111100010000” gen = 7 i = 0 while i < gen: ans = "" j = 0 while j < 15: chk = "" if j == 0: chk = init[14]+init[0]+init[1] if j == 14: chk = init[13]+init[14]+init[0] if j >= 1 and j <= 13: chk = init[j-1]+init[j]+init[j+1] #print(chk) if chk == '111': ans = ans + "0" if chk == '110': ans = ans + "0" if chk == '101': ans = ans + "0" if chk == '100': ans = ans + "1" if chk == '011': ans = ans + "1" if chk == '010': ans = ans + "1" if chk == '001': ans = ans + "1" if chk == '000': ans = ans + "0" j = j + 1 #print(ans) init = ans i = i + 1 print(ans) 実行した結果を入力してみると > 111011101100001 Great! The second question is below. init = 000011000000000 gen = 997 > 今度は 997 世代目だと 計算した結果を入れる。 > 110101111010111 Even if human become extinct, we wanna see the view of prosperity of cell automaton. This is last question. init = 000000000000001 gen = 145180878248087930822675870521224824318594123419581197764296767187269329560188872269760488314219678660744026209365352834395821787688590524538339794462323979413816885896955061908051716263494446436907527717746123918714900672662822274220789622765357101233975574606429515821751260196625638309200227868630073832431 > まだあるのか? もはや計算できない世代数だ。 どうする? 15ビットしかないので同じビットパターンが出現しないか確認することに。 init = 000010000010000 gen = 997 のデータの答えが途中で重複していないか確認 ダメだ重複していない。 ここで一度,心が折れる。 翌日 計算可能な大きい世代でもう一度重複しないか確認する。 init = "000000000000101" gen = 132767 10547 - 9092 = 1455 9092 - 7637 = 1455 7637 - 6182 = 1455 6182 - 4727 = 1455 4727 - 3272 = 1455 3271 - 1817 = 1455 1817 - 362 = 1455 1455周期だ。 剰余でいけるはず。 ソルバーを改良する solver_kai.py init = "000000000000001" gen = 145180878248087930822675870521224824318594123419581197764296767187269329560188872269760488314219678660744026209365352834395821787688590524538339794462323979413816885896955061908051716263494446436907527717746123918714900672662822274220789622765357101233975574606429515821751260196625638309200227868630073832431 gen = gen % 1455 # この一行が全て! i = 0 while i < gen: ans = "" j = 0 while j < 15: chk = "" if j == 0: chk = init[14]+init[0]+init[1] if j == 14: chk = init[13]+init[14]+init[0] if j >= 1 and j <= 13: chk = init[j-1]+init[j]+init[j+1] #print(chk) if chk == '111': ans = ans + "0" if chk == '110': ans = ans + "0" if chk == '101': ans = ans + "0" if chk == '100': ans = ans + "1" if chk == '011': ans = ans + "1" if chk == '010': ans = ans + "1" if chk == '001': ans = ans + "1" if chk == '000': ans = ans + "0" j = j + 1 #print(ans) init = ans i = i + 1 print(ans) nc に答えを投げてみる > 001011010110011 Jesus!!! Are you the genius of future sight? We award you the special prize. FLAG{W3_4w4rd_y0u_d0c70r473_1n_Fu7ur3_S1gh7} ビンゴ!   作問者writeup
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonとSeleniumで自動ログインを行う

はじめに 某クレジットカードの明細書の確認に毎回ログインが必要なため、自動でログインできるようなシステムを作成する。 ブラウザ上に直接情報を入力するために今回はSeleniumというスクレイピングやWEB操作のフレームワークを利用します。 クイックリファレンスが分かりやすくて便利かなと思います。 環境 ・Windows 10 ・Chrome 90.0.4430.93 ・Python 3.9.1 ・ChromeDriver 90.0.4430.24.0 ・Selenium 3.141.0 初期設定 seleniumu Pythonが入っていればpipでインストールできます。 $ pip install selenium ChromeDriver Pythonが入っていればselenium同様にpipできます。 $ pip install chromedriver-binary 本来ならバイナリを直接ダウンロードして環境変数でPathを通す必要があるみたいですが、pipでインストールした場合には自動的にPathが通っているみたいです。 Seleniumの動作 動作を確認するため、公式サイトにあるコードを実行してみます。(少し編集してます) sele-tu.py import time import chromedriver_binary from selenium import webdriver driver = webdriver.Chrome() #WEBブラウザの起動 driver.get('http://www.google.com/') #特定のURLへ移動 time.sleep(2) search_box = driver.find_element_by_name('q') #ページ上の要素の取得 search_box.send_keys('ChromeDriver') search_box.submit() 簡単に説明 driver = webdriver.Chrome() ブラウザを開きます。本来ならwebdriver.Chrome()の部分でChromedriverのバイナリファイルのパスを引数に設定する必要があるのですが、今回はpipでchromedriver_binaryをインストールしているため不要 driver.get('http://www.google.com/') 任意のURLのページに移動する search_box = driver.find_element_by_name('q') #ページ上の要素の取得 search_box.send_keys('ChromeDriver') search_box.submit() ページ上の要素を取得して、その要素に対してアクションを行っています。 この流れでWEBページの情報を取得して、ページを切り替えていくのがSeleniumの強みらしいです。 結果(エラー) selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 91 Current browser version is 90.0.4430.93 with binary path C:\Program Files\Google\Chrome\Application\ ChromedriverとChromeのバージョンが合ってないとの事なので、Chromedriverの方を調整します $ pip pip install chromedriver_binary==90.0.4430.24.0 Chromedriver_binaryのバージョンはコチラ 再度実行 無事Chromeが開いて検索まで行うことが出来ました。 実装 某クレジットカードページに対して、ページを開きログインまでを実装してみます。 import time import chromedriver_binary from selenium import webdriver driver = webdriver.Chrome() #WEBブラウザの起動 driver.get('https://www.rakuten-card.co.jp/e-navi/index.xhtml') time.sleep(1) mail = driver.find_element_by_name('u') password=driver.find_element_by_name('p') mail.clear() password.clear() mail.send_keys("my email") password.send_keys("my password") mail.submit() 簡単に解説 driver.get('https://www.rakuten-card.co.jp/e-navi/index.xhtml') 楽天カードのログインページを開きます mail = driver.find_element_by_name('u') password=driver.find_element_by_name('p') ChromeのデベロッパーツールなどでHTMLの要素を確認します。基本的に特定のページに関してはname,class,idタグからなどから要素を特定する方が楽なのかなと思います。 上の図だと、ユーザIDのテキストボックスのnameタグはu、パスワードのnameタグはpと分かったのでそれに対する要素を取得します。 mail.clear() password.clear() 要素の初期値を消します。 mail.send_keys("my email") password.send_keys("my password") IDとPASSの要素に値を入れます。 mail.submit() 要素に入れた情報を送信します。 今回はsumbit()にしていますがログインボタンの要素を取得してclick()でもいけるみたいです。 結果 無事ログイン後の明細書の画面まで行くことが出来ました。 使いこなせればデータの収集や自動操作など、幅広い用途に関して使えそうだと思いました。 次回はこれをワンクリックできるようなWindowsアプリを実装する予定です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Paiza】2項間漸化式 1【レベルアップ問題集】

はじめに IT/WEBエンジニアに特化した転職・就職・学習サイト「paiza(パイザ)」のレベルアップ問題集から1問ピックアップして、とことん噛み砕いた解説を備忘録としてまとめました。 今回は「DPメニュー」から「2項間漸化式 1」を解説していきます。 問題 整数 x, d, k が与えられます。 次のように定められた数列の k 項目の値を出力してください。 a_1 = x a_n = a_{n-1} + d (n ≧ 2) 解説 動的計画法では、 部分問題 に分ける DPテーブル を作成する ことが重要です。 部分問題に分ける 部分問題 について考える際は、「最後から」手順を追っていくことがポイントとなります。今回の問題では k 項目の値、すなわち a_k を得ることが目的です。ここで、与えられた数列の式から a_k は a_{k-1} + d で表されることが分かります。次に a_{k-1} について考えると、これは a_{k-2} + d で表されます。このようにして考えると、a_k は a_1 ~ a_{k-1} が分かれば求められることが分かります。 DPテーブルを作成する 先ほど、部分問題に分けて考えたことによって a_k は a_1 ~ a_{k-1} が分かれば求められることが判明しました。ということは、a_1 から順番に計算していけばいいということになります。ここで、計算結果を収める DPテーブル を作成します。今回は a_1 ~ a_k までの計算結果を格納したいため、DPテーブルは以下のようになります。ここで、DPテーブルの最初の値 dp[0] は当然 a_1 すなわち x となります。 DPテーブルの作成 dp = [None] * k dp[0] = x また、最初の値以降は前項の値に d を足したものであるため、DPテーブルは以下のように更新すれば良いことが分かります。 DPテーブルの更新 for i in range(1, k): dp[i] = dp[i-1] + d こうしてDPテーブルが作成できました。 解答例 以上の解説を踏まえた解答コードです。 問題文では Index が 1 からスタートしていますが、Python では 0 からスタートすることに注意してください。 解答例(Python) # 入力値の取得 x, d, k = map(int, input().split()) # DPテーブルの初期化 dp = [None] * k dp[0] = x # DPテーブルの更新 for i in range(1, k): dp[i] = dp[i-1] + d # 結果の表示 print(dp[k-1])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む