20210623のPythonに関する記事は28件です。

Python Challenge level 2

続けてPython Challenge を解いていきます。 level 2: http://www.pythonchallenge.com/pc/def/ocr.html Level 2 本の画像にキャプションがついています。 recognize the characters. maybe they are in the book, but MAYBE they are in the page source. これがヒントでしょう。文字を認識せよ。多分その本の中にある、もしくは「多分」このページのソースコードの中にあるだろう。ぐらいの意味ですので、ヒントに従いソースコードを見てみましょう。 ocr.html <html> <head> <title>ocr</title> <link rel="stylesheet" type="text/css" href="../style.css"> </head> <body> <center><img src="ocr.jpg"> <br><font color="#c03000"> recognize the characters. maybe they are in the book, <br>but MAYBE they are in the page source.</center> <br> <br> <br> <font size="-1" color="gold"> General tips: <li>Use the hints. They are helpful, most of the times.</li> <li>Investigate the data given to you.</li> <li>Avoid looking for spoilers.</li> <br> Forums: <a href="http://www.pythonchallenge.com/forums"/>Python Challenge Forums</a>, read before you post. <br> IRC: irc.freenode.net #pythonchallenge <br><br> To see the solutions to the previous level, replace pc with pcc, i.e. go to: http://www.pythonchallenge.com/pcc/def/ocr.html </body> </html> <!-- find rare characters in the mess below: --> <!-- %%$@_$^__#)^)&!_+]!*@&^}@[@%]()%+$&[(_@%+%$*^@$^!+]!&_#)_*}{}}!}_]$[%}@[{_@#_^{* @##&{#&{&)*%(]{{([*}@[@&]+!!*{)!}{%+{))])[!^})+)$]#{*+^((@^@}$[**$&^{$!@#$%)!@(& +^!{%_$&@^!}$_${)$_#)!({@!)(^}!*^&!$%_&&}&_#&@{)]{+)%*{&*%*&@%$+]!*__(#!*){%&@++ !_)^$&&%#+)}!@!)&^}**#!_$([$!$}#*^}$+&#[{*{}{((#$]{[$[$$()_#}!@}^@_&%^*!){*^^_$^ ]@}#%[%!^[^_})+@&}{@*!(@$%$^)}[_!}(*}#}#___}!](@_{{(*#%!%%+*)^+#%}$+_]#}%!**#!^_ )@)$%%^{_%!@(&{!}$_$[)*!^&{}*#{!)@})!*{^&[&$#@)*@#@_@^_#*!@_#})+[^&!@*}^){%%{&#@ ...続く 謎の記号列が延々とあります。ヒントとして find rare characters in the mess below 下部の乱雑記号の中から珍しい文字を探せ。 とあるのでこの中から探していけばいいようです。ただ、どう探していけばいいでしょうかというところで出てくるのが前問で使ったstr.maketransメソッドです。 str.maketrans 第 3 引数を指定する場合、文字列を指定する必要があり、それに含まれる文字が None に対応付けられます。 Noneに対応づけられるというのがわかりにくですが、要するにmaketransの第3引数に指定した文字列はtranslateしたときに削除されるということです。 というわけで、pythonプログラムの方針は固まりましたので書いてみましょう。 解答 2.py #!/usr/bin/env python3 removeChars = "#_!$@*+()[]{}%&^\n" table = str.maketrans("","",removeChars) coText=""" %%$@_$^__#)^)&!_+]!*@&^}@[@%]()%+$&[(_@%+%$*^@$^!+]!&_#)_*}{}}!}_]$[%}@[{_@#_^{* #省略 #@}&$[[%]_&$+)$!%{(}$^$}* """ print(coText.translate(table)) 出力結果 equality Level 3 ocr.html を equality.html に書き換えると次のレベルの扉が開かれました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

時変データを2値化して変化の時間間隔をもとめる

import pandas as pd import numpy as np import matplotlib.pyplot as plt #データ読み込み df = pd.read_excel('sampdata.xlsx') #変数格納 1行目(時間)と2行目(電圧) time = df.iloc[:,0] volt = df.iloc[:,1] #1,0のデータに変換 volt[volt>1] = 1 volt[volt<1] = 0 #電圧の微分を求める volt_diff = np.diff(volt.values) #電圧の微分と時間の数を合わせる time2 = time[1:] #電圧の微分が1以上の時間間隔と-1以下の時間間隔を求める time_diff_p = np.diff(time2.values[volt_diff>0]) time_diff_m = np.diff(time2.values[volt_diff<0]) #グラフ描画 fig = plt.figure(figsize=(6, 4), dpi=72, facecolor='skyblue', linewidth=10, edgecolor='green') ax = fig.add_subplot(1,1,1) ax.plot(time2[volt_diff>0].values[1:], time_diff_p/np.max(time_diff_p), marker='o', color='tomato', linestyle='None') ax.plot(time2[volt_diff<0].values[1:], time_diff_m/np.max(time_diff_m), marker='x', color='aqua', linestyle='None') ax.plot(time, volt, marker='None', color='orange', linestyle='-') #グラフ保存 fig.savefig('edge.png')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonでSymbolブロックチェーンとwebsocket通信を行う

Pythonでリアルタイムに検知するためにwebsocket通信を行ってみます。 ライブラリのインストール まず初めに必要なライブラリをインストールします。 websocket-client 1.1.0 pip install websocket-client 実装 websocketの実装サンプルが記載されているので真似ながら実装していきます。 import websocket # メッセージを受信した場合の処理. def on_message(ws, message): # 取得したメッセージの出力 json_data = json.loads(message) print(json.dumps(json_data, indent=2)) # メッセージにuidが含まれる場合は取得する. if 'uid' in json_data: uid = json_data['uid'] # 取得したuidを送信する body = '{"uid":"' + uid + '", "subscribe":"block"}' ws.send(body) def on_error(ws, error): print(error) def on_close(ws): print("closed") def on_open(ws): print("open") if __name__ == "__main__": websocket.enableTrace(True) # コネクションを作成 host = "ngl-dual-101.testnet.symboldev.network:3000" ws = websocket.WebSocketApp("ws://" + host + "/ws", on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open) # 処理実行 ws.run_forever() WebSocketAppでは、接続先とメッセージを受信した場合の処理の設定を行います。 この場合では引数:on_messageに対して、自分で定義したon_messageの関数を割り当てています。 on_error, on_close, on_openも同様です。 on_messageの処理はSymbolのwebsocketを利用した方法を参考にします。 websocketの接続が行われると、WebSocketAppで設定したノードからuidという識別文字列が送信されます。 継続的にブロックの状態を監視するために、UIDをもとにメッセージを作成します。 今回の例ではblockについて実装していますが、他にも監視できる種別があるため後述します。 UIDを送信した後、新しいブロックが作成されるたびに通知が行われるため、 その都度、on_messageで定義した処理が行われます。 受信するJSONの例 { "topic": "block", "data": { "block": { "signature": "73BCD511E9D276DA5C94335FDA0D1149023D736C488007E73144CA858DF0D8036D64320887138F213DE6B354BA012DE7D9B7069720516506C920A82679842C01", "signerPublicKey": "7665AD40455CDCADB1A9CFF4F4962D49D9BFE267741853113A32D85685B93870", "version": 1, "network": 152, "type": 33091, "height": "189010", "timestamp": "7733184559", "difficulty": "10000000000000", "proofGamma": "179B50CFF7B5CED61E56B5A2884904C95186EA2A96140F2A19E78489937A884B", "proofVerificationHash": "15FDBC81639F67804924EE4934064CCA", "proofScalar": "CF585558C104D86B65771F9EEFA3AAFDE4D7673C44C04F89C08AAA338820510E", "previousBlockHash": "C1B1F2D4D9BC17912CA74DA2B10476BA2392D87DD4B2EE766AD8BA3BB2721572", "transactionsHash": "0000000000000000000000000000000000000000000000000000000000000000", "receiptsHash": "9CF0EB26A3B6E7E6068D1BDB373AFEA055A043901F95326B391601F942323519", "stateHash": "B36F891CDC05494A9043C1E888D5825F51004C17B406025E9C8031199A6BE5E7", "beneficiaryAddress": "9881F6CC5C1639DEC640DF22187EB86B3385A58957D26FE4", "feeMultiplier": 0 }, "meta": { "hash": "211F50EB999179C4F2A0E760368BB7E5B0D859C1C230DA2C6A24DE07CB184FAA", "generationHash": "F22FEAB60452173570A4D3B04C3F07F91884B17388A62611E87F9D73DDD9AA59" } } } 監視できる種別について 詳細は公式のリファレンスを参照してください。 監視できる種別としては以下の種類が存在します。 {address}となっているものは、アドレスを指定することで特定アドレスの検知を行えます。 アドレスを指定しなかった場合は、指定したチャネルの全てのアドレスの情報が取得できます。 (confirmedAddedにて挙動の確認を行いました。) また、同時に複数のチャネルの監視が行えるため 用途に合わせてチャネルを組み合わせることで、柔軟な開発が行えそうです。 Channels Response data block BlockInfoDTO finalizedBlock FinalizedBlockDTO confirmedAdded/{address} TransactionInfoDTO unconfirmedAdded/{address} TransactionInfoDTO unconfirmedRemoved/{address} Hash partialAdded/{address} TransactionInfoDTO partialRemoved/{address} Hash cosignature/{address} CosignatureDTO status/{address} TransactionStatusDTO 他の言語での実装例 javasctipt java 参考 Symbol-SDKなしで着金イベント発火方法 https://qiita.com/bootarouapp/items/dfe55168986e63c28321 JavaでもSymbolブロックチェーン~送金編~ https://qiita.com/nem_takanobu/items/545a1b7fa98683a4c0da 新しいブロックの監視 https://docs.symbolplatform.com/ja/guides/blockchain/listening-new-blocks.html LIFE WITH PYTHON Python Tips: JSON を整形して表示したい https://www.lifewithpython.com/2018/04/python-json-prettification.html websocket-client 1.1.0 https://pypi.org/project/websocket-client/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでAmazon Rekognition カスタムラベルを利用して画像認識をやってみた。

はじめに 本記事が自身初投稿になります。拙い文章ですが、ぜひご覧頂けますと幸いです。 きっかけ 業務で開発中のwebアプリにて、ユーザーが身分証明書の画像をアップロードし、管理者が画像を閲覧できる機能を追加しました。(画像はweb画面に組み込まれて表示されます。) web画面で定められた横長の画像領域に対して縦画面で撮影された画像が見づらく表示されるということがあったため、縦画面または横画面どちらで撮影された画像でも身分証部分を識別してトリミングする機能があればなあと思いました。 また、先日AWS SAA認定試験に合格し、AWSで何かできないかなと考えたときに、特に上司からの指示はないのですが上記の機能が実現できないか挑戦してみました。 Amazon Rekognition とは AWSが提供する画像分析、動画分析サービスになります。特に機械学習の知識がなくとも、APIを使用することで画像や動画の物体、人物、テキスト、シーン、活動を特定できます。 カスタムラベル とは Amazon Rekognition カスタムラベルを使用して、ビジネスニーズに合わせた画像の物体やシーンを特定できます。たとえば、モデルを構築して、アセンブリライン上の特定の機械部品を分類したり、不健康な植物を検出したりできます。Amazon Rekognition カスタムラベルは、モデル開発の手間のかかる作業を処理するので、機械学習の経験は必要ありません。特定したい物体やシーンの画像を提供するだけで、残りはサービスが処理します。 要するに、画像をAWSに提供して自分の好きなようにラベル(特定したいもの)を設定できるということです。 カスタムラベルを用意する 今回は簡単な説明のみとさせて頂きます。 1. プロジェクトを作成する。 2. データセットを作成する。 3. 特定したものが写っている画像を複数用意し、データセットに画像を追加する。 4. それぞれの画像に自作のタグを付ける。 5. 新しいモデルをトレーニングする。(自分の場合は1時間以上かかりました。) カスタムラベルの作成については下記サイトを参考にさせていただきました。 第5回 Amazon Rekognition カスタムラベル(準備) pythonコード APIコードでPythonを選択すると、3種類のコードが表示されます。 モデルを開始するコード 画像を分析するコード モデルを停止するコード この3種類をそのまま使うだけでS3の画像を認識することができます。 1と3にはデータセットの画像やARNが自動で入力されているので、そのまま使用できるはずです。 画像を分析するコード #Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. #PDX-License-Identifier: MIT-0 (For details, see https://github.com/awsdocs/amazon-rekognition-custom-labels-developer-guide/blob/master/LICENSE-SAMPLECODE.) import boto3 import io from PIL import Image, ImageDraw, ExifTags, ImageColor, ImageFont def display_image(bucket,photo,response): # Load image from S3 bucket s3_connection = boto3.resource('s3') s3_object = s3_connection.Object(bucket,photo) s3_response = s3_object.get() stream = io.BytesIO(s3_response['Body'].read()) image=Image.open(stream) # Ready image to draw bounding boxes on it. imgWidth, imgHeight = image.size draw = ImageDraw.Draw(image) # calculate and display bounding boxes for each detected custom label print('Detected custom labels for ' + photo) for customLabel in response['CustomLabels']: print('Label ' + str(customLabel['Name'])) print('Confidence ' + str(customLabel['Confidence'])) if 'Geometry' in customLabel: box = customLabel['Geometry']['BoundingBox'] left = imgWidth * box['Left'] top = imgHeight * box['Top'] width = imgWidth * box['Width'] height = imgHeight * box['Height'] fnt = ImageFont.truetype('/Library/Fonts/Arial.ttf', 50) draw.text((left,top), customLabel['Name'], fill='#00d400', font=fnt) print('Left: ' + '{0:.0f}'.format(left)) print('Top: ' + '{0:.0f}'.format(top)) print('Label Width: ' + "{0:.0f}".format(width)) print('Label Height: ' + "{0:.0f}".format(height)) points = ( (left,top), (left + width, top), (left + width, top + height), (left , top + height), (left, top)) draw.line(points, fill='#00d400', width=5) image.show() def show_custom_labels(model,bucket,photo, min_confidence): client=boto3.client('rekognition') #Call DetectCustomLabels response = client.detect_custom_labels(Image={'S3Object': {'Bucket': bucket, 'Name': photo}}, MinConfidence=min_confidence, ProjectVersionArn=model) # For object detection use case, uncomment below code to display image. # display_image(bucket,photo,response) return len(response['CustomLabels']) def main(): bucket='MY_BUCKET' photo='MY_IMAGE_KEY' model='arn:aws:rekognition:ap-northeast-1:913059777631:project/custom-label-for-insurance-card/version/custom-label-for-insurance-card.2021-06-23T09.53.17/1624409597359' min_confidence=95 label_count=show_custom_labels(model,bucket,photo, min_confidence) print("Custom labels detected: " + str(label_count)) if __name__ == "__main__": main() image.show()でカスタムラベルを識別し、枠で囲った画像が表示されます。画像をBoundingBoxの範囲でトリムして別名保存することで画像認識+トリミングを実現できます。 終わりに 実は今週Pythonを触り始めたばかりで、まだ基礎を勉強中です。 今回は日常の業務の中で自分が学習中のAWSとPythonを活用できるユースケースを思いついたのが嬉しかったので、初めて記事を書いてみました。 今後も備忘録や作成したものなどを共有できればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テリア犬6種を区別するAIアプリを作成してみた

はじめに テリアはイギリスなどが原産で32種類が知られています。小型犬から大型犬までサイズも豊富で、短毛・長毛と被毛の長さも様々です。 この中から容姿の似たオーストラリアン・シルキー・テリア、ノーリッチ・テリア、アイリッシュ・テリア、チベタン・テリア、スコティッシュ・テリア、ヨークシャー・テリアを見分けるAIアプリを作ってみました。 実行環境 ・Python 3.8.5 ・Visual Studio Code 1.52.1 ・Google colaboratory 画像データセットの作成 スタンフォード 犬のデータセットからテリア犬6種の画像をダウンロードしました。 データは全部で1065画像あり、250×250のサイズに変更し、以下のコードでdataset.pklというファイルを作成しローカル環境に保存しました。 dataset.py import os import cv2 import numpy as np import pickle path = './images' list1 = os.listdir(path) print(list1) # 画像サイズの横と縦 width = 250 height = 250 # 犬種の画像を格納する配列 dog_imgs = [] # 犬種の画像に対応した犬種のindexを格納する配列 dog_breeds = [] # 犬種とindexを対応させた配列 dog_breeds_list = [] # 各犬種の画像リストを作成する # 各犬種の空リストを作成し、犬種ごとのフォルダから画像を1枚ずつ追加 for folder_name in os.listdir(path): path_folder_name = os.listdir(path+ "/" + folder_name) dog_breeds_list.append(folder_name[10:]) for i in range(len(path_folder_name)): img = cv2.imread(path + "/" + folder_name+ "/" + path_folder_name[i]) img = cv2.resize(img, (width, height)) dog_imgs.append(img) dog_breeds.append(len(dog_breeds_list)-1) print(len(dog_imgs)) print(len(dog_breeds)) # データセットの作成・保存 X = np.array(dog_imgs) y = np.array(dog_breeds) z = np.array(dog_breeds_list) with open("./dataset.pkl", "wb") as f: pickle.dump((X, y, z), f) モデルを構築する/学習する ローカル環境に保存したdataset.pklをGoogle colaboratoryにアップロードします。 Google colaboratoryで下記のコードを実行し、VGG16を使って転移学習を行いモデルを構築しました。テストデータでの正答率は81.6%でした。 作成したモデルをダウンロードしローカル環境に保存します。 model.py import matplotlib.pyplot as plt import pickle import cv2 import numpy as np from sklearn.model_selection import train_test_split from tensorflow.keras.applications.vgg16 import VGG16 from tensorflow.keras.utils import to_categorical from tensorflow.keras.layers import Dense, Dropout, Flatten, Input from tensorflow.keras.models import Model, Sequential from tensorflow.keras import optimizers # モデルの保存 import os from google.colab import files # データセットのload with open("./dataset.pkl", "rb") as f: X, y, z= pickle.load(f) # 画像サイズの横と縦 width = 250 height = 250 # トレーニングデータとテストデータに分割 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) X_train = X_train.reshape(-1, height, width, 3) X_test = X_test.reshape(-1, height, width, 3) y_train = to_categorical(y_train) y_test = to_categorical(y_test) # VGG16のload input_tensor = Input(shape=(height, width, 3)) vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor) # モデルの構築 top_model = Sequential() top_model.add(Flatten(input_shape=vgg16.output_shape[1:])) top_model.add(Dense(512, activation='relu')) top_model.add(Dropout(0.2)) top_model.add(Dense(256, activation = 'relu')) top_model.add(Dropout(0.2)) top_model.add(Dense(128, activation = 'relu')) top_model.add(Dropout(0.2)) top_model.add(Dense(6, activation='softmax')) model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output)) # modelの19層目までがvggのモデル for layer in model.layers[:19]: layer.trainable = False model.compile(loss='categorical_crossentropy', optimizer=optimizers.SGD(lr=1e-4, momentum=0.9), metrics=['accuracy']) model.fit(X_train, y_train, batch_size=64, epochs=10, verbose=1, validation_data=(X_test, y_test)) # 精度の評価 scores = model.evaluate(X_test, y_test, verbose=1) print('Test loss:', scores[0]) print('Test accuracy:', scores[1]) for i in range(5): x = X_test[i] x = cv2.cvtColor(x, cv2.COLOR_BGR2RGB) plt.imshow(x) plt.show() pred = np.argmax(model.predict(x.reshape(1,height,width,3))) print(z[pred]) print(pred) print(y_test[i]) # resultsディレクトリを作成 result_dir='results' if not os.path.exists(result_dir): os.mkdir(result_dir) # 重みを保存 model.save(os.path.join(result_dir,'model.h5')) files.download('/content/results/model.h5') テリア犬判別アプリ ローカルに保存したモデルを用いて、HTML、CSSコードを記述しテリア犬6種を識別するアプリを作成します。AIが判断した確率も載せることにしました。 main.py import os from flask import Flask, request, redirect, render_template, flash from werkzeug.utils import secure_filename from tensorflow.keras.models import Sequential, load_model from tensorflow.keras.preprocessing import image import numpy as np classes = ["オーストラリアン・シルキー・テリア","ノーリッチ・テリア","アイリッシュ・テリア","チベタン・テリア","スコティッシュ・テリア","ヨークシャー・テリア"] # 画像サイズの横と縦 width = 250 height = 250 UPLOAD_FOLDER = "uploads" ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg']) app = Flask(__name__) def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS model = load_model('./model.h5') @app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': if 'file' not in request.files: flash('ファイルがありません') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('ファイルがありません') return redirect(request.url) if file and allowed_file(file.filename): filename = secure_filename(file.filename) file.save(os.path.join(UPLOAD_FOLDER, filename)) filepath = os.path.join(UPLOAD_FOLDER, filename) #受け取った画像を読み込み、np形式に変換 img = image.load_img(filepath, grayscale=False, target_size=(height,width)) img = image.img_to_array(img) data = np.array([img]) #変換したデータをモデルに渡して予測する result = model.predict(data)[0] predicted = result.argmax() per = result[predicted] * 100 pred_answer = "これは {} です(確率:{:.2f}%)".format(classes[predicted], per) return render_template("index.html",answer=pred_answer) return render_template("index.html",answer="") if __name__ == "__main__": app.run() index.html <!DOCTYPE html> <html lang='ja'> <head> <meta charset='UTF-8'> <meta name='viewport' content="device-width, initial-scale=1.0"> <meta http-equiv='X-UA-Compatible' content="ie=edge"> <title>Terrier Classifier</title> <link rel='stylesheet' href="./static/stylesheet.css"> </head> <body> <header> <div style="text-align: left;"></div><a class='header-logo' href="#">Terrier Classifier</a></div> </header> <div class='main'> <h2> AIがテリア犬を見分けます!</h2> <p>画像を送信してください</p> <form method='POST' enctype="multipart/form-data"> <input class='file_choose' type="file" name="file"> <input class='btn' value="submit!" type="submit"> </form> <div class='answer'>{{answer}}</div> </div> <footer> </footer> </body> </html> index.css header { background-color: #76B55B; height: 60px; margin: -8px; display: flex; flex-direction: row-reverse; justify-content: space-between; } .header-logo { color: #fff; font-size: 25px; margin: 15px 25px; } .main { height: 370px; } h2 { color: #444444; margin: 90px 0px; text-align: center; } p { color: #444444; margin: 70px 0px 30px 0px; text-align: center; } .answer { color: #444444; margin: 70px 0px 30px 0px; text-align: center; } form { text-align: center; } footer { background-color: #F7F7F7; height: 110px; margin: -8px; position: relative; } 検証 テリア犬6種のWikipediaのトップ画像を認識できるか確認します。 オーストラリアン・シルキー・テリア:判定× ヨークシャー・テリア(確率:86.26%) ノーリッチ・テリア:判定× スコティッシュ・テリア(確率:73.09%) アイリッシュ・テリア:判定× チベタン・テリア(確率:89.32%) チベタン・テリア:判定○ チベタン・テリア(確率:99.99%) スコティッシュ・テリア:判定○ スコティッシュ・テリア(確率:57.38%) ヨークシャー・テリア:判定○ ヨークシャー・テリア(確率:99.99%) Wikipediaは判定しやすい画像を載せていると思われるので検証に用いましたが、3勝3敗でした。 ランダム回答での正解数の期待値は1/6 × 6 = 1であり、ランダム予測は上回っています。 犬の大きさや向きが様々であったり、学習用・検証用とも縦長、横長と様々のサイズの画像をすべて正方形に成形しているため識別率が低下していると考えられました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GameGan-codeが動いたので、動かし方のメモ

概要  GameGanとはゲームの画像と操作履歴をGeneratorに学ばせて、そのゲームを再現できるというものです。  このコードを動作させる方法を説明します。  動作させる方法のメモなので、処理内容については詳しくは説明していないので、内容については論文を読むことを推奨します。 動作環境  以下の環境で動作させましたが、設定で小さくしたモデルをギリギリ動作可能というレベルでした。  おそらく実用レベルのものを作ろうと思うと、ビデオメモリお化けやGPU界のポルシェとかが必要になるだろうなと思っています。(うまく設定すれば必要ないかもしれません。)  また、最初はWindowsで動作させようとしたのですが、pytorchのバージョン1.4.0がうまくインストールできなかったので、Ubuntuで動作させることを推奨します。 OS Ubuntu 20.04 LTS CPU intel core i7 8700 GPU GeForce RTX 2060 SUPER メモリ 32GB データの作成  まずは上のリポジトリをクローンし、以下のコマンドを実行する。 docker image build -t wm:1.0 -f docker/Dockerfile.wm . docker container run -p 8888:8888 --gpus '"device=0"' --detach -it --name wm wm:1.0 docker attach wm  すると 「/home/ubuntu/WM」にいるはずなので、「cd WorldModels」で移動する。  次に以下のコードを実行することで、データの作成が開始される、結構時間がかかるので気長に待つ。  処理が終わると、「results/WorldModels/DoomTakeCover-v0/record」の下にデータが入っている。 bash launch_scripts/wm_doom.bash  コンテナをCtrl+P,Ctrl+Qで抜け、「sudo docker cp」を使って、コンテナ内のデータをホストにある「GameGAN_code/data/vizdoom」へコピーする。 コードの修正と加筆  このままではデータを読み込めないので、「GameGAN_code/data/dataloader.py」を以下のように修正します。  これで、正常にゲームの画像と操作履歴を読み込むことができます。 dataloader.py #globをimportする import glob class vizdoom_dataset(data_utils.Dataset): def __init__(self, opts, set_type=0, permute_color=False, datadir=''): self.opts = opts self.set_type = set_type self.permute_color = permute_color self.samples = [] #ここをコメントアウト #list_=os.listdir(datadir + "/*.npz") #下の1行を追加 list_ = glob.glob(datadir + "/*.npz") num_data = len(os.listdir(datadir)) print(num_data) if set_type == 0: sample_list = list(range(0, int(num_data*0.9))) else: sample_list = list(range(int(num_data*0.9), num_data)) #ここをコメントアウト、ファイル名はシード値です #for el in sample_list: # self.samples.append('%s/%d.npy' % (datadir, el)) #下の2行を追加 for el in list_: self.samples.append(el) self.end_bias = 0 if utils.check_arg(self.opts, 'end_bias'): self.end_bias = self.opts.end_bias def __len__(self): return len(self.samples) def __getitem__(self, idx): fn = self.samples[idx] #下をコメントアウト、読み込むデータにitem()はありません #data = np.load(fn, allow_pickle=True).item() #下の1行を追加 data = np.load(fn, allow_pickle=True) #以下コードが続く  ビデオメモリ少ないGPUを使う場合は、GameGAN_code/scripts/vizdoom_multi.shを以下のように書き換えてください。  以下の設定でGeForce RTX 2060 SUPERがギリギリ動くことを確認しました。  なにをしたのかというと、私も中途半端にしか理解していないのですが、generatorへ入力する画像の枚数を表す「num_steps」を半分に減らし、初期の予測画像を再起的に処理する枚数を設定する「warm_up」を「num_steps」に設定した数よりも少なくし、何かの窓長を設定する「config_temporal」を「num_steps」以下にしています。(実は「config_temporal」を減らすだけで動くかもしれません。) #!/usr/bin/env bash python main_parallel.py \ --data vizdoom:./data/vizdoom \ --log_dir ./results/ \ --num_steps 16 \ --warm_up 8 \ --warmup_decay_epoch 60 \ --bs 4 \ --num_components 2 \ --fine_mask \ --config_temporal 16 \ --do_memory \ --cycle_loss \ --alpha_loss_multiplier 0.000075 \ --softmax_kernel \ --sigmoid_maps \ --save_epoch 5 \ --rev_multiply_map \ --num_gpu 1 \ --temperature 0.1 \ --nfilterG 16 \ --spade_index 2 \ --seed 1 \ --img_size 64 \ --end_bias 0.5 \ --simple_blocks 実行  「GameGAN_code/scripts/vizdoom_multi.sh」を実行すれば大体、1日くらいすれば、ちゃんとした画像が作成されるようになります。  結果はtensorboardで確認できます。  「tensorboard --port=PORT --logdir=./results」 参考文献 Learning to Simulate Dynamic Environments with GameGAN https://github.com/nv-tlabs/GameGAN_code https://github.com/zacwellmer/WorldModels
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KaggleチュートリアルのTitanicに取り組んでみた

はじめに Kaggleの初心者向けチュートリアルであるTitanic(タイタニック)に取り組んでみました。 最初は精度80%超えることを目標としていましたが、チュートリアルなので可視化とか色々触ってみることを目的に途中で変えたので最終的な精度は77%です。 参考にした記事 Kaggleに登録したら次にやること ~ これだけやれば十分闘える!Titanicの先へ行く入門 10 Kernel ~ KaggleチュートリアルTitanicで上位2%以内に入るノウハウ タイタニック号の乗客の生存予測〜80%以上の予測精度を超える方法(モデル構築&推論編) Notebook コードやグラフ等は下記のNotebookを参照してください。 Kaggle Notebook やってみて感じたこと データの背景について理解することが大切 データにも現れているが、避難では女性や子供が優先されていた。 港の値はS(サウサンプトン)が多いが、これは出発の港であるため。 事故は深夜に起きた。 データの傾向から想像することが大切 いいチケット、高い運賃の人は生存率が高い。→高いお金を出せる(社会的地位があると思われる)人も優遇されていた可能性があると思われる。 日中なら家族や同室の人でもバラバラに行動していることも考えられるが、深夜ならみんな部屋で寝ている可能性が高く、行動を一緒にした可能性が高い。 1人や5人以上で行動したと思われる人は生存率が低い。→1人ではどう逃げればいいか分からず逃げれなかったり、人数が多いと身動きが取りにくかったのではないかと思われる。 欠損値の補完はデータによって単純に全体の最頻値や中央値、類似データの最頻値や中央値、モデルを作って推定等といった手法がある。 これまで数値や区分値といった値にばかり目がいっていたが、Nameのような文字列データから傾向を読み取ることも大切。 データセットにある特徴量の取捨選択だけでなく、データから特徴量を作ることで精度向上に役立てられる。 次の予定 今度は回帰問題であるHouse Priceをやってまた記事に書こうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kubeflow入門

はじめに Kubeflowとは何なのか。 どんな知識が必要でどんなメリットがあるの? エコシステムなんかはどうなっているの? 現状これらの問いの答えとなるような記事を見つけることができなかったため、本記事を執筆しました。 本記事ではKubeflowとは何なのか。そしてどのようなツールで構成されているのか図を交えながらできる限りわかりやすくまとめました。(本記事で使用している図に関してはご連絡いただければ元データをお送りできます。) MLOps、Kubeflowについての日本語の学習リソースがほとんどないですが、MLOpsの概念や技術が日本で広まる一助になればいいなと思っています。 Kubeflowとは 近年の機械学習の急速な発展に伴って、機械学習を本番運用する機会が増えてくる中、てデータサイエンティストと開発者の連携やモデルのバージョン管理などの課題が重要視されてきています。そんな中で出てきた概念がMLOpsという考え方で、簡単に説明するとDevOpsを機械学習開発に応用しようというようなものです。 詳しくはこちらの記事を参照していただくとわかりやすいかと思います。 MLOpsを実現する上での重要な考え方としてML Pipelineとがあり、これはデータ抽出、特徴長エンジニアリング、モデル学習、モデルデプロイといった機械学習タスクを一連のパイプラインとして定義します。 MLOpsではこのパイプラインをワークフローとして自動化し効率的に管理を行います。 そして、これらのパイプラインを定義しそれらを管理、統合するためのツールがKubeflowです。 KubeflowはKubernetes上でML Pipelineワークフローを管理するためのオープンソースツール群です。 元々はGoogleが社内でTensorFlowを用いたMLワークフローを管理するために使用していましたが、それを一般化しオープンソースにしました。 Kubeflowを用いてML Pipelineを実現することには以下のメリットがあります。 スケーラビリティ Kubernetesのスケーリング機能を活かして、データ処理やモデル学習などを必要に応じてスケーリング可能です。 再利用性 コンテナベースなKubernetesを用いることで1度作成したパイプラインや環境をクラウドオンプレミス問わず実現可能です。 柔軟性 必要に応じて様々なツールと統合でき、柔軟なワークフローを定義可能です。また、Kubeflowツールの中でKubeflow Pipelineのみを既存のk8sクラスターに適応するといったこともできます。 また、上に記した図にある通り、パイプラインの管理にBeamやAirflowを用いることが可能です。そのほかにもMLflowなどのツールがありますが、前述のメリットから現状KubeflowがMLOpsのstate of artなようです。 Kubeflowの構成 Central Dashboard Central dashboardはKubeflowのWebフロントエンドです。パイプラインの作成やKatibを使ったモデルの最適化、Jupyter Notebooksを使った分析、Artifact(パイプラインによって生成されたモデルやデータ)の管理などをWeb UIで行うことができます。 JupyterHub MLプロジェクトは最初はデータの分析、MLモデルのプロトタイプの作成などの実験を行うことから始まります。 KubeflowではJupyterHubという複数人で共同で実験を進めるためのツールを提供しています。JupyterHubは複数人のJupyter Notebookを一元管理することができます。また、各ノートブックごとにGPUやCPUメモリを設定でき、ノートブックごとのリソースの最適化を行うことができるようになっています。 JuypterHubで実行されるノートブックはコンテナインスタンス上で動作しており、インスタンスにはデフォルトでkubectlがインストールされているので、ノートブックからkubectlコマンドの実行ができます。 Jupyterインスタンスはdefault-editorというサービスアカウントで実行されており、以下のnamescopeのk8sリソースへの権限を持っています。 Pods Deployment Services Jobs TFJobs PyTorchJobs ちなみに、 ノートブックからkubectlを実行するには!kubectl apply -f <YAML_FILE>みたいな感じにします。 Training Operators JupyterHubはプロトタイピングなどには有効ですが、本番運用の際にはKubeflowが提供するコンポーネントを利用してモデルの学習を自動化します。 モデル学習における分散処理だとかはOperatorと呼ばれるコントローラによって管理、実行されます。 例えば、TensorFlowの学習を実行する際には学習パラメータとWorker数を定義することでOperatorが自動で調整してくれます。以下はTFXを利用してパイプラインを定義する例です。 from tfx.orchestration import pipeline from tfx.orchestration.kubeflow import kubeflow_dag_runne pipeline_name ='example_pipeline' direct_num_workers = 2 # 省略 pipeline_root = ... components = ... runner_config = ... output_dir = ... output_filename = ... beam_arg = [f"--direct_num_workers={direct_num_workers}"] p = pipeline.Pipeline( pipeline_name=pipeline_name, pipeline_root=pipeline_root, components=components, beam_pipeline_args=beam_arg, ) kubeflow_dag_runner.KubeflowDagRunner( config=runner_config, output_dir=output_dir, output_filename=output_filename, ).run(p) TensorFlow以外にも、Pytorch、MXNet、MPI、Chainerなどの学習をサポートしています。 Kubeflow Pipelines Kubeflow PipelinesはML Pipelineの実行をオーケストレートします。 Kubeflow PipelinesはコンテナネイティブなワークフローエンジンであるArgo Workflowsをベースとしていて、有向非巡回グラフ(DAG)で定義します。 以下はKubeflow Pipelineの例です。 Kubeflowではそれぞれのパイプラインコンポーネントに対し、Kubernetesがコンテナを割り当て実行しています。 Demystifying Kubeflow pipelines: Data science workflows on Kubernetes – Part 1 by CANONICAL Kubeflow Pipelinesは以下のコンポーネントから構成されています。 Python SDK パイプラインをdomain-specific-language(DSL)を用いて定義できます。 DSL compiler Pythonコードからパイプラインの設定をYAMLへと変換します。 Pipeline Service YAMLから設定を読み取りパイプラインの実行を作成します。 k8s resources Pipeline Serviceがk8s APIを呼び出し。必要なcustome resource definitions(CRDs)を作成します。 Orchestration controller Orchestration controllerはCRDsに応じてコンテナの実行をおこないます。Orchestratin controllersは実行するパイプラインの設定に応じていくつか用意されており、例としてtask-drivenなワークフローではArgo Workflowが用いられています。 Artifact storage MetadataとArtifactsの2種類のデータを管理します。 MetadataはExperiments、ジョブ、Runのスカラー情報でMySQLなどのDBに保存します。Artifactはパイプラインのパッケージなどであり、MinIOサーバーやGCSなどバケットを保存先として使用します。 MetadataやArtifactはKubeflowにより追跡され、デバッグやパイプラインJobを把握するために利用されます。 Hyperparameter Tuning 現在公開されているハイパーパラメータチューニングのツールは特定のMLライブラリへの依存が強かったり、サポートされている手法の数が少ないという課題があります。 Kubeflowの提供するKatlibはGoogle Vizierをベースとして作られたツールで幅広い要件に柔軟に対応でき、ベイズ最適化などの強力な手法をサポートしています。 KatlibはPytorch、TensorFlow、MXNetなど主要なライブラリをサポートしています。 Kubeflowでは本番運用のためにモデル提供ツールの利用をサポートしており、TFServing、Seldon serving、PyTorch serving、TensorRTなど多数のモデル提供ツールを利用可能です。 Model Inference 本番運用で機械学習モデルを提供する場合、基本的に機械学習モデルをAPIとして提供することになります。 機械学習モデルはデータやモデル構造、ハイパーパラメータの変更があったときに、提供しているモデルを更新する必要があります。それらを人手により管理するのは大きな人的コストがかりますし、予期せぬバグにつながってしまう可能性があります。そこでモデルのバージョニングやルーティングを提供してくれるのかモデル提供ツールです。 KFServing KFServing by Kubeflow official Kubeflowはモデル推論、自動スケーリング、ネットワーキング、ヘルスチェックを一般化した包括的モデル提供ツールであるKFServingを提供しています。KFServingはIstioとKnative servingをベースとしてサーバレスコンテナとして実装されており、高速化つスケーラビリティに優れた実装を実現しています。 KFServingはPreprocessor、Predictor、PostProcessorの3つのコンポーネントから構成されています。Preprocessorでは学習時に用い前処理と同様の処理を適応することができ、Postprocessorでは推論結果をユーザに返すときに必要な処理(例えばフォーマットの変換など)をオプションで指定できます。 Metadata Metadata管理は日々MLモデルを構築していく中で、それぞれのモデルに関係する情報を管理、追跡するのに非常に便利です。 Metadetaには以下の情報を登録可能です。 学習に利用したデータソース それぞれのパイプラインで生成されたArtifact パイプラインコンポーネントの実行情報(ログなど) パイプラインと関係するその他の情報 これらの情報は、例えば、今までに生成された全てのモデルの表示、パイプラインの実行状況の表示などセントラルダッシュボードを通して有益な情報を提供してくれます。 その他のコンポーネント これまでに説明してきたコンポーネント以外にもMinIOやIstio、Knative、Apach Sparkなど表面上見えていませんが、陰で活躍してくれているコンポーネントのおかげでKubeflowは成り立っています。 それぞれのコンポーネントの詳しく説明したいところですが、1つのコンポーネントで1つの記事が書けてしまうくらいなのでKubeflowではどのように貢献しているのかを簡単に説明して、詳しい説明は他の記事に譲ることにします。 簡単に説明すると、MiniIOは特定のクラウドに依存しないMLワークフローの構築のためのストレージを提供し、Istioはロードバランシングからサービスディスカバリ、モニタリングなど幅広くサービスメッシュとしての機能を提供しています。 Knativeは前述の通りKFServingなどのモデル提供の部分をサーバレスに実行するために使われていて、Apach Sparkをサポートすることにより大規模なデータ処理をDataprocやElastic Map Reduceなどをバックエンドに用いて実装することができます。 終わりに 本記事で紹介したKubeflowコンポーネントはそれぞれが相互作用しあってKubeflowエコシステムを支えています。本記事を通して、Kubeflowの概要を掴んでもらえれば嬉しいです。 学習リソースは日本語では現状ほとんど見つけることができないので、本記事が学習者の助けになれば幸いです。 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonでHTMLをPDFに変換する際に画像を埋め込む

はじめに 昨今の"DX"ブームにより、自動化検討が進んでいます。レポートの自動化は誰もが思いつくDXの初手ともいうべきものです。その際に、Pythonでレポートを作成しPDFで書き換え不能にして保管する場面があるかもです。 私はさらに、そのPDFに画像を埋め込むという場面に遭遇しました。AtomエディタやVSCodeを使っている人であればMarkdownをPDFに変換するのが簡単だと思うでしょうか。今回PythonでHTMLをPDFに変換する際に、ローカルに保存した画像の扱いに手こずったため備忘録として以下の記事を残します。 環境 とりあえずWindows10の環境で実行します。pdfkitというライブラリが動くようであれば他のOSでもかまいません。 画像の準備 今回は以下のガイ・フォークスマスクをPDFに埋め込みます。権威に対する叛乱の象徴です。'Guy-Fawkes.jpg'という名前で保存します。 ライブラリのインストール:pdfkit pdfkitというPythonライブラリをインストールします。また、pdfkitのためにmkhtmltopdfというexeソフトウェアを使います。 画像埋め込みの要諦:Base64 URI HTMLにローカルの画像を埋め込む際、画像をBase64 URIという文字列へ変換し、そのURIをimgタグのsrcで参照させれば画像が表示されます。これはJupyter NotebookでノートブックからHTMLをエクスポートした際にHTMLファイルに画像が埋め込む方法と同じです(Jupyterから出力したHTMLファイルのソース中で、base64で検索するとヒットします)。 Base64 URIを参照するとPNG画像は <img src="data:image/png;base64,..." />のように表記されます。JEPG画像を埋め込むときは"data:image/jpg;base64,..."のように書きます。"..."の部分はハッシュ化されたような人の目で理解できない文字列になっています。非常に長くなるので基本的に割愛します。 実装1:素直に画像を読み出してBase64を使う HTMLのヘッダーを追加する関数を定義します。これを使うことでHTMLの内容だけに注力して記録できます。 def set_html_meta(body, lang='ja'): """ <!DOCTYPE html> is essential """ assert lang in ('ja', 'en'), "it can take ('ja', 'en'); but %s was fed"%lang html = f'<!DOCTYPE html><html lang="{lang}">\n <meta charset="utf-8">\n' html += '<style> body { font-size: 1rem; } </style>\n <body>\n' html += body + '\n </body>\n</html>' return html 参考:Open base64 String Image in Jupyter Notebook Without Saving 次に、画像ファイルをopen()で読み出して、Base64を使ってエンコードします。エンコードされた文字はbytes型で、通常のstring型と異なるためPython3で比較/結合する前には.decode()でstring型へ直す必要があります。 import base64 imgname='Guy-Fawkes.jpg' encoded_str = base64.b64encode(open(imgname,'rb').read()) print(type(encoded_str)) data_uri = 'data:image/jpg;base64,'+encoded_str.decode() body="<img src='{}' width='200px'/>".format(data_uri) html=set_html_meta(body) with open('test.html', 'w') as out: """for debug purpose""" for row in html.split('\n'): out.write(row+'\n') # <class 'bytes'> 最後に、出来上がったHTMLファイルをPDFへ変換します。ここでpdfkitを使います。pdfkitの設定について、pdfkit.configurationで指定することができます。mkhtmltopdf.exeへのパスを指定しないと動作しないので気を付けてください。 import pdfkit options = { 'page-size': 'A4', 'margin-top': '1in', 'margin-right': '1in', 'margin-bottom': '1in', 'margin-left': '1in', 'encoding': "UTF-8", 'no-outline': None, 'disable-smart-shrinking': '', } conf_ = pdfkit.configuration(wkhtmltopdf='C:/wkhtmltox/bin/wkhtmltopdf.exe') pdfkit.from_string(html, 'test.pdf', options=options, configuration=conf_) #Loading pages (1/6) #Counting pages (2/6) #Resolving links (4/6) #Loading headers and footers (5/6) #Printing pages (6/6) #Done # Out[]: True 出来上がったtest.pdfを開くと、下図のように画像が埋め込まれていました。 躓いた点 変換したいHTMLファイルに<!DOCTYPE html>が記載されていないと、pdfkitがHTMLファイルを認識できず画像埋め込みに失敗します。正確にHTMLの文法を守るようにしましょう。 実装2:画像をBytesIOに書き出してgetvalue()する 参考:Is it possible to create encoded base64 URL from Image object? Base64の理解を深めるために少しExerciseします。PillowのImageオブジェクトとして読み出した画像をnumpy arrayに変換して、matplotlibで表示することができます。 import numpy as np from PIL import Image import matplotlib.pyplot as plt from io import BytesIO imgname='Guy-Fawkes.jpg' # Take in base64 string and return a numpy image array def stringToRGB(base64_string): imgdata = base64.b64decode(base64_string) image = Image.open(BytesIO(imgdata)) return np.array(image) byte_string=open(imgname,'rb').read() encoded_string = base64.b64encode( byte_string ) plt.imshow(stringToRGB(encoded_string)) plt.axis('off') plt.show()# Guy-Fawkesの画像がでる 上記のコードを踏まえて、BytesIOを利用していきます。以下のコードではPillowオブジェクトをBytesIOに保存し、BytesIOオブジェクトのgetvalue()メソッドを利用したらbytes型が得られました。これは画像データをrawのまま取得した形で、これをbase64.b64encode関数で変換するとBase64 URIが得られます。あとは先ほどと同じですね。 imgname='Guy-Fawkes.jpg' im = Image.open(imgname) output = BytesIO() im.save(output, format='JPEG') im_data = output.getvalue() print(type(im_data)) image_data = base64.b64encode(im_data) if not isinstance(image_data, str): # Python 3, decode from bytes to string image_data = image_data.decode() data_uri = 'data:image/jpg;base64,' + image_data body="<img src='{}' width='200px'/>".format(data_uri) html2=set_html_meta(body) #<class 'bytes'> 実行例1と同様にしてPDFを保存します。 options = { 'page-size': 'A4', 'margin-top': '1in', 'margin-right': '1in', 'margin-bottom': '1in', 'margin-left': '1in', 'encoding': "UTF-8", 'no-outline': None, 'disable-smart-shrinking': '', } conf_ = pdfkit.configuration(wkhtmltopdf='C:/wkhtmltox/bin/wkhtmltopdf.exe') pdfkit.from_string(html2, 'test2.pdf', options=options, configuration=conf_) #Loading pages (1/6) #Counting pages (2/6) #Resolving links (4/6) #Loading headers and footers (5/6) #Printing pages (6/6) #Done # Out[]: True ファイルを開けば以下のようにtest2.pdfが生成されていることが確認できます。 応用 Markdownを使ってMarkdown→HTMLへの変換をしたり、Jinja2を使ってMarkdownをテンプレートとして扱い動的にコンテンツを埋め込むことにすれば、機械的に様々なレポートを生成するソフトが作成できます。是非実装してみてください。 終わりに V for Vendetta(映画)は何度見ても名作です。1605年の火薬陰謀事件から400年後にガイ・フォークスをモチーフにした V for Vendettaが公開されたのは非常に上手かったなと思います。 “Beneath this mask there is more than just flesh. Beneath this mask there is an idea... and ideas are bulletproof." - Alan Moore, V for Vendetta 安直な"DX"ブームへ反抗していきましょう。 関連文献 Markdownの議事録 → HTML → PDF へ変換出力 【Python】WebページやHTMLをPDF出力する - wkhtmltopdfのインストールに言及している。 PythonでマークダウンをPDFに変換する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python virtualenvの使用方法

virtualenvの使い方 ちょっとしたメモです。 仮想環境の作成 virtualenv myenv 仮想環境に入る source myenv/bin/activate 仮想環境から出る deactivate 仮想環境でインストールしたパッケージを保存する pip freeze > requirements.txt 仮想環境でインストールした環境を再インストールする。 pip install -r requirements.txt
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】DjangoでHandsontableをAjaxしてみる 【JaveScript】

はじめに HandsontableというJavaScriptライブラリを使うと、WEBでExcelのようなスプレッドシートライクな入力が可能になります。が、更新方法が分からなかったので、いろいろ調べてみました。 なお、JavaScriptについては全くの初学者なので、おかしいところも多々あると思いますが、ご笑覧いただければ幸いです。 できたもの データベース管理システム(PostgreSQL)からデータを取得し、Handsontableを使ってテーブルを表示。 ブラウザ上でデータを更新し(garlicをAPPLEへ)、上書きボタンをクリックすると… Ajax(非同期通信)を使って、ポスグレ上のデータを更新します。 念のため、管理サイトでも更新を確認。 準備 概要は以下の通りです。 django admin startproject conf . タイムゾーン、言語の設定 STATICFILES_DIRSの設定 TEMPLATESの設定 DATABASE(PostgreSQL)の設定 アプリケーション作成 $ python manage.py startapp hot アプリケーションを作成します。 conf/settings.py INSTALLED_APPS = [ 'hot.apps.HotConfig', # 以下省略 ] 次にsettings.pyにアプリを登録します。 conf/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('hot/', include('hot.urls') # 以下省略 ] confフォルダのurls.pyを修正します。 hot/urls.py from django.urls import path from . import views app_name = 'hot' urlpatterns = [ path('', views.Top.as_view(), name='top'), # path('ajax_update/', views.ajax_update, name='ajax_update') ] hotフォルダ内にurls.pyを作成し、上記のように書きますクラスベースビューのTopはテーブルを表示するページで、関数ビューのajax_updateはデータを上書きするためのページです。Ajaxなので、ページ遷移はしません。 モデルを作成 hot/models.py from django.db import models class Merchandise(models.Model): merchandise = models.CharField('merchandise', max_length=255) price = models.IntegerField('price') origin = models.CharField('origin', max_length=255) def __str__(self): return self.merchandise hot/admin.py from django.contrib import admin from .models import Merchandise admin.site.register(Merchandise) $ python manage.py makemigrations hot $ python manage.py migrate $ python manage.py createsuperuser モデルを作成します。商品名と価格と生産地をデータとしました。管理サイトへの登録、マイグレーション、スーパーユーザーの作成を忘れずに。 適当に三つほどデータを入れておきます。 テーブルを表示するページ(Top)を作成 hot/views.py import psycopg2 from django.http.response import JsonResponse from django.shortcuts import render from django.views import generic from .models import Merchandise class Top(generic.TemplateView): template_name = 'table/top.html' # contextをテンプレートファイルに渡す。 def get_context_data(self, **kwargs): # はじめに継承元のメソッドを呼び出す context = super().get_context_data(**kwargs) # データベースに接続→データ取得→切断 connect_str = '' connect_str += "dbname=test1 " connect_str += "user=postgres " connect_str += "password=password" conn = psycopg2.connect(connect_str) cur = conn.cursor() cur.execute('select * from hot_merchandise') table = cur.fetchall() cur.close() conn.close() # handsontable用のデータを作成 key = ('id', 'marchandise', 'price', 'origin') data = [] for record in table: data.append(dict(zip(key, record))) columns = [] for k in key: columns.append(dict(data=k)) colHeaders = list(key) context['data'] = data context['columns'] = columns context['colHeaders'] = colHeaders return context Topページのビューを作成します。データベースからデータを取得し、Handsontable用にかたちを整えて、テンプレートファイルに渡します。テーブルの名称はhot_merchandiseとなっています。アプリ名小文字_モデル名小文字になるみたいです。 参考 # table(整形前のデータ) [(1, 'carrot', 150, 'kanagawa'), (2, 'garlic', 200, 'aomori'), (3, 'onion', 300, 'hyogo')] # data [ {'id': 1, 'merchandise': 'carrot', 'price': 150, 'origin': 'kanagawa'}, {'id': 2, 'merchandise': 'garlic', 'price': 200, 'origin': 'aomori'}, {'id': 3, 'merchandise': 'onion', 'price': 300, 'origin': 'hyogo'} ] # columns [ {'data': 'id'}, {'data': 'merchandise'}, {'data': 'price'}, {'data': 'origin'} ] # colHeaders ['id', 'merchandise', 'price', 'origin'] ちなみに、handsontableに渡すデータの形状は上記のような感じです。 base.html {% load static %} <!doctype html> <html lang="ja"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <!-- handsontable --> <script src="{% static 'hot/jquery.min.js' %}"></script> <script src="{% static 'hot/handsontable.full.min.js' %}"></script> <link rel="stylesheet" href="{% static 'hot/handsontable.full.min.css' %}"> <title>HandsontableでAjax</title> </head> <body> <!-- ナビゲーションバー --> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="{% url 'home:top' %}">Test</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="{% url 'home:top' %}">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'table:top' %}">Table</a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'hot:top' %}">HOT</a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'admin:index' %}">Admin</a> </li> </ul> </div> </nav> <div class="container"> {% block content %}{% endblock %} </div> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <!-- jQueryはslim版じゃないやつを使う。slimはajaxに対応していないため。 --> <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> {% block extrajs %}{% endblock %} </body> </html> ベースとなるテンプレートファイルを作成します。handsontableのjsやcssはCDNも公開されています。この記事では触れていないアプリ(home, table)へのリンクもありますが、そちらは無視してください。 hot/top.html {% extends 'base.html' %} {% load static %} {% block content %} <h3 class="mt-3">Table</h3> <div id="grid"></div> <div class="mt-1"> <button type="button" id="btn_update_data" class="btn btn-primary">上書き</button> </div> {% endblock %} {% block extrajs %} <script> 'use strict'; // 変数の定義 var data = {{ data | safe }}; var columns = {{ columns | safe }}; var colHeaders = {{ colHeaders | safe }}; // テーブルを表示 var container = document.getElementById('grid'); var hot = new Handsontable(container, { data: data, columns: columns, columnSorting: { // CDNはこの書き方じゃない? column: 0, sortOrder: 'asc' }, rowHeaders: true, colHeaders: colHeaders, filters: true, dropdownMenu: true }); </script> {% endblock %} top.htmlはbase.htmlをextendsします。block contentには見出しとテーブルと上書きボタンを設置しています。テーブルを書き込むdiv要素のid属性を'grid'としています。それを受けてextrajsブロックでgetElementById('grid')としています。 ブラウザ上ではこのような画面が表示されます。この時点でブラウザ上でデータは編集可能ですが、データベースの更新はできないので、リロードすると元に戻ります。 ↑ブラウザ上では編集できますが、更新はできません↓ ↑リロードすると元に戻ってしまいます。 データを上書きする データを上書き更新できるようにしたいと思います。おおまかな手順としては、テーブルの下に設置した上書きボタンを押下すると、その時点のデータを取得し、データベースを更新するようにしたいと思います。 hot/top.html {% block extrajs %} <script> // 省略 // Ajaxでデータベースを更新 function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken'); function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } }); // ↑ ここまではおまじない $('#btn_update_data').on('click', function(event){ event.preventDefault(); // ボタンをクリックした時点のデータを取得 var data_hot = hot.getData(); console.log(data_hot); // Ajax実行 $.ajax({ 'url': '{% url "hot:ajax_update" %}', 'type': 'POST', 'data': { 'temp_data': 'aaa', 'str_hot': JSON.stringify(data_hot), 'str_col': JSON.stringify(columns) }, 'dataType': 'json' }) .done(function(data) { window.alert("done!!"); }) .fail(function(){ window.alert("error!!"); }); }); </script> {% endblock %} hot/top.htmlのextrajsブロックにスクリプトを追記します。前半はおまじないらしく、よくわかりません…。 $('#btn_update_data').on('click', function(event)以降がデータの上書きです。上書きボタンがクリックされたら、データを取得します。次にhot:ajax_upで指定した関数を実行するのですが、その時にdataを渡します。dataの中身は'temp_data'と'str_hot'と'str_col'です。'temp_data'は適当なデータです。'str_hot'はテーブルのデータをJSON形式に変換したものです。今回はJavaScript(Handsontable)からPythonへデータを渡すのですが、いろいろ調べてなんとかこの形にたどり着きました。(もっと良い方法があったら知りたい…) hot/urls.py app_name = 'hot' urlpatterns = [ path('', views.Top.as_view(), name='top'), path('ajax_update/', views.ajax_update, name='ajax_update') # 追加 ] top.htmlにhot:ajax_updateというURLを指定しましたが、その様なURLは無いので作ります。次にviews.pyにajax_updateという関数ビューを作ります。 hot/views.py def ajax_update(request): # POSTされたデータを受け取る。この時点では文字列。 str_hot = request.POST.get('str_hot') # リストっぽい文字列[[1, "apple", 200, "aomori"], ...] str_col = request.POST.get('str_col') # リストっぽい文字列[{'data':'id'}, {'data':'merchandise}, ...] # 文字列をリストに変換 exec('data_hot={}'.format(str_hot), globals()) exec('data_col={}'.format(str_col), globals()) lis_col = [] for elm in data_col: lis_col.append(elm['data']) # 辞書を作成 # [{'id':1, 'marchandise'='apple', 'price':100, 'origin':"aomori"}, {...}, ...] lis_dic = [] for row in data_hot: lis_dic.append(dict(zip(lis_col, row))) # データベースに接続 connect_str = '' connect_str += "dbname=test1 " connect_str += "user=postgres " connect_str += "password=password" conn = psycopg2.connect(connect_str) cur = conn.cursor() # データ更新 for dic in lis_dic: id = dic['id'] sql = 'UPDATE hot_merchandise SET' for k,v in dic.items(): if k != 'id': if type(v) is str: sql += f" {k}='{v}'," elif type(v) is int or type(v) is float: sql += f" {k}={v}," sql = sql[:-1] # 末尾のカンマを削除 sql += f" where id={id};" print(sql) # 参考: "UPDATE hot_merchandise SET marchandise='orange', price=200, origin='aomori' WHERE id=1" cur.execute(sql) conn.commit() # データベース切断 cur.close() conn.close() # レスポンスを返さないといけないらしいので、適当に… return JsonResponse({'ret': 'return'}) いよいよデータベースの更新です。いろいろ書いてありますが、やっていることはデータを受け取り、形を整えて、SQL文を生成し、実行の繰り返しです。ポイントは、ajaxで送られてきたデータは一見リストの様な形式をしていますが文字列です。なので、それをリストに戻してやる必要があります。いろいろ試した結果exec関数を使うことになりました。(もっと良い方法があったら知りたい…切実に…) 実行してみる ブラウザ上でデータを更新し、上書きボタンをクリックします。 データの更新がうまくいけば、このようなウィンドウが開きます。ページ遷移はしません。 前回はリロードすると元に戻ってしまいましたが、今回は大丈夫です。 管理サイト上でもデータベースが更新されています! おわりに なんとか動いてよかった…。あやふやなところがたくさんあるので、少しずつ勉強していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローソク足に自動的にトレンドラインを引く

以前の記事でローソク足を作成しました。今回は、それにトレンドラインを自動的に引いてみたいと思います。 まずは弊社の株価を取得します。 import pandas_datareader.data as web df = web.DataReader("3040.JP", "stooq").dropna().sort_index() 次にローソク足を作成する関数を定義して実行します。この詳細は以前の記事を参照してください。 from bokeh.plotting import figure, show, output_notebook from bokeh.models import ColumnDataSource import pandas as pd def candlestick(df, plot_width=600, plot_height=300): df = df.reset_index() source = ColumnDataSource(df) inc = ColumnDataSource(df[df.Close >= df.Open]) dec = ColumnDataSource(df[df.Open > df.Close]) w = (df.index[1] - df.index[0]) / 2 # X軸の1メモリの半分 fig = figure(plot_width=plot_width, plot_height=plot_height) fig.segment('index', 'High', 'index', 'Low', source=source, color="black") fig.vbar('index', w, 'Open', 'Close', source=inc, line_color="black", fill_color="white") fig.vbar('index', w, 'Open', 'Close', source=dec, line_color="black", fill_color="black") fig.xaxis.major_label_overrides = { i: pd.to_datetime(date).strftime('%Y-%m-%d') for i, date in enumerate(source.data['Date']) } fig.xaxis.bounds = (0, df.index[-1]) # X軸の範囲を明示的に指定 fig.outline_line_color = 'black' return fig fig = candlestick(df['2020-8-1':]) output_notebook() show(fig) ※ Qiitaでは出力結果を記事内に埋め込めないので、イメージ画像と出力されたファイル例へのリンクで代替します。ぜひColabで実際に動かしてみてください。 このローソク足にトレンドラインをひくために、まずはピークの点を取得する必要があります。ピークの点とは、前後のある程度の範囲において最も価格が高いあるいは低い点です。 前後の7点(前に3点、後ろに3点)の範囲を取得するにはpandasのrolling(7, center=True)を使います。 df.rolling(7, center=True) Rolling [window=7,center=True,axis=0] これはそれぞれ7個の要素を持った配列の配列とみなせます。その様子を見てみましょう list(df[:10].rolling(7, center=True)) [ Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973, Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973 2016-02-09 556.75 561.63 546.52 548.47 15178, Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973 2016-02-09 556.75 561.63 546.52 548.47 15178 2016-02-10 536.27 536.27 511.90 523.12 6974, Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973 2016-02-09 556.75 561.63 546.52 548.47 15178 2016-02-10 536.27 536.27 511.90 523.12 6974 2016-02-12 479.23 514.82 477.78 510.43 23588, Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973 2016-02-09 556.75 561.63 546.52 548.47 15178 2016-02-10 536.27 536.27 511.90 523.12 6974 2016-02-12 479.23 514.82 477.78 510.43 23588 2016-02-15 510.43 530.92 487.52 495.82 7179, Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973 2016-02-09 556.75 561.63 546.52 548.47 15178 2016-02-10 536.27 536.27 511.90 523.12 6974 2016-02-12 479.23 514.82 477.78 510.43 23588 2016-02-15 510.43 530.92 487.52 495.82 7179 2016-02-16 530.92 565.52 516.28 541.15 32203, Open High Low Close Volume Date 2016-02-08 563.09 589.41 558.71 588.92 14973 2016-02-09 556.75 561.63 546.52 548.47 15178 2016-02-10 536.27 536.27 511.90 523.12 6974 2016-02-12 479.23 514.82 477.78 510.43 23588 2016-02-15 510.43 530.92 487.52 495.82 7179 2016-02-16 530.92 565.52 516.28 541.15 32203 2016-02-17 536.27 541.15 487.52 490.45 45946, Open High Low Close Volume Date 2016-02-09 556.75 561.63 546.52 548.47 15178 2016-02-10 536.27 536.27 511.90 523.12 6974 2016-02-12 479.23 514.82 477.78 510.43 23588 2016-02-15 510.43 530.92 487.52 495.82 7179 2016-02-16 530.92 565.52 516.28 541.15 32203 2016-02-17 536.27 541.15 487.52 490.45 45946 2016-02-18 502.16 515.81 492.39 493.38 26870, Open High Low Close Volume Date 2016-02-10 536.27 536.27 511.90 523.12 6974 2016-02-12 479.23 514.82 477.78 510.43 23588 2016-02-15 510.43 530.92 487.52 495.82 7179 2016-02-16 530.92 565.52 516.28 541.15 32203 2016-02-17 536.27 541.15 487.52 490.45 45946 2016-02-18 502.16 515.81 492.39 493.38 26870 2016-02-19 493.38 495.32 488.98 488.98 8615, Open High Low Close Volume Date 2016-02-12 479.23 514.82 477.78 510.43 23588 2016-02-15 510.43 530.92 487.52 495.82 7179 2016-02-16 530.92 565.52 516.28 541.15 32203 2016-02-17 536.27 541.15 487.52 490.45 45946 2016-02-18 502.16 515.81 492.39 493.38 26870 2016-02-19 493.38 495.32 488.98 488.98 8615 2016-02-22 488.98 502.16 488.98 497.27 4307] ここから高値だけを取り出すには以下のようにします。 list(df[:10].rolling(7, center=True).High) [Date 2016-02-08 589.41 Name: High, dtype: float64, Date 2016-02-08 589.41 2016-02-09 561.63 Name: High, dtype: float64, Date 2016-02-08 589.41 2016-02-09 561.63 2016-02-10 536.27 Name: High, dtype: float64, Date 2016-02-08 589.41 2016-02-09 561.63 2016-02-10 536.27 2016-02-12 514.82 Name: High, dtype: float64, Date 2016-02-08 589.41 2016-02-09 561.63 2016-02-10 536.27 2016-02-12 514.82 2016-02-15 530.92 Name: High, dtype: float64, Date 2016-02-08 589.41 2016-02-09 561.63 2016-02-10 536.27 2016-02-12 514.82 2016-02-15 530.92 2016-02-16 565.52 Name: High, dtype: float64, Date 2016-02-08 589.41 2016-02-09 561.63 2016-02-10 536.27 2016-02-12 514.82 2016-02-15 530.92 2016-02-16 565.52 2016-02-17 541.15 Name: High, dtype: float64, Date 2016-02-09 561.63 2016-02-10 536.27 2016-02-12 514.82 2016-02-15 530.92 2016-02-16 565.52 2016-02-17 541.15 2016-02-18 515.81 Name: High, dtype: float64, Date 2016-02-10 536.27 2016-02-12 514.82 2016-02-15 530.92 2016-02-16 565.52 2016-02-17 541.15 2016-02-18 515.81 2016-02-19 495.32 Name: High, dtype: float64, Date 2016-02-12 514.82 2016-02-15 530.92 2016-02-16 565.52 2016-02-17 541.15 2016-02-18 515.81 2016-02-19 495.32 2016-02-22 502.16 Name: High, dtype: float64] そして、それぞれの中の最高値を取り出すのは以下。 df[:20].rolling(7, center=True).High.max() Date 2016-02-08 NaN 2016-02-09 NaN 2016-02-10 NaN 2016-02-12 589.41 2016-02-15 565.52 2016-02-16 565.52 2016-02-17 565.52 2016-02-18 565.52 2016-02-19 565.52 2016-02-22 541.15 2016-02-23 515.81 2016-02-24 553.34 2016-02-25 553.34 2016-02-26 556.26 2016-02-29 567.95 2016-03-01 567.95 2016-03-02 567.95 2016-03-03 NaN 2016-03-04 NaN 2016-03-07 NaN Name: High, dtype: float64 上記がそれぞれの前後の範囲での最高値なので、それがその日の高値と一致する場合、その日は前後の範囲での高値のピークであることが分かります。以下で実際に算出してみましょう。Trueとなっている日が高値のピークの日です。 df[:20].rolling(7, center=True).High.max() == df[:20].High Date 2016-02-08 False 2016-02-09 False 2016-02-10 False 2016-02-12 False 2016-02-15 False 2016-02-16 True 2016-02-17 False 2016-02-18 False 2016-02-19 False 2016-02-22 False 2016-02-23 False 2016-02-24 False 2016-02-25 False 2016-02-26 False 2016-02-29 False 2016-03-01 False 2016-03-02 False 2016-03-03 False 2016-03-04 False 2016-03-07 False Name: High, dtype: bool これを利用して高値と安値のピークを赤丸と青丸で表示するローソク足を生成する関数を作成します。 def show_peaks(target): fig = candlestick(target) target = target.reset_index() upPeak = target[target.rolling(7, center=True).High.max() == target.High] # (1) fig.circle(upPeak.index, upPeak.High, color='red') dnPeak = target[target.rolling(7, center=True).Low.min() == target.Low] # (2) fig.circle(dnPeak.index, dnPeak.Low, color='blue') return fig, upPeak, dnPeak fig, _, _ = show_peaks(df['2020-8-1':]) output_notebook() show(fig) 上記では、pandasのファンシーインデックスを利用しています。これは、array[[True, False, True, False]]のようにインデックスに論理値の配列を渡すことでTrueに対応した位置の値で構成された配列を返す機能です。この場合では1番目の3番目の値で構成された配列が返ることになります。従って、上記の(1)や(2)のようにすることでピークの要素だけで構成された配列を取得できます。後で使用するので、関数ではそれらのピーク値も戻り値として返しています。 ピーク値を結ぶことでトレンドラインを引くことができます。そこでまず高値のピーク値を結ぶ直線を描きます。 from itertools import combinations from scipy.stats import linregress target = df['2020-8-1':] fig, upPeak, _ = show_peaks(target) # (3) lgs = [] for select in combinations(upPeak.index, 3): # (4) peaks = [upPeak.High[i] for i in upPeak.index if i in select] # (5) lgs.append(linregress(select, peaks)) # (6) for lg in [lg for lg in lgs if lg.pvalue < 0.05]: # (7) fig.line([0, len(target)], [lg.intercept, lg.intercept + len(target) * lg.slope], color='red') output_notebook() show(fig) (3) upPeakに高値のピークを取得しています。 (4) combinations()は組合せを生成します。ここではupPeakのインデックスから3点を選ぶ組合せを生成してselectに格納し、繰り返します。 (5) 選ばれたインデックス(X座標)に対応する値(Y座標)をpeaksに格納します。 (6) linregress()は引数に渡された点を結ぶ直線を生成します。第一引数がX座標の配列、第二引数がY座標の配列です。 (7) pvalueは直線の信頼度を表す値です。値が小さいほど各点が直線上に乗っている度合いが大きいと言えます。ここでは0.05未満の直線のみを描画しています。 上記では全ての高値のピーク値を対象に3点の組合せを選んでいるので、非常に遠く離れた点も含まれてしまいます。範囲が広がりすぎると無意味なトレンドラインの可能性が高くなるので、範囲を絞りましょう。 from itertools import combinations from scipy.stats import linregress target = df['2020-8-1':] fig, upPeak, _ = show_peaks(target) lgs = [] for r in upPeak.rolling(5): # (8) for select in combinations(r.index, 3): peaks = [upPeak.High[i] for i in upPeak.index if i in select] lgs.append(linregress(select, peaks)) for lg in [lg for lg in lgs if lg.pvalue < 0.05]: fig.line([0, len(target)], [lg.intercept, lg.intercept + len(target) * lg.slope], color='red') output_notebook() show(fig) (8) rolling(5)によってupPeakの中で連続する5点ずつがグループ化されます。各グループがrに代入され、繰り返します。 高値のピークの連続した5点のなかで直線を構成するものによるトレンドラインが引けています。 同様に安値のトレンドラインを生成しましょう。 from itertools import combinations from scipy.stats import linregress target = df['2020-8-1':] fig, _, dnPeak = show_peaks(target) lgs = [] for r in dnPeak.rolling(5): for select in combinations(r.index, 3): peaks = [dnPeak.Low[i] for i in dnPeak.index if i in select] lgs.append(linregress(select, peaks)) for lg in [lg for lg in lgs if lg.stderr < 0.5]: fig.line([0, len(target)], [lg.intercept, lg.intercept + len(target) * lg.slope], color='blue') output_notebook() show(fig) 高値と安値のピークを同時に使ったトレンドラインは以下のように生成します。 from itertools import combinations from scipy.stats import linregress from pandas import concat target = df['2020-8-1':] fig, upPeak, dnPeak = show_peaks(target) lgs = [] peaks = concat([upPeak.rename(columns={'High': 'price'}).price, dnPeak.rename(columns={'Low': 'price'}).price]).sort_index() # (9) for r in peaks.rolling(7): for select in combinations(r.index, 3): prices = [peaks[i] for i in select] lgs.append(linregress(select, prices)) for lg in [lg for lg in lgs if lg.stderr < 0.5]: fig.line([0, len(target)], [lg.intercept, lg.intercept + len(target) * lg.slope], color='green') output_notebook() show(fig) (9) concat()を使って、upPeakとdnPeakを連結し、インデックス順にソートしています。 トレンドラインが発生する前の時点では描画しないようにするには、以下のようにして始点を設定します。 from itertools import combinations from scipy.stats import linregress from pandas import concat target = df['2020-8-1':] fig, upPeak, dnPeak = show_peaks(target) peaks = concat([upPeak.rename(columns={'High': 'price'}).price, dnPeak.rename(columns={'Low': 'price'}).price]).sort_index() lgs = [] for r in peaks.rolling(7): for select in combinations(r.index, 3): prices = [peaks[i] for i in select] lgs.append((select, linregress(select, prices))) for lg in [lg for lg in lgs if lg[1].pvalue < 0.05]: select, lin = lg fig.line([select[0], len(target)], [lin.intercept + select[0] * lin.slope, lin.intercept + len(target) * lin.slope], color='green') output_notebook() show(fig) ほぼ同じトレンドラインがいくつも重なっているのが気になります。それらの内、一つだけを表示する処理を追加しましょう。 from itertools import combinations from scipy.stats import linregress from pandas import concat from math import atan, degrees def trendlines(peaks, last_index, rolling=7) -> []: std = peaks.std() items = [] for r in peaks.rolling(rolling): for select in combinations(r.index, 3): prices = [peaks[i] for i in select] items.append((sorted(select), linregress(select, prices))) lines = [] for item in items: lin = item[1] if lin.pvalue < 0.05: lines.append(item) if len(lines) > 0: thAngle = 0.5 thDistance = 5 newLines = [lines[0]] for line1 in lines[1:]: for line2 in newLines: if (abs(degrees(atan(line1[1].slope / std)) - degrees(atan(line2[1].slope / std))) <= thAngle and abs(line1[1].intercept + line1[1].slope * last_index - line2[1].intercept - line2[1].slope * last_index) <= thDistance * std ): # (10) break else: newLines.append(line1) lines = newLines return lines target = df['2020-8-1':] fig, upPeak, dnPeak = show_peaks(target) peaks = concat([upPeak.rename(columns={'High': 'price'}).price, dnPeak.rename(columns={'Low': 'price'}).price]).sort_index() lines = trendlines(peaks, len(target)) for line in lines: pos, lin = line fig.line([pos[0], len(target)], [lin.intercept + pos[0] * lin.slope, lin.intercept + len(target) * lin.slope], color='green') output_notebook() # 出力先をノートブックに設定 show(fig) (10) 二つの直線の角度が0.5度以下で、右端におけるy座標の差分が5以下の場合、同一直線とみなします。 それらしいトレンドラインが引けるようになりました。ただ、実際に投機の判断をする場合、現在の値、つまり一番右端の値と大きく離れたラインは参考になりません。そこで、現在の値に近いトレンドラインを4本だけ引いてみます。 target = df['2020-8-1':] fig, upPeak, dnPeak = show_peaks(target) peaks = concat([upPeak.rename(columns={'High': 'price'}).price, dnPeak.rename(columns={'Low': 'price'}).price]).sort_index() lines = trendlines(peaks, len(target)) pos = len(target) - 1 price = (df.High.iloc[pos] + df.Low.iloc[pos]) / 2 lines = [line for line in sorted(lines, key=lambda x:abs(x[1].intercept + x[1].slope * pos - price))][:4] # (11) for line in lines: pos, lin = line fig.line([pos[0], len(target)], [lin.intercept + pos[0] * lin.slope, lin.intercept + len(target) * lin.slope], color='green') output_notebook() # 出力先をノートブックに設定 show(fig) (11) posの位置の価格とトレンドライン上の価格の差の絶対値でソートして、小さい順に4個を取得します。 これで一応完成です。ただ、現時点だけでなく、過去の時点でのトレンドラインから有益な知見が得られるかもしれません。そこで、現時点を動かせるようにしてみましょう。 以下では引数で指定した位置より前の情報でトレンドラインを生成するtrendlines()を定義しています。その後、右半分の各時点でのトレンドラインを取得して蓄積します。さらに図中に現時点を表す緑の縦棒を表示しています。そしてBokehのカスタム機能を使って、その縦線をドラッグできるようにし、その変化に伴って対応するトレンドラインだけを表示するようにしました。 %time from bokeh.plotting import figure, show, output_notebook, output_file from bokeh.models import Slope, Span, CustomJS from bokeh import events from itertools import combinations from scipy.stats import linregress from math import atan, degrees def trendlines(peaks, pos, offset, rolling=7) -> []: std = peaks.std() lines = [] for r in peaks[(pos - offset < peaks.index) * (peaks.index <= pos)].rolling(rolling): for select in combinations(r.index, 3): prices = [peaks[i] for i in select] lin = linregress(select, prices) if lin.pvalue < 0.05: lines.append(lin) if len(lines) > 0: thAngle = 0.5 thDistance = 5 newLines = [lines[0]] for line1 in lines[1:]: for line2 in newLines: if (abs(degrees(atan(line1.slope / std)) - degrees(atan(line2.slope / std))) <= thAngle and abs(line1.intercept + line1.slope * pos - line2.intercept - line2.slope * pos) <= thDistance * std ): break else: newLines.append(line1) lines = newLines return lines target = df['2020-8-1':] fig, upPeak, dnPeak = show_peaks(target) peaks = concat([upPeak.rename(columns={'High': 'price'}).price, dnPeak.rename(columns={'Low': 'price'}).price]).sort_index() offset = 50 rolling = 7 lines = [None] * offset prev = None for pos in range(offset, len(target)): if prev is None or pos - rolling//2 - 1in peaks.index: prev = trendlines(peaks, pos, offset, rolling=rolling) price = (target.High.iloc[pos] + target.Low.iloc[pos]) / 2 lines.append([line for line in sorted(prev, key=lambda x:abs(x.intercept + x.slope * pos - price))][:4]) lines.append([line for line in sorted(prev, key=lambda x:abs(x.intercept + x.slope * len(target) - price))][:4]) slopes = [] for line in lines[-1]: slope = Slope(gradient=line.slope, y_intercept=line.intercept, line_color='red') fig.add_layout(slope) slopes.append(slope) span = Span(location=len(target), dimension='height', line_color='green', line_width=5, line_alpha=0.2) fig.add_layout(span) def pan_event(span, lines, slopes): return CustomJS(args=dict(span=span, lines=lines, slopes=slopes), code=""" var loc = Math.round(parseFloat(cb_obj['x'])) if (span.location != loc) { span.location = loc if (loc < lines.length && lines[loc]) { for (var i = 0; i < Math.min(lines[loc].length, slopes.length); ++i) { slopes[i].visible = true slopes[i].gradient = lines[loc][i][0] slopes[i].y_intercept = lines[loc][i][1] } for (var i = lines[loc].length; i < slopes.length; ++i) { slopes[i].visible = false } } else { for (var i = 0; i < slopes.length; ++i) { slopes[i].visible = false } } } """) fig.js_on_event(events.Tap, pan_event(span=span, lines=lines, slopes=slopes)) fig.js_on_event(events.Press, pan_event(span=span, lines=lines, slopes=slopes)) fig.js_on_event(events.Pan, pan_event(span=span, lines=lines, slopes=slopes)) fig.toolbar.active_drag = None output_notebook() show(fig) CPU times: user 3 µs, sys: 0 ns, total: 3 µs Wall time: 4.77 µs さらに十字線とその値を表示する機能を追加しておきましょう。詳細は以前の記事をご参照ください。 from bokeh.models import ColumnDataSource, CrosshairTool, Label xlabel = Label(x_units='data', y_units='screen', render_mode='css', border_line_width=1, border_line_color='black', border_line_alpha=1.0, background_fill_color='white', background_fill_alpha=1.0, visible=False) ylabel = Label(x_units='screen', y_units='data', render_mode='css', border_line_width=1, border_line_color='black', border_line_alpha=1.0, background_fill_color='white', background_fill_alpha=1.0, visible=False) fig.add_layout(xlabel) fig.add_layout(ylabel) # https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html def display_event(source, xlabel, ylabel): return CustomJS(args=dict(source=source, xlabel=xlabel, ylabel=ylabel), code=""" try { xlabel.visible = true xlabel.x = cb_obj['x'] xlabel.y = 25 // 日付を下辺に表示 xlabel.x_offset = -40 xlabel.y_offset = -20 var date = new Date(source.data['Date'][Number(cb_obj['x']).toFixed(0)]) xlabel.text = date.toISOString().substr(0, 10) } catch (e) {} ylabel.visible = true ylabel.y = cb_obj['y'] ylabel.x = cb_obj['sx'] - 20 // 価格を十字の右に表示 ylabel.y_offset = -10 ylabel.text = Number(cb_obj['y']).toFixed(0) """) def leave_event(xlabel, ylabel): return CustomJS(args=dict(xlabel=xlabel, ylabel=ylabel), code=""" xlabel.visible = false ylabel.visible = false """) source = ColumnDataSource(target) fig.js_on_event(events.MouseMove, display_event(source=source, xlabel=xlabel, ylabel=ylabel)) fig.js_on_event(events.MouseLeave, leave_event(xlabel=xlabel, ylabel=ylabel)) fig.add_tools(CrosshairTool()) fig.toolbar.active_drag = None output_notebook() # 出力先をノートブックに設定 output_file('trendline.html') # 出力先にファイルも指定 show(fig) 完成です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習とポケモン(1)

やりたいこと 久しぶりの投稿です。 クラクラは少し休憩です。 今回はポケモンのタイプの強さの数値的評価方法について考えてみました。この投稿で紹介しているコードは以下のノートブックにもまとめてあります。 https://colab.research.google.com/drive/1t0q9Fj6MNTY-itminHmxhe04_7S8GUoP?usp=sharing はじめに ポケモンのタイプは18タイプがあり、タイプ相性の関係が複雑であり、そのため、数値評価が難しい。そこでページランク手法を用いてタイプの強さを評価することを考える。ポケモンの強さを攻撃面と耐性面に分け、それぞれページランクを用いてそれぞれ数値化する。その後、タイプの強さを求めた攻撃面、耐久面の演算で評価するものとする。 ページランクとは ページランクは、Webページの重要度を決定するためのアルゴリズムである.検索エンジンのGoogleにおいては,検索語に対する適切な結果を得るために用いられている中心的な技術である.Webページにおいては多くのページからリンクされている,良質なページからリンクされているとそのページの重要度が高くなる. ページランクは $MP=P$ の解を要素の和が 1 になるように正規化することで求めることができる.ここで$M$は遷移確率行列,$P$は重要度のベクトルを表している.式の意味としては「注目したタイプの重要度=$\sum$(注目している度合 $\times$ 注目したタイプの重要度)」と考えることができる. ポケモンの強さの数値化の説明 ここでは簡単のため、水、草、炎、氷の4タイプのみを用いてページランク手法について説明する。図で表すと以下のようになる。ピンク色の矢印が2倍、黒色が等倍、灰色が0.5倍の倍率を表している。 この相性関係を有効グラフとみなすと、隣接行列は以下のように表すことができる。 \begin{matrix} 炎 &水 &草 &氷 \end{matrix} \\ \begin{pmatrix} 0.5 & 0.5 & 2 & 2 \\ 2 & 0.5 & 0.5 & 1 \\ 0.5 & 2 & 0.5 & 1 \\ 0.5 & 0.5 & 2 & 0.5 \end{pmatrix} \begin{matrix} 炎 \\ 水\\ 草\\ 氷\\ \end{matrix} 耐久面の評価方法 まず、ページランク手法を用いるために先程の隣接行列の転置し、列ごとの総和が1になるように正規化する。そして、ページランクを用いてそれぞれのノードの大きさを求める。この値の意味について考えると、列が攻撃する側のタイプ、行が攻撃される側のタイプであるため,行の要素の和が大きいとそのタイプが受けるダメージが大きいことを意味している。つまり、$MP=P$において、 (ノードの重要度) = $\sum$ {(ノードnの重要度) $\times$(nからの攻撃の重み} と考えることができる。そのため、弱点を疲れやすいタイプや等倍以上で入るタイプが多いほど大きな値となり、値が大きいと耐久面が低いと見なすことができる。4タイプでのの計算結果は炎0.21、水0.23、草0.29、氷0.25となった。 攻撃面の評価方法 有効グラフの矢印の向きを逆にしたものを用いることとする。耐久面の計算と同様に求めると$MP=P$は (ノードの重要度) = $\sum$ {(ノードnの重要度) $\times$(nへの攻撃の重み} とみなすことができ、今度は弱点をつきやすい、半減以下で受けられにくい(一貫性が高い)タイプほど大きな値となる。4タイプでの計算結果は炎0.27、水0.26、草0.26、氷0.20となった。 タイプの強さの評価 タイプの強さを(タイプの強さ)=(攻撃面)/(耐久面)でそれぞれ計算し、わかりやすいように全体の和が100になるように変換し、その値をタイプの強さと考える。4タイプでの計算結果は炎31.3、水27.5、草21.6、氷19.6となった。 18タイプでのタイプの強さの数値化 結果は以下の通りである。 概ね合っているように感じられる。 ノーマルとドラゴンが低いようにも感じるが… 今後したいこと 今回の評価方法では「耐久面で弱いタイプに耐久面で弱ければ弱い」「攻撃面で強いタイプに攻撃面で強ければ強い」という考え方の計算方法であるため、「耐久面で強いタイプに攻撃面で強ければ強い」のような計算方法ができればなおいいと感じた。 また、今回は単タイプに絞ってタイプの強さを評価したが、今後は複合タイプを考慮した相性関係について考えてみたいと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeでPleasanterとpostgresqlのコンテナ作ってみた

経緯 お仕事でPleasanter作成お願いされる事が何回かあったんでdocker初心者だけどdocker-compose.yml作ってみたよ( ˘ω˘ ) 備忘録。 作ったもの pleasanterのベースは公式推奨のcentos7ベースと個人的な好みでdebianベースの2通り作ったよ( ˘ω˘ ) 前提環境 python3.6系以上 windowsも多分問題ないは。。。ず?(wsl2内のdockerでは動かず。無念。) 使い方 設定変更 ./.env DISTRIBUTION: pleasanterのベースイメージ。 (default) deb -> debian, cent -> centos7 TZ: timezone。よしなに。(default) Asia/Tokyo POSTGRES_PORT: postgres用のポート。(default) 5432 POSTGRES_PASSWORD: postgres用パスワード。変更不要。任意で。 POSTGRES_MEM: postgres割り当てメモリ。(default) 1G PLS_PORT: pleasanter用ポート。(default)8080 PLS_MEM: pleasanter割り当てメモリ(default) 1G スクリプト実行 python3 init.py ボリューム初期化、イメージのビルドしなおすか、コンテナ作成するか聞かれるので適当にyで流す。 やったこと。 参考にしたqiita: https://qiita.com/ta24toy27/items/986b3057e08f3da2fc06 上記から可能な部分のみDockerfile化。 パッケージのインストール回り プリザンターのソースzipをimage内で展開 ※pleasanterのソース更新は./pleasanter/pleasanter.zipを公式ダウンロードzipに差し替え、イメージリビルド想定。 pleasanterソースのRds.jsonの中身をコンテナの情報を(.envを元に)更新 pleasanter設定の反映。 pleasanterのサービスとnginxのサービスを起動。 心残り サービス起動の兼ね合いでprivileged: trueを外せない。 pleasanterのソースがzip展開
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GPT-3でFAQデータを生成する【python】

背景 チャットボットに必要なデータをGPT-3で生成できないかを検討してみました。 目的 GPT-3を使ってFAQデータを取得したいです。 始めにQuestionからAnswerを生成してみます。このとき、簡単なAnswerではなく付加情報も加わった回答が欲しい。またできるだけ他の解答とは異なった付加情報をもった回答があったらなおよし。 次にQuestionとAnswer両方を生成できないか検討してみます。 QuestionからAnswerの生成 同じ質問から異なる付加情報を含む回答文を生成したい。 参考サイト GPT-3でanswerとcompletionのエンドポイントを比較する 入力テキスト 以下のようなタスク固有のデータを考えました。 Q: AIが面接評価をすることにどう思いますか? A: 私は反対です。なぜならAIでの評価を就活生は受け入れられないからです。私はそういった何でもかんでもAIに置き換える慣習がとっても嫌いです。 ### Q: 100万円を手に入れたらあなたはどうしますか? A: 私は両親に全額渡そうと思います。学生時代に両親が支えてくれたおかげで今があります。家族には感謝しかないです。 ### Q: あなたは夏にエアコンを使いますか? A: いいえ、付けないです。エアコンは電力消費が大きく、CO2の排出量が増えるため環境に悪いからです。 ### Q: 東京オリンピックの開始は賛成ですか? A: リクエスト文 translate_text = '東京オリンピックの開始は賛成ですか?' prompt = '' dics = { 'AIが面接評価をすることにどう思いますか?':'私は反対です。なぜならAIでの評価を就活生は受け入れられないからです。私はそういった何でもかんでもAIに置き換える慣習がとっても嫌いです。', '100万円を手に入れたらあなたはどうしますか?':'私は両親に全額渡そうと思います。学生時代に両親が支えてくれたおかげで今があります。家族には感謝しかないです。', 'あなたは夏にエアコンを使いますか?':'いいえ、付けないです。エアコンは電力消費が大きく、CO2の排出量が増えるため環境に悪いからです。' } for q,a in dics.items(): prompt += "Q: {question}\nA: {answer}\n###\n".format(question=q, answer=a) prompt += "Q: {text}\nA:".format(text=translate_text) パラメータ response = openai.Completion.create( engine='davinci', prompt=prompt, temperature=float(0.4), max_tokens=int(300), top_p=1.0, frequency_penalty=0.0, presence_penalty=0.0, stop=["###"] ) curieとdavinciでのエンジン出力を比較してみました。 curieの場合 #1回目 東京オリンピックの開始は賛成です。東京オリンピックは日本の文化を発信する大事なイベントです。 #2回目 東京オリンピックは私の大好きなスポーツです。しかし、私は東京オリンピックには賛成できません。 #3回目 はい。東京オリンピックは日本の象徴です。 #4回目 はい。東京オリンピックは日本の若者を支える大きなイベントです。 #5回目 私は賛成です。東京オリンピックは日本の文化を発信していくために必要なことです。 付加情報を含んだ回答を得られました。 ただ2回目の「東京オリンピックは私の大好きなスポーツです。」のような意味的におかしい回答も含んでいます。 davinciの場合 #1回目 反対です。オリンピックは経済効果があるだけで、それ以外には何の役にも立たないからです。 #2回目 反対です。なぜなら2020年に開催される東京オリンピックは環境への負荷が大きすぎるからです。 #3回目 私は賛成です。今までも東京オリンピックは開催されていますし、東京は世界の視線が集まるので、東京の良さを世界に発信できるチャンスです。 #4回目 いいえ、反対です。私は東京オリンピックは日本の財政を破綻させるだけだと思います。 #5回目 反対です。なぜなら東京オリンピックは費用が高く、経済効果が小さいからです。 davinciは環境の話や経済効果等を根拠にしており付加情報は含んでいるが、入力データへ少し依存した回答が生成されている気もします。 ただ付加情報がcurieと比べて充実している印象もあります。 さすがにコロナを付加情報に含んだ回答は得られませんでした。 QuestionとAnswer両方の生成 タスク固有データは以下のようなテキスト Q: AIが面接評価をすることにどう思いますか? A: 私は反対です。なぜならAIでの評価を就活生は受け入れられないからです。私はそういった何でもかんでもAIに置き換える慣習がとっても嫌いです。 ### Q: 100万円を手に入れたらあなたはどうしますか? A: 私は両親に全額渡そうと思います。学生時代に両親が支えてくれたおかげで今があります。家族には感謝しかないです。 ### Q: あなたは夏にエアコンを使いますか? A: いいえ、付けないです。エアコンは電力消費が大きく、CO2の排出量が増えるため環境に悪いからです。 ### Q: 生成できたFAQ(davinci) 今の時代をどう思いますか? A:私は現代の便利さに感謝しています。例えば、スマホで何でも情報が手に入るのは素晴らしいと思います。 今の職場では、どんな仕事をしていますか? A:今は、研究室で働いています。自動車に関する研究をしています。 今までに一番感動した体験は何ですか? A:私は高校時代に部活でバスケをやっていたのですが、部活に所属していた先輩に、「あなたは将来、プロになってもいい選手だ」と言われたことです。 びっくりしました。 まさかこんなきれいな日本語で質問、応答文を生成してくれると思わなかったです。 また思ったより質問パターンが多くてびっくりしました。 これなら付加情報をもったFAQデータとして使えそうです。 制限 大事な話を忘れていました。APIにかかる費用です。 約300セットのFAQを生成するのに約7.8ドルかかりました。 無料トライヤルで18ドル分はもらえていますが、300セットで7.8ドルもかかってしまったら、チャットボットに必要なデータ分はGPT-3では絶対にとれませんね。 このFAQをどうにかかさ増し出来無いか検討してみます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習勉強記録 RandomForestClassifier

1 RandomForestClassifierとは RandomForestClassifierとは、複数の決定木を集めたものです。だから森なんですね。 アンサンブル学習と言って、別々の決定木が出した結果を統一させて総合的な分析結果を得ることができるものです。 このサイトがわかりやすかったです。 「cacoo」決定木分析(ディシジョンツリー)とは?概要や活用方法、ランダムフォレストも解説 https://cacoo.com/ja/blog/what-is-decision-tree/ 2 RandomForestClassifierの実装 データフレーム(df)のデータ部分と目標値部分(column名はClass)をx,tとして、モデルを学習させる時は、このように書きます。 x = df.drop('Class', axis=1).values t = df['Class'].values # モデルの定義 from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier(random_state=0) model.fit(x, t) そして、学習したモデルをもとにdf_testのClassを推論します。 # 推論 x_test = df_test.drop('Class', axis=1).values t_test = df_test['Class'].values y_pred = model.predict(x_test) これでy_predの中に、x,tで学習したモデルによって推論された、df_testのClass値が入ります。 2.1 混合行列を表示する 混合行列を sklearn.metricsを用いて表示します。 from sklearn import metrics # 混同行列の作成 labels = list(set(t)) c_matrix = metrics.confusion_matrix(t_test, y_pred, labels=labels) # DataFrameに整形 df_matrix = pd.DataFrame(c_matrix, columns=labels, index=labels) 2.2 precision, recall, F値を調べる それぞれのスコアは、metricsモジュール内の関数に目標値と予測値を渡すことで確認できます。 precision = metrics.precision_score(t_test, y_pred, average=None) recall = metrics.recall_score(t_test, y_pred, average=None) f1_score = metrics.f1_score(t_test, y_pred, average=None) average=Noneと指定することで、それぞれのクラスを正例としたスコアを確認することができます。 3 閾値の設定 precision, recall, F値を調整するために、予測確率における分類の閾値を調整します。 y_proba = model.predict_proba(x_test) これで、x_testの一つ一つの行に対してのClass=0である確率とClass=1である確率を求め、y_probaに代入します。元のデータにもよりますが、中身はこんな感じです。この例だと1列目の数字がClass=0である確率で、2列目の数字がClass=1である確率です。 # y_probaを10行目まで表示 print(y_proba[:10]) """ array([[1. , 0. ], [1. , 0. ], [1. , 0. ], [1. , 0. ], [1. , 0. ], [0.97, 0.03], [1. , 0. ], [1. , 0. ], [1. , 0. ], [1. , 0. ]]) """ そして、この2列目の数字に対して、閾値をもうけ、調整していきます。 # 閾値0.4として、y_probaの2列目が0.4を超える場合、Trueと返す行列をy_predに代入 y_pred = (y_proba[:,1] > 0.4) # .astype('i')はデータ型をintに変えている。 Falseなら 0, Trueなら 1 に変換する。 y_pred = (y_proba[:,1] > 0.4).astype('i') こうして閾値を調整して、最終的にprecision, recall, F値を調べます。 # 「t_testに対してのy_predの予測」のrecall, precision, f1score を表示する print('recall :', metrics.recall_score(t_test, y_pred, average=None)) print('precision :', metrics.precision_score(t_test, y_pred, average=None)) print('f1score :', metrics.f1_score(t_test, y_pred, average=None)) これで、求めることができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習勉強記録 分類の評価方法

1 分類の評価方法 分類において、学習雨済みモデルを評価する指標で、代表的なものが以下の4つです。 Accuracy(正解率) Precision(適合率) Recall(再現率) F1score(F値) 2 混合行列 上記の指標を理解するのに、混合行列と呼ばれる表が使用されます。 がんの診断をした結果、それが正しかったかどうかを表した表を例にしてみるとこんな感じになります。(図1) 図1 TP (True Positive、真陽性):予測値を正例として、その予測が正しい場合の数 FP (False Positive、偽陽性):予測値を正例として、その予測が誤りの場合の数 TN (True Negative、真陰性):予測値を負例として、その予測が正しい場合の数 FN (False Negative、偽陰性):予測値を負例として、その予測が誤りの場合の数 3 Accuracy(正解率) Accuracyの式は以下。 Accuracy=\frac{TP+TN}{TP+FP+TN+FN} 正しく予測できたものの数を、全ての数で割ると正解率がでてきます。簡単ですね。 4 Precision(適合率) Precisionの式は以下。 Precision=\frac{TP}{TP+FP} 正例(がん)と予測したもののうち、本当にがんだった数の割合を表します。 Precisionを高くするということは、誤診を少なくすることを意味します。 これが低いと、「あのお医者さん、めっちゃがんって診断するけど、外れること多いからあんまり当てにならんで。」と言われます。 高いと、「あのお医者さんががんと言ったら、がんだ。間違いない。」と言われます。 がんではない人をがんだと誤診したくないときは、これを高めたほうがいいでしょう。 5 Recall(再現率) Recallの式は以下。 Recall=\frac{TP}{TP+FN} 陽性(がんがある人)のうち、がんだと正しく診断された割合を表します。 Recallを高くするということは、がんの人の見落としを少なくすることを意味します。 低いと、「あのがん検診では、健康と言われても本当はがんかもしれないから当てにならんで。」と言われます。 高いと、「あのがん検診では、がんと言われても健康な時もあるし、健康って言われたら大体健康やから安心や。」と言われます。 がんの見逃しをどうしても避けたいとき、これが高いほうがいいでしょう。 6 F1score(F値) F1scoreの式は以下。 F1score=\frac{2\times Recall \times Precision}{Recall + Precision} RecallとPrecisionは、どちらかを上げようとすると、もう一方が下がってしまう関係にあります。 両者のバランスをとるために調和平均で計算される指標がF値です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Slackボットから送られてきたメッセージを削除する

はじめに Slackボットを開発していると,テストチャンネルがボットからのメッセージで溢れたり,またはボットのメッセージが作成した本人でも権限がなくて消せなかったりする際にAPI経由で消すと楽だったのでメモとして残しておきたいと思います. (入退室管理のボットを作った程度の初心者です...) また,アプリの作成や必要な権限などについては明記してませんが,適宜APIのリンクから確認していただければと思います. さらに,APIを呼ぶときのPythonのコードも記載するのですが,今回はいつもお世話になっているPythonのBoltのWebClient経由で呼んでいます.結局はSlack-SDKのWebClientで送ってるのでBoltを使ってないときはSDKのWebClinetやAPIに直接POSTする方法などに置き換えてもらえたら幸いです. 実行環境 Python: 3.9.5 (>=3.6) 必要なモジュール: slack_bolt Botのメッセージを削除する まずBotが送信したメッセージを削除するときはchat.deleteメソッドを使います. 必要なのはボットのトークン(xoxb-xxxx-xxxx-xxxx)とチャンネルのID,メッセージのタイムスタンプです.この3点がわかればいいので,一番簡単な方法としてはメッセージのリンク(https://hogehoge.slack.com/archives/<channnelID>/p<timestamp>)をコピーしてそのリンクを元にリクエストを投げるという形です. タイムスタンプは後ろから6桁目にピリオドを付与したものを用います.例えば1234567891011120の場合は1234567891.011120をリクエストで使います. 試す方法はchat.deleteメソッドのテスターに記述するのが手っ取り早いですが,個人的にチャンネルIDやタイムスタンプを手作業で抜き出すのが面倒だったので以下のようなPythonスクリプトを使っています.(リンクを直接指定したいときとかは結構便利かもしれません) deleteMessage.py import os, sys from slack_bolt import App args = sys.argv try: url = args[1] # スレッド時のクエリを削除 if url.find("?") != -1: url = url[:url.find("?")] split_url = url.split("/") channel_id = split_url[-2] timestamp_orginal = split_url[-1].lstrip("p") timestamp = "{0}.{1}".format(timestamp[:-6], timestamp[-6:]) except Exception as e: print("ERROR") print(e) sys.exit(1) app = App(token=os.environ["SLACK_BOT_TOKEN"]) res = app.client.chat_delete( channel=channel_id, ts=timestamp, ) print(res) # $ python3 deleteMessage.py # $ https://hogehoge.slack.com/archives/<channnelID>/p<timestamp> # >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp>'} 直近に送信したBOTのメッセージを削除する リンクをコピーしてPythonスクリプトを実行するというのは特定のメッセージを削除するときに有用です.しかし,それ以外のときは楽なようで全く楽じゃなくて,例えば「直近から3件削除したい」みたいなときにいちいちリンクをコピーして実行を繰り返すのは流石に手間です.そこで,チャンネルのメッセージIDすら自動で取得して消せたら楽なのにと思いAPIを探しました. ここで使うのは,conversations.historyメソッドとconversations.repliesメソッドの2つです. 後者は,ボットが生成したスレッド(ボットのみが返信をしているスレッド)を削除するときに使うのですが,以下のような流れで作成されているときを想定しています. 例.入退室管理ボット (チャンネルへ投稿されたメッセージ...)  |  部屋解錠メッセージ  | |--- メッセージ1 [入室] Aさん  | |--- メッセージ2 [退室] Aさん  | |--- メッセージ3 部屋施錠メッセージ  |  部屋施錠メッセージ(reply_broadcast) また,conversations.historyメソッドとconversations.repliesメソッドを呼び出す上で最低限必要なのはボットのトークンとチャンネルのIDだけです.これらのメソッドを使って取得したメッセージに含まれるタイムスタンプを元にchat.deleteメソッドにリクエストを送りメッセージを削除します. 通常のメッセージのみを削除する ここでの通常のメッセージとは,上記の例における部屋解錠メッセージや部屋施錠メッセージ(reply_broadcast)などのチャンネルに送信されたメッセージを指します.スレッドが生えている場合,通常メッセージを削除してもそのスレッドへの返信までは削除されず残ります. conversations.historyメソッドを使えばチャンネルに送信されたメッセージの履歴を取得できます.これを利用して最新のメッセージのタイムスタンプを取得することでchat.deleteメソッドにリクエストを送ります. 特に,conversations.historyメソッドのAPIに記載されている通り,取得するメッセージの件数(limit)はデフォルトで100になっているので,これを1に設定することで最新のメッセージのみを取得します. 取得したメッセージのJSONの中にあるtsキーがメッセージのタイムスタンプなので,これを用いてchat.deleteメソッドにリクエストを投げます. conversations.historyメソッドに関する余談 limitを1に設定することで最新を取得すると記載しましたが,limitをどの値に設定したとしてもmessagesというキーの配列の中にメッセージが格納されているので必須というわけではありません.しかし,今回最新のメッセージ以外はすべて不要なので1に設定することをお勧めします. また,APIを見るとlatestやoldestというオプションがあります,これをうまく使えばより柔軟な操作ができます. スレッドのメッセージを削除する スレッドを削除する場合は,はじめに記載した通り,通常メッセージを削除するだけではスレッドに生えたリプライまでは削除されません. そこで.conversations.repliesメソッドを用いて親のメッセージを起点としてスレッドに対して返信されたメッセージのタイムスタンプを取得し,それらのタイムスタンプを使ってchat.deleteメソッドを呼び出すというアプローチをとります.そうすることで,スレッドに関係したメッセージも含めて丸ごと削除できます. ここでスレッドに関係したメッセージというのは,スレッドの親となるメッセージ,スレッドに返信されたメッセージ,スレッドに返信しかつチャンネルにも送信されたメッセージ(reply_broadcast=Trueとして送信したメッセージ)を指します. 流れとしては以下のような手順です. 1. conversations.historyメソッドを使って最新のメッセージを取得する 2. スレッドに関係したメッセージかどうか判断する  - スレッドに関係ないならばメッセージのタイムスタンプを使ってchat.deleteメソッドにリクエストを投げる  - スレッドに関係したメッセージならばconversations.repliesメソッドを用いてスレッドに関係したすべてのメッセージを取得する 3. スレッドに関係したすべてのメッセージそれぞれのタイムスタンプを使ってchat.deleteメソッドにリクエストを投げる スレッドに関係したメッセージかどうかを判断するためには,今回はreply_countキーとsubtypeキーに注目しました.reply_countキーはスレッドが生えている親のメッセージに対して付与されているキーで,subtypeキーはreply_broadcast=Trueとして送信されたメッセージに対して付与されるキーです.これらがメッセージの情報に含まれている場合にはスレッドに関係したメッセージとして判断します. 上記の二つの削除をまとめたPythonコード deleteMessageNow.py import os, time from slack_bolt import App channel_id = os.environ["SLACK_CHANNEL_ID"] app = App(token=os.environ["SLACK_BOT_TOKEN"]) def deleteMessage(channel, ts): return app.client.chat_delete( channel=channel_id, ts=ts, ) def deleteThreadMessage(channel, thread_ts): # https://api.slack.com/methods/conversations.replies messages = app.client.conversations_replies( channel=channel_id, ts = thread_ts, )["messages"] responses = list() # 後ろから順番に削除していく for message in reversed(messages): responses.append(deleteMessage( channel, message["ts"], )) time.sleep(0.5) # 一応待ちます. return responses def run(): # https://api.slack.com/methods/conversations.history result = app.client.conversations_history(channel=channel_id, limit=1) message = result["messages"][0] # スレッドに関係したメッセージかどうか if "reply_count" in message.keys() or "subtype" in message.keys(): # thread_tsが親のタイムスタンプ return deleteThreadMessage(channel_id, message["thread_ts"]) # それ以外は通常メッセージなのでそのまま消す ts = message["ts"] return [deleteMessage(channel_id, ts)] if __name__ == "__main__": responses = run() for res in responses: print(res) # スレッドの場合 # $ python3 deleteMessageNow.py # >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp3>'} # >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp2>'} # >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp1>'} シェルスクリプトと上記のPythonコードを組み合わせる 直近から3個削除したいときにdeleteMessageNow.pyを3回実行するのは面倒ですので,シェルスクリプトと組み合わせることでとっても楽に消せるようになります. run_deleteMessageNow.sh #!/bin/sh echo "直近何回分消しますか?": read num echo "OK! running deleteMessageNow.py [$num]" # 入力された回数だけloopを回す for i in `seq 1 $num` do python3 deleteMessageNow.py done 参考URL slack-api  - chat.delete  - conversations.history  - conversations.replies Qiita記事  - [Slack]自作したBotのメッセージを削除する  - Slack APIのchannels.historyが使えなくなる対策 その他  - Bolt for Python  - Bolt for Python (Github)  - Python Slack SDK
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ラビットチャレンジ Day1 線形代数

1.はじめに 本記事は、ラビットチャレンジの応用数学①:線形代数に関する記事です。 ラビットチャレンジは、現場で潰しが効くディープラーニング講座の教材を活用した講座です。 詳細は以下のリンクをご確認ください。 ラビットチャレンジ 2. 線形代数 今回の線形代数で扱う項目は次の通り。 スカラー、ベクトル、行列 行列とベクトル、行列と行列の積 連立方程式と行列 逆行列 行列式 固有値、固有値分解 特異値分解 2.1. スカラー、ベクトル、行列 ・スカラー 普通の数。1,2,3とか。 四則演算可能 ・ベクトル 大きさに加え向きを持つ 例えば、x,y座標上で、それぞれ(2,1)と表すと、どの方向かが分かる。 右に2歩進んで、上に1歩進んだ場所とか、具体的に位置を表すことができる。 $$     \vec{a} = (2, 1) $$ ・行列 ベクトルを並べたもの 元々は連立方程式を解くための過程で作られた。 $$ A = \left( \begin{matrix} 1 & 2 \\ 3 & 4 \end{matrix} \right) $$ 2.2. 行列とベクトル、行列と行列の積 ・行列とベクトルの積 行列とベクトルの積は、次のように計算される。 $$ \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \times \begin{pmatrix} 2 \\ 4 \end{pmatrix} = \begin{pmatrix} 10 \\ 20 \end{pmatrix} $$ ・行列と行列の積 行列と行列の積は、次のように計算される。 m行×n列とn行×k列の積において、結果はm行×k列の行列となり、 i行j列の要素は、以下の通りになる。 $$ d_{i j} = \sum_{k=1}^na_{ik}b_{kj} ​$$ 具体的な計算の様子は次の通り。 $$ AB = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \times \begin{pmatrix} 3 & 4 \\ 5 & 6 \end{pmatrix} = \begin{pmatrix} 13 & 16 \\ 29 & 36 \end{pmatrix} $$ 2.3. 連立方程式と行列 次のような、連立方程式があったとする。 $$ 1x + 3y = 5 \\ 2x + 4y = 10 \\ $$ これを行列を用いて表すと次の通りになる。 $$ \begin{pmatrix} 1 & 3 \\ 2 & 4 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} 5 \\ 10 \end{pmatrix} $$ 上記の係数行列に着目して解く場合、以下の方法を利用して解くことができる。 1) i行目をc倍する 2) s行目にt行目のc倍を加える 3) p行目とq行目を入れ替える 上記のような手順を行基本変形と呼ぶ。 行基本変形を行っていき、係数行列を階段行列にすることで解を求めることができる。 2.4. 逆行列 逆数のようなもの。 次の様な行列を単位行列と定義する。 (対角成分が1で、そのほかの要素が0の行列) $$ I = \begin{pmatrix} 1 & & \\ & \ddots & \\ & & 1 \end{pmatrix} $$ ある行列をAとしたとき、 $$ AI =A $$ を満たす。 この時、次の式を満たす様な $$ AA^{-1} = A^{-1}A = I $$ を逆行列と定義する。 逆行列を求める時は、Aを行列、Iを単位行列として、 $$ (A|I) = (I|A^{-1}) $$ となる様に、行基本変形を行っていく。 これを掃き出し法と呼ぶ。 具体的には、 $$ ( A | I ) = \begin{pmatrix} 1 & 4 \\ 2 & 6 \end{pmatrix} \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} =\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} \begin{pmatrix} -3 & 2 \\ 1 & 1/2 \end{pmatrix} = ( I | A^{-1} ) $$ と計算する。 元の行列と単位行列を横に拡張し、Aが単位行列となる様に行基本変形をする。 2.5. 行列式 行列の大きさの様なもの。 正方行列に対して決まるスカラー。 2次元で考えたときは、平行四辺形の面積の様にイメージできる。 2×2の場合、次の様に行列式を求めることができる。 $$ |A| = \begin{pmatrix} a & b \\ c & d \end{pmatrix} = ad - bc $$ 3×3の場合、次の様に分解して計算する。 $$ |A| = \begin{pmatrix} a & b & c\\ d & e & f \\ g & h & i \end{pmatrix} = aei + bfg + cdh - ceg - bdi - afh $$ $$ ?=\begin{pmatrix}1 & 5 & 8 \\ 3 & 4 & 9 \\ 2 & 6 & 7\end{pmatrix} $$ $$ |?|=\\ (1\times4\times7)+(3\times6\times8)+(2\times5\times9)\\−(2\times4\times8)−(3\times5\times7)−(1\times6\times9) \\ =28 + 144 + 90 − 64 − 105 − 54 \\ =39 $$ 2.6. 固有値、固有値分解 固有値 線形変換の特徴を表す指標。 簡単にいえば、ある行列をかけても、結局は方向は変わらずスカラー倍するだけみたいな感じ。 ある行列Aに対して、以下の様な式が成り立つとする。 $$ A\vec{x} = λ\vec{x} $$ このとき、λを固有値、xを固有ベクトルという。 固有値分解 ある正方行列Aに対して、固有値λと固有ベクトルvを持つとする。 固有値λを対角線上に並べ、 $$ Λ = \begin{pmatrix} λ{1} & & \\ & λ{2} & \\ & & \ddots \end{pmatrix} $$ と定義し、それに対応する固有ベクトルを並べ、 $$ V = \begin{pmatrix} v_{1} & & \\ & v_{2} & \\ & & \ddots \end{pmatrix} $$​ としたとき、それらは、 $$ AΛ = VΛ $$ と関係づけられる。このとき、以下の様に変形ができる。 $$ A = VΛV^{-1} $$ この様な正方行列を3つの行列の積に変換することを固有値分解という。 特徴が見れたり、行列の累乗計算が楽になるなどの利点がある。 2.6. 特異値分解 正方行列以外の場合に、固有値分解と似たことをやりたい。 このときに行うのが特異値分解。 ある実数が長方形に並んだ行列Mに対して、以下の式が成り立つ様な、 特殊な単位ベクトルv,uと右辺の係数σがある。 $$ M\vec{v} = σ\vec{u} \\ M^{T}\vec{u} = σ\vec{v} $$ この特殊な単位ベクトルv,uとその係数σを、行列Mに対する 特異ベクトル、特異値という。 (特にvを右特異ベクトル、uを左特異ベクトル) このとき、特異値σを対角線上に並べた行列を、 $$ S = \begin{pmatrix} σ{1} & & \\ & σ{2} & \\ & & \ddots \end{pmatrix} $$​ と定義し、それに対応する特異ベクトルを並べた行列 $$ V = (\vec{v_1} \vec{v_{2}} \dots) \\ U = (\vec{u_1} \vec{u_{2}} \dots) $$ を用意したとき、それらは、 $$ MV = US \\ M^{T}U = VS^{T} $$ と表すことができ、 $$ M = USV^{-1} \\ M^{T} = VS^{T}U^{-1} $$ と変形できるので、 $$ MM^{T} = USV^{-1}VS^{T}U^{-1} = USS^{T}U^{-1} $$ と変形できる。 つまり、 $$MM^{T} $$ を固有値分解すれば、その左特異ベクトルと特異値の2乗が求まる。   特異値分解は、固有値分解と同様に行列の特徴を抽出などに利用される。 3. 確率 ​・確率は大きく分けて頻度確率(客観確率)とベイズ確率(主観確率)に分けられる。 ・条件付確率はある事象Aがすでに起こっているとしたときに、事象Bが発生する確率を指す。 $$ P(B|A)=P(B∩A)P(A) $$ ・AとBが独立事象であるならば $$ P(B∩A)=P(A)P(B)  $$ である。 ・変数Xの取りうる値x1,x2,...,xnに対してそれぞれ確率p1,p2,...,pnが定まっているとき、変数Xを確率変数という。また、XからPへの対応関係を、確率変数Xの確率分布という。 $$ \sum_{n=1}^{k}x_ip_i $$ をXの平均値または期待値とよぶ。 (確率変数および確率が連続値をとるならば∫X(x)p(x)dx) ・分散:データの散らばり具合$$V(X)=E(X^2)−(E(X))^2$$  共分散:2つのデータ系列の傾向の違い $$Cov(f,g)=E(fg)−E(f)E(g)$$  標準偏差:$$σ=\sqrt{V(X)}$$ ・ベルヌーイ分布:$$P(x|μ)=p^x(1−p)^{(1−x)}$$  二項分布:$$P(x|λ,n)=\frac{n!}{x!(n−x)!}λ^x(1−λ)^{(n−x)}$$  正規分布:$$N(x;μ,σ2)=\frac{1}{\sqrt{2πσ^2}}exp{(-\frac{(x-μ)^2}{2σ^2})}$$ 4.情報理論 ・ある数値から1増加(Δw=1)を考えるとき、10から1増加と1から1増加増えるときでは増加の比率が異なる。  後者の場合のほうが違いを見つける上で情報量が多いと考えることができる。 ・自己情報量 $$ I(x)=−log(P(x)) $$ ・シャノンエントロピー $$ H(x)=−∑(P(x)log(P(x))) $$  →自己情報量の期待値 ・カルバックライブラー情報量 $$ DKL(P||Q)=∑xP(x)logP(x)Q(x) $$ →同じ事象・確率変数における異なる確率分布P,Qの違いを表す ・交差エントロピー $$ H(P,Q)=H(P)+DKL(P||Q)=−∑xP(x)logQ(x) $$ →KL情報量の一部分を取り出したもの →Qについての事故情報量をPの分布で平均している
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

excelでcsvエクスポートし、pandasで読み込む時

通常読み込み 通常、csvファイルは以下のコードで読み込みができる。 import pandas as pd path = "filename" data = pd.read_csv(path) しかし、excelからエクスポートしてきたcsvファイルを読み込むと UnicodeDecodeError: 'utf-8' codec can't decode byte 0x95 in position 0: invalid start byte 上のようなエラーが出る。 解決方法 以下のコードで読み込むことで、解決できます。 import pandas as pd import codecs path = "filename" with codecs.open(path, "r", "Shift-JIS", "ignore") as file: data = pd.read_table(file, delimiter=",")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

公式実装のvision transformerがfrom vit-jax import hyperのエラーで使えなくなった話

Vision Transformerのアップデート 6/20にVision Transformerのアップデートがありました。その関係で以前の公式実装を頼ると使うことができなくなりました。 下記がVision Transformerの公式実装になります。 https://colab.research.google.com/github/google-research/vision_transformer/blob/master/vit_jax.ipynb URLに飛び、変更履歴を見るとNotebookでも変更が加わっています。 原因というか変更点はVision Transformerように作成されたvit-jaxというライブラリの中身になります。Hyperなどの一部のモジュールが削除されています。 対処法 自分の場合は記事を参考にfine tuningを行っていたためにコードをいじっていたものですからすぐに対応ができない状態でした。 もちろん本当は公式実装をみて変更を確認、修正するべきですがとりあえずの対応としてVision TransformerのGitHubから過去のcomitt(例えばこれ)のコードは過去の状態で使えるのでそれを利用しました。 しかし、理由があってアップデートされたものを過去のバージョンのまま使い続けるのは良いところ悪いところあると思うので時間ができ次第最新版の状態にアップデートします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

天文データ解析入門 その20 (pycupid: pythonでclumpfind等を使う)

本記事では、pycupid の基本的な使い方について記述します。pycupid には GaussClump, ClumpFind, FellWalker, Reinhold の4つの構造同定アルゴリズムが入っています。 今回、例として国立天文台の FUGIN プロジェクトで得られた野辺山45m電波望遠鏡の CO 輝線のアーカイブデータを用います。データは http://jvo.nao.ac.jp/portal/nobeyama/ から FGN_03100+0000_2x2_13CO_v1.00_cube.fits をダウンロードします (重いです)。 インストール pycupid (ver. 0.1.4) は2021年6月現在、Python2.7, 3.4, 3.5, 3.6 でしか動作しません。 pip install pycupid もしくは pip2.7 install pycupid などでインストールしましょう。 基本的な使い方 今回は簡単のため 2D fits での使い方について紹介します。3D (cube) でも基本的には同じです。 まずは例によって必要なものを import し、fitsを読み込みます。 from astropy.io import fits import numpy as np from matplotlib import pyplot as plt import aplpy from astropy.wcs import WCS from pycupid import clumpfind, fellwalker, reinhold, gaussclumps hdu = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_13CO_v1.00_cube.fits")[0] # 3D w = WCS(hdu.header) 2D fitsにするため、以下のような関数を定義します。 def v2ch(v, w): # v(km/s)をchに変える x_tempo, y_tempo, v_tempo = w.wcs_pix2world(0, 0, 0, 0) x_ch, y_ch, v_ch = w.wcs_world2pix(x_tempo, y_tempo, v*1000.0, 0) v_ch = int(round(float(v_ch), 0)) return v_ch def del_header_key(header, keys): # headerのkeyを消す import copy h = copy.deepcopy(header) for k in keys: try: del h[k] except: pass return h def make_new_hdu_integ(hdu, v_start_wcs, v_end_wcs, w): # 積分強度のhduを作る data = hdu.data header = hdu.header start_ch, end_ch = v2ch(v_start_wcs, w), v2ch(v_end_wcs, w) new_data = np.nansum(data[start_ch:end_ch+1], axis=0)*header["CDELT3"]/1000.0 header = del_header_key(header, ["CRVAL3", "CRPIX3", "CRVAL3", "CDELT3", "CUNIT3", "CTYPE3", "CROTA3", "NAXIS3"]) header["NAXIS"] = 2 new_hdu = fits.PrimaryHDU(new_data, header) return new_hdu 以下のように 2D fits を作成します。 integ_hdu = make_new_hdu_integ(hdu, 25.0, 125.0, w) このデータに clumpfind をかけます。一番シンプルな方法は、 cf = clumpfind(integ_hdu.data, 20).astype("float32") これだけです。cf は同定された構造の ID が入った numpy array です。引数の 20 (今はてきとう) は r.m.s. を意味しています。デフォルトでは、この値の2倍が最小コントアレベルとコントアレベル間隔に設定されます。 同定されなかった場所 (ピクセル) にはマイナスの大きな値が入っているので、NaN にします。 cf[cf==cf.min()] = np.nan ID map を plot してみます。 fig = plt.figure(figsize=(8, 8)) plt.imshow(cf, cmap="jet", origin="lower left") plt.show() 何かがおかしいです。 どうやらバグ (仕様?) のようで、以下を実行すると元の fits と同じになります (3D でも同様の仕様を確認)。 (pycupid (ver. 0.1.4) 2021年6月現在) cf_2 = cf.reshape(cf.shape[::-1]).T もう一度 ID map を plot してみます。 fig = plt.figure(figsize=(8, 8)) plt.imshow(cf_2, cmap="jet", origin="lower left") plt.show() 正しくなりました。 同定した構造の数は print(np.nanmax(cf_2)) #1335.0 これでわかります。 特定の構造 (例えば ID=1000) を取ってきたい場合は、 ind = np.where(cf_2==1000) print(ind) #(array([452, 452, 452, 452, 453, 453, 453, 453, 454, 454, 454, 454, 455, # 455, 455, 456, 456, 456]), array([337, 338, 339, 340, 337, 338, 339, 340, 337, 338, #339, 340, 337, # 338, 339, 337, 338, 339])) とすると、tuple でその構造の index が返ってきます。ここまで来れば、他の記事を参考にして自由に好きな解析ができると思います。 しかしながら、np.where は非常に遅いので、以下のようなコードを使うことをお勧めします (参考)。 from scipy.sparse import csr_matrix def compute_M(data): cols = np.arange(data.size) return csr_matrix((cols, (data.ravel(), cols)), shape=(data.max() + 1, data.size)) def get_indices_sparse(data): M = compute_M(data) return [np.unravel_index(row.data, data.shape) for row in M] ind_list = get_indices_sparse(cf_2.astype("int")) また、例えば、コントアを引きたい場合は、 fig = plt.figure(figsize=(8, 8)) f = aplpy.FITSFigure(integ_hdu, slices=[0], convention='wells', figure=fig) f.show_colorscale(stretch='linear', cmap="Greys", aspect="equal") f.show_contour(cf_hdu, levels=np.arange(1, np.nanmax(cf_2)+1), colors="c") f.recenter(30.75, 0.0, width=0.5, height=0.5) plt.show() などでできます。 同定パラメータを変更する 辞書形式で詳細な設定を渡すことができます。詳しくはドキュメントを参照してください。 例えば、 config = {} config["DELTAT"] = "3*RMS" config["MINPIX"] = 25 config["LEVEL1"] = 30 config["LEVEL2"] = 50 config["LEVEL3"] = 100 config["LEVEL4"] = 150 config["LEVEL5"] = 200 config["LEVEL6"] = 250 config["LEVEL7"] = 300 cf_3 = clumpfind(integ_hdu.data, 20, config=config).astype("float32") などといった感じで使います。 ClumpFind 以外のアルゴリズム (GaussClump, FellWalker, Reinhold) も概ね同様かと思います。 以上です。 リンク 目次
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macでpythonの環境を設定しなおす

狙い bashやbrewを中途半端にいじった結果、自分の環境がわからなくなったため、最初からやり直します。 環境 macOS Big Sur 11.0 Xcodeをインストール・アップデート済み brewインストール済み ターミナルを整える デフォルトシェルをzshにする https://support.apple.com/ja-jp/guide/terminal/trml113/mac インストールしていく 以下をターミナルに打っていきます。 zsh brew install pyenv pyenv install 3.9.5 touch ~/.zshrc echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc echo 'eval "$(pyenv init -)"' >> ~/.zshrc echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.zshrc export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init --path)" source ~/.zshrc pyenv install 3.9.5 pyenv global 3.9.5 参考 https://prog-8.com/docs/python-env https://blog.serverworks.co.jp/2021/05/12/233520
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【LeetCode】就活に向けたコーディングテスト対策 #12

はじめに おはようございます. M1就活生がLeetCodeから,easy問題を中心にPythonを用いて解いていきます. ↓では,解いた問題のまとめを随時更新しています. まとめ記事 問題 今回解いたのは,難易度easyから 問題53のMaximum Subarray です. 問題としては,整数配列numsが与えられたとき,最大の和を持つ連続した部分配列を見つけ,その和を返すというもの. 入力例と出力例は以下の通りです. Example 1: Input: nums = [-2,1,-3,4,-1,2,1,-5,4] Output: 6 Explanation: [4,-1,2,1] has the largest sum = 6. Example 2: Input: nums = [1] Output: 1 Example 3: Input: nums = [5,4,-1,7,8] Output: 23 書いたコード 始めに書いたコードが以下で,こちらのコードでは,Time Limit Exceededとなりました.2重で繰り返し処理を行い,整数配列numsの先頭から順に部分配列を求めていくものになります.こちらのコードでは,numsの要素数が少ない場合,少ない計算時間で済みますが,大きすぎる場合,計算処理に相当時間がかかってしまいます.そのため,効率の良い処理を書く必要があります. class Solution: def maxSubArray(self, nums: List[int]) -> int: max_sum = nums[0] for i in range(len(nums)): for j in range(len(nums) - i): subarray_sum = sum(nums[i:i+j+1]) if max_sum < subarray_sum: max_sum = subarray_sum return max_sum どうにか,繰り返し処理を1回のみ使って書き直してみます.上のコードでは,numsの0番目要素から計算していましたが,今回は部分配列をnumsの0番目要素で初期化し,numsの1番目以降の要素(次の要素)を基準に,部分配列の和を計算しています(+nums[i:i+j+1]の1って何を防げる). 繰り返し処理では,numsの1番目以降を走査していきます.今回の問題は,”現在の部分配列+次の要素”と”次の要素”を比較し,その結果が大きいほうを次の部分配列の数としていきます.6〜9行目がその処理です.これで,得られた数と,現在の最大の和を比較することで部分配列の最大の和を見つけることができます. class Solution: def maxSubArray(self, nums: List[int]) -> int: subarray_sum = max_sum = nums[0] for num in nums[1:]: if num < subarray_sum + num: subarray_sum = subarray_sum + num else: subarray_sum = num if max_sum < subarray_sum: max_sum = subarray_sum return max_sum おわりに Pythonのリスト操作では,範囲外を参照した場合,IndexErrorとなります.しかし,スライスを使った場合にはこのエラーは出ず,うまく処理してくれます.スライスの範囲指定が間違っていても気が付かない場合があるため,注意する必要がありますね. 今回書いたコードはGitHubにもあげておきます. 前回 次回
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LineMessagingAPIを利用してトーク上で乗り換え案内の結果を送信する

はじめに yahoo天気予報をLineMessagingAPIを使用して、トーク上でやりとりする記事を投稿してから 長い時間が経ってしまいましたが、久しぶりに進捗が出たので投稿しようと思います。 今回は、本来の目標でもあったyahoo乗り換え案内をLINE上でやりとりを行う方法について 説明していこうと思います。 設計・構造について どのような方法で実装しようかと考えたのですが、大体二案でした。 スクレイピング yahoo Web API まず、yahoo天気予報の際にも使用したスクレイピングです。 こちらは一番慣れていることと、手軽に実装することができるというところから第一候補でした。 ただし、方法によってはアクセス数過多になったりすることがありそうなので、 もしも色々な方に使ってもらえるように開発するとなると、この方法はいまいちかもしれません。 代わりに、二つ目の案であるyahooのAPIを使用した場合であれば、アクセスのリミットなども 定められているため広く普及させる機会ができれば、こちらを使用して開発したいなと考えています。 今回もこっちの方法で開発すればよかったのでは?と思った方がいそうですが、 とりあえず自分で実装してみたいということなのでスクレイピングの方法を採用しました。 (ちょっとモチベが見出せなかったことと、公式ドキュメントを読んでもあまり理解できなかったとは言えない、、) 駅データのDBテーブル作成 構造は決まりましたので、次は駅名のDBが必要です。 今回使用したデータはこちらのサイトからいただきました。 こちらのデータをbot側の選択肢として表示、照合を行っていきます。 作成したテーブル 駅名を保存するテーブル(table名:stations) column1:station_name(駅名),String 入力したデータを保存するテーブル(table名:transit_input) column1:user_id(ユーザーのID),String column2:departure_station(出発する駅),String column3:arrival_station(到着する駅),String column4(オプション1):date_and_time(日時),String column5(オプション2):condition(出発or到着など),Integer column6(オプション3):show_rule:(条件のうち表示順序について),Integer 処理の流れ コード さて、ながれを説明したところで実際に使用したコードを記載します。 メッセージ受信時のコード # リッチメニューで乗り換え案内を選択した場合(乗り換え案内1) # ユーザー登録、出発or到着の選択 if received_message == "乗り換え案内": user_id = str(event.source.user_id) used_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") commonDB.create_data(user_id) commonDB.update_data(user_id, used_time, received_message) transitDB.create_data(user_id) transitMethod.condition_turn(line_bot_api, event) # 検索条件選択後の処理(乗り換え案内2) # 出発or到着の登録、時間の指定 if received_message == "出発" or received_message == "到着": user_id = str(event.source.user_id) if transitDB.exist_record(user_id, "start_up"): transitDB.update_data(user_id, received_message, "condition") transitMethod.date_and_time_turn(line_bot_api, event) transitDB.update_data(user_id, None, "departure_station") transitDB.update_data(user_id, None, "arrival_station") # 出発駅選択後の処理(乗り換え案内4、5) # 出発駅の入力が初めての場合、出発駅の登録 # 到着駅の入力が初めての場合、到着駅の登録 # どちらも登録されている場合は出発->到着の順に入力のループ if received_message in list(station_list["station_name"]): user_id = str(event.source.user_id) if transitDB.exist_record(user_id, "date_and_time"): if transitDB.exist_record(user_id, "departure_station") and \ transitDB.exist_record(user_id, "arrival_station"): transitDB.update_data(user_id, None, "departure_station") transitDB.update_data(user_id, None, "arrival_station") if transitDB.exist_record(user_id, "departure_station"): transitDB.update_data(user_id, received_message, "arrival_station") transitMethod.options_turn(line_bot_api, event) else: transitDB.update_data(user_id, received_message, "departure_station") transitMethod.arrival_turn(line_bot_api, event) # 表示条件選択後の処理(乗り換え案内6) # 表示条件選択した場合、表示条件の登録 if received_message in ["到着が早い順", "乗り換え回数順", "料金が安い順"]: user_id = str(event.source.user_id) if received_message == "乗り換え回数順": rule_number = 1 elif received_message == "料金が安い順": rule_number = 2 else: rule_number = 0 if transitDB.exist_record(user_id, "arrival_station"): transitDB.update_data(user_id, rule_number, "show_rule") transitMethod.result(line_bot_api, user_id, event) # postback受信時のメソッド @handler.add(PostbackEvent) def handle_postback(event): if isinstance(event, PostbackEvent): # 時間選択後の処理(乗り換え3) # 時間の登録、出発駅の選択 user_id = str(event.source.user_id) if transitDB.exist_record(user_id, "condition"): select_date = transitMethod.time_received(event).strftime("%m-%d %H:%M") transitDB.update_data(user_id, select_date, "date_and_time") transitMethod.departure_turn(line_bot_api, select_date, event) こちらは、Line側に送られたメッセージを処理するコードの塊です。 大まかな流れは、コメントアウトの部分に番号が振ってあります。 以前記事にした、天気予報を取得するコードに似たようなところが多いですが、時間を選択形式で 入力するため一部だけPostbackEventを受け取った際に実行されるコードが必要となりました。 そのためhandle_addで受信するイベントを追加しています。 次の入力操作を促すコード from datetime import datetime, timezone, timedelta from selenium import webdriver from selenium.webdriver.chrome.options import Options from bs4 import BeautifulSoup from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, QuickReplyButton, MessageAction, QuickReply, TemplateSendMessage, ButtonsTemplate, DatetimePickerTemplateAction ) from DB import transitDB # 検索条件の選択(出発or到着) def condition_turn(line_bot_api, event): items = [QuickReplyButton(action=MessageAction(label=f"{con}", text=f"{con}")) for con in ["出発", "到着"]] line_bot_api.reply_message( event.reply_token, TextSendMessage(text="検索条件を入力してください", quick_reply=QuickReply(items=items)) ) # 日付の選択 def date_and_time_turn(line_bot_api, event): # タイムゾーンの設定 jst = timezone(timedelta(hours=9)) # 現在時刻を取得する(日本標準時JST) now = datetime.now(tz=jst) # 乗り換え案内選択時の日時取得 initial_date = datetime.now(tz=jst) string_initial_date = initial_date.strftime("%Y-%m-%d" + "T" + "%H:%M") # 現在時刻よりも7日前の日付 before_date = initial_date - timedelta(days=7) string_before_date = before_date.strftime("%Y-%m-%d" + "T" + "%H:%M") # 現在時刻よりも7日後の日付 after_date = initial_date + timedelta(days=7) string_after_date = after_date.strftime("%Y-%m-%d" + "T" + "%H:%M") date_picker = TemplateSendMessage( alt_text="時刻の設定", template=ButtonsTemplate( text="時刻の設定", actions=[ DatetimePickerTemplateAction( label="select date", data="backId=12345", mode="datetime", initial=string_initial_date, min=string_before_date, max=string_after_date ) ] ) ) line_bot_api.reply_message( event.reply_token, date_picker ) # 入力された日付の受け取り def time_received(event): # 「yyyy-mm-ddThh:MM」の日付形式 # dictのキーはmodeのもの # 取得しやすいように一旦datetime型に変更 # year = select_date.yearなどで一部だけ取ることも可能 return datetime.fromisoformat(event.postback.params["datetime"]) # 入力された日付の確認送信 def time_confirm(li_bot_api, event): li_bot_api.reply_message( event.reply_token, TextSendMessage() ) # 出発駅の処理案内 def departure_turn(line_bot_api, select_date, event): line_bot_api.reply_message( event.reply_token, [ TextSendMessage(text=select_date), TextSendMessage(text="出発駅を入力してください") ] ) # 到着駅の処理案内 def arrival_turn(line_bot_api, event): line_bot_api.reply_message( event.reply_token, TextSendMessage(text="到着駅を入力してください") ) # 表示条件の処理案内 def options_turn(line_bot_api, event): items = [ QuickReplyButton( action=MessageAction(label=f"{con}", text=f"{con}") ) for con in ["到着が早い順", "乗り換え回数順", "料金が安い順"] ] line_bot_api.reply_message( event.reply_token, TextSendMessage( text="オプションを選択してください", quick_reply=QuickReply(items=items) ) ) def result(line_bot_api, user_id, event): result_data = transitDB.select_userdata(user_id) departure_station = result_data.iat[0, 1] arrival_station = result_data.iat[0, 2] show_rule = result_data.iat[0, 5] line_bot_api.reply_message( event.reply_token, [ TextSendMessage( text=departure_station + "駅から" + arrival_station + "駅への直近の電車を調べます..." ), TextSendMessage( text=transit_scraping(departure_station, arrival_station, show_rule) ) ] ) def transit_scraping(departure_station, arrival_station, show_rule): options = Options() options.add_argument('--headless') driver = webdriver.Chrome(options=options) # 対象ページにアクセス URL = "https://transit.yahoo.co.jp/" try: driver.get(URL) # 出発駅と到着駅の入力 driver.find_element_by_id("sfrom").send_keys(departure_station) driver.find_element_by_id("sto").send_keys(arrival_station) # 検索ボタンをクリックする driver.find_element_by_id("searchModuleSubmit").click() # 検索結果のページのHTMLをBeautifulSoupに流し込む soup = BeautifulSoup(driver.page_source, "html.parser") # 時間が書かれた部分をCSSセレクタで指定し、テキストを抜き出す time = soup.select(".routeSummary li.time")[0].select("span")[0].text return time finally: driver.close() はい、かなり長いです。 ただやっていることは非常に単純明快です。 lineのメソッドとして使っているのは、reply_messageであったりQuickReplyButton、 そして時間を選択することができるDatetimePickerTemplateActionです。 特に、DatetimePickerTemplateActionはラベルやモードなどの変更が まだまだ全て把握できていないところなので、是非とも調べてみてください。 おおまかな流れとしましては、前のコードで乗り換え案内の入力規則・順序に則った文字列が入力されるとその文字列の次のデータ入力を促すといったことを行なっているコードになっています。 DBコード そして最後にDBに関するコードです。 import sqlite3 import pandas as pd # DBから駅名一覧を取得するメソッド def station_list(): # コネクタ作成。dbnameの名前を持つDBへ接続する。 conn = sqlite3.connect("weatherDatabase.db") cur = conn.cursor() # ここから好きなだけクエリを打つ table = cur.execute("select * FROM stations;") data = table.fetchall() # 処理をコミット conn.commit() # 接続を切断 conn.close() # select結果をリストからデータフレームにして返却 data_list = [] for i in data: data_list.append(list(i)) return pd.DataFrame(data_list, columns=["station_name", "ruby"]) # user_idは処理を要求してきたユーザのID # CREATE TABLE transit_input(user_id STRING PRIMARY KEY, departure_station STRING, arrival_station STRING, # date_and_time STRING, condition INTEGER, show_rule INTEGER) # CREATE TABLE stations(station_name STRING, ruby STRING) def create_data(user_id): conn = sqlite3.connect("weatherDatabase.db") cur = conn.cursor() # IDが登録されていなければtry文、登録されていればレコードを削除して再度登録 try: cur.execute("INSERT INTO transit_input(user_id) values(?);", (user_id,)) except sqlite3.IntegrityError: cur.execute("DELETE FROM transit_input WHERE user_id = ?;", (user_id,)) cur.execute("INSERT INTO transit_input(user_id) values(?);", (user_id,)) finally: # データベースへコミット。これで変更が反映される。 conn.commit() conn.close() # user_idは処理を要求してきたユーザのID def exist_record(user_id, column): conn = sqlite3.connect("weatherDatabase.db") cur = conn.cursor() try: # ユーザーレコードの確認->出発か到着の確認->時間の確認->出発駅の確認->到着駅の確認->優先的に表示する指定があるかの確認 if column == "start_up": cur.execute("SELECT * FROM transit_input WHERE user_id = ?;", (user_id,)) elif column == "condition": cur.execute("SELECT * FROM transit_input WHERE user_id = ? and condition IS NOT NULL;", (user_id,)) elif column == "date_and_time": cur.execute("SELECT * FROM transit_input WHERE user_id = ? and date_and_time IS NOT NULL;", (user_id,)) elif column == "departure_station": cur.execute("SELECT * FROM transit_input WHERE user_id = ? and departure_station IS NOT NULL;", (user_id,)) elif column == "arrival_station": cur.execute("SELECT * FROM transit_input WHERE user_id = ? and arrival_station IS NOT NULL;", (user_id,)) elif column == "show_rule": cur.execute("SELECT * FROM transit_input WHERE user_id = ? and show_rule IS NOT NULL;", (user_id,)) # データがあるかどうか判定 if cur.fetchone()[0] != 0: return True else: return False except TypeError: return False finally: conn.close() # user_idは処理を要求してきたユーザのID # dataはユーザIDと一致したレコードを更新する # column更新するカラム def update_data(user_id, data, column): conn = sqlite3.connect("weatherDatabase.db") cur = conn.cursor() try: if column == "condition": cur.execute("UPDATE transit_input SET condition = ? WHERE user_id = ?;", (data, user_id)) elif column == "date_and_time": cur.execute("UPDATE transit_input SET date_and_time = ? WHERE user_id = ?;", (data, user_id)) elif column == "departure_station": cur.execute("UPDATE transit_input SET departure_station = ? WHERE user_id = ?;", (data, user_id)) elif column == "arrival_station": cur.execute("UPDATE transit_input SET arrival_station = ? WHERE user_id = ?;", (data, user_id)) elif column == "show_rule": cur.execute("UPDATE transit_input SET show_rule = ? WHERE user_id = ?;", (data, user_id)) finally: # データベースへコミット。これで変更が反映される。 conn.commit() conn.close() # 乗り換え検索する際に情報を取得する def select_userdata(user_id): conn = sqlite3.connect("weatherDatabase.db") cur = conn.cursor() try: data = [] cur.execute("SELECT * FROM transit_input WHERE user_id = ?;", (user_id,)) data.append(list(cur.fetchone())) return pd.DataFrame( data, columns= [ "user_id", "departure_station", "arrival_station", "date_and_time", "condition", "show_rule" ] ) finally: # データベースへコミット。これで変更が反映される。 conn.commit() conn.close() def delete_record(user_id): conn = sqlite3.connect("weatherDatabase.db") cur = conn.cursor() try: cur.execute("DELETE FROM transit_input WHERE user_id = ?;", (user_id, )) finally: # データベースへコミット。これで変更が反映される。 conn.commit() conn.close() こちらも非常に長いですが、特にやっていることはデータの作成、更新、削除などです。 あまり気にしなくても良いと思います。 最後に この記事を書こうとしてからかなりの日にちが経ってしまいました、、、 実装やアイディア出しにはあまり時間がかからなかったのですが、herokuのdeployやherokuでchromedriverを使用する際の挫折によって非常に時間を取られてしまいました。 (記事の最後に備忘録として残します。) 今回記載したコードを使えば、時間・出発駅・到着駅を入力すれば一応yahoo乗り換え案内のページから情報を取ってきて、乗り換えにかかる時間を表示することができると思います。 ただし、最低限の情報しか取ることができていないので、スクレイピングの改良が必要そうです。 次回の記事はスクレイピングに関する記事になりそうです!! 長々と、書きましたがありがとうございました!! 備忘録 herokuでchromedriverを使用する方法 主にこの記事を参考にさせていただきました。ありがとうございます! どこで詰まったかというと、興味がある方は実際にやってみてもらえると良いのですが、デプロイ後のherokuのログを覗くと次のように表示されます。 /app/.heroku/python/lib/python3.8/site-packages/selenium/webdriver/firefox/firefox_profile.py:208: SyntaxWarning: "is" with a literal. Did you mean "=="? if setting is None or setting is '': 、、、いってることはわかるけどどこいじるの、、、??? まあとりあえず、動作確認してみるか、、、と思い確認すると、特に問題なくスクレイピングの情報がlineに届く。 通るんかい!!!っていう感じでした。 まあ、chromedriverなら特に気にすることはないといったことでしょうかね。 herokuのアプリケーション名 この見出しだけ見ると、どこで詰まる部分あるん?と思う方が大半だと思います。 ただ、一つ上の備忘録と相まってとんでもなくややこしいことになってしまったのです。 まず、この見出しの点で何をしたかというとアプリケーション名を変更しました。 当初、lineアプリの開発を始めようとしていた頃はherokuがどういうものかというのも分からないまま、 調べては実行しを繰り返していました。 そのため、アプリケーション名がbotsampleという非常にダサい名前で開発を行なっていたわけです。 そこで、久しぶりにherokuの管理画面開いたからアプリケーション名変えよう!と思い、いざ変えてみるとlineにメッセージ送っても返信返ってこず。 そこで発生していたエラーのメッセージが上の備忘録のものだったので、 勘違いしてずっとfirefoxは使っていないのにもうこの記事書けないなと思っていました。 もう一旦全部アプリケーション削除して一からやり直そうと思い、 lineの管理画面まで行き色々情報を確認していると、登録していたURLの項目がありました。そこにはhttps://botsample.herokuapp.com/callbackという風に設定されたままでした。 、、、おいおいおい、ここ変えたらもしかしていけるんか?と思い、 変更後のアプリケーション名に変えてみると無事にlinebotから返信が返ってくるという奇跡的な復活を遂げました。 非常に嬉しかったです。。。 ということで、変更は一度で複数のことを行わないことを改めて実感したという報告でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SimSwap で遊んでみました

概要 SimSwap という写真の顔を取り替えるプログラムが公開されていたので、インストールして遊んでみました。 OS が Windows なのと Anaconda を入れていないことで少し手間取ったので、手順を共有したいと思います。 環境 Windows 10 Python 3.8.1 CUDA Toolkit 10.2 NVIDIA GeForce GTX 1080 GeForce Game Ready ドライバ 456.71 手順 1. CUDA Toolkit をインストールする SimSwap/preparation.md を見ると CUDA をインストールした方が良さそうなので、バージョンを合わせてインストールします。 上でも書いたとおり私は Anaconda を入れていないので、conda ではなくインストーラをダウンロードして入れました。 こちら → CUDA Toolkit 10.2 Download 2. git clone する SimSwap の GitHub で URL をコピーして git clone します。 > git clone https://github.com/neuralchen/SimSwap.git 私が clone した時点では以下のコミットでした。 439f6ff - Update README.md 3. 仮想環境を構築する > python -m venv .venv > .venv\Scripts\activate (.venv) > 先程の preparation.md を見て、なるべくパッケージのバージョンを合わせるようにインストールしました。 conda が使えないことで苦労するのは pytorch のインストールだと思います。 Start Locally | PyTorch というページで pip コマンドを調べられますが、そのまんまだとインストールできなかったので、以下のように変えてみました。 (.venv) > pip install torch==1.8.1+cu102 torchvision==0.9.1+cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html CUDA が使えるか確認します。 (.venv) > python >>> import torch >>> torch.cuda.is_available() True >>> exit() (.venv) > あとは書いてあるとおりインストールしました。 (.venv) > pip install insightface==0.2.1 onnxruntime moviepy 4. preparation のつづき insightface のモデルをダウンロードして (解凍して) 配置するようにとあるので、そのとおりにします。 We use the face detection and alignment methods from insightface for image preprocessing. Please download the relative files and unzip them to ./insightface_func/models from this link. (SimSwap の) 学習済みモデルもダウンロードして (解凍して) 配置します。 There are two archive files in the drive: checkpoints.zip and arcface_checkpoint.tar Copy the arcface_checkpoint.tar into ./arcface_model Unzip checkpoints.zip, place it in the root dir ./ [Google Drive] [Baidu Drive] Password: jd2v 上記で配置したものだけ抜粋すると、こんなフォルダ構成になると思います。 SimSwap ├ arcface_model │ └ arcface_checkpoint.tar ├ checkpoints │ └ people │ └ ... ├ insightface_func │ ├ models │ │ ├ glintr100.onnx │ │ └ scrfd_10g_bnkps.onnx │ └ ... └ ... 5. EncodeError 対応 SimSwap/usage.md に実行例があるのでそのとおりにすると、以下のエラーが出ました。 Traceback (most recent call last): File "test_one_image.py", line 35, in <module> model = create_model(opt) File "F:\Dev\Python\clone\SimSwap\models\models.py", line 18, in create_model model.initialize(opt) File "F:\Dev\Python\clone\SimSwap\models\fs_model.py", line 64, in initialize netArc_checkpoint = torch.load(netArc_checkpoint) File "F:\Dev\Python\clone\SimSwap\.venv\lib\site-packages\torch\serialization.py", line 593, in load return _legacy_load(opened_file, map_location, pickle_module, **pickle_load_args) File "F:\Dev\Python\clone\SimSwap\.venv\lib\site-packages\torch\serialization.py", line 772, in _legacy_load result = unpickler.load() File "F:\Dev\Python\clone\SimSwap\.venv\lib\site-packages\torch\serialization.py", line 720, in persistent_load _check_container_source(*data) File "F:\Dev\Python\clone\SimSwap\.venv\lib\site-packages\torch\serialization.py", line 639, in _check_container_source f.write(lines) UnicodeEncodeError: 'cp932' codec can't encode character '\xe0' in position 1153: illegal multibyte sequence Windows 特有の問題かと思いますが、以下のファイルを変更して対応しました。 .venv\Lib\site-packages\torch\serialization.py 635行目 (前) with open(file_name, 'a+') as f: (後) with open(file_name, 'a+', encoding='utf-8') as f: 実行例 (.venv) > python test_one_image.py --isTrain false --name people --Arc_path arcface_model/arcface_checkpoint.tar --pic_a_path crop_224/cage.jpg --pic_b_path crop_224/mouth_open.jpg --output_path output/ pic_a pic_b result
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pyenv経由でMacにPythonをインストールする

MacにはデフォルトでPythonがインストールされていますが、バージョンが古く、今後複数のバージョンを使用する可能性も考慮してバージョン管理ツールである「pyenv」(ぱいえんぶ)を使って新しいPythonをインストールしてみました。 2021/5以降はパスを通す際に一部記述を変更する必要があるということがあったので、記事にしました。内容に不備などありましたら教えていただけると幸いです。 Homebrewのバージョン確認 まずはpyenvのインストールをするためにHomebrewを準備します。Macでは最初から用意されていますが、一応確認しておきます。 ターミナルを開き、下記コードをコピペしてください。 brew -v 以下のようになったらOKです。 Homebrew 3.2.0 もしbrew: command not foundとか出てきた場合はHomebrewがインストールされていませんので、下記の記事などを参考にお持ちのPCにHomebrewをインストールしてください。 pyenvをインストールする 続いてPythonのバージョン管理ツール「pyenv」をインストールします。 一応既にインストールされているか、下記のコードを入力してみてください。 pyenv -v インストールした覚えはないので、「コマンドが見つかりません」的なメッセージが返ってくるのではないでしょうか。 pyenv: command not found ですよね。 もしここで、 pyenv x.x.x のようにバージョン名が表示されていれば、既にインストールされているので、以下のコマンドは飛ばして結構です。 私と同様、まだインストールしていない方は以下のコマンドを入力し、pyenvをインストールしましょう。 brew install pyenv こういう画面が出ました。 よくわからないけど、できてるっぽい。 Updating Homebrew... ==> Auto-updated Homebrew! Updated 2 taps (homebrew/core and homebrew/cask). ==> New Formulae baidupcs-go pillow plow pyflow soapyrtlsdr soapysdr terminator waypoint ==> Updated Formulae Updated 146 formulae. ==> New Casks affinity-photo castr ears hyperkey keyboardholder lagrange ==> Updated Casks Updated 83 casks. ==> Downloading https://ghcr.io/v2/homebrew/core/pyenv/manifests/2.0.1 ######################################################################## 100.0% ==> Downloading https://ghcr.io/v2/homebrew/core/pyenv/blobs/sha256:06646f2a7779fc545226e0a79b26e3c7a3e50f5 ==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:06646f2a7779fc545226e0 ######################################################################## 100.0% ==> Pouring pyenv--2.0.1.big_sur.bottle.tar.gz ? /usr/local/Cellar/pyenv/2.0.1: 760 files, 2.6MB ちゃんとインストールできたか確認しましょう。 pyenv -v すると以下のようになりました。 pyenv 2.0.1 順調です。いい感じ。 pyenvのPATHを通す こういう、新しくツールをインストールしたときはPATH通さなきゃ使えないんですよね。私の場合シェルはzshを使っているので、Qiitaなどを参考に以下のコードを入力しました。 echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc echo 'eval "$(pyenv init -)"' >> ~/.zshrc zshrcを書き換えたときは変更を反映させる必要があります。 以下のコードを入力してください。 source ~/.zshrc (参考記事) 【投稿】bashが起動時〜bashrcとbash_profileについて〜 自分メモ用 するとこんな表示が、、 WARNING: `pyenv init -` no longer sets PATH. Run `pyenv init` to see the necessary changes to make to your configuration. おとなしく言われた通りにコードを入力してみる。 pyenv init すると以下のような表示が返ってきました。 # (The below instructions are intended for common # shell setups. See the README for more guidance # if they don't apply and/or don't work for you.) # Add pyenv executable to PATH and # enable shims by adding the following # to ~/.profile and ~/.zprofile: export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init --path)" # <--- ココがなんか違う # Load pyenv into the shell by adding # the following to ~/.zshrc: eval "$(pyenv init -)" # Make sure to restart your entire logon session # for changes to profile files to take effect. あれ、さっきzshrcに書いたのと違うやつがいる。 ということでコードを修正します。(vimコマンドを使ってターミナル上で済ませます。) vi .zshrc iキーを押してインサートモードにした上で、先ほどの eval "$(pyenv init --path)" に書き換えましょう。 書き換えたらescキーでコマンドモードに戻り、:wqで保存して終了します。 そしてzshrcを書き換えたら変更を反映させましょう。 source ~/.zshrc 最初からコレ書いとけよ、というご意見はごもっともなのですが、執筆時現在において多くの教材や記事に上記コードで説明されているものが多かったため、あえてそのままの流れて書いています。ご了承くださいませ。 で、Githubのイシュー見るともうそのままのことが書いてありますね。2021年5月にあった仕様の変更で今回のような警告が出るようになったみたいです。 https://github.com/pyenv/pyenv/issues/1906 Pythonをインストールする さて、気を取り直してPythonのインストールをしていきます。 今回は、先ほどインストールしたpyenvを使います。 まずは以下のコードを入力してみましょう。 pyenv install --list インストール可能なPythonのバージョン一覧が表示されます。 Available versions: 2.1.3 2.2.3 ~~ 3.9.5 3.10.0b2 3.10-dev ~~ stackless-3.5.4 stackless-3.7.5 今回は3.9.5をインストールします。 pyenv install 3.9.5 結構時間がかかりましたが、以下のような表示が返ってきました。 python-build: use openssl@1.1 from homebrew python-build: use readline from homebrew Downloading Python-3.9.5.tar.xz... -> https://www.python.org/ftp/python/3.9.5/Python-3.9.5.tar.xz Installing Python-3.9.5... python-build: use readline from homebrew python-build: use zlib from xcode sdk Installed Python-3.9.5 to /Users/bnn/.pyenv/versions/3.9.5 よくわかりませんが、できてるみたいです。 Pythonのバージョンを確認してみましょう。 python --version Python 2.7.16 あれ? あぁ、pyenvを用いて使用するPythonのバージョンを変更する必要がありました。 では以下のコードでpyenvで管理しているPythonのバージョン一覧を確認してみましょう。 pyenv versions すると以下の表示が返ってきました。 * system (set by /Users/bnn/.pyenv/version) 3.9.5 インストールはできているけどデフォルトのバージョンを使っているということです。せっかくインストールしたんだから最新版を使いたいです。 pyenv global 3.9.5 これで使用するバージョンを変更できます。 再度バージョンを確認してみましょう。 pyenv versions すると以下の表示が返ってきました。 system * 3.9.5 (set by /Users/bnn/.pyenv/version) ちゃんと切り替わっているみたいです。 一応Pythonコマンドでも確認してみます。 python --version ドキドキ... Python 3.9.5 完璧です。 天才かもしれません。 さいごに 今回はバージョン管理ツールのpyenvのインストール及びパスの通し方、使用するバージョンの変更方法について理解することができました。 予期せぬ警告が出たときは、落ち着いて読んでみることが大事ですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

立科町の形で学ぶ一番細い場所の測定方法

はじめに 長野県の地図を見ていると気になるものがありました。 この町、めっちゃ細いところがありますね。 これは長さを測ってみるしかないですね どれくらい細いのか、調べてみました。 手順 立科町の画像を用意し、グレースケールにします。 img = cv2.imread("tateshina.png",3) gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) thresh,bin_img = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) plt.imshow(bin_img,cmap='gray') よくある、モノクロ立科町が出来ました。 2つ以上に分割する この状態から任意の二点を取得して、最短距離を求めても望みのものは出ないので 二分割しましょう。 itter=1 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)) while True : erosion = cv2.erode(bin_img,kernel,iterations = itter) contours_erosion, _ = cv2.findContours(erosion, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours_erosion) > 1: break itter += 1 plt.imshow(erosion) plt.show() kernelを用意して、少しずつerodeをかけていき、二つに分割されたところで切ります。 2つのブロブの最短距離を探す 分かりにくいですが、切科町になりました。 この時の2つのブロブの最短距離を求めましょう。 def get_distance(x1, y1, x2, y2): return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) distance_list = [] ch_list = [] for i in (contours_erosion[0]): for j in (contours_erosion[1]): distance_list.append(get_distance(*i[0],*j[0])) ch_list.append([i,j]) ch_list[np.argmin(distance_list)] x1 = ch_list[np.argmin(distance_list)][0][0][0] y1 = ch_list[np.argmin(distance_list)][0][0][1] x2 = ch_list[np.argmin(distance_list)][1][0][0] y2 = ch_list[np.argmin(distance_list)][1][0][1] fig, ax = plt.subplots(figsize=(8, 8)) fig = 0 cv2.line(erosion, (x1, y1), (x2, y2), (128, 128, 255), thickness=1) plt.imshow(erosion,cmap="gray") 最短となる線分が引けました。 何がしたかったと申しますと、この線を通る任意の二点を結ぶ線分がもっとも細い箇所となるので それらを求めたいと思います。 エッジを取得する エッジを取得しましょう。 edge = cv2.morphologyEx(bin_img, cv2.MORPH_GRADIENT, kernel) plt.imshow(edge,cmap="gray") サイバー立科町が取れました。これで輪郭の座標が取れますね。 分割した線に近い点を集める 任意の二点で計算してもいいですが、計算量を減らす工夫をしましょう。 今回は先ほど引いた2点の重心からの距離が近い点を集めましょう。 row_list = np.where(edge == 255)[0] col_list = np.where(edge == 255)[1] near_list = [] for i in range(len(col_list)): u = np.array([(x2 + x1)/2, (y2 + y1)/2]) v = np.array([col_list[i], row_list[i]]) L = np.linalg.norm(u-v) if L > itter*2: continue near_list.append(v) これで、多少点は減らせたはずです。 最短の線を通る2点を探す 2つの線分が、交わっているか否かは def intersect(p1, p2, p3, p4): tc1 = (p1[0] - p2[0]) * (p3[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p3[0]) tc2 = (p1[0] - p2[0]) * (p4[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p4[0]) td1 = (p3[0] - p4[0]) * (p1[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p1[0]) td2 = (p3[0] - p4[0]) * (p2[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p2[0]) return tc1*tc2<0 and td1*td2<0 shortest_list = [] from itertools import combinations comb = list(combinations(near_list,2)) for tmp in comb: if (intersect([x1,y1],[x2,y2],tmp[0],tmp[1])) == 1: L = np.linalg.norm(np.array(tmp[0])-np.array(tmp[1])) shortest_list.append(L) else: shortest_list.append(50000) もし交わっているなら長さを保存し、そうでないなら、あり得ない距離を入れています。 short = np.argmin(shortest_list) f1x,f1y,f2x,f2y=*comb[short][0],*comb[short][1] cv2.line(erosion,(f1x,f1y) ,(f2x,f2y), (192,0, 192), thickness=1) res = cv2.addWeighted(gray, 0.5, erosion, 0.5, 0) fig, ax = plt.subplots(figsize=(32, 32)) plt.imshow(res) 小さくてわかりにくいですが、最短の場所が分かりました。 これで、実際に立科町で測定する必要がなくなりました。 総括 画像処理は単純な処理が多い分、複雑なことが出来て面白いですね。 縮小や拡大の方法に不安があるので、ほかに良い方法がないかは考えてみたいと思います。  参考 https://www.st-hakky-blog.com/entry/2018/09/05/012837 https://qiita.com/Fumio-eisan/items/05a6506da8cc88d89e49
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む