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

Pythonで学ぶ制御工学 第24弾:ロバスト制御

#Pythonで学ぶ制御工学< ロバスト制御 > はじめに 基本的な制御工学をPythonで実装し,復習も兼ねて制御工学への理解をより深めることが目的である. その第24弾として「ロバスト制御」を扱う. 概要 ロバスト制御 以下にロバスト制御についてまとめたものを示す. ロバスト安定化問題 次にロバスト安定化についてもまとめておく. 実装 垂直駆動アームの角度制御を例に不確かさを有する制御対象とロバスト制御器の設計について実装する.以下にソースコードとそのときの出力をそれぞれ示す. ソースコード:不確かさを有する制御対象 uncertainty.py """ 2021/04/10 @Yuya Shimizu 乗法的不確かさを有する制御対象 """ import numpy as np import matplotlib.pyplot as plt from control import bode, tf from control.matlab import logspace from tf_arm import arm_tf from for_plot import bodeplot_set ###垂直アームのノミナルモデル g = 9.81 #重力加速度 [m/s^2] l = 0.2 #アームの長さ[m] M = 0.5 #アームの質量[kg] mu = 1.5e-2 #粘性摩擦係数[kg*m^2/s] J = 1.0e-2 #慣性モーメント[kg*m^2] Pn = arm_tf(J, mu, M, g, l) ###不確かさ delta = np.arange(-1, 1, 0.1) WT = tf([10, 0], [1, 150]) fig, ax = plt.subplots(1, 2) for i in range(len(delta)): #不確かさをもつ制御対象 P = (1 + WT*delta[i])*Pn gain, _, w = bode(P, logspace(-3, 3), Plot = False) ax[0].semilogx(w, 20*np.log10(gain), color='k', lw=0.3) #乗法的不確かさ DT = (P - Pn)/Pn gain, _, w = bode(DT, logspace(-3, 3), Plot = False) ax[1].semilogx(w, 20*np.log10(gain), color='k', lw=0.3) gain, _, w = bode(Pn, logspace(-3, 3), Plot = False) ax[0].semilogx(w, 20*np.log10(gain), color='k', lw=2) gain, _, w = bode(WT, logspace(-3, 3), Plot = False) ax[1].semilogx(w, 20*np.log10(gain), color='k', lw=2) bodeplot_set(ax) ax[0].set_xlabel('$\omega$ [rad/s]') ax[0].set_ylabel('Gain of $P$ [dB]') ax[1].set_xlabel('$\omega$ [rad/s]') ax[1].set_ylabel('Gain of $\Delta W_T/P$ [dB]') fig.suptitle("Gain of control target with Certainty") plt.show() 出力 図の左が不確かさをもつ制御対象を,右がその不確かさをプロットしたものである.なお,太線のグラフは不確かさをもたないノミナルモデルである.高周波域でゲインがバラついていることが分かる.これは,入力信号の高周波成分に対する応答がバラつくことを意味する.例えば,速応性をよくするために制御器のゲインを大きくしたときに,望ましい応答が得られない(不安定化する)可能性がある. ソースコード:ロバスト制御器の設計 robust.py """ 2021/04/10 @Yuya Shimizu ロバスト制御器の設計 """ import numpy as np import matplotlib.pyplot as plt from control import bode, tf, mixsyn, ss2tf from control.matlab import logspace, feedback, step from tf_arm import arm_tf from for_plot import plot_set ###垂直アームのノミナルモデル g = 9.81 #重力加速度 [m/s^2] l = 0.2 #アームの長さ[m] M = 0.5 #アームの質量[kg] mu = 1.5e-2 #粘性摩擦係数[kg*m^2/s] J = 1.0e-2 #慣性モーメント[kg*m^2] Pn = arm_tf(J, mu, M, g, l) ###重み関数の定義 WS = tf([0, 1], [1, 1, 0.25]) #感度関数に対する重み関数 WU = tf(1, 1) WT = tf([10, 0], [1, 150]) #相補感度関数に対する重み関数 ###混合感度問題 K, _, gamma = mixsyn(Pn, w1 = WS, w2 = WU, w3 = WT) #混合感度問題を解く print('K=', ss2tf(K)) print('gamma=', gamma[0]) fig, ax = plt.subplots(1, 2) ###感度関数 Ssys = feedback(1, Pn*K) gain, _, w = bode(Ssys, logspace(-3, 3), Plot = False) ax[0].semilogx(w, 20*np.log10(gain), lw=2, label='$S$') gain, _, w = bode(1/WS, logspace(-3, 3), Plot = False) ax[0].semilogx(w, 20*np.log10(gain), ls='-.', label='$1/W_S$') ###相補感度関数 Tsys = feedback(Pn*K, 1) gain, _, w = bode(Tsys, logspace(-3, 3), Plot = False) ax[1].semilogx(w, 20*np.log10(gain), lw=2, label='$T$') gain, _, w = bode(1/WT, logspace(-3, 3), Plot = False) ax[1].semilogx(w, 20*np.log10(gain), ls='--', label='$1/W_T$') for i in range(2): ax[i].set_ylim(-40, 40) ax[i].legend() ax[i].grid(which="both", ls=':') ax[i].set_ylabel('Gain [dB]') ax[i].set_xlabel('$\omega$ [rad/s]') fig.tight_layout() fig.suptitle("robust controller") plt.show() ###設計した制御器の性能確認 fig, ax = plt.subplots() ref = 30 #目標値30 #不確かさ delta = np.arange(-1, 1, 0.1) WT = tf([10, 0], [1, 150]) #不確かさを有するモデルに対する性能 for i in range(len(delta)): #不確かさをもつ制御対象 P = (1 + WT*delta[i])*Pn Gyr = feedback(P*K, 1) y, t = step(Gyr, np.arange(0, 5, 0.01)) ax.plot(t, y*ref, color='k', lw=0.3) #ノミナルモデル(不確かさなし)に対する性能 Gyr = feedback(Pn*K, 1) y, t = step(Gyr, np.arange(0, 5, 0.01)) ax.plot(t, y*ref, color='r', lw=2, label="nominal model") ax.legend() plot_set(ax, 't', 'y') ax.set_title("robust controller test in model") plt.show() 出力 K= 7.21 s^4 + 1098 s^3 + 3259 s^2 + 1.081e+05 s + 9.032e+04 --------------------------------------------------------------- s^5 + 165.1 s^4 + 2448 s^3 + 2.449e+04 s^2 + 2.273e+04 s + 5540 gamma= 0.9527651218302327 上の計算結果から,混合感度問題を解くと,5次の制御器$K(s)$が得られることが分かる.また,$\gamma$は1より小さい値であることも確認できる. 上図は,感度関数と相補感度関数のゲイン線図である.それぞれ重み関数の逆数のゲイン線図よりも下側に描かれていることが確認できる. 以上により,設計された制御器を用いたときのステップ応答を次に示す. 図において,赤線は不確かさをもたない元の制御対象ノミナルモデルであり,それ以外が不確かさをもつ制御対象である.速やかに目標に追従していることと,不確かさがあっても応答があまり変わっていないことが分かる. 感想 ロバスト制御についての概要を把握することはできた.しかしながら,これをどのようにうまく利用してくのかという部分についてはまだまだ経験・知識不足であると感じた.概要はつかめたため,何かしらの教材か論文などでそのあたりの知識を埋めていきたいと思う. 参考文献 Pyhtonによる制御工学入門  南 祐樹 著  オーム社
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

量子符号化の手法を実装してみます

はじめに 量子コンピュータにて,古典のデータを用いた学習を行うとすると,量子系によってはどのようにその古典データを表現するのかを考えなければならない. ここでは,その量子コンピュータへデータを読み込む方法である計算基底符号化と振幅符号化について説明を行い,qiskitによる実装を与えます. わかりにくい箇所等あるかもしれませんがよろしくお願いします. 計算基底符号化 計算基底符号化は$n$量子ビットシステムの計算基底状態と従来の$n$ビットの配列を紐づける方法である.ある意味では,これはもっとも単純な計算方法であると言える. 例としてベクトル$\mathcal{x}=(0.1, -0.6, 1.0)$を符号化することを考える.正負を最初のビットに符号化して,残りで精度$\tau =4$の2値の分数表現で符号化すると, \begin{align} 0.1 \to 0 \ 0001 \\ -0.6 \to 1 \ 1001 \\ 1.0 \to 0 \ 1111 \end{align} したがって,このベクトルは量子状態$|00001 11001 01111>$と表すことができます. 計算基底符号化の理論 それぞれのデータ$\mathcal{x}^m$がビット列$\mathcal{x}^m = (b_1^m, \cdots, b_N^m)$であるような2値ので0たセット$\mathcal{D}$が与えられているとします.このデータセット$\mathcal{D}$は計算基底状態$|\mathcal{x}^m>$の重ね合わせ |\mathcal{D}> = \frac{1}{\sqrt{M}}\sum_{m=1}^M |\mathcal{x}^m> を用意することで表すことができます. 例として$\mathcal{x}^1=(01,01), \mathcal{x}^2=(11,01)$を計算基底状態による重ね合わせで表すと |\mathcal{D}=\frac{1}{\sqrt{2}}|0101>+\frac{1}{\sqrt{2}}|1110> となります. ここからはこのデータの重ね合わせを用意する方法について説明していきたいと思います.簡単のため,2値入力に含まれる全てのビットがそれぞれ1つの特徴量を表す場合(つまりは精度$\tau =1$)を考えます. step1 まず,三つのレジスタをもつ量子系が必要となります. |l_1,\cdots, l_N;a_1, a_2; s_1, \cdots, s_N> step2 左から読み込みレジスタ(loading),補助レジスタ(ancilla),記憶レジスタ(storage)とします. 次に補助レジスタの二番目の量子ビットにアダマールゲートを作用させます. \frac{1}{\sqrt{2}}|0, \cdots, 0;00;0, \cdots, 0>+\frac{1}{\sqrt{2}}|0, \cdots, 0;01;0, \cdots, 0> 左の項を記憶ブランチ,右の項を処理ブランチと呼ぶことにします. これで量子状態の準備は完了です. 次に実際にデータを符号化していくことを考えてみましょう.既に$m$個の訓練データが符号化されていて,新しく$m+1$個目のデータを符号化するとします. 既に$m$個の訓練データが符号化されている状態は次のように表すことができます. |\psi^{(m)}>=\frac{1}{\sqrt{M}}\sum_{k=1}^{M}|0,\cdots,0;00;x_1^k,\cdots, x_N^k> +\sqrt{\frac{M-m}{M}}|0, \cdots, 0;01;0, \cdots, 0> 記憶ブランチは既に$m$個の入力を記憶レジスタの中に持っており,処理ブランチの記憶レジスタは基底状態にあります. $m+1$個目のデータを符号化するには,まず$m+1$個目のビット列$x^{m+1}=(x_1^{m+1},\cdots,x_N^{m+1})$を読み込みレジスタに書き込みます.これは入力ビット列が$1$の時に$X$ゲートを作用させることで実現できます.次に処理ブランチ内で読み込みレジスタから記憶レジスタへ入力ビット列をコピーします.これはトフォリゲートを使用して実現することができます.ここまでの手順によって量子状態は次のようになっています. \frac{1}{\sqrt{M}}\sum_{k=1}^{M}|x_1^{m+1},\cdots,x_N^{m+1};00;x_1^k,\cdots, x_N^k> +\sqrt{\frac{M-m}{M}}|x_1^{m+1},\cdots,x_N^{m+1};01;x_1^{m+1},\cdots,x_N^{m+1}> その後補助レジスタ内でa2→a1でCNOTゲートを作用させます. 次に,1量子ビットユニタリゲート U_{a_2}(\mu)=\frac{1}{\mu} \left( \begin{array}{rr} \sqrt{\mu-1} & 1 \\ -1 & \sqrt{\mu-1} \end{array} \right) をa1に制御された形でa2に作用させます.ここで$\mu = M+1-(m+1)$です. この演算を行うことで処理ブランチを二つの下位ブランチに分割します. \frac{1}{\sqrt{M}}\sum_{k=1}^{M}|x_1^{m+1},\cdots,x_N^{m+1};00;x_1^k,\cdots, x_N^k> +\frac{1}{\sqrt{M}}|x_1^{m+1},\cdots,x_N^{m+1};10;x_1^{m+1},\cdots,x_N^{m+1}> +\sqrt{\frac{M-(m+1)}{M}}|x_1^{m+1},\cdots,x_N^{m+1};11;x_1^{m+1},\cdots,x_N^{m+1}> $|a_1 a_2>$を記憶ブランチに付け加えるには,この下位ブランチの$a_1$を$0$に戻す必要がある.最後に処理ブランチの記憶レジスタと全てのブランチの読み込みレジスタを初期化すれば終了である. 計算基底符号化の実装 理論的には量子計算を少し学んだ者なら簡単に理解できると思うが,実際に実装を行ってみると難所がいくつかある.それら全てを解決しながら実装を行ってみました.ここでも量子回路を記述する手法としてQiskitを使用します. とりあえず使用するライブラリーをimport computational_basis.py from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister from qiskit import Aer, execute import numpy as np 初期設定及び,使用するデータセット backend = Aer.get_backend('qasm_simulator') N = 2 M = 3 data_set = [[1, 1], [1, 0], [0, 1]] 続いて各レジスタを設定して量子回路を作成します. loading_reg = QuantumRegister(N, name='loading') ancilla_reg = QuantumRegister(2, name='ancilla') storage_reg = QuantumRegister(N, name='storage') reserve_reg = QuantumRegister(N, name='reserve') mct_ancilla = QuantumRegister(N, name='mct_ancilla') classical_reg = ClassicalRegister(N) circuit = QuantumCircuit(loading_reg, ancilla_reg, storage_reg, reserve_reg, mct_ancilla, classical_reg) ここでreserve_regは初期化の際に使用するレジスタ,mct_ancillaはマルチコントロールゲートを使用する際の補助レジスタとしています. 次にstep2の実装ですが,通常はアダマールを作用させますがここでは別で作成したユニタリー演算子$U$ U= \frac{1}{\sqrt{M}} \left( \begin{array}{rr} 1 & -\sqrt{M-1} \\ \sqrt{M-1} & 1 \end{array} \right) をアダマールゲートの代わりに作用させます. 理論通りにアダマールを作用させてしまうと$M=2$の時のみに対応する形になってしまい,最初に符号化されたデータが量子状態の半分を占めてしまうことになってしまうからです. その関数がこちら def initial_hadamard(M): # 回路を準備 qc = QuantumCircuit(1, name='hadamard') # ユニタリー演算子を定義 unit = np.array([[1, -np.sqrt(M-1)], [np.sqrt(M-1), 1]]) unit *= 1 / np.sqrt(M) # ゲートとして回路に作用させる qc.unitary(unit, [0]) # ゲートに変換して返す return qc.to_gate() gate = initial_hadamard(M) circuit.append(gate, [N+1]) # hadamard gateの代わり またここでも新たに作成した関数を差し込みます. 理論のままだと既に$|0,\cdots,0>$というデータが格納されているということになってしまっているのでそこを改良します. def first_data(circuit, loading_reg, ancilla_reg, storage_reg, my_data, N): # 読み込みレジスタにデータを入力 loading(circuit, loading_reg, my_data) # 処理ブランチの記憶レジスタにコピー all_ccnot(circuit, loading_reg, ancilla_reg, storage_reg, N, X=True) # 読み込みレジスタを初期化 loading(circuit, loading_reg, my_data) return circuit first_data(circuit, loading_reg, ancilla_reg, storage_reg, data_set[0], N) それぞれの関数については後ほど説明します. この作業を行うことによって$|0,\cdots,0>$が含まれることを防ぎます.(もちろんデータとしてある場合はやらなくて良いと思います) ここからは実際にデータを符号化していくプログラムになります.先に使用する関数を説明します. def loading(circuit, loading_reg, my_data): # データを読み込みレジスタに書き込む for d in range(len(my_data)): if my_data[d] == 1: circuit.x(loading_reg[d]) return circuit def all_ccnot(circuit, loading_reg, ancilla_reg, storage_reg, N, X=False): # 処理ブランチの記憶レジスタにコピーする # Xはfirst dataの時のみに使用する if X is True: circuit.x(ancilla_reg[1]) for n in range(N): circuit.ccx(loading_reg[n], ancilla_reg[1], storage_reg[n]) if X is True: circuit.x(ancilla_reg[1]) return circuit def U_a2(mu): # ユニタリー演算子を作成 qc = QuantumCircuit(1, name='Ua2') unit = np.array([[np.sqrt(mu - 1), 1], [-1, np.sqrt(mu - 1)]]) unit *= 1 / np.sqrt(mu) qc.unitary(unit, [0]) # .control(1)で制御することが可能になる return qc.to_gate().control(1) def match(circuit, loading_reg, reserve_reg, ancilla_reg, mct_ancilla, N): # 同じかどうかを判定する回路を作成する # 基本的には読み込みレジスタと処理レジスタが同じかどうかをCNOTを用いて比較 # reserveレジスタで判定する for n in range(N): circuit.cx(loading_reg[n], reserve_reg[n]) circuit.cx(storage_reg[n], reserve_reg[n]) circuit.x(reserve_reg[:]) circuit.x(ancilla_reg[1]) control_qubits = [[N+1] + [q for q in range(N*2+2, N*3+2)] circuit.mct(control_qubits, ancilla_reg[0], mct_ancilla) # 一緒で10ならばa1をflipさせて元に戻すけど(最初に全体をflipさせているのでa2をflip) circuit.x(ancilla_reg[1]) return circuit def initialize(circuit, loading_reg, ancilla_reg, storage_reg, N, my_data): # 初期化する circuit.cx(ancilla_reg[1], ancilla_reg[0]) all_ccnot(circuit, loading_reg, ancilla_reg, storage_reg, N) loading(circuit, loading_reg, my_data) return circuit では実際に符号化していきます. # data_set[0]は既に符号化済 for m in range(1, M): my_data = data_set[m] loading(circuit, loading_reg, my_data) all_ccnot(circuit, loading_reg, ancilla_reg, storage_reg, N) circuit.cx(ancilla_reg[1], ancilla_reg[0]) mu = M + 1 - (m + 1) Ua2 = U_a2(mu) circuit.append(Ua2, [N, N+1]) match(circuit, loading_reg, reserve_reg, ancilla_reg, mct_ancilla, N) initialize(circuit, loading_reg, ancilla_reg, storage_reg, N, my_data) circuit.reset(reserve_reg) circuit.reset(mct_ancilla) では符号化できているかどうか測定して確認をしてみましょう. circuit.measure(storage_reg, classical_reg) result = execute(circuit, backend, shots=8192).result() counts = result.get_counts() print(counts) ''' {'01': 2734, '10': 2796, '11': 2662} ''' 良い感じにできてますね. 振幅符号化 振幅符号化は実数ベクトルなどの古典的な情報を量子振幅と関連付ける. 振幅符号化の理論 今ここに規格化された古典的なベクトル$\mathcal{x}\in \mathbb{C}^{2^n}$があるとします. \mathcal{x}=[x_1,\cdots, x_{2^n}]^T これを振幅符号化した量子状態は |\psi_x>=\sum_{j=1}^{2^n}x_j|j> と表されます. と言った様に雑に言えば簡単ですね(これを実装するのが大変なんだが...) 振幅符号化の実装 振幅符号化のキーとなるのはmulti-controlled rotation gateです.量子ビット$q_s$に対する回転を前の量子ビット$q_1, \cdots, q_{s-1}$のとりうる全ての状態によって制御します,全ての状態というのは,例えば$s=3$に対する制御を考えると前にある量子ビット$q_1, q_2$のとりうる全ての状態というのは$00, 01, 10, 11$の四つとなります. では,$q_1, \cdots, q_{s-1}$の状態が$|0,\cdots,0>$である時,$q_s$に作用させるゲートはどうなるのでしょうか.このゲートは次の式で表されます. c_{q_1=0}\cdots c_{q_{s-1}=0}R_{q_s}(\mathcal{v}^1, \beta_1)|q_1\cdots q_{s-1}>|q_s> となります.個でを全ての状態について行いますので,他のゲートは次の様に表されます. \begin{align} c_{q_1=0}\cdots c_{q_{s-1}=1}R_{q_s}(&\mathcal{v}^2, \beta_2)|q_1\cdots q_{s-1}>|q_s> \\ &\vdots \\ c_{q_1=1}\cdots c_{q_{s-1}=1}R_{q_s}(&\mathcal{v}^{2^{s-1}}, \beta_{2^{s-1}})|q_1\cdots q_{s-1}>|q_s> \end{align} 振幅符号化では$R_y$ゲートを使用していきます.また,各$\beta$については次の式で求めることができます. \beta_j^s = 2arcsin(\frac{\sqrt{\sum_{l=1}^{2^{s-1}}|\alpha_{(2j-1)2^{s-1}+l}|^2}}{\sqrt{\sum_{l=1}^{2^{s}}|\alpha_{(j-1)2^{s}+l}|^2}} この$\beta$の添字$s$は量子ビットのインデックス,$j$は$s-1$までの量子ビットがとりうる全ての状態のインデックスとなっています. ここで例をあげます.状態$|\psi>=\sqrt{0.2}|000>+\sqrt{0.5}|010>+\sqrt{0.2}|110>+\sqrt{0.1}|111>$を作りたいと考えます. この時の振幅はすなわち$\alpha_1=\sqrt{0.2},\alpha_3=\sqrt{0.5},\alpha_7=\sqrt{0.2},\alpha_8=\sqrt{0.1}$です.(一般的に振幅を表すときは$\alpha_0$から始めると思いますが,整合性を保つためにここではこの様な表記の仕方をしたいと思います.) この時の量子回路には七つの複数制御$y$回転ゲート \begin{align} c_{q1=0}c_{q2=0} R_{y,q_3}(\beta_1^1), \beta_1^1 &= 0 \\ c_{q1=0}c_{q2=1} R_{y,q_3}(\beta_2^1), \beta_2^1 &= 0 \\ c_{q1=1}c_{q2=0} R_{y,q_3}(\beta_3^1), \beta_3^1 &= 0 \\ c_{q1=1}c_{q2=1} R_{y,q_3}(\beta_4^1), \beta_4^1 &= 1.231... \\ c_{q1=0} R_{y,q_2}(\beta_1^2), \beta_1^2 &= 2.014... \\ c_{q1=1} R_{y,q_2}(\beta_2^2), \beta_2^2 &= 3.142... \\ R_{y,q_1}(\beta_1^3), \beta_1^3 &= 1.159... \end{align} この時の$\beta_1^1,\beta_1^2$を例として計算式を挙げてみます. \begin{align} \beta_1^1 &= 2arcsin(\frac{\sqrt{|\alpha_2|^2}}{\sqrt{|\alpha_1|^2 +|\alpha_2|^2}}) \\ \beta_1^2 &= 2arcsin(\frac{\sqrt{|\alpha_3|^2+|\alpha_4|^2}}{\sqrt{|\alpha_1|^2 +|\alpha_2|^2+|\alpha_3|^2+|\alpha_4|^2}}) \end{align} この様に計算されています. 実際にpythonとQiskitを用いて振幅符号化の実装を行っていきましょう.まずはimportからです. amplitude.py from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit import Aer, execute import numpy as np # 自作ライブラリ from Functions.Binary import dec2bin_for_counts 初期値を設定します.ここでは先程紹介した例を実装していきます. if __name__ == '__main__': backend = Aer.get_backend('qasm_simulator') amplitude_data = [0.2, 0, 0.5, 0, 0, 0, 0.2, 0.1] nQ = 3 回路の準備です.ancilla_regを複数制御回転ゲート用に用意しておきます. # 回路を準備 encode_reg = QuantumRegister(nQ, name='encode') ancilla_reg = QuantumRegister(nQ, name='ancilla') output_reg = ClassicalRegister(nQ, name='measure') circuit = QuantumCircuit(encode_reg, ancilla_reg, output_reg) ここで二つの関数を定義します. # beta_j^sを求める関数 def beta_js(amplitude_data, j, s): denominator = 0 # 分母 numerator = 0 # 分子 for l in range(0, 2**s): denominator += abs(amplitude_data[(j-1)*(2**s)+l]) for l in range(0, 2**(s-1)): numerator += abs(amplitude_data[(2*j-1)*(2**(s-1))+l]) if denominator == 0: return 0. else: return 2*np.arcsin(np.sqrt(numerator)/np.sqrt(denominator)) # 二進数表記で1ならばx gateをapplyする関数 def apply_x_gate(circuit, bin2, s_): for i in range(len(bin2)): if bin2[i] == '0': circuit.x(s_-i-1) return circuit # 複数制御回転ゲートを実際に行う関数 def mcry_for_encode(circuit, encode_reg, ancilla_reg, bin2, beta): apply_x_gate(circuit, bin2) # multi-controlled ry gate circuit.mcry(beta, encode_reg[:len(bin2)], encode_reg[len(bin2)], ancilla_reg) apply_x_gate(circuit, bin2) return circuit 実際にencodeしていきます. # 全てのsに対してloop for s in range(1, nQ+1): s_ = nQ-s # 全てのjに対してloop for j in range(1, 2**s_+1): beta = beta_js(amplitude_data, j, s) bin2 = dec2bin_for_counts(j-1, s_) if bin2 is True: circuit.ry(beta, 0) else: if beta == 0.: pass else: mcry_for_encode(circuit, encode_reg, ancilla_reg, bin2, beta) この回路を逆演算に変換します. inverse_circuit = circuit.inverse() 実際に測定を行いencodeできているか確認しましょう. # 測定する inverse_circuit.measure(encode_reg, output_reg) result = execute(inverse_circuit, backend=backend, shots=1000).result() counts = result.get_counts() print(counts) ''' {'000': 188, '010': 506, '011': 202, '111': 104} ''' 良い感じの結果になりましたね. 最後に 今回は計算基底符号化と振幅符号化の実装を行いました.他にもいくつか符号化の方法があると思いますが,それはまた別の機会に実装を行いたいと思います. また,他にも符号化について細かい理論等あるとは思いますが,私個人の興味がこの符号化を用いたものの先にあるのでここでは割愛させていただきたいと思います.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】pandasというライブラリを使ってCSVファイルを読み込み、横棒グラフを作成する。

pythonを使用してExcelファイルの操作を勉強しています。 本日の気づき(復習)は、CSVの読み込みとグラフ作成に関しての続きです。 pythonでExcelを操作するため、openpyxlというパッケージを使用しています。 売上.csv 部門,1月,2月,3月 商品A,600,700,800 商品B,4100,3800,4500 商品C,2900,1800,3000 商品D,800,900,1000 商品E,600,550,720 上記のようなCSVファイルを読み込み この様な棒グラフを作り貼り付けたいです。 CSVを読み込み、張り付け そちらを元に、グラフを作成します。 違いは縦棒グラフか横棒グラフかだけで BarChartオブジェクトで使用できる属性の値を変更することで縦を横にします。 ここまでは前回と同じです。 ついでに他の属性も見つけたので主要そうなものを羅列します。 BarChartオブジェクトで使用できる属性 type:縦棒グラフは'col'、横棒グラフは'bar' bar.x_axis.scaling.min:横軸の最小値 bar.x_axis.scaling.max:横軸の最大値 bar.y_axis.scaling.min:縦軸の最小値 bar.y_axis.scaling.max:縦軸の最大値 width:グラフの幅 height:グラフの高さ varyColors:系列ごとの色を変える際は「True」 legend.position:凡例項目の位置。右は'r'、左は'l'、上は't'、下は'b' ここで気を付けないといけないのは 見た目的に横軸になっている値の属性は縦軸(y_axis)で設定する ということです。 上手く表現できないのですが 先ず、縦軸でグラフの内容を設定してから、 横軸グラフになるように、90度傾ける。 と、表現すればいいのでしょうか。(前回に引き続き語彙力が欲しい) 最終的なコード import pandas as pd from openpyxl import Workbook from openpyxl.chart import BarChart, Reference from openpyxl.utils.dataframe import dataframe_to_rows wb = Workbook() ws = wb.active # CSVファイル読込み df = pd.read_csv('売上.csv', encoding='utf-8') for row in dataframe_to_rows(df, index=None, header=True): # データを1行ずつ追加 ws.append(row) # 棒グラフを選択 bar = BarChart() # 横棒グラフに設定 bar.type = 'bar' # グラフのデータ範囲を設定 data = Reference(ws, min_col=2, min_row=1, max_col=ws.max_column, max_row=ws.max_row) # ラベルの範囲を設定 labels = Reference(ws, min_col=1, min_row=2, max_row=ws.max_row) # グラフにデータを追加 bar.add_data(data, titles_from_data=True) # 縦軸(グラフ上では横軸)のラベルを追加 bar.set_categories(labels) # 縦軸(グラフ上では横軸)の最小値を設定 bar.y_axis.scaling.min = 0 # 縦軸(グラフ上では横軸)の最大値を設定 bar.y_axis.scaling.max = 4200 # グラフの幅 bar.width = 15 # グラフの高さ bar.height = 7.5 # 凡例項目の位置を設定 bar.legend.position = 'b' # 横軸(グラフ上では縦軸)タイトルを追加 bar.x_axis.title = '部門' # 縦軸(グラフ上では横軸)タイトルを追加 bar.y_axis.title = '売上高(千円)' # グラフのタイトルを追加 bar.title = '部門別売上高' # 棒グラフをシートに追加 ws.add_chart(bar, 'A9') wb.save('部門別売上_横棒グラフ.xlsx') 補足 bar.add_data(data, titles_from_data=True)でデータを追加してから bar.set_categories(labels)で、ラベルを追加しないと 上手くラベルが反映されないようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

長文要約API(朝日新聞社)を桃太郎で試してみたら桃太郎が家に帰らなかった件について

はじまりはじまり 朝日新聞社から長文を要約するAPIが無償公開されていましたので、試してみました。 ↑上のURLから登録すると1日20回のリクエストまで無料で使えるそう。 メールアドレスを登録して、認証すれば簡単にAPIキーを取得できました。 桃太郎を要約する 長い文章から、各文を圧縮したり、指定文字数ごとに区切って要約したり、重要な文を抽出したりするAPIです。 長文要約生成APIには上記のように記載があったのですが、長い文章を思いつかなかったので 童話「桃太郎」 を要約してみました。 用途が違ったらすみません。 元となる桃太郎の文章は↓のサイトから借用しました。 著作権とか問題でしたら、この記事は即座に削除します。(一応、運営者に連絡します。) 実際にやってみた 桃太郎の文章は長いので、記事の最後に記載します。 使い方は公式サイトをご覧ください。 基本的なAPIを飛ばす方法と変わりはありませんでした。 import json import requests text = '桃太郎' key = 'XXXXXXXX' endpoint = "https://clapi.asahi.com/abstract" data = json.dumps({"text": text, "length": 600, "auto_paragraph": True}) headers = {"accept": "application/json", "Content-Type": "application/json", "x-api-key": key} response = requests.post(endpoint, data, headers=headers) if response.status_code == 200: result = response.json()["result"] pprint.pprint(result) else: print(response) lengthは 何文字ずつ区切って要約するか指定します。この値が長いほど要約は短くなります。 とあったので、三段楽構成と考えて、600文字ずつ要約してもらいました。(textの中身は1999文字)  結果は ['「おやおや、かわそうに、逃がしておやりよ」「いやだよ」。', 'ある村に心のやさしい浦島太郎という若者がいました。カメにカメを助けてくださってありがとうございます。', 'この3年月が過ぎて、乙姫さまに言いました。「家族も友だちもみんな死んでしまったのか・・・ああ、確か浦島という人なら700年前に海へ出たきり。帰らないそうですよ」'] 「ああ、確か浦島という人なら700年前に海へ出たきり。帰らないそうですよ」 帰らない人としてようやくされていました。 そもそもこのAPIは物語には向いてなさそうな予感はしていましたが、想像よりいい感じにようやくされていてびっくりしました。 おしまい ちなみにlengthを変えると ['「おやおや、かわいそうに、逃がしておやりよ」「いやだよ。おらたちの勝手だろ」。ある村に心のやさしい若者がいました。「おや?誰が呼んでいるの?」「竜宮?さあ、どこにある?」「海の底へなんか。行けるのかい?」。カメが頭を出して言います。', '本日発行の「asahi+C(朝日プラス・シー)」33号は、乙姫さまに言われるまま竜宮で過ごすうちに「もう一日、いてください」と語る。「おや?わずか三年で、ずいぶんと様子が変わったな」。釣りをしていた浦島さんは、海に出たきりで、帰らないそうです 「本日発行の「asahi+C(朝日プラス・シー)」33号」と謎の単語が現れてきました。 リクエストで投げた文章だけが表示されるわけではないんですね。 asahi+C(朝日プラス・シー)の宣伝もうまい具合に入っていました。 本日のリクエスト回数は終わってしまったので、明日は他の童話で試してみようかと思います。 textの中身 text = "むかしむかし、ある村に、心のやさしい浦島太郎という若者がいました。浦島さんが海辺を通りかかると、" \ "子どもたちが大きなカメを捕まえていました。そばによって見てみると、子どもたちがみんなでカメをいじめています。「おやおや、かわいそうに、逃がしておやりよ」" \ "「いやだよ。おらたちが、やっと捕まえたんだもの。どうしようと、おらたちの勝手だろ」見るとカメは涙をハラハラとこぼしながら、浦島さんを見つめています。" \ "浦島さんはお金を取り出すと、子どもたちに差し出して言いました。「それでは、このお金をあげるから、おじさんにカメを売っておくれ」「うん、それならいいよ」" \ "こうして浦島さんは、子どもたちからカメを受け取ると、「大丈夫かい?もう、捕まるんじゃないよ」と、カメをそっと、海の中へ逃がしてやりました。" \ "さて、それから二、三日たったある日の事、浦島さんが海に出かけて魚を釣っていると、「・・・浦島さん、・・・浦島さん」と、誰かが呼ぶ声がします。" \ "「おや?誰が呼んでいるのだろう?」「わたしですよ」すると海の上に、カメが頭を出して言いました。" \ "「このあいだは助けていただいて、ありがとうございました」「ああ、あの時のカメさん」「はい、おかげで命が助かりました。ところで浦島さんは、竜宮へ行った事がありますか?」" \ "「竜宮?さあ?竜宮って、どこにあるんだい?」「海の底です」「えっ?海の底へなんか、行けるのかい?」「はい。わたしがお連れしましょう。さあ、背中へ乗ってください」カメは浦島さんを背中に乗せて、海の中をずんずんともぐっていきました。" \ "「わあ、きれいだな」やがて立派なご殿へ着きました。" \ "「着きましたよ。このご殿が竜宮です。さあ、こちらへ」カメに案内されるまま進んでいくと、この竜宮の主人の美しい乙姫(おとひめ)さまが、色とりどりの魚たちと一緒に浦島さんを出迎えてくれました。" \ "「ようこそ、浦島さん。わたしは、この竜宮の主人の乙姫です。このあいだはカメを助けてくださって、ありがとうございます。お礼に、竜宮をご案内します。どうぞ、ゆっくりしていってくださいね」" \ "浦島さんは、竜宮の広間ヘ案内されました。ここはまるで、天国のようです。そして、「もう一日、いてください。もう一日、いてください」と、乙姫さまに言われるまま竜宮で過ごすうちに、三年の月日がたってしまいました。ある時、浦島さんは、はっと思い出しました。(家族や友だちは、どうしているだろう?)そこで浦島さんは、乙姫さまに言いました。" \ "「乙姫さま、今までありがとうございます。ですが、もうそろそろ家へ帰らせていただきます」「帰られるのですか?よろしければ、このままここで暮しては」「いいえ、わたしの帰りを待つ者もおりますので」すると乙姫さまは、さびしそうに言いました。" \ "「・・・そうですか。それはおなごりおしいです。では、おみやげに玉手箱を差し上げましょう」「玉手箱?」「はい。この中には、浦島さんが竜宮で過ごされた『時』が入っております。" \ "これを開けずに持っている限り、浦島さんは年を取りません。ずーっと、今の若い姿のままでいられます。ですが一度開けてしまうと、今までの『時』が戻ってしまいますので、決して開けてはなりませんよ」" \ "「はい、わかりました。ありがとうございます」乙姫さまと別れた浦島さんは、またカメに送られて地上へ帰りました。地上にもどった浦島さんは、まわりを見回してびっくり。" \ "「おや?わずか三年で、ずいぶんと様子が変わったな」確かにここは浦島さんが釣りをしていた場所ですが、何だか様子が違います。" \ "浦島さんの家はどこにも見あたりませんし、出会う人も知らない人ばかりです。「わたしの家は、どうなったのだろう?みんなはどこかへ、引っ越したのだろうか?" \ "・・・あの、すみません。浦島の家を知りませんか?」浦島さんが一人の老人に尋ねてみると、老人は少し首をかしげて言いました。「浦島?・・・ああ、確か浦島という人なら七百年ほど前に海へ出たきりで、帰らないそうですよ」" \ "「えっ!?」老人の話しを聞いて、浦島さんはびっくり。竜宮の三年は、この世の七百年にあたるのでしょうか?" \ "「家族も友だちも、みんな死んでしまったのか・・・」がっくりと肩を落とした浦島さんは、ふと、持っていた玉手箱を見つめました。" \ "「そう言えば、乙姫さまは言っていたな。この玉手箱を開けると、『時』が戻ってしまうと。・・・もしかしてこれを開けると、自分が暮らしていた時に戻るのでは」" \ "そう思った浦島さんは、開けてはいけないと言われていた玉手箱を開けてしまいました。" \ "すると中から、まっ白のけむりが出てきました。「おおっ、これは」けむりの中に、竜宮や美しい乙姫さまの姿がうつりました。" \ "でも玉手箱から出てきたけむりは次第に薄れていき、その場に残ったのは髪の毛もひげもまっ白の、ヨポヨポのおじいさんになった浦島さんだったのです。" ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

長文要約API(朝日新聞社)を浦島太郎で試してみたら浦島太郎が家に帰らなかった件について

はじまりはじまり 朝日新聞社から長文を要約するAPIが無償公開されていましたので、試してみました。 ↑上のURLから登録すると1日20回のリクエストまで無料で使えるそう。 メールアドレスを登録して、認証すれば簡単にAPIキーを取得できました。 浦島太郎を要約する 長い文章から、各文を圧縮したり、指定文字数ごとに区切って要約したり、重要な文を抽出したりするAPIです。 長文要約生成APIには上記のように記載があったのですが、長い文章を思いつかなかったので 童話「浦島太郎」 を要約してみました。 用途が違ったらすみません。 元となる浦島太郎の文章は↓のサイトから借用しました。 著作権とか問題でしたら、この記事は即座に削除します。(一応、運営者に連絡します。) 実際にやってみた 浦島太郎の文章は長いので、記事の最後に記載します。 使い方は公式サイトをご覧ください。 基本的なAPIを飛ばす方法と変わりはありませんでした。 import json import requests text = '浦島太郎の話' key = 'XXXXXXXX' endpoint = "https://clapi.asahi.com/abstract" data = json.dumps({"text": text, "length": 600, "auto_paragraph": True}) headers = {"accept": "application/json", "Content-Type": "application/json", "x-api-key": key} response = requests.post(endpoint, data, headers=headers) if response.status_code == 200: result = response.json()["result"] print(result) else: print(response) lengthは 何文字ずつ区切って要約するか指定します。この値が長いほど要約は短くなります。 とあったので、三段楽構成と考えて、600文字ずつ要約してもらいました。(textの中身は1999文字)  結果は ['「おやおや、かわそうに、逃がしておやりよ」「いやだよ」。', 'ある村に心のやさしい浦島太郎という若者がいました。カメにカメを助けてくださってありがとうございます。', 'この3年月が過ぎて、乙姫さまに言いました。「家族も友だちもみんな死んでしまったのか・・・ああ、確か浦島という人なら700年前に海へ出たきり。帰らないそうですよ」'] 「ああ、確か浦島という人なら700年前に海へ出たきり。帰らないそうですよ」 帰らない人として要約されていました。 このAPIは物語には向いてなさそうな予感はしていましたが、想像よりいい感じにようやくされていてびっくりしました。 おしまい ちなみにlengthの値を変えると ['「おやおや、かわいそうに、逃がしておやりよ」「いやだよ。おらたちの勝手だろ」。ある村に心のやさしい若者がいました。「おや?誰が呼んでいるの?」「竜宮?さあ、どこにある?」「海の底へなんか。行けるのかい?」。カメが頭を出して言います。', '本日発行の「asahi+C(朝日プラス・シー)」33号は、乙姫さまに言われるまま竜宮で過ごすうちに「もう一日、いてください」と語る。「おや?わずか三年で、ずいぶんと様子が変わったな」。釣りをしていた浦島さんは、海に出たきりで、帰らないそうです 「 本日発行の「asahi+C(朝日プラス・シー)」33号 」と謎の単語が現れてきました。 リクエストで投げた文章だけが表示されるわけではないんですね。 asahi+C(朝日プラス・シー)の宣伝もうまい具合に入っていました。 本日のリクエスト回数は終わってしまったので、明日は他の童話で試してみようかと思います。 textの中身 text = "むかしむかし、ある村に、心のやさしい浦島太郎という若者がいました。浦島さんが海辺を通りかかると、" \ "子どもたちが大きなカメを捕まえていました。そばによって見てみると、子どもたちがみんなでカメをいじめています。「おやおや、かわいそうに、逃がしておやりよ」" \ "「いやだよ。おらたちが、やっと捕まえたんだもの。どうしようと、おらたちの勝手だろ」見るとカメは涙をハラハラとこぼしながら、浦島さんを見つめています。" \ "浦島さんはお金を取り出すと、子どもたちに差し出して言いました。「それでは、このお金をあげるから、おじさんにカメを売っておくれ」「うん、それならいいよ」" \ "こうして浦島さんは、子どもたちからカメを受け取ると、「大丈夫かい?もう、捕まるんじゃないよ」と、カメをそっと、海の中へ逃がしてやりました。" \ "さて、それから二、三日たったある日の事、浦島さんが海に出かけて魚を釣っていると、「・・・浦島さん、・・・浦島さん」と、誰かが呼ぶ声がします。" \ "「おや?誰が呼んでいるのだろう?」「わたしですよ」すると海の上に、カメが頭を出して言いました。" \ "「このあいだは助けていただいて、ありがとうございました」「ああ、あの時のカメさん」「はい、おかげで命が助かりました。ところで浦島さんは、竜宮へ行った事がありますか?」" \ "「竜宮?さあ?竜宮って、どこにあるんだい?」「海の底です」「えっ?海の底へなんか、行けるのかい?」「はい。わたしがお連れしましょう。さあ、背中へ乗ってください」カメは浦島さんを背中に乗せて、海の中をずんずんともぐっていきました。" \ "「わあ、きれいだな」やがて立派なご殿へ着きました。" \ "「着きましたよ。このご殿が竜宮です。さあ、こちらへ」カメに案内されるまま進んでいくと、この竜宮の主人の美しい乙姫(おとひめ)さまが、色とりどりの魚たちと一緒に浦島さんを出迎えてくれました。" \ "「ようこそ、浦島さん。わたしは、この竜宮の主人の乙姫です。このあいだはカメを助けてくださって、ありがとうございます。お礼に、竜宮をご案内します。どうぞ、ゆっくりしていってくださいね」" \ "浦島さんは、竜宮の広間ヘ案内されました。ここはまるで、天国のようです。そして、「もう一日、いてください。もう一日、いてください」と、乙姫さまに言われるまま竜宮で過ごすうちに、三年の月日がたってしまいました。ある時、浦島さんは、はっと思い出しました。(家族や友だちは、どうしているだろう?)そこで浦島さんは、乙姫さまに言いました。" \ "「乙姫さま、今までありがとうございます。ですが、もうそろそろ家へ帰らせていただきます」「帰られるのですか?よろしければ、このままここで暮しては」「いいえ、わたしの帰りを待つ者もおりますので」すると乙姫さまは、さびしそうに言いました。" \ "「・・・そうですか。それはおなごりおしいです。では、おみやげに玉手箱を差し上げましょう」「玉手箱?」「はい。この中には、浦島さんが竜宮で過ごされた『時』が入っております。" \ "これを開けずに持っている限り、浦島さんは年を取りません。ずーっと、今の若い姿のままでいられます。ですが一度開けてしまうと、今までの『時』が戻ってしまいますので、決して開けてはなりませんよ」" \ "「はい、わかりました。ありがとうございます」乙姫さまと別れた浦島さんは、またカメに送られて地上へ帰りました。地上にもどった浦島さんは、まわりを見回してびっくり。" \ "「おや?わずか三年で、ずいぶんと様子が変わったな」確かにここは浦島さんが釣りをしていた場所ですが、何だか様子が違います。" \ "浦島さんの家はどこにも見あたりませんし、出会う人も知らない人ばかりです。「わたしの家は、どうなったのだろう?みんなはどこかへ、引っ越したのだろうか?" \ "・・・あの、すみません。浦島の家を知りませんか?」浦島さんが一人の老人に尋ねてみると、老人は少し首をかしげて言いました。「浦島?・・・ああ、確か浦島という人なら七百年ほど前に海へ出たきりで、帰らないそうですよ」" \ "「えっ!?」老人の話しを聞いて、浦島さんはびっくり。竜宮の三年は、この世の七百年にあたるのでしょうか?" \ "「家族も友だちも、みんな死んでしまったのか・・・」がっくりと肩を落とした浦島さんは、ふと、持っていた玉手箱を見つめました。" \ "「そう言えば、乙姫さまは言っていたな。この玉手箱を開けると、『時』が戻ってしまうと。・・・もしかしてこれを開けると、自分が暮らしていた時に戻るのでは」" \ "そう思った浦島さんは、開けてはいけないと言われていた玉手箱を開けてしまいました。" \ "すると中から、まっ白のけむりが出てきました。「おおっ、これは」けむりの中に、竜宮や美しい乙姫さまの姿がうつりました。" \ "でも玉手箱から出てきたけむりは次第に薄れていき、その場に残ったのは髪の毛もひげもまっ白の、ヨポヨポのおじいさんになった浦島さんだったのです。" ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dcoker + python/seleniumでWEBページの全画面スクリーンショットを取得してみる

はじめに WEBページの全画面スクリーンショットを手軽に撮りたくなりトライアルしてみました. 備忘メモとして残します. docker/docker-composeは別途インストールしてください. 登場人物 python3実行用コンテナ ※リクエスト実行用 selenium HUB用コンテナ ※python3のリクエストの受け口 selenium Chrome用コンテナ クライアントPCに準備するファイル 以下のファイルを準備します. - Dockerfile - docker-compose.yml - get-sc-website.py(※後述) Dockerfile(python3実行用コンテナ) Dockerホスト⇄python3実行用コンテナでファイルをやり取りできるようにしておきます Dockerfile FROM python:3 RUN pip install --upgrade pip && pip install selenium ADD . /opt WORKDIR /opt docker-compose(python/selenium用) seleniumは単一ノードで起動します. Firefox/Operaのseleniumノードが欲しくなったら追加してください. docker-compose.yml version: '3' services: python3: restart: always build: context: . dockerfile: ./Dockerfile container_name: 'python3' working_dir: '/root/' tty: true volumes: - ./opt:/root/opt chrome: image: selenium/node-chrome:4.0.0-beta-1-20210215 volumes: - /dev/shm:/dev/shm depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 ports: - "6900:5900" selenium-hub: image: selenium/hub:4.0.0-beta-1-20210215 container_name: selenium-hub ports: - "4442:4442" - "4443:4443" - "4444:4444" docker composeで起動 docker pull python:3 docker compose up -d pythonファイルの用意 python3実行用コンテナに配置します.対象WEBページはサンプルです. get-sc-website.py import os from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.common.exceptions import WebDriverException def screenshot(driver, filename): w = driver.execute_script('return document.body.scrollWidth') h = driver.execute_script('return document.body.scrollHeight') driver.set_window_size(w, h) driver.save_screenshot(filename) if __name__ == '__main__': ## set capabilities capabilities = DesiredCapabilities.CHROME.copy() driver = webdriver.Remote( ## set selenium server command_executor='http://selenium-hub:4444', desired_capabilities=capabilities ) website = ['https://AAA.jp/', 'https://BBB.jp/'] ## get website try: ## increments of website[] i=0 for filename in website: print(website[i]) driver.get(website[i]) filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), website[i].replace('/','').replace(':','')+".png") ## get screenshot screenshot(driver, filename) i+=1 driver.quit() except WebDriverException: print('cannot get webpage') python用コンテナに配置し,スクレイプ実行 mv ./get-sc-website.py ./opt docker exec -it python3 python opt/get-sc-website.py 実行結果 終わりに 注意点 selenium4.0になってからリクエストのURLが変わっています. 2.X/3.X) command_executor='http://localhost:4444/wd/hub' 4.X) command_executor='http://localhost:4444/'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dcoker + python/seleniumで車屋さんのWEBページの全画面スクリーンショットを取得してみる

はじめに WEBページの全画面スクリーンショットを手軽に撮りたくなりトライアルしてみました. 備忘メモとして残します. docker/docker-composeは別途インストールしてください. 登場人物 python3実行用コンテナ ※リクエスト実行用 selenium HUB用コンテナ ※python3のリクエストの受け口 selenium Chrome用コンテナ 実行準備 クライアントPCに準備するファイル 以下のファイルを準備します. - Dockerfile - docker-compose.yml - get-sc-website.py(※後述) Dockerfile(python3実行用コンテナ) Dockerホスト⇄python3実行用コンテナでファイルをやり取りできるようにしておきます Dockerfile FROM python:3 RUN pip install --upgrade pip && pip install selenium ADD . /opt WORKDIR /opt docker-compose(python/selenium用) seleniumは単一ノードで起動します. Firefox/Operaのseleniumノードが欲しくなったら追加してください. docker-compose.yml version: '3' services: python3: restart: always build: context: . dockerfile: ./Dockerfile container_name: 'python3' working_dir: '/root/' tty: true volumes: - ./opt:/root/opt chrome: image: selenium/node-chrome:4.0.0-beta-1-20210215 volumes: - /dev/shm:/dev/shm depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 ports: - "6900:5900" selenium-hub: image: selenium/hub:4.0.0-beta-1-20210215 container_name: selenium-hub ports: - "4442:4442" - "4443:4443" - "4444:4444" docker composeで起動 docker pull python:3 docker compose up -d pythonファイルの用意 python3実行用コンテナに配置します.対象WEBページはサンプルです. get-sc-website.py import os from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.common.exceptions import WebDriverException def screenshot(driver, filename): w = driver.execute_script('return document.body.scrollWidth') h = driver.execute_script('return document.body.scrollHeight') driver.set_window_size(w, h) driver.save_screenshot(filename) if __name__ == '__main__': ## set capabilities capabilities = DesiredCapabilities.CHROME.copy() driver = webdriver.Remote( ## set selenium server command_executor='http://selenium-hub:4444', desired_capabilities=capabilities ) website = ['https://toyota.jp/', 'https://www.suzuki.co.jp/'] ## get website try: ## increments of website[] i=0 for filename in website: print(website[i]) driver.get(website[i]) filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), website[i].replace('/','').replace(':','')+".png") ## get screenshot screenshot(driver, filename) i+=1 driver.quit() except WebDriverException: print('cannot get webpage') docker上でpythonを実行 python用コンテナに配置し,スクレイプ実行 mv ./get-sc-website.py ./opt docker exec -it python3 python opt/get-sc-website.py 実行結果 しばらく待つと ./opt フォルダに <サイトURL.png> で画像が出力されます. (seleniumは実行速度が遅いです) 終わりに 注意点 selenium4.0になってからリクエストのURLが変わっています. 2.X/3.X) command_executor='http://localhost:4444/wd/hub' 4.X) command_executor='http://localhost:4444/'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WindowsにAnacondaをインストールする

いくらでも記事はありますが個人的な備忘録として。 参考:Windows版Anacondaのインストール 1. 公式サイトからWindows版をダウンロードする。 2. ダブルクリック→基本的に「Next」を押して行けば良い。 3. インストール完了。スタートメニューからAnaconda Navigatorをクリックすればホーム画面が起動する。 4. Windows PowerShellからも起動できるように設定する。まず、スタートメニューから「Anaconda Prompt (anaconda3)」を起動し、下記コマンドを実行する。 conda init 5.次にwindows PowerShellを起動し、下記コマンド実行する。 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force 6. 以降はPowerShellを起動することでAnaconda環境を利用できる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonを用いたdiscord botを作る際の機能逆引きリファレンス

初めに discord botを作成する際に使うと思われる機能群を逆引きで調べられるようにほぼ備忘録的に書きます。 ついでにbotをcog化する方法も書きます。ついでにコード内で文章では触れてない、陥りやすい罠にも触れます。 実行環境 python 3.9 discord.py 1.6.0 requests 2.24.0 psycopg2 2.8.5 Heroku (環境変数などはHerokuに保存して、それをos.environで取得して運用します。) cog化 botを作っていくと全体のコードが長くなっていきます。これを続けると非常に見づらく、例えばバグが発生したときに発生箇所を探すのが面倒臭くなります。そこで、機能単位でファイルを分けられるcog, Extensionの考え方にとりあえず慣れましょう。 詳しくはこの記事にまとまっています。非常にわかりやすいので一読することをお勧めします。 https://qiita.com/Takkun0530/items/45b4a6acd7c74e651ec2 Extensionの考え方は以下から(公式リファレンス) https://discordpy.readthedocs.io/ja/latest/ext/commands/extensions.html 私は簡単に実装するための下地を載せることにします。 cog化するための下地 import os import discord from discord import Embed from discord.ext import commands class BOT_NAME(commands.Bot): def __init__(self, prefix): intents = discord.Intents.all() # discordにはIntentの考えが最近追加されました。とりあえず全部許可する方向で書きます。 super().__init__(command_prefix=prefix, help_command=None, intents=intents) self.cur = cur for cog in os.listdir(f"./cogs"): # cogの読み込み if cog.endswith(".py"): try: self.load_extension(f"cogs.{cog[:-3]}") except Exception: traceback.print_exc() async def on_command_error(self, ctx, error): db.commit() """すべてのコマンドで発生したエラーを拾う""" if isinstance(error, commands.CommandInvokeError): # コマンド実行時にエラーが発生したら orig_error = getattr(error, "original", error) error_msg = ''.join(traceback.TracebackException.from_exception(orig_error).format()) error_message = f'```{error_msg}```' ch = ctx.guild.get_channel(CHANNEL_ID) # botで不具合が発生したときここで指定したチャンネルにエラー文を出力します。 d = datetime.now() # 現在時刻の取得 time = d.strftime("%Y/%m/%d %H:%M:%S") embed = Embed(title='Error_log', description=error_message, color=0xf04747) embed.set_footer(text=f'channel:{ctx.channel}\ntime:{time}\nuser:{ctx.author.display_name}') await ch.send(embed=embed) if __name__ == '__main__': bot = BOT_NAME(prefix="!") # prefix(コマンドの接頭辞)を定める。 bot.run(os.environ['TOKEN']) そして、同ディレクトリ内に新しくフォルダを1つ作り、名前をcogsとします。(フォルダ名の理由はos.listdir(f"./cogs")の部分で察してください。) そしてそのフォルダ内に実装する機能群をファイルに分けながら記述していきます。その下地も載せておきます。 例としてMinecraftのゲームに登場する概念である、1LC, 2stなどの記号を整数値に書き換えるコマンド!csコマンドを書いています。 機能実装用の下地 import traceback from discord.ext import commands import discord import os from datetime import datetime """Stackを計算するコマンド""" # 機能群でclassに纏める。この例だとコマンドは1つしかないが複数置くことも可能。 class StackCalc(commands.Cog): # 初期設定。基本はこれで問題ないが、実装内容によってはここを変える必要がある。(初期で変数に何か値を入れたい場合はここで操作する) def __init__(self, bot): self.bot = bot # コマンドを配置する際はデコレーターを入れる。 # @commands.command() (コマンドの場合、コマンド名を複数定めたい場合(!csでも!check_stackでもこのコマンドを動かしたい場合)、 # @commands.command(aliases=["check_stack"])のように定める。), # @commands.Cog.listener() (on_messageなど、discord.pyで既にある関数の場合), # @staticmethod (ファイル外で使用する場合) # @tasks.loop() (繰り返し処理する場合, seconds=20, minutes=2などの定め方ができる。) などが自分ではよく使うデコレータ。 @commands.command() async def cs(self, ctx, amount): try: try: if int(amount): if self.bot.stack_check_reverse(amount) == 0: await ctx.channel.send(f"入力した値が0または不正な値です。") return else: await ctx.channel.send(f"{amount}はスタック表記で{self.bot.stack_check_reverse(amount)}です。") except ValueError: if self.bot.stack_check(amount) == 0: await ctx.channel.send(f"入力した値が0または不正な値です。") return else: await ctx.channel.send(f"{amount}は整数値で{self.bot.stack_check(amount)}です。") except Exception as e: orig_error = getattr(e, "original", e) error_msg = ''.join(traceback.TracebackException.from_exception(orig_error).format()) error_message = f'```{error_msg}```' ch = self.bot.get_channel(628807266753183754) d = datetime.now() # 現在時刻の取得 time = d.strftime("%Y/%m/%d %H:%M:%S") embed = discord.Embed(title='Error_log', description=error_message, color=0xf04747) embed.set_footer(text=f'stack_calc\ntime:{time}\nuser:None') await ch.send(embed=embed) def setup(bot): bot.add_cog(StackCalc(bot)) # 例えば、上記下地で登場したcsコマンドにて、self.bot.stack_check(amount)という関数が存在しますが、これは先ほど挙げたcogの下地ファイルに以下のように追記します。ついでにファイルごとに権限設定ができるのでその例も載せます。 cog化するための下地(stack_checkを載せたバージョン) import os import discord import random from discord import Embed from discord.ext import commands class BOT_NAME(commands.Bot): def __init__(self, prefix): intents = discord.Intents.all() # discordにはIntentの考えが最近追加されました。とりあえず全部許可する方向で書きます。 super().__init__(command_prefix=prefix, help_command=None, intents=intents) self.cur = cur for cog in os.listdir(f"./cogs"): # cogの読み込み if cog.endswith(".py"): try: self.load_extension(f"cogs.{cog[:-3]}") except Exception: traceback.print_exc() async def cog_check(self, ctx): # cog内のコマンド全てに適用されるcheck。例えばadminロールがついてないと利用できないようにしたい場合 if discord.utils.get(ctx.author.roles, name="admin"): return True else: await ctx.send('運営以外のコマンド使用は禁止です') return False async def on_ready(self): color = [0x126132, 0x82fc74, 0xfea283, 0x009497, 0x08fad4, 0x6ed843, 0x8005c0] await self.get_channel(CHANNEL_ID).send( embed=discord.Embed(description="起動しました", color=random.choice(color))) # channel_idに起動を知らせるembedを出力。 @staticmethod # 今回の場合、他のファイルでこの関数を使用したい&どのファイルからアクセスしても動作に変わりがないのでstaticmethodとする。 def stack_check(value) -> int: """ [a lc + b st + c]がvalueで来ることを想定する(関数使用前に文の構造確認を取る) 少数出来た場合、少数で計算して最後にintぐるみをして値を返す :param value: [a lc + b st + c]の形の価格 :return: 価格をn個にしたもの(少数は丸め込む) """ value = str(value).replace("椎名", "").lower() stack_frag = False lc_frag = False calc_result = [0, 0, 0] if "lc" in value: lc_frag = True if "st" in value: stack_frag = True try: data = value.replace("lc", "").replace("st", "").replace("個", "").split("+") if lc_frag: calc_result[0] = data[0] data.pop(0) if stack_frag: calc_result[1] = data[0] data.pop(0) try: calc_result[2] = data[0] except IndexError: pass a = float(calc_result[0]) b = float(calc_result[1]) c = float(calc_result[2]) d = int(float(a * 3456 + b * 64 + c)) if d <= 0: return 0 else: return d except ValueError: return 0 async def on_command_error(self, ctx, error): db.commit() """すべてのコマンドで発生したエラーを拾う""" if isinstance(error, commands.CommandInvokeError): # コマンド実行時にエラーが発生したら orig_error = getattr(error, "original", error) error_msg = ''.join(traceback.TracebackException.from_exception(orig_error).format()) error_message = f'```{error_msg}```' ch = ctx.guild.get_channel(CHANNEL_ID) # botで不具合が発生したときここで指定したチャンネルにエラー文を出力します。 d = datetime.now() # 現在時刻の取得 time = d.strftime("%Y/%m/%d %H:%M:%S") embed = Embed(title='Error_log', description=error_message, color=0xf04747) embed.set_footer(text=f'channel:{ctx.channel}\ntime:{time}\nuser:{ctx.author.display_name}') await ch.send(embed=embed) if __name__ == '__main__': bot = BOT_NAME(prefix="!") # prefix(コマンドの接頭辞)を定める。 bot.run(os.environ['TOKEN']) では各機能、それに必要なコードを記していきます。cog化されている前提で話を進めていきます。 各種機能実装例 Ⅰロールを操作する 実装例: サーバーにユーザーが入った時にロールを付与する roleを付与 @commands.Cog.listener() async def on_member_join(self, member): if member.bot: role = discord.utils.get(member.guild.roles, name="bot") # ロール名からRoleオブジェクトを取得できる await member.add_roles(role) # ここで付与 return else: role = discord.utils.get(member.guild.roles, name="ユーザー") await member.add_roles(role) roleをはく奪 @commands.command() async def rr(self, ctx, check_role: discord.Role, member_id): i = 0 if member_id == "a": for mem in check_role.members: i += 1 await mem.remove_roles(check_role) embed = discord.Embed( description=f"{ctx.author.display_name}により、\n" f"{i}人の{check_role.name}役職\n" f"をはく奪しました。", color=0x006400) await ctx.channel.send(embed=embed) else: try: member = discord.utils.get(ctx.guild.members, id=int(member_id)) if discord.utils.get(member.roles, id=check_role.id): await member.remove_roles(check_role) embed = discord.Embed( description=f"{ctx.author.display_name}により、\n" f"{member.display_name}から\n" f"{check_role.name}をはく奪しました。", color=0x006400) await ctx.channel.send(embed=embed) else: await ctx.channel.send(f"{member.display_name}は{check_role.name}を所持していません。") except ValueError: await ctx.channel.send("引数が不正です。") except discord.errors.HTTPException: await ctx.channel.send("空白は一つだけ有効です。(Roleをメンションで挿入した際に空白を更に一つ開けていませんか?)") Ⅱ メッセージを送信する 実装例: コマンド実行時刻からn時間後の時間を10回分連続出力するコマンド メッセージを送信 # time引数にはn時間後のnを表す文字列が来ることを想定 @commands.command(aliases=["add_times"]) async def at(self, ctx, hour): now = datetime.now() description = "計算結果\n-----------------------\n" for i in range(10): description += f"{i + 1}: {now.strftime('%m/%d %H:%M')}\n" now += timedelta(hours=int(hour)) await self.bot.get_channel(id=740183927729160252).send(description) Ⅲ メッセージを削除する 実装例 指定数分、コマンドを入力したチャンネルにおける最新のメッセージから削除するコマンド メッセージを削除 @commands.command(name='del') # python自体にdel関数が用意されており、名前が一致してしまう際には_delのように書き、nameで指定する。 async def _del(self, ctx, n): # メッセージ削除用 p = re.compile(r'^[0-9]+$') if p.fullmatch(n): count = int(n) await ctx.channel.purge(limit=count + 1) Ⅳ SQLを利用する HerokuにはHeroku Postgreがあり、この機能を組み込むことでPostgre SQLが利用できます。 実装していきましょう。自分のプロジェクトにHeroku PostgreのAdd-onを追加する方法はここでは省略します。 クレジットカード情報の登録が必要だったはずです。但し無料プランがあるのでそれを利用すれば請求は起こりません。 DBを利用する import traceback from discord.ext import commands import discord import psycopg2 import os from datetime import datetime SQLpath = os.environ["DATABASE_URL"] db = psycopg2.connect(SQLpath) # sqlに接続 cur = db.cursor() # なんか操作する時に使うやつ # ここまで出来たら、要所要所でデータを取得、登録したい、操作したい部分で以下のように入力する # データを取得したい(SELECT文。test_tableというテーブルから全データを取得する場合) # ex. test_tableをカラム長2, レコード長2の2*2のテーブルだったとする cur.execute("SELECT * FROM test_table") data = cur.fetchall() await ctx.channel.send(data) # 出力: [[データ1,データ2],[データ3,データ4]] # 2元配列になる # データを登録、編集、操作したい場合 # CREATE, UPDATE, INSERTなどは基本的に同様の操作で実装できる。f-stringを用いて入力したデータの入力もできる。 # f-stringは処理が遅いという話があるが、正直運用で困ることは大規模でない限りほぼ無い # 入力されたデータをinput_dataとする。ここで、input_dataを[5,6]の1元配列とする。 cur.execute(f"INSERT INTO test_table values ('{input_data[0]}, '{input_data[1]}'')") db.commit() # 確認用。db.commit()を通して問題なかったら一応データの挿入には成功している。その前にデータのフォーマットの確認位はしたほうがいいだろう。 await ctx.channel.send("処理完了") ※重要 SQL文は一つ一つのオーダー(SELECTでデータを取って来い!!、INSERTでデータを保存しろ!!など)で、SQL文が違う、データ型が一致しないなどが起きると、オーダーのトランザクションが閉じるというエラー(cursor already closed)が出てSQLが使えなくなる。 これを回避するために、全てのエラーが出た時にとりあえずdb.commit()を入れておけば解決する。 なので、最初のcogの基盤の部分でon_command_error関数はエラーが出たら必ず処理が通るので最初にdb.commit()を通している。 ↑これはpsycopg2ライブラリを使用しているからである。cursorを作成する必要がないasyncpgライブラリを使うと非同期的処理が可能である。 以下のサイトを参考してください。 https://qiita.com/hoto17296/items/fcefdb308d95c01606c7 Ⅴ APIを利用する APIは機能実装の幅を大きく広げるので覚えておくといい。 APIは、json形式、xml形式(割と希少)などがある。とりあえずjson形式で紹介する。 apiのレスポンスを読み込む import requests url = "APIのURL" response = requests.get(url) jsonData = response.json() ここでjsonDataは多重配列になっている。 テストjson { success:true lastUpdated:1618056025550 products:{ product_1:{ product_id:11023 sell_summary:[ 0:{ amount:200 pricePerUnit:2.4 orders:2 } 1:{ amount:240 // これを知りたい pricePerUnit:2.7 orders:100 } ] } } } 上のようなjsonを読み込んだときに、知りたいとコメントしてある部分のデータを取得するなら、 jsonData[products][product_1][sell_summary][1][amount]と入力すると取得できる。 100件~1000件程度のデータ量だとそこまで時間はかからないが1万を越してくると少し時間がかかるので実装を工夫するといい。 スレッドをいくつか立てて並列処理を組ませるなどの手が有効である。 終わりに 基本的には上記5つの操作が分かれば簡単なbotなら作れる。 分からないことがあればdiscord botを作るコミュニティがあるので質問しよう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python ループ処理速度(append vs reduce)

結論 割と大きめのデータ処理をしている時、もっと処理速度を早くしようと思って色々いじっていたら、reduceよりappendの方が断然早かった話 はじめに 実行時間計測に使用するデータはこれで生成 import random data_list = [random.randint(-2, 2) for _ in range(100000)] 作ったデータをどのように処理したいかというと、 配列のある要素x_nとその要素のひとつ前x_(n-1)を加算した新しい配列を生成したい↓ [1, 2, -1, -2] => [1, 3, 2, 0] ループ処理 appendメソッドを使う def append_loop(data): out_put = [data[0]] for i in data[1:]: out_put.append(out_put[-1] + i) return out_put # 実行時間: 0.019423961639404297 appendは破壊的メソッドとよばれ、思わぬバグを発生させることが多く。また空の配列を用意しなくてはいけないため、コード量がその分多くなったりするので割と避けていたが速度は非常に早かった。 reduce def reduce_append(li, elem): return [elem] if not li else li + [li[-1] + elem] # 実行時間: 11.915228843688965 reduce(reduce_append, data_list, []) reduceを使う場合は非常にコード簡潔になるが、速度に関しては大きな差が開いている。およそ600倍の時間がかかっている。 これにはif文があるから実行時間が遅くなった可能性があるので、少々可読性は落ちるが、そのパターンも作成 def reduce_append(li, elem): return li + [li[-1] + elem] # 実行時間: 10.55799412727356 reduce(reduce_append, data_list[1:], [data_list[0]]) 実行時間は1秒くらいしかかわらなかった。 おまけ 内包表記 [sum(data_list[:i + 1]) for i in range(len(data))] # 実行時間: 86.43631505966187 一行でかけるが、遅すぎて使い物にならない。sum関数自体にループがあるからしゃーない 再帰関数 @tail_recursion def recursive_function(result_array, origin_array, index): if len(result_array) > 0: result_array.append(result_array[-1] + origin_array[index]) else: result_array.append(origin_array[0]) if len(origin_array) > index + 1: return recursive_function(result_array, origin_array, index + 1) # 実行時間: 0.08165287971496582 recursive_function(result, data_list, 0) わかりにくいがそこそこはやい。テクニークの練習にはなる。 おわり append使うのはダサいとか、カッコ悪いとか、初心者とかおもっていたけどちょっと見直しました。まぁ私、初心者なんですが笑 なにか他に良い方法があったら教えてくださいヽ( ◔ ౪ ◔ )ノイッパツデコンパイルトオッタァァァァwwwwww
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ROS Python】Subscriberがqueue_size=1でも最新のメッセージを受け取れいないときの対処

この記事の要約 ROSにおいて,PythonのSubscriber内部の処理が重い場合,queue_size=1としても最新のメッセージを受け取らなくなる Subscriberのbuff_size=2**24などとすると対処できるとあると他の記事で書かれている(しかしこれで対処できなかった) 結果,元のPublishrをqueue_size=1とすることで対処できた queue_size=1としても最新のメッセージを受け取らない場合がある sub.py sub = rospy.Subscriber('/image_raw', Image, imageCb, queue_size=1) queue_size=1とすると,複数のメッセージ(上の場合は/image_rawという画像)を保持しなくなるため,たとえimageCBの処理が重くても,常に最新のメッセージに対する処理ができる様に思えます.しかしなぜか,メッセージを複数保持してしまい,時間が経てば経つほど最新のメッセージと処理するメッセージの間に時間遅れが出てきてしまいます. 大きめのbuff_sizeを指定して対処 sub_buf.py sub = rospy.Subscriber('/image_raw', Image, imageCb, queue_size=1, buff_size=2**24) 上に示す様に,Subscrierのbuff_sizeを大きくすると対処できるという記事を発見.例えばココ.しかしこれでも問題は直せず. Publisherもqueue_size=1とする pub.py pub = rospy.Publisher('/image_raw', Image, queue_size=1) ここまでやってちゃんと予期した挙動になりました.理由はよくわからないけど,とりあえずこれで最新のメッセージに対してだけ処理ができる様になりました. この記事のまとめ ROSにおいてPythonのSubscriberの処理が重い場合,queue_size=1としても最新のメッセージだけを処理することができないという問題を見かけました.buff_size=2**24など,Subscriberのbuff_sizeを大きくすると直せるという記事を見つけましたが,結果直せませんでした.最終的に,Publisherもqueue_size=1とすることで予期した挙動となりました.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python Library

ライブラリ pythonのライブラリは非常に優秀なものが揃っていると言われる システム pip(標準) ライブラリを管理 sys(標準 「システムパラメーター」という、システムの挙動に外部から干渉できるデータに関する関数が含まれています。 日付 datetime(標準) datetimeの主な機能は、時刻に関するものです。 ・現在の日付と時刻の取得 ・年月日から時分秒までを単体で取得 ・日付の足し算や引き算 dateutil dateutilは、標準ライブラリであるdatetimeの扱いにくさ、機能不足を補ってくれます。 dateutilの主な機能はこちらです。 ・現在時刻の取得 ・日付の足し算と引き算 calendar その名の通りカレンダーの表示や、日付の計算等ができます。 日付データを表示するライブラリとしてある程度便利 数学系 math(標準) ・円周率などの表示 ・平方根や自然対数などの計算 Numpy 配列や行列の演算を高速で行うことができ、非常に人気のあるライブラリでもあります。 ・配列の演算 ・行列の演算 matplotlib データをグラフ化するのに非常に便利なのが、このmatplotlibです。 ・sinやcosの計算式をグラフ化 ・グラフにタイトル、軸名、凡例などを付ける ・グラフの線の色、線種を変更できる matplotlibは、mathや numpyとも相性がいいです 統計・データ処理 pandas データの統計量の表示や、そのグラフ化ができます。 Pythonでデータ分析や機械学習を行う場合には必須となるライブラリです。 ・データ概要の表示 ・データの行数列の表示 ・統計量を表示 sklearn こちらは、比較的簡単な機械学習アルゴリズムです。 学習させたいデータを入力して学習させ、予測したいデータを入力することで予測の正答率を算出することができます。 ・分類 ・回帰 ・クラスタリング 画像処理 Pillow 基本的な画像処理を行うライブラリです。 次に紹介する「OpenCV」のような高度な処理はできないものの、シンプルな構造で使いやすいという特徴があります。 ・画像の表示 ・画像のリサイズ ・画像のトリミング OpenCV コンピュータで画像、データを処理するのに便利な様々な機能を備えています。 Pythonで画像処理を行う場合は欠かせないライブラリです。 ・画素データの表示 ・輪郭の抽出 ・合成 TWINT TWINTはツイッターのスクレイピングライブラリーです。 ツイッターAPIなしにツイートをスクレイピングできます。 便利 iclawler iclawlerはキーワードを指定して、まとまった画像を一気にダウンロードできるライブラリ 機械学習などのトレーニング用の画像をキーワード指定で一気に集められるのがこのライブラリ。 100枚など枚数を決めてダウンロードすることができます。 フォルダを作って指定のキーワードでネット上にある画像を集めてくれるので、とても便利 PyCaret わずか数行という簡単なコードの記述のみで機械学習の前処理、モデルの構築、はたまたデータ可視化まで行ってくれるライブラリです。 機械学習でおなじみのscikit-learnなどのライブラリの機能を含んでいます。 PyCaret2.0ではautomlの機能が追加され、かなりパワーアップされました。 正直AUTO MLか!とまで言われたライブラリ。 機械学習入門者にはぜひ使って欲しい。 pickle pickleはあらゆるデータを保存しておくのに便利なライブラリ。 リスト、文字列、クラスのインスタンス、データフレームなど色々なデータをバイナリファイル形式に変換して保存することができ、呼び出すのも簡単です。 メリットはプログラムをシャットダウンしてもデータが保存されていること、CSVからの読み込みよりもスピードが数十倍から100倍程度早いことなどです。 pytube Youtubeの動画をダウンロードすることができるライブラリです。 例えば動画の文字起こしのプログラムを作るために、何か適当な動画を使いたいなどの理由で使えます。 pytubeを使えば、好きな動画を指定して自由にダウンロードすることができるので、便利
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Slack APIを用いてある一定期間より前のメッセージを自動削除する

Slack APIとpythonを用いて一定期間より前のSlackのメッセージを削除するスクリプトの例を示す。 参考URLではlegacy Tokenを使用しているが、現在は使用できないので、OAuth Tokenを使用する必要がある。 また、chat.deleteメソッドとchannels.historyメソッドを使用しているが、channels.historyは使用できないので、conversations.historyメソッドを使用する必要がある。 参考にしたURL https://michimani.net/post/programming-delete-old-slack-messages/ pythonファイル from datetime import datetime #日付を扱うため from time import sleep # 処理を一旦停止するため import json # json形式のデータを扱うため import re #正規表現の操作を行うため import sys # システムに関する処理を行うため import urllib.parse #URLを解析して構成要素を得るため import urllib.request #URLを開くため DELETE_URL = "https://slack.com/api/chat.delete" #メッセージを削除するためのSlackのAPIのURL HISTORY_URL = "https://slack.com/api/conversations.history" #メッセージの履歴を得るためのSlackのAPIのURL API_TOKEN = '*********************************' #メッセージに対する操作をする際に必要となるAPI_TOKEN※legacy Tokenは使用できないことに注意。各自で変更 TERM = 60 * 60 * 24 * 7 #1週間なので7を指定。ここの数字を変更することで、削除対象のメッセージの期間を変更できる def clean_old_message(channel_id): #メッセージを削除するための関数 print('Start cleaning message at channel "{}".'.format(channel_id)) # どのチャンネルのメッセージを削除するのか表示する current_ts = int(datetime.now().strftime('%s')) #現在の時刻を取得 messages = get_message_history(channel_id) #channel_idで指定したchannelのメッセージを取得する for message in messages: #このfor文である期間以上より前のメッセージを自動的に削除する if current_ts - int(re.sub(r'\.\d+$', '', message['ts'])) > TERM: delete_message(channel_id, message['ts']) sleep(1) def get_message_history(channel_id): #メッセージの履歴を取得するための関数 params = { 'token': API_TOKEN, #使用するAPI_TOKENを指定する 'channel': channel_id, # メッセージ履歴を取得するchannelのidを指定する 'limit': 500 #メッセージを取得する件数の数を指定する } req_url = '{}?{}'.format(HISTORY_URL, urllib.parse.urlencode(params)) #HTTP requestを投げるためのURLを構成する req = urllib.request.Request(req_url) #実際にそのURLにrequestを投げる message_history = [] with urllib.request.urlopen(req) as res: data = json.loads(res.read().decode("utf-8")) #utf-8でjsonのデータをデコードする if 'messages' in data: # dataがレスポンスの文言に含まれているか確認する message_history = data['messages'] return message_history def delete_message(channel_id, message_ts): #メッセージを削除する関数 headers = { 'Content-Type': 'application/x-www-form-urlencoded' } params = { 'token': API_TOKEN, #使用するAPI_TOKENを指定する 'channel': channel_id, # メッセージ履歴を削除するchannelのidを指定する 'ts': message_ts # 削除するメッセージのタイムスタンプを指定する } req_url = '{}?{}'.format(DELETE_URL, urllib.parse.urlencode(params)) #URLを構成する文字列を定義する req = urllib.request.Request(req_url, headers=headers) # requestを投げる with urllib.request.urlopen(req) as res: data = json.loads(res.read().decode("utf-8")) if 'ok' not in data or data['ok'] is not True: print('Failed to delete message. ts: {}'.format(message_ts)) if __name__ == "__main__": args = sys.argv if len(args) < 2: print("The first parameter for slack channel id is required.") else: clean_old_message(args[1])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

__init__メソッド

__init__メソッド __init__メソッドはインスタンスを生成した直後に処理を実行することができるメソッドです。 「クラス名()」でインスタンスを生成した直後に自動で呼び出されます。 __init__メソッドは他のインスタンスメソッドと同様に定義することができます。 例 class FruitPrice: def __init__(self): print('自動で出力されました') fruit_price = FruitPrice() 出力結果 自動で出力されました と記述することで、fruit_price = FruitPrice()でFruitPriceクラスのインスタンスが生成された直後に__init__メソッドが呼び出され、その中の処理が実行されます。 これまでは、インスタンスメソッドがinfoであればfruit_price.info()と記述しなければ呼び出すことができませんでした。 __init__メソッドを用いる利点 これまでは、 1.クラスを用意する 2.クラスからインスタンスを生成する 3.インスタンス変数に値を代入する を順に行っていたが、__init__メソッドを用いることで手順の2,3をまとめて、 「インスタンス生成すると同時にインスタンス変数に値を代入する」を行うことができます。 例 class FruitPrice: def __init__(self): self.name = 'apple' # インスタンス生成直後にインスタンス変数nameに'apple'を代入 fruit_price = FruitPrice() print(fruit_price.name) 出力結果 apple と記述することで、インスタンス生成直後にインスタンス変数nameに'apple'を代入することができました。 しかし上記のままだと、どのインスタンスにもnameが'apple'になってしまいます。 そのため、__init__メソッドに引数を渡すことで、インスタンスごとに値を変えることができます。 __init__メソッドは通常のインスタンスメソッドと同じように、引数を受け取ることもできます。 その際、インスタンスを生成している「クラス名()」に対して引数を渡すことで、__init__メソッドにその値を渡すことができます。 例 class FruitPrice: def __init__(self, name): # nameに'apple'が渡されている self.name = name fruit_price = FruitPrice('apple') print(fruit_price.name) 出力結果 apple
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

競技プログラミングメモ(Python)

グリッドに関する処理 ABC75 B Minesweeper H, W = map(int, input().split()) S = [] for i in range(H): S.append(list(input())) #上、下、右、左、右上、右下、左上、左下 dy = [-1, 1, 0, 0, -1, 1, -1, 1] dx = [0, 0, 1, -1, 1, 1, -1, -1] for y in range(H): for x in range(W): if S[y][x] == '#': continue cnt = 0 for i in range(8): ny = y + dy[i] nx = x + dx[i] if ny < 0 or ny >= H: continue if nx < 0 or nx >= W: continue if S[ny][nx] == '#': cnt += 1 S[y][x] = str(cnt) for y in range(H): print("".join(S[y])) ABC96 C Grid Repainting 2 import sys H, W = map(int, input().split()) S = [] for i in range(H): S.append(list(input())) #上、下、右、左 dy = [-1, 1, 0, 0] dx = [0, 0, 1, -1] for y in range(H): for x in range(W): if S[y][x] == '.': continue if S[y][x] == '#': for i in range(4): ny = y + dy[i] nx = x + dx[i] if ny < 0 or ny >= H: continue if nx < 0 or nx >= W: continue if S[ny][nx] == '#': break if i == 3: print('No') sys.exit() print('Yes')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

USBドライブを遠隔のWindows共有フォルダに同期するソフトを作った[rsync,CIFS,Raspberry Pi]

はじめに ネットにつながっていない機器上でUSBドライブに保存したデータを簡単に遠隔のサーバーにアップロードする必要があったため、作りました。 動作環境 ファイルを受け取る側 Windows 10 (ソースコードを修正すれば*nixでも使える) ネットワークにつながっていること 共有フォルダを作る権限 Softether VPNサーバー ファイルを送る側 Raspberry Pi 3 (通常のPCでも問題ない) Raspbian ネットワークにつながっていること 動作概要 Raspberry PiにUSBドライブを挿す。すると、VPN経由で遠隔の場所にあるCIFSディレクトリに、USBドライブの中身がrsyncによって同期される。 構成 autofsはUSBドライブをマウントする用途で用いる マウント先を選ぶことができる udevはUSBドライブマウント時に同期用のスクリプトを実行する用途で用いる cifs-utilsは遠隔のCIFSディレクトリをマウントする用途で用いる softether-vpnは遠隔のネットワークのCIFSディレクトリにアクセスする用途で用いる systemdは以上の内容をサービスとして登録し、起動時に自動的に立ち上がるようにする用途で用いる 環境構築 以下の手段はRaspberry Pi上で行う。Windows上ではSoftether VPNサーバーの構築を行う。 インストール sudo apt install git, autofs # gitとautofsをインストール git clone https://github.com/rayanti/usb_sync.git # githubのリポジトリ(当プロジェクト)をクローン sudo cp {auto.master,auto.misc} /etc # autofsのファイルを/etcにコピー sudo cp 1-usb.rules /etc/udev/rules.d # udevのファイルを/etcにコピー 静的ルーティング 以下のファイルをコピーし、各自の環境に合わせて設定する sudo cp 40-route /lib/dhcpcd/dhcpcd-hooks/ Softether vpncmdインストール vpncmdはSoftether VPNのコマンドラインツールである https://www.softether-download.com/en.aspx?product=softether で必要なファイルをダウンロードする ターミナル上で以下の手段にしたがって、環境の設定を行う tar xzvf vpnserver-5070-rtm-linux-x86.tar.gz cd vpnclient make #(1,1,1) sudo ./vpnclient start ./vpncmd #(2," ") PasswordSet RemoteDisable NicCreate #VPN AccountCreate #{account name} AccountPasswordSet #standard AccountConnect AccountStartupSet quit cifs-utilsの設定 共有フォルダを閲覧する権限のあるwindowsユーザーのログイン情報を入力する。これにより、Windowsの共有フォルダをLinux上でマウントすることができる。 cifs-credientialsにはパスワードを保存するため、rootユーザー以外から見えないようにする必要がある。 sudo apt install cifs-utils sudo chown root: /home/pi/usb_sync/cifs_credentials sudo chmod 600 /home/pi/usb_sync/cifs_credentials vpncmdを自動起動するサービスに登録する systemdのサービスとして登録する。 sudo cp vpnserver.service /etc/systemd/system/ sudo cp vpn_dhclient.service /etc/systemd/system/ systemdサービスを自動起動するように設定 startで起動、enableでスタートアップ時自動起動する systemctl daemon-reload systemctl start vpnserver systemctl enable vpnserver systemctl start vpn_dhclient systemctl enable vpn_dhclient systemctl start cifs_mount systemctl enable cifs_mount 各自の環境に合わせて設定する項目 nmtuiでipアドレスの固定 40-routeでルーティングの設定 1-usb.rules内で用いるUSBドライブのUUIDの設定 付録 automountが行われない場合 autofsのファイルは実行可能な権限を持っていると動かないため、以下のコマンドを実行 sudo chmod -x /etc/auto*
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラスとインスタンス

クラスとインスタンス プログラミングでものを生成するには、まずそのものの設計図を用意する必要があります。 設計図をクラス、ものをインスタンスと呼びます。 クラスとインスタンスは理解が難しく様々な捉え方があります。 私はたいやきでイメージしており、 ・クラス:たい焼きの型 ・インスタンス:たい焼き と捉えています。 クラスの定義 クラスは「class クラス名:」と記述することで定義できます。 クラス名は「FruitPrice」のように大文字で始めるようにします。 例 class FruitPrice: #処理を記入 インスタンス生成 クラス()と記述してクラスを呼び出すことで、クラスを用いて新しくインスタンスを生成することができます。 また、「変数名 = クラス名()」と記述することで生成したインスタンスを変数に代入することができます。 例 class FruitPrice: #処理を記入 fruit_price = FruitPrice() インスタンスにはそれぞれ名前や値段などの情報を追加することができます。 例えば上記では、「fruit_price.name = 'apple'」とすることで、変数fruit_priceにnameがappleであるという情報を追加することができます。この時、nameのことを「インスタンス変数」と呼びます。 例 class FruitPrice: pass # passは処理が何もないことを表します fruit_price = FruitPrice() fruit_price.name = 'apple' print(fruit_price.name) 出力結果 apple メソッド クラスの中では関数を定義することができます。クラスの中で定義した関数を「メソッド」と呼びます。 メソッドの定義の方法は通常の関数のと同じですが、第1引数にselfを追加する必要があります。 クラスの中で定義したメソッドを呼び出す 呼び出すには「インスタンス.メソッド名()」と記述することで呼び出すことができます。 クラスの中で定義し、インスタンスに対して呼び出すメソッドのことを、「インスタンスメソッド」と呼びます。 例ではhelloがインスタンスメソッドに当たります。 例 class FruitPrice: def hello(self): print('おはよう') fruit_price = FruitPrice() # インスタンス生成し、変数に代入 fruit_price.hello() # クラスの中で定義したメソッドを呼び出す 出力結果 おはよう self ここでインスタンスメソッドでの第1引数に指定したselfにはそのメソッドを呼び出したインスタンス自身が代入されています。 下図の例では、メソッド内でself.nameとすることで、そのメソッドを呼び出しているfruit_priceのnameの値を取得することができます。 例 class FruitPrice: def info(self): # selfにfruit_priceが代入される print(self.name) fruit_price = FruitPrice() fruit_price.name = 'apple' fruit_price.info() # クラスの中で定義したメソッドを呼び出す 出力結果 apple ここまで学んだことを用いて、fruitの合計金額の出力を行います 例 class FruitPrice: def info(self): return self.name + 'は'+ str(self.price) + '円です' #self.priceは数値なので文字列に変換する必要があります def get_total_price(self, count): total_price = self.price * count return total_price fruit_price = FruitPrice() fruit_price.name = 'apple' fruit_price.price = 300 print(fruit_price.info()) result = fruit_price.get_total_price(3) print('合計金額は' + str(result) + '円です') 出力結果 appleは300円です 合計金額は900円です ここで気をつけるポイントは、インスタンスメソッドに引数を渡す場合、メソッドの定義側ではselfの分だけ引数の順番がずれることです。 fruit_price.get_total_price(3)の3はget_total_price(self, count)のcountに代入されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SIGNATE ファンダメンタルズ分析〜探索的データ解析1

はじめに 2021/01~2021/03まで開催されたSIGNATEファンダメンタルズ分析チャレンジに参戦。 本コンペでは、株価データ・企業のファンダメンタル情報などのデータを使って、 ​「東​証​上​場​企​業​が、​決算発表した日にちから20営業日以内に、株価がどれだけ上昇するか、および、どれだけ減少するか?」 を予測します。 詳しくは、Signateのチュートリアルを参照。 なお関連してSIGNATEニュース分析チャレンジは2021/05まで開催されてます。 環境:Google Collaboratoryを使ってます。 業種(17 Sector Code)ごとにファンダメンタル分析 ある銘柄の株を買うときに、利益率とかPER(株価収益率)とかを、同業他社と比べるはずなので、 特徴量をつくるときには、業種ごとに標準化すると良いのではと考えました。 そこで、業種ごとにファンダメンタル情報を整理しました。 17業種の名称(英語) Sector Code 名称 1 FOODS 2 ENERGY RESOURCES 3 CONSTRUCTION & MATERIALS 4 RAW MATERIALS & CHEMICALS 5 PHARMACEUTICAL 6 AUTOMOBILES & TRANSPORTATION EQUIPMENT 7 STEEL & NONFERROUS METALS 8 MACHINERY 9 ELECTRIC APPLIANCES & PRECISION INSTRUMENTS 10 IT & SERVICES, OTHERS 11 ELECTRIC POWER & GAS 12 TRANSPORTATION & LOGISTICS 13 COMMERCIAL & WHOLESALE TRADE 14 RETAIL TRADE 15 BANKS 16 FINANCIALS (EX BANKS) 17 REAL ESTATE 業種ごとに銘柄数のヒストグラム sns.histplot(stock_list["17 Sector(Code)"], bins = 17) (10)ITが多く、(2)エネルギー(11)電気・ガスが少ない。 時価総額(=株価*発行株式数) sns.stripplot(y = "market_cap", x = "17 Sector(Code)", data = feature) (6)自動車・輸送機(10)IT(9)電機(15)銀行(5)製薬などに時価総額が高い企業が多い。 純利益率(=純利益/売上高) sns.stripplot(y = "NetReturn", x = "17 Sector(Code)", data = feature) plt.ylim((-1, 1)) マイナスは赤字。 (10)ITは儲かっている銘柄も多ければ、大損している銘柄も多い。 (2)エネルギーや(11)電気・ガスは大きく儲かることもないけれど大損するリスクもほとんどない。 PER(株価収益率=株価/一株あたりの純利益) sns.stripplot(y = "per", x = "17 Sector(Code)", data = feature) plt.ylim((-1, 100)) (10)ITはPERが高く割高感がある。言い換えれば将来の収益性が期待されている。 (1)食品(3)建設(4)化学(8)機械(9)電機(13)商社(14)小売などにもPERが高い成長銘柄がある。 一方で、(2)エネルギー(11)電気・ガス(15)銀行はPERが低く割安感がある。安定銘柄。 ROE(自己資本利益率=純利益/純資産) sns.stripplot(y = "roe", x = "17 Sector(Code)", data = feature) plt.ylim((-1, 1)) マイナスは赤字。 (10)ITや(14)小売には、高ROEで、純資産にたいして収益性が高い銘柄が多いが、赤字にとなっている銘柄も多い。 (5)製薬は純資産にたいして大きな収益を上げられていない銘柄が多い。 一方で、(1)食品(2)エネルギー(6)自動車・輸送機(7)鉄鋼(8)機械(11)電気・ガス(12)運送・ロジスティクス(15)銀行は、純資産にたいして大きく儲かっている銘柄は少ないが、大損している銘柄も少ない。 自己資本比率(=純資産/総資産) sns.stripplot(y = "equity_ratio", x = "17 Sector(Code)", data = feature) plt.ylim((0, 1)) 経理COMPASSによると、「製造業など固定資産を多く使う業種は、最低でも20%は欲しいところです。 商社や卸売業など、固定資産が少なく、その代わりに流動資産である売掛金や在庫などが多い業種は、最低でも15%の自己資本比率を保つことを心がけましょう。」とのこと。 たしかに製造業の(1)建設(4)化学(5)製薬(8)機械(9)電気などは自己資本比率が高い。 一方で、金融商品を扱う(15)銀行(16)金融(銀行を除く)は自己資本比率が低い。 さいごに 業種ごとにファンダメンタル情報の傾向(最大・最小・分散)が異なることが分かりました。 そのため、業種の異なる銘柄のファンダメンタル情報を単純に比較しまうと、銘柄の収益性や割安感を見落とす可能性があります。 一方で、同業他社と比較することで、適切に銘柄の割安感、収益性、時価総額を適切に評価し、投資判断できます。 本コンペにおいても、業種ごとにファンダメンタル情報を標準化したものを特徴量に組み入れることで、株価予測精度の向上が見込めます。 また、今回のコンペでは、「定常性を意識した特徴量設計」が重要である観点からも、標準化することは理にかなっていると考えられます。 定常性についてはSignateのチュートリアルを参照。 以上、最後までご覧いただきありがとうございます。 ご指摘・コメントなど頂けると嬉しいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BERTでサクっと文章要約する

はじめに BERTを用いたテキスト要約のライブラリbert-extractive-summarizerを見つけたので試してみた。あたかも自分がサクッと作ったような書き方になってしまったが、実際はこのツールがサクっとやってくれたという話。 bert-extractive-summarizer とは 講義要約用のライブラリである。仕組みとしては、まず文章をエンベディングし、クラスタリングした後、重心に近い文章(クラスタを代表するもの)をピックアップしているようである。 講義用とは言え、他の文章にも色々使えそうである。 ちなみに本ライブラリは英語用であるが、自分は英語を要約したいのでこれで十分である。 インストール pip install bert-extractive-summarizer 実際には、pytorch等いくつか依存するライブラリがあるので、試行錯誤しながら入れてみよう。 プログラム コマンドラインで動かすプログラムを書いてみた。入力とするテキストファイル、出力ファイル、抽出文章の数を指定できる。 text_summarizer.py import argparse from summarizer import Summarizer def main(): parser = argparse.ArgumentParser() parser.add_argument("-input", type=str, required=True) parser.add_argument("-num_sentences", type=int, default=5) parser.add_argument("-output", type=str, required=True) args = parser.parse_args() model = Summarizer() with open(args.input, "r", encoding="utf-8") as f: body = "".join(f.readlines()) #print(body) result = model(body, num_sentences=args.num_sentences) full = ''.join(result) with open(args.output, "w", encoding='utf-8') as fw: fw.write(full) if __name__ == "__main__": main() 動かしてみた とりあえず、CNNの以下のニュースをやってみた。 https://edition.cnn.com/2021/04/10/sport/san-diego-padres-no-hitter-joe-musgrove/index.html 上の文章をinput.txtに保存して以下を実行。今回は3つの文章を抽出する。 $ python.exe text_summarizer.py -input input.txt -num_sentences 3 -output output.txt 結果は以下の通り output.txt Pitcher Joe Musgrove tossed the first no-hitter in Padres franchise history on Friday night, as San Diego defeated the Texas Rangers 3-0 at Globe Life Field in Arlington, Texas. Musgrove struck out 10 and only allowed one base runner when he hit Joey Gallo, which prevented a perfect game. I looked up at the scoreboard to see what (the) pitch count was, and I was at, like, 67, and I was like, we've got some room to work with." Musgrove added, "It feels really good to be in a Padres uniform when I did it. 一応ノーヒットノーランを達成した様子が伝わってくる。 参考 bert-extractive-summarizer
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flake8 Pythonコードに対して使ってみた

インストール $ pip install flake8 インストール方法 バージョン確認 flake8 --version 3.8.4 (mccabe: 0.6.1, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.8.2 on Windows 実行例 $ flake8 flake8_sample.py flake8_sample.py:1:1: F401 'sys' imported but unused flake8_sample.py:3:15: W292 no newline at end of file エラーコード コード メッセージの例 F401 module インポートされたが未使用 Fに関するエラーコード W2 空白の警告 W291 末尾の空白 W292 ファイルの終わりに改行がありません W293 空白行に空白が含まれています E, Wに関するエラーコード エラーコード コード 内容 E pycodestyleによるエラー F pyflakesによるエラー W pycodestyleによる警告 詳細はこちら オプション例 --filename 対象にするファイルを指定 flake8 --filename *.py .\flake8_sample.py:1:1: F401 'sys' imported but unused .\test_sample.py:4:1: E302 expected 2 blank lines, found 1 --count エラーの数を表示する flake8 flake8_sample.py --count flake8_sample.py:1:1: F401 'sys' imported but unused flake8_sample.py:3:15: W292 no newline at end of file 2 --show-source 該当ソースを表示する flake8 flake8_sample.py --show-source flake8_sample.py:1:1: F401 'sys' imported but unused import sys ^ flake8_sample.py:3:15: W292 no newline at end of file print('hello') --select 指定したエラーコードのみ表示する flake8 flake8_sample.py --select=W292 flake8_sample.py:3:15: W292 no newline at end of file エラーコードの種類だけを選択することも可能 flake8 flake8_sample.py --select=F flake8_sample.py:1:1: F401 'sys' imported but unused F4で始まるエラーコードのみ選択することも可能 flake8 flake8_sample.py --select=F4 flake8_sample.py:1:1: F401 'sys' imported but unused --statics エラーごとにカウント数、エラーコードを表示 flake8 flake8_sample.py --statistics flake8_sample.py:1:1: F401 'sys' imported but unused flake8_sample.py:2:1: F401 'os' imported but unused flake8_sample.py:4:15: W292 no newline at end of file 2 F401 'sys' imported but unused 1 W292 no newline at end of file --max-line-length=n(デフォルトは79) 文字数超過を表示 flake8 flake8_sample.py --max-line-length=90 flake8_sample.py:1:91: E501 line too long (99 > 90 characters) --tee 標準出力とファイルに出力 --output-file 結果をファイルに出力 flake8 flake8_sample.py --tee --output-file outpu.txt flake8_sample.py:1:1: F401 'sys' imported but unused --help flake8 --help --exit-zero エラーがあってもステータスコード "0 "で終了 私の人生のように。 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】S&P500から低PERの銘柄をスクリーニングする

米国株からスクリーニングするとき,ナスダック銘柄は1万銘柄とかあるので,そこからスクリーニングするのは時間がかかって仕方がありません. そこで思いつくのが,500銘柄程度しかないインデックスであるS&P500からスクリーニングするということです. S&P500の銘柄データ(チェッカーシンボル)を持ってくる S&P500銘柄が一覧になっているデータを探すしかありません. Wikipediaに記載されている銘柄をまとめたと考えられているCSVデータが,以下のURLにあります. 参考:配当再投資でのんびり投資(https://nonbiri-reinvest.net/post-692/) ただし,公式かどうかは怪しいので,正確ではないかもしれません.また,S&P500の組入れ銘柄は定期的に入れ替わるので,最新の銘柄を取得し続ける必要があります.公式で一覧になっているものが見つかれば,再掲したいです. これを以下のようにPython(pandas)で呼んでやれば,S&P500だけのデータを取得できます. import pandas as pd url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv" df = pd.read_csv(url, encoding="SHIFT_JIS") print(df) Symbol Name Sector 0 MMM 3M Company Industrials 1 AOS A.O. Smith Corp Industrials 2 ABT Abbott Laboratories Health Care 3 ABBV AbbVie Inc. Health Care 4 ABMD Abiomed Health Care .. ... ... ... 500 YUM Yum! Brands Inc Consumer Discretionary 501 ZBRA Zebra Technologies Information Technology 502 ZBH Zimmer Biomet Health Care 503 ZION Zions Bancorp Financials 504 ZTS Zoetis Health Care これで,S&P500の銘柄を取得できました(たぶん). 低PERの銘柄をスクリーニングする 以下のコードでできます. import pandas as pd import pandas_datareader.data as web pers = pd.DataFrame({'PER':['']}, index=['銘柄']) error_symbols = [] for c in codelist['Symbol']: try: per = web.get_quote_yahoo(c)['trailingPE'] if per[c] < 10: pers.loc[c] = per[c] except: error_symbols.append(c) print(pers) 構造としては,pandas_datareaderのget_quote_yahooで各銘柄のPER(正確にはtrailingPE)を見ていき,PERが10以下ならpersというDataFrameにシンボルとPERを格納します. 深くは考えていませんが,エラーが起きた時にはerror_symbolsというリストに入るようにしています. これ(もしくは本記事の最後のまとめコード)を実行すると,以下が得られます. PER 銘柄 AFL 7.81709 ALL 6.7747 BIO 4.51307 CE 8.71157 EBAY 7.3074 KIM 8.76055 MHK 3.7743 PGR 9.66667 PHM 9.60201 COO 8.52827 UNM 7.40874 3分ほどでスクリーニングできました.S&P500でも結構かかりますね. まとめコード S&P500から低PERの銘柄をスクリーニングするコードです. import pandas as pd import pandas_datareader.data as web url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv" codelist = pd.read_csv(url, encoding="SHIFT_JIS") pers = pd.DataFrame({'PER':['']}, index=['銘柄']) error_symbols = [] for c in codelist['Symbol']: try: per = web.get_quote_yahoo(c)['trailingPE'] if per[c] < 10: pers.loc[c] = per[c] except: error_symbols.append(c) print(pers) ちなみに:ナスダック銘柄からスクリーニングする ナスダック銘柄からスクリーニングする場合は, from pandas_datareader.nasdaq_trader import get_nasdaq_symbols codelist = get_nasdaq_symbols() #ナスダック銘柄を取得 という感じでナスダック銘柄を取得できます. 今回の内容も含めて,詳しくは以下の記事に書いてあります.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アナログメータのデジタル化計画

0. はじめに。 ある昼下がり、ふと思い立った。アナログメータをデジタルで取得しようと。 今回は同じような気持ちになったエンジニアに向けて、こんな方法もあるよ ということを提案したい。 ※尚、とりあえず動けばいいやコードなので、その点ご容赦ください 目的のメータの感じ。 リアルなやつ ちなみに結論を先に示すと、認識の結果はこんな感じになる。 (数字が違うのはテストはランダムな画像を抽出しているので) なんとなく成功している気もする... では始めていくが、そもそも何からやろうか 画像を集める 学習させる/チューニングする テストする 大きく分けるとこんな感じか?とりあえずやってみよう! 1. 学習用画像データの準備 始めようとした時ふと思った。「画像集めるのめんどくさくね?」 データセットがネットに落ちてないか調べてみた。 . . . ないですね。 「なら、MNISTのデータセット使ってやればできんじゃね?」 [MNIST]:https://ja.wikipedia.org/wiki/MNIST%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9 すまん無理だったわ。手書きとアナログ数字では結構違う特徴があるみたいで、 正解率が70%くらいだった。 覚悟を決めて、画像を収集することに イメージはこんな感じで、ウェブカメラ(¥1000くらい)でひたすら画像を撮影し、データを蓄積していく。 (我ながらひどいイメージですみません) 画像を集める 今回は長時間放置して撮影したいので、ARマーカーを使って、ちょっとカメラがズレても良い仕様にしようと思う。 もっといい方法はあると思うけど。 また、切り取り位置は調整してください。 メータ任せでデータをひたすら取得するので、止めるタイミングは自分自身で決める。 この時、取れないデータやデータ量の偏りが発生するので、意識してデータを取得する。 captureMeterImage.py # 必要なライブラリ import cv2 import time # ARマーカーの設定 aruco = cv2.aruco dictionary = aruco.getPredefinedDictionary(aruco.DICT_4X4_50) def meterImageSaver(camera_num, cycle): # ウェブカメラ検出、設定 cap = cv2.VideoCapture(camera_num) img_cnt = 0 # 自分が納得するまでデータを撮り続ける。 while True: ret, frame = cap.read() cv2.imshow("frame", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break Height, Width = frame.shape[:2] img = cv2.resize(frame,(int(Width),int(Height))) #マーカーを検出 corners, ids, rejectedImgPoints = aruco.detectMarkers(img, dictionary) if len(corners) > 0: #マーカーid=01検出時(今回マーカーID:01を使用) if ids[0] == 1: x_AR = int(corners[0][0][3][0]) y_AR = int(corners[0][0][3][1]) print(x_AR, y_AR) # 切り取り範囲の指定 x:横, y:縦 # (今回はメータ4桁だったのでnumber1~4) # number 1 x1_l = x_AR + 17 # 左上X y1_l = y_AR - 8 # 左上Y x1_r = x1_l + 30 # 右上X y1_r = y1_l + 35 # 右上X # number 2 x2_l = x1_l + 39 y2_l = y1_l x2_r = x2_l + 30 y2_r = y2_l + 35 # number 3 x3_l = x2_l + 39 y3_l = y2_l x3_r = x3_l + 30 y3_r = y3_l + 35 # number 4 x4_l = x3_l + 39 y4_l = y3_l x4_r = x4_l + 30 y4_r = y4_l + 35 img_list = list(range(4*img_cnt+4)) img_list = img_list[-4:] # Cut val_1 name = "imageFolder/" + str(img_list[0]) + ".jpg" img1 = img[y1_l:y1_r, x1_l:x1_r] cv2.imwrite(name, cv2.resize(img1,(x1_r-x1_l,y1_r-y1_l))) # Cut val_2 name = "imageFolder/" + str(img_list[1]) + ".jpg" img2 = img[y2_l:y2_r, x2_l:x2_r] cv2.imwrite(name, cv2.resize(img2,(x2_r-x2_l,y2_r-y2_l))) # Cut val_3 name = "imageFolder/" + str(img_list[2]) + ".jpg" img3 = img[y3_l:y3_r, x3_l:x3_r] cv2.imwrite(name, cv2.resize(img3,(x3_r-x3_l,y3_r-y3_l))) # Cut val_4 name = "imageFolder/" + str(img_list[3]) + ".jpg" img4 = img[y4_l:y4_r, x4_l:x4_r] cv2.imwrite(name, cv2.resize(img4,(x4_r-x4_l,y4_r-y4_l))) img_cnt += 1 # 今回は1時間に一回撮影(メータがそこそこ回る時間) time.sleep(cycle) cv2.destroyWindow("frame") # 実行 if __name__ == '__main__': meterImageSaver(camera_num=0, cycle=3600) 漂うクソコード臭ですね。リファクタリングは読者にお任せします ふむふむどんな感じか確認してみよう。とその前に、フォルダないにある画像データを datasetsフォルダ内に、正解ラベルをつけて以下のようなフォルダ構造に保存。 (瞬時に画像を識別して正解を割り出す人間は凄いと再確認) エラーデータもたくさん。アナログメータなので、数字と数字の間の瞬間はデータとして分類不可でした。 (これ後々の問題提起になります) # フォルダー構造 datasets ├── 0 ├── 1 . . └── 9 2日ほどプログラムを放置した結果... 取得できた数値データ↓ 数値 0 1 2 3 4 5 6 7 8 9 枚数 35 16 20 30 13 22 37 24 27 12 かなり偏りがあるけど仕方ない。 というかデータ少なすぎ?? 「ないなら増やせばいいじゃない」ということで、省エネ万歳です。 データの水増しをしていきます。 データの水増し データの水増しは色々と方法があります。 こちらが参考になります。 https://qiita.com/bohemian916/items/9630661cd5292240f8c7 コントラスト変更 ガンマ変換 平滑化 ヒストグラム均一化 回転や反転 どれを使用しても良いですが、今回は回転や反転は使用しません。 回転してしまっては6と9が同じものになってしまったりするので。状況に応じて使い分けてください。 expansionImage.py import cv2 import numpy as np import sys import os # ヒストグラム均一化 def equalizeHistRGB(src): RGB  = cv2.split(src) Blue = RGB[0] Green = RGB[1] Red = RGB[2] for i in range(3): cv2.equalizeHist(RGB[i]) img_hist = cv2.merge([RGB[0],RGB[1], RGB[2]]) return img_hist # ガウシアンノイズ def addGaussianNoise(src): row,col,ch= src.shape mean = 0 var = 0.1 sigma = 15 gauss = np.random.normal(mean,sigma,(row,col,ch)) gauss = gauss.reshape(row,col,ch) noisy = src + gauss return noisy # salt&pepperノイズ def addSaltPepperNoise(src): row,col,ch = src.shape s_vs_p = 0.5 amount = 0.004 out = src.copy() # Salt mode num_salt = np.ceil(amount * src.size * s_vs_p) coords = [np.random.randint(0, i-1 , int(num_salt)) for i in src.shape] out[coords[:-1]] = (255,255,255) # Pepper mode num_pepper = np.ceil(amount* src.size * (1. - s_vs_p)) coords = [np.random.randint(0, i-1 , int(num_pepper)) for i in src.shape] out[coords[:-1]] = (0,0,0) return out if __name__ == '__main__': # ルックアップテーブルの生成 min_table = 50 max_table = 205 diff_table = max_table - min_table gamma1 = 0.75 gamma2 = 1.5 LUT_HC = np.arange(256, dtype = 'uint8' ) LUT_LC = np.arange(256, dtype = 'uint8' ) LUT_G1 = np.arange(256, dtype = 'uint8' ) LUT_G2 = np.arange(256, dtype = 'uint8' ) LUTs = [] # 平滑化用 average_square = (10,10) # ハイコントラストLUT作成 for i in range(0, min_table): LUT_HC[i] = 0 for i in range(min_table, max_table): LUT_HC[i] = 255 * (i - min_table) / diff_table for i in range(max_table, 255): LUT_HC[i] = 255 # その他LUT作成 for i in range(256): LUT_LC[i] = min_table + i * (diff_table) / 255 LUT_G1[i] = 255 * pow(float(i) / 255, 1.0 / gamma1) LUT_G2[i] = 255 * pow(float(i) / 255, 1.0 / gamma2) LUTs.append(LUT_HC) LUTs.append(LUT_LC) LUTs.append(LUT_G1) LUTs.append(LUT_G2) # 先ほど作成した画像データファイルごとに8倍に水増ししていく DATADIR = "datasets/" CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] for n, category in enumerate(CATEGORIES): path = os.path.join(DATADIR, category) print(path) img_len = len(os.listdir(path)) # 連番を名付け for i, pic in enumerate(os.listdir(path)): os.rename(DATADIR + CATEGORIES[n] + "/" + pic, DATADIR + CATEGORIES[n] + "/" + str(i)+".jpg") for num, image_name in enumerate(os.listdir(path)): # 画像の読み込み img_src = cv2.imread(os.path.join(path, image_name)) trans_img = [] trans_img.append(img_src) # LUT変換(4種類) for i, LUT in enumerate(LUTs): trans_img.append(cv2.LUT(img_src, LUT)) # 平滑化(1種類) trans_img.append(cv2.blur(img_src, average_square)) # ヒストグラム均一化(1種類) trans_img.append(equalizeHistRGB(img_src)) # ノイズ付加(1種類) trans_img.append(addGaussianNoise(img_src)) trans_img.append(addSaltPepperNoise(img_src)) # 保存(計8種類) for i, img in enumerate(trans_img): cv2.imwrite(path + "/" + str(i+num*8+img_len) + ".jpg" ,img) なんということでしょう。画像データが爆増しました。これで学習できそうですね。 Before 数値 0 1 2 3 4 5 6 7 8 9 枚数 35 16 20 30 13 22 37 24 27 12 After 数値 0 1 2 3 4 5 6 7 8 9 枚数 280 128 160 240 104 176 296 192 216 96 余談ですが... MNISTのような手書きデータの場合はこんな量では全く不足していますが、 決まった形の数値データであればこれくらいの枚数でも十分に学習可能と思います。 2. 学習させる/チューニングする まずは取得してきたデータがどんな風になっているか確認。 ここからはJupyterなどのインターラクティブな開発環境を用いると可視化しながらできるのでおすすめ。 画像を確認 captureMeterImage.py import matplotlib.pyplot as plt import os import cv2 DATADIR = "datasets/" CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8"] # CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] for category in CATEGORIES: path = os.path.join(DATADIR, category) print(path) for image_name in os.listdir(path): try: img_array = cv2.imread(os.path.join(path, image_name), cv2.IMREAD_GRAYSCALE) plt.imshow(img_array, cmap="gray") plt.show() break except Exception as e: pass break print(img_array.shape) print(img_array) お気づきでしょうか。CATEGORIESがコメントアウトされ、9のラベルフォルダが消されていることに。 作者はどうやら9の画像データを無くしてしまったそうだ。信じられない。 従って、0~8までの画像データで学習を進める。(もちろん9は認識不能となる) 気を取り直して画像データを確認する。 0のラベルにある画像ファイルを可視化。この時、カラー情報は不要なので、グレースケール化しているので、 下の方に、画像の白黒明度を示す値を表示している。 問題なさそう。 ラベルを確認 続いて、各ラベルフォルダ内に保存されているデータとラベルが一致するか確認しておく。 もちろん、9は除く confirmLabelCorrection.py import matplotlib.pyplot as plt import os import cv2 import random import numpy as np DATADIR = "datasets/" CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8"] # CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] IMG_SIZE = 28 training_data = [] def create_training_data(): for class_num, category in enumerate(CATEGORIES): path = os.path.join(DATADIR, category) for image_name in os.listdir(path): try: img_array = cv2.imread(os.path.join(path, image_name), cv2.IMREAD_GRAYSCALE) # 画像読み込み img_resize_array = cv2.resize(img_array, (IMG_SIZE, IMG_SIZE)) # 画像のリサイズ training_data.append([img_resize_array, class_num]) # 画像データ、ラベル情報を追加 except Exception as e: pass create_training_data() random.shuffle(training_data) # データをシャッフル X_train = [] # 画像データ y_train = [] # ラベル情報 # データセット作成 for feature, label in training_data: X_train.append(feature) y_train.append(label) # numpy配列に変換 X_train = np.array(X_train) y_train = np.array(y_train) # データセットの確認(全データをシャッフルした最初の4枚) for i in range(0, 4): print("学習データのラベル:", y_train[i]) plt.subplot(2, 2, i+1) plt.axis('off') plt.imshow(X_train[i], cmap='gray') print(X_train.shape) 表示情報の意味 ※ 学習データのラベル:フォルダの名前 ※ (枚数、縦ピクセル、横ピクセル) ※ 読み込んだ画像 こちらも問題なさそう。 学習モデルの構築/チューニング ここからが本番です。 今回は学習モデルにCNNを使用します。画像認識といえばCNNという単純な思考です。 今はもっと良いモデルもあるかもしれませんね。 自分はこちらの書籍でディープラーニングモデルについて勉強しました。とても理解しやすい。 https://www.amazon.co.jp/Python%E3%81%A8Keras%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-Francois-Chollet/dp/4839964262 generateCNNModel.py from keras.models import Sequential from keras.layers.core import Dense, Activation, Flatten from keras.layers import Conv2D, Reshape, MaxPooling2D, Dropout from keras.utils import np_utils import numpy as np # モデル構築 # ============================================================= model = Sequential() model.add(Reshape((28,28,1), input_shape=(28,28))) model.add(Conv2D(32,(3,3))) model.add(Activation("relu")) model.add(MaxPooling2D((2, 2))) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D((2, 2))) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dropout(0.5)) # model.add(Dense(10, activation='softmax')) model.add(Dense(9, activation='softmax')) # ============================================================= X_train = np.array(X_train)/255 # 0-255の白黒のピクセル値を最大1に正規化する y_train = np_utils.to_categorical(y_train) # [0,0,1,0,0,0,0,0,0,0]=>3の場合の変換例 #コンパイル model.compile(loss="categorical_crossentropy", optimizer="sgd", metrics=["accuracy"]) #学習実行 history = model.fit(X_train, y_train, batch_size=1, verbose=1, epochs=5, validation_split=0.1) #モデルを保存 json_string = model.model.to_json() open('recog_val.json', 'w').write(json_string) #重みの保存 hdf5_file = "recog_val.hdf5" model.model.save_weights(hdf5_file) 結果はこちら。97.49%の正解率となった。まずまずだ。 しかし、この結果は幾度とモデルを見直した結果による。 勘のいい読者は下記の部分に気づかれたと思う。 0〜9までならこれでも良かったが、9を無くしてしまったからである。反省しろ。 # model.add(Dense(10, activation='softmax')) 可視化 上記の結果を一応可視化しておこう。見えなかった部分が見えてくるかもしれない。 visibilityLearningResult.py #学習結果を表示 import matplotlib.pyplot as plt acc = model.history.history['accuracy'] val_acc = model.history.history['val_accuracy'] loss = model.history.history['loss'] val_loss = model.history.history['val_loss'] epochs = range(len(acc)) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.figure() 上が正解率、下が損失を示している。 モデルはうまく構築され、評価された。これで行こう。 少し過学習によって正解率が落ち込んでいるが、安定して高い数値を出すことができた モデルの為、採用した。本来はここからデータを追加したり、水増し方法を詰めたりと、でき ることは山ほどあるが、今は結果を急ぐ。 3. テストする ようやくここまで来た。最後に、テスト画像を用意して学習モデルを適用してみる。 この時、テスト画像は学習用に用いたデータを用いることは厳禁。カンニングになります。 では、テスト画像を3枚新たに撮影してきて、予測してみます。 test.py from keras.datasets import mnist from keras.layers import Dense, Dropout, Flatten, Activation from keras.layers import Conv2D, MaxPooling2D from keras.models import Sequential, load_model, model_from_json from keras.utils.np_utils import to_categorical from keras.utils.vis_utils import plot_model import numpy as np import cv2 import matplotlib.pyplot as plt # モデルの読み込み model = model_from_json(open('recog_val.json').read()) model.load_weights('recog_val.hdf5') # テスト画像を準備 name1 = "val_1.jpg" name2 = "val_2.jpg" name3 = "val_3.jpg" name = [name1, name2, name3] for i in name: img = cv2.imread(i) # Grayed img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 28*28 resize img = cv2.resize(img,(28, 28), cv2.INTER_CUBIC) plt.imshow(img) plt.show() Xt = [] Xt.append(img) Xt = np.array(Xt)/255 # 正規化 # 予測 result = model.predict_classes(Xt) print("-----------------------------") print(result) print("-----------------------------") なんとか予測できております。長い戦いでしたが、なんとかここまでできました。 これで完璧です。(9の認識は除く) あとは省略しますが、上記のプログラムを一部修正し、画像を一定時間ごとに取得するコードを 追加すれば自動での画像取得、画像認識ソフトの完成です。 課題 とはなりません。なぜならば、前述したようにアナログメータには数値と数値の狭間という概念が存在します。 つまり、数字のドラムが回転している間に画像認識すると、とんでもない数字が認識される可能性があります。 この課題を乗り越える方法があれば教えてください。 4. 終わりに 最後までお付き合いありがとうございます。 恐らく、市販でこのようなアプリケーションを取り扱っている会社は多々あると思いますが、各社ごとに 学習方法やデータセットを強みとしているのだと思います。 今回のプログラムは、アナログメータのデジタル化の一例にすぎず、より良いものはゴマンとあります。 画像認識を始めたばかりという方の参考になればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アナログメータの画像認識

0. はじめに。 ある昼下がり、ふと思い立った。アナログメータを画像認識で取得しようと。 今回は同じような気持ちになったエンジニアに向けて、こんな方法もあるよ ということを提案したい。 ※尚、とりあえず動けばいいやコードなので、その点ご容赦ください 目的のメータの感じ。電気のメータなんかはこれが多いのではないか。 アナログメータといえば、検針がある丸型を想像するかもしれないが、 そちらに関しての記事はいくつか見つけたので、こちらを対象にした。 リアルなやつ ちなみに結論を先に示すと、認識の結果はこんな感じになる。 (数字が違うのはテストはランダムな画像を抽出しているので) なんとなく成功している気もする... では始めていくが、そもそも何からやろうか 画像を集める 学習させる/チューニングする テストする 大きく分けるとこんな感じか?とりあえずやってみよう! 1. 学習用画像データの準備 始めようとした時ふと思った。「画像集めるのめんどくさくね?」 データセットがネットに落ちてないか調べてみた。 . . . ないですね。 「なら、MNISTのデータセット使ってやればできんじゃね?」 [MNIST]:https://ja.wikipedia.org/wiki/MNIST%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9 すまん無理だったわ。手書きとアナログ数字では結構違う特徴があるみたいで、 正解率が70%くらいだった。 覚悟を決めて、画像を収集することに イメージはこんな感じで、ウェブカメラ(¥1000くらい)でひたすら画像を撮影し、データを蓄積していく。 (我ながらひどいイメージですみません) 画像を集める 今回は長時間放置して撮影したいので、ARマーカーを使って、ちょっとカメラがズレても良い仕様にしようと思う。 もっといい方法はあると思うけど。 また、切り取り位置は調整してください。 メータ任せでデータをひたすら取得するので、止めるタイミングは自分自身で決める。 この時、取れないデータやデータ量の偏りが発生するので、意識してデータを取得する。 captureMeterImage.py # 必要なライブラリ import cv2 import time # ARマーカーの設定 aruco = cv2.aruco dictionary = aruco.getPredefinedDictionary(aruco.DICT_4X4_50) def meterImageSaver(camera_num, cycle): # ウェブカメラ検出、設定 cap = cv2.VideoCapture(camera_num) img_cnt = 0 # 自分が納得するまでデータを撮り続ける。 while True: ret, frame = cap.read() cv2.imshow("frame", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break Height, Width = frame.shape[:2] img = cv2.resize(frame,(int(Width),int(Height))) #マーカーを検出 corners, ids, rejectedImgPoints = aruco.detectMarkers(img, dictionary) if len(corners) > 0: #マーカーid=01検出時(今回マーカーID:01を使用) if ids[0] == 1: x_AR = int(corners[0][0][3][0]) y_AR = int(corners[0][0][3][1]) print(x_AR, y_AR) # 切り取り範囲の指定 x:横, y:縦 # (今回はメータ4桁だったのでnumber1~4) # number 1 x1_l = x_AR + 17 # 左上X y1_l = y_AR - 8 # 左上Y x1_r = x1_l + 30 # 右上X y1_r = y1_l + 35 # 右上X # number 2 x2_l = x1_l + 39 y2_l = y1_l x2_r = x2_l + 30 y2_r = y2_l + 35 # number 3 x3_l = x2_l + 39 y3_l = y2_l x3_r = x3_l + 30 y3_r = y3_l + 35 # number 4 x4_l = x3_l + 39 y4_l = y3_l x4_r = x4_l + 30 y4_r = y4_l + 35 img_list = list(range(4*img_cnt+4)) img_list = img_list[-4:] # Cut val_1 name = "imageFolder/" + str(img_list[0]) + ".jpg" img1 = img[y1_l:y1_r, x1_l:x1_r] cv2.imwrite(name, cv2.resize(img1,(x1_r-x1_l,y1_r-y1_l))) # Cut val_2 name = "imageFolder/" + str(img_list[1]) + ".jpg" img2 = img[y2_l:y2_r, x2_l:x2_r] cv2.imwrite(name, cv2.resize(img2,(x2_r-x2_l,y2_r-y2_l))) # Cut val_3 name = "imageFolder/" + str(img_list[2]) + ".jpg" img3 = img[y3_l:y3_r, x3_l:x3_r] cv2.imwrite(name, cv2.resize(img3,(x3_r-x3_l,y3_r-y3_l))) # Cut val_4 name = "imageFolder/" + str(img_list[3]) + ".jpg" img4 = img[y4_l:y4_r, x4_l:x4_r] cv2.imwrite(name, cv2.resize(img4,(x4_r-x4_l,y4_r-y4_l))) img_cnt += 1 # 今回は1時間に一回撮影(メータがそこそこ回る時間) time.sleep(cycle) cv2.destroyWindow("frame") # 実行 if __name__ == '__main__': meterImageSaver(camera_num=0, cycle=3600) 漂うクソコード臭ですね。リファクタリングは読者にお任せします ふむふむどんな感じか確認してみよう。とその前に、フォルダないにある画像データを datasetsフォルダ内に、正解ラベルをつけて以下のようなフォルダ構造に保存。 (瞬時に画像を識別して正解を割り出す人間は凄いと再確認) エラーデータもたくさん。アナログメータなので、数字と数字の間の瞬間はデータとして分類不可でした。 (これ後々の問題提起になります) # フォルダー構造 datasets ├── 0 ├── 1 . . └── 9 2日ほどプログラムを放置した結果... 取得できた数値データ↓ 数値 0 1 2 3 4 5 6 7 8 9 枚数 35 16 20 30 13 22 37 24 27 12 かなり偏りがあるけど仕方ない。 というかデータ少なすぎ?? 「ないなら増やせばいいじゃない」ということで、省エネ万歳です。 データの水増しをしていきます。 データの水増し データの水増しは色々と方法があります。 こちらが参考になります。 https://qiita.com/bohemian916/items/9630661cd5292240f8c7 コントラスト変更 ガンマ変換 平滑化 ヒストグラム均一化 回転や反転 どれを使用しても良いですが、今回は回転や反転は使用しません。 回転してしまっては6と9が同じものになってしまったりするので。状況に応じて使い分けてください。 expansionImage.py import cv2 import numpy as np import sys import os # ヒストグラム均一化 def equalizeHistRGB(src): RGB  = cv2.split(src) Blue = RGB[0] Green = RGB[1] Red = RGB[2] for i in range(3): cv2.equalizeHist(RGB[i]) img_hist = cv2.merge([RGB[0],RGB[1], RGB[2]]) return img_hist # ガウシアンノイズ def addGaussianNoise(src): row,col,ch= src.shape mean = 0 var = 0.1 sigma = 15 gauss = np.random.normal(mean,sigma,(row,col,ch)) gauss = gauss.reshape(row,col,ch) noisy = src + gauss return noisy # salt&pepperノイズ def addSaltPepperNoise(src): row,col,ch = src.shape s_vs_p = 0.5 amount = 0.004 out = src.copy() # Salt mode num_salt = np.ceil(amount * src.size * s_vs_p) coords = [np.random.randint(0, i-1 , int(num_salt)) for i in src.shape] out[coords[:-1]] = (255,255,255) # Pepper mode num_pepper = np.ceil(amount* src.size * (1. - s_vs_p)) coords = [np.random.randint(0, i-1 , int(num_pepper)) for i in src.shape] out[coords[:-1]] = (0,0,0) return out if __name__ == '__main__': # ルックアップテーブルの生成 min_table = 50 max_table = 205 diff_table = max_table - min_table gamma1 = 0.75 gamma2 = 1.5 LUT_HC = np.arange(256, dtype = 'uint8' ) LUT_LC = np.arange(256, dtype = 'uint8' ) LUT_G1 = np.arange(256, dtype = 'uint8' ) LUT_G2 = np.arange(256, dtype = 'uint8' ) LUTs = [] # 平滑化用 average_square = (10,10) # ハイコントラストLUT作成 for i in range(0, min_table): LUT_HC[i] = 0 for i in range(min_table, max_table): LUT_HC[i] = 255 * (i - min_table) / diff_table for i in range(max_table, 255): LUT_HC[i] = 255 # その他LUT作成 for i in range(256): LUT_LC[i] = min_table + i * (diff_table) / 255 LUT_G1[i] = 255 * pow(float(i) / 255, 1.0 / gamma1) LUT_G2[i] = 255 * pow(float(i) / 255, 1.0 / gamma2) LUTs.append(LUT_HC) LUTs.append(LUT_LC) LUTs.append(LUT_G1) LUTs.append(LUT_G2) # 先ほど作成した画像データファイルごとに8倍に水増ししていく DATADIR = "datasets/" CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] for n, category in enumerate(CATEGORIES): path = os.path.join(DATADIR, category) print(path) img_len = len(os.listdir(path)) # 連番を名付け for i, pic in enumerate(os.listdir(path)): os.rename(DATADIR + CATEGORIES[n] + "/" + pic, DATADIR + CATEGORIES[n] + "/" + str(i)+".jpg") for num, image_name in enumerate(os.listdir(path)): # 画像の読み込み img_src = cv2.imread(os.path.join(path, image_name)) trans_img = [] trans_img.append(img_src) # LUT変換(4種類) for i, LUT in enumerate(LUTs): trans_img.append(cv2.LUT(img_src, LUT)) # 平滑化(1種類) trans_img.append(cv2.blur(img_src, average_square)) # ヒストグラム均一化(1種類) trans_img.append(equalizeHistRGB(img_src)) # ノイズ付加(1種類) trans_img.append(addGaussianNoise(img_src)) trans_img.append(addSaltPepperNoise(img_src)) # 保存(計8種類) for i, img in enumerate(trans_img): cv2.imwrite(path + "/" + str(i+num*8+img_len) + ".jpg" ,img) なんということでしょう。画像データが爆増しました。これで学習できそうですね。 Before 数値 0 1 2 3 4 5 6 7 8 9 枚数 35 16 20 30 13 22 37 24 27 12 After 数値 0 1 2 3 4 5 6 7 8 9 枚数 280 128 160 240 104 176 296 192 216 96 余談ですが... MNISTのような手書きデータの場合はこんな量では全く不足していますが、 決まった形の数値データであればこれくらいの枚数でも十分に学習可能と思います。 2. 学習させる/チューニングする まずは取得してきたデータがどんな風になっているか確認。 ここからはJupyterなどのインターラクティブな開発環境を用いると可視化しながらできるのでおすすめ。 画像を確認 captureMeterImage.py import matplotlib.pyplot as plt import os import cv2 DATADIR = "datasets/" CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8"] # CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] for category in CATEGORIES: path = os.path.join(DATADIR, category) print(path) for image_name in os.listdir(path): try: img_array = cv2.imread(os.path.join(path, image_name), cv2.IMREAD_GRAYSCALE) plt.imshow(img_array, cmap="gray") plt.show() break except Exception as e: pass break print(img_array.shape) print(img_array) お気づきでしょうか。CATEGORIESがコメントアウトされ、9のラベルフォルダが消されていることに。 作者はどうやら9の画像データを無くしてしまったそうだ。信じられない。 従って、0~8までの画像データで学習を進める。(もちろん9は認識不能となる) 気を取り直して画像データを確認する。 0のラベルにある画像ファイルを可視化。この時、カラー情報は不要なので、グレースケール化しているので、 下の方に、画像の白黒明度を示す値を表示している。 問題なさそう。 ラベルを確認 続いて、各ラベルフォルダ内に保存されているデータとラベルが一致するか確認しておく。 もちろん、9は除く confirmLabelCorrection.py import matplotlib.pyplot as plt import os import cv2 import random import numpy as np DATADIR = "datasets/" CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8"] # CATEGORIES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] IMG_SIZE = 28 training_data = [] def create_training_data(): for class_num, category in enumerate(CATEGORIES): path = os.path.join(DATADIR, category) for image_name in os.listdir(path): try: img_array = cv2.imread(os.path.join(path, image_name), cv2.IMREAD_GRAYSCALE) # 画像読み込み img_resize_array = cv2.resize(img_array, (IMG_SIZE, IMG_SIZE)) # 画像のリサイズ training_data.append([img_resize_array, class_num]) # 画像データ、ラベル情報を追加 except Exception as e: pass create_training_data() random.shuffle(training_data) # データをシャッフル X_train = [] # 画像データ y_train = [] # ラベル情報 # データセット作成 for feature, label in training_data: X_train.append(feature) y_train.append(label) # numpy配列に変換 X_train = np.array(X_train) y_train = np.array(y_train) # データセットの確認(全データをシャッフルした最初の4枚) for i in range(0, 4): print("学習データのラベル:", y_train[i]) plt.subplot(2, 2, i+1) plt.axis('off') plt.imshow(X_train[i], cmap='gray') print(X_train.shape) 表示情報の意味 ※ 学習データのラベル:フォルダの名前 ※ (枚数、縦ピクセル、横ピクセル) ※ 読み込んだ画像 こちらも問題なさそう。 学習モデルの構築/チューニング ここからが本番です。 今回は学習モデルにCNNを使用します。画像認識といえばCNNという単純な思考です。 今はもっと良いモデルもあるかもしれませんね。 自分はこちらの書籍でディープラーニングモデルについて勉強しました。とても理解しやすい。 https://www.amazon.co.jp/Python%E3%81%A8Keras%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-Francois-Chollet/dp/4839964262 generateCNNModel.py from keras.models import Sequential from keras.layers.core import Dense, Activation, Flatten from keras.layers import Conv2D, Reshape, MaxPooling2D, Dropout from keras.utils import np_utils import numpy as np # モデル構築 # ============================================================= model = Sequential() model.add(Reshape((28,28,1), input_shape=(28,28))) model.add(Conv2D(32,(3,3))) model.add(Activation("relu")) model.add(MaxPooling2D((2, 2))) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D((2, 2))) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dropout(0.5)) # model.add(Dense(10, activation='softmax')) model.add(Dense(9, activation='softmax')) # ============================================================= X_train = np.array(X_train)/255 # 0-255の白黒のピクセル値を最大1に正規化する y_train = np_utils.to_categorical(y_train) # [0,0,1,0,0,0,0,0,0,0]=>3の場合の変換例 #コンパイル model.compile(loss="categorical_crossentropy", optimizer="sgd", metrics=["accuracy"]) #学習実行 history = model.fit(X_train, y_train, batch_size=1, verbose=1, epochs=5, validation_split=0.1) #モデルを保存 json_string = model.model.to_json() open('recog_val.json', 'w').write(json_string) #重みの保存 hdf5_file = "recog_val.hdf5" model.model.save_weights(hdf5_file) 結果はこちら。97.49%の正解率となった。まずまずだ。 しかし、この結果は幾度とモデルを見直した結果による。 勘のいい読者は下記の部分に気づかれたと思う。 0〜9までならこれでも良かったが、9を無くしてしまったからである。反省しろ。 # model.add(Dense(10, activation='softmax')) 可視化 上記の結果を一応可視化しておこう。見えなかった部分が見えてくるかもしれない。 visibilityLearningResult.py #学習結果を表示 import matplotlib.pyplot as plt acc = model.history.history['accuracy'] val_acc = model.history.history['val_accuracy'] loss = model.history.history['loss'] val_loss = model.history.history['val_loss'] epochs = range(len(acc)) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.figure() 上が正解率、下が損失を示している。 モデルはうまく構築され、評価された。これで行こう。 少し過学習によって正解率が落ち込んでいるが、安定して高い数値を出すことができた モデルの為、採用した。本来はここからデータを追加したり、水増し方法を詰めたりと、でき ることは山ほどあるが、今は結果を急ぐ。 3. テストする ようやくここまで来た。最後に、テスト画像を用意して学習モデルを適用してみる。 この時、テスト画像は学習用に用いたデータを用いることは厳禁。カンニングになります。 では、テスト画像を3枚新たに撮影してきて、予測してみます。 test.py from keras.datasets import mnist from keras.layers import Dense, Dropout, Flatten, Activation from keras.layers import Conv2D, MaxPooling2D from keras.models import Sequential, load_model, model_from_json from keras.utils.np_utils import to_categorical from keras.utils.vis_utils import plot_model import numpy as np import cv2 import matplotlib.pyplot as plt # モデルの読み込み model = model_from_json(open('recog_val.json').read()) model.load_weights('recog_val.hdf5') # テスト画像を準備 name1 = "val_1.jpg" name2 = "val_2.jpg" name3 = "val_3.jpg" name = [name1, name2, name3] for i in name: img = cv2.imread(i) # Grayed img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 28*28 resize img = cv2.resize(img,(28, 28), cv2.INTER_CUBIC) plt.imshow(img) plt.show() Xt = [] Xt.append(img) Xt = np.array(Xt)/255 # 正規化 # 予測 result = model.predict_classes(Xt) print("-----------------------------") print(result) print("-----------------------------") なんとか予測できております。長い戦いでしたが、なんとかここまでできました。 これで完璧です。(9の認識は除く) あとは省略しますが、上記のプログラムを一部修正し、画像を一定時間ごとに取得するコードを 追加すれば自動での画像取得、画像認識ソフトの完成です。 課題 お気づきの方もおられるとは思いますが、これで完璧とはなりません。なぜならば、前述したようにアナログメータには数値と数値の狭間という概念が存在します。 つまり、数字のドラムが回転している間に画像認識すると、とんでもない数字が認識される可能性があります。 4. 終わりに 最後までお付き合いありがとうございます。 恐らく、市販でこのようなアプリケーションを取り扱っている会社は多々あると思いますが、各社ごとに 学習方法やデータセットを強みとしているのだと思います。 今回のプログラムは、アナログメータのデジタル化の一例にすぎず、より良いものはゴマンとあります。 特に今時はクラウドサービスの一部に学習モデルの最適化などもやってくれるのもあったりなかったり。 画像認識を始めたばかりという方の参考になればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonによる『三国志大戦』の実装(1)

Pythonを使って『三国志大戦』の実装 目次 作成の経緯 作成手順 次回に向けて 作成したコード全体 1. 作成の経緯 ゲームの制作をしてみたいと思い、好きでよくやっていた「三国志大戦」に目をつけて、自分で作ってみようと思いました。ゲームであればC++やC#がメジャーだとは思いますが、自由に扱える訳ではない(力量が足りない)ため、自信が得意としているPythonで実装することにしました。 2. 作成手順 1. キャラクター設定 今回使用するキャラクターは三国志でも有名な3名を使います。 ・曹操 → 長安 ・劉備 → 成都 ・孫堅 → 建業 ※都は適当につけています。 今回の注意点として、 1. 武将による能力の差はない。 2. 能力の代わりに兵量によって分ける。 ということです。(今後、改良予定です。) こちらの武将・都・人数を配列に格納します。 sangoku.py army = ['曹操', '孫堅', '劉備'] #武将(キャラクター) countries = ['長安', '建業' ,'成都'] #都 army_num = [10, 5, 3] #人数 2. 画面(地図の描画) 続けて地図を描画します。今回は何度も地図を呼び出すため関数にしています。 sangoku.py def Drawmap(): os.system('clear') print(str(year) + "年") print() print(" 長安") print(" " + army[0] + " " + str(army_num[0])) print() print(" 建業") print(" " + army[1] + " " + str(army_num[1])) print() print(" 成都") print(" " + army[2] + " " + str(army_num[2])) print() 実際に実行するとこのような画面になります。 220年 長安 曹操 10 建業 孫堅 5 成都 劉備 3 簡易的ではありますが、地図を表示することができました。 領地が略奪されたり、武将が倒されると初期化した武将のリストが書き換えられます。 3. 攻撃する場所・攻撃人数の入力 続けてどこに攻撃するか、何人で攻撃するのかを入力します。 sangoku.py print(countries[i] + "の" + army[i] + "様 どちらへ攻撃しますか?" + " (" + str(i+1) + "/" + str(len(army)) + ")") for j in range(len(army)): if not j == i: print(str(j) + " : " + countries[j] + " " + army[j]) print("3 : 温存") attack_counrty = int(input("攻撃する場所を入力 => ")) こちらでは、長安に攻撃→0、建業に攻撃→1、成都に攻撃→2、兵力を温存→3、を入力することができます。 攻撃人数はこのように指定します。 sangoku.py if (attack_counrty == 0) or (attack_counrty == 1) or (attack_counrty == 2): at_num = int(input("何千人で攻撃しますか(1~9) => ")) print() print(countries[i] + "の" + army[i] + "様が" + str(at_num) + "000人で" + countries[attack_counrty] + "の" + army[attack_counrty] + "様に攻撃!!") input() attack(i, at_num, attack_counrty) else: print() print("今年は兵力を温存するでござる") input() 4. 攻撃する 実際に攻撃をします。 攻撃は1000人ずつ減るように指定しました。初期化で、 sangoku.py attack_num = [0, 1] を指定しました。randomのchoiceを使って、どちらかを選択して人数が減るようになっています。 攻撃全体のコードはこちらです。 sangoku.py def attack(own, own_num, enemy): os.system('clear') print(countries[own] + " " + countries[enemy]) print(army[own] + str(own_num) + "000人" + " 対 " + army[enemy] + str(army_num[enemy]) + "000人") print() number = own_num enemy_num = army_num[enemy] while(True): offence = rd.choice(attack_num) deffence = rd.choice(attack_num) if offence > deffence: enemy_num -= offence if enemy_num <= 0: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " 0人") break else: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") elif offence < deffence: own_num -= deffence if own_num <= 0: print(army[own] + " 0人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") break else: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") else: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") input() if own_num > enemy_num: print() print(army[own] + "様 勝利!!") army[enemy] = army[own] army_num[enemy] = own_num army_num[own] -= number else: print() print(army[enemy] + "様 勝利!!") army_num[own] -= number army_num[enemy] = enemy_num input() 3. 次回に向けて 次回に向けて以下のことを改良していきます。 ・ 武将を増やす ・ 自分で操作する武将と自動で動く武将を追加する ・ バグを減らす ・ ゲームクリア、ゲームオーバーの実装 ※2021.4.10現在 完成しました。 4. 作成したコード sangoku.py #---------------------------------# # 全て自分で操作する三国志 # #---------------------------------# import os import random as rd army = ['曹操', '孫堅', '劉備'] countries = ['長安', '建業' ,'成都'] army_num = [10, 5, 3] attack_num = [0, 1] year = 220 # 画面マップを描画する関数 def Drawmap(): os.system('clear') print(str(year) + "年") print() print(" 長安") print(" " + army[0] + " " + str(army_num[0])) print() print(" 建業") print(" " + army[1] + " " + str(army_num[1])) print() print(" 成都") print(" " + army[2] + " " + str(army_num[2])) print() # 対戦をする関数 def attack(own, own_num, enemy): os.system('clear') print(countries[own] + " " + countries[enemy]) print(army[own] + str(own_num) + "000人" + " 対 " + army[enemy] + str(army_num[enemy]) + "000人") print() number = own_num enemy_num = army_num[enemy] while(True): offence = rd.choice(attack_num) deffence = rd.choice(attack_num) if offence > deffence: enemy_num -= offence if enemy_num <= 0: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " 0人") break else: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") elif offence < deffence: own_num -= deffence if own_num <= 0: print(army[own] + " 0人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") break else: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") else: print(army[own] + " " + str(own_num*1000) + "人 X " + army[enemy] + " " + str(enemy_num*1000) + "人") input() if own_num > enemy_num: print() print(army[own] + "様 勝利!!") army[enemy] = army[own] army_num[enemy] = own_num army_num[own] -= number else: print() print(army[enemy] + "様 勝利!!") army_num[own] -= number army_num[enemy] = enemy_num input() # メイン関数 while(True): for i in range(len(army)): Drawmap() print(countries[i] + "の" + army[i] + "様 どちらへ攻撃しますか?" + " (" + str(i+1) + "/" + str(len(army)) + ")") for j in range(len(army)): if not j == i: print(str(j) + " : " + countries[j] + " " + army[j]) print("3 : 温存") attack_counrty = int(input("攻撃する場所を入力 => ")) if (attack_counrty == 0) or (attack_counrty == 1) or (attack_counrty == 2): at_num = int(input("何千人で攻撃しますか(1~9) => ")) print() print(countries[i] + "の" + army[i] + "様が" + str(at_num) + "000人で" + countries[attack_counrty] + "の" + army[attack_counrty] + "様に攻撃!!") input() attack(i, at_num, attack_counrty) else: print() print("今年は兵力を温存するでござる") input() os.system('clear') print("兵力が回復しました。") for n in range(len(army)): army_num[n] += 1 year += 1 input()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PEP 647 (User-Defined Type Guards) を読んだよメモ

今朝、PEP 647 (User-Defined Type Guards) が Accepted になったというPRを見かけました。 そこで、今回は PEP 647 を読んでみようと思います。 概要 型チェッカーツールでは type narrowing と呼ばれる手法を使って、プログラム内で型情報をより正確に決定しています。 以下の例では if文と is None を利用して、自動的に if 文の中の型が絞り込まれます。 def func(val: Optional[str]): # "is None" type guard if val is not None: # Type of val is narrowed to str ... else: # Type of val is narrowed to None ... 他にも isinstance() など、いくつかの判定で type narrowing が行われています。 しかし、以下のようにユーザー関数を利用して判定している場合には、type narrowing は意図通りには働きません。 def is_str_list(val: List[object]) -> bool: """Determines whether all objects in the list are strings""" return all(isinstance(x, str) for x in val) def func1(val: List[object]): if is_str_list(val): print(" ".join(val)) # Error: invalid type そこで、新しい型情報 typing.TypeGuard を通じて ユーザー定義 型ガード (user-defined type guard) を定義できるようにします ユーザー定義 型ガードを用いることで、type narrowing のサポートを受けやすくなります ユーザー定義 型ガードは pyright にはすでに実装済みで、 typing.TypeGuard による定義は 3.10 から利用可能です アプローチ is_str_list() のような bool を返す関数の返り値の型として typing.TypeGuard を指定します。 from typing import TypeGuard def is_str_list(val: List[object]) -> TypeGuard[List[str]]: """Determines whether all objects in the list are strings""" return all(isinstance(x, str) for x in val) 型チェッカーは、この関数が True を返した場合に、先頭の引数を TypeGuard に指定した型に合致するとみなします。上記の例では、 is_str_list() をパスしたデータは List[str] として扱われます。 なお、この関数が False を返した場合は type narrowing は行われません。 以下の例では、 if is_two_element_tuple(...) のブロックでは type narrowing が行われた結果、型が Tuple[str, str] に絞り込まれるのに対して、else ブロックでは型は変化しません。 def is_two_element_tuple(val: Tuple[str, ...]) -> TypeGuard[Tuple[str, str]]: return len(val) == 2 OneOrTwoStrs = Union[Tuple[str], Tuple[str, str]] def func(val: OneOrTwoStrs): if is_two_element_tuple(val): reveal_type(val) # Tuple[str, str] ... else: reveal_type(val) # OneOrTwoStrs Union を使っているので、 Tuple[str] に絞り込まれるような印象を持ちますが、ユーザー定義 型ガードではそのようには動かないので注意が必要です。 感想 条件文で型が絞り込まれていることは知っていたけど、type narrowing と呼ばれているのは知らなかった 致し方なく type: ignore している箇所が減らせるのは気持ちよさそう 細かすぎる機能な気がするものの、かゆいところに手が届く感じがある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分のサイトに入力された情報をLINEに通知する方法

自分のサイトに入力フォームを設置し,ユーザーによって入力された値を使ってサーバーで何かしらの計算を行えるようにしたとき,ユーザーが使ったタイミングで通知が欲しい時があります. 今回は,WordPressで作られたサイトに入力された値を使ってPythonで計算したときにLINEに通知を入れる方法を書きます. 環境 サイト:WordPress サーバー:mixhost サーバーから通知:Python,LINE Notifyを使用 システム構成のイメージはこんな感じです. WordPressに入力フォームを設置して計算ツールを作る WordPress上に入力フォームを設置→サーバーでPython計算→サイトに計算結果を表示までの流れは,以下の記事を参考にしました. ブログ(WordPress)のフォームの入力値を使用してPythonで計算しブログにグラフを表示 同じ環境なら,同じようにできると思います. 計算が行われたら通知をLINEに送る 基本的には,LINE NotifyというサービスをPythonで使って通知を送るだけです. トークンを取得(LINE Notify) 「LINE Notify Python」などで検索すればたくさんやり方が出てきます. 例:https://qiita.com/ken_yoshi/items/7879b3117d298a143101 (というか,この例のリンク記事だけで完結できるかもしれません) 通知を送るソースコードをPythonに書く 以下のコードをサーバーに用意すればOKです. line_notify_bot.py (実行するPythonファイルと同じディレクトリに置く) import requests class LINENotifyBot: API_URL = 'https://notify-api.line.me/api/notify' def __init__(self, access_token): self.__headers = {'Authorization': 'Bearer ' + access_token} def send( self, message, image=None, sticker_package_id=None, sticker_id=None, ): payload = { 'message': message, 'stickerPackageId': sticker_package_id, 'stickerId': sticker_id, } files = {} if image != None: files = {'imageFile': open(image, 'rb')} r = requests.post( LINENotifyBot.API_URL, headers=self.__headers, data=payload, files=files, ) ※requestsはpipでインストールしておいてください. 実行するPythonファイル #何かしらの計算がここに書かれている #以下,LINE Notifyによる通知 from line_notify_bot import LINENotifyBot bot = LINENotifyBot(access_token='取得したトークン') bot.send( message='サイト上でツールが使用されました', #image='test.png', # png or jpg sticker_package_id=11537, sticker_id=52002759, ) 「取得したトークン」のところに先ほど取得したトークンをコピペしてください. これで,サイト上で入力された値を使用してに計算が行われたとき,LINEに「サイト上でツールが使用されました」という通知がきます. ちなみに,sticker_package_id,sticker_idのところは通知のときにスタンプを送ります.どのスタンプにするかは以下を参照. https://developers.line.biz/en/docs/messaging-api/sticker-list/ messageに値を代入すればサイトに入力された値や計算結果をLINEに送ることができます. また,画像を送ったりもできるようです.色々工夫すると面白いと思います. 結果 自分のサイト上のフォームに値を入力すると,こんな感じで通知が来ました. ちなみにですが,私が実行したサイト上のツールは以下です.米国株ポートフォリオを分析するために作りました.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】【nltk】バーティカルバーを用いたFreqDistのマージは、組み込み演算子とは挙動が異なる件について

前置き Natural Language Processing with Pythonの1章を見ていると、以下のような機能説明が。 # Functions Defined for NLTK's Frequency Distributions fdist1 |= fdist2 # update fdist1 with counts from fdist2 fdist1とfdist2はFreqDistクラスのインスタンスです。FreqDistクラスは単語の頻度分布、つまりFrequency Distributionを取り扱うための色々便利な機能が揃っています。 一方でこの記号"|="、これはPython3.9で登場したマージ演算子による累積代入と同じ書き方です。この書き方だと、存在しないキーについては単純な追加。キーが重複しているときは、fdist1側の要素をfdist2側の要素が一律に上書きするようになっています。(参考: Pythonで辞書に要素を追加、辞書同士を連結(結合) sample_dict1 = {"k1": 1000, "k2": 2000, "k5": 5} sample_dict2 = {"k2": 200, "k3": 300, "k4": 400} sample_dict1 |= sample_dict2 print(sample_dict1) 出力 {'k1': 1000, 'k2': 200, 'k5': 5, 'k3': 300, 'k4': 400} 私はこの記号を見て、「あぁFreqDistもマージ演算の累積代入が可能なんだな」と確かめもせずに思っていましたが、実際は全く同じというわけではありませんでした。 FreqDistの挙動 結論から言いますと、「キーが存在しない場合は単純な追加」という点は同じですが、キーが重複していた場合は必ずしも上書きされるわけではなく、「要素数が多い方が優先される」というのが正しい理解になります。 ここではサンプルプログラムとして、上の書籍のChapter1: Language Processing and PythonのSection3.4: Counting Other Thingsで使われていた単語の長さを確認するプログラムを少しいじって利用します。サンプルの文字列を空白ごとに分割し、文字数をカウントしてから辞書に割り当てる、といった流れですね。 from nltk.tokenize import word_tokenize from nltk.probability import FreqDist sample_text1 = "a a a aa aaa aaaa" fdist1 = FreqDist() for word in word_tokenize(sample_text1): fdist1["length" + str(len(word.lower()))] += 1 print(fdist1.tabulate()) sample_text2 = "a aa aa aaaa aaaa aaaaa" fdist2 = FreqDist() for word in word_tokenize(sample_text2): fdist2["length" + str(len(word.lower()))] += 1 print(fdist2.tabulate()) print("------------after------------------") fdist1 |= fdist2 print(fdist1.tabulate()) print(fdist2.tabulate()) こちらの出力は以下のようになります。 出力 length1 length2 length3 length4 3 1 1 1 None length2 length4 length1 length5 2 2 1 1 None ------------after------------------ length1 length2 length4 length3 length5 3 2 2 1 1 None length2 length4 length1 length5 2 2 1 1 None まずはlength5に注目しましょう。 1つ目のサンプルにはlength5,つまり"aaaaa"が存在していません。よって、afterの方では単純にlength5: 1の組み合わせで追加されています。 次はlength2に注目しましょう。 1つ目のサンプルにはlength2,つまりaaが1個存在しています。テキストの"aa"由来ですね。しかし2つ目のテキストにも"aa aa"、つまりlength2: 2の組み合わせがあったので、そちらでlength2が上書きされています。 とまぁここまでは従来のマージ演算と同じだと言っても問題はありません。length2に対する上書きは単純に右側にあったが故、と考えても成立するからです。 問題はlength1、こちらに注目してください。 1つ目のサンプルではlength1: 3となっています。テキストに"a a a"が含まれているからですね。一方で2つ目のサンプルの方でもlength1: 1があります。テキストに"a"がありますので。 しかし、結果を見てみると、length1: 3はそのままで上書きされていません。これが一律上書きをする組み込み演算子による辞書マージとの違いです。値が大きいためそちらが優先された、と考えると一応挙動に説明はつきます。 まとめ ここまでで"|="の挙動をまとめると、 1.)左側にキーが無い時は右側からそのまま追加 2.)キーが重複している場合は要素数が多いほうが優先される。つまり、左側のほうが大きかった場合は上書きが起きない。 ということなると思います。FreqDistクラスが辞書と同じ挙動をしている、という考え方がそもそもの間違いの原因である気もします。どちらかというと集合で使われる和集合の演算の方が近いような……でもFreqDistの和集合というのにもどうにも違和感が……的確な説明が出来る方がいたら、ぜひ教えてもらいたいところです。 いずれにせよ、変なハマり方をする前に気づけてよかったです。集合知に感謝。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複素数を掛け算してみるPython

Pythonにおける複素数 Pythonでは標準で虚数単位をjで表します。あまり使われてない機能かもしれませんが平面上でベクトルを直行させたい場合にこれがあるととても便利です。なぜならば、数学的には$\displaystyle j = e^{j\frac{\pi}{2}}$だからです。これがなぜかといえばあの有名なオイラーの公式$\displaystyle e^{j\theta}=cos\theta + j\cdot sin\theta$の通り、虚数単位は実部が0の複素数だと考えてもよいからです。そして、Pythonで複素数は例えば0+1jとするだけで定義できます。プログラミングでiもjもforループなどで使いがちなので知らないでいるとトラブルのもとかも知れません。 虚数単位とはなにかといえば$\displaystyle j = \sqrt{-1}$と考えるのはよく聞く話ですが、図解することもできます。複素平面という言葉があります。どういうことかというと実数は1本の軸の数直線上で示せますが、複素数は数直線に直行する軸を考えるととてもうまくいくのです。 ここで、-1の連続的な掛け算がどうなるか見てみましょう。中学生の数学なので迷うことはないでしょう。 print([(-1)**n for n in range(9)]) [1, -1, 1, -1, 1, -1, 1, -1, 1] 1と-1を行ったり来たりします。この値を順に線で繋ぐと次の図になります。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_visible(False) ax.set_aspect('equal') ax.set_yticks([]) plt.plot(*list(zip(*[((-1)**n,0) for n in range(9)]))) plt.show() この動きは原点と中心として-1をかけるごとに180度回転しているという発想もできます。 ならば、-1はjを2回かけた結果なのですから、jをかけるごとに90度回転していると考えても間違いではないはずです。ほんとかうそか、実際にやってみるのが早いです。 i = 0+1j print([i**n for n in range(9)]) [(1+0j), 1j, (-1+0j), (-0-1j), (1+0j), 1j, (-1+0j), (-0-1j), (1+0j)] これだけだと作図のイメージが湧きませんので実部と虚部に分けて表示してみます。 print([(c.real,c.imag) for c in [i**n for n in range(9)]]) [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (-0.0, -1.0), (1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (-0.0, -1.0), (1.0, 0.0)] なにか法則が見えてきた気がします。これをプロットしてみましょう。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') plt.plot(*list(zip(*[(c.real,c.imag) for c in [i**n for n in range(9)]]))) 思ったとおり、ぐるぐるしています。iを4回かけると元の位置に戻っていることがよく分かります。これが複素平面で複素数を表示する概念です。 ところで半径が1のままだと感覚的に理解しにくいので、半径を比例的に伸ばしてみます。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') ax.set_xlim(-1.1,1.1) ax.set_ylim(-1.1,1.1) count = 9 i = 0+1j ax.plot(*list(zip(*[(c.real,c.imag) for c in [(n/count)*(i**(n)) for n in range(count)]]))) plt.show() こんな感じでうずまきします。 更に別の観点で、iをかけるごとに90度回るのなら、iの平方根をかけるごとに45度回ることも作図で理解が可能です。この場合はいよいよ、座標は軸線上から離れます。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') ax.set_xlim(-1.1,1.1) ax.set_ylim(-1.1,1.1) count = 9 i = 0+1j ax.plot(*list(zip(*[(c.real,c.imag) for c in [(i**(n/2)) for n in range(count)]]))) plt.show() 虚数単位の平方根をjの$\frac{1}{2}$乗と記述しています。 思ったとおり、図は正八角形となりました。 いろんな複素数の連続的な掛け算を考える さて、ここからが本題ですが、、、半径を変えながらプロットしたらどんな図が描けるでしょうか。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') ax.set_xlim(-1.1,1.1) ax.set_ylim(-1.1,1.1) count = 9 i = 0+1j ax.plot(*list(zip(*[(c.real,c.imag) for c in [(n/count)*(i**(n/2)) for n in range(count)]]))) plt.show() 渦巻状になりはじめました。上記でcountを増やしたり、乗数の部分の分母を2じゃなくていろんな小数にしたりすると、急に芸術的になってきます。 虚数単位に対する乗数の分母が1でcountが20のとき 虚数単位に対する乗数の分母が2でcountが20のとき 虚数単位に対する乗数の分母が4でcountが20のとき 虚数単位に対する乗数の分母が4でcountが200のとき 単純な複素数の掛け算の連続で蚊取り線香っぽいものが描けました! 遊んでみる ここでもっと柔軟に考えて、乗数の分母を半端な小数にしてみます。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') ax.set_xlim(-1.1,1.1) ax.set_ylim(-1.1,1.1) count = 9 i = 0+1j ax.plot(*list(zip(*[(c.real,c.imag) for c in [(n/count)*(i**(n/0.111)) for n in range(count)]]))) plt.show() このままcountを100まで大きくしてみます。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(5,5),facecolor='white',dpi=100) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') ax.set_xlim(-1.1,1.1) ax.set_ylim(-1.1,1.1) count = 100 i = 0+1j ax.plot(*list(zip(*[(c.real,c.imag) for c in [(n/count)*(i**(n/0.111)) for n in range(count)]]))) plt.show() こんな美しい絵になると予想できたでしょうか。 芸術性にこだわってみる countの数だけプロットを重ねるごとに色合いを変えてみます。 import matplotlib.pyplot as plt fig,ax = plt.subplots(figsize=(3,3),facecolor='white',dpi=240) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_position(('data', 0)) ax.spines['left'].set_position(('data', 0)) ax.set_aspect('equal') ax.set_xlim(-1.1,1.1) ax.set_ylim(-1.1,1.1) count = 400 i = 0+1j values = [(c.real,c.imag) for c in [(n/count)*(i**(n/0.110025)) for n in range(count)]] cmap = plt.get_cmap('BuGn') for n in range(count): ax.plot(*list(zip(*values[:n])),alpha=0.01,c=cmap(float(n/count))) plt.tight_layout() plt.show() ここまでくるともはやアートです。 このような図形の描画の過程をアニメーションにしました。 開発環境はJupyterです。 import matplotlib.pyplot as plt from PIL import Image from io import BytesIO from IPython.display import HTML import base64 ※描画の範囲がブレないように白い四角で囲っています。 i = 0+1j base = 1 count = 200 imgs = [] values = [(c.real,c.imag) for c in [(n/count)*(i**(n/base)) for n in range(count)]] cmap = plt.get_cmap('BuGn') for n in range(count): fig,ax = plt.subplots(figsize=(2,2),facecolor='white',dpi=150) ax.set_aspect('equal') ax.axis('off') ax.set_xlim(-1,1) ax.set_xlim(-1,1) ax.plot(*list(zip(*[(-1,-1),(1,-1),(1,1),(-1,1),(-1,-1)])),c='white') for m in range(n): ax.plot(*list(zip(*values[:m])),alpha=0.01,c=cmap(float(m/count))) ax.plot(*list(zip(*values[n-1:n+1])),alpha=0.5,c='green') buf = BytesIO() plt.savefig(buf) imgs.append(Image.open(buf)) plt.clf() plt.close() fig,ax = plt.subplots(figsize=(2,2),facecolor='white',dpi=150) ax.set_aspect('equal') ax.axis('off') ax.set_xlim(-1,1) ax.set_xlim(-1,1) ax.plot(*list(zip(*[(-1,-1),(1,-1),(1,1),(-1,1),(-1,-1)])),c='white') for n in range(count): ax.plot(*list(zip(*values[:n+1])),alpha=0.01,c=cmap(float(n/count))) buf = BytesIO() plt.savefig(buf) imgs.append(Image.open(buf)) plt.clf() plt.close() buf = BytesIO() imgs[0].save(buf, format='gif', save_all=True, append_images=imgs[1:]+imgs[-1:]*20, duration=50, loop=0) HTML('<img src="data:image/gif;base64,'+base64.encodebytes(buf.getvalue()).decode('utf8')+'"/>') 上記で1になっている変数 base (虚数単位に対する乗数の分母) を変化させてみます。 base = 0.111 base = 0.110025 Qiitaに貼るためにgifにしたので画質は微妙です。Animated pngの形式のほうが色合いが綺麗ですが、現時点でのChromeの仕様なのかクリックしないと動きませんでした。多分高画質っぽいものはMP4にしてTwitterに上げました。 好みの問題ですが、私はこれらの図が綺麗だと思いました。学校で習ったときより、複素数に興味が持てたのでしたら幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Pandas】Pandasの集計結果をEXCELファイルに書き込む方法 .no.36

こんにちは、まゆみです。 Pandasの記事をシリーズで書いています。 今回は第36回目になります。 前回の記事では、CSVファイルに書き込む方法を解説させていただきました。 今回の記事では、Pandasの集計結果をEXCELファイルに書き込む方法を解説していこうと思います。 ではさっそく始めていきますね。 EXCELファイルをPandasで扱う時に必要なライブラリー EXCELファイルを扱うには『xlrd』と『openpyxl』というライブラリーが必要なので、こちらをインストールしていない人はこちらの記事にインストール方法を書いています。 今回使うデータ 前回の記事で使ったデータと同じセントラルパークのリスについてのデータを使います。 .read_csv()で読み込むと下記のようになります。 『Primary Fur Color』(主な毛の色)というコラムを分類分けした結果(グレー、ブラック、シナモン色に分類できる)をEXCELの別々のワークシートにそれぞれ取り出してみましょう df["Primary Fur Color"] == "Gray" で、Primary Fur Color の値がGray になっているものが、『True』そうでないものが『False』として返されるので(下記参考) このbooleanのデータを使って、dataframeからGray がTrue のrow のみを抜き出します df[df["Primary Fur Color"] == "Gray"] これをgrayという変数に代入しました。 同様にして、black とcinnamon でもそれぞれの値を別々に取り出して変数に代入しました。 Excel ファイルに分けて書きたい物が揃ったところで、さっそくPandasの.to_excel()メソッドのドキュメントを見てみましょう .to_excel()メソッドを使う時に必要なパラメーター 引用元:Pandasドキュメント まず、最初に必要なのが、『ExcelWriter オブジェクト』になります。 Series やDataFrameを作るのと同じ要領で、ExecelWriter クラスからオブジェクトを作りましょう ExcelWriter クラスからオブジェクトを作る pd.Excelと打ち込んだ時点で『Tab』キーを押せば候補を出してくれます(下のスクショ参考) その中の『ExcelWrite』クラスからオブジェクトを作り、好きな変数に代入します。 パラメーター『sheet_name』 excelファイルの一番下は こんな風になっていますが、.to_excel()のパラメーターsheet_name を使うと、ワークシートに名前を付けることができます。 それぞれのワークシートを Gray Squirrels Black Squirrels Cinnamon Squirrels と名前を付けました。 パラメーターindex をTrue にするかFalse にするかは、数字のインデックスを一番前に入れたいかどうかです。 詳しくはこちらに書いてますので、参考にどうぞ .save() 今の時点でコードを実行しても、カレントフォルダーには、エクセルファイルはできていません。 最後、『.save()』でファイルを保存します .save()はExcelファイルにエクスポートしたいものの処理が全て終わってから最後にします。 .save()が終わればカレントフォルダー内に、ExcelWriterオブジェクトを作る時につけたファイル名がついたエクセルファイルができているはずです。 エラーが起こる場合もある 以上のプロセスで上手くいく場合もいますが、エラーが出る場合もあります。 原因により、色んな解決方法があると思いますが、私の調べた方法を書いておきますね。 引用元:stackoverflow 無事、ダウンロードしてExcelファイルに毛色で分けたデータを書き出すことができました。 『全てのコラムは要らない。AgeとPrimary Fur Colorのみエクセルファイルに書き出したい』っていう人は、 to_excel(columns= ["コラムA", "コラムB"]) と.to_excel()のパラメーターcolumnsにリスト型で引数を渡して下さい。 Gray Squirrels タブのデータのみ、毛色と年齢のコラムのみにしてみました。 まとめ 今回の記事はこれくらいで終わりにします。 前回扱った、CSVに書き出す方法より少し複雑になります。 でもエクセルファイルに書き出す方が断然見やすいデータになりますよね。 この記事があなたのお役に立てれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyTorchでEarlyStoppingを実装する

0.はじめに 今までKerasを使っていた人がいざpytorchを使うってなった際に、Kerasでは当たり前にあった機能がpytorchでは無い!というようなことに困惑することがあります。 KerasではcallbackとしてEarlyStoppingの機能が備わっていますが、Pytorchではデフォルトでこの機能は存在せず、自分で実装する必要があります。 今回はそれを実装したので共有しておきます。 参考:KerasのEarlyStopping 1.EarlyStoppingって何なのか? 本記事を見るような方はすでにご存じかもしれませんが、一応説明します。 ・そもそも何Epoch学習を回せばいいのかなんて初見ではわからない ・Epoch数を重ねるたびにlossは下がるが、訓練データに過学習する可能性がある ・せっかくlossが下がっても、そのまま放置しすぎるとlossが上がっていってしまうケースがある こんなの時に便利なのがこのEarlyStoppingなのである。 ★とりあえずEpoch数は大きく設定しておけばいい(EarlyStoppingが止めてくれるから) ★学習が収束した時に自動的に学習を止めてくれる(過学習防止) 2.EarlyStoppingクラスを作成する プログラム的には ・何回lossの最小値を更新しなかったら学習をやめるか?を決めて(patience) ・監視しているlossが最低値を更新できない数をカウントし(counter) ・監視しているlossが最低値を更新したときだけ学習済モデルを保存しておき、そのlossを記録(checkpoint) ・監視しているlossが設定数だけ最低値を更新できない場合に学習ループを抜ける(early_stop ) これらを実装すればいいだけである。 class EarlyStopping: """earlystoppingクラス""" def __init__(self, patience=5, verbose=False, path='checkpoint_model.pth'): """引数:最小値の非更新数カウンタ、表示設定、モデル格納path""" self.patience = patience #設定ストップカウンタ self.verbose = verbose #表示の有無 self.counter = 0 #現在のカウンタ値 self.best_score = None #ベストスコア self.early_stop = False #ストップフラグ self.val_loss_min = np.Inf #前回のベストスコア記憶用 self.path = path #ベストモデル格納path def __call__(self, val_loss, model): """ 特殊(call)メソッド 実際に学習ループ内で最小lossを更新したか否かを計算させる部分 """ score = -val_loss if self.best_score is None: #1Epoch目の処理 self.best_score = score #1Epoch目はそのままベストスコアとして記録する self.checkpoint(val_loss, model) #記録後にモデルを保存してスコア表示する elif score < self.best_score: # ベストスコアを更新できなかった場合 self.counter += 1 #ストップカウンタを+1 if self.verbose: #表示を有効にした場合は経過を表示 print(f'EarlyStopping counter: {self.counter} out of {self.patience}') #現在のカウンタを表示する if self.counter >= self.patience: #設定カウントを上回ったらストップフラグをTrueに変更 self.early_stop = True else: #ベストスコアを更新した場合 self.best_score = score #ベストスコアを上書き self.checkpoint(val_loss, model) #モデルを保存してスコア表示 self.counter = 0 #ストップカウンタリセット def checkpoint(self, val_loss, model): '''ベストスコア更新時に実行されるチェックポイント関数''' if self.verbose: #表示を有効にした場合は、前回のベストスコアからどれだけ更新したか?を表示 print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}). Saving model ...') torch.save(model.state_dict(), self.path) #ベストモデルを指定したpathに保存 self.val_loss_min = val_loss #その時のlossを記録する 3.CIFER10の画像分類に組み込んで使う サンプルとして作成したクラスを使って、CIFER10の画像分類で使ってみます。 pytorch初心者もわかるように、1文1文に何やってるか?のコメントも書いてますので参考にしてください。 また、分かりやすいようにearlystoppingに関連する部分は★~~★でコメント書いています。 3-1.まずはCIFER10の分類に使うCNNクラス作成まで import torch import torchvision from torch.utils.data import Dataset import torchvision.transforms as transforms from torchvision import models from torch import nn,optim import numpy as np import matplotlib.pyplot as plt class EarlyStopping: """★上で書いてるので省略★""" class Mynet(nn.Module): """自作CNN""" def __init__(self): super(Mynet, 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プーリング 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 3-2.学習ループ関数 def train_net(n_epochs, train_loader, net, optimizer_cls, loss_fn, device='cpu'): """学習ループ部分""" #★EarlyStoppingクラスのインスタンス化★ earlystopping = EarlyStopping(patience=1, verbose=True) #検証なのでわざとpatience=1回にしている losses = [] #loss遷移の記録用 net.to(device) #ネットワークをGPUへ for epoch in range(n_epochs): running_loss = 0.0 #loss初期化 net.train() #netをtrainingモードに for index, data in enumerate(train_loader): inputs, labels = data #画像とラベルを取り出す inputs = inputs.to(device) #画像をGPUへ labels = labels.to(device) #ラベルもGPUに optimizer.zero_grad() #勾配を初期化 outputs = net(inputs) #ネットワークで予測 loss = criterion(outputs, labels) #loss計算 loss.backward() #逆伝番 optimizer.step() #勾配を更新 running_loss += loss.item() #バッチごとのlossを足していく losses.append(running_loss / index) #loss遷移を記録 print("epoch", epoch, ": ", running_loss / index) #学習経過の表示 #★毎エポックearlystoppingの判定をさせる★ earlystopping((running_loss / index), net) #callメソッド呼び出し if earlystopping.early_stop: #ストップフラグがTrueの場合、breakでforループを抜ける print("Early Stopping!") break return losses 3-3.main部分 #前処理部分(Tensor化、正規化) transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize( (0.5, 0.5, 0.5), # RGB 平均 (0.5, 0.5, 0.5)# RGB 標準偏差 ) ]) #CIFAR10をダウンロードしてデータセット作成 trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) #データローダーを作成 trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2) #ネットワークをインスタンス化 model = Mynet() #学習設定 criterion = nn.CrossEntropyLoss() #損失関数をクロスエントロピーに optimizer = optim.Adam(model.parameters(), lr=0.001) #オプティマイザはAdam epochs = 10000 #エポック数 ★とりあえず大きく設定しておけばいいので1万としておいた★ device = torch.device("cuda:0" if torch.cuda. is_available() else "cpu") #デバイス(GPU or CPU)設定 #学習開始(作成した学習関数を呼び出す) losses = train_net(n_epochs=epochs, train_loader=trainloader, net=model, optimizer_cls=optimizer, loss_fn=criterion, device=device ) すると、以下の実行結果が表示される。 実行結果 epoch 0 : 1.0410267220779432 Validation loss decreased (inf --> 1.041027). Saving model ... epoch 1 : 1.0064437736963907 Validation loss decreased (1.041027 --> 1.006444). Saving model ... ・ (略) ・ epoch 25 : 0.7393526346215752 Validation loss decreased (0.741965 --> 0.739353). Saving model ... epoch 26 : 0.7438237962901487 EarlyStopping counter: 1 out of 1 Early Stopping! ・26epoch目でloss値が「0.743」となっており、それまでのベストスコア(25epoch目)である「0.739」を更新できなかった ・patience=1としているので、1回でもベストスコアを更新できなければ学習ループを抜ける設定 3-4.loss推移+保存したモデル確認 lossの推移を見てみると、確かに26epoch目でloss値が上向いていることが確認できる。 さらに保存した学習済モデルに関してはこの26epoch目ではなく、25epoch目が保存されている plt.plot(losses, label='train_loss') plt.xlabel('Epochs') plt.ylabel('loss') plt.legend() plt.show() 3-5.ベストモデルを適応させる このままだとmodel変数には26epoch目の学習が反映されている為、保存したモデルを一度適応させる必要がある。 これで最高性能のモデルを使える。 ※以下省略 model.load_state_dict(torch.load('checkpoint_model.pth')) 実行結果 <All keys matched successfully> 4. さいごに 久々に画像を題材に記事書いてみました。 PytorchはKerasより記載量は多いものの、細かい部分をカスタマイズできるので今後はますます採用比率が上がると個人的には考えています。 それでは良きPytorchライフを!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む