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

EasyOCRメモ

ISSUES/365まとめ original repo Trainするのは別repoを使用 trainmodel None-ResNet-BiLstm-CTC https://github.com/JaidedAI/EasyOCR/issues/365#issuecomment-789957678 quickdemo python3 demo.py --image_folder /path/to/images/folder/ \ --saved_model ~/.EasyOCR/model/latin.pth \ --Transformation None \ --FeatureExtraction ResNet \ --SequenceModeling BiLSTM \ --Prediction CTC \ --hidden_size 512 \ --character "$(cat characters.txt)" datafiltering off valid_dataを作成する(今は適当なので作ってなし) python3 train.py --train_data result/ --valid_data result/ --Transformation None --FeatureExtraction ResNet --SequenceModeling BiLSTM --Prediction CTC - -data_filtering_off diff --- a/train.py +++ b/train.py @@ -246,9 +246,9 @@ if name == 'main': parser.add_argument('--grad_clip', type=float, default=5, help='gradient clipping value. default=5') parser.add_argument('--baiduCTC', action='store_true', help='for data_filtering_off mode') """ Data processing """ - parser.add_argument('--select_data', type=str, default='MJ-ST', + parser.add_argument('--select_data', type=str, default='/', help='select training data (default is MJ-ST, which means MJ and ST used as training data)') - parser.add_argument('--batch_ratio', type=str, default='0.5-0.5', + parser.add_argument('--batch_ratio', type=str, default='1', help='assign ratio for each selected data in the batch') parser.add_argument('--total_data_usage_ratio', type=str, default='1.0', help='total data usage ratio, this ratio is multiplied to total number of data.') @@ -257,7 +257,7 @@ if name == 'main': parser.add_argument('--imgW', type=int, default=100, help='the width of the input image') parser.add_argument('--rgb', action='store_true', help='use rgb input') parser.add_argument('--character', type=str, - default='0123456789abcdefghijklmnopqrstuvwxyz', help='character label') + default='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYJ', help='character label') parser.add_argument('--sensitive', action='store_true', help='for sensitive character mode') parser.add_argument('--PAD', action='store_true', help='whether to keep ratio then pad for image resize') parser.add_argument('--data_filtering_off', action='store_true', help='for data_filtering_off mode')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WindowsのOpenCVでCUDAを使うためにビルドするメモ

https://twitter.com/yasnis/status/1406499958413426689 この辺に関連して苦戦した記録。あとでちゃんと書く(かも)。 RTX 3070 Game Ready Driver 461.40 CUDA ToolKit 11.2 cuDNN OpenCV 4.5.1 opencv_contrib Ninja "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Auxiliary/Build/vcvars64.bat" set "generator=Ninja" set "openCvSource=C:/tools/opencv" set "openCVExtraModules=C:/tools/opencv_contrib-4.5.1/modules" set "openCvBuild=%openCvSource%/build" set "buildType=Release" set "pathToPythonDir=C:/Program Files/Python37" cmake -B"%openCvBuild%/" -H"%openCvSource%/" -G"%generator%" ^ -DCMAKE_BUILD_TYPE=%buildType% ^ -DOPENCV_EXTRA_MODULES_PATH="%openCVExtraModules%/" ^ -DINSTALL_TESTS=ON -DINSTALL_C_EXAMPLES=ON -DBUILD_EXAMPLES=ON ^ -DBUILD_opencv_world=ON ^ -DWITH_CUDA=ON ^ -DCUDA_TOOLKIT_ROOT_DIR="C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v11.2" ^ -DCUDA_FAST_MATH=ON ^ -DWITH_CUBLAS=ON ^ -DCUDA_ARCH_PTX=8.6 ^ -DCUDA_ARCH_BIN=8.6 ^ -DWITH_NVCUVID=ON ^ -DWITH_OPENGL=ON ^ -DWITH_MFX=ON ^ -DBUILD_opencv_python3=ON ^ -DPYTHON3_INCLUDE_DIR="%pathToPythonDir%/include" ^ -DPYTHON3_LIBRARY="%pathToPythonDir%/libs/python37.lib" ^ -DPYTHON3_EXECUTABLE="%pathToPythonDir%/python.exe" ^ -DPYTHON3_NUMPY_INCLUDE_DIRS="%pathToPythonDir%/lib/site-packages/numpy/core/include" ^ -DPYTHON3_PACKAGES_PATH="%pathToPythonDir%/Lib/site-packages/" ^ -DOPENCV_SKIP_PYTHON_LOADER=ON ... # OpenCVのビルド cmake --build . # OpenCVのインストール cmake --build . --target install この辺が参考になった https://jamesbowley.co.uk/accelerate-opencv-4-5-0-on-windows-build-with-cuda-and-python-bindings/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python Challenge level 0

Pythonに慣れたいので暇をみてですが、The python challengeを今更(10年以上前のサイト!)ながら挑戦していこうと思います。 では早速、level 0 なんだかよく分からない絵です。2の38乗(2^38)を計算せよということなのでしょうか。Pythonプロンプトで入力すればすぐに答えは出てきます。 0.py ]>> 2**38 274877906944 したがって、次のレベルのアドレスは274877906944.htmlでしょう。 正解でした。 次→The python challenge level1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンテナの型ヒントのつけ方まとめ

1. はじめに 「2021年版Pythonの型ヒントの書き方」の「コレクションの種類の使い分け」を読んだ際「何をどう使い分ければいいんだ...」と絶句したので、簡単にまとめてみました。 2. 型ヒントの注意点 Python3.8までは、以下の書き方がされていました。 Python3.8以前 from typing import List, Sequence def func(a: List[int], b: Sequence[float]) -> float: ... Python3.9からは、typingからコンテナのgeneric versionを呼び出すのがdeprecatedになり、以下の書き方が推奨されています。 Python3.9以降 from collections.abc import Sequence def func(a: list[int], b: Sequence[float]) -> float: ... ちなみにPython3.7以降であれば、__future__.annotationsをimportすれば後者の書き方ができます。 Python3.7-3.8 from __future__ import annotations from collections.abc import Sequence def func(a: list[int], b: Sequence[float]) -> float: ... この記事では、前者の書き方をもとに、型ヒントは全てtypingの形に合わせて記載します。 3. 対象のコンテナ 以下の8つのコンテナについて調べました。 tuple list deque dict Counter defaultdict set frozenset 4. 継承関係の概観図 継承関係をまとめると、以下の図の通りになります。 (PlantUMLで作成しました。) 以下については、図がごちゃごちゃになるため、今回は省きました。 MappingView ItemsView KeysView ValuesView OrderedDict 5. 型ヒントの使い分け目安 それぞれについて、mypy --strictがSuccessになる簡単な例を載せています。 5-1. Iterable:forしたい 「forループしたいだけで型は何でもいい」という場合はこれが使えます。中でループしているsumやmaxもこれでいけます。 def func(values: Iterable[int]) -> int: return sum(values) func((1, 2)) func([1, 2]) func(deque([1, 2])) func({1: 'a', 2: 'b'}) func(Counter([1, 1, 2])) func(defaultdict(str, {1: 'a', 2: 'b'})) func({1, 2}) func(frozenset([1, 2])) 5-2. Sized:len()したい 「サイズが欲しいだけで型は何でもいい」という場合はこれが使えます。 def func(values: Sized) -> int: return len(values) func((1, 2)) func([1, 2]) func(deque([1, 2])) func({1: 'a', 2: 'b'}) func(Counter([1, 1, 2])) func(defaultdict(str, {1: 'a', 2: 'b'})) func({1, 2}) func(frozenset([1, 2])) 5-3. Container:inしたい 「ある値が入っているか知りたいだけで型は何でもいい」という場合はこれが使えます。 def func(values: Container[int]) -> bool: return 1 in values func((1, 2)) func([1, 2]) func(deque([1, 2])) func({1: 'a', 2: 'b'}) func(Counter([1, 1, 2])) func(defaultdict(str, {1: 'a', 2: 'b'})) func({1, 2}) func(frozenset([1, 2])) 5-4. Collection:1から3まで全部したい 例えば下のように平均を求めてみると、IterableかSizedだけではエラーが出てしまいます。ここでCollectionの出番です。 def func(values: Collection[int]) -> float: return sum(values) / len(values) func((1, 2)) func([1, 2]) func(deque([1, 2])) func({1: 'a', 2: 'b'}) func(Counter([1, 1, 2])) func(defaultdict(str, {1: 'a', 2: 'b'})) func({1, 2}) func(frozenset([1, 2])) 5-5. Sequence:object[index]したい 添え字で中身を取得したい場合はこれが使えます。 ここからはコンテナが限定されて、対応するのはtuple, list, dequeです。 def func(values: Sequence[int]) -> Sequence[int]: return values[:2] func((1, 2)) func([1, 2]) func(deque([1, 2])) 5-6. MutableSequence:5 + 代入や削除もしたい Sequenceでは取得はできても、代入や削除しようとするとエラーになります。その場合はこれが使えます。 対応するのはlist, dequeです。 def func(values: MutableSequence[int]) -> MutableSequence[int]: values[0] = 3 return values[:2] func([1, 2]) func(deque([1, 2])) 5-7. Mapping:辞書だけど取り出すだけでいい 辞書っぽいコンテナから中身を取得したい場合はこれが使えます。 対応するのはdict, Counter, defaultdictです。 def func(mapping: Mapping[str, int]) -> int: return mapping['a'] func({'a': 1, 'b': 2}) func(Counter(['a', 'a', 'b'])) func(defaultdict(int, {'a': 1, 'b': 2})) 5-8. MutableMapping:7 + 代入や削除もしたい Mappingでは取得はできても、代入や削除しようとするとエラーになります。その場合はこれが使えます。 対応するのはdict, Counter, defaultdictです。 def func(mapping: MutableMapping[str, int]) -> int: mapping['a'] = 3 return mapping['a'] func({'a': 1, 'b': 2}) func(Counter(['a', 'a', 'b'])) func(defaultdict(int, {'a': 1, 'b': 2})) 5-9. AbstractSet:セットだけど何もしない セットを、特に変更を加えずに使う場合はこれが使えます。 対応するのはset, frozensetです。 def func(values: AbstractSet[int]) -> AbstractSet[int]: return values & {1, 2} func({1, 2}) func(frozenset([1, 2])) 5-10. MutableSet:9 + 代入や削除もしたい AbstractSetでは追加や削除しようとするとエラーになります。その場合はこれが使えます。 対応するのはsetです。 def func(values: MutableSet[int]) -> MutableSet[int]: values.pop() return values func({1, 2}) 6. 参考 typing.py _collections_abc.py collections.abc — Abstract Base Classes for Containers
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PySide2を使ったPyshonのツール作成

Pythonの初学者ですが、初めてツールを作成しました。 PySide2でcsvファイルを読み込んで、JSON,YAMLファイルに変換するだけのツールです。 これを使った目的は学習でDockerCompose.Ymlファイルを作成するが、慣れていないので見えないバグにいつも悩まされる。 一括で変換してなるべく入力ミスをなくしたい。 PySide2を勉強していたので、その復習も兼ねて作りました。 ・QfileDialogでcsvファイルを選択して、ListWidgetに内容を表示させる。 ・ListWidgetの情報をJSONボタンをクリックした時に情報を取得してJSONファイルに変換して、ファイルを書き込む。 ・ListWidgetの情報をYAMLボタンをクリックした時に情報を取得してYAMLファイルに変換して、ファイルを書き込む。 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import csv import json import yaml import codecs import sys from PySide2 import QtWidgets, QtCore #init関数を定義 class Jymaker(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Json Yaml fileMaker") self.build_ui() #SignalとSlotを設定 self.connect_signal_slot() def callback_line_edit_editingFinished(self): file = self.line_edit.text() if not file.endswith(".csv"): return url = QtCore.QUrl(file) if url.isLocalFile(): file = url.toLocalFile() if os.path.exists(file): self.line_edit.setText(file) json_list = self.load_csv(file) self.listwgt.clear() self.listwgt.addItems(json_list) #Openボタンを押したときの挙動を設定 def callback_file_dialog_button_clicked(self): file, filter_ = QtWidgets.QFileDialog.getOpenFileName( self, "Open CSV", "C:/Users/User/Documents/Jymaker/csv", "CSV (*.csv)" ) #csvファイルの中の情報を取り出す #listWedgetに情報を入れていく if file: self.line_edit.setText(file) json_list = self.load_csv(file) self.listwgt.clear() self.listwgt.addItems(json_list) #returnで返された、変数を受け取り表示させる。 #JSONボタンを押したときにの処理 def callback_create_file_button_clicked(self): jsdata = [] for row in range(self.listwgt.count()): item = self.listwgt.item(row) jsdata.append(item.text()) filename = "C:/Users/User/Documents/Jymaker/json/sample.json" with open(filename, "w", encoding="utf_8") as f: json.dump(jsdata, f ,indent=4) #メッセージボックス QtWidgets.QMessageBox.about( self, "json file was created.", "json file was created." ) #YAMLボタンを押したときにの処理 def callback_create_yaml_button_clicked(self): yamldata = [] for row in range(self.listwgt.count()): item = self.listwgt.item(row) yamldata.append(item.text()) filename = "C:/Users/User/Documents/Jymaker/yml/sample.yml" with codecs.open(filename, 'w', 'utf-8') as f: yaml.dump(yamldata, f, encoding='utf-8', allow_unicode=True) #メッセージボックス QtWidgets.QMessageBox.about( self, "Yaml file was created.", "Yaml file was created." ) #パーツを作る関数の定義 def build_ui(self): self.line_edit = QtWidgets.QLineEdit() self.file_dialog_button = QtWidgets.QPushButton("Open") self.line = QtWidgets.QFrame() self.line.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) self.listwgt = QtWidgets.QListWidget() self.create_file_button = QtWidgets.QPushButton("JSON") self.create_yaml_button = QtWidgets.QPushButton("YAML") #GUIを表示させるためのレイアウト設置 form_layout = QtWidgets.QFormLayout() form_layout.addRow("CSV File", self.line_edit) form_layout.addRow("", self.file_dialog_button ) form_layout.addRow(self.line) form_layout.addRow("File", self.listwgt) form_layout.addRow(self.create_file_button) form_layout.addRow(self.create_yaml_button) self.setLayout(form_layout) #SignalとSlotを繋げる処理を行う関数 def connect_signal_slot(self): self.line_edit.editingFinished.connect(self.callback_line_edit_editingFinished) self.file_dialog_button.clicked.connect(self.callback_file_dialog_button_clicked) self.create_file_button.clicked.connect(self.callback_create_file_button_clicked) self.create_yaml_button.clicked.connect(self.callback_create_yaml_button_clicked) #csvファイルの内容をを読み込む。 # 読み込んだ内容をQListWidgetItemに返して表示させる。 def load_csv(self, file): json_list =[] with open(file, "r", encoding="utf_8") as f: reader = csv.reader(f) for row in reader: json_list.append(",".join(row)) return(json_list) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) win = Jymaker() win.show() sys.exit(app.exec_()) 苦労したこと 1.listWedgetに情報を表示せせるのに苦労しました。 思うように情報が表示できない。 エラーが発生しないから、どこが間違っているのか分からない。 listWedgetが間違っているとの指摘もあり、 QTableWidget や QTableView への表示が適切との御意見を頂きましたが、 teratail.comに質問を投稿して、 引数に,を入れることを教えて頂き、無事、情報を表示せることができました。 json_list.append(",".join(row)) 2.listWedgetからの情報を取得してJsonファイルに変換できない。 csvファイルから単にJsonファイルに変換するのなら、Qiitaに情報があります。 listWedgetの情報を取得してJSONファイルに変換できない。 ファイルが出力されない。 ググっても分からなくて心が折れそうになりました。 QListWidgetのitermを取り出して、文字情報を取り出すには、text()メソッドを使いました。 そこからJSONファイルを作成するコードを書いたのですが、上書きされてしまい最後の行しか内容がない。 これが問題の行 def callback_create_file_button_clicked(self): for row in range(self.listwgt.count()): item = self.listwgt.item(row) jsonlist = item.text() filename = "C:/Users/User/Documents/Jymaker/json/sample.json" with open(filename, "w", encoding="utf_8") as f: json.dump(jsonlist, f) 上のコードは、jsonlistという変数にitem.text()の内容が上書きされて保持される。 Udemyの先生に教えて頂き、これを以下のように修正 def callback_create_file_button_clicked(self): jsdata = [] for row in range(self.listwgt.count()): item = self.listwgt.item(row) jsdata.append(item.text()) filename = "C:/Users/User/Documents/Jymaker/json/sample.json" with open(filename, "w", encoding="utf_8") as f: json.dump(jsdata, f ,indent=4) 変数を変更しましたが、jsdata変数を初期化して、list.appendで、リストに要素を追加するようにしました。 これによりcsvフィルの内容が全部、JSONファイルに変換され保存できました。 YAMLファイルも同様ですが、PyYAML というパッケージをインストールする必要があります。 書き込むとき(日本語が入っているとき)は.codecsをインポートする必要があります。 今後について JSONファイルもYAMLファイルも文法的に問題ないか専用のチェッカーWebサービスで確認したら問題はありませんでした。 DockerCompose.ymlを生成し問題なく出力されたのですが、csvファイルのフォーマットに注意が必要。 これが正常に使えるか検証。 今度はJSONファイルやYAMLファイルをcsvファイルに変換する機能を実装したい。 コード上でファイル名を指定しているので、GUIでファイル名を指定できるようにしたい。 PyInstallerを使いexeファイルに変換する。 listWedgetからQTableWidgetに変更。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

scipyでFittingするためのテンプレート

概要 たまに簡単なFittingをする時があるのですが、たまになのでよくやり方を忘れます。なので忘れないためにここに書いちゃおうという流れになります。 今回はScipy.optimizeのCurve_Fitを使用します。 from scipy.optimize import curve_fit import numpy as np 1次関数のFit 1次関数で最適化しようと思っている仮データを作ります。 x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 4, 3, 5, 4, 7, 6, 9, 10, 13, 10 ] x, y = np.array(x), np.array(y) 関数を定義し、Fittingするだけです。 def fit_linear(x, a, b): return a*x + b popt, pcov = curve_fit(fit_linear, x, y) popt[0]にaが、popt[1]にbが入っています。pcovは共分散です。 この場合はpopt = [1.01818182, 1.45454545]となりました。 もし元データと比較したい場合は...matplotlib.pyplot as pltをインポートしている前提で... plt.scatter(x, y) plt.plot(x, popt[0]*x + popt[1]) としてあげればOKでしょう。 指数関数を使ったFit これも基本は同じなのですが... なにかしら原点を通っていて、2次関数では表せないようなグラフがあり、 y = exp(a*x)-1 でFittingするとします。 def fit_func(x, a): return np.exp(a*x)-1 popt, pcov = curve_fit(fit_func, x, y) plt.scatter(x, y) plt.plot(x, np.exp(param[0]*x)-1) まあほとんど同じです。 とりあえず簡易的に最適化したい時はこれでなんとかしましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Clean Architecture完全に理解したので、二酸化炭素濃度システムで再現してみた

所感など 先日CleanArchitectureを読んで感想をまとめました。 なんとなく書いてある意味は伝わったし、完全に理解したと確信したので「いっちょ趣味アプリでも作ってみますか〜w」と軽い気持ちでアプリを作ってみました。 やはり手を動かしてみると気づくことが多く、それこそ途中で「CleanArcitectureなにもわからない」状態に陥ったりもしました 何度か立ち止まって書籍を読み返したりして、一度読んだだけでは理解できていなかった部分を掘り下げることができました。しんどいですけど、やっぱり読みっぱなしじゃなくアウトプットすることは大切ですね。(自戒 成果物 MH-Z19C(二酸化炭素濃度センサー)を使って部屋の二酸化炭素濃度を測定するCUIアプリです。 co2checker ちなみにラズパイZeroで作ったのですが、本筋と外れるので結線や環境構築は割愛します。知りたい方がいたらコメントください。 ディレクトリ構成 src/  ├ main  ├ domain/ ・・・図でいうところの「Entity」 。システムの登場人物たち。  ├ driver/  ・・・図で言うところの「Gateway」。低レイヤ層との通信を担う。  ├ handler/ ・・・図で言うところの「Controller」。  └ usecase/ ・・・そのままUsecase層 クラス図 Pythonなので厳密には抽象クラスはないですが、依存性の逆転箇所がわかりやすいのでAbstract扱いにしています 迷ったポイントなど まずそこまでpython慣れてないのでその辺は辛かったのですが、もう少しアーキテクチャ寄りのはなしなど。 ユースケースって、何? 途中ゲシュタルト崩壊するくらい悩みました。 具体的には「センサーで二酸化炭素濃度を測り、1001以上ならLINEで通知する」を実装するときに、どこまでを1ユースケースにするかを迷いました。 「測定する」と「LINEで通知する」の2つに分けるか、「測定して濃度次第では通知する」の1つにまとめるかの2パターンが考えられましたが、今回は後者を選択しました。 前者は測定結果を見て通知するかどうか判断するのがController層になってしまうのですが、Entityが持つメソッドを叩くのにふさわしいのはUsecaseだと判断したので、後者を選びました。 とはいえ、今回DTOを使わなかったのでこういう形になったのかとも思います。仮にプレーンなオブジェクトをControllerが扱えるなら、やはりユースケースを2つに分ける選択肢もアリだったんじゃないかと思います。 DTOって本当に要る? いや要るんですが、今回はどう考えても冗長なので作りませんでした。 書籍では「レイヤをまたぐオブジェクトと、ドメイン知識を表すオブジェクトは変更される理由が異なる。なのでDTOでEntityをシステム都合の変更から守りましょう」という趣旨のことが書かれていたと思います。とはいえ今回のCo2エンティティはvalueとis_dangerous()メソッドしか持たないミニマムなクラスです。 Co2NotifierCommandみたいなDTOクラスを作ったところでvalueくらいしか渡すものがないので今回はわざわざDTOを作るのは控えました。 でもお仕事で作るようなシステムなら絶対最初から作っておいたほうがいいんでしょうね・・・。 Boundaryどこに置くか問題 依存性の逆転を担う抽象クラスですが、ディレクトリ構成的にどこに置くかは悩みました。 UseCase層に置くと境界線が際立つのですが、いまいちパッとしないんですよね・・・。Usecaseディレクトリにユースケース以外のクラスが置いてあるのが少し気持ち悪いと言うか・・・。 ということで今回は実装クラスとまとめてGateway層の「Driver」ディレクトリに突っ込んでおきました。 この辺も正解はないところだと思いますが…。皆さんは抽象はどこに置いているんでしょう? 作ってわかるイケてるポイント 単体テストの書きやすさ 依存がコントロールされているため、基本的に1つ下の(内側の)レイヤだけをモックするだけでテストができるため、考えるコストが少ないです。 差替可能 こういったシステムだと効果が実感しやすいと思いました。 例えば二酸化炭素濃度を測定するセンサーを変えたとしても、AbstractSensorクラスさえ実装すればこれまでのクラス群がそのまま使えます。 WebアプリでUIやDBを差し替えることはそうそうないと思いますが、IoTといった世界では差し替え可能のアーキテクチャは強みだと感じます。 このあと 次はフロント側も作ってみたいです。 Reactでクリーンアーキテクチャとか本当にできるのか・・・?と懐疑的なので、それらしい書籍などを見つけて実装してみたいなと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リアルタイムチャートをDjango ChannelsとEpoch.jsでつくってみた

はじめに あるとき、モーションセンサデータをリアルタイムで可視化したいなと思うことがありました。そこで、ネットでリアルタイム可視化ツールについて調べてみると、近年のIoTブームもありたくさんのツール、製品が世の中に出ていました。無料のツールだと、「Grafana」などがあります。個人的には「Node-RED」のdashboardも気に入っています。 では、なぜDjangoでリアルタイムチャートを作ろうと思ったのか? 理由は3つです。 WebアプリケーションであるためWebブラウザから手軽に利用できる 可視化をちょっと試してみたい、ちょっとカスタマイズしたいとき、言語がPythonだと嬉しい人が多そう(フロントエンド部分は結局jsに頼るのですが。。) 自分の勉強のため 自分の勉強がてら、誰かのお役に立てれば幸いです。 システム構成 先に結論です。完成版はこちらです。 概要 今回のシステムはDjango Channelsを用いて実装します。Django Channelsでは、Channelsを介してメッセージをやり取りすることで、HTTPと似た方法でWebsocketの実装が行えます。(画像:Finally, Real-Time Django Is Here: Get Started with Django Channelsより) HTTP & Websocket 今回のシステムではブラウザから送られてくるHTTPリクエストの処理はHTTP(VIEW)が、センサーから送られてくるデータをブラウザへ送信する処理はWebsocket(COUSUMER)が実行します。 センサとの通信 センサデータはCONSUMERへ送信します。プロトコルで任意です(MQTTやUDPなど)。今回はHASC Loggerというモーションセンサアプリを用いて、加速度をセンシングし、UDPで送信します。 可視化 可視化部分はリアルタイムチャートの実装に特化したライブラリであるEpoch.jsを使用します。 環境構築 今回はWSL(ubuntu-18.04)上で作業していきます(普段はLinux上で作業することが多いので)。 Python環境はpyenvで構築します。anaconda3-5.3.1をインストールします(pyenvのインストール方法は省略します) pyenv install anaconda3-5.3.1 pyenv global anaconda3-5.3.1 Pythonのバージョンは3.7.0です。 python -V Python 3.7.0 djangoとchannelsはpipでインストールします(django==3.2.4 channels==3.0.3で動作確認しました)。 pip install django==3.2.4 channels==3.0.3 実装 プロジェクトを作成する realtime_chart_projectという名前のプロジェクトを作成します。 django-admin startproject realtime_chart_project 一応、動作確認します cd realtime_chart_project python manage.py runserver ブラウザからhttp://127.0.0.1:8000へアクセスします。 アプリケーションを作成 realtime_chartという名前のアプリを作成します。 python manage.py startapp realtime_chart settings.pyのINSTALLED_APPSに'realtime_chart'を追加します。 realtime_chart_project/realtime_chart_project/settings.py INSTALLED_APPS = [ 'realtime_chart', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] HTTPリクエスト/レスポンスの実装 はじめにURLを設定していきます。DjangoはHTTPリクエストを受け取ると、ルートのURLの設定から参照して views.pyを探し、views.pyを呼び出してリクエストを処理します。 realtime_chart_project/realtime_chart/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.chart, name='chart'), ] realtime_chart_project/realtime_chart_project/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path('realtime_chart/', include('realtime_chart.urls')), path('admin/', admin.site.urls), ] 続いてviews.pyにリクエストを処理するコードを記述します。ここではチャートを表示するchart.htmlを返す関数を定義します。 realtime_chart_project/realtime_chart/views.py from django.shortcuts import render def chart(request): return render(request, 'chart.html', {}) chart.htmlの中身は可視化の部分で説明します。 Websocketの実装(Django Channelsの設定) settings.pyのINSTALLED_APPSに'channels'を追加します。 settings.py INSTALLED_APPS = [ 'channels', 'realtime_chart', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] また、ルートのルーティング設定にChannelsを追加するために、以下をsettings.pyに追記します。 realtime_chart_project/realtime_chart_project/settings.py # Channels ASGI_APPLICATION = 'realtime_chart_project.asgi.application' consumers.pyを実装します。 ここではUDPでセンサからデータを受け取り、Websocketでブラウザにデータを送信する処理が行われています。 realtime_chart_project/realtime_chart/consumers.py from channels.generic.websocket import WebsocketConsumer import json import threading import time import random from socket import socket, AF_INET, SOCK_DGRAM class SensorConsumer(WebsocketConsumer): def connect(self): self.accept() self.start_publish() def disconnect(self, close_code): self.stop_publish() def start_publish(self): self.publishing = True self.t = threading.Thread(target=self.publish) self.t.start() def stop_publish(self): self.publishing = False self.t.join() def publish(self): # UDPの設定 HOST = '' PORT = 4001 s = socket(AF_INET, SOCK_DGRAM) s.bind((HOST, PORT)) while True: # センサーからUDPでデータを受信 msg, address = s.recvfrom(8192) t = int(float(msg.decode('utf-8').split('\t')[0])) x = float(msg.decode('utf-8').split('\t')[-1].split(',')[1]) y = float(msg.decode('utf-8').split('\t')[-1].split(',')[2]) z = float(msg.decode('utf-8').split('\t')[-1].split(',')[3]) # Websocketで送信 if self.publishing == False: break self.send(text_data=json.dumps([ {'time': t,'y': x,}, {'time': t,'y': y,}, {'time': t,'y': z,}, ])) s.close() rounting.pyを設定します。 realtime_chart_project/realtime_chart/rounting.py from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path('ws/', consumers.SensorConsumer.as_asgi()), ] 最後にルートのルーティング設定にrealtime_chart.routingモジュールを追加します。asgi.pyで、AuthMiddlewareStack、URLRouter、realtime_chart.routingをインポートし、ProtocolTypeRouterを以下の形式で"websocket"キーに挿入します。 realtime_chart_project/asgi.py import os from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application import realtime_chart.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'realtime_chart_project.settings') application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AuthMiddlewareStack( URLRouter( realtime_chart.routing.websocket_urlpatterns ) ), }) 可視化の実装 Epoch.jsを使用するのに必要なファイルをダウンロードします。 ダウンロードしたファイルの中のcss/epoch.min.cssとjs/epoch.min.jsをrealtime_chart_project/static/下に置きます。 static ├── css │ └── epoch.min.css └── js └── epoch.min.js D3.jsはCDN(Content Delivery Network)上のものを使用します。 <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> chart.htmlを作成します。 realtime_chart_project/templates/chart.html <html> <head> <title>graph test</title> </head> <body> <h1>Real time chart</h1> <div id="graph" class="epoch" style="height: 200px;"></div> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> {% load static %} <script type="text/javascript" src="{% static 'js/epoch.min.js' %}"></script> <link rel="stylesheet" type="text/css" href="{% static 'css/epoch.min.css' %}"> <script type="text/javascript"> const chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/' ); var data = [ { label: "X-axis", values: [] }, { label: "Y-axis", values: [] }, { label: "Z-axis", values: [] }, ]; var lineChart = $('#graph').epoch({ type: 'time.line', data: data, axes: ['left', 'right', 'bottom'], }); chatSocket.onmessage = function(e) { const current = JSON.parse(e.data); lineChart.push(current); }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; </script> </body> </html> 最後に、templatesとstaticのディレクトリをsettings.pyに追加します。 realtime_chart_project/realtime_chart_project/settings.py TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR, 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] STATICFILES_DIRS = ( [BASE_DIR, 'static'] ) 実行 今回はスマートフォンで観測した3軸加速度データをリアルタイムで可視化するデモを行います。 初めにHASC Loggerをスマートフォンにインストールします。 HASC Loggerから[ホストのIP]:4001へ3軸加速度をUDPで送信します。設定の方法は「角速度から回転行列を求める - 実装編」を参考にしてください。 リアルタイムチャートアプリを起動します。 python manage.py migrate python manage.py runserver ブラウザからhttp://127.0.0.1:8000/realtime_chartへアクセスします。 グラフがリアルタイムで表示されたら成功です。 Django ChannelsとEpoch.jsでセンサデータをリアルタイムに表示するチャート作ってみました。Django Channelsを使用することで、HTTPと同じくらい簡単にWebsocketを実装出来ました。Epoch.jsも学習コストが低く使いやすかったです。 pic.twitter.com/oflpVucoVk— yakiimo121 (@yakiimo121) June 20, 2021 まとめ Django ChannelsとEpoch.jsを用いてリアルタイムチャートを作成しました。Django Channelsを使用することで、HTTPと同じくらい簡単にWebsocketを実装出来ました。Epoch.jsも学習コストが低く使いやすかったです。 ただし、Epoch.jsでも100Hzのモーションセンサデータをリアルタイムで描画するのは困難でした。遅延が発生したり、それにともないキューからデータがこぼれたりします。さすがにWebベースの限界なのでしょうか?それとも他のライブラリを使えば解決できるのでしょうか??もちろん、温度データを可視化する場合などでは、もっとサンプリングレートが低くていいので問題ありません。 今後は機能を拡張していきたいと思います。データの保存機能、MQTTへの対応、デザインをもっとかっこよくするなどですかね。 参考 chart https://qiita.com/shiro-kuma/items/0607e01a19e093fdb631 https://4009.jp/post/archives/20 Websocket https://www.keicode.com/script/html5-websocket-1.php https://pypi.org/project/gevent-websocket/ https://pypi.org/project/django-gevent-websocket/ Django Channels https://docs.djangoproject.com/ja/3.2/intro/tutorial01/ https://channels.readthedocs.io/en/latest/tutorial/index.html https://qiita.com/massa142/items/cbd508efe0c45b618b34#groups https://qiita.com/ekzemplaro/items/a6b81bd1d181fdd0cc24 http://engmng.blog.fc2.com/blog-entry-110.html https://kivantium.hateblo.jp/entry/2020/05/02/151321 https://blog.heroku.com/in_deep_with_django_channels_the_future_of_real_time_apps_in_django Epoch https://qiita.com/okoppe8/items/d8d8bc4e68b1da4a0a36 https://syncer.jp/d3js http://epochjs.github.io/epoch/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonを便利に使おう第二回! 数学編

初めに 問題を解くのに利用するとすれば、何が良いのか。紙には自由自在に文字を書き殴れるという利点があり、パソコンであれば、プログラムにあらゆる処理を肩代わりさせることが出来るという利点がある。それぞれの利点は、お互いの弱点にもなり得るのは、わかりやすい問題点でしょう。 ここで、どちらのほうが良いのか、という問に対して、一言で答えを出すというのは難しいことでしょう。ならば、試してみれば良いのです。 正答率が高いほうが、より優秀な手段と言えるでしょう。人類が先に、先に進むのであれば、優秀な手段でより優秀な手段を子供に学習させていくという必要があるでしょうし、教育の研究等にはきっとこういうことなのでしょう(何も知らないけど)。 とかいう前置きはさておき! 数学の問題をpythonで解いてやろうというお話です。 問題を解く上でのレギュレーションとしては人に答えを聞きに行かないこと、それ以外であれば問題を解くための何をしてもOK!という気分でやってきます。 問題としてこちらを使わせていただきました。 https://web.math-aquarium.jp/reidai-zissuu_1zihutousiki-2.pdf 実数,1 次不等式 人にとって記憶するというのが一番苦手な人も多いでしょう。数学は記憶することが少ないから得意、という人はたまにいます。確かに、数学に記憶することが少ないというのはわかりますが、しかしほんの少しは覚える必要のあることがあります。 数学を使うということは、覚える必要のある物事をほんの少しでも覚えておく必要がありますが、それをプログラムで解決して、記憶しないという効率化を図りたいと思います。 第一問 次の分数を小数で表わせ。 \frac{1}{6}\qquad\frac{1}{8}\qquad\frac{1}{11}\\ これについてはsympyに入れずに、とりあえずpythonで出力させましょう >>> 1/6 0.16666666666666666 >>> 1/8 0.125 >>> 1/11 0.09090909090909091 しかし解答を見てみると、循環小数で表現するようにとなっています。つまりは0.16の6に上にドットを置く書き方が良いでしょう。それがsympyで出来る関数があれば良いのですが…と、アニメ(ひげを剃って女子高生を拾うやつ)を一話見終わるくらいの時間ググってみたんですが、見つかりませんでした。 これでは効率化を出来ません。では、どうしましょう。 ちょっくら循環小数について研究してみましょう。 小数 先に言っておきますが、行き当りばったりでやっています。答えが出ないことがありますので、悪しからず。 んなわけで、小数について調べて…みずに、とりあえずpythonで色々やってみる! for i in range(0,10): print("i="+str(i+1)) print("1/i="+str(1/(i+1))+"\n") >>> %Run a.py i=1 1/i=1.0 i=2 1/i=0.5 i=3 1/i=0.3333333333333333 i=4 1/i=0.25 i=5 1/i=0.2 i=6 1/i=0.16666666666666666 i=7 1/i=0.14285714285714285 i=8 1/i=0.125 i=9 1/i=0.1111111111111111 i=10 1/i=0.1 以上のようなことを1~10だけではなく30まで確認してみました。 有限小数はiが2,4,5,8,10,16,20,25。これはつまり、2と5の倍数のときだけということです。もしくは他の言い方をすれば、2と5以外の素数のある倍数ではない、ということです。 とりあえず、有限小数を除外する目的にも、有限小数を求めるプログラムを作成してみましょう。 for i in range(0,30): value = i+1 while value>0: if value%2==0: value/=2 elif value%5==0: value/=5 else: break if value==1: print("i="+str(i+1)) print("1/i="+str(1/(i+1))) >>> %Run a.py i=1 1/i=1.0 i=2 1/i=0.5 i=4 1/i=0.25 i=5 1/i=0.2 i=8 1/i=0.125 i=10 1/i=0.1 i=16 1/i=0.0625 i=20 1/i=0.05 i=25 1/i=0.04 とりま、作ってみました。適当に作ってたら出来ましたね。求めたいのは循環小数のわけなので、とりあえず、これの逆を表示してみましょう。 for i in range(0,30): value = i+1 while value>0: if value%2==0: value/=2 elif value%5==0: value/=5 else: break if value!=1: #!変更部分  == を != に print("i="+str(i+1)) print("1/i="+str(1/(i+1))) >>> %Run a.py i=3 1/i=0.3333333333333333 i=6 1/i=0.16666666666666666 i=7 1/i=0.14285714285714285 i=9 1/i=0.1111111111111111 i=11 1/i=0.09090909090909091 i=12 1/i=0.08333333333333333 i=13 1/i=0.07692307692307693 i=14 1/i=0.07142857142857142 i=15 1/i=0.06666666666666667 i=17 1/i=0.058823529411764705 i=18 1/i=0.05555555555555555 i=19 1/i=0.05263157894736842 i=21 1/i=0.047619047619047616 i=22 1/i=0.045454545454545456 i=23 1/i=0.043478260869565216 i=24 1/i=0.041666666666666664 i=26 1/i=0.038461538461538464 i=27 1/i=0.037037037037037035 i=28 1/i=0.03571428571428571 i=29 1/i=0.034482758620689655 i=30 1/i=0.03333333333333333 iが3の倍数の時は比較的に分かりやすく循環小数が展開されていますね。 3の時は0.3333333333333333。 6の時は0.16666666666666666。 9の時は,0.1111111111111111。 12の時は0.08333333333333333。 15の時は0.06666666666666667。 18の時は0.05555555555555555。 2の倍数が絡んできたときには、循環を始める地点が変更されていますね。2だけであれば一つ位が下がり、2の二乗であれば二つ位が下がっています。 ただ、18の3の二乗と2の二乗の時は循環は位を一つ下げるだけです。 次に21の時ですが、これが興味深い。 21の時は0.047619047619047616。 24の時は0.041666666666666664。 と、21のときだけ、循環に6個の幅が出来ています。その次の24は単にpythonの割り算の形式的にこのような微妙な最後になっているだけで6で循環しています。すなわち、3×7の21だけが、今までで、中々におかしな挙動をしているのです。 ならば、次は7の倍数について見ていきましょう。 7の時は0.14285714285714285 14の時は0.07142857142857142 7の時の循環の幅は6でこの幅は21のときと同等です。14の循環の始りは位が一つ下がっており、循環の幅は6です。 これよりわかることといえば、おそらくですが、それぞれの素数の循環の性質は、素数同士を掛けると性質を足し合わせたような結果を得られるということでしょうか? 素数には「それぞれの性質が存在するよ」として、これまでのプログラムには有限小数は2と5の倍数だけとしましたが、他の素数でも存在するかもしれません。 その可能性を考えて、プログラムを再始動 for i in range(0,300): value = i+1 while value>0: if value%2==0: value/=2 elif value%5==0: value/=5 else: break if value!=1: print("i="+str(i+1)) print("1/i="+str(1/(i+1))) これで短めの小数点を出すものが存在しないのか、調べてみましたが、残念ながら見当たりませんでした。2と5という数字が特別なのか…だとすれば、なぜ特別なのか…それとも、私が単純に他の素数を見落としているのか。 素数にそれぞれ性質があるという可能性があるのであれば、2と5,3と7以外の素数も探してみるべきなんでしょうけれど、一日という時間は24という数字に縛られているもので、タイムアップです。 最後 数学の根底のようなお話は、きちんと数学を理解しながらではなければ、答えを求めるに至れないのでしょう。 問題解決の効率化するために、循環小数というものをどのように求めるか、ということについて調べようとしましたが、調べた内容すらも理解できないのでは、自分はもっと精進しなければいけないのでしょう。 しかし、この循環小数について、法則性を求める旅は、今回は時間がなかったために短かった上に中途半端で終わってしまいますが、中々に楽しいものでした。 今後に、またこのような問題を振り返ってやってみることにしましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RaspberryPiでリアルタイム顔検出

今回やること RaspberryPiとWebカメラを使ってリアルタイム顔検出 環境 Rasbian(RaspberryPi 4) Python Opencv 処理 カスケード分類器のインストール git clone https://github.com/opencv/opencv コード git cloneしたのと同じフォルダにコードを作成し、実行する必要があります。 (パスを変えればどこでもよい) import tkinter import cv2 import PIL.Image, PIL.ImageTk class App: def __init__(self, window, window_title): self.window = window self.window.title(window_title) self.vcap = cv2.VideoCapture(0) self.width = self.vcap.get(cv2.CAP_PROP_FRAME_WIDTH) self.height = self.vcap.get(cv2.CAP_PROP_FRAME_HEIGHT) self.canvas = tkinter.Canvas(window, width=self.width, height=self.height) self.canvas.pack() self.close_btn = tkinter.Button(window, text="Close") self.close_btn.pack() self.close_btn.configure(command=self.destructor) self.delay = 15 self.update() self.window.mainloop() def update(self): _, frame = self.vcap.read() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) cascade_path = "./opencv/data/haarcascades/haarcascade_frontalface_alt.xml" cascade = cv2.CascadeClassifier(cascade_path)   faces=cascade.detectMultiScale(frame, scaleFactor=1.1, minNeighbors=1, minSize=(10,10)) for x,y,w,h in faces: cv2.rectangle(frame, (x,y), (x+w, y+h), (0, 0, 255), thickness=30) self.photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(frame)) self.canvas.create_image(0, 0, image = self.photo, anchor = tkinter.NW) self.window.after(self.delay, self.update) def destructor(self): self.window.destroy() self.vcap.release() App(tkinter.Tk(), "Tkinter & Camera module") 参考 https://yamitomo.com/article/133 https://www.argocorp.com/UVC_camera/Sample_OpenCV_cascade.detectMultiScale.html 注意書き とりあえずやってみました。 めちゃくちゃ遅いです。 詳細の解説や改良、カメラモジュールでの確認は気が向いたらやっていきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLAlchemyのトランザクション処理

はじめに SQLAlchemy を使って insert 処理を実行した際にトランザクションの扱い方で処理時間に結構な差がでました。(※注) 以下、サンプルコードと自身の環境での検証結果ではありますが知見を共有したく思います。 (※注) 本記事のサンプルコードでは insert 処理のみを扱いますが、update 処理でもトランザクションの扱いによって実行時間に差が出ます。 環境 環境 バージョン 備考 macOS Catalina v10.15.7 Docker Desktop for Mac v3.3.3 Docker v20.10.6, build 370c289 $ docker --version Docker Compose v1.29.1, build c34c88b2 $ docker-compose --version Docker MySQL v8.0.x Dockerfile で指定 Docker Python v3.9.x 同上 SQLAlchemy v1.4.17 requirement.txt で指定 検証 以下の設定でそれぞれ 10,000 行の insert と update 処理で検証。 autocommit ありでループ内で flush() autocommit ありでループ外で flush() autocommit なしでループ外で commit() 結果から insert トランザクション flush() / commit() の場所 結果 備考 1 autocommit あり ループ内で flush() 10,000件 insert するのに約3分 毎行 commit が実行され、下の 2 つに比べて大幅に時間がかかる 2 autocommit あり ループ外で flush() 10,000件 insert するのに約8秒 commit は全行に対して実行され、下記 3 と同じクエリが発行された 3 autocommit なし ループ外で commit() 10,000件 insert するのに約8秒 commit は全行に対して実行され、上記 2 と同じクエリが発行された コードサンプルとログ autocommit ありでループ内で flush() コードサンプル # セッションの設定(autocommit あり) Session = sessionmaker( autocommit=True, class_=MasterSlaveSession ) # ループ内で flush() def insert(): rdb_session = Session() for i in range(10000): work_num = i + 1 work_time = jst_time(time.time()) employee = Employee() employee.name = 'employee' + str(work_num).zfill(6) employee.phone = str(work_num).zfill(6) employee.created_at = work_time employee.updated_at = work_time rdb_session.add(employee) # autocommit ありでここで flush() すると毎行トランザクションが発生するので遅い rdb_session.flush() # autocommit ありの場合は flush(), なしの場合は commit() するとトランザクションは処理対象行全体にかかるので早い # rdb_session.flush() # rdb_session.commit() ログ(実行結果) ## # 10,000 件処理したときの時間 # * autocommit あり # * loop 内で flush # 2021-06-08 15:36:16.736761+09:00 process start process had got 171.57751655578613sec to done. # 10,000件 insert するのに約3分 2021-06-08 15:39:08.314277+09:00 process had finished ## # ログ( これは 10件処理したときのログから2件抜粋 ) # 1件ずつトランザクションがかかっている事がわかる # 2021-06-08 15:46:56,357 INFO sqlalchemy.engine.Engine BEGIN (implicit) ##トランザクション開始 2021-06-08 15:46:56,360 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:46:56,360 INFO sqlalchemy.engine.Engine [generated in 0.00036s] {'name': 'employee000001', 'phone': '000001', 'created_at': datetime.datetime(2021, 6, 8, 15, 46, 56, 341841, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 46, 56, 341841, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:46:56,365 INFO sqlalchemy.engine.Engine COMMIT ##トランザクション終了 2021-06-08 15:46:56,377 INFO sqlalchemy.engine.Engine BEGIN (implicit) ##トランザクション開始 2021-06-08 15:46:56,377 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:46:56,377 INFO sqlalchemy.engine.Engine [cached since 0.01766s ago] {'name': 'employee000002', 'phone': '000002', 'created_at': datetime.datetime(2021, 6, 8, 15, 46, 56, 377034, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 46, 56, 377034, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:46:56,379 INFO sqlalchemy.engine.Engine COMMIT ##トランザクション終了 いちレコード単位でトランザクションが発生されているのがわかる。遅い。 autocommit ありでループ外で flush() コードサンプル # セッションの設定(autocommit あり) Session = sessionmaker( autocommit=True, class_=MasterSlaveSession ) # ループ外で flush() def insert(): rdb_session = Session() for i in range(10000): work_num = i + 1 work_time = jst_time(time.time()) employee = Employee() employee.name = 'employee' + str(work_num).zfill(6) employee.phone = str(work_num).zfill(6) employee.created_at = work_time employee.updated_at = work_time rdb_session.add(employee) # autocommit ありでここで flush() すると毎行トランザクションが発生するので遅い # rdb_session.flush() # autocommit ありの場合は flush(), なしの場合は commit() するとトランザクションは処理対象行全体にかかるので早い rdb_session.flush() # rdb_session.commit() ログ(実行結果) ## # 10,000 件処理したときの時間 # * autocommit あり # * loop 外で flush # % python sqlalchemy_work.py 2021-06-08 15:34:45.603209+09:00 process start process had got 8.878259658813477sec to done. # 10,000件 insert するのに約8秒 2021-06-08 15:34:54.481468+09:00 process had finished ## # ログ( これは 10件処理したときのログ ) # 10件に対してまとめてトランザクションがかかっている事がわかる 2021-06-08 15:49:04,296 INFO sqlalchemy.engine.Engine BEGIN (implicit) ##トランザクション開始 2021-06-08 15:49:04,298 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,298 INFO sqlalchemy.engine.Engine [generated in 0.00040s] {'name': 'employee000001', 'phone': '000001', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 277344, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 277344, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,302 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,302 INFO sqlalchemy.engine.Engine [cached since 0.004541s ago] {'name': 'employee000002', 'phone': '000002', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279356, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279356, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,305 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,306 INFO sqlalchemy.engine.Engine [cached since 0.00756s ago] {'name': 'employee000003', 'phone': '000003', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279418, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279418, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,309 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,309 INFO sqlalchemy.engine.Engine [cached since 0.01123s ago] {'name': 'employee000004', 'phone': '000004', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279465, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279465, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,312 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,312 INFO sqlalchemy.engine.Engine [cached since 0.01392s ago] {'name': 'employee000005', 'phone': '000005', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279509, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279509, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,314 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,314 INFO sqlalchemy.engine.Engine [cached since 0.01604s ago] {'name': 'employee000006', 'phone': '000006', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279603, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279603, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,315 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,316 INFO sqlalchemy.engine.Engine [cached since 0.01768s ago] {'name': 'employee000007', 'phone': '000007', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279643, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279643, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,318 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,318 INFO sqlalchemy.engine.Engine [cached since 0.01985s ago] {'name': 'employee000008', 'phone': '000008', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279680, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279680, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,319 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,319 INFO sqlalchemy.engine.Engine [cached since 0.02132s ago] {'name': 'employee000009', 'phone': '000009', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279720, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279720, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,320 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:49:04,321 INFO sqlalchemy.engine.Engine [cached since 0.02268s ago] {'name': 'employee000010', 'phone': '000010', 'created_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279759, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 49, 4, 279759, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:49:04,323 INFO sqlalchemy.engine.Engine COMMIT ##トランザクション終了 全レコードに対してトランザクションが発生していることがわかる。早い。 autocommit なしでループ外で commit() コードサンプル # セッションの設定(autocommit なし) Session = sessionmaker( # autocommit=True, class_=MasterSlaveSession ) # ループ外で commit() def insert(): rdb_session = Session() for i in range(10000): work_num = i + 1 work_time = jst_time(time.time()) employee = Employee() employee.name = 'employee' + str(work_num).zfill(6) employee.phone = str(work_num).zfill(6) employee.created_at = work_time employee.updated_at = work_time rdb_session.add(employee) # autocommit ありでここで flush() すると毎行トランザクションが発生するので遅い # rdb_session.flush() # autocommit ありの場合は flush(), なしの場合は commit() するとトランザクションは処理対象行全体にかかるので早い # rdb_session.flush() rdb_session.commit() ログ(実行結果) ## # 10,000 件処理したときの時間 # * autocommit なし # * loop 外で commit # % python sqlalchemy_work.py 2021-06-08 15:31:14.633875+09:00 process start process had got 8.680613994598389sec to done. # 10,000件 insert するのに約8秒 2021-06-08 15:31:23.314489+09:00 process had finished ## # ログ( これは 10件処理したときのログ ) # 10件に対してまとめてトランザクションがかかっている事がわかる 2021-06-08 15:52:46,799 INFO sqlalchemy.engine.Engine BEGIN (implicit) ##トランザクション開始 2021-06-08 15:52:46,801 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,801 INFO sqlalchemy.engine.Engine [generated in 0.00026s] {'name': 'employee000001', 'phone': '000001', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 780246, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 780246, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,804 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,804 INFO sqlalchemy.engine.Engine [cached since 0.003321s ago] {'name': 'employee000002', 'phone': '000002', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782529, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782529, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,806 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,806 INFO sqlalchemy.engine.Engine [cached since 0.00499s ago] {'name': 'employee000003', 'phone': '000003', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782593, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782593, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,807 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,807 INFO sqlalchemy.engine.Engine [cached since 0.006349s ago] {'name': 'employee000004', 'phone': '000004', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782633, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782633, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,809 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,809 INFO sqlalchemy.engine.Engine [cached since 0.008276s ago] {'name': 'employee000005', 'phone': '000005', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782673, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782673, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,812 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,813 INFO sqlalchemy.engine.Engine [cached since 0.01162s ago] {'name': 'employee000006', 'phone': '000006', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782711, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782711, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,814 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,814 INFO sqlalchemy.engine.Engine [cached since 0.01341s ago] {'name': 'employee000007', 'phone': '000007', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782786, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782786, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,816 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,816 INFO sqlalchemy.engine.Engine [cached since 0.01495s ago] {'name': 'employee000008', 'phone': '000008', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782848, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782848, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,817 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,818 INFO sqlalchemy.engine.Engine [cached since 0.01665s ago] {'name': 'employee000009', 'phone': '000009', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782887, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782887, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,819 INFO sqlalchemy.engine.Engine INSERT INTO employee (name, phone, created_at, updated_at) VALUES (%(name)s, %(phone)s, %(created_at)s, %(updated_at)s) 2021-06-08 15:52:46,820 INFO sqlalchemy.engine.Engine [cached since 0.01851s ago] {'name': 'employee000010', 'phone': '000010', 'created_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782928, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST')), 'updated_at': datetime.datetime(2021, 6, 8, 15, 52, 46, 782928, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))} 2021-06-08 15:52:46,823 INFO sqlalchemy.engine.Engine COMMIT ##トランザクション終了 こちらも同じ。 全レコードに対してトランザクションが発生していることがわかる。早い。 補足 autocommit モードは非推奨 -> 廃止に autocommit モードは v1.4.x で非推奨、v2.0.x からは廃止になるそうです。 参考 https://docs.sqlalchemy.org/en/13/orm/session_transaction.html#autocommit-mode 転載 Deprecated since version 1.4: “autocommit” mode is a legacy mode of use and should not be considered for new projects. The feature will be deprecated in SQLAlchemy 1.4 and removed in version 2.0; both versions provide a more refined “autobegin” approach that allows the Session.begin() method to be used normally. If autocommit mode is used, it is strongly advised that the application at least ensure that transaction scope is made present via the Session.begin() method, rather than using the session in pure autocommit mode. バルク処理 今回はトランザクションによって処理時間の短縮に努めましたが、 bulk_inserts bulk_updates といった並列処理もあります。 これらの学習を進め、今後機会があればはそちらも積極的に使っていきたいと思います。 参考 公式ドキュメント-Transactions and Connection Management SQLAlchemyを使ってPythonでORM ― SQLAlchemy ORMを知る ソースコード 本記事であげたコードは こちら にあげております。 ご興味あればご覧ください。 ( 上記には冒頭で触れた update 処理のサンプルコードもあります )
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonからInstagramの投稿を行う

注意 悪用厳禁です。instagramの利用規約に従って利用してください。 本記事記載のコード、手法を使って損害を被っても、自己責任でお願いします。 本記事は、seleniumの可能性を紹介し、seleniumの機能を学習することが目的ですので、半自動で実行するにとどめましょう。 コード import time from selenium import webdriver import chromedriver_binary import pywinauto def upload_file(file_path): pass # https://syunkan81.hatenablog.com/entry/2020/01/28/091019 # に記載の関数 # def upload_file(file_path): # の「# pywinautoによる制御」以降の行をコピペした関数です。 # Qiita外の記事なので、著作権的にコピペしたものを載せるのはどうかと思い、未記載にしました。 # 動かすときは、自身でブログからコピペしてきてください。 def upload_instagram(image_path, post_text, user_id, password): url = "https://www.instagram.com/accounts/login/?source=auth_switcher" mobile_emulation = { "deviceName": "Galaxy S5" } options = webdriver.ChromeOptions() options.add_experimental_option("mobileEmulation", mobile_emulation) driver = webdriver.Chrome(options=options) driver.delete_all_cookies() # 念のためクッキーを消す driver.get(url) #メアド(もしくは電話番号)とパスワードを入力 time.sleep(4) # 画面取得まで待つ driver.find_element_by_name('username').send_keys(user_id) time.sleep(1) driver.find_element_by_name('password').send_keys(password) time.sleep(1) driver.find_element_by_xpath('//*[@id="loginForm"]/div[1]/div[6]/button/div').click() # ログインボタンを押す time.sleep(5) # ログイン完了まで待つ driver.find_element_by_xpath('//*[@id="react-root"]/section/main/div/div/div').click() # ログイン情報を保存しますか?→後でを押す time.sleep(5) # 画面切り替わりまで待つ driver.find_element_by_xpath("/html/body/div[4]/div/div/div/div[3]/button[2]").click() # 後でを押す time.sleep(5) # ログイン完了まで待つ driver.find_element_by_xpath('//*[@id="react-root"]/section/nav[2]/div/div/div[2]/div/div/div[3]').click() time.sleep(1) # ここでwindowsのアップロードの画面がでてくる upload_file(image_path) time.sleep(2) # 画面切り替わりを待つ driver.find_element_by_xpath('//*[@id="react-root"]/section/div[1]/header/div/div[2]/button').click() time.sleep(2) # 画面切り替わりを待つ if post_text is not None: driver.find_element_by_xpath('//*[@id="react-root"]/section/div[2]/section[1]/div[1]/textarea').send_keys(post_text) driver.find_element_by_xpath('//*[@id="react-root"]/section/div[1]/header/div/div[2]/button').click() time.sleep(20) # 投稿されるのを待つ driver.close() 以下のように使用します。 # 使用例 upload_instagram(image_path="cafe.png", post_text="今日カフェで美味しいコーヒー飲んだよ~", user_id="your_mail_address", password="password"): # post_textがないときはNoneを指摘してください。 仕様上の注意点 (念のためもう一度)悪用厳禁です。instagramの利用規約に従って利用してください。 (念のためもう一度)本記事記載のコード、手法を使って損害を被っても、自己責任でお願いします。 Windows環境でないと動かないと思います。 おそらくこのコードを使って大量アカウントを使って自動化をしよう、とする方がいるがもしれませんが、IPアドレスなどでアカウントバンされる可能性がありますのでおすすめしません。 私自身、本コードを使って長期運用をしたことはないので、長期運用時に想定外のエラーが生じる可能性があります(instagramの画面の仕様の変更、など)。 関数upload_file(file_path):については自身で作成してください seleniumのインストール時に、seleniumのバージョンを合わせるとかなんか面倒な作業が必要だった覚えがあるので、頑張ってググって調べましょう。 seleniumのインストール時に、seleniumのバージョンを合わせるとかなんか面倒な作業が必要だった覚えがあるので、頑張ってググって調べましょう。 time.sleep()の括弧内の数字は、画面切り替わり時に待つ時間です。自身の通信環境などにより、適宜変更してください。(一般に大きくした方が安全ですが、関数実行時間が長くなります。) なんか肝心のコードの解説を書く気力がなくて申し訳ないです。自身で読解してください...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonでエラーが出たときに再起動させる

はじめに プログラムを組むにあたり、バグを出さないよう論理的にコーディングするのは当然のことだ。 とはいえ、バグがないことを断言することはできない。人間だもの。 いろいろなエラーを起こすプログラム こんなプログラムを書いてみた。 実行するたびにさまざまなステップでエラーで停止したり最後まで完走したりするという内容だ。 make_error.py import random import math import sys print ("="*20) """ 後でここに追記する """ p = random.randint(-3, 3) print (f"{p}です") if p == 3: print (p + "の倍数だとアホになります") elif p == -3: print (p , "だと中断します") exit() elif p == -2: print (p , "だとKeyboardInterruptが発生します") raise KeyboardInterrupt print ("平方根:") print (f" sqrt({p}) = {math.sqrt(p)}") print ("逆数:") print (f" 1/{p} = {1/p}") subprocess プログラムから他プログラムを呼び出し、またその結果(標準出力)を拾うことができる。それがsubprocessだ。MS-DOSの時代を体験していない私には標準出力やパイプは少々難解だった、がんばって勉強した。 以下の記事がたいへん役に立った。 subprocessの使い方(Python3.6) subprocessについてより深く(3系,更新版) 上のmake_error.pyに以下のコードを追記しよう。追記するとこれ単品では動かなくなってしまう。それを避けるのは容易だが本題から外れるので省略している。で、できないわけじゃないからね。勘違いしないでよね。 make_error.pyに追加 args = sys.argv e = sys.argv[1] if e != "": print (f"前回のエラー:{e}") else: print ("前回は正常に終了しました\n") そして実行するのはmake_error.pyではなく次のsubprocess_watch.pyだ。 subprocess_watch.py import time import subprocess as sp def run_subprocess(e): proc = sp.Popen(["python", "make_error.py", e], stdout = sp.PIPE, stderr = sp.PIPE) return proc proc = run_subprocess("") while True: ecode = proc.poll() if ecode is None: # サブプロセスが実行中 time.sleep(2) else: # サブプロセス終了 for line in proc.stdout.readlines(): print (line.decode(encoding="cp932"), end="") # utf-8だと全角文字でエラーになる(Windows) if ecode == 0: # 正常終了 e = "" else: # 異常終了 e = proc.stderr.readlines()[-1].decode() proc = run_subprocess(e) これによりmake_error.pyがエラーで落ちても再起動し、かつ前回のエラーメッセージを拾うことができる。 もちろん大元のsubprocess_watch.pyが落ちてしまってはダメだけど。 結果(例) ==================== 前回は正常に終了しました 2です 平方根:  sqrt(2) = 1.4142135623730951 逆数:  1/2 = 0.5 ==================== 前回は正常に終了しました -1です 平方根: ==================== 前回のエラー:ValueError: math domain error -3です -3 だと中断します ==================== 前回は正常に終了しました -2です -2 だとKeyboardInterruptが発生します ==================== 前回のエラー:KeyboardInterrupt 1です 平方根:  sqrt(1) = 1.0 逆数:  1/1 = 1.0 まともな使い方 以上のことを家族見守りサービスを自作するの開発中に習得した。 それにより不慮のエラーが発生しても再起動することができるようになった。 どのようなエラーが発生したかわかるようになったのならば分析して修正しろって? ごもっともです。 終わりに 今回はPythonプログラムの再起動に使っただけだが、用途はそれだけに限らない。Pythonでできないことを外に出し、その結果をもらってPythonで処理するという利用法もある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでInstagramの投稿を行う

はじめに instagramは、APIを使って記事の自動投稿ができますが、APIの審査は何かとハードルが高いです。 なら、seleniumを使えばいいじゃん、というわけで実装してみました。 注意 悪用厳禁です。instagramの利用規約に従って利用してください。 本記事記載のコード、手法を使って損害を被っても、自己責任でお願いします。 本記事は、seleniumの可能性を紹介し、seleniumの機能を学習することが目的ですので、半自動で実行するにとどめましょう。 コード import time from selenium import webdriver import chromedriver_binary import pywinauto def upload_file(file_path): pass # https://syunkan81.hatenablog.com/entry/2020/01/28/091019 # に記載の関数 # def upload_file(file_path): # の「# pywinautoによる制御」以降の行をコピペした関数です。 # Qiita外の記事なので、著作権的にコピペしたものを載せるのはどうかと思い、未記載にしました。 # 動かすときは、自身でブログからコピペしてきてください。 def upload_instagram(image_path, post_text, user_id, password): url = "https://www.instagram.com/accounts/login/?source=auth_switcher" mobile_emulation = { "deviceName": "Galaxy S5" } options = webdriver.ChromeOptions() options.add_experimental_option("mobileEmulation", mobile_emulation) driver = webdriver.Chrome(options=options) driver.delete_all_cookies() # 念のためクッキーを消す driver.get(url) #メアド(もしくは電話番号)とパスワードを入力 time.sleep(4) # 画面取得まで待つ driver.find_element_by_name('username').send_keys(user_id) time.sleep(1) driver.find_element_by_name('password').send_keys(password) time.sleep(1) driver.find_element_by_xpath('//*[@id="loginForm"]/div[1]/div[6]/button/div').click() # ログインボタンを押す time.sleep(5) # ログイン完了まで待つ driver.find_element_by_xpath('//*[@id="react-root"]/section/main/div/div/div').click() # ログイン情報を保存しますか?→後でを押す time.sleep(5) # 画面切り替わりまで待つ driver.find_element_by_xpath("/html/body/div[4]/div/div/div/div[3]/button[2]").click() # 後でを押す time.sleep(5) # ログイン完了まで待つ driver.find_element_by_xpath('//*[@id="react-root"]/section/nav[2]/div/div/div[2]/div/div/div[3]').click() time.sleep(1) # ここでwindowsのアップロードの画面がでてくる upload_file(image_path) time.sleep(2) # 画面切り替わりを待つ driver.find_element_by_xpath('//*[@id="react-root"]/section/div[1]/header/div/div[2]/button').click() time.sleep(2) # 画面切り替わりを待つ if post_text is not None: driver.find_element_by_xpath('//*[@id="react-root"]/section/div[2]/section[1]/div[1]/textarea').send_keys(post_text) driver.find_element_by_xpath('//*[@id="react-root"]/section/div[1]/header/div/div[2]/button').click() time.sleep(20) # 投稿されるのを待つ driver.close() 以下のように使用します。 # 使用例 upload_instagram(image_path="cafe.jpg", post_text="今日カフェで美味しいコーヒー飲んだよ~", user_id="your_mail_address", password="password"): # post_textがないときはNoneを指摘してください。 使用上の注意 (念のためもう一度)悪用厳禁です。instagramの利用規約に従って利用してください。 (念のためもう一度)本記事記載のコード、手法を使って損害を被っても、自己責任でお願いします。 Windows環境でないと動かないと思います。 おそらくこのコードを使って大量アカウントを使って自動化をしよう、とする方がいるがもしれませんが、IPアドレスなどでアカウントバンされる可能性がありますのでおすすめしません。 私自身、本コードを使って長期運用をしたことはないので、長期運用時に想定外のエラーが生じる可能性があります(instagramの画面の仕様の変更、など)。 関数upload_file(file_path):については自身で作成してください seleniumのインストール時に、seleniumのバージョンを合わせるとかなんか面倒な作業が必要だった覚えがあるので、頑張ってググって調べましょう。 seleniumのインストール時に、seleniumのバージョンを合わせるとかなんか面倒な作業が必要だった覚えがあるので、頑張ってググって調べましょう。 time.sleep()の括弧内の数字は、画面切り替わり時に待つ時間です。自身の通信環境などにより、適宜変更してください。(一般に大きくした方が安全ですが、関数実行時間が長くなります。) なんか肝心のコードの解説を書く気力がなくて申し訳ないです。自身で読解してください...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python画像処理のためのGUI入門(PySimpleGUI解説)

はじめに PySimpleGUI を使用して作成したアプリの紹介記事を作っており、本記事はその詳細解説として作成しました。 とはいえ広く入門用の記事として書いたつもりなので、 Python で GUI をやってみたいと思った方の参考になればと思います。 それはさておき Pythonを使って画像処理を行う際、パラメータを動的に変更しながら処理結果を確認したいということは多いかと思います(ですよね?)。こういった時にGUIベースのアプリケーションが作れると非常に便利ですが、PySimpleGUI でそれを簡単に実現することができます。 本記事では PySimpleGUI の基本的動作を理解しながら、インタラクティブな画像処理アプリケーションの作成方法を習得することを目的とします。 PySimpleGUI に関する基本的な説明から入るので、ある程度使い方が分かっている方は実践編から入って、不明点は都度戻って補完するのが良いかと思います。 よく見そうなところ 項目 概要 基本レイアウト コピペすればとりあえず動く雛形 要素一覧 画面を構成する要素の一覧 レイアウトの引数 レイアウト調整に使う引数について メソッド一覧 要素に適用できるメソッドを見る bind_string 一覧 特殊な操作を受け付けたいときに Graph 用メソッド一覧 Graphで描画処理をする 実践編 画像ビューワー作成の実践 完成物のイメージ 例として、選んだフォルダにある画像を一覧で見られる画像ビューワーを作成します。多機能ではありませんが、そのぶん好きなようにカスタマイズして使えるかと思います。 導入 pip によるインストールが可能です。ターミナルから下記コマンドを入力します。 pip install pysimplegui インストールが完了したら、import PySimpleGUI で使用できるようになります。長いのでだいたい sg と略されることが多いです。 import PySimpleGUI as sg 基本的な処理の流れ 最低限の構成は以下のようになります。 import PySimpleGUI as sg # 1. レイアウト layout = [ [ sg.Button('押してね', size=(30, 3), key='BUTTON'), ], ] # 2. ウィンドウの生成 window = sg.Window( title='Window title', layout=layout ) window.finalize() # 3. GUI処理 while True: event, values = window.read(timeout=None) if event is None: break window.close() 処理は大きく レイアウト ウィンドウの生成 GUI処理 の 3 つに分かれます。それぞれに分けて解説します。 レイアウト layout = [ [ sg.Button('押してね', size=(60, 5), key='BUTTON'), ], ] 基本的にレイアウトは2次元のリストで表します。 0次元目は行を表すので要素が縦に並び、 layout = [ [ sg.Button('押してね', size=(30, 3), key='BUTTON_1'), ], [ sg.Button('押してね', size=(30, 3), key='BUTTON_2'), ], ] 1次元目は列となるので要素は横に並びます。 layout = [ [ sg.Button('押してね', size=(30, 3), key='BUTTON_1'), sg.Button('押してね', size=(30, 3), key='BUTTON_2'), ], ] ウィンドウの生成 window = sg.Window( title='Window title', layout=layout ) window.finalize() 変数 window にウィンドウを格納し、window.finalize() でウィンドウを確定、表示させます。window.finalize() は必須ではありませんが、個人的には入れることを推奨します。 GUI処理 while True: event, values = window.read(timeout=None) if event is None: break window.close() ここがメインの処理になります。window.read() を呼び出すとウィンドウの内容を更新し、ユーザーのアクション(ボタンを押す、スライダーを動かす、など)を待ちます。ユーザーがアクションを起こしたら、起こしたアクションの内容と、ウィンドウ内の各要素が持つ値をそれぞれ event, values に格納します。 試しにこれらの値を print でチェックしてみましょう。該当する部分を以下のように変更し、実行してみます。 while True: event, values = window.read(timeout=None) # 変更部分 print('Event: ', event) print('Values: ', values) # 変更部分終わり if event is None: break window.close() すると、起こしたアクションによって以下のような出力が得られます。 # 「押してね」ボタン押下 Event: BUTTON Values: {} # ×ボタンで閉じる Event: None Values: None 「押してね」ボタンを押すと event に 'BUTTON' が格納されていることが分かりますが、これはボタンをレイアウトする際に key='BUTTON' を指定しているためです。つまり、event にはユーザーがアクションを起こした対象の key が格納されるということになります。レイアウト時の key= は省略可能ですが、ボタンやスライダーなど、ユーザーが操作することを前提とした要素について省略することは推奨しません。 もう少し詳しく key= を省略するとレイアウトされた順番に 0, 1, 2, 3 と整数が格納されていきます。ここで、特定の要素への操作を if event == 0: のような処理でキャッチしようとした場合、レイアウトを変更してしまうと全く別の要素が反応してしまいます。Button, Text については例外的に「中身のテキスト」が key として格納されますが、これまた後でボタンのテキストを変更したり、同じテキストのボタンを複数配置しようとしたときに泣きを見るのでおすすめしません。 ×ボタンには key が存在せず、None が格納されます。これを利用して×ボタンの押下をキャッチし、 While 文の外に出ます。 一方で values には辞書が格納されていますが、空の辞書しか返してくれません。これは Button 要素が保持している値がないためです。試しにテキストボックスである Input 要素をレイアウトに追加してみましょう。 変数 layout を以下のように変更します。 layout = [ [ sg.Button('押してね', key='BUTTON'), ], [ sg.Input(key='INPUT1'), ], [ sg.Input(key='INPUT2'), ], [ sg.Input(key='INPUT3'), ], ] 今度はボタンを押すと values に値が格納されました。テキストボックスに文字を入力してボタンを押すと、それらが対応する key の値に反映されていることが分かります。 # 「押してね」ボタン押下 Event: BUTTON Values: {'INPUT1': '', 'INPUT2': '', 'INPUT3': ''} # 文字を入力してもう一度ボタン押下 Event: BUTTON Values: {'INPUT1': '1 行目だよ', 'INPUT2': '2 行目だよ', 'INPUT3': '3 行目だよ'} # ×ボタンで閉じる Event: None Values: None このように値を保持する要素がある場合は、values に各要素の key とその値の組み合わせが辞書として格納されます。特定の要素の値を取り出したい場合は values['INPUT1'] のように書きます。 ここで要注意なのが、×ボタンを押した場合は values に None が格納されるという点です。以下のコードを例にとります。 while True: event, values = window.read(timeout=None) # 変更部分 print('INPUT1: ', values['INPUT1']) # 変更部分終わり if event is None: break window.close() この場合は、×ボタンでウィンドウを閉じた際に TypeError: 'NoneType' object is not subscriptable と怒られます。×ボタンを押すと values に None が格納されるので、None['INPUT1'] なんて無理ですよということですね。 要素 前章では Button 要素と Input のみ扱いましたが、PySimpleGUIには他にもさまざまな特徴を持った要素があります。ここでは個人的に利用頻度の高い要素を解説します。1から10まで通しで覚えるよりは、とりあえずざっと見て、ほしい所だけ都度見直す感じがいいかもしれません。というわけでザクッと表にまとめました。 補足: イベントの有効化 ここに挙げた要素のうち Button, Menu を除くすべての要素は、初期状態でイベントの発生を無効化されています。これを有効にしたい場合は、レイアウトする際に引数 enable_events=True を指定します。 要素一覧 要素名 概要 event values Button 押しボタン ボタンクリック - Text ラベル用の固定テキスト テキストクリック - Input 入力可能なテキストボックス キー入力 テキスト内容 Multiline 入力可能なテキストボックス(改行あり) キー入力 テキスト内容 Slider つまみで値を変更できるスライダー つまみ位置変更 現在値 Spin ▲▼ボタンによって値の増減が可能なテキストボックス ▲▼ボタン or ボックス内クリック 現在値 Combo 選択肢から内容を選べるテキストボックス 選択肢の選択 現在値 Checkbox クリックで ON / OFF を切り替えるチェックボックス クリック チェック有無 Radio クリックで複数の選択肢から 1 つを選ぶラジオボタン クリック チェック有無 Menu ウィンドウ上部のメニューバー 項目選択 - Table 行の選択が可能な表(列は選べない) 要素選択 選択要素のリスト Graph 図形の描画や画像の貼り付けができるエリア グラフ範囲内クリック 最終クリック座標 Column 複数の要素を 1 つの要素にまとめる入れ物 - - Button sg.Button('Button text', key='BUTTON') 押すとイベントを発生させるボタンです。 一覧に戻る Text sg.Text('Text') 文字列を表示します。基本的にはラベル用の固定テキストとして使うことが大半なので、key を設定する必要はありません。Text から values を取得しようとして KeyError で怒られるのは誰もが通る道。 一覧に戻る Input sg.Input('Default_text', disabled=False, key='INPUT') 入力可能なテキストボックスです。disabled=True とすることでユーザーの手入力による変更を禁止することができ、表示のみ行いたい場合に有効です。 一覧に戻る Multiline sg.Multiline('Multi \ntext', disabled=False, key='MULTILINE') Inputを複数行対応にしたものです。print メソッドにより通常の print 関数によるコンソール出力のような使い方ができます(後述)。こちらも Input 同様に disabled=True とすることで手入力を禁止できます。 一覧に戻る Slider # 範囲(0, 10), 初期値0, 1刻み、縦方向のスライダー sg.Slider((0, 10), 0, 1, orientation='v', disable_number_display=False, key='SLIDER') つまみを動かすことで値を変更可能なスライダーです。下(左)端の数値、上(右)端の数値をタプルで指定します。加えて初期値と取りうる数値の刻みを指定します。0.5 刻みや 0.1 刻みなど整数以外も指定可能です。スライダーの方向を orientation= で指定でき、'v' が上下方向、h が左右方向です。デフォルトはつまみの隣に現在値が表示されますが、disable_number_display=True を指定すると非表示になります。 一覧に戻る Spin # 松竹梅から選択する。初期値'梅' sg.Spin(['松', '竹', '梅'], '梅', readonly=False, key='SPIN_1') # range()の返り値をそのまま渡すとバグるのでリストに変換しておく sg.Spin(list(range(100)), 0, readonly=False, key='SPIN_2') テキストボックスですが、▲▼ボタンを押すことで、値の変更が可能です。値の候補はリストかタプルで指定し、中身は文字列、数値以外でも問題ありません。range() 関数を使う場合は必ずリストかタプルに変換しておきましょう。readonly=True とすることで▲▼ボタン以外での値の変更を禁止することができます。 一覧に戻る Combo # 松竹梅から選択する。初期値'梅' sg.Combo(['松', '竹', '梅'], '梅', readonly=False, key='COMBO') テキストボックスですが、▼ボタンを押すことで事前に設定した選択肢から値を選ぶことができます。選択肢はリストかタプルで指定します。readonly=True とすることで▼ボタン以外での値の変更を禁止することができます。 一覧に戻る Checkbox sg.Checkbox('Checkbox', False, key='CHECKBOX') クリックでチェックを ON / OFF できるチェックボックスです。初期状態のチェック有無を True or False で指定します。 一覧に戻る Radio sg.Radio('松', 'group_1', True, key='RADIO_MATSU'), sg.Radio('竹', 'group_1', False, key='RADIO_TAKE'), sg.Radio('梅', 'group_1', False, key='RADIO_UME'), 複数のボタンから ON にするものを選択するラジオボタンです。Checkbox と似ていますが、大きく異なるのは複数のボタンがグループ化されることです。上の例では 3 つのボタンを group_1 に登録しており、どれか 1 つが ON になれば残り 2 つは OFF になります。Checkbox と同様に初期状態のチェック有無を True or False で指定します。ただしこのとき同じグループの 2 つ以上のボタンに True を指定しないよう注意してください。 一覧に戻る Menu sg.Menu( [ [ 'ファイル(&F)', [ '新規作成 (&N)::MENU_NEW::', '開く (&O)::MENU_OPEN::', '保存 (&S)::MENU_SAVE::', '名前を付けて保存 (&A)::MENU_SAVEAS::', '終了 (&X)::MENU_EXIT::', ], ], [ '編集(&E)', [ '元に戻す (&Z)::MENU_UNDO::', 'やり直し (&Y)::MENU_REDO::', '変形 (&F)::', [ '反転 (&F)::MENU_FLIP::', '回転 (&R)::MENU_ROTATE::', ], ], ], [ 'ヘルプ(&H)', [ 'ユーザーマニュアル (&M)::MENU_MANUAL::', 'バージョン情報 (&A)::MENU_VERSION::', ], ], ], ), よくあるウィンドウ上部のメニューバーです。これがあるだけでグッとそれらしくなりますし、画面がすっきりするのでおすすめです。 レイアウトが多少ややこしいですが、 ['メニュータイトル', ['項目1', '項目2', ...]] をリストでまとめたものと理解すれば、そこまで難しくはありません。入れ子構造にすることで、メニューの項目からさらに選択肢を展開することも可能です(変形(F)の欄を参照)。 追加機能として '&'+半角英数 によりキーボードでのアクセスが有効になります。これを項目名に組み込むことで、Alt + 該当キーでその項目が選択されるようになります。 この要素でイベントを発生させたときの特徴として、event に key= の値ではなく項目の名前が格納されます。したがって key= の指定は必要ないのですが、後から項目名を変更した際にバグが発生してしまいます。 回避策として、項目名の末尾に ::[key代わりの文字列] を挿入する方法があります。上の例の Menu 要素を表示してみると、:: より後ろの文字はメニューに表示されていないかと思います。一方で項目を選択した際、 event には :: 以降も含めた文字列が格納されています。つまり、 if '::MENU_NEW::' in event: のような式があれば、表示されている内容によらず末尾に ::MENU_NEW:: をもつ項目に対応することができます1。末尾にも :: をつけているのは、key の終わりを判定するためで、これを書かずに if '::MENU_SAVE' in event: と書けば '名前を付けて保存 (&A)::MENU_SAVEAS' にも反応してしまいます(::MENU_SAVEASなので)。 一覧に戻る Table sg.Table( [[]], # 表の中身 ['Col 1', 'Col 2', 'Col 3'], # ヘッダー名 col_widths=[5, 5, 5], # 列幅(列ごとに個別で指定) auto_size_columns=False, # col_widthを指定するなら必ずFalseにすること! select_mode=None, # 行の選択方法 num_rows=None, # 表示する行数(はみ出た分はスクロールバーで表示できる) key='TABLE' ) リストを表として表示することができます。後述の update メソッドを使ってリストの中身を確認するビューワーとして利用されることが多いです。他の要素と比べると複雑で覚えることが多いですが、非常に強力な要素で多くの場面で活用することができます。ここでつまづきがちなポイントを 2 つ紹介します。 中身は必ず 2 次元リストで指定する 一列だけの表であっても必ず 2 次元リストを指定する必要があります。1 次元リストを与えた場合は displaycolumns = element.ColumnHeadings if element.ColumnHeadings is not None else element.Values[0] IndexError: list index out of range res = tk.call(*(args + options)) _tkinter.TclError: Invalid column index T などと怒られることになります。 表の中身を空リスト [[]] にしたら必ず auto_size_columns=False とする これをしないと width = max(column_widths[i], len(heading)) * _char_width_in_pixels(font) KeyError: 0 といって怒られます。そもそも列幅の自動調整自体あまり使い勝手が良くないので、col_width= を自分で指定して auto_size_columns=False を基本とするのがいいと思います。ちなみに auto_size_columns=False を指定しないと col_width= は全く効きません。なんだそれ……。 それはさておき: 機能の話に戻ります 行はクリックすることで選択可能で、インデックスは values に格納されます。選択モードは select_mode= で指定可能で、モードに応じた操作が可能です。 select_mode= 機能 sg.TABLE_SELECT_MODE_NONE 行を選択できない sg.TABLE_SELECT_MODE_BROWSE 1 行のみ選択可能 sg.TABLE_SELECT_MODE_EXTENDED 複数行を選択可能 ここで選択行の取得をしようとしてやりがちなのが、 if values['TABLE'] == 0: print('ROW 0 SELECTED!') という書き方です。一見正しいように見えますが、Table 要素の values は選択した行のリストで返されるので、0 行目が選択されていたとしても [0] == 0 を評価することとなり、False になります2。 では、以下のように書けばいいのでしょうか。 if values['TABLE'][0] == 0: print('ROW 0 SELECTED!') これも不十分です。このままでは行が選択されていない状態で別のイベントが発生すると、 IndexError: list index out of range と怒られます。選択されている行がない場合は values に空のリストが格納されるため、インデックス 0 番を呼び出すことはできません。 これらをふまえ、下記のように書くとよいです。 if values['TABLE'] and values['TABLE'][0] == 0: print('ROW 0 SELECTED!') こうすれば空のリストであっても if values['TABLE'] の時点で False が確定して3 and 以降がスキップされるため、エラーを回避できます。 一覧に戻る Graph # 左下(0, 0), 右上(100, 100) sg.Graph((100, 100), (0, 0), (100, 100), key='GRAPH_1') # 左上(0, 0), 右下(100, 100) sg.Graph((100, 100), (0, 100), (100, 0), key='GRAPH_2') 図形の描画や画像の貼り付けができるエリアです。Graphと言いながら目盛りや軸の表示もないので、どちらかというと Canvas と言った方がしっくりきます。要素のサイズ (x, y), 左下座標 (x, y), 右上座標 (x, y) を順番に指定します。この要素も画像処理を行う上で要となりますので、ぜひ覚えてください。 描画には draw_line() や draw_image() などといったメソッドを使用します。詳しくは後の章で解説します。 一覧に戻る Column sg.Column( [ [ sg.Button('ボタン', size=(10, 2)) ], [ sg.Button('ボタン', size=(10, 2)) ], ] ), 複数の要素を一つの要素としてレイアウトできるようになります。レイアウトを入れ子構造にすることで、より複雑なレイアウトが可能になります。Table や Graph のような縦に長い要素を配置しながらサイドバー的なものを作るなど、画面を「縦に割りたい」ときに効果を発揮します。 一覧に戻る デザインの調整: サイズと配置、色 要素のサイズは中に入れるテキストの長さなどにより自動で調整されますが、もう少し見栄えを整えたいなどはありますよね。PySimpleGUIではレイアウト時に追加のパラメータを設定することでウィンドウの配置を調整することができます。 共通のパラメータ 色 text_color 文字色をカラーコード( '#FF0000' など)もしくは色名( 'red' など)で指定します。 background_color 背景色をカラーコード( '#FF0000' など)もしくは色名( 'red' など)で指定します。 サイズ・位置 size (幅、高さ) のタプルで指定します。単位は px ではなく半角文字数です。 pad 上下左右の余白を指定します。(左右, 上下) で指定し、左右, 上下 はそれぞれ (左, 右), (上, 下) に置き換えることで分けて指定ができます。単位は px です。 その他 font フォントを (フォント名, サイズ) のタプルで指定します。フォント名は 'メイリオ' など日本語名でOK、フォントサイズの単位は pt です(多分)。サイズのみ変更したい場合は、(None, サイズ) とします。 パラメータ早見表 要素名 text_color background_color size pad font Button ○ ○ ○ Text ○ ○ ○ ○ ○ Input ○ ○ ○ ○ ○ Multiline ○ ○ ○ ○ ○ Slider ○ ○ ○ ○ ○ Spin ○ ○ ○ ○ ○ Combo ○ ○ ○ ○ ○ Checkbox ○ ○ ○ ○ ○ Radio ○ ○ ○ ○ ○ Menu ○ ○ ○ Table ○ ○ ○ ○ Graph ○ ○ Column ○ ○ ○ 要素固有のパラメータ Button button_color (文字色, ボタン色) をタプルで指定します。 Table justification 各行の揃え方向を指定します。'left'(左揃え), 'center'(中央揃え), 'right'(右揃え)から選べます。 alternating_row_color 行の縞模様を作ります。奇数行の背景が指定した色になります。 selected_row_colors 選択中の行の色を (文字色, 背景色) で指定します。 header_text_color text_color と同様にヘッダーの文字色を指定します。 header_background_color background_color と同様にヘッダーの背景色を指定します。 header_font font と同様にヘッダーのフォントを指定します。 row_colors 特定行の色を指定します。(行インデックス, 文字色, 背景色) もしくは (行インデックス, 背景色) をまとめたリストで指定します。ex. row_colors=[(0, 'red', black), (1, 'blue')] col_widths 列ごとの幅をリストで指定します。auto_size_columns=False を指定する必要があります。 num_rows 表示する行数を指定します。表示しきれない分はスクロールとなります。 Column scrollable 範囲外の要素をスクロールで表示されるようにします。 vertical_scroll_only scrollable=True のとき、縦方向のスクロールのみ許可します。 element_justification Column 内の要素の横方向の揃え方向を 'left'(左揃え), 'center'(中央揃え), 'right'(右揃え)から指定します。 vertical_alignment Column 自体の縦方向の揃え方向を'top'(上揃え), 'center'(中央揃え), 'bottom'(下揃え)から指定します。 expand_x 横方向の余白いっぱいに要素サイズを拡張します。 expand_y 縦方向の余白いっぱいに要素サイズを拡張します。 メソッドの活用 これまではユーザー操作をプログラムに反映させる方法について解説してきましたが、今度は逆にプログラムからインターフェースを変化させる方法について説明していきます。 要素の取得方法 # 変数inputにkey='INPUT'をもつ要素オブジェクトを格納 input = window['INPUT'] # 対象の要素にupdate()メソッドを実行 input.update() # 変数を介さず直接実行してもOK window['INPUT'].update() メソッドを使うためにはまず、要素のオブジェクトを取得する必要があります。window には辞書型のように要素オブジェクトが格納されており、レイアウト時に key= で指定した文字列をキーとして要素オブジェクトを取得することができます。 共通メソッドとGraph固有メソッド すべての要素に共通なメソッドには以下の 3 つがあります。 メソッド 概要 update() 要素の持つ値を更新する bind() 規定のイベント以外の特殊な操作を受け付けるようにする get_size() 要素のサイズを取得する またスクロール可能な Table, Column 要素にはスクロール制御用の、様々な図形を描画できる Graph 要素に描画用のメソッドがあるため、そちらもあわせて解説します。 update() 要素の値や外観を更新します。元の値はクリアされるので、現在の内容に追加したい場合は工夫が必要です。以下に例を示します。 # 元のテキスト末尾に「!」を追加 window['INPUT'].update(values['INPUT'] + '!') values で現在値を取得し、その後ろに追加したい文字列 '!' を追加して更新することで、現在値の末尾に '!' が追加されたように見えます。 要素ごとに更新される値は以下の通りです。 要素名 変化する値 Button ボタンテキスト Text テキスト Input テキスト Multiline テキスト Slider 現在値 Spin 現在値 Checkbox チェック有無(True / False) Radio チェック有無(True / False) Menu 新しいメニュー構造 Table テーブル構造 Graph 背景色 Column =False で非表示 その他のパラメータの更新 表に示したのはあくまで第一引数のみで、その他の引数を指定することで他のパラメータについても変更が可能です。詳細は PySimpleGUI の Call reference で確認できます。 ここでは例として使用頻度の高い Table の select_rows= を挙げます。 window['TABLE'].update(select_rows=[]) select_rows= に選択したい行のインデックスのリストを与えると、その行が選択された状態になります。空のリストを与えるとすべての選択が解除されます。引数にリストではなく int を渡してしまうと、 rows_to_select = [i + 1 for i in select_rows] TypeError: 'int' object is not iterable と怒られてしまいます。 表示されていない行を選択するのもNGです。3 行しか表示していない Table に select_rows=[10] などとやってしまうと、 self.tk.call(self._w, "selection", selop, items) _tkinter.TclError: Item 11 not found といって怒られることになります。空のテーブルに対して select_rows=[0] とするのも同様にNGです。 bind() # key='GRAPH'をもつ要素にドラッグ操作を追加 # ドラッグを検知すると event='GRAPH__DRAG' となる window['GRAPH'].bind('<Button1-Motion>', '__DRAG') 任意のアクションをイベントとして捕捉することができます。画像処理においては「画像上にあるカーソルの座標を取得」、「表示した画像から ドラッグ & ドロップで矩形選択」といった複雑なアクションをしたいことも多く、特に Graph との組み合わせが有効です。 bind() は一度実行すればウィンドウを閉じるまで有効ですが、必ず window.finalize() してから行います。finalize() 前にバインドしようとすると、 ERROR Unable to complete operation on element with key *** と怒られます。 bind() は 2 つの引数 bind_string, key_modifierをもちます。 bind_string 捕捉したいイベントを格納する bind_string は修飾子、種類、詳細を -(ハイフン) でつなげた文字列を <> で囲んで記述します。例えば '<Control-Shift-S>' は Ctrl + Shift + s キー、'<Button1-Motion>' はマウスドラッグです。修飾子は何個でも連結することができますが、詳細は 1 個しか指定できません。 修飾子(modifier) 文字列 概要 Control Ctrl キーを押しながら Shift Shift キーを押しながら Alt Alt キーを押しながら Button1, B1 マウス左ボタンを押しながら Button2, B2 マウスホイールボタンを押しながら Button3, B3 マウス右ボタンを押しながら 種類(type) 文字列 概要 KeyPress, Key, 省略4 キーボードを押す KeyRelease キーボードを離す Return Enterキーを押す ButtonPress, Button マウスボタンを押す ButtonRelease マウスボタンを離す MouseWheel マウスホイールを回す Motion 要素の上でカーソルを動かす Enter 要素の内側にカーソルを入れる Leave 要素の外側にカーソルを出す 詳細(detail) 文字列 概要 1 マウス左クリック 2 マウスホイールクリック 3 マウス右クリック a, b, c, ... , z, A, B, C, ... , Zその他記号 対応するキー キー入力については大文字小文字が区別されます。'<Control-Shift-s>' などと書いてしまうと全然反応しないので注意が必要です(Shift + s の組み合わせが大文字 S として認識されるため)。Caps Lock がかかると文字の大小は逆転するので、厳密には s と S の両方を bind() すべきなのですが、個人的にはそこまでしなくてもいいかなとも思います。 key_modifier bind_string= で指定したユーザー操作を捕捉したときに、他のイベントと区別するため末尾に修飾子をつけることができます。 window['GRAPH'].bind('<Button1-Motion>', '__DRAG') のように書いた場合、この操作を捕捉すると従来のキー 'GRAPH' に '__DRAG' を付加して 'GRAPH__DRAG' というキーがイベントとして格納されます。 Table, Column 用メソッド set_vscroll_position() # 一番上にスクロール window['TABLE'].set_vscroll_position(0) # 中央にスクロール window['TABLE'].set_vscroll_position(0.5) # 一番下にスクロール window['TABLE'].set_vscroll_position(1) # 選択中の位置に合わせてスクロール # 0 除算回避のため len(list)-1 とはしない window['TABLE'].update(list) window['TABLE'].set_vscroll_position(values['TABLE'][0]/len(list)) スクロール位置を 0 ~ 1 の範囲で指定することができます。Table.update() で内容を更新したり選択行を変更したとき、その行が画面外にあっても自動でスクロールはしてくれません。なのでこのメソッドを使い必要な位置にスクロールしてやる必要があります。 Graph 用メソッド Graph はメソッドを利用して図形や文字、画像の描画が可能です。主な描画関連のメソッドを表にまとめました。 メソッド一覧 メソッド 概要 draw_point() 点を描画する draw_line() 線を描画する draw_rectangle() 矩形を描画する draw_polygon() 多角形を描画する draw_circle() 円を描画する draw_text() テキストを描画する draw_image() 画像を描画する move_figure() 描画済みの図を移動する move() 描画全体を移動する bring_figure_to_front() 描画済みの図を最前面に移動する send_figure_to_back() 描画済みの図を最背面に移動する delete_figure() 図を削除する erase() 描画全体を削除する change_coodinates() 左下、右上座標を変更する draw_point() # 座標(10, 20)に直径5, 赤色の点を描画 window['GRAPH'].draw_point((10, 20), size=5, color='#FF0000') 一覧に戻る draw_line() # 座標(10, 20)と(110, 120)を結ぶ幅1, 緑色の線を描画 window['GRAPH'].draw_line((10, 20), (110, 120), color='#00FF00', width=1) 一覧に戻る draw_rectangle() # 座標(10, 20)と(110, 120)を対角線とする矩形を描画。塗りつぶしは青、線は黄色、線幅は1 window['GRAPH'].draw_rectangle( (10, 20), (110, 120), fill_color='#0000FF', line_color='#FFFF00', line_width=1 ) 一覧に戻る draw_polygon() # pointsに指定した点を結ぶ三角形を描画。塗りつぶしなし、線は赤、線幅は1 points=[ (10, 20), (110, 120), (210, 100), ] window['GRAPH'].draw_polygon( points, fill_color=None, line_color='#FF0000', line_width=1 ) 一覧に戻る draw_circle() # 座標(10, 20)に半径30の円を描画。塗りつぶしは青、線なし window['GRAPH'].draw_circle( (10, 20), 30, fill_color='#0000FF', line_color=None, line_width=None ) 一覧に戻る draw_text() # 文字中央を基準として座標(110, 20)に白で'Text'と表示 # フォントはメイリオ 12pt、文字の回転はなし window['GRAPH'].draw_text( 'Text', (110, 20), color='#FFFFFF', font=('メイリオ', 12), angle=0, text_location=sg.TEXT_LOCATION_CENTER ) 一覧に戻る draw_image() import cv2 # OpenCVで取り込んだ画像imgを変換して、画像左上を基準として座標(10, 20)に表示 img = cv2.imread('image.png') img_bytes = cv2.imencode('.png', img)[1].tobytes() window['GRAPH'].draw_image(data=img_bytes, location=(10, 20)) 一覧に戻る move_figure() # draw_point()で描画した点を横に20移動、縦に-10移動 id_ = window['GRAPH'].draw_point((10, 20), size=5, color='#FF0000') window['GRAPH'].move_figure(id_, 20, -10) 一覧に戻る move() # 描画全体を横に20移動、縦に-10移動 window['GRAPH'].move(20, -10) 一覧に戻る bring_figure_to_front() # 矩形に隠れてしまった点を前面に移動 point = window['GRAPH'].draw_point((50, 50), size=5, color='#FF0000') rect = window['GRAPH'].draw_rectangle( (0, 0), (100, 100), fill_color='#0000FF', ) window['GRAPH'].bring_figure_to_front(point) 一覧に戻る send_figure_to_back() # 点を隠してしまった矩形を背面に移動 point = window['GRAPH'].draw_point((50, 50), size=5, color='#FF0000') rect = window['GRAPH'].draw_rectangle( (0, 0), (100, 100), fill_color='#0000FF', ) window['GRAPH'].send_figure_to_back(rect) 一覧に戻る delete_figure() # 描画した点を削除 id_ = window['GRAPH'].draw_point((10, 20), size=5, color='#FF0000') window['GRAPH'].delete_figure(id_) 一覧に戻る erase() # 描画をすべて削除 window['GRAPH'].erase() 一覧に戻る change_coodinates() # グラフ内の座標を左上(0, 0), 右下(640, 480)に変更 window['GRAPH'].change_coordinates((0, 480), (640, 0)) 一覧に戻る 実践編 PySimpleGUIを用いた実装例として、画像ビューワーを作成してみます。 画像ビューワ概要 動作イメージは下図の通りです。 ファイル > 開く と選択するとフォルダ選択ダイアログが表示されます。フォルダを選択すると、その中にある画像ファイルが画面左側のリストに一覧表示されます。一覧から表示する画像を選び、表示範囲を選ぶことで拡大ができます。表示範囲は縦横比を指定が可能です。 操作一覧 操作 アクション リストをクリック 表示画像を選択する 画像上でマウススクロール 表示画像の順送り / 逆送り 画像上でドラッグ 表示範囲の選択 ドラッグ中に右クリック 選択中止 画像上でダブルクリック 表示範囲の選択解除 コード 以下のコードをコピペすれば動作します。 コード全文を表示 import PySimpleGUI as sg import tkinter as tk import glob import cv2 import numpy as np # 画像読込 # cv2.imread()は日本語パスに対応していないのでその対策 def imread(filename, flags=cv2.IMREAD_UNCHANGED, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None # 画像を表示(sg.Graph インスタンスメソッド) def draw_image_plus(self, img, location=(0,0)): if type(img) == np.ndarray: img = cv2.imencode('.png', img)[1].tobytes() id_ = self.draw_image(data=img, location=location) return id_ sg.Graph.draw_image_plus = draw_image_plus # 1. レイアウト # 描画エリア canvas = sg.Graph( (1920, 1920), # 大きめに作って画面外にはみ出させる (0, 1920), # 表示サイズに合わせる (1920, 0), background_color='#000000', pad=(0, 0), key='CANVAS', ) # 画像リスト表示 table_source = sg.Table( [[]], ['画像'], col_widths=[20], auto_size_columns=False, num_rows=100, # 長めに作ってはみ出させる justification='left', select_mode=sg.TABLE_SELECT_MODE_BROWSE, background_color='#000000', pad=(0, 0), enable_events=True, key='TABLE_SOURCE', ) layout = [ [ sg.Menu( [ [ 'ファイル(&F)', [ '開く (&O)::MENU_OPEN_FOLDER::', '終了 (&X)::MENU_EXIT::', ], ], ], ), ], [ sg.Checkbox('縦横比固定', True, pad=(0, 0), key='ENABLE_ASPECT'), sg.Combo(['画面サイズ', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16', '指定比率'], '画面サイズ', size=(12, 1), readonly=True, enable_events=True, key='ASPECT_MODE'), sg.Text('', size=(2, 1), pad=(0, 0)), sg.Column( [ [ sg.Input('', size=(5, 1), pad=(0, 0), key='ASPECT_X'), sg.Text(' : ', pad=(0, 0)), sg.Input('', size=(5, 1), pad=(0, 0), key='ASPECT_Y'), ] ], visible=False, key='COLUMN_ASPECT', ) ], [ table_source, canvas, ], ] # 2. ウィンドウの生成 window = sg.Window( title='Window title', layout=layout, resizable=True, size=(800, 600), margins=(0, 0), ) window.finalize() table_source.bind('<ButtonPress-1>', '__LEFT_PRESS') # テーブル選択 canvas.bind('<MouseWheel>', '__SCROLL') # 表示画像のスクロール変更 canvas.bind('<ButtonPress-1>', '__LEFT_PRESS') # 範囲選択開始 canvas.bind('<Button1-Motion>', '__DRAG') # ドラッグで範囲選択 canvas.bind('<Button1-ButtonPress-3>', '__DRAG_CANCEL') # ドラッグ中止(ドラッグ中に右クリック) canvas.bind('<ButtonRelease-1>', '__LEFT_RELEASE') # ドラッグ範囲確定 canvas.bind('<Double-ButtonPress-1>', '__DOUBLE_LEFT') # 選択範囲解除 canvas.drag_from = None # ドラッグ開始位置 canvas.current = None # カーソル現在位置 canvas.selection = None # 選択範囲 canvas.selection_figure = None # 選択範囲の描画ID trim_areas = {} # 選択範囲記憶用 key=ファイルパス, value=選択範囲 img_update = False # 画像の更新要否 previous_canvas_size = None # 前フレームのキャンバスサイズ(ウィンドウサイズ変更検出用) # 3. GUI処理 while True: event, values = window.read(timeout=100, timeout_key='TIMEOUT') # 終了 if event is None or '::MENU_EXIT::' in event: break # ウィンドウサイズ変更の検出 if event == 'TIMEOUT': if previous_canvas_size != canvas.get_size(): event = 'CANVAS_RESIZE' previous_canvas_size = canvas.get_size() if event == 'TIMEOUT': continue # フォルダを開く if '::MENU_OPEN_FOLDER::' in event: source_dir = tk.filedialog.askdirectory().replace('[', '[[]').replace(']', '[]]').replace('[[[]]', '[[]') if source_dir: trim_areas = {} # ファイル一覧を取得 fullpath_list = glob.glob('{}/*.png'.format(source_dir)) \ + glob.glob('{}/*.jpg'.format(source_dir)) \ + glob.glob('{}/*.jpeg'.format(source_dir)) \ + glob.glob('{}/*.bmp'.format(source_dir)) \ + glob.glob('{}/*.gif'.format(source_dir)) fullpath_list.sort() fullpath_list = [s.replace('\\', '/') for s in fullpath_list] if fullpath_list: select_rows=[0] else: select_rows=[] # ファイル一覧更新 window['TABLE_SOURCE'].update([[s.split('/')[-1]] for s in fullpath_list], select_rows=select_rows) # 縦横比選択 if event == 'ASPECT_MODE': if values['ASPECT_MODE'] == '指定比率': aspect_visible = True else: aspect_visible = False window['COLUMN_ASPECT'].update(aspect_visible) # 選択スクロール if event == 'CANVAS__SCROLL' and values['TABLE_SOURCE']: row = values['TABLE_SOURCE'][0] item_len = len(fullpath_list) if canvas.user_bind_event.delta > 0 and row > 0: row -= 1 elif canvas.user_bind_event.delta < 0 and row < item_len - 1: row += 1 window['TABLE_SOURCE'].update( [[s.split('/')[-1]] for s in fullpath_list], select_rows=[row], ) # スクロール外の要素を選択しても融通はきかないので自分で動かす必要あり # スクロール位置を0~1で指定 window['TABLE_SOURCE'].set_vscroll_position(row/item_len) # 選択中の画像があれば処理 if values['TABLE_SOURCE']: current_fullpath = fullpath_list[values['TABLE_SOURCE'][0]] # アス比取得 if values['ENABLE_ASPECT']: if ':' in values['ASPECT_MODE']: (x, y) = values['ASPECT_MODE'].split(':') aspect = np.array(( int(x), int(y), )) elif values['ASPECT_MODE'] == '指定比率': try: aspect = np.array((int(values['ASPECT_X']), int(values['ASPECT_Y']))) except ValueError: aspect = None # get_size()で表示エリアサイズを測定 elif values['ASPECT_MODE'] == '画面サイズ': aspect = np.array(canvas.get_size()) else: aspect = None # 矩形選択開始 if event == 'CANVAS__LEFT_PRESS': canvas.drag_from = np.array((canvas.user_bind_event.x, canvas.user_bind_event.y)) canvas.current = np.array((canvas.user_bind_event.x, canvas.user_bind_event.y)) # ドラッグ処理 if event == 'CANVAS__DRAG' and canvas.drag_from is not None: canvas.current = np.array((canvas.user_bind_event.x, canvas.user_bind_event.y)) canvas.selection = np.array((canvas.drag_from, canvas.current)) canvas.selection = np.array((canvas.selection.min(axis=0), canvas.selection.max(axis=0))) # ((左上), (右下))の順に並び替える # アスペクト比の適用 if aspect is not None: selection_size = (canvas.selection[1] - canvas.selection[0]) aspected = (aspect[0]/aspect[1]*selection_size[1], aspect[1]/aspect[0]*selection_size[0]) + canvas.selection[0] canvas.selection = np.vstack([canvas.selection, [aspected]]) # アス比適応時と合体させる canvas.selection = np.array((canvas.selection.min(axis=0), canvas.selection.max(axis=0))).clip((0, 0), img_area_limit) # アス比適応、上下限適応 # 矩形選択キャンセル if event == 'CANVAS__DRAG_CANCEL': canvas.selection = None canvas.drag_from = None # 矩形選択完了 current_is_key = current_fullpath in list(trim_areas.keys()) # 記録済みの選択範囲があるか if event == 'CANVAS__LEFT_RELEASE' and canvas.selection is not None: # 面積0の選択範囲はスキップ if (canvas.selection[1] - canvas.selection[0]).min() >= 1: canvas.selection = (canvas.selection.astype(float)*image_scale).astype(int) # すでに選択範囲がある場合はオフセットする if current_is_key: canvas.selection += trim_areas[current_fullpath][0] # 選択範囲の記録 trim_areas[current_fullpath] = canvas.selection # 範囲を記録したらリセット canvas.selection = None canvas.drag_from = None current_is_key = current_fullpath in list(trim_areas.keys()) # 選択範囲の登録解除 if event == 'CANVAS__DOUBLE_LEFT' and current_is_key: trim_areas.pop(current_fullpath) current_is_key = False # 画像更新 if event in ('TABLE_SOURCE', 'CANVAS__LEFT_RELEASE', 'CANVAS__DOUBLE_LEFT'): filename = fullpath_list[values['TABLE_SOURCE'][0]] img = imread(filename) # 登録済みの選択範囲があればトリミングする if current_is_key: rect = trim_areas[current_fullpath] img_trim = img[rect[0, 1]:rect[1, 1], rect[0, 0]:rect[1, 0]] else: img_trim = img.copy() img_update = True # 画像表示(画像が更新された場合か、ウィンドウがリサイズされた場合) if img_update or (event == 'CANVAS_RESIZE' and values['TABLE_SOURCE']): img_size = np.array(img_trim.shape[1::-1], dtype=int) # shapeは縦、横の順なのでスライスは反転させる canvas_size = np.array(canvas.get_size()) # キャンバス比で長い方の割合をsceleとする image_scale = (img_size / canvas_size).max() # キャンバスに対して長い方を基準に縮小するので、画像が画面外にはみ出ない img_resize = cv2.resize(img_trim, tuple((img_size/image_scale).astype(int))) # 画像端座標を取得 img_area_limit = ((np.array(img_resize.shape[1::-1])-1)) # キャンバスリセット→画像表示 canvas.erase() canvas.draw_image_plus(img_resize) img_update = False # 選択範囲表示 if canvas.selection_figure is not None: canvas.delete_figure(canvas.selection_figure) if canvas.selection is not None: canvas.selection_figure = canvas.draw_rectangle( list(canvas.selection[0]), list(canvas.selection[1]), line_color='#FF0000', line_width=1 ) window.close() ポイント 以下の処理に分けて解説します。 cv2.imread() の改良 Graph に画像表示用メソッドを追加 Column による表示切り替え レイアウト Graph 要素のセットアップ 描画エリアサイズ変更検知 ファイル一覧を取得し、Table に一覧表示 Table の操作 画像の矩形選択 選択範囲の確定 選択範囲のリセット 画像の更新 画像表示 選択範囲表示 cv2.imread() の改良 # 画像読込 # cv2.imread()は日本語パスに対応していないのでその対策 def imread(filename, flags=cv2.IMREAD_UNCHANGED, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None いきなり PySimpleGUI じゃないですが・・・。 OpenCV の画像読み込み関数 cv2.imread() は日本語パスに対応していません。今回はユーザー指定のフォルダからファイルを読み込むので、日本語パスに対応する必要があります。下記の記事より imread() 関数を拝借しました。 一覧に戻る Graph に画像表示用メソッドを追加 # 画像を表示(sg.Graph インスタンスメソッド) def draw_image_plus(self, img, location=(0,0)): if type(img) == np.ndarray: img = cv2.imencode('.png', img)[1].tobytes() # cv2.imencode('.png', img)[0] はエンコード成否 id_ = self.draw_image(data=img, location=location) return id_ sg.Graph.draw_image_plus = draw_image_plus Graph には画像を描画する draw_image() というメソッドがありますが、imread() で取り込んだ画像をそのまま表示することはできません。cv2.imencode() 関数により画像を image オブジェクト化する必要があります。今回は無劣化で表示したいので png 形式でエンコードします。その後 tobyte() でバイナリ化すると Graph で表示できるようになります。 この処理を画像表示のたびに書くのは非効率なので関数化します。さらにその関数を Graph 要素のメソッドとして組み込むことで、他の描画用メソッドと同じような書き方で扱えます。個人的にはおすすめのやり方です。 一覧に戻る Column による表示切り替え sg.Column( [ [ sg.Input('', size=(5, 1), pad=(0, 0), key='ASPECT_X'), sg.Text(' : ', pad=(0, 0)), sg.Input('', size=(5, 1), pad=(0, 0), key='ASPECT_Y'), ] ], visible=False, key='COLUMN_ASPECT', ) 横一列に要素が並んでいるだけで、一見 Column 必要ないように見えます。しかしこうすることで、 # 表示 window['COLUMN_ASPECT'].update(True) # 非表示 window['COLUMN_ASPECT'].update(False) のように表示を切り替えることができます。ただし非表示→表示とするとなぜか行の右端に出現するので、右側に別の要素を配置している場合はレイアウトが崩壊します5。 一覧に戻る レイアウト # Graph canvas = sg.Graph( (1920, 1920), # 大きめに作って画面外にはみ出させる (0, 1920), # 表示サイズに合わせる (1920, 0), background_color='#000000', pad=(0, 0), key='CANVAS', ) # Window window = sg.Window( title='Window title', layout=layout, resizable=True, size=(800, 600), margins=(0, 0), ) あらゆる要素に pad=0 を指定してカツカツのレイアウトにしています。Window も margins=(0, 0) を指定することで窓枠ギリギリまで要素を置くことができます。 一覧に戻る Graph 要素のセットアップ # 定義 canvas = sg.Graph( (1920, 1920), # 大きめに作って画面外にはみ出させる (0, 1920), # 表示サイズに合わせる (1920, 0), background_color='#000000', pad=(0, 0), key='CANVAS', ) # イベントバインド canvas.bind('<ButtonPress-1>', '__LEFT_PRESS') # テーブル選択 canvas.bind('<MouseWheel>', '__SCROLL') # 表示画像のスクロール変更 canvas.bind('<Button1-Motion>', '__DRAG') # ドラッグで範囲選択 canvas.bind('<Button1-ButtonPress-3>', '__DRAG_CANCEL') # ドラッグ中止(ドラッグ中に右クリック) canvas.bind('<ButtonRelease-1>', '__LEFT_RELEASE') # ドラッグ範囲確定 canvas.bind('<Double-ButtonPress-1>', '__DOUBLE_LEFT') # 選択範囲解除 # メンバ変数定義 canvas.drag_from = None # ドラッグ開始位置 canvas.current = None # カーソル現在位置 canvas.selection = None # 選択範囲 canvas.selection_figure = None # 選択範囲の描画ID Graph 要素は画像処理 GUI で特に使用頻度が高いので使い勝手を上げます。メソッド呼び出すごとに window['CANVAS'] は面倒なので、短めの変数に要素を格納しておきます。あとは必要なイベントをバインドして、カーソル位置や選択範囲などのパラメータを定義しておきます。Graph 操作関連のパラメータはメンバ変数として定義しておいたほうが拡張性がある6のでおすすめです。 一覧に戻る 描画エリアサイズ変更検知 # 描画エリアサイズ監視用にタイムアウトを指定 event, values = window.read(timeout=100, timeout_key='TIMEOUT') # 描画エリアサイズ変更の検出 if event == 'TIMEOUT': if previous_canvas_size != canvas.get_size(): event = 'CANVAS_RESIZE' previous_canvas_size = canvas.get_size() if event == 'TIMEOUT': continue ウィンドウサイズに合わせて画像の大きさを調節するため、描画エリアサイズの変更を捕捉します。正攻法ではイベントとして登録できないので、リアルタイムにウィンドウサイズを監視して、変化があった場合は手動でイベントを起こします。リアルタイム処理を行う場合は window.read() に timeout= を指定します。単位はミリ秒です。あわせて timeout_key= を指定すると、timeout= 時間内に何もイベントがおきなければ指定したキーでイベントを発生させます。canvas.get_size() で描画エリアのサイズを検出し前回の結果と比較、変化があれば event='CANVAS_RESIZE' として手作りイベントを発生させます。 一覧に戻る ファイル一覧を取得し、Table に一覧表示 import tkinter as tk # フォルダを開く if '::MENU_OPEN_FOLDER::' in event: source_dir = tk.filedialog.askdirectory().replace('[', '[[]').replace(']', '[]]').replace('[[[]]', '[[]') if source_dir: trim_areas = {} # ファイル一覧を取得 fullpath_list = glob.glob('{}/*.png'.format(source_dir)) \ + glob.glob('{}/*.jpg'.format(source_dir)) \ + glob.glob('{}/*.jpeg'.format(source_dir)) \ + glob.glob('{}/*.bmp'.format(source_dir)) \ + glob.glob('{}/*.gif'.format(source_dir)) fullpath_list.sort() fullpath_list = [s.replace('\\', '/') for s in fullpath_list] if fullpath_list: select_rows=[0] else: select_rows=[] # ファイル一覧更新 window['TABLE_SOURCE'].update([[s.split('/')[-1]] for s in fullpath_list], select_rows=select_rows) フォルダを指定して中にある画像ファイルを一覧取得します。フォルダ選択ダイアログには tkinter を使用します。PySimpleGUI にも FolderBrowse() というフォルダ選択用の要素がありますが、ボタンの形に縛られたり、値の更新に window.read() が必要だったりと制約が多いです。その分組み込みが楽だったりはあるので良し悪しですが、迷ったら tkinter で良いと思います。 中にあるファイルの一覧取得には glob() を使います。指定した文字列に合致するファイル(とフォルダ)をリストで返します。* はワイルドカードで任意の文字列にマッチします。ちなみに ? は任意の 1 文字にマッチします。また [, ] は「[] 中の 1 文字にマッチ」を表す文字列なので、[[], []] に置換します。replace('[', '[[]'), replace(']', '[]]') と置換を繰り返すと [ から [[] に置換されたものが [[[]] に再置換されるので [[] に戻す必要があります。スマートにやりたい方は正規表現を使いましょう。 Table.update() メソッドには二次元リストを渡します。内包表記を使って、1次元リストから縦 1 列の二次元リストを作成します。 一覧に戻る Table の操作 # 選択スクロール if event == 'CANVAS__SCROLL' and values['TABLE_SOURCE']: row = values['TABLE_SOURCE'][0] item_len = len(fullpath_list) if canvas.user_bind_event.delta > 0 and row > 0: row -= 1 elif canvas.user_bind_event.delta < 0 and row < item_len - 1: row += 1 window['TABLE_SOURCE'].update( [[s.split('/')[-1]] for s in fullpath_list], select_rows=[row], ) マウスホイールの動きに合わせて選択画像を切り替えられるようにします。select_rows= に負数や行数以上の数が入らないように条件を指定します。最初に values['TABLE_SOURCE'][0] 選択行を指定していますが、選択していない状態だとエラーが発生するため、if values['TABLE_SOURCE'] で回避します3。マウスホイールの動きに合わせて user_bind_event.delta に値が代入されます。上回転が +, 下回転が - ですので、それに合わせて行番号を増減させます。 一覧に戻る 画像の矩形選択 canvas.bind('<ButtonPress-1>', '__LEFT_PRESS') # 範囲選択開始 canvas.bind('<Button1-Motion>', '__DRAG') # ドラッグで範囲選択 canvas.bind('<Button1-ButtonPress-3>', '__DRAG_CANCEL') # ドラッグ中止(ドラッグ中に右クリック) canvas.bind('<ButtonRelease-1>', '__LEFT_RELEASE') # ドラッグ範囲確定 # 矩形選択開始 if event == 'CANVAS__LEFT_PRESS': canvas.drag_from = np.array((canvas.user_bind_event.x, canvas.user_bind_event.y)) canvas.current = np.array((canvas.user_bind_event.x, canvas.user_bind_event.y)) # ドラッグ処理 if event == 'CANVAS__DRAG' and canvas.drag_from is not None: canvas.current = np.array((canvas.user_bind_event.x, canvas.user_bind_event.y)) canvas.selection = np.array((canvas.drag_from, canvas.current)) canvas.selection = np.array((canvas.selection.min(axis=0), canvas.selection.max(axis=0))) # ((左上), (右下))の順に並び替える # アスペクト比の適用 if aspect is not None: selection_size = (canvas.selection[1] - canvas.selection[0]) aspected = (aspect[0]/aspect[1]*selection_size[1], aspect[1]/aspect[0]*selection_size[0]) + canvas.selection[0] canvas.selection = np.vstack([canvas.selection, [aspected]]) # アス比適応時と合体させる canvas.selection = np.array((canvas.selection.min(axis=0), canvas.selection.max(axis=0))).clip((0, 0), img_area_limit) # アス比適応、上下限適応 # 矩形選択キャンセル if event == 'CANVAS__DRAG_CANCEL': canvas.selection = None canvas.drag_from = None 画像上をドラッグすることで範囲を指定できるようにします。 左クリックでドラッグの起点座標 canvas.drag_from を設定します。カーソルの座標は canvas.user_bind_event.x, canvas.user_bind_event.y で取得可能です。ただし、取得できる座標は左上基準のピクセル単位で固定です。Graphをレイアウトしたときに指定した座標系は関係ないので注意してください7。 ドラッグ中はカーソル現在地 canvas.current を更新し続け、canvas.drag_from と canvas.current の組み合わせで選択範囲 canvas.selection を定義します。このとき2 点の位置関係が不明なので、 canvas.selection = np.array((canvas.drag_from, canvas.current)) canvas.selection = np.array((canvas.selection.min(axis=0), canvas.selection.max(axis=0))) として x, y それぞれの最小値(左上)と最大値(右上)に振り分けます。この辺りの計算を楽にするために、座標を numpy.array で定義しています。アスペクト比の適用については割愛します。そんなもんなんだな~位に思っておいてください。 ドラッグ中に右クリックを押すと、canvas.selection と canvas.drag_from をリセットして範囲選択を取り消します。 一覧に戻る 選択範囲の確定 # 矩形選択完了 current_is_key = current_fullpath in list(trim_areas.keys()) # 記録済みの選択範囲があるか if event == 'CANVAS__LEFT_RELEASE' and canvas.selection is not None: # 面積0の選択範囲はスキップ if (canvas.selection[1] - canvas.selection[0]).min() >= 1:  canvas.selection = (canvas.selection.astype(float)*image_scale).astype(int) # 記録済みの選択範囲がある場合はオフセットする if current_is_key: canvas.selection += trim_areas[current_fullpath][0] # 選択範囲の記録 trim_areas[current_fullpath] = canvas.selection # 範囲を記録したらリセット canvas.selection = None canvas.drag_from = None current_is_key = current_fullpath in list(trim_areas.keys()) ドラッグした状態からマウスを離すと選択範囲を確定し、変数 trim_areas に登録します。trim_areas は画像のファイルパスと選択範囲を組み合わせた辞書型の変数です。 まず選択範囲の大きさを確認し、高さ / 幅が 0 であれば登録しません。矩形選択が可能な形状であれば、選択範囲を画像の表示倍率 image_scale で拡大 / 縮小します。 すでに一度範囲の登録を終えていて拡大表示されている場合は、Graph 左上 ≠ 元画像原点となるのでそのまま登録できません。 # 記録済みの選択範囲がある場合はオフセットする if current_is_key: canvas.selection += trim_areas[current_fullpath][0] 登録済みの選択範囲を参照し、左上座標のぶんだけ全体をオフセットします。 一覧に戻る 選択範囲のリセット # 選択範囲の登録解除 if event == 'CANVAS__DOUBLE_LEFT' and current_is_key: trim_areas.pop(current_fullpath) current_is_key = False ダブルクリックをトリガーにして選択範囲をリセットします。pop() 関数を使って現在の選択範囲を削除します。 一覧に戻る 画像の更新 # 画像更新 if event in ('TABLE_SOURCE', 'CANVAS__LEFT_RELEASE', 'CANVAS__DOUBLE_LEFT'): filename = fullpath_list[values['TABLE_SOURCE'][0]] img = imread(filename) # 登録済みの選択範囲があればトリミングする if current_is_key: rect = trim_areas[current_fullpath] img_trim = img[rect[0, 1]:rect[1, 1], rect[0, 0]:rect[1, 0]] else: img_trim = img.copy() img_update = True 画像の選択や選択範囲の登録・解除に関わる操作があった場合は画像を更新します。先に解説した imread() 関数で画像を表示します。 trim_areas に選択範囲が登録されている場合は、その範囲に応じて画像をトリミングします。img[上:下, 左:右] とスライスを指定すると、その範囲のみの画像が得られます。 後ろの描画処理に更新したことを伝えるため img_update を True にします。 一覧に戻る 画像表示 # 画像表示(画像が更新された場合か、ウィンドウがリサイズされた場合) if img_update or (event == 'CANVAS_RESIZE' and values['TABLE_SOURCE']): img_size = np.array(img_trim.shape[1::-1], dtype=int) # shapeは縦、横の順なのでスライスは反転させる canvas_size = np.array(canvas.get_size()) # キャンバス比で長い方の割合をsceleとする image_scale = (img_size / canvas_size).max() # キャンバスに対して長い方を基準に縮小するので、画像が画面外にはみ出ない img_resize = cv2.resize(img_trim, tuple((img_size/image_scale).astype(int))) # 画像端座標を取得 img_area_limit = ((np.array(img_resize.shape[1::-1])-1)) # キャンバスリセット→画像表示 canvas.erase() canvas.draw_image_plus(img_resize) img_update = False 画像が更新された、もしくはウィンドウサイズが変更された場合は画像を表示しなおします。画像をそのまま表示すると、画面に対して小さすぎたり、逆に画面からはみ出てしまったりするので、ウィンドウサイズに合わせて画像をリサイズして表示します。 画像のサイズ取得には shape を使います。imread() 関数で画像は配列 numpy.ndarray となり、shape にアクセスすると (縦画素数, 横画素数, チャンネル数) のタプルを得ることができます8。ここで得られた画像サイズ img_size とキャンバスサイズ canvas_size の比を縦横それぞれで計算し、その最大値をリサイズの倍率にします。 得られた倍率から、cv2.resize() を使って画像をキャンバスサイズに合わせてリサイズします。この関数には画像と変更後のサイズ (幅, 高さ) を渡しますが、numpy.ndarray を渡すと、 SystemError: new style getargs format but argument is not a tuple というエラーが発生してしまいます。必ずリストなりタプルに変換して渡しましょう。リサイズが終われば、そのサイズを選択可能範囲 img_area_limit として登録します。こうすることで画像でない範囲を選択してエラーが起きるのを防ぐことができます。 その後はリサイズした画像を自作のメソッド draw_image_plus() に渡して画像を表示させます。このときキャンバスに残った画像を削除しないと、表示する画像がどんどん増えていくので、事前に erase() メソッドでクリアしておきます。画像更新が完了したら image_update を False に戻します。 一覧に戻る 選択範囲表示 # 選択範囲表示 if canvas.selection_figure is not None: canvas.delete_figure(canvas.selection_figure) if canvas.selection is not None: canvas.selection_figure = canvas.draw_rectangle( list(canvas.selection[0]), list(canvas.selection[1]), line_color='#FF0000', line_width=1 ) ドラッグ中の選択範囲を表示します。後から描画した要素が上に表示されるので、画像の描画をしてから矩形を描画します。画像の描画時と同様に描画済みの図形をリセットする必要があるので、delete_figure() メソッドを使って描画済みの図形を削除してから描画します。 一覧に戻る おわりに お疲れさまでした。自分自身このライブラリを使う中であちこち参照して回るのが億劫になり、全体を網羅的にまとめたい!と思ったら相当なボリュームになってしまいました・・・。 とはいえ PySimpleGUI を使えばかんたんなGUIをサクッと作ることから、マルチウィンドウを駆使した複雑な構成まで可能になっています。公式の解説も英語ではありますが、中々親切です。 ぜひかんたんなところから色々試してみてください! 参考 PySimpleGUI https://pysimplegui.readthedocs.io/en/latest/ Tkinterを使うのであればPySimpleGUIを使ってみたらという話 https://qiita.com/dario_okazaki/items/656de21cab5c81cabe59 PysimpleGUIを用いたOpenCV画像処理表示 https://qiita.com/Gyutan/items/b4781ee1baa4966699ef Tkinter の bind とイベントシーケンス http://www.rouge.gr.jp/~fuku/tips/python-tkinter/bind.shtml Python OpenCV の cv2.imread 及び cv2.imwrite で日本語を含むファイルパスを取り扱う際の問題への対処について https://qiita.com/SKYS/items/cbde3775e2143cad7455 '[文字列B]' in '[文字列A]' は '[文字列B]' が '[文字列A]' に含まれる場合に True になります。 ↩ これは TABLE_SELECT_MODE_BROWSE を選んで複数行の選択を禁止したとしても変わりません。 ↩ if 文に空のリストを渡すと False 扱いとなります。 ↩ 種類を書かず詳細をそのまま書くということです。<h>, <Control-c> など。 ↩ 対策知ってる方いたら教えてください。 ↩ 2 画面表示などでも個別のメンバ変数として一意な名前付けができる、など。 ↩ values で取得できる座標だけはレイアウト時の座標系に従います。 ↩ モノクロ画像の場合は (縦画素数, 横画素数) になります。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[kaggle / python] 回帰問題(house prices)の超初歩(3)~特徴量の追加と削除~

前回の続きです。 前回の記事はこちら。 Summary of the previous article(前回のあらまし) 前回は、kaggle の House Prices の課題に対して、 「Label Encodingを使用して、初期状態で存在する特徴量全てをxgboostの学習データとして活用する」 というところまでやりました。 Label Encodingを使用した結果、順位が上位68% -> 64%へと4%上昇しました。 Scoreでいうと、0.15663->0.15160という変化がありました。 Today's result 今回は、特に難しいことはしないのですが、特徴量を新しく作成して追加したり、不要と考えられる特徴量を削除してみます。 やったこと score 前回 - 0.15160 今回 Part.1 特徴量の追加 0.15058 今回 Part.2 特徴量の削除 0.15140 今回は2つ作業をしたので、二つスコアが出てます。 二つ目の結果は、scoreが下がっているのですが.... この操作は通常行うべきものと考えられるため、scoreが下がったとしてもこれは実施することにしました。 理由は後半で書こうと思います。 さて、具体的に何をしたのか見ていきましょう~ Part.1 1. source cord 1-0. same as the previous cord またまた、前回と同じソースをこの「1-0.」にまとめておきます(^ワ^*) 今回ここで、やっていることは、 モジュール読み込み データ読み込み です。 # import modules import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv) from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error from xgboost import XGBClassifier, XGBRegressor # load data import os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) train = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/train.csv') test = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/test.csv') 1-1. add new column 今回はここで、新しいカラム(特徴量)を追加します。 物件の1階の広さ、2階の広さ、地下の広さを足し合わせた特徴量としてTotalBsmtSFというデータを生成します。 # add new column train['totalRoomsSF'] = train['1stFlrSF'] + train['2ndFlrSF'] + train['TotalBsmtSF'] test['totalRoomsSF'] = test['1stFlrSF'] + test['2ndFlrSF'] + test['TotalBsmtSF'] 1-2. same as the previous cord そして実は、以降は全く同じソースとなりますw つまり、先ほどのTotalBsmtSFという特徴量を作成することによる効果を今回確認する形になります。 以降は前回と同じコードですが、何はともあれ、一応同じコードも書いておきます。 説明変数と目的変数の分割 学習データとテストデータの分割 LabelEncoding 学習model作成 価格予測 kaggleに提出(submission)するデータの生成 # 1. split data into explanatory variable(説明変数) and response variable(目的変数) tmp_train_x = train.drop('SalePrice', axis=1) tmp_train_y = train['SalePrice'] # 2. split data into training data and test data x_train, x_test, y_train, y_test = \ train_test_split(tmp_train_x, tmp_train_y, test_size=0.20, random_state=0) # 3. LabelEncoding from sklearn.preprocessing import LabelEncoder x_train_label_encoded = x_train.copy() x_test_label_encoded = x_test.copy() # ※注意 df_all_data = pd.concat([x_train, x_test, test]) for col_name in not_expected_type_column_names: # nanを'NaN'という文字列に置換してます target_all_data_column = df_all_data[col_name].fillna('NaN') le = LabelEncoder() le.fit(target_all_data_column) target_train_column = x_train[col_name].fillna('NaN') target_test_column = x_test[col_name].fillna('NaN') x_train_label_encoded[col_name] = le.transform(target_train_column) x_test_label_encoded[col_name] = le.transform(target_test_column) # 4. create model(学習) model = XGBRegressor(n_estimators=20, random_state=0) model.fit(x_train_label_encoded, y_train) # 5. prediction(推論) predict_result_for_tr_data = model.predict(x_train_label_encoded) predict_result = model.predict(x_test_label_encoded) # 学習データをpredictした結果 rmse_of_train = np.sqrt(mean_squared_error(predict_result_for_tr_data, y_train)) rmse_of_train 7681.997640750206 (前回: 8146.100714171564) # テストデータをpredictした結果 rmse_of_test = np.sqrt(mean_squared_error(predict_result, y_test)) rmse_of_test 30411.796986629754 (前回: 33584.266159693645) # 6. create csv for submission test_encoded = test.copy() for col_name in not_expected_type_column_names: # ここでも、全データをlabel encoderのfitに食べさせるのを忘れずに! target_all_data_column = df_all_data[col_name].fillna('NaN') le = LabelEncoder() le.fit(target_all_data_column) target_test_column = test[col_name].fillna('NaN') test_encoded[col_name] = le.transform(target_test_column) predict_submission = model.predict(test_encoded) submission = pd.DataFrame( {'Id': test['Id'], 'SalePrice': predict_submission} ) submission.to_csv('submission_new.csv', index=False) この結果、スコアは0.15160から0.15058へと改善されました! 順位は記録取ってなかったのですが、大して上がりませんでした(๑>؂<๑) Part.2 Part.2は、変更した箇所だけを書きます! 1. source cord 1-1. drop columns 今回ここで、Id列を特徴量から削除します。 Part.1の# add new columnのところで、totalRoomsSFという特徴量を作成しましたが、この直後でId列をdropします。 # add new column train['totalRoomsSF'] = train['1stFlrSF'] + train['2ndFlrSF'] + train['TotalBsmtSF'] test['totalRoomsSF'] = test['1stFlrSF'] + test['2ndFlrSF'] + test['TotalBsmtSF'] # drop ID column train_ID = train['Id'] test_ID = test['Id'] train = train.drop(['Id'], axis=1) test = test.drop(['Id'], axis=1) 注意点としては、この処理のために変数testからId列が無くなってしまいますので、kaggleへのsubmit用csvを作成する際の一列目には、test_IDを指定しましょう。 以下のような感じです。 submission = pd.DataFrame( # {'Id': test['Id'], 'SalePrice': predict_submission} <- コレでは動かない # test['Id']は削除してしまったので、test_IDを使う {'Id': test_ID, 'SalePrice': predict_proto2} ) submission.to_csv('submission_new.csv', index=False) この結果、スコアは0.15058から0.15140へと悪化してしまいます... 2. Why drop Id? データ分析の経験が少ない私ですが、Id列を削除した理由を書いてみます。 Id列は、通し番号のようなものです。 データ分析一般に視野を広げて考えると、基本的にはId自体に意味はないはずです。 ただ、今回の House Price においては、価格を決める重要な特徴量として作用していたようです。 XGBoostのモジュールで、特徴量ごとの重要度を表示できるのですが、以下のとおりです。 from xgboost import plot_importance import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline ax = plot_importance(model, max_num_features = 100) fig = ax.figure fig.set_size_inches(10, 30) 「ポイントが高い方ければ高いほど、価格の推論にとって重要だった」ということを示しているので、これだけを見ると、Idは価格を決める上でかなり重要な値として作用していたことがわかります。。。 実際、データベースに登録された順番が意味を持つような大規模データなどもなくはないと思います。 そういう場合は、Id列を残しておくことで良い結果をもたらすこともあるでしょう。 しかし、データというものを一般的に考えれば、多くの場合はIdが意味を持つことはなく、推論の邪魔になると考えられるためにId列は削除されることが多いはずです。 このkaggleの学習では、点数を高めるためになんでもする、ということは目指しません。 あくまでもデータ分析一般に活用可能な方法を、一つ一つ学んでいくことを目指しています。 そんな経緯もあり、点数が下がったとしてもId列を削除して分析を進めることにしました。 Next step 次は何をしようか... いくつか試してみて、うまくいった方法で記事を書いてみようと思う References 参考文献 Kaggleで勝つデータ分析の技術 シリーズ一覧 No New Trial Score Link Note 1 xgboost 0.15663 こちら 2 Label Encoding 0.15160 こちら 3 Add and delete column 0.15140 - 本記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Macでpythonを使うときに覚えておいた方がいい備忘録

はじめに Macでpythonを使う時に覚えておいた方がいい備忘録について記載する Windowsは手順が変わると思われるのであくまでMacのみということで限定する pyenv pythonを複数バージョンで使いたいという時に使う 具体的にはbrewから以下の手順でインストールする brewのインストールがまだというかたは以下のサイトを参照し TOPページにあるとても長いコマンドをコピペしてインストールしておく インストールが終わったら brew install pyenv とする この時エラーが私の環境だと出たのでユーザの環境変数のファイル(私はzshを使っていたので.zshrc)に 以下の記載を追加する export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init --path)" pyenvのインストールが終わったらまずは自分が使いたいpythonのバージョンをインストールする 以下のコマンドを実行するとインストールできるバージョンがリストアップされる pyenv install --list 今回は3.6.6をインストールしたいので以下のようにする pyenv install 3.6.6 インストールが終わったら以下のようにバージョンの切り替えができる pyenv global <version名>で覚えておこう pyenv global 3.6.6;python3 -V 現在のインストール状態が知りたい場合は以下の通り pyenv versions 以下のように今使っているバージョンがコメ印で表示される 出力結果 * system (set by /Users/yue-pc/.pyenv/version) 2.7.16 3.6.6 また元に戻したい場合は次のコマンドを実行 pyenv global system;python3 -V
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kindle端末にさくっと転送する方法

概要 Kindle端末上で動作するKindleアプリは、PDFやtxtファイルなどを読めます――つまり購入した電子書籍以外のファイルも読めます――。ところが、ファイル転送にはPCと端末とをケーブルをつなぐ必要がありとても面倒です。そこでもう一つのファイル転送方法、つまりEメールでの転送を利用するとワイヤレスで楽です。本稿では、PCからKindle端末にファイルをGmail経由で転送するあれこれを整理します。Python3 on Windows で動作させます。 01. Amazon側の設定 概要: 各Kindle端末には固有のEmailアドレスが付与されており、そこに添付ファイル付きのメールを送ると、その添付ファイルが読めるようになります。スパム対策のため、送信元アドレスを登録する必要があります。 手順: TOP>アカウントサービス>コンテンツと端末の管理>設定タブ>パーソナル・ドキュメント設定>承認済みEメールアドレス一覧 と進んでいくと、送信元アドレスが登録できます。自分のGmailアドレスを登録しましょう。また、端末固有のEメールアドレスが表示されるので控えておきましょう。 確認: 自分のGmailから上記端末固有のアドレスに添付ファイルをつけてメールしてライブラリに登録できることを確認しましょう。動作確認なのでtxt形式でよいです。 02. Google側の設定 概要: Gmail送付スクリプトを作るための下準備をします。プロジェクトを作成しAPIを有効化し、OauthクライアントIDを登録し、テストユーザとして自分を登録し、アプリの認証情報をダウンロードします。 概念の理解: これから作るスクリプトは、Googleの認証基盤を通じてスクリプト利用者からGmail送付権限を委譲され、利用者の代わりにAPIを叩くことでメールを送付します。Gmail送付権限はセンシティブな権限なので審査が必要ですが、利用者が自分だけであるためテストユーザとして自分を登録すれば審査をスキップできます。 手順: 一言で言えば新規プロジェクトを作ってGmailAPIを有効化しOauthIDクライアントを作りテストユーザとして自分を登録します。具体的には以下の手順です WebブラウザでGoogleにログインした状態で https://console.cloud.google.com/flows/enableapi?apiid=gmail にアクセスする スクリプトはどこか一つのプロジェクトに紐づけする必要があるので、このスクリプト用に新規に作成します(画面上でその旨選択できる) アクセスするデータの種類は「ユーザデータ」を選択。 アプリ名は「Kindle送付用」とか適当に。 スコープは省略(スクリプト内で明示する) アプリケーションの種類は「デスクトップアプリ」 するとクライアントIDが作成されるので認証情報をダウンロードする GCPのAPI管理ダッシュボードに画面遷移するので「OAuth 同意画面」タブをクリックし、「テストユーザ」項目から自分のメールアドレスをテストユーザに登録する 確認: 上記手順によりダウンロードした認証情報(名前はclient_secret_xxxxx.json) をスクリプトと同階層に移動させます。 03. スクリプトの作成 以下、開発者用Gmailドキュメント、特に https://developers.google.com/gmail/api/quickstart/python や https://developers.google.com/gmail/api/guides/sending あたりを読むと詳細や記述の正当性を判断できると思います。 まずはライブラリのインストール: pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib スクリプトは二つの変数を個人ごとに書き換えればよいように作りました。 APP_SECRET: 上述のGoogle上のアプリ用の認証情報のjsonのパスです KINDLE_ADDRESS: Kindle端末のアドレスです スクリプトと client_secret_xxxxx.json を同じディレクトリに配置して、コマンドライン引数に送信したいファイルを指定することでいけます。 import os, sys, base64, mimetypes from email.mime.text import MIMEText from email.mime.image import MIMEImage from email.mime.audio import MIMEAudio from email.mime.application import MIMEApplication from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials APP_SECRET = "client_secret.json" KINDLE_ADDRESS = "XXXXXXXXX@kindle.com" def main(filepath): service = auth() msg = create_message(KINDLE_ADDRESS, filepath) service.users().messages().send(userId="me", body=msg).execute() print ("Mail Sent.") def auth(): scopes = ["https://www.googleapis.com/auth/gmail.send"] scriptdir = os.path.dirname(os.path.abspath(__file__)) secret = os.path.join(scriptdir, APP_SECRET) token_path = os.path.join(scriptdir, "token.json") creds = None if os.path.exists(token_path): creds = Credentials.from_authorized_user_file(token_path, scopes) if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file(secret, scopes) creds = flow.run_local_server(port=0) with open(token_path, 'w') as fh: fh.write(creds.to_json()) return build('gmail', 'v1', credentials=creds) def create_message(to, file): message = MIMEMultipart() message["to"] = to message["subject"] = "" message.attach(MIMEText("")) content_type, encoding = mimetypes.guess_type(file) if content_type is None or encoding is not None: content_type = 'application/octet-stream' main_type, sub_type = content_type.split('/', 1) with open(file, 'rb') as fp: if main_type == 'text': raw = fp.read() try: txt = raw.decode("utf8") except: txt = raw.decode("sjis") msg = MIMEText(txt, _subtype=sub_type) elif main_type == 'application': msg = MIMEApplication(fp.read(), _subtype=sub_type) elif main_type == 'image': msg = MIMEImage(fp.read(), _subtype=sub_type) elif main_type == 'audio': msg = MIMEAudio(fp.read(), _subtype=sub_type) else: msg = MIMEBase(main_type, sub_type) msg.set_payload(fp.read()) filename = os.path.basename(file) msg.add_header('Content-Disposition', 'attachment', filename=filename) message.attach(msg) return {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode() } if __name__ == "__main__": main(sys.argv[1]) 04. おまけ スクリプト作成時に気づいた点などを記載します create_message() でMIMEメッセージを構築していますが、わざと Fromフィールドを空にしています。自身のGmailアドレスをここに入れてしまうと、Gmailがなりすましではないかと疑ってきます。空であってもGoogleが勝手に補完してくれるので入れない方がよいでしょう 公式のサンプルが動きません。これはbase64.urlsafe_b64encodeが文字列ではなくバイト列を要求するからです(Python2時代の遺産?)なのでcreate_message()の最後の行のようにバイト列にしてbase64エンコードしてから、それを文字列に変換しています テキストファイルのデコードが必要ですがファイルのエンコーディング判定がむりくりです。私の用途ではよいのですが改良の余地があると思います あとはこのスクリプトを呼ぶようbatファイルを用意してあげれば、batファイルに添付ファイルをドラッグ&ドロップするだけでKindle端末に登録されるはずです。一応私の作ったバッチファイルを記載します: set current_dir=%~dp0 C:\Users\XXXX\Anaconda3\python.exe %current_dir%send_to_kindle.py %1 pause
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

驚くほど簡単なラズパイでの格安モーターコントロール(IoT)

1.はじめに IoTでは必須の「アクチュエーター」の基礎としてのモーターコントロールです。格安ステッピングモーターを前から持っていましたが、あまり使っていなかったところ、ラズパイで動かしてみようと思いたちました。ライブラリを探してみたところ、とても簡単なpythonコードで動かせることがわかりました。 ラズパイをお持ちの方は、モーターが使えると「できること」が大きく広がってくると思いますのでぜひトライしてみてください。面白いですよ。以下の内容の実践は自己責任でお願いします。 2.今回使うモーター ユニポーラ ステッピングモーター 28BYJ-48という数百円で売られているモーターを使います。 あと、モータドライバモジュールとしては、ZC-A0591を使います。(アマゾンなどでは、セットで販売されているようです) 3.Pythonのモジュール ステッピングモータの理論的なところ、原理は、Youtubeなどでわかりやすく解説されているので、もし時間があれば、一度みておくことをお勧めします。(例えば以下。英語です) その上で、今回はRpiMotorLibというモジュール(Python)を使いたいと思います。以下のGitHubサイトに丁寧な解説があります。(英語) それでは、このライブラリをRasberryPiにインストールしておきましょう。 $ pip3 install RpiMotorLib 4.ステッピングモータとRasberry Piの結線 ラズパイのGPIOとモータードライバーのIN1~IN4をつなぐとともに、モータードライバーに電源を供給します。 以下、結線の例です。 実際に結線した時の画像がこちら↓ 5.Pythonコード モータを動かすプログラムをPythonで書いて実行します。コード例を以下に示します。 import time import RPi.GPIO as GPIO from RpiMotorLib import RpiMotorLib # 先ほど結線した際に使ったGPIOピンの番号をIN1〜In4まで順にリスト形式で定義しておきます。 GpioPins = [17, 18, 27, 22] f = 3 # 回転速度 [Hz] Rev = 1 # 1回転に設定します s_angle = 5.625 # ステップ角 [deg]を設定します。これはモーターによって異なります wait = (1/f)*(s_angle/360)/2 # パルス間の時間を定義しておきます # モーターの名前をつけて、モーターの型式を指定します。(ここでは、28BYJですね。). mymotortest = RpiMotorLib.BYJMotor('Mymotor', '28BYJ') # 引数は、①GPIOピン,②wait,③ステップ数,④回転方向,⑤実行中の各PINの動作表示の要否,⑥ステップモード,⑦最初の遅延時間の設定 [ms] mymotortest.motor_run(GpioPins, wait, Rev*512, True, False, 'half', 0.05) # 上記が終了するとPINをクリーンアップして終えます。 GPIO.cleanup() 6.終わりに どうでしたか?たったこれだけのプログラムでモーターが1回転してくれましたね。ぜひ、プログラムのパラメーターを変えてみて、いろいろ試していただければと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サウナイキタイの口コミ(サ活)をコーパス分類する

1.はじめに 筆者は、Aidemy PremiumPlanデータ分析コースを受講し、本記事は最終課題の成果物をテーマとしたブログ作成となっています。今回は自分の興味があるもので作成しました。次回以降はより難易度の高いものに挑戦していきたいと思います 2.簡単な概要 筆者はサウナが大好きであり、週に2~3日ほどサウナに行くミドルサウナーです。(浴室なしのアパートで家賃を抑えて、毎日行けないか検討しています) 良いサウナに出会うには、サウナ検索サイト「サウナイキタイ」は欠かせません。 サウナイキタイで人気の施設は日々サ活(口コミ)更新されて賑わっていて、 サ活の内容で施設の特徴がなんとなく分かります。(例えば、昭和ストロングといえば100℃を超える高温でカラッカラのドライサウナということが読み取れます。) そこで今回は、施設毎のサ活情報で機械学習をしたモデルを作成し、新しいサ活に対してどの程度施設を分類出来るか検証してみました。 3. 使用環境 GoogleColaboratory こちらは環境構築は不要でブラウザ上で実行出来ます。 まだ仮想環境の構築が出来ていないのでさくっと試すのもとても便利です。 4.実装 まずは、5つのサウナ施設のサ活をrequests、BeautifulSoupによってクローリング、スクレイピングし、txt形式で保存しました。 他の記事でも言われていますが、クローリングやスクレイピングでデータにアクセスする時には、相手のサーバーに負荷をかけることは避ける必要があります。アクセス集中によりサービス妨害の攻撃みなされることがあります。 基本は以下のコードでリクエストの間隔を空けるようにしたほうがいいようです。 timeモジュールのインポート import time #引数には任意のステップで待機する時間を記述 time.sleep() それでは実際のコードを見ていきましょう。 scraping.py # 必要なライブラリのインポート import requests from bs4 import BeautifulSoup import os # はじめに、ベースとなる1ページ目のURLを定義する base_url = "http://scraping.aidemy.net" #saunas/店舗の固有番号/posts(サ活一覧ページ) url_lists = ["https://sauna-ikitai.com/saunas/2303/posts",#各務原温泉 恵みの湯 "https://sauna-ikitai.com/saunas/1551/posts",#馬橋湯 馬橋バスセンター "https://sauna-ikitai.com/saunas/1708/posts",#カプセルホテルレインボー本八幡店 "https://sauna-ikitai.com/saunas/1523/posts",#湯乃泉 草加健康センター "https://sauna-ikitai.com/saunas/4044/posts"]#サウナと天然温泉 湯らっくす # リスト内のHTMLを取得 for url_list in url_lists: # スクレイピング対象の URL にリクエストを送り HTML を取得する response = requests.get(url_list) # BeautifulSoupによるHTMLのパース処理 soup = BeautifulSoup(response.text, "lxml") # サウナ施設の固有番号を取得 path = url_list.split("/")[-2] # サ活一覧の最後のページリンクを取得 last_page = soup.find_all("li",class_="c-pagenation_link")[-2] # 最後のページ番号を取得 last_page = int(last_page.a.get("href").split("=")[-1]) # ディレクトが存在するか確認し、なければディレクトリを作成する if not os.path.isdir(path): os.makedirs(path) # サ活一覧の1~7ページ分を取得する for i in range(1,min(last_page,8)):#1~7ページまで search_url = "https://sauna-ikitai.com/saunas/{}/posts?page={}".format(path,i) # スクレイピング対象の URL にリクエストを送り HTML を取得する response_page = requests.get(search_url) # BeautifulSoupによるHTMLのパース処理 soup_page = BeautifulSoup(response_page.text, "lxml") # 各ページの上位サ活順にラベル付け index=0 # サ活のテキスト情報を全て取得 boxes = soup_page.find_all("p",class_="p-postCard_text") for box in boxes: s = box.get_text(strip=True) #strip=Trueでテキスト内の改行や空白文字を削除 #ラベル番号の更新 index+=1 #サ活に何も書いてない、チェックイン以外を保存 if len(s)>0 and s!="チェックイン": #ファイル名を、サウナ施設の固有番号/ページ数_ラベル番号で保存 with open("{}/{}_{}.txt".format(path,i,index), mode='w') as f: f.write(s) ディレクトリには以下のようにtextファイルが保存されています。 クローリングとスクレイピングについては以下の記事が参考になりました。 Python,Requestsの使い方 10分で理解するBeautifulSoup 次に、空のリストを作成し、取得したテキストを改行文字で分割して、['文書1', '文書2', '文書~~3', ... ]のようにリストの中に加えていきます。 predict.py #必要なライブラリのインポート import glob #カテゴリ辞書を定義 def load_sauna_text(): category = { 'yunoizumi': 1, 'meguminoyu': 2, 'rainbow-motoyawata': 3, 'yulax': 4, 'mabashi-yu': 5, } # 空の配列を準備 docs = [] labels = [] # 全てのカテゴリのディレクトリについて実行 for c_name, c_id in category.items(): files = glob.glob( "/content/drive/MyDrive/text/{c_name}/*.txt".format(c_name=c_name)) # 空の変数を準備 text = '' for file in files: with open(file, 'r', errors='ignore') as f: #文字列+splitlines() :デフォルトで改行文字があった場合は分割してリストとして返 lines = f.read().splitlines() text="".join(lines) #textをdocsに追加 docs.append(text) #C_idをlabelに追加 labels.append(c_id) return docs, labels docs, labels = load_sauna_text() 次にリストの中に入れた日本語の文章を形態素に分解します。 形態素とは、意味を持つ表現要素での最小単位です。 例えば、「隣の客はよく柿食う客だ」という文章を形態素に分解すると、 「隣/の/客/は/よく/柿/食う/客/だ」に分解が出来ます。 形態素に分解する事で不要なワードを除き、重要なワードを抽出することが出来ます。 しかし、形態素まで分解することはしてもまだモデルに学習させることは出来ません。 そこでベクトル化(数字のリスト)にする必要があります。 文章のベクトル化 文章中の単語の出現頻度に表現するBag of Word(BOW)を使います。 BOWにはカウント表現という文章中にある単語の出現回数を要素として使用したベクトルに変換する方法があります。 しかし、カウント表現の問題点としては、「です」や「ます」のような、カテゴリ分類において重要ではない単語の値が大きくなってしまうことがあります。 そこで、今回はtf-idf表現という手法で計算された、文章中の各単語の重み情報を扱う方法を使います。 tf-idfは各文書毎での単語の出現頻度である tf(Term frequency) と、特定の単語が含まれる文書の頻度の逆数idf (Inverse Document Frequency) の 積で表されます。 tf-idfを使うと、多くの文書に出現する語(一般的な語)の重要度を下げ(tf-idf値が、特定の文書にしか出現しない単語の重要度を上げる役割を果たします。これにより、「です」や「ます」などの値が小さくなり、正しく重要度を設定することができます。 参考記事:【Python】自然言語処理で使われるTF-IDFと単純ベイズ分類器(Naive Bayes)について使いながら解説する 以下が形態素に分解し、tf-idf による重み付けを行うコードになります。 ますは、前処理の効果を確認する為、まずは前処理をせずに(文章をそのまま使用する)コードを作成します。 predict.py # indices は0からドキュメントの数までの整数をランダムに並べ替えた配列 random.seed() indices = list(range(len(docs))) # 9割をトレーニングデータとする separate_num = int(len(docs) * 0.9) random.shuffle(indices) train_data = [docs[i] for i in indices[0:separate_num]] train_labels = [labels[i] for i in indices[0:separate_num]] test_data = [docs[i] for i in indices[separate_num:]] test_labels = [labels[i] for i in indices[separate_num:]] # テキストを分割する関数 t = Tokenizer() def tokenize1(text): tokens = t.tokenize(",".join(text)) noun = [] for token in tokens: noun.append(token.surface) return noun # Tf-idfを用いてtrain_dataをベクトル化 vectorizer = TfidfVectorizer(tokenizer=tokenize1) train_matrix = vectorizer.fit_transform(train_data) ナイーブベイズとランダムフォレストによる学習 今回はナイーブベイズとランダムフォレストで学習を行います。 ナイーブベイズは、ベイズの定理を用いてある文章をカテゴリ分けする際に、テキスト中の単語の出現率を調べます。 その際に文章がどのカテゴリに分類するのが相応しいか調べます。 参考記事:[AI・機械学習の数学]機械学習でよく使われる「ベイズの定理」を理解する【Python】自然言語処理で使われるTF-IDFと単純ベイズ分類器(Naive Bayes)について使いながら解説する ランダムフォレストは、少しずつ異なる決定木をたくさん用意し、アンサンブル学習のバギングによって過学習をしてしまう度合いを減らすことが出来ます。 決定木とは、クラス分類、回帰タスクに広く用いられて、Yes/Noで答えられる質問で構成された階層的な木構造を学習します。決定木の深さに制約を与えないと、決定木はいくらでも深く複雑になり、過学習をしやすく汎化性能が低い傾向にあります。 決定木をたくさん用意したランダムフォレストの回帰であれば各決定木の予測値の平均、分類であれば多数決で予測を行います。 参考記事:【機械学習】ランダムフォレストを理解する predict.py # ナイーブベイズを用いて分類を行う clf = MultinomialNB() clf.fit(train_matrix, train_labels) # ランダムフォレストを用いて分類を行う clf2 = RandomForestClassifier(n_estimators=100) clf2.fit(train_matrix, train_labels) # テストデータを変換 test_matrix = vectorizer.transform(test_data) 次に前処理として文章中で重要そうな品詞データのみを抽出して予測精度を求めてみます。 今回は、「名詞」、「動詞」、「形容詞」、「形容動詞」の形態素を抽出しました。 predict.py # 単語の抽出 def tokenize2(text): tokens = t.tokenize(text) noun = [] for token in tokens: # 「名詞」「動詞」「形容詞」「形容動詞」を取り出す partOfSpeech = token.part_of_speech.split(',')[0] if partOfSpeech == '名詞': noun.append(token.surface) if partOfSpeech == '動詞': noun.append(token.surface) if partOfSpeech == '形容詞': noun.append(token.surface) if partOfSpeech == '形容動詞': noun.append(token.surface) return noun # 単語の抽出して学習 t = Tokenizer() vectorizer = TfidfVectorizer(tokenizer=tokenize2) train_matrix = vectorizer.fit_transform(train_data) test_matrix = vectorizer.transform(test_data) clf.fit(train_matrix, train_labels) clf2.fit(train_matrix, train_labels) # 結果を表示 # 分類結果を表示 print("前処理を行っていないデータ") print("ナイーブベイズによる学習結果") print(clf.score(train_matrix, train_labels)) print("ナイーブベイズによる評価用データの予測精度") print(clf.score(test_matrix, test_labels)) print("ランダムフォレストによる学習結果") print(clf2.score(train_matrix, train_labels)) print("ランダムフォレストによる評価用データの予測精度") print(clf2.score(test_matrix, test_labels)) print("前処理を行ったデータ") print("ナイーブベイズによる学習結果") print(clf.score(train_matrix, train_labels)) print("ナイーブベイズによる評価用データの予測精度") print(clf.score(test_matrix, test_labels)) print("ランダムフォレストによる学習結果") print(clf2.score(train_matrix, train_labels)) print("ランダムフォレストによる評価用データの予測精度") print(clf2.score(test_matrix, test_labels)) 前処理を行っていないデータ ナイーブベイズによる学習結果 0.4166666666666667 ナイーブベイズによる評価用データの予測精度 0.34615384615384615 ランダムフォレストによる学習結果 1.0 ランダムフォレストによる評価用データの予測精度 0.75 前処理を行ったデータ ナイーブベイズによる学習結果 0.9572649572649573 ナイーブベイズによる評価用データの予測精度 0.8269230769230769 ランダムフォレストによる学習結果 0.9978632478632479 ランダムフォレストによる評価用データの予測精度 0.7884615384615384 前処理により不要な単語の影響を少なくすることで予測精度を上げることが出来ました。 パラメーター調整していない状態では、ナイーブベイズでの予測精度は、「83%」、ランダムフォレストでの予測精度は、「79%」という結果になりました。 5. 最後に 最後まで読んでいただきありがとうございます。 株価の予測などで特徴量をダミー変数化して...というようなデータ分析も良かったかもしれないのですが、まずは自分のモチベーションを保つ為、 興味のあるテーマで慣れていこうと思います。 今回は急いで仕上げたため、質の良いものとはいえないと思いますが、 日々キャッチアップした内容からブラッシュアップしていきたいと思います。 次回は「自分の好きなサウナ施設のサ活情報をベクトル化により類似度を算出してどのサウナ施設が最も近いか」ということをやっていきたいと思います。 今回は時間の関係でwordcloudによる可視化が出来なかったので合わせて作成したいと思います。 今回作成したコードはgithubの使い方に慣れる目的も兼ねてアップしておきます。 https://github.com/YushiAkahori/sauna-classData
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【YOLOv5】Google ColabでYOLOv5を使って物体検出を試してみる

はじめに 物体検出を試してみる方法について備忘録としてまとめました。 Google Colaboratory上でYOLOv5を使用して、物体検出を試していきます。 今回の方法ではGoogleアカウントがあれば誰でも同じように試すことが可能です。 (Qiita初投稿です。わかりにくい箇所があるかと思いますがご容赦ください。) Google Colaboratoryの設定 Googleアカウントを作成し以下のリンクから開始します。 物体検出とは 物体検出(object detection)とは、画像内の「どこに」「何が」写っているかを検出する技術のことです。 物体検出としては、SSDやYOLOといったものがよく使用されます。 YOLOv5とは YOLOv5とは、物体検出をするアルゴリズムですがYOLOv3の後継にあたり、2020年に公開された最新のモデルです。YOLOv4という高精度化したYOLOv3の後継モデルもありますが、YOLOv5は推論処理時間がより速くなっているのが特徴です。 YOLOv5の準備 Google Colaboratoryを起動したら順番にやっていきます。 YOLOv5をインストールします。 python.py !git clone https://github.com/ultralytics/yolov5 %cd yolov5/ !pip install -qr requirements.txt 以上で準備は終了です。 実際に物体検出してみる 以下のコードを実施するだけ物体検出が可能です。 python.py !python detect.py --source /content/yolov5/data/images/zidane.jpg 結果を表示してみます。 python.py import matplotlib.pyplot as plt img = plt.imread('/content/yolov5/runs/detect/exp/zidane.jpg') plt.imshow(img) plt.show() 以下の画像が表示されたら成功です。 それぞれ検出した範囲に枠付けがされ、左上に検出した物体名と確信度が表示されます。 確信度とは物体検出の精度がどのくらい確実であるかの指標で0~1の間の値をとります。 この値が1に近いほど機械が自信を持って判定していることになります。 任意の画像で物体検出してみる サンプル画像以外でも任意の画像で物体検出が可能かどうか試してみます。 実際に物体検出したい画像を/content/yolov5/data/images/にbus.jpgを格納します。 (bus.jpgはサンプル画像として元から入っているが、ここでは自分で追加したものとして扱う) 格納した画像のファイル名として以下のコードを実行します。 python.py !python detect.py --source /content/yolov5/data/images/bus.jpg 先ほどと同様に結果を確認します。 python.py import matplotlib.pyplot as plt img = plt.imread('/content/yolov5/runs/detect/exp2/bus.jpg') plt.imshow(img) plt.show() 任意の画像でも物体検出ができました。 終わりに 最後までご覧いただきましてありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習による株価予想の十八手

概要 株式投資家にとっての当たり前を機械学習アルゴリズムに落とし込むための18の手法を紹介する。 この手法を応用して、Signate主催のコンペで9位(金メダル)を獲得。(ただし、本記事投稿時点では入賞者候補確認中のため、最終順位は変わる可能性あり。) 手法一覧 No. 株式投資の当たり前 機械学習アルゴリズムでの工夫 1 株価はサプライズによって動く サプライズ度合いを特徴量にする 2 業績数値の単純な変化率では株価インパクトは測れない 変化率の分母を企業規模に相当する変数にする 3 インサイダーはいて当たり前と思う 「理由なき上昇」が好決算発表前にあったかを見抜く 4 予想発表時にすべての業績数値を発表するわけではない 一部の業績指標がnull値だった場合、その変化率を他の指標の変化率で補完する 5 会社業績には季節性がある 業績の季節性偏りを平準化し、年換算する 6 過去の数値より未来の数値が大事 決算と来期予想が同じタイミングで発表されたら、来期予想を重視する 7 業績予想に対する強気度は企業ごとに異なる 業績予想の信頼度を定量化する 8 四半期決算と通年予想が矛盾しているときがある 四半期進捗度から、通年予想を予想する 9 会計基準が変更になると数値の意味が変わる 会計基準が同一の数値間でのみ比較する 10 決算期が変更されるときがある 短縮された通年を12か月で換算する 11 まったく価値観の異なる業種がある 特定業種銘柄フラグを設ける 12 権利付き最終日前に株価は上がり、権利付き最終日後に株価は下がる 権利付き最終日を特定し、前後の日付かの判定をする 13 配当には普通配当と記念配当がある 記念配当は一時的なので、価値を間引く 14 配当とともに株主優待がもらえる銘柄がある 株主優待も金銭価値に換算する 15 株式分割・併合が起こる 3つの落とし穴に対処する 16 売りが売りを呼ぶときがある 下落率が閾値を超えた場合をフラグ化する 17 チャート理論に基づいて株価が動くことがある チャート理論をアルゴリズム化する 18 個別銘柄の株価はマクロ相場の影響を大きく受けるが、マクロ相場の予測は困難 マクロ影響を除去した目的変数にする 1. 株価はサプライズによって動く 株式相場には常にプロの投資家がうごめいており、各銘柄の各種業績数値を常に予想して投資活動をしている。そんな状況下において、仮に「売上が前年比2倍」という決算が発表されても、株価が2倍になるわけではない。むしろ3倍が予想されていたのに、2倍だったら失望売りとなる。つまり事前予想と比較してこそ意味があり、staticな値や過去実績との比較を特徴量にすることはあまり意味がない。事前予想と決算の乖離、または前回予想と今回予想の乖離こそが意味のある特徴量であると言える。 2. 業績数値の単純な変化率では株価インパクトは測れない 営業利益の事前予想100億円に対し、決算が200億円の場合、 ( 実績 - 予想 ) / 予想 の計算式を使うと、変化率は100%となる。この変化率を特徴量にするのは一見もっともらしいが、株においてはこれは使いづらい。 営業利益の事前予想1億円に対し、決算が200億円の場合、変化率は19900%となる。 営業利益の事前予想0億円に対し、決算が200億円の場合、変化率は無限大となる。 営業利益の事前予想▲100億円に対し、決算が200億円の場合、変化率は-300%となる。 これらは明らかに、決算が与えたインパクトを表現できていない。 実績と予想の差額であるサプライズ金額の重みが銘柄によって異なるので、それを補正するような適切な分母を考えるべきである。時価総額だったり、数年間の売上の平均値だったり、企業規模を類推できるような数値を分母に据えることが望ましいと考える。 3. インサイダーはいて当たり前と思う インサイダー取引は処罰の対象であるが、実際に株取引をしていると、何の材料も出ていないのに株価が急騰する場面によく出くわす。明らかにどこかで情報が漏れており、不当に利益を得ている方々がいるのであろうと思わざるを得ない。例えば好決算発表が事前に漏れている場合には、好決算発表とともインサイダーによる売り浴びせが待っている可能性がある。 であれば、インサイダーの存在も織り込んでアルゴリズムを組む必要がある。つまり、好決算発表前に「理由なき」大幅上昇がなかったかを見抜く必要がある。 4. 予想発表時にすべての業績数値を発表するわけではない 決算短信において、事前に業績予想を発表するときがあるが、このときに営業利益の予想は出しても、経常利益の予想は出さないときがある。つまり、データとしては欠損値が発生することになる。これを欠損値のまま放置する手もあるかもしれないが、機械学習に落とし込むときには使い勝手が悪い。最悪の場合、せっかくの営業利益のデータまで過小評価されてしまうリスクがある。このような場合には他の業績数値の変化率で置換をするというのが手っ取り早くもっともらしいと考える。例えば、営業利益が前期比2倍だった場合、経常利益も前期比2倍だろうと類推し、その数値で欠損値補完をするのである。 どの業績数値をどの業績数値で補完すればよいかは悩むところであるが、以下の並び順で考えて、隣接している上位の数値で補完するのがよいと考える。 売上⇒営業利益⇒経常利益⇒最終利益 5. 会社業績には季節性がある Q1の売上が100億円だったからと言って、年間売上予想が400億円前後だろうとは言えない。企業ごとに業績には季節性があり、例えばQ1の売上がQ2の1.5倍程度であることが多いという傾向がある。逆に言えば、企業の年間の業績推移の傾向を捉えて係数化しておけば、四半期決算から年間決算を予想することも可能になるのである。 6. 過去の数値より未来の数値が大事 決算発表前は今期の決算はどうなるかとやきもきするのだが、いざ決算が発表されると、前期はもういい、大事なのは来期だ、と急に価値観が切り替わる。株価とは常に現在から将来にわたる企業価値を反映するものだからである。もちろん、前期の決算があまりにひどいと、資金繰り悪化で破綻するリスクが高まったり、将来展望も下方修正するリスクも出てくるので無視はできないが、決算と来期予想が同時に発表される場合には、来期予想を重視するほうがよい。 7. 業績予想に対する強気度は企業ごとに異なる 同じ売上予想100億円でも、A社とB社が発表した場合で市場の捉え方は異なることがある。A社は強気な予想をしがちで後々下方修正することが多い、B社は保守的な予想をしがちで後々上方修正することが多い、と経験則から判断する投資アナリストは多いのである。実際の決算がどうなれど、予想発表時点でそのようなフィルタをかける市場関係者が多いのであれば、それもファクターとして考慮すべきである。各企業の過去の予想と実績の傾向を分析し、予想の信頼度を定量化すべきである。 8. 四半期決算と通年予想が矛盾しているときがある Q3の利益が予想の3倍などとサプライズ決算が出たにも関わらず、「通年の予想は変更無し」としれっと書いている決算短信をみかける。これは本当に通年の業績に影響がないのではなく、通年予想の更新が間に合わなかったか、あるいは意図的に変更をしなかったという場合が多いと見ている。実際にその後すぐに通年の上方修正をするというケースがある。であれば、四半期決算に基づいて、通年予想が修正されることを先回りして予想するという戦術が有効となる。 9. 会計基準が変更になると数値の意味が変わる 昨年と本年で会計基準が変更になったことで、突然最終利益が数十パーセント増加、ということがある。これを額面どおりに取り入れてしまうと、会社の業績は好調だから買い、というような判断になりかねない。連続性を持たせてはいけないのであり、会計基準はGroupByのキーのひとつに組み込むべきである。つまり、会計基準が変わったら、もうそれは別の会社の決算と思った方が無難だと考える。 10. 決算期が変更されるときがある 機械学習エンジニアには迷惑な話であるが、決算期(決算する月または日)を変更するときがある。これまでは6月が決算期だったのに、今年から3月に変更となった場合には、今年の業績は9か月分しかない、となる。月数や日数で補正をして12か月分に換算しなおさないと計算が狂ってしまうのである。 11. まったく価値観の異なる業種がある 例えば、創薬ベンチャーのように、毎年大幅赤字の連続だが株価は絶好調というような特異な業種が存在する。創薬ベンチャーの場合には、一発当てるという長期的な夢に基づいて株価が形成されているのであろう。このような特異業種を通常の業種と混ぜてしまうと、法則が狂ってしまう。one-hotベクトルなどで特別扱いするか、初めから分析対象から外すなどの対策が必要となる。 12. 権利付き最終日前に株価は上がり、権利付き最終日後に株価は下がる 配当を設定している銘柄であれば、権利付き最終日に株を保有しているだけで配当金がもらえる。(配当金額の決定は決算発表時なので、権利付き最終日時点では本当にもらえるかは不明であり、予想配当金額を信じるしかないのだが。) となれば、普段は保有していなくともその日だけ保有しようというにわか投資家がやってくる。配当欲しさに多少の高値でも株は買われるので、株価は上がることが多い。なるべく安値で仕入れたいと思う投資家は権利付き最終日より少し前から仕込んでおこうとなるので、結果的に権利付き最終日以前の一定期間は株価が緩やかに上昇する傾向もある。逆に権利付き最終日を過ぎたら、配当がもらえるだろうと見込んで、多少の安値で売却しても元がとれると思い、株は売られる。配当金額が大きい銘柄ほど株価の下落幅は大きくなる。これはかなり手堅い法則であるため、アルゴリズムにも組み込む必要がある。少なくとも、今日という日は権利付き最終日にどれだけ近いのか、前なのか後なのかは考慮すべきファクターとなる。 上記実現のために、面倒ではあるが、東証の休日カレンダーを準備する必要がある。月の最終営業日起算で営業日数ベースで権利付き最終日が決まるからである。年末年始など変則的なので、通常の祝日カレンダーでは対応できないし、日本の祝日も毎年のように変わるので、最新の情報を参照しないといけない。 また、2019年7月18日より前は、月の最終営業日から3営業日前が権利付き最終日であったが、2019年7月18日以降は月の最終営業日から2営業日前が権利付き最終日となるので、ロジック対応が必要になる。 13. 配当には普通配当と記念配当がある これまで配当金額が年10円だったのに、突然今年は100円となるときがある。創立100周年記念だから特別に奮発するというような時である。この背景を知らないと、A社の配当は10倍になった、高利回りだから買いだ、という判断をしてしまう。一過性のものであるので、額面どおりに受け取らず、金額の平準化を図るべきである。問題は記念配当なのかの判断が難しいということである。筆者の知る限り、テーブルデータでそのような項目を取得するのは難しく、某情報サイトでスクレイピングをするか、自然言語処理に頼らざるをえない。次善の策としては、ある一定の上昇率や上昇幅であれば、記念配当とみなして価値を間引く、という方法がある。 14. 配当とともに株主優待がもらえる銘柄もある 飲食、観光、交通、小売など、特にBtoC業界においては、配当以外に株主優待を設定している企業は多いが、株主優待目当ての投資家も多い。某有名レストランの食事代金が20%OFFになるクーポンがもらえるとなれば、人によってはそこに1万円の価値を感じ、その優待をもらえる最低の投資額から考えて数千円分の株価下落があろうともトータルで得なので安値でも売るという行動につながる。 であれば、株主優待を金銭的価値に換算し、その金額が大きいほど権利付き最終日後に株価が下がる可能性が高いということも考慮すべきファクターとなる。ただし、これは定量化が非常に難しいタスクである。株主優待のデータを取得し、投資金額に応じた株主優待の種類を把握し、株主優待から各投資家が感じる金銭的価値の期待値を算出しないといけないのである。Quoカード500円がもらえる優待であれば、価値は500円と考えても差し支えないだろうが、「当社カタログの全商品が10%OFF」のような優待の場合、その価値は人それぞれであろう。 15. 株式分割・併合が起こる 保有している株の枚数が、1枚だったのに自動的に2枚になったり(株式分割)、2枚が1枚になったりする(株式併合)。1⇒2の株式分割の場合、通常それが発生した直後は株価は半分になる。トータルの価値は変わらないのである。この仕組みを理解していないとある日突然株価が半分になるという怪奇現象を鵜呑みにすることになり、法則が狂う。たいていの株価データは、過去日付の株価を最新の株式数に応じた数値に補正してくれているものなので、通常は意識しなくてもよいが、落とし穴が3つある。 ひとつ目は、過去に取得した株価データに、新しい株価データを差分として追加する場合。当然過去に取得した株価データは補正されていない状態なので、新しい株価と結合すると連続性が失われる。この場合は再度過去分まで取得しなおすほうが手っ取り早いことが多い。 ふたつ目は、過去分の配当利回りが狂うことである。株式分割に合わせて、過去の配当金額まで補正してくれていればよいのだが、補正をしてくれているデータとしてくれていないデータが世の中には存在する。補正をしていないデータの場合、過去の配当金額は30円で、その時の株価は3000円だったので、当時は利回り1%だったが、株価だけ株式分割で補正されて過去の株価データが1500円に変更されているが、過去の配当金額は30円のままで、利回りは過去は2%だったとなってしまう。このようなデータの場合には自分で補正をせざるを得ない。 3つ目は、値幅制限いっぱいだったのかが不明となることである。株価によって一日の上昇・下落の値幅は決まっているが、株価が補正されていると、その日の株価の動きが値幅いっぱい(ストップ高・安)だったのかどうかはわからなくなる。結局はオリジナルの株価がいくらだったのを計算しなおさないといけなくなる。 16. 売りが売りを呼ぶときがある 株価予測をアルゴリズム化するときには、時系列データとして考える方が多いようだが、筆者はその側面はあまり重視しない。時系列とは突き詰めると、上がったらまた上がる、上がったら次は下がる、という考えに行きつくと思うが、そのような法則は信頼性が低いと思っている。インデックス投資をされる方は、日経平均などは長期的に見れば上昇基調なので、上がり続けることは間違いない、と思っている方も多いようだが、それも所詮は数十年単位での傾向に過ぎず、もう少し長いスパンでどうなるかはデータもないし、かつ人の一生と比較した場合には気長すぎる法則であると考えている。 ただし、株式市場の性質上、短期的な株価の動きが次の株価を決めることはあると考えている。○○ショックと呼ばれるように、下落が止まらなくなる現象が時折発生する。毎日下落幅が倍くらいのペースで数日間下がり続けることがあり、この場合はさすがに前日の株価下落幅が本日の下落の要因であると思わざるを得ない。 このメカニズムの原因の証明は難しいかもしれないが、筆者は信用取引の存在が大きいと考える。信用取引には、買いと売りの2種類があるが、このうち信用買いとは自身の現金または現物株を担保に借金をして株を買う行為である。当然借金なので、借入額に上限がある。現金よりも現物株のほうが担保能力は低い。そして、一定の閾値を超えたら証券会社は強制決済を執行する。そうすると、ある程度マクロ相場が下落すると、売りたくないのに売らざるを得ない投資家が出てくる。彼らが売りを入れると、さらに信用枠に余裕のあった投資家まで売らざるを得なくなる。「パニック売り」「狼狽売り」と呼ばれたりするが、心理的にパニックになるというよりも、仕組みがそうさせているというほうが妥当だと思う。 そうであれば、パニック売りが発生するような条件をアルゴリズム化すればよい。短期的なマクロ相場の値動きがファクターとなりえると考えている。 17. チャート理論に基づいて株価が動くことがある チャーチストと呼ばれる投資家がいる。株価チャートの形状を頼りに投資をしている投資家を指す。「このようなチャートの形状が発生すると、このような値動きになることが多い」というなんとも眉唾モノの法則があるのだが、実際にそれを信じている人が一定数いると、彼らが本当にその理論に基づいて売買をするために、結果的に法則どおりの値動きになってしまう、ということが少なからずあるのだろうと筆者も考えている。 そうであれば、根拠が乏しくも大勢が依存しているチャート理論があれば、それもアルゴリズムに組み込むことが得策となる。 18. 個別銘柄の株価はマクロ相場の影響を大きく受けるが、マクロ相場の予測は困難 どんなに個別銘柄の業績が良かったとしても、相場の地合いには勝てないことが多い。マクロ相場は個別銘柄のパフォーマンスにとって重要なファクターであることは間違いない。しかし、マクロ相場を予測することは非常に困難であると考えている。だったら、むしろマクロ相場を予測することはあきらめたほうがよい。ただし、過去の銘柄の値動きを説明するときにはマクロ相場の影響も考慮しないと誤差が大きくなる。そこで筆者が考える手法は、マクロ相場の値動きを説明変数にするのではなく、目的変数から控除してしまうというものである。 (本来) \ \ \ \ \ \ \ y = a + bx_m + cx_1 + dx_2 (代替案:訓練時) \ \ \ \ \ \ \ y - bx_m = a + cx_1 + dx_2 (代替案:推論時) \ \ \ \ \ \ \ y = a + cx_1 + dx_2 y: 個別銘柄の変化率 x_m: マクロ相場の変化率 x_1, x_2: その他の説明変数 マクロ相場の変化率の期待値は、データが多いほど0に近づくはずであるという仮説のもと、その他の説明変数のみで個別銘柄の変化率との相関関係を炙り出そうという試みである。 実際には、マクロ相場の変化率も、全銘柄の変化率と業種ごとの変化率に分けて訓練したりもする。 実証実験 Signate主催のコンペ「日本取引所グループ ファンダメンタルズ分析チャレンジ」に参加し、上記の手法を応用してみた。決算短信発表後20営業日以内における個別銘柄の最高値・最安値を予測するというコンペである。評価指標がスピアマン順位相関係数であるため、実際には絶対的な株価の予測ではなく、相対的な上昇しやすさ、下落しやすさの予測とも言える。 一部の手法においてはデータ制約から代替案を使用したりもしたが、18の手法をほぼすべて取り入れることができた。 最終順位は9位で、金メダル獲得となった。(総投稿者数211名、参加者数1443人) 上記の手法がある程度有効であると確信を持つことができた。 結果的には、上位者のスコアには及ばないスコアとなったが、筆者のスコアは他者のスコアよりも対象期間が長くなるほど大きく改善するという傾向があったように感じており、より長期の期間の場合には定常性が確認できるのではないか、というポテンシャルを感じ、今後も研鑽していく所存である。 上記コンペのソースコード Signateの検証完了の連絡を受けたら、Githubに公開予定。 株価予測における機械学習の有用性 アルゴリズム株取引はかなりブームであるようだが、筆者は未来永劫有用なモデルが開発できるかどうかには懐疑的である。なぜなら、株価を形成するのは結局市場参加者であり、「この通り売買したら儲かる」というモデルが普及することにより、それが市場参加者の行動を変え、結果として旧来モデルどおりの動きではなくなると信じるからである。複数のAIが自動売買を繰り返す世界ではなおさらである。 しかしながら、現代においてはまだまだ自身の判断に頼る投資家は多く、かつ特に日本市場においてはそれは顕著であると思う。であれば、そこには何らかの法則が残っており、機械学習で儲けのアルゴリズムを見つけ出すチャンスはまだまだあるのではないかと考えている。 株以外のコンペで優秀な成績を出している参加者が、今回のコンペでも優秀な成績を残していることがそれを証明しているように思う。つまり、株は博打ではなく、サイエンスが入り込む余地があるのだ。 興味がある方はJPX(日本取引所グループ)が提供する東証APIサービス等を活用してみるのがよいと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Pyrhon演算処理】同心集合(Concentric Set)について。①乗法的同心集合(Multiplicative Concentric Set)とは?

かつての投稿における「数直線=同心円=同心球面」のイメージより出発します。 【Rで球面幾何学】等差数列(算術数列)②数直線概念から同心円集合概念へ 小学生の時習った数直線(Number Line)概念は、今から思えば原点の一点のみで固定されているだけなので、全体としては同心円/同心球面集合(Concentric Set)を構成するのです。考えてみればこれこそがスカラー(Scalar)概念そのものなんですね。 スカラー (数学) - Wikipedia 最も単純な2次元(xy面)上における同心円構造のイメージは以下となります。 import numpy as np import matplotlib.pyplot as plt import numpy.random as random import matplotlib.patches as patches import matplotlib.animation as animation %matplotlib inline #単位円データ作成 c0=np.linspace(-np.pi,np.pi,61,endpoint = True) s0=[] for num in range(len(c0)): s0.append(complex(np.cos(c0[num]),np.sin(c0[num]))) s1=np.array(s0) #描画準備 fig = plt.figure() ax = plt.axes() def circle_draw(n): plt.cla() plt.title("Concentric Circles") plt.xlabel("X") plt.ylabel("Y") plt.ylim([-4.0,4.0]) plt.xlim([-4.0,4.0]) # 同心円描画 plt.plot(0,0,marker='x', color='green') circle1=patches.Circle((0,0),radius=1, fill=True, color='gray',lw=0.5,alpha=0.5) circle2=patches.Circle((0,0),radius=2, fill=True, color='gray',lw=0.5,alpha=0.2) circle3=patches.Circle((0,0),radius=3, fill=True, color='gray',lw=0.5,alpha=0.2) circle4=patches.Circle((0,0),radius=4, fill=True, color='gray',lw=0.5,alpha=0.2) circle5=patches.Circle((0,0),radius=5, fill=True, color='gray',lw=0.5,alpha=0.2) circle6=patches.Circle((0,0),radius=10, fill=True, color='gray',lw=0.5,alpha=0.2) ax.add_patch(circle1) ax.add_patch(circle2) ax.add_patch(circle3) ax.add_patch(circle4) ax.add_patch(circle5) ax.add_patch(circle6) ax.set_aspect('equal', adjustable='box') #補助線描画 plt.axvline(0, 0, 1,color="black",lw=0.5) plt.axhline(0, 0, 1,color="black",lw=0.5) #移動線描画 plt.plot([0,s1[n].real*6],[0,s1[n].imag*6],color="green",lw=1) plt.plot(s1[n].real,s1[n].imag,marker='x', color='green') plt.plot(s1[n].real*2,s1[n].imag*2,marker='x', color='green') plt.plot(s1[n].real*3,s1[n].imag*3,marker='x', color='green') plt.plot(s1[n].real*4,s1[n].imag*4,marker='x', color='green') plt.plot(s1[n].real*5,s1[n].imag*5,marker='x', color='green') plt.plot(s1[n].real*6,s1[n].imag*6,marker='x', color='green') #circle_draw(1) #plt.show() ani = animation.FuncAnimation(fig, circle_draw, interval=50,frames=len(s1)) ani.save("circle_draw60001.gif", writer="pillow") このアニメーションで回転している線(数列)の定義は(それ自体は観測対象とならない)観測原点(Observation Origin)0を下限、観測限度(Observation Limit)を上限とする正の実数(非負実数)の開集合で、これまでの投稿では「自然数(Natural)を十進法概念導入によって実数列化した数列」と説明してきました。【数理考古学】とある実数列の規定例①等差数列から加法整数群へ自然数同様、単位元も逆元も完備していませんが、どうしてそうなるかはその振る舞いを見ても一目瞭然としてきました。【数理考古学】とある実数列の規定例③オイラーの等式が意味するもの? どうやらとうとう、この部分から見直しを図らざるを得なくなった様なんです。 乗法的同心円/球面集合(Multiplicative Concentric Set) これを二次元極座標(r,φ)形式や三次元極座標(r,θ,φ)形式に拡張してみましょう。その全体像はスカラー=半径rと水平角φと垂直角θの関係関数として構成される形となります。 Python (SymPy) で方程式・連立方程式を解く、数列を求める SymPy による数式処理 ところで、これまでの投稿では角度を表すのに以下の関数を使ってきました。atan2 - Wikipedia - ウィキペディア \begin{align*} &φ =\frac{\pi}{2} - \operatorname{atan_{2}}{\left(x,y \right)}\\ &θ =\frac{\pi}{2} - \operatorname{atan_{2}}{\left(\sqrt{x^{2} + y^{2} + z^{2}},z \right)}\\ &θ =\frac{\pi}{2} - \operatorname{atan_{2}}{r,z}\\ \end{align*} 以降の投稿では符号関数の概念を導入します。符号関数(sign function) \mathrm{sgn}x=\left\{\begin{matrix} 1\quad (x \gt 0)\\0\quad (x=0)\\ -1\quad (x \lt 0)\end{matrix}\right. # モジュールをインポート import numpy as np import matplotlib.pyplot as plt # フィギュアを設定 fig = plt.figure() # グラフ描画領域を追加 ax = fig.add_subplot(111) # グリッド線を表示 ax.grid(linestyle = "--") # グラフタイトルを設定 ax.set_title("Sign Function", fontsize = 16) # x軸, y軸のラベルを設定 ax.set_xlabel("x", fontsize = 16) ax.set_ylabel("y", fontsize = 16) # 区間 [-5, 5] を64分割 x = np.linspace(-5, 5, 64) # y = sgn(x) y = np.sign(x) # 符号関数のグラフを描画 ax.plot(x, y, color = "darkblue") *結果として、使用関数は以下の様に修正される事になります。 \begin{align*} &φ = \operatorname{atan_{2}}{(y,x)} \operatorname{sign}{(y)} \\ &θ = \operatorname{atan_{2}}{(z,\sqrt{x^{2} + y^{2} + z^{2}})}\\ &θ = \operatorname{atan_{2}}{(z,r)}\\ \end{align*} 【Python演算処理】単位球面(Unit Shere)を巡る数理①とりあえず描画してみる。 二次元空間(円弧)におけるデカルト座標系(x,y)と極座標系(r,φ)の相互変換 原点(0,0)からの距離$r=\sqrt{x^2+y^2}$ $φ=siqn(y)\arctan^2(y,x)$ $x=r×\cos(φ)$ $y=r×\sin(φ)$ import sympy as sp import numpy as np from sympy import I, pi, E x,y,r,φ= sp.symbols('x,y,r,φ') #デカルト座標系(x,y,z)と極座標系(r,φ,θ)の相互変換 eq01=sp.Eq(r,sp.sqrt(x**2+y**2)) eq02=sp.Eq(φ,pi/2-sp.atan2(x,y)) eq03=sp.Eq(x,r*sp.cos(φ)) eq04=sp.Eq(y,r*sp.sin(φ)) #単位円(半径r=1)の場合 eq11=eq01.subs(r,1) eq12=eq02.subs(r,1) eq13=eq03.subs(r,1) eq14=eq04.subs(r,1) #単位方眼(x=y=1)に外接する場合 eq21=eq01.subs([(x,1),(y,1)]) #r=sqrt(2) eq22=eq02.subs([(x,1),(y,1)]) #φ=π/4 #極座標(r=sqrt(2),φ=π/4)の場合 eq23=eq03.subs([(r,sp.sqrt(2)),(φ,pi/4)]) #x=1 eq24=eq04.subs([(r,sp.sqrt(2)),(φ,pi/4)]) #y=1 #極座標(r=sqrt(1),φ=π/4)の場合 eq33=eq03.subs([(r,1),(φ,pi/4)]) #x=sqrt(2)/2 eq34=eq04.subs([(r,1),(φ,pi/4)]) #y=sqrt(2)/2 #単位方眼(x=y=1)に内接する場合 eq31=eq01.subs([(x,sp.sqrt(2)/2),(y,sp.sqrt(2)/2)]) #r=1 eq32=eq02.subs([(x,sp.sqrt(2)/2),(y,sp.sqrt(2)/2)]) #φ=π/4 #tex sp.init_printing() print("デカルト座標系(x,y)と極座標系(r,φ)の相互変換") display(eq01) print(sp.latex(eq01)) display(eq02) print(sp.latex(eq02)) display(eq03) print(sp.latex(eq03)) display(eq04) print(sp.latex(eq04)) print("単位円(半径r=1)の場合") display(eq11) print(sp.latex(eq11)) display(eq12) print(sp.latex(eq12)) display(eq13) print(sp.latex(eq13)) display(eq14) print(sp.latex(eq14)) print("単位方眼(x=y=z=1)に外接する場合") display(eq21) print(sp.latex(eq21)) display(eq22) print(sp.latex(eq22)) print("極座標(r=sqrt(2),φ=π/4)の場合") display(eq23) print(sp.latex(eq23)) display(eq24) print(sp.latex(eq24)) print("極座標(r=1,φ=π/4)の場合") display(eq33) print(sp.latex(eq33)) display(eq34) print(sp.latex(eq34)) print("単位方眼(x=y=z=1)に内接する場合") display(eq31) print(sp.latex(eq31)) display(eq32) print(sp.latex(eq32)) デカルト座標系(x,y)と極座標系(r,φ)の相互変換 \begin{align*} &r = \sqrt{x^{2} + y^{2}}\\ &φ = \operatorname{atan_{2}}{\left(y,x \right)} \operatorname{sign}{\left(y \right)}\\ &x = r \cos{\left(φ \right)}\\ &y = r \sin{\left(φ \right)} \end{align*} 単位円(半径r=1)の場合 \begin{align*} &1 = \sqrt{x^{2} + y^{2}}\\ &φ = \operatorname{atan_{2}}{\left(y,x \right)} \operatorname{sign}{\left(y \right)}\\ &x = \cos{\left(φ \right)}\\ &y = \sin{\left(φ \right)} \end{align*} この辺りからおもむろに「同心円問題」が浮上してくる展開を迎えます。正方形の同心円集合を規定する演算は$E_n=2^{\frac{n}{2}}$となります。 単位方眼(x=y=z=1)に外接する場合 \begin{align*} &r = \sqrt{2}\\ &φ = \frac{\pi}{4}\\ &x=1\\ &y=1 \end{align*} 単位方眼(x=y=z=1)に内接する場合 \begin{align*} &r = 1\\ &φ = \frac{\pi}{4}\\ &x = \frac{\sqrt{2}}{2}\\ &y = \frac{\sqrt{2}}{2} \end{align*} 演算$2^{\frac{n}{2}}$の結果集合としての$\frac{平方対角線}{2}$=同心円半径集合$e_n$の推移 E_n(n=-∞→0→+∞)=(E_{-∞}=\frac{1}{∞}=0,…,E_{-2}=2^{-\frac{2}{2}}=2^{-1}=\frac{1}{2},E_{-1}=2^{-\frac{1}{2}}=\frac{\sqrt{2}}{2},E_0=1,E_1=2^{\frac{1}{2}}=\sqrt{2},E_2=2^{\frac{2}{2}}=2^1=2,…,E_{+∞}=∞) import numpy as np import matplotlib.pyplot as plt import numpy.random as random import matplotlib.patches as patches import matplotlib.animation as animation %matplotlib inline #単位円データ作成 c0=np.linspace(-np.pi,np.pi,61,endpoint = True) s0=[] for num in range(len(c0)): s0.append(complex(np.cos(c0[num]),np.sin(c0[num]))) s1=np.array(s0) #描画準備 fig = plt.figure() ax = plt.axes() def circle_draw(n): plt.cla() plt.title("Concentric Circles") plt.xlabel("X") plt.ylabel("Y") plt.ylim([-2.1,2.1]) plt.xlim([-2.1,2.1]) # 同心円描画 plt.plot(0,0,marker='x', color='green') circle1=patches.Circle((0,0),radius=2, fill=True, color='gray',lw=0.5,alpha=0.5) circle2=patches.Circle((0,0),radius=np.sqrt(2), fill=True, color='gray',lw=0.5,alpha=0.2) circle3=patches.Circle((0,0),radius=1, fill=True, color='gray',lw=0.5,alpha=0.2) circle4=patches.Circle((0,0),radius=1/np.sqrt(2), fill=True, color='gray',lw=0.5,alpha=0.2) circle5=patches.Circle((0,0),radius=1/2, fill=True, color='gray',lw=0.5,alpha=0.2) circle6=patches.Circle((0,0),radius=10, fill=True, color='gray',lw=0.5,alpha=0.2) ax.add_patch(circle1) ax.add_patch(circle2) ax.add_patch(circle3) ax.add_patch(circle4) ax.add_patch(circle5) ax.add_patch(circle6) ax.set_aspect('equal', adjustable='box') #補助線描画 plt.axvline(0, 0, 1,color="black",lw=0.5) plt.axhline(0, 0, 1,color="black",lw=0.5) #内内接立方体 plt.plot([-1/2,1/2],[1/2,1/2],color="gray",lw=1) plt.plot([1/2,1/2],[1/2,-1/2],color="gray",lw=1) plt.plot([1/2,-1/2],[-1/2,-1/2],color="gray",lw=1) plt.plot([-1/2,-1/2],[-1/2,1/2],color="gray",lw=1) #内接立方体 plt.plot([-np.sqrt(2)/2,np.sqrt(2)/2],[np.sqrt(2)/2,np.sqrt(2)/2],color="gray",lw=1) plt.plot([np.sqrt(2)/2,np.sqrt(2)/2],[np.sqrt(2)/2,-np.sqrt(2)/2],color="gray",lw=1) plt.plot([np.sqrt(2)/2,-np.sqrt(2)/2],[-np.sqrt(2)/2,-np.sqrt(2)/2],color="gray",lw=1) plt.plot([-np.sqrt(2)/2,-np.sqrt(2)/2],[-np.sqrt(2)/2,np.sqrt(2)/2],color="gray",lw=1) #単位立方体 plt.plot([-1,1],[1,1],color="black",lw=1) plt.plot([1,1],[1,-1],color="black",lw=1) plt.plot([1,-1],[-1,-1],color="black",lw=1) plt.plot([-1,-1],[-1,1],color="black",lw=1) #外接立方体 plt.plot([-np.sqrt(2),np.sqrt(2)],[np.sqrt(2),np.sqrt(2)],color="gray",lw=1) plt.plot([np.sqrt(2),np.sqrt(2)],[np.sqrt(2),-np.sqrt(2)],color="gray",lw=1) plt.plot([np.sqrt(2),-np.sqrt(2)],[-np.sqrt(2),-np.sqrt(2)],color="gray",lw=1) plt.plot([-np.sqrt(2),-np.sqrt(2)],[-np.sqrt(2),np.sqrt(2)],color="gray",lw=1) #外外接立方体 plt.plot([-2,2],[2,2],color="gray",lw=1) plt.plot([2,2],[2,-2],color="gray",lw=1) plt.plot([2,-2],[-2,-2],color="gray",lw=1) plt.plot([-2,-2],[-2,2],color="gray",lw=1) #移動線描画 plt.plot([0,s1[n].real*6],[0,s1[n].imag*6],color="green",lw=1) plt.plot(s1[n].real*2,s1[n].imag*2,marker='x', color='green') plt.plot(s1[n].real*np.sqrt(2),s1[n].imag*np.sqrt(2),marker='x', color='green') plt.plot(s1[n].real*1,s1[n].imag*1,marker='x', color='green') plt.plot(s1[n].real*np.sqrt(2)/2,s1[n].imag*np.sqrt(2)/2,marker='x', color='green') plt.plot(s1[n].real*1/2,s1[n].imag*1/2,marker='x', color='green') #circle_draw(1) #plt.show() ani = animation.FuncAnimation(fig, circle_draw, interval=50,frames=len(s1)) ani.save("circle_draw70002.gif", writer="pillow") 三次元球面空間におけるデカルト座標系(x,y,z)と極座標系(r,φ,θ)の相互変換 原点(0,0,0)からの距離$r=\sqrt{x^2+y^2+z^2}$ $φ=sign(y)\arctan^2(y,x)$ $θ=\arctan^2(z,\sqrt{x^2+y^2+z^2})$ $θ=\arctan^2(z,r)$ $x=r×\sin(θ)\cos(φ)$ $y=r×\sin(θ)\sin(φ)$ $z=r×\cos(θ)$ import sympy as sp import numpy as np from sympy import I, pi, E #プログラム中では色々試みてますが、 #失敗に終わった試行も多く、全部は説明しません。 x,y,z,r,φ,θ = sp.symbols('x,y,z,r,φ,θ') #デカルト座標系(x,y,z)と極座標系(r,φ,θ)の相互変換 eq01=sp.Eq(r,sp.sqrt(x**2+y**2+z**2)) eq02=sp.Eq(φ, sp.sign(y)*sp.atan2(y,x)) eq03=sp.Eq(θ, sp.atan2(z,sp.sqrt(x**2+y**2+z**2))) eq03r=sp.Eq(θ,sp.atan2(z,r)) eq04=sp.Eq(x,r*sp.sin(θ)*sp.cos(φ) ) eq05=sp.Eq(y, r*sp.sin(θ)*sp.sin(φ)) eq06=sp.Eq(z, r*sp.cos(θ)) #s0=sp.solve([eq01, eq02,eq03,eq04,eq05,eq06],[r,φ,θ,x,y,z]) #解けない(出力[]) #単位球面(半径r=1)の場合 eq11=eq01.subs(r,1) eq12=eq02.subs(r,1) eq13r=eq03r.subs(r,1) eq14=eq04.subs(r,1) eq15=eq05.subs(r,1) eq16=eq06.subs(r,1) #単位方眼(x=y=z=1)に内接する球面(半径r=1)について #角度φ=θ=pi/4ラジアンの位置を求めると #水平面(x,y)と垂直面(z)で結果が異なる。 eq24=eq04.subs([(r,1),(φ,pi/4),(θ,pi/4)])#x=1/2=2^-1 eq25=eq05.subs([(r,1),(φ,pi/4),(θ,pi/4)])#y=1/2=2^-1 eq26=eq06.subs([(r,1),(φ,pi/4),(θ,pi/4)])#z=sqrt(2)/2=2^-0.5 eq21a=eq01.subs([(x,1),(y,1),(z,1)]) #r=sqrt(3)sqrt(2)/2 eq22a=eq02.subs([(x,1),(y,1),(z,1)]) #φ=π/4ラジアン=45度 eq23a=eq03.subs([(x,1),(y,1),(z,1)]) #θ=π/6ラジアン=30度 #xyz座標(2,2,2)を通る内接球面 eq21d=eq01.subs([(x,2),(y,2),(z,2)]) #r=0.866025403784439=cos(π/6)=sin(π/3) eq22d=eq02.subs([(x,2),(y,2),(z,2)]) #φ=0.785398163397448=π/4ラジアン=45度 eq23d=eq03.subs([(x,2),(y,2),(z,2)]) #θ=0.523598775598299=π/6ラジアン=30度 #xyz座標(1/2,1/2,1/2)を通る内接球面 eq21a=eq01.subs([(x,1/2),(y,1/2),(z,1/2)]) #r=0.866025403784439=cos(π/6)=sin(π/3) eq22a=eq02.subs([(x,1/2),(y,1/2),(z,1/2)]) #φ=0.785398163397448=π/4ラジアン=45度 eq23a=eq03.subs([(x,1/2),(y,1/2),(z,1/2)]) #θ=0.523598775598299=π/6ラジアン=30度 #xyz座標(sqrt(2)/2,sqrt(2)/2,sqrt(2)/2)を通る内接球面 eq21b=eq01.subs([(x,sp.sqrt(2)/2),(y,sp.sqrt(2)/2),(z,sp.sqrt(2)/2)])#r=sqrt(3)sqrt(2)/2 eq22b=eq02.subs([(x,sp.sqrt(2)/2),(y,sp.sqrt(2)/2),(z,sp.sqrt(2)/2)])#φ=π/4ラジアン=45度 eq23b=eq03.subs([(x,sp.sqrt(2)/2),(y,sp.sqrt(2)/2),(z,sp.sqrt(2)/2)])#θ=π/6ラジアン=30度 #xyz座標(sqrt(2),sqrt(2),sqrt(2))を通る内接球面 eq21c=eq01.subs([(x,sp.sqrt(2)),(y,sp.sqrt(2)),(z,sp.sqrt(2))])#r=sqrt(3)sqrt(2)/2 eq22c=eq02.subs([(x,sp.sqrt(2)),(y,sp.sqrt(2)),(z,sp.sqrt(2))])#φ=π/4ラジアン=45度 eq23c=eq03.subs([(x,sp.sqrt(2)),(y,sp.sqrt(2)),(z,sp.sqrt(2))])#θ=π/6ラジアン=30度 #単位球面に外接する立方体(半径r=sqrt(3))の場合 eq34=eq04.subs([(r,sp.sqrt(3)),(φ,pi/4),(θ,pi/4)])#x=sqrt(3)/2 eq35=eq05.subs([(r,sp.sqrt(3)),(φ,pi/4),(θ,pi/4)])#y=sqrt(3)/2 eq36=eq06.subs([(r,sp.sqrt(3)),(φ,pi/4),(θ,pi/4)])#z=sqrt(3)sqrt(2)/2 eq31=eq01.subs([(x,sp.sqrt(3)),(y,sp.sqrt(3)),(z,sp.sqrt(3))])#r=sqrt(3)sqrt(2)/2 eq32=eq02.subs([(x,sp.sqrt(3)),(y,sp.sqrt(3)),(z,sp.sqrt(3))])#φ=π/4ラジアン=45度 eq33=eq03.subs([(x,sp.sqrt(3)),(y,sp.sqrt(3)),(z,sp.sqrt(3))])#θ=π/6ラジアン=30度 #xyz座標(sqrt(3)/2,sqrt(3)/2,sqrt(3)/2)を通る内接球面 eq31a=eq01.subs([(x,sp.sqrt(3)/2),(y,sp.sqrt(3)/2),(z,sp.sqrt(3)/2)])#r=3/2 eq32a=eq02.subs([(x,sp.sqrt(3)/2),(y,sp.sqrt(3)/2),(z,sp.sqrt(3)/2)])#φ=π/4ラジアン=45度 eq33a=eq03.subs([(x,sp.sqrt(3)/2),(y,sp.sqrt(3)/2),(z,sp.sqrt(3)/2)])#θ=π/6ラジアン=30度 #xyz座標(sqrt(6)/2,sqrt(6)/2,sqrt(6)/2)を通る内接球面 eq31b=eq01.subs([(x,sp.sqrt(6)/2),(y,sp.sqrt(6)/2),(z,sp.sqrt(3)/2)])#r=sqrt(15)/2 eq32b=eq02.subs([(x,sp.sqrt(6)/2),(y,sp.sqrt(6)/2),(z,sp.sqrt(3)/2)])#φ=π/4ラジアン=45度 eq33b=eq03.subs([(x,sp.sqrt(6)/2),(y,sp.sqrt(6)/2),(z,sp.sqrt(3)/2)])#θ=atan(sqrt(5)/5)(1.150262)=65.90516度 #xyz座標(sqrt(6),sqrt(6),sqrt(6))を通る内接球面 eq31c=eq01.subs([(x,sp.sqrt(6)),(y,sp.sqrt(6)),(z,sp.sqrt(3))])#r=sqrt(15)/2 eq32c=eq02.subs([(x,sp.sqrt(6)),(y,sp.sqrt(6)),(z,sp.sqrt(3))])#φ=π/4ラジアン=45度 eq33c=eq03.subs([(x,sp.sqrt(6)),(y,sp.sqrt(6)),(z,sp.sqrt(3))])#θ=atan(sqrt(5)/5)(1.150262)=65.90516度 #単位方眼(x=y=z=1)の場合 #eq21=eq01.subs([(x,1),(y,1),(z,1)]) #eq22=eq02.subs([(x,1),(y,1),(z,1)]) #eq23=eq03.subs([(x,1),(y,1),(z,1)]) #r=sqrt(1^2+1^2+1^2)=sqrt(3) #φ=pi/2-atan2(1,1)=pi/2-pi/4=pi/4 #θ=pi/2-atan2(r,1)=pi/2-pi/3=pi/6 #この時点で既に異なる同心球面を指している。 #極座標(r=sqrt(3),φ=π/4,θ=π/6)の場合 #eq24=eq04.subs([(r,sp.sqrt(3)),(φ,pi/4),(θ,pi/6)]) #eq25=eq05.subs([(r,sp.sqrt(3)),(φ,pi/4),(θ,pi/6)]) #eq26=eq06.subs([(r,sp.sqrt(3)),(φ,pi/4),(θ,pi/6)]) #cos(π/4)=sin(π/4)=sqrt(2)/2 #acos(sqrt(2)/2)=asin(sqrt(2)/2)=π/4 #cos(π/6)=sqrt(3)/2,acos(sqrt(3)/2)=π/6 #sin(π/6)=1/2,asin(1/2)=π/6 #?=?sin(θ)cos(φ)=sqrt(3)*1/2*sqrt(2)/2=sqrt(6)/4 #?=?sin(θ)sin(φ)=sqrt(3)*1/2*sqrt(2)/2=sqrt(6)/4 #z=?cos(θ)=sqrt(3)*sqrt(3)/2=3/2 #当然xy軸上の円弧の半径とz軸が示す半径は一致しない。 #tex sp.init_printing() print("デカルト座標系(x,y,z)と極座標系(r,φ,θ)の相互変換") display(eq01) print(sp.latex(eq01)) display(eq02) print(sp.latex(eq02)) display(eq03) print(sp.latex(eq03)) display(eq03r) print(sp.latex(eq03r)) display(eq04) print(sp.latex(eq04)) display(eq05) print(sp.latex(eq05)) display(eq06) print(sp.latex(eq06)) print("単位球面(半径r=1)の場合") display(eq11) print(sp.latex(eq11)) display(eq12) print(sp.latex(eq12)) display(eq13) print(sp.latex(eq13)) display(eq14) print(sp.latex(eq14)) display(eq15) print(sp.latex(eq15)) display(eq16) print(sp.latex(eq16)) print("単位方眼(x=y=z=1)に内接する球面の場合") display(eq24) print(sp.latex(eq24)) display(eq25) print(sp.latex(eq25)) display(eq26) print(sp.latex(eq26)) display(eq21) print(sp.latex(eq21)) display(eq22) print(sp.latex(eq22)) display(eq23) print(sp.latex(eq23)) print("xyz座標(2,2,2)を通る内接球面") display(eq21d) print(sp.latex(eq21d)) display(eq22d) print(sp.latex(eq22d)) display(eq23d) print(sp.latex(eq23d)) print("xyz座標(1/2,1/2,1/2)を通る内接球面") display(eq21a) print(sp.latex(eq21a)) display(eq22a) print(sp.latex(eq22a)) display(eq23a) print(sp.latex(eq23a)) print("xyz座標(sqrt(2)/2,sqrt(2)/2,sqrt(2)/2)を通る内接球面") display(eq21b) print(sp.latex(eq21b)) display(eq22b) print(sp.latex(eq22b)) display(eq23b) print(sp.latex(eq23b)) print("xyz座標(sqrt(2),sqrt(2),sqrt(2))を通る内接球面") display(eq21c) print(sp.latex(eq21c)) display(eq22c) print(sp.latex(eq22c)) display(eq23c) print(sp.latex(eq23c)) print("単位方眼(x=y=Z=1)に外接する球面の場合") display(eq34) print(sp.latex(eq34)) display(eq35) print(sp.latex(eq35)) display(eq36) print(sp.latex(eq36)) display(eq31) print(sp.latex(eq31)) display(eq32) print(sp.latex(eq32)) display(eq33) print(sp.latex(eq33)) print("xyz座標(sqrt(3)/2,sqrt(3)/2,sqrt(3)/2)を通る内接球面") display(eq31a) print(sp.latex(eq31a)) display(eq32a) print(sp.latex(eq32a)) display(eq33a) print(sp.latex(eq33a)) print("xyz座標(sqrt(6)/2,sqrt(6)/2,sqrt(6)/2)を通る内接球面") display(eq31b) print(sp.latex(eq31b)) display(eq32b) print(sp.latex(eq32b)) display(eq33b) print(sp.latex(eq33b)) print("xyz座標(sqrt(6),sqrt(6),sqrt(6))を通る内接球面") display(eq31c) print(sp.latex(eq31c)) display(eq32c) print(sp.latex(eq32c)) display(eq33c) print(sp.latex(eq33c)) デカルト座標系(x,y,z)と極座標系(r,φ,θ)の相互変換の計算結果 \begin{align*} &r = \sqrt{x^{2} + y^{2} + z^{2}}\\ &φ = \operatorname{atan_{2}}{\left(y,x \right)} \operatorname{sign}{\left(y \right)}\\ &θ = \operatorname{atan_{2}}{\left(z,\sqrt{x^{2} + y^{2} + z^{2}} \right)}\\ &θ = θ = \operatorname{atan_{2}}{\left(z,r \right)}\\ &x = r \sin{\left(θ \right)} \cos{\left(φ \right)}\\ &y = r \sin{\left(θ \right)} \sin{\left(φ \right)}\\ &z = r \cos{\left(θ \right)} \end{align*} 単位円(半径r=1)の場合の計算結果 \begin{align*} &1 = \sqrt{x^{2} + y^{2} + z^{2}}\\ &φ = \operatorname{atan_{2}}{\left(y,x \right)} \operatorname{sign}{\left(y \right)}\\ &θ = - \operatorname{atan_{2}}{\left(1,z \right)} + \frac{\pi}{2}\\ &x = \sin{\left(θ \right)} \cos{\left(φ \right)}\\ &y = \sin{\left(θ \right)} \sin{\left(φ \right)}\\ &z = \cos{\left(θ \right)} \end{align*} ついでに立方対角線に沿った同心円展開に挑戦してみましたが、あえなく座説。 Axis(x,y,z) Decimal A Radius Decimal R 0 sqrt(6) 2.44949000000000 sqrt(15) 3.87298300000000 1 2 2.00000000000000 2*sqrt(3) 3.46410200000000 2 sqrt(3) 1.73205100000000 sqrt(6)/2 1.22474500000000 3 sqrt(2) 1.41421400000000 sqrt(6) 2.44949000000000 4 sqrt(6)/2 1.22474500000000 sqrt(15)/2 1.93649200000000 5 1 1.00000000000000 sqrt(6)/2 1.22474500000000 6 sqrt(3)/2 0.866025400000000 3/2 1.50000000000000 7 sqrt(2)/2 0.707106800000000 sqrt(6)/2 1.22474500000000 8 1/2 0.500000000000000 cos(π/6)/sin(π/3) 0.866025400000000 import sympy as sp import numpy as np import pandas as pd a,b,x,y = sp.symbols('a,b,x,y') X1 = sp.Matrix([ ["sqrt(6)","2","sqrt(3)","sqrt(2)","sqrt(6)/2","1","sqrt(3)/2","sqrt(2)/2","1/2"], [2.44949,2.0,1.732051,1.414214,1.224745,1.0,0.8660254,0.7071068,0.5], ["sqrt(15)","2*sqrt(3)","sqrt(6)/2","sqrt(6)","sqrt(15)/2","sqrt(6)/2","3/2","sqrt(6)/2","cos(π/6)/sin(π/3)"], [3.872983,3.464102,1.224745,2.44949,1.936492,1.224745,1.5,1.224745,0.8660254]]) x=X1.transpose() df=pd.DataFrame(np.matrix(x),columns=['Axis(x,y,z)', 'Decimal A', 'Radius', 'Decimal R']) sp.init_printing() org=df.to_html() print(org.replace('\n', '')) 特に理解不能なのが以下。どうして素直に45度($\frac{π}{4}$ラジアン)×3と展開してくれないんでしょうか? 単位方眼(x=y=z=1)に外接する球面の計算結果 \begin{align*} &r =\sqrt{1^2+1^2+1^2}=\sqrt{3}\\ &φ =\frac{\pi}{2}-\operatorname{atan_{2}}{(1,1)}=\frac{\pi}{2}-\frac{\pi}{4}=\frac{\pi}{4}\\ &θ = \frac{\pi}{2} - \operatorname{atan_{2}}{(\sqrt{3},1)} = \frac{\pi}{2}-\frac{\pi}{3}=\frac{\pi}{6} \end{align*} 極座標($r=\sqrt{3},φ=\frac{π}{4},θ=\frac{π}{6}$)に対応するデカルト座標(x,y,z) \begin{align*} &\cos(\frac{π}{4})=\sin(\frac{π}{4})=\frac{\sqrt{2}}{2}\\ &\arccos(\frac{\sqrt{2}}{2})=\arcsin(\frac{\sqrt{2}}{2})=\frac{π}{4}\\ &\cos(\frac{π}{6})=\frac{\sqrt{3}}{2},\arccos(\frac{\sqrt{3}}{2})=\frac{π}{6}\\ &\sin(\frac{π}{6})=\frac{1}{2},\arcsin(\frac{1}{2})=\frac{π}{6}\\ すなわち\\ &x =?*\sin(θ)*\cos(φ)=\sqrt{3}*\frac{1}{2}*\frac{\sqrt{2}}{2}=\frac{\sqrt{6}}{4}\\ &y =?*\sin(θ)*\sin(φ)=\sqrt{3}*\frac{1}{2}*\frac{\sqrt{2}}{2}=\frac{\sqrt{6}}{4}\\ &z=?*\cos(θ)=\sqrt{3}*\frac{\sqrt{3}}{2}=\frac{3}{2} \end{align*} とりあえず演算$E_n=\sqrt{3}2^{\frac{n}{2}}$の結果としての同心球集合の抽出には成功したので先に進む事にします。現段階ではイメージ構築の為に1系列に注目するのが精一杯ですが、考え方さえ整理出来たら他にもいくらでも見つかりそうな気がしています。 E_n(n=-∞→0→+∞)=\sqrt{3}2^{\frac{n}{2}}=(E_{-∞}=\frac{1}{∞}=0,…,E_{-2}=\sqrt{3}2^{-1}=\frac{\sqrt{3}}{2}),E_{-1}=\sqrt{3}2^{-0.5}=\frac{\sqrt{6}}{2}),E_{0}=\sqrt{3}2^{0}=\sqrt{3},E_{1}=\sqrt{3}2^{0.5}=\sqrt{6},E_{2}=\sqrt{3}2^{1}=2\sqrt{3},…,E_{∞+}=∞) import numpy as np import matplotlib.pyplot as plt import numpy.random as random import matplotlib.patches as patches import matplotlib.animation as animation %matplotlib inline #単位円データ作成 c0=np.linspace(-np.pi,np.pi,61,endpoint = True) s0=[] for num in range(len(c0)): s0.append(complex(np.cos(c0[num]),np.sin(c0[num]))) s1=np.array(s0) #描画準備 fig = plt.figure() ax = plt.axes() def circle_draw(n): plt.cla() plt.title("Concentric Circles") plt.xlabel("X") plt.ylabel("Y") plt.ylim([-4.0,4.0]) plt.xlim([-4.0,4.0]) # 同心円描画 plt.plot(0,0,marker='x', color='green') circle1=patches.Circle((0,0),radius=np.sqrt(3)/2, fill=True, color='gray',lw=0.5,alpha=0.5) circle2=patches.Circle((0,0),radius=np.sqrt(6)/2, fill=True, color='gray',lw=0.5,alpha=0.2) circle3=patches.Circle((0,0),radius=np.sqrt(3), fill=True, color='gray',lw=0.5,alpha=0.2) circle4=patches.Circle((0,0),radius=np.sqrt(6), fill=True, color='gray',lw=0.5,alpha=0.2) circle5=patches.Circle((0,0),radius=2*np.sqrt(3), fill=True, color='gray',lw=0.5,alpha=0.2) circle6=patches.Circle((0,0),radius=10, fill=True, color='gray',lw=0.5,alpha=0.2) ax.add_patch(circle1) ax.add_patch(circle2) ax.add_patch(circle3) ax.add_patch(circle4) ax.add_patch(circle5) ax.add_patch(circle6) ax.set_aspect('equal', adjustable='box') #補助線描画 plt.axvline(0, 0, 1,color="black",lw=0.5) plt.axhline(0, 0, 1,color="black",lw=0.5) #移動線描画 plt.plot([0,s1[n].real*6],[0,s1[n].imag*6],color="green",lw=1) plt.plot(s1[n].real*np.sqrt(6)/2,s1[n].imag*np.sqrt(6)/2,marker='x', color='green') plt.plot(s1[n].real*np.sqrt(3),s1[n].imag*np.sqrt(3),marker='x', color='green') plt.plot(s1[n].real*np.sqrt(6),s1[n].imag*np.sqrt(6),marker='x', color='green') plt.plot(s1[n].real*2*np.sqrt(3),s1[n].imag*2*np.sqrt(3),marker='x', color='green') #circle_draw(1) #plt.show() ani = animation.FuncAnimation(fig, circle_draw, interval=50,frames=len(s1)) ani.save("circle_draw80002.gif", writer="pillow") 水平角φ(0~2π)の推移と垂直角θ(0~π)の推移を同期させると以下の様に見えます。 【Python演算処理】単位球面(Unit Shere)を巡る数理①とりあえず描画してみる。 ここで全体像をイメージする上での難所として浮上してくるのが、プログラム上球面はz座標区間(1~0~-1)を範囲とする同心円集合(半径0~1~0)として描画されるにも関わらず、各z座標における球面上の座標は$\cos(θ)$で与えられる辺り。 Z_n(n=1→0→-1)=(1,…,0,…,-1)\\ \cos(θ)(θ=0→\frac{π}{2}→πラジアン(rad(ian)))=Z_n\\ \arccos(Z_n)=(0,…,\frac{π}{2},…,πラジアン(rad(ian)) この関係式がしっかり頭に入ってないとすぐに行き詰まってしまうのです(それにつけても突如として自明の場合として割り込んでくるπ概念!!)。さらにはその同心球面展開を考えてみます。 %matplotlib nbagg import math as m import cmath as c import numpy as num import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import matplotlib.animation as animation #円柱データ作成 c0=num.linspace(0,m.pi*120,1201,endpoint = True) s0=[] for nm in range(len(c0)): s0.append(complex(m.cos(c0[nm]),m.sin(c0[nm]))) s1=num.array(s0) z0=num.linspace(-1,1,1201,endpoint = True) #断面線(z) cutz0=num.linspace(-1,1,61,endpoint = True) cutz=cutz0[::-1] cutx=num.sqrt(1-cutz**2) cuty=num.repeat(0,61) #「曲率」を計算 cv0=num.linspace(-1,1,1201,endpoint = True) cv1=num.sqrt(1-cv0**2) #単位円データ作成 u0=num.linspace(0,m.pi*2,61,endpoint = True) u1=[] for nm in range(len(u0)): u1.append(complex(m.cos(u0[nm]),m.sin(u0[nm]))) uc=num.array(u1) uz0=num.repeat(-1,61) uz1=num.repeat(0,61) uz2=num.repeat(1,61) #断面円 ucv0=num.linspace(-1,1,61,endpoint = True) ucv=num.sqrt(1-ucv0**2) #グラフ表示 plt.style.use('default') fig = plt.figure() ax = Axes3D(fig) #関数定義 def unit_cylinder(n): plt.cla() ucutz=num.full(61,cutz[n]) #球面描画 ax.plot(s1.real*cv1,s1.imag*cv1,z0,color="gray",lw=0.5) #半球面描画 ax.plot(s1.real*cv1/2,s1.imag*cv1/2,z0/2,color="gray",lw=0.5) #スポーク描画 for nm in range(len(uc)): ax.plot([0,uc[nm].real*ucv[n]],[0,uc[nm].imag*ucv[n]],[cutz[n],cutz[n]],color="black",lw=0.5) #半スポーク描画 #for nm in range(len(uc)): # ax.plot([0,uc[nm].real*ucv[n]]/2,[0,uc[nm].imag*ucv[n]]/2,[cutz[n],cutz[n]]/2,color="black",lw=0.5) #単位円描画 ax.plot(uc.real,uc.imag,uz0,color="red",lw=1) ax.plot(uc.real,uc.imag,uz1,color="green",lw=1) ax.plot(uc.real,uc.imag,uz2,color="blue",lw=1) #半単位円描画 ax.plot(uc.real/2,uc.imag/2,uz0,color="red",lw=1) ax.plot(uc.real/2,uc.imag/2,uz1,color="green",lw=1) ax.plot(uc.real/2,uc.imag/2,uz2,color="blue",lw=1) #実数線追加 ax.plot([0,0],[0,0],[-1,1],color="black",lw=1.5) ax.plot([0,1],[0,0],[-1,-1],color="red",lw=1) ax.plot([0,cutx[n]],[0,cuty[n]],[0,cutz[n]],color="purple",lw=1) ax.plot([0,cutx[n]],[0,cuty[n]],[cutz[n],cutz[n]],color="purple",lw=1) ax.plot([0,cuty[n]],[0,cutx[n]],[0,cutz[n]],color="blue",lw=1) ax.plot([0,cuty[n]],[0,-1*cutx[n]],[0,cutz[n]],color="red",lw=1) ax.plot([0,1],[0,0],[0,0],color="purple",lw=1) ax.plot([0,0],[0,1],[0,0],color="blue",lw=1) ax.plot([0,0],[0,-1],[0,0],color="red",lw=1) ax.plot([0,1],[0,0],[1,1],color="blue",lw=1) ax.plot([1,1],[0,0],[-1,0],color="red",lw=1) ax.plot([1,1],[0,0],[0,1],color="blue",lw=1) #半実数線追加 ax.plot([1/2,1/2],[0,0],[-1,0],color="red",lw=1) ax.plot([1/2,1/2],[0,0],[0,1],color="blue",lw=1) #断面線描画 ax.plot(cutx,cuty,cutz,color="purple",lw=1) ax.plot(cuty,cutx,cutz,color="blue",lw=1) ax.plot(cuty,-1*cutx,cutz,color="red",lw=1) #断面円描画 ax.plot(uc.real*ucv[n],uc.imag*ucv[n],ucutz,color="black",lw=1) #半断面円描画 ax.plot(uc.real*ucv[n]/2,uc.imag*ucv[n]/2,ucutz/2,color="black",lw=1) #諸元追加 ax.set_ylim([-1.1,1.1]) ax.set_xlim([-1.1,1.1]) ax.set_zlim([-1.1,1.1]) ax.set_title("Unit Cylinder") ax.set_xlabel("Real") ax.set_ylabel("Imaginal") ax.set_zlabel("Cycle") # グラフを回転(elev=0にすると水平表示、90にすると垂直表示) ax.view_init(elev=45, azim=Time_code[n]) Time_code0=num.arange(0,360,6) Time_code=Time_code0[::-1] #unit_cylinder(len(s1)) #plt.show() ani = animation.FuncAnimation(fig, unit_cylinder, interval=50,frames=len(Time_code)) ani.save("cylinder201.gif", writer="pillow") プログラム上半径1の球面と半径$\frac{1}{2}$の球面を対峙させましたが、比率としては半径2の円と半径1の円を対峙させた場合と変わりません。とどのつまりかかる同心円構造の半径は$2^n$乗のオーダーで増減する事が想定されます。 2^n(n=-\infty→0→+\infty)=(2^{-\infty}=\frac{1}{\infty}=0,…,2^{-2}=\frac{1}{2^2}=\frac{1}{4},2^{-1}=\frac{1}{2^1}=\frac{1}{2},2^{0}=\frac{2^1}{2^1}=1,2^1=1,2^2=4,…,2^{+\infty}=\infty) import numpy as np import matplotlib.pyplot as plt import numpy.random as random import matplotlib.patches as patches import matplotlib.animation as animation %matplotlib inline #単位円データ作成 c0=np.linspace(-np.pi,np.pi,61,endpoint = True) s0=[] for num in range(len(c0)): s0.append(complex(np.cos(c0[num]),np.sin(c0[num]))) s1=np.array(s0) #描画準備 fig = plt.figure() ax = plt.axes() def circle_draw(n): plt.cla() plt.title("Concentric Circles") plt.xlabel("X") plt.ylabel("Y") plt.ylim([-4.0,4.0]) plt.xlim([-4.0,4.0]) # 同心円描画 plt.plot(0,0,marker='x', color='green') circle1=patches.Circle((0,0),radius=1/4, fill=True, color='gray',lw=0.5,alpha=0.5) circle2=patches.Circle((0,0),radius=1/2, fill=True, color='gray',lw=0.5,alpha=0.2) circle3=patches.Circle((0,0),radius=1, fill=True, color='gray',lw=0.5,alpha=0.2) circle4=patches.Circle((0,0),radius=2, fill=True, color='gray',lw=0.5,alpha=0.2) circle5=patches.Circle((0,0),radius=4, fill=True, color='gray',lw=0.5,alpha=0.2) circle6=patches.Circle((0,0),radius=10, fill=True, color='gray',lw=0.5,alpha=0.2) ax.add_patch(circle1) ax.add_patch(circle2) ax.add_patch(circle3) ax.add_patch(circle4) ax.add_patch(circle5) ax.add_patch(circle6) ax.set_aspect('equal', adjustable='box') #補助線描画 plt.axvline(0, 0, 1,color="black",lw=0.5) plt.axhline(0, 0, 1,color="black",lw=0.5) #移動線描画 plt.plot([0,s1[n].real*6],[0,s1[n].imag*6],color="green",lw=1) plt.plot(s1[n].real,s1[n].imag,marker='x', color='green') plt.plot(s1[n].real*1/4,s1[n].imag*1/4,marker='x', color='green') plt.plot(s1[n].real*1/2,s1[n].imag*1/2,marker='x', color='green') plt.plot(s1[n].real*1,s1[n].imag*1,marker='x', color='green') plt.plot(s1[n].real*2,s1[n].imag*2,marker='x', color='green') plt.plot(s1[n].real*4,s1[n].imag*4,marker='x', color='green') #circle_draw(1) #plt.show() ani = animation.FuncAnimation(fig, circle_draw, interval=50,frames=len(s1)) ani.save("circle_draw60003.gif", writer="pillow") 全ての指数関数(Exponential Function)$a^n$は以下の変換式を用いて自然指数関数(Natural Exponential Function)$e^n$へと変換可能ですから、この比例関係は自然指数関数の一種と定義されます。 a^n=e^{n\log{a}}\\ 2^n=e^{n\log{2}}\\ \log{2}=0.6931472 import numpy as np import matplotlib.pyplot as plt import numpy.random as random import matplotlib.patches as patches import matplotlib.animation as animation %matplotlib inline #単位円データ作成 c0=np.linspace(-np.pi,np.pi,61,endpoint = True) s0=[] for num in range(len(c0)): s0.append(complex(np.cos(c0[num]),np.sin(c0[num]))) s1=np.array(s0) #描画準備 fig = plt.figure() ax = plt.axes() def circle_draw(n): plt.cla() plt.title("Concentric Circles") plt.xlabel("X") plt.ylabel("Y") plt.ylim([-4.0,4.0]) plt.xlim([-4.0,4.0]) # 同心円描画 plt.plot(0,0,marker='x', color='green') circle1=patches.Circle((0,0),radius=np.exp(-2), fill=True, color='gray',lw=0.5,alpha=0.5) circle2=patches.Circle((0,0),radius=np.exp(-1), fill=True, color='gray',lw=0.5,alpha=0.2) circle3=patches.Circle((0,0),radius=np.exp(0), fill=True, color='gray',lw=0.5,alpha=0.2) circle4=patches.Circle((0,0),radius=np.exp(1), fill=True, color='gray',lw=0.5,alpha=0.2) circle5=patches.Circle((0,0),radius=np.exp(2), fill=True, color='gray',lw=0.5,alpha=0.2) circle6=patches.Circle((0,0),radius=10, fill=True, color='gray',lw=0.5,alpha=0.2) ax.add_patch(circle1) ax.add_patch(circle2) ax.add_patch(circle3) ax.add_patch(circle4) ax.add_patch(circle5) ax.add_patch(circle6) ax.set_aspect('equal', adjustable='box') #補助線描画 plt.axvline(0, 0, 1,color="black",lw=0.5) plt.axhline(0, 0, 1,color="black",lw=0.5) #移動線描画 plt.plot([0,s1[n].real*6],[0,s1[n].imag*6],color="green",lw=1) plt.plot(s1[n].real,s1[n].imag,marker='x', color='green') plt.plot(s1[n].real*np.exp(-2),s1[n].imag*np.exp(-2),marker='x', color='green') plt.plot(s1[n].real*np.exp(-1),s1[n].imag*np.exp(-1),marker='x', color='green') plt.plot(s1[n].real*np.exp(0),s1[n].imag*np.exp(0),marker='x', color='green') plt.plot(s1[n].real*np.exp(1),s1[n].imag*np.exp(1),marker='x', color='green') plt.plot(s1[n].real*np.exp(2),s1[n].imag*np.exp(2),marker='x', color='green') #circle_draw(1) #plt.show() ani = animation.FuncAnimation(fig, circle_draw, interval=50,frames=len(s1)) ani.save("circle_draw60004.gif", writer="pillow") $2^n$オーダーの同心円集合すら全体像を一望に収めるのが困難なくらいですから、さらに増率が加速度的に(それこそ文字通り指数関数的に)増大する$e^n$オーダーの同心円集合を納得のいく形で可視化するのはほぼ不可能です。指数関数的と幾何級数的をざっくり振り返りしかしまぁ、とりあえずはこれこそが「掛け算が足し算に、割り算が引き算に、冪乗算が掛け算に変換される」指数写像(Exponential Map)/対数写像(Logarithmic Map)の世界観なのです。【数理考古学】常用対数表を使った計算 こうした全体像を、以降は「乗法的同心円/球面集合(Multiplicative Concentric Set)」と呼び分けたいと思います。 ところで上掲の「乗法的同心円/球面集合」においては半径r=2(1)の同心円/球面の半径は2~0(1~0)の間、半径r=1($\frac{1}{2}$)の同心円/球面の半径は1~0($\frac{1}{2}$~0)の間を往復します。しかし我々の抱える一般的な同心円イメージにおいては、半径r=2(1)の同心円/球面の半径は2~1(1~$\frac{1}{2}$)の間、半径r=1($\frac{1}{2}$~0)の同心円/球面の半径は1~0($\frac{1}{2}$~0)の間を往復するのではないでしょうか? この期待にぴったり応えるのが(半径1を大半径=小半径とする)単位トーラス(Unit Torus)概念なのです。 【Python演算処理】単位トーラスを巡る数理①平坦トーラスとの往復 まさしくこれこそが虚数冪(Imaginal Exponentiation)$e^{iθ}$の世界(それにつけても突如として自明の場合として割り込んでくる虚数i概念!!)。 【数理考古学】群論とシミュレーション原理④群導出演算としてのオイラーの公式 $\cos(θ)=\frac{e^{θi}+e^{-θi}}{2}$ $\sin(θ)=\frac{e^{θi}-e^{-θi}}{2i}$ $e^{ix}\frac{d^n}{dθ^n}=(i e^{i x}(-\log ix),-e^{ix},-i e^{i x}(\log ix),e^{ix},…)$ $\int \int \int … \int e^{ix}(dθ)=(- i e^{i x}(\log ix),- e^{i x},i e^{i x}(-\log ix),e^{ix},…)$ しかしながらこの式はその本来の限界たる観測原点0に差し掛かっていて、虚数(Imaginal)の概念を導入しないのなら、例えば冪乗算の定積分たる以下の式でβ=-1の時、分母が0になり計算不能となってしまいます。 【数理考古学】冪乗算の微積分 {\int_{a}^{b}f(x)dx\\ =(\frac{1}{β+1}b^{β+1}+C)-(\frac{1}{β+1}a^{β+1}+C)\\ =\frac{1}{β+1}b^{β+1}-\frac{1}{β+1}a^{β+1} } β=-1の時(計算自体は途中で座説するが、考え方としてはこう) {\int_{a}^{b}f(x)dx\\ =(\frac{1}{0}b^{0}+C)-(\frac{1}{0}a^{0}+C)\\ =\frac{1}{0}b^{0}-\frac{1}{0}a^{0}\\ =\frac{1}{0}-\frac{1}{0}=0} 計算的には$\int_{1}^{2}\frac{1}{x}dx$すなわち反比例関数$y=\frac{1}{x}$における区間1→2の定積分を求める場合と一致する。 そこで数学の世界は以下の方便を採用する事に決めたのです。 【数理考古学】解析学史に「虚数概念」をもたらした交代級数 2の冪乗算$f(x)=2^x=e^{n\log{2}}$と「0から2までの正の実数」の組み合わせは任意の正の実数を表せる。 2^n(n=-\infty→+\infty)=(2^{-\infty}=\frac{1}{\infty}=0,…,2^{-2}=\frac{1}{2^2}=\frac{1}{4},…,2^{-1}=\frac{1}{2},…,2^0=\frac{2}{2}=1,…,2^1=2,…,2^2=4,…,2^{+\infty}=\infty) Values Explessions 1 1 2^0 2 2 2^1 3 3 2^0+2^1 / 2^log(3,base=2) 4 4 2^2 5 5 2^2+2^0 6 6 2^2+2^1 7 7 2^2+2^log(3,base=2) 8 8 2^3 その対数写像、すなわち「0から2までの正の実数」と$\log2$概念を組み合わせは任意の正の対数を表せる。 Numbers Explessions Values 1 log(1) log(2^1)+log(1/2) 0 2 log(2) log(2^1) 0.6931472 3 log(3) log(3/2)+log(2^1) 1.098612 4 log(4) log(2^2) 1.386294 5 log(5) log(5/4)+log(2^2) 1.609438 6 log(6) log(3/2)+log(2^2) 1.791759 7 log(7) log(7/4)+log(2^2) 1.94591 8 log(8) log(2^3) 2.079442 こう考えるなら、$\log{2}(0.6931472)$をあらかじめ計算しておく事で、全ての実数と指数を表す事が出来る様になる。 【Rで球面幾何学】オイラーの公式を導出したマクローリン級数の限界? ちなみに自然対数の真数(Antilogarithm)が1または-1の場合は以下の様に規定される(当然の様に円周率πと虚数iが登場)。log(-1)って、iπのように思えますが、実際は(2n+1)iπ? {多価関数log(-1)=(2n+1)iπ(n \in \mathbb{Z})\\ z=|z|*e^{arg(z)*i} → log(z)=log|z|+arg(z)*i\\ log(ー1)=log|ー1|+arg(ー1)*i=(2n+1)*π*i、(n \in \mathbb{Z})\\ 一方log(1)=(2n)*π*i(n=0)=0} そして特殊直交行列(Special Orthogonal Matrix)においては半径1の単位円をSO(2),半径1の単位球面をSO(3)と表記する。一方リー群論(Lee Group Theory)においては同じ半径1の単位円を1次元トーラス$S_0$と規定し、上掲の単位トーラスを2次元トーラス$S_1$、さらに原点を中心に回転する小半径円盤(回転軸i)に回転軸j,kを加えた四元数座標系(Quaternion Coordinate System)を3次元トーラス$S_3$と呼ぶ(おそらく結論からいえばたったこれだけの事の様なのだが、素直にそう記述している説明に邂逅出来ず、全体イメージをこういう形にまとめるのに数ヶ月を要し、なおかつ現時点でそれが正解かわからない)。【数理考古学】群論とシミュレーション原理⑦三次元トーラスとしての四元数概念導入 ここまで考え抜いてやっと群としても考えられる乗法的同心円/球面集合の全体像が浮かび上がってくる訳です。 そんな感じで以下続報…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyTorch Lightningを使ったCIFER10の画像分類

0.はじめに Pytorchを書くのってKerasに比べて結構大変じゃないですか?(まぁ逆にそれが魅力でもあるんですが) そんな時は、お試しでPyTorch Lightningを使ってみるのはどうでしょうか? この手の記事って小難しくて途中で読むの挫折するケースが多いと素がポンコツな私自身は感じるので、本記事はいつも通り初心者向けに平易な形で解説していきます。 ※PyTorchの基礎部分は解説してないので、それはある程度分かっている前提なのはあしからず。 ・Github ・公式ドキュメンテーション 動作環境 OS : Windows10 pro Python : 3.8.3// Miniconda 4.9.1 (py)torch:1.7.1 PyTorch Lightning:1.3.6 ※バージョンによってかなり差異がある模様 jupyter notebook 1.導入 pip install pytorch-lightningで導入できた。 2.CIFER10の画像分類に組み込んで使う 2-1.インポートモジュールとバージョン確認 # ! pip install pytorch-lightning import os import torch from torch import nn,optim import torch.nn.functional as F from torch.utils.data import DataLoader, random_split from torchvision import transforms import pytorch_lightning as pl from pytorch_lightning import LightningModule, Trainer from pytorch_lightning.callbacks.early_stopping import EarlyStopping #pytorchとpytorch_lightningのバージョン確認 print(torch.__version__) print(pl.__version__) 実行結果 1.7.1 1.3.6 2-2.データローダー作成まで """通常のPytorch同様な処理を行う(適当に)""" #前処理+Tensor化 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize( (0.5, 0.5, 0.5), # RGB 平均 (0.5, 0.5, 0.5)# RGB 標準偏差 ) ]) #CIFAR10データセットをダウンロード(train+test用) 直下のdataフォルダに格納する train = torchvision.datasets.CIFAR10(root='./data', train=True,download=True, transform=transform) testset = torchvision.datasets.CIFAR10(root='./data', train=False,download=False, transform=transform) #更にtest用のデータセットをvalidation,test用へ分ける n_val, n_test = int(len(testset)*0.9), int(len(testset)*0.1) #valid9割、test1割 val, test = torch.utils.data.random_split(testset, [n_val, n_test]) #データローダーをそれぞれ準備 trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True, num_workers=2) valloader = torch.utils.data.DataLoader(val, batch_size=32,shuffle=False, num_workers=2) testloader = torch.utils.data.DataLoader(test, batch_size=32,shuffle=False, num_workers=2) #データ数確認 print(len(train)) print(len(val)) print(len(test)) 実行結果 50000 9000 1000 2-3.LightningModuleでクラス定義 PyTorch Lightningでは、ニューラルネット/損失関数/オプティマイザを1つのLightningModuleを継承したクラスにまとめて定義する。細かい中身はコメント参照。 ※なお、この中に上で作成したデータローダーを入れることもできるが今回はそれをしていない。(なぜか私はエラーになってしまったので) """ ・torch.nn.functional形式 ・self.log()はLightningModuleのlogでデフォルトはTensorBoard形式 ・(必須)と(無くてもいいoption)があるので色々試してほしい """ class My_litmodel(pl.LightningModule): """★(必須)initは通常のPytorchと同じ★""" def __init__(self): super(My_litmodel, self).__init__() # 畳み込み層の定義 self.conv1 = nn.Conv2d(3, 6, 5) # コンボリューション1 self.conv2 = nn.Conv2d(6, 16, 5) # コンボリューション2 # 全結合層の定義 self.fc1 = nn.Linear(16 * 5 * 5, 120) # 全結合 self.fc2 = nn.Linear(120, 84) # 全結合 self.fc3 = nn.Linear(84, 10) # CIFAR10のクラス数が10なので出力を10にする #プーリング層の定義 self.pool = nn.MaxPool2d(2, 2) # maxプーリング """★(必須)forwardも通常のPytorchと同じでいい★""" def forward(self, x): x = self.pool(F.relu(self.conv1(x))) # conv1~relu~pool x = self.pool(F.relu(self.conv2(x))) # conv2~relu~pool x = x.view(-1, 16 * 5 * 5) # 平坦化(1次元化する) x = F.relu(self.fc1(x)) # fc1~relu x = F.relu(self.fc2(x)) # fc2~relu x = self.fc3(x) #fc3~10分類 return x """★(必須)学習設定(training_step)★""" def training_step(self, batch, batch_idx): img, label = batch out = self(img) #これでforward部分を呼び出す ※self.forward(img)でもOK loss = F.cross_entropy(out, label) #nn.CrossEntropyLoss()でもいい #logの設定。Tensorbordで見たいように設定する self.log("train_loss", loss, on_step=False, on_epoch=True, prog_bar=True, logger=True) return loss """ ★(必須)バリデーション設定(validation_step)★ stepは各イテレーションごとの結果 """ def validation_step(self, batch, batch_idx): img, label = batch pred = self(img) #これでforward部分を呼び出す val_loss = F.cross_entropy(pred, label) #loss計算 # 正解率(acc)の算出 pred_label = torch.argmax(pred, dim=1) val_acc = torch.sum(label == pred_label) * 1.0 / len(label) #logの設定 self.log('val_loss', val_loss, on_step=False, on_epoch=True, prog_bar=True, logger=True) self.log('val_acc', val_acc, on_step=False, on_epoch=True, prog_bar=True, logger=True) results = {'val_loss': val_loss, 'val_acc': val_acc } return results """ ★(無くてもいいoption)バリデーションループの値出力★ validation_endはエポック毎の集計 """ def validation_end(self, outputs): avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean() avg_acc = torch.stack([x['val_acc'] for x in outputs]).mean() results = {'val_loss': avg_loss, 'val_acc': avg_acc} return results """ ★(無くてもいいoption)test設定★ ※log名称以外中身はvalidation_stepと同じ """ def test_step(self, batch, batch_idx): #testの実行;最終的なlossとaccの確認 img, label = batch pred = self(img) #これでforward部分を呼び出す test_loss = F.cross_entropy(pred, label) # 正解率の算出 pred_label = torch.argmax(pred, dim=1) test_acc = torch.sum(label == pred_label) * 1.0 / len(label) #logの設定 self.log('test_loss', test_loss, prog_bar=True, logger=True) self.log('test_acc', test_acc, on_step=False, on_epoch=True, prog_bar=True, logger=True) results = {'test_loss': test_loss, 'test_acc': test_acc} return results """ ★(無くてもいいoption)テストループの値出力 ※log名称以外中身はvalidation_endと同じ """ def test_end(self, outputs): avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean() avg_acc = torch.stack([x['test_acc'] for x in outputs]).mean() results = {'test_loss': avg_loss, 'test_acc': avg_acc} return results """★(必須)オプティマイザーの設定★""" def configure_optimizers(self): return optim.Adam(self.parameters(), lr=0.001) #オプティマイザ 2-4.学習 2-3で定義したクラスを使用して学習をする。 もちろんfor文なんて書かなくていいし、GPUへのデータ転送等も書かなくていいので楽ちん。 この部分が楽できるのがPyTorch Lightningと通常のPyTorchの大きな違いである Trainerには引数オプションがいくつかあるが、詳細はドキュメント参照。 #乱数初期値固定 pl.seed_everything(0) #作成したクラスのインスタンス化 ※GPU使用でも「.to(device)」は不要 net = My_litmodel() """ Trainerのインスタンス化 gpus:GPUを何個使用するか? ※torch.device("cuda:0")は不要 max_epochs:エポック数指定 """ trainer = Trainer(gpus=1, max_epochs=20) #Kerasチックにfit()で学習をスタート trainer.fit(net, trainloader, valloader) 2-4.結果確認 学習が終わったら結果を確認する。 #self.logを設定した内容が表示される print(trainer.callback_metrics) 実行結果 {'val_loss': tensor(1.0951, device='cuda:0'), 'val_acc': tensor(0.6247, device='cuda:0'), 'train_loss': tensor(1.1489, device='cuda:0')} 次に、lossやaccの経過を表示する。 デフォルトのlogがtensorboardなので、jupyterからはマジックコマンドで呼び出す。 ※logはデフォルトだと直下に「lightning_logs」というフォルダが出来ていて、その中に存在 """ マジックコマンドでtensorboardをJupyter内で呼び出す 確認したい学習時のlossをRunsから選択すれば、Notebook内で推移が確認できる """ %reload_ext tensorboard %tensorboard --logdir ./lightning_logs --bind_all --port 6006 2-5.テストデータを確認 まずは全テストデータの正解率がどんなもんか?を確認しておく。 #まとめて一気に確認 ※Class内のtest_stepで定義した部分が使用されている test_result = trainer.test(test_dataloaders=testloader) print(test_result) 実行結果 [{'test_loss': 1.0554693937301636, 'test_acc': 0.6190000176429749}] うーん、6割なので全然ですね。 まぁ本記事はやり方だけ学べればいいので、次は1枚の画像に関しての予測をしてみる。 #CIFER10のラベルをlistで用意 classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') #1個ずつ確認 index = 50 #何番目のテストデータセットを確認したいか?を指定 #データセットから__getitem__で中身を1個取得し、viewで入力形式を変更(1枚を付け加える) test_picture = test.__getitem__(index)[0].view(1, 3, 32, 32) test_picture_label = test.__getitem__(index)[1] #取り出した画像を予測させる test_picture_pred = net(test_picture) print(test_picture_pred) print("正解ラベルは" + str(classes[test_picture_label])) # 予測ラベルを表示 print("予測ラベルは" + str(classes[torch.argmax(test_picture_pred)])) 実行結果 tensor([[-0.4176, 4.6303, -2.1115, -2.8112, -4.2020, -4.5353, -6.8572, -5.9206, 0.7465, 1.0103]], grad_fn=<AddmmBackward>) 正解ラベルはcar 予測ラベルはcar こんな感じで1枚1枚の結果確認もできる。 2-6.モデル保存と読み出し 通常と変わらない #保存 torch.save(net.state_dict(), 'cifer.pt') #読み出し net.load_state_dict(torch.load('cifer.pt')) 3.EarlyStoppingを2通りで実装する 以前以下のような記事を書いたが、実はPyTorch Lightningだともっと簡単に使うことが出来る。 Trainerのインスタンス化の個所を以下のように変更するだけで簡単に使用できる。 なお、方法は2パターンあるがearly_stop_callback=Trueだと色々設定できないのでcallbackの方がいい? from pytorch_lightning.callbacks.early_stopping import EarlyStopping """ ★EarlyStoppingのcallbackパラメータ★ monitor:何を監視するか patience :何回更新しないと発動するか? verbose:進捗表示の有無 mode:lossがどうしたらカウントするか?min一択(maxはよくわからん) """ #callback利用の場合 early_stop_callback = EarlyStopping( monitor='val_loss', patience=1, verbose=True, mode='min' ) #callback利用の場合 trainer = Trainer(gpus=1, max_epochs=10000, callbacks=[early_stop_callback]) """early_stop_callback利用の場合は以下1行だけでOK""" # trainer = Trainer(gpus=1, max_epochs=10000, early_stop_callback=True) trainer.fit(model, trainloader, valloader) 以下のように、毎Epochの経緯も表示設定すれば確認可能。 4.おわりに 今回はPyTorch Lightningを使ってみました。 やはり自分でforiループ書かなくていいのは便利だし、callbackが手軽に利用できるのも魅力だと感じました。だんだんKerasの方が簡単!なんて言えなくなる時代が近づいているのかもですね。 それでは引き続きよきPyTorchライフを! <おまけ> 公式のチュートリアルにもCIFER10の画像分類があって、こっちは94%の精度らしいですし。 中読んで理解できる人は読んでみてもいいかもしれません。 cifar10_normalizationってのがあるんですね。こっち使えばよかったかも・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GCP Natural Languageでブログのカテゴリを自動で付けてみた

GCP Natural Language GCP Cloud Natural Language APIの「コンテンツの分類(Classifying Text)API」を使うと、テキストを「Arts & Entertainment」や「Computers & Electronics」といったカテゴリに自動で分類できます。GCPの構築済モデルを使うので、特に機械学習の準備は必要ありません。 このコンテンツ分類機能は日本語には対応していませんが、同じくGCPのCloud Translationを使って日本語を英語に変換してからであれば使うことができます。機械翻訳を介しても実際に実用に足る精度が出るのかどうか、自分のブログの記事を実際に分類することで試してみました。 環境 Python 3.9.5 google-cloud-language v2.0.0 本稿に掲載している実行結果は2021/06/12時点のものです。 なお実際には、下記記事のようにDocker+Poetryで環境構築して実行しています。 Google Colabを使えばPython環境を構築する事なく実行できますが、デフォルトで入っているgoogle-cloud-languageとgoogle-cloud-translateライブラリのバージョンが古くてうまく行かないので、アップグレードする必要があります。 !pip install --upgrade google-cloud-language google-cloud-translate ソースコード 翻訳部分 from google.cloud import translate_v2 def translate_text(text: str) -> str: client = translate_v2.Client() result = client.translate(text, target_language="en") # type: ignore return result['translatedText'] # type: ignore カテゴリ分類部分 from google.cloud import language_v1 def sample_classify_text(text: str): client = language_v1.LanguageServiceClient() document = language_v1.Document( content=text, type_=language_v1.Document.Type.PLAIN_TEXT ) response = client.classify_text({'document': document}) # type: ignore return response カテゴリ分類結果 25の大カテゴリ・620の小カテゴリに分類されます。具体的にどんなカテゴリがあるのかは公式ドキュメントに記載があります。 これを実際に自分がブログで書いた記事に適用してみて、最もConfidenceが高かったカテゴリの一覧が下記です。 記事 category confidence Fastlyが落ちた時アマゾンはDNSフェイルオーバーしてた(かもしれない) /Internet & Telecom/Web Services 0.9700000286 Ruby on RailsでHeadless CMSを作った /Computers & Electronics/Programming 0.9700000286 七つの大罪とアルストとマスターオブマスターの対応表 /Games/Roleplaying Games 0.9399999976 ポケモン25周年動画の元ネタを全部調べた /Games/Computer & Video Games 0.8600000143 ブログをAWS AppRunnerに移行したかった /Internet & Telecom/Web Services 0.8299999833 Microsoft Ignite 2021でPokémon GO on HoloLens技術デモ映像が披露 /Games/Computer & Video Games 0.8299999833 Microsoft Build 2021基調講演を聴講 /News 0.7799999714 JavaScriptで自動音楽生成してみた /Computers & Electronics/Software/Multimedia Software 0.7400000095 ブログ作ってみた /Online Communities/Blogging Resources & Services 0.7099999785 ファイナンシャルプランナー(FP)の勉強をして良かったこと /Finance/Accounting & Auditing/Tax Preparation & Planning 0.6700000167 VS Code 1.53はCentOS7で動かない /Computers & Electronics/Software 0.6600000262 Zenn書いてみた /Science/Computer Science 0.6299999952 Ghost of Tsushimaがおもしろい /Games/Computer & Video Games 0.5500000119 私のブログはIT系の話とゲームの話がほとんどなのですが、たとえば 「Microsoft Build 2021」というイベントを見たその日に書いた記事を "News" と分類している 「Xenoblade 2」と「KINGDOM HEARTS」というロールプレイングゲームを取り上げた記事を "Roleplaying Games" と分類できている 以上より、分類の精度は結構高いように感じます。 特に、「Microsoft Ignite 2021」という技術系イベントで「Pokémon GO on HoloLens」というゲームネタを取り扱った「Microsoft Ignite 2021でPokémon GO on HoloLens技術デモ映像が披露」という記事があるのですが、この分類結果は以下のようになっていました。 Category Confidence /Games/Computer & Video Games 0.8299999833106995 /Science/Engineering & Technology 0.6499999761581421 /Computers & Electronics 0.5699999928474426 /Arts & Entertainment 0.5400000214576721 ゲーム系と技術系の両方のカテゴリをバランス良く予測結果として返しているのが分かります。 一方で「/Computers & Electronics/Programming」と「/Computers & Electronics/Software」のように、小カテゴリの細かい分類の違いがよくわからないものが多いです。実際に私のブログのカテゴリとしてはそこまで細かい分類は不要なので、いくつかをまとめて「Technology」や「VideoGame」といった独自のカテゴリにマッピングしたものを表示しています。 まとめ GCP Cloud Natural Languageを使うことで、ブログのカテゴリを自動的に分類できました。 参考 https://cloud.google.com/translate/docs/basic/quickstart?hl=ja
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Perl 脳のための Python メモ

こういうのは Python が書けると書けないかもしれないので書けないうちに書いておく。入出力やオブジェクト指向など不足部分はたくさんあるが、とりあえずできたところまで。当たり前だが、Python が書ける人には役に立たない。本当に知らない内容を調べながらなので、間違いは優しく指摘していただけるとありがたい。 英語が読める人は、まず最後の参考でも紹介している Python for Perl Programmers をご覧になるといいと思う。 バージョン 気にしだすとキリがないので Python 3 を対象にしている。 perldoc perldoc に相当するのは pydoc。perldoc の -f, -v, -q などに相当するオプションはないように見える。 関数を調べる: perldoc -f pydoc3 open ステートメントも調べられる pydoc3 for モジュールを調べる pydoc3 fileinput . でつなげることで、モジュール内のメソッドを指定することもできる。 pydoc3 fileinput.filename FILES セクションにモジュールのパスが書かれている。 モジュールのパスを調べる: perldoc -l pydoc 出力の FILES セクションに書かれている。 モジュールのソースを表示する: perldoc -m 不明。FILES セクションのファイルを直接開く。 perlrun コマンド行オプション -e, -E コマンドラインでスクリプトを指定するのは -c オプション。 -M モジュールは -m オプションで読み込む。 -d デバッグはオプションではなく pdb というモジュールで行う。 python3 -m pbm a.py デバッグではないが、-i オプションを指定すると、スクリプトの実行後に対話型モードに入る。 変数 / 名前空間 use / require import を使って別モジュールを利用する。use のように自動的に名前空間に取り込まれることはないので require に近い。identifier に * を指定すると、__all__ があればその内容、なければすべてが import される(使うなと言われている)。 import foo # use foo(); / require foo; from foo import * # use foo; import foo.bar.baz # use foo::bar::baz(); import foo.bar.baz as fbb # use aliased 'foo::bar::baz' => 'fbb'; # use Package::Alias 'fbb' => 'foo::bar::baz'; from foo.bar import baz # use foo::bar::baz qw(baz); from foo import attr # use foo qw(attr) package ファイルがモジュールに対応し、宣言する必要も方法もない。グローバルというのは、モジュール内でグローバルという意味。別のモジュールを参照する場合は import する。Python にも「パッケージ」はあるが別のもの。 my 対応するものはない。関数内で代入すると自動的にローカルになる。グローバルで扱いたい場合には global 宣言する。また、関数以外のブロックスコープのようなものは存在しない1。 参照する場合は、ローカルになければグローバル変数が参照される。しかし、スコープ内で代入していると、関数内のすべてがローカルになる。つまり次のコードは i = 42 の行がなければグローバル変数を参照するので問題ないが、あることで実行時に未初期化エラーになる。JavaScript で var が嫌われるのと同質の問題。 i = 0 def f(): print(i) i = 42 f() our 関数内での our 宣言は global に近い意味を持つが、Perl の場合トップレベルで my で宣言するのが普通なので、あまりそのような使い方はしない。 一般的な用法に対応するものはない。特に宣言しなくても import すれば、別モジュールのデータにアクセスできて、それを防ぐ手段はない。常に no strict 'vars' の状態か。 import される側で __all__ を定義してあると * で import されるのを防ぐことはできるが、直接指定すればアクセス可能。 local Python はレキシカルスコープなので、ダイナミックスコープの変数を宣言することはできない。 nonlocal 非ローカルスコープを参照するための宣言。 state 同じものはない。 ジェネレータで代用できることもある。 # sub nextint { state $i++ } def nextint(next=0): while 1: yield next next += 1 クロージャー ローカルスコープの関数を作って、それを返すことでクロージャーを実現できる。必要なくても名前はつけなければならないらしい。関数の外側のスコープを参照するために nonlocal 宣言する。 # do { my $i; sub { ++$i } } # sub { ++(state $i) } } def counter(): i = 0 def c(): nonlocal i i += 1 return i return c 文字列マッチ: m// 単純なマッチ: m// 正規表現を使うには re モジュールを使う。パターンで使用する r で始まるのは raw string と呼ばれ、バックスラッシュをエスケープする必要がないだけで、普通の文字列リテラル。 re.search import re if (re.search(r'\d+', '123-4567')): # if ("123-4567" =~ /\d+/) { print("found") m// に相当するのは re.match ではなく re.search。re.match は、文字列の先頭からマッチする。re.Match オブジェクトが返る。 str.find 固定文字列の場合は、str オブジェクトのメソッドが使える。 if ("abcde".find("bcd") > 0): print("found") in 文字列に対して in オペレータを使うこともできる。 if ('bcd' in 'abcde'): print("found") str.endswith 末尾マッチは str.endswith でも可能。 name = 'a.out' if name.endswith('.out'): print("executable") キャプチャーグループ: (...) m = re.search(r'(\d+)-(\d+)', "123-4567") m.group(0) # 123-4567 m.group(1) # 123 m.group(2) # 4567 繰り返しマッチ: m//g re.finditer re.Match オブジェクトを返すイテレータ。 # while ('123-4567' =~ /\d+/g) { for m in re.finditer(r'\d+', '123-4567'): print(m.group(0)) re.findall 文字列のリストを返す。 # @all = '123-4567' =~ /\d+/g; all = re.findall(r'\d+', '123-4567') # ['123', '4567'] all = re.findall(r'(\d)(\d*)', '123-4567') # [('1', '23'), ('4', '567')] %+, %LAST_PAREN_MATCH re.Match オブジェクトの re.Match.start, re.Match.end メソッドを使う。 for m in re.finditer(r'\d+', '123-4567'): print(m.start(0), m.end(0)) # 0 3 # 4 8 pos() 相当するものが見当たらない。re.finditer などの引数でマッチする範囲を指定することはできる。参照であれば、前述の re.Match.end で代用は可能か。そもそも Perl でも pos を使ったことがある人は少ないだろう。 複数行マッチ: m//m 3番目の引数に re.M あるいは re.MULTILINE を指定する。 # while (/^\d+/m) { for m in re.finditer(r'^\d+', "123\n4567\n", re.M): print(m.group(0)) これは re.finditer(r'^\d+', "123\n4567\n", flags=re.M) と書いても同じだ。詳しくは Python の名前付き引数について調べるべし。 flags のパラメータは、1文字であれば Perl 互換なのでそれを使うのがわかりやすい。しかし re.S は re.DOTALL、re.X は re.VERBOSE という具合で、ロングネームはちょっと違う。 大文字小文字を無視: m//i re.I あるいは re.IGNORECASE を指定する。フラグを複数指定するためには | で連結する。 # while (/^abc/im) { for m in re.finditer(r'^abc', 'abc\nABC', re.I|re.M): print(m.group(0)) qr/.../ 事前にコンパイルするためには re.compile を使う。 # my $startdigit = qr/^\d+/m; import re startdigit = re.compile(r'^\d+', re.M) print(re.findall(startdigit, "123-4567\n234-5678")) # ['123', '234'] 文字列置換: s/// Python では、文字列データを編集することはできず、常に新しい文字列を生成する。s///r がデフォルト動作だと思えばいい。 str.replace 固定文字列の置換は str オブジェクトの replace メソッドでできる。複数ある場合はすべて置換され、3番目の引数で回数を指定できる。 'abcdefg'.replace('cde', '_') # 'ab_fg' 'ababab'.replace('ab', 'xx') # 'xxxxxx' 'ababab'.replace('ab', 'xx', 2) # 'xxxxab' re.sub 正規表現による文字列置換には re.sub を使う。 print(re.sub(r'\d+', 'XXXX', "12-345-6789")) # s/\d+/XXXX/g # XXXX-XXXX-XXXX デフォルトで /g をつけたので同じ動作になるので、回数を制限したければ、3番目の引数に指定する。 print(re.sub(r'\d+', 'XXXX', "12-345-6789", 1)) # s/\d+/XXXX/ # XXXX-345-6789 print(re.sub(r'\d+', 'XXXX', "12-345-6789", 2)) # XXXX-XXXX-6789 スクリプトの実行結果で置換する: s///e 置換文字列の部分には関数を指定することができる。関数には re.Match オブジェクトが渡され、その結果に置換される。 import re def quote(m): return '[' + m.group(0) + ']' print(re.sub(r'\d+', quote, "12-345-6789")) # s/(\d+)/quote($1)/ge # [12]-[345]-[6789] 当然、lambda 式も書ける。 print(re.sub(r'\d+', lambda m: '[' + m.group(0) + ']', "12-345-6789")) # [12]-[345]-[6789] が、lambda 式には式しか書けないので、複雑な処理を記述することはできない。 パターンの方には r''' 文字列と re.X を使って s///x 相当のことはできるので、複雑なパターンを記述することはできる。 Unicode プロパティ 正規表現で Unicode プロパティを使うためには re ではなく、regex モジュールを使わなければならないようだ。日本語を処理する場合使えないと結構困る。 参考: 正規表現での漢字マッチをUnicodeプロパティーを使って綺麗に書く方法 in Python - Qiita printf, sprintf 似たような機能がいくつもある。% 演算子は古くからあって f-文字列は比較的最近導入されたものらしい。 % 演算子 sprintf に近い感覚で使える。 "Hello %s" % "World!" "%s %s!" % ("Hello", "World") "%10s: %d (%3d%%)" % ("Tokyo", 22, 22/55*100) # Tokyo: 22 ( 40%) str.format {数字}: プレースホルダー 'Hello {}!'.format('World') '{} {}!'.format('Hello', 'World') '{1} {0}!'.format('World', 'Hello') # Hello World! キーワード引数 print("{say} {who}!".format(say='Hello', who='World')) vars 関数 say='Hello' who='World' print("{say} {who}!".format(**vars())) f-文字列 say='Hello' who='World' f'{say} {who}!' # Hello World! 書式指定 %s 'Hello {:10s}!'.format('World') # Hello World ! 'Hello {:<10s}!'.format('World') # Hello World ! 'Hello {:>10s}!'.format('World') # Hello World! 'Hello {:^10s}!'.format('World') # Hello World ! %d '1+2={}'.format(1+2) # 1+2=3 '1+2={:d}'.format(1+2) # 1+2=3 '1+2={:3d}'.format(1+2) # 1+2= 3 '1+2={:<3d}!'.format(1+2) # 1+2=3 ! '1+2={:>03d}!'.format(1+2) # 1+2=003! '1+2={:<03d}!'.format(1+2) # 1+2=300! リスト操作 push, pop, shift, unshift a.pop() # pop @a a.pop(0) # shift @a a.append(50) # push @a, 50 a.insert(0,10) # unshift @a, 10 splice slice で同様のことができる。文字列と違って代入もできる。 s=list(range(10)) s[:2] # [0, 1] s[2:4] # [2, 3] s[-2:] # [8, 9] s[::2] # [0, 2, 4, 6, 8] grep grep 相当の filter と、そのままの map があるが、返すのはリストではなくイテレータオブジェクトであることに注意。内包表記 (comprehension) で書くこともできるし、その方が望ましいとされているらしい。 # grep { $_ % 2 == 0 } 0..9 list(filter(lambda x: x % 2 == 0, range(10))) [ x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8] map # map { $_ * 2 } 0..9 list(map(lambda x: x * 2, range(10))) [ x * 2 for x in range(10) ] # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] [ x if x % 2 == 0 else 'odd' for x in range(10) ] # [0, 'odd', 2, 'odd', 4, 'odd', 6, 'odd', 8, 'odd'] reverse reversed もイテレータを返す。 list(reversed(range(10))) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] array.reverse は、リストの内容を反転する。 l=list(range(10)) l.reverse() # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] sort sorted を使う。リストの内容を操作するためには sort メソッドを使う。key 関数で比較対象を指定することができる。 sorted(n) # sort @n sorted(n, reverse=True) # sort { $b <=> $a } @n sorted(s, key=len) # sort { length($a) <=> length($b) } @s sorted(s, key=str.lower) # sort { lc($a) cmp lc($b) } @s sorted(s, key=lambda x:x[1:]) # sort { substr($a,1) cmp substr($b,1) } @s Perl で比較関数を指定する場合、効率を求めるなら Schwartzian transform とかを使うわけだが、Python の key 関数にはそのような問題はない。key 関数で複数の値を返すことで、複数キーでのソートも可能。 文字列操作 chop, chomp: 削除する s = s[:-1] # 最後の文字を削除する s.replace("\n", "") # (すべての)改行を削除する if s.endswith("\n"): s = s[:-1] # 最後の改行を削除する re.sub(r"\n\Z", '', s) # 最後の改行を削除する なぜ \z ではないかというと、Python の正規表現に \z は存在しないから。"\n\n" で終わってる場合、どちらの改行が削除されるのかわからなくて気持ち悪くても我慢しよう 2。 s.strip() # 行頭と行末の空白をすべて削除する s.lstrip() # 行頭の空白をすべて削除する s.rstrip() # 行末の空白をすべて削除する join: 連結する リストを文字列で連結する '-'.join(['foo', 'bar', 'baz']) # join('-', qw(foo bar baz)) # 'foo-bar-baz' split: 分割する split '-', ... 固定文字列で分割するなら str の split メソッド。 '12-345-6789'.split('-') # ['12', '345', '6789'] split /re/, ... 正規表現で分割するなら re を使う。 import re re.split(r'[-+]', '12-345+6789') # ['12', '345', '6789'] re.split(r'([-+])', '12-345+6789') # ['12', '-', '345', '+', '6789'] split //, ... str.split に空文字列を渡すとエラーになる。 文字列を1文字ずつ分割なら list に渡せばいい。 # split(//, "abcde") list("abcde") # ['a', 'b', 'c', 'd', 'e'] いろいろあってややこしい。 substr slice を利用すれば、だいたい同じことができる。ただし、代入はできない。(offset, length) ではなく (start, end) であることに注意。 s = 'abcdef' s[2] " 'c' s[:2] # 'ab' s[2:4] # 'cd' s[-2:] # 'ef' s[::2] # 'ace' die, exit quit(), exit() quit か exit を使えと書いてある資料が多いが、公式ドキュメントには使うなと書いてある。 sys.exit() Perl の die に近いのは sys.exit か。内部的には例外を発生させるだけなので try 文で捕捉することもできる。 import sys sys.exit(1) # status 1 で終了 sys.exit("bye") # メッセージを表示して status 1 で終了 os._exit() 直接的に終了したければ os._exit を使う。 raise 例外を発生させるためには raise を使う。 eval eval, exec Python の eval が対象とするのは式だ。Python スクリプトを評価したい時には exec を使う。 eval('a = 1 + 2') # ERROR! exec('a = 1 + 2') # OK a = eval('1 + 2') # OK 2番目以降の引数で名前空間を指定することができる。 変数 @INC sys.path @ARGV sys.argv モジュール Getopt::Long argparse import argparse import fileinput def main(): global args parser = argparse.ArgumentParser() parser.add_argument('files', nargs='*') parser.add_argument('--number', '-n', action='store_true') args = parser.parse_args() with fileinput.input(args.files) as f: process(f) List::Util min, max, sum ママ any 文字列がリストに含まれるかは in で調べられる。 # if (any { $_ eq 'bar' } qw(for bar baz)) { if ('bar' in [ 'foo', 'bar', 'baz' ]): print("yes") Python には集合を扱うための set 型がある。 foobar = { 'foo', 'bar', 'baz' } if ('bar' in foobar): print("yes") reduce functools.reduce を使う。 zip zip はある。 pairmap スマートだが、長さが違う場合には要注意。 # pairmap { say($a, $b) } @data # while (my($a, $b) = splice @data, 0, 2) { data = [1,2,3,4,5,6] for a,b in zip(data[0::2], data[1::2]): print(a, b) 参考 Python for Perl Programmers これはよい。筆者が Phython の参考書には Perl プログラマが知りたいことがちっとも書いてないことを不満に思い、第一章に書いとけという内容をまとめたというページ。量的には少ないのですぐ読めて役に立つ代わりに、リファレンスにはならない。 PerlPhrasebook もっと長い文章が読める人は PerlPhrasebook という資料がある。Guido van Rossum、Tom Christiansen、Larry Wall も協力しているので内容に間違いはない。ただ、バージョン 2.4 の頃に書かれたものでかなり古い。また、データ構造に関する記述が多いためか、一通り目を通してはみたが案外求めることは書かれていない印象だ。ベースになっているのが Perl Data Structures Cookbook ということなので仕方ない。ただ、これを読む暇があったら、普通の入門資料を読んだ方が早いんじゃないだろうか。 PLEAC Project PLEAC Project は、Perl Cookbook の内容を様々なプログラミング言語に翻訳するものだ。Python 版は pleac_python。しかし、内容はすべて Python になっているので、単なる Python の優れた文献となっているはず。両者を比較して読めば役に立つに違いないので、時間がある方はどうぞ。 PerlユーザのためのPython移行ガイド まさにそのもずばりの本だが、いかんせん2002年と古すぎる。読んでないので、コメントあればお願いします。 Equivalents in Perl and Python 配列操作の比較表: Ruby, Python, JavaScript, Perl, C++ 各言語について配列操作を比較したもの。逆引き的な使い方は簡単ではないが、参考にはなる。Perl 視点で整理したらいいかも。 lambda 式や内包表記など明示的に指定するものは別。 ↩ これについて調べてたら、こんな記事を発見して、ちょっと笑った :-)。「違いませんか」ってどういう意味だろう?日本語って難しい。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AutoGluon 0.2.0によるAutoML環境(Docker前提)の作り方

1.はじめに AutoGluonは、その性能の良さから2020年に話題になった1AutoMLツールである。例えば、とあるKaggleコンペで99%の人間に勝ったという話2もある。 翌2021年4月にはバージョン0.2.0が公開3されたが、環境によってはマニュアル通りにインストールするだけでは十分正しい動作がなされないことがある。 本記事では、AutoGluonを実行するDockerコンテナと、それをVSCodeから利用するための手順を解説する。 2.AutoGluon用のDockerfileの作り方 本章では、AutoGluonの公式インストール手順によって起こりうる問題点を指摘し、その代案となるインストール方法をDockerfileに記述する方法を述べる。 2.1.公式のインストール手順の問題点 2021年6月現在の公式サイト4によると、Linux環境では以下のコマンド実行によりAutoGluonがインストール可能としている。ここでGPUを利用する場合は、導入しているCUDAのバージョンに対応したGPU版mxnetをかわりにインストールすればよい。 python3 -m pip install -U pip python3 -m pip install -U setuptools wheel python3 -m pip install -U "mxnet<2.0.0" python3 -m pip install autogluon しかし、筆者が「nvidia/cuda:10.1-cudnn7-runtime-ubuntu18.04」のDockerイメージを利用し、Python3.7を用いて一連のインストールを試みたところ、以下2点の問題が生じることがわかった。 AutoGluonが、Jupyter用のtqdmのプログレスバーを表示する際に、AttributeError: 'tqdm' object has no attribute 'disp'が発生して異常終了する。 Daskのプロファイラ画面など、bokehを利用する画面が出力できない。 前者の原因は、マニュアル通りのインストール方法では適切なバージョンのipywidgetsがインストールされないこと5である。また後者の原因は、同じく適切なバージョンのbokehがインストールされないことである。よって、これらが予め適切にインストールされている環境でなければ、本件はLinux環境全般で再現する恐れがある。 2.2.requirements.txtの設定 前節で挙げた問題の対策として、以下のrequirements.txtを作成した。筆者がGPU環境を利用しているため、ここではmxnet-cu101をインストールしているが、CPU環境なら代わりにmxnetをインストールすればよいはずである。 autogluon==0.2.0 bokeh==2.1.1 dask==2021.5.0 ipywidgets==7.6.3 mxnet-cu101==1.8.0.post0 tqdm==4.60.0 2.3.Dockerfileの定義 先に述べたとおり、今回はDocker環境上でAutoGluonを実行する。そのためのDockerfileは以下の通り定義した。ここでは、VSCodeとの連携に必要なユーザ設定などもあわせて行っている。 なお、本Dockerfileの作成にあたってはQiitaの記事「VS CodeでDocker開発コンテナを便利に使おう」を参考にした。 ARG nvidia_cuda_version=10.1-cudnn7-runtime-ubuntu18.04 FROM nvidia/cuda:${nvidia_cuda_version} ARG USERNAME=vscode ARG USER_UID=1000 ARG USER_GID=$USER_UID ARG REQUIREMENTS_TXT=requirements.txt RUN apt update && apt -y full-upgrade \ && apt -y install build-essential swig \ && apt -y install apt-utils \ && apt -y install --no-install-recommends apt-utils dialog 2>&1 \ && apt -y install git iproute2 procps lsb-release \ && apt -y install \ python3.7 \ python3-pip \ python3-venv \ python3.7-venv \ && apt -y install python3.7-dev \ && python3.7 -m pip install -U pip \ && python3.7 -m pip install setuptools wheel \ && groupadd --gid $USER_GID $USERNAME \ && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ && apt -y install sudo \ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ && chmod 0440 /etc/sudoers.d/$USERNAME \ && rm -rf /var/lib/apt/lists/* COPY $REQUIREMENTS_TXT /tmp/ RUN python3.7 -m pip install -r /tmp/$REQUIREMENTS_TXT 3.VSCodeからDocker上のAutoGluonを利用するための手順 前章で作成したDockerfileを用いて、VSCodeから利用できるAutoGluon環境を構築・利用する手順を説明する。本章の執筆にあたっては、先に紹介した「VS CodeでDocker開発コンテナを便利に使おう」を大いに参考とした。 3.1.拡張機能「Remote-Containers」のインストール Dockerコンテナ上のPythonやJupyterをVSCodeから利用するためには、拡張機能「Remote-Containers」6が必要となる。これは、VSCodeのExtensionsタブから直接インストールすることができる。 3.2.devcontainer.jsonの定義 次に、Dockerコンテナを利用するための設定ファイルである、devcontainer.jsonを作成する。本ファイルには、DockerコンテナとVSCodeの連携に必要な設定や、利用したい拡張機能の情報を列挙していく。その記述例を以下に示す。 ただし、"runArgs": ["--gpus", "all"],の部分はDocker側からGPUを利用するための設定なので、CPUのみを利用する場合は削除が必要となる。 {"name": "autogluon", "context": "..", "dockerFile": "Dockerfile", "settings": { "http.proxySupport": "off", "jupyter.alwaysTrustNotebooks": true, "python.pythonPath": "/usr/bin/python3.7", "python.linting.enabled": true, "terminal.integrated.shell.linux": "/bin/bash" }, "appPort": [ 9000 ], "remoteUser": "vscode", "extensions": [ "ms-python.vscode-pylance", "ms-python.python", "ms-toolsai.jupyter", ], "workspaceFolder": "/home/vscode", "workspaceMount": "type=bind,source=${localWorkspaceFolder},target=/home/vscode/autogluon/,type=bind", "runArgs": ["--gpus", "all"], "remoteUser": "vscode" } 3.3.workspaceの設定 最後に、Dockerコンテナを利用するworkspaceの設定方法について述べる。 あらかじめ所望のworkspaceをVSCodeで作成したのち、そのルート直下に.devcontainerという名前の隠しディレクトリを作成する必要がある。.devcontainerの中には、これまでに作成したDockerfileとdevcontainer.jsonを配置する。また、Dockerfileが参照するrequirements.txtを、workspaceのルート直下に忘れず置く必要もある7。 まとめると、下記のファイル配置を行う必要がある。(ここでのルートはworkspaceのもの) . ├── .devcontainer │   ├── Dockerfile │   └── devcontainer.json ├── requirements.txt └── workspace.code-workspace 以上の設定をしたあとでworkspaceをVSCodeで開き直せば、Dockerイメージの作成からビルド処理が進む。ビルドが終われば、ローカル環境とまったく変わらない使い方で、コンテナ上にあるPythonを実行してAutoGluonを利用できるようになっている。 4.AutoGluonのコード実行例 前章までの手順で作ったworkspaceを使えば、AutoGluonを利用したコードを動かすことができるはずである。ipywidgetsが適切にインストールできているなら、下記のキャプチャのようにtqdmがカラーのプログレスバーを出力できるようになっている。 4.1.サンプルコードの入手方法 AutoGluon 0.2.0は、以前のバージョンのものとAPIが大きく変わっている。そのため、これより前に書かれたQiitaの記事にあるAutoGluonのコードはほぼ、そのままでは動かなくなっている。 AutoGluon 0.2.0のサンプルコードは、公式のチュートリアルページからのコピペで入手できる。もっとも単純なものは、https://auto.gluon.ai/stable/tutorials/tabular_prediction/tabular-quickstart.htmlにある。 4.2.コード実行中に表示される、誤った内容のダイアログ VSCodeでAutoGluonのコードを実行する際、処理によっては「Path does not exist」というタイトルのダイアログが表示され、「SummaryOfModels.htmlが見つからないように見える」と言われることがある。 しかしbokehが適切にインストールできていれば、下記のような内容のSummaryOfModels.htmlが、ダイアログに書かれていたパスに正しく出力されている。 5.おわりに AutoGluon 0.2.0は本来なら、AutoMLとしての強力さのみならず、画面出力機能も充実しており使いやすいツールである。しかし、現状はインストールに難のあるツールでもある。 本記事を書いたのは、こうした問題からAutoGluonが使われないことはもったいないと思ったためである。これをきっかけに、一人でも多くの方がAutoGluonを利用していただけるようになったら幸いである。 また、AutoGluonのTabularPredictorにあるfitメソッドは、scikit-learnの教師あり学習クラスのものとは互換性がない。そのため、scikit-learnのPipelineとはそのままでは連携できない。 ただし、簡単なラッパクラスを書けば互換性を実現できるそうに見えるので、今後はその開発・公開を予定している。 例えば、https://upura.hatenablog.com/entry/2020/03/18/190300を参照。 ↩ https://twitter.com/jaguring1/status/1240108860796366853 ↩ https://pypi.org/project/autogluon/0.2.0/ ↩ https://auto.gluon.ai/stable/index.html#installation ↩ 原因特定にあたっては、https://blog.csdn.net/weixin_40539826/article/details/112854159を参考にした。 ↩ https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers ↩ devcontainer.jsonのcontextで、本ファイルの一つ上のディレクトリ(workspaceのルートに相当)を指定していることが理由。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで文字を並び替えて名詞を見つける

プログラムを作成しようとしたキッカケ 毎週日曜はクロスワードパズルを解いているのですが、 解いた後に、特定マスの文字を並び変えて 名詞を探すのですが、なかなか出てこないことがあり、 Pythonに探させてはどうだろうと思った為 プログラムでやりたい事 クロスワードで出てきた文字を並び変えて、 名詞を見つける 例:  並び替え前の文字列:ティアースト  求めたい答え(名詞):アーティスト プログラム(forでゴリゴリ版) from janome.tokenizer import Tokenizer t = Tokenizer() text = "ティアースト" start = 0 end = len(text) uniq_list = [0,1,2,3,4,5] uniq_no=0 for a in range(start,end): for b in range(start,end): for c in range(start,end): for d in range(start,end): for e in range(start,end): for f in range(start,end): l = [a,b,c,d,e,f] var_list = list(set(l)) if(var_list == uniq_list): text2 = text[a] + text[b] + text[c]+ text[d]+ text[e] + text[f] for token in t.tokenize(text2): part_of_speech = token.part_of_speech.split(',')[1] if part_of_speech == u'一般': word = token.surface if(len(word)==6): print(word) uniq_no+=1 print(uniq_no) 実行結果 アーティスト 720 解説 For文をゴリゴリ回して文字列を並び変える。 Setを使った結果、 [0,1,2,3,4,5] に なったものだけ(同じ文字が重複していない) tokenizeにかける。 パターンは6!で720通りになるはず。 (念のため通った回数をカウント) token.part_of_speech.split(',')[0] で名詞のものを抜き出してしまうと なんでもかんでも出力されてしまうので、 token.part_of_speech.split(',')[1] が一般のものだけ抜き出す。 抜き出した結果が6文字のものだけ出力しました。 qiita 初投稿してみました! itertoolsを使ってプログラムを修正してみました。 from janome.tokenizer import Tokenizer import itertools t = Tokenizer() text = "ティアースト" str1="" a = list(itertools.permutations([0,1, 2, 3, 4, 5])) for i in range(len(a)): for token in t.tokenize(str1): part_of_speech = token.part_of_speech.split(',')[1] if part_of_speech == u'一般': word = token.surface if (len(word) == 6): print(word) str1 = "" for j in range(len(a[i])): str1 = str1 + text[(a[i][j])]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kivyMDチュートリアル其の弍什弍 Components - Menu篇

みなさん、おはこんばんは。 天気がグズつく日が多くなってきましたが、いかがお過ごしでしょうか。 Virtyはそれほどよい体調で過ごしてはいませんね・・・ # よい体調であったことがあまり記憶にありません KivyMDを触ってからまもなく半年が過ぎようとしているところです。えぇぇ、季節が すぎるの早くないぃですかぁぁと思ってしまいましたが、毎週毎週記事を欠かさず書け てきたのは、これまで見ていただいた方のおかげかと思っているところです。本当に 感謝、感激、高○英○(さん)であります。# 意味不明 また、しらずしらずkivyMDのComponentsが半分を過ぎていたことも今日知り意気揚々 としています。ですが、浮つくことなく、粛々とこれまで通り記事を生成して参る、所存、 で、あります。# 急にかしこまるという # モノマネでもないよ そこで、先週との伏線回収となるのですが、今日は順番通りいくとMDSwiperとなります。 しかし先週言ってたことというと、以下の通りとなります。 ということで今週はここまでということで!来週はMenu篇となります。 あれっ、MDSwiperが先なのでは?と思われた方はその通りなのですが、色々事情が あってですね・・・まぁでもそれは来週話すとします。 kivyMDチュートリアル其の弍什壱 Components - List篇より https://qiita.com/virty/items/3871b85bc67179d67b30 この理由としては、なんとも単純なワケがありましてもうお察しかもしれませんが、なんと 動かなかっただけなのですねw なので、動かなければ直しなさいよというご指摘があれば んんんんーと黙りこくってしまう(黙ってない)だけなのですが、そんな技術力は私にはない わけなのです。 # そもそもそんな技術力があれば記事を書くこともなかった これも少し調査をしまして、行き着いた結果としてはそもそも機能自体まだ用意されていなく、 KivyMDの次バージョンで直ってるみたいだぞという、まぁStackOverFlowで見たものをその まま言っているだけなのですが。。んで、伝聞をここで載せるだけというのは小学生かもしくは 幼稚園児くらいしか許されません。なので裏付けを取るべく、以下のような記述を公式マニュアル から読み取れましたので、引用しておきます。 0.104.2 ・ Added MDSwiper component [参照] Changelog https://kivymd.readthedocs.io/en/latest/changelog/ なんと・・・、これは現バージョンでは動かないはずだ・・・ あ、この記事で採用している現バージョンはずっと以下から変更せず、0.104.1になります。 # なつかしい んで、Changelogからも実際に動かしてみたことからもComponentsの後半にかけては、結構 動かないものもポロポロ出てきました。具体的な対応方針としては、来週の冒頭で案内する形 となりますが一度0.104.1で出来るところまで進めるかと思います。なので、いきなりバージ ョンを上げましたーみたいなことはないのでご安心のほどを。 ということで、長かったな、MDSwiperをスキップした理由が上記の通りとなります。まぁ、 よく分からんとなった方は単に動かなかっただけだよー、心配することないよーということ だけ分かってもらえればと思います。 ということで、今日はMenu篇となります。 Menu まぁ、恒例行事であるリンクはスキップするのですけれども(固定化)。こちらに関しては、 なじみのあるもので見たことあるという方のほうが多いのではないでしょうか。Components でも似たようなものは以前でもありましたね。はいせーの、Dropdown Itemですね(誰も 言ってなさそう)。そこでも、ここ(Menu篇)をお楽しみにと言っていましたがそちらが気になる という方は、以下のページをご覧ください。 こちらの方でもさっき言ってた影響はなくはありません。というか無茶苦茶おおありで、なんと Usageより下にあるコードは全て動きません。まぁ、これも来週の対応方針で触れることなので 一旦ここではスルーすることとします。なんにせよ、スルーしっぱなしということはありません のでご安心のほどを。 とまぁ、歴代以上前置きが長くなってしまいましたが、ここからはマニュアルのほうに戻ります。 概要としては以下のように記載があります。 Menus display a list of choices on temporary surfaces. ずっと、表示されることがないということですかね。確かにこういうウィジェットは表示されたり 消えたりします。まぁ、これも見た方が早いと思うのでコードと結果を見てみましょうか。 Usage xxii from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.menu import MDDropdownMenu KV = ''' MDScreen: MDRaisedButton: id: button text: "PRESS ME" pos_hint: {"center_x": .5, "center_y": .5} on_release: app.menu.open() ''' class Test(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) self.screen = Builder.load_string(KV) menu_items = [ { "text": f"Item {i}", "viewclass": "OneLineListItem", "on_release": lambda x=f"Item {i}": self.menu_callback(x), } for i in range(5) ] self.menu = MDDropdownMenu( caller=self.screen.ids.button, items=menu_items, width_mult=4, ) def menu_callback(self, text_item): print(text_item) def build(self): return self.screen ここもいつものように、分けて触れ込みたいと思います。 import文 今回におけるimportするパッケージは以下の通りとなります。 from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.menu import MDDropdownMenu 今回はクラス側にてMDDropdownMenuが必要なので、指定をしています。あとは、おなじみ ですかね。 kv側 同様に、kvでどのようなものが使われているか引用します。 KV = ''' MDScreen: MDRaisedButton: id: button text: "PRESS ME" pos_hint: {"center_x": .5, "center_y": .5} on_release: app.menu.open() ''' ここの定義はそれほど、触れるまでもないかもしれませんね。MDRaiseButton1つだけの 超シンプルな定義です。その中のon_releaseコールバックメソッドにapp側のopenメソ ッドが指定されていますね。これはクラス側の方を見てみるしかないようです。 Testクラス側 問答無用にTestクラス側を引用してみます。 class Test(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) self.screen = Builder.load_string(KV) menu_items = [ { "text": f"Item {i}", "viewclass": "OneLineListItem", "on_release": lambda x=f"Item {i}": self.menu_callback(x), } for i in range(5) ] self.menu = MDDropdownMenu( caller=self.screen.ids.button, items=menu_items, width_mult=4, ) def menu_callback(self, text_item): print(text_item) def build(self): return self.screen まずは、initメソッドから。ここではなにやら、menu_itemsというものがありますね。 これはなんでしょうか。1番中を注視してみると、どうやらディクショナリ型で定義している ようですね。んで、その外にこの[]括弧があるので複数のディクショナリを格納するリストの ようです。[リスト{複数のディクショナリ}]という形になりますかね。# 余計ややこしいか 中身はというと、textやらviewclass、on_releaseプロパティなどそれぞれ定義をされて います。あとは忘れてはいけませんが、rangeとあるので5回分ListItemの要素となるものを 作っていることもあります。 これはなぜこのような形をしているのかと考えると、プロパティを入れ込むにあたってはこの 形をとらざるを得なかったのでしょう。というかkv側で定義するときもpython側で色々生成 するとなると、この形を取るしかなさそうな気もします。 あとはここだけを見ると、何か見覚えありませんでしたでしょうか。そうです(誰もうんとは 言っていないと思う)、「Themes - Icon Definitions篇」になります。そこでもview- classは出てきましたが、詳細な説明はしていませんでしたけどね・・・まぁ、一応参考程度 に以下を見てもらうと嬉しいです。今回はMenuの配下としてviewclassプロパティにOne- LineListItemを指定しています。 そしてここからはMDDropdownMenuインスタンスの生成に移りますが、ここではcallerプロ パティにkv側で定義しておいたbutton、itemsには先程のmenu_items、width_multに4を 指定しています。width_multだと・・・?まぁこれは単なる横幅と考えてもらえればですね。 詳しくはAPIのところで触れておきます。 あと、少し忘れていましたがkv側でのコールバックメソッド(app.menu.open())はこのself .menuすなわちMDDropdownMenuが持っているopenメソッドのことになります。 あとは、menu_itemsの中で指定していたコールバックメソッド(menu_callback)は渡されて いたテキストを単にprintしているだけになりますね。まぁ、これも動いてないのだけれどもと いう前提はあるのですが・・・ あとは、なじみのあるscreenをリターンしているbuildメソッドなどがあります。 結果 さて、論より証拠、結果の方を見てみましょう。 まぁ、載せなくともいいような感じはありますが・・ んで、やっとメニューが開きました。全然問題はありませんね。 printメソッドの機能以外は・・ そこでさっき言ってたwidth_multプロパティを倍数の8にするとどうなるか見てみました。 広いですねぇー。タブレッドサイズならとは思いましたが、少しくどいような気もします。 API えぇ、もうAPIに入るの?と言われそうなくらい早く逃げるようにAPIに入っていますが、 動かない以上はどうしようもありません(無責任)。まぁ、安心してください。アップデート 後に触れるつもりではありますから。 class kivymd.uix.menu.MDDropdownMenu(**kwargs) on_release The method that will be called when you click menu items. 今回のサンプルでは、menu_itemsのそれぞれに指定したon_releaseプロパティなら指定 していましたが、これはどっちなんだろう。直接指定することはできるのか・・まぁ、今度 精査することとします。 items See data. items is a ListProperty and defaults to []. まぁ、これは触れるまでもありませんが、リスト型であるのは忘れてはいけません。中身に ディクショナリを持つのは構いませんが。 width_mult This number multiplied by the standard increment (56dp on mobile, 64dp on desktop, determines the width of the menu items. If the resulting number were to be too big for the application Window, the multiplier will be adjusted for the biggest possible one. width_mult is a NumericProperty and defaults to 1. どうやら、デスクトップとモバイルで使用するdpが異なるのですね。デスクトップやスマホの タイプによってサイズが異なることも注意が必要そうです。 caller The widget object that caller the menu window. caller is a ObjectProperty and defaults to None. 今回はボタンを選択しましたね。今度触れるときは違うウィジェットを選択するかもしれません。 まとめ はい、いかがだったでしょうか。 本当なら、ボタンを増やして色々挙動を見るというのもやってみたかったのですが今回は タイムアップということで、すみません。まぁ、今度触れるしで許してもらえればと思い ます。 これだけだと、何か物足りないな〜というのは私も同じ気持ちになっています。今度触れる ときのお楽しみということで、今日はこの辺で締めくくりたいと思います。 来週は予定通りいくとNavigation Drawerなのですが、これも動かない以上どうしようも ないので次回は結構飛ばしてPickers篇となります。これも忘れてはいけませんが、今後の 対応についても冒頭にて触れるようにします。 それでは、ごきげんよう。 参照 Components » Menu https://kivymd.readthedocs.io/en/latest/components/menu/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoの旅 ~Part4~ モデル作成編

目標 モデルの作成 モデルとは モデルとはデータベースのレコードをpythonのオブジェクトに割り当てる機能。 通常、データベースを操作するにはSQLを使用するがモデルを使用すればSQLを書かずともpythonのコードでデータベースのレコードをオブジェクトとして操作できる。 ※補足:このようにオブジェクトでデータベースを操作するツールをORマッパーと呼ぶ!! マイグレーションとは マイグレーションとはデータベースの中身を一括して移行したり変更する作業。 Djangoのマイグレーション は2段階で行う。 1段階目 専用のコマンドを用い、マイグレーションファイルを作成 2段階目 マイグレーションを実行 Djangoでは、モデルを作成しておくと、マイグレーション でDBのテーブルを作成することができる。 手順 1・postgresなどでDBを作成 2・settings.pyに接続するDBを登録 3・models.pyにモデル内容を書き込む 4・マイグレーションファイルを作成 5・マイグレーション実行 コード解説 1・postgresなどでDjangoと接続したいデータベースを作成 DjnagoのデフォルトではSQLite3がある。 今回はローカルでpostgresを用い、データベースmydbを作成 settings.py DATABASES = { #'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': BASE_DIR / 'db.sqlite3', #} 'default':{ 'ENGINE':'django.db.backends.postgresql', 'NAME': 'mydb', 'USER': 'USERNAME', 'PASSWORD': '', 'HOST': 'localhost', 'PORT': '5432', } } 2・settings.pyに接続するDBを登録 'ENGINE': 自分が使うDBのツール 'django.db.backends.sqlite3'、 'django.db.backends.postgresql'、 'django.db.backends.mysql' または 'django.db.backends.oracle' 'NAME':自分が接続するデータベース名 'USENAME':データベースの所有者 'PASSWPRD':パスワード 'HOST':ホスト番号 'PORT': ポート番号 models.py from django.db import models from django.db.models.fields import CharField class Artice(models.Model): content = CharField(max_length=200) def __str__(self): return self.content 3・models.pyにモデル内容を書き込む データベースのテーブル内容をモデルを利用して作成 $ python manage.py makemigrations testapp(アプリ名) 4・マイグレーションファイルを作成 ターミナルで上記を実行 今回はtestappにしているが任意のアプリ名でOK $ python manage.py migrate 5・マイグレーション実行 ここまで実行すると 接続されているデータベースにmodels.pyで記述したデーブルが作成されているはず $ python manage.py runserver ターミナルで上記のコードを実行すると以下の文章が出てくることがあるが今回の作業で解消できる。 You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. 参考文献 データベースエンジンについて https://docs.djangoproject.com/ja/3.2/intro/tutorial02/ モデルの削除・追加について https://zerofromlight.com/blogs/detail/61/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む