- 投稿日:2021-08-15T23:07:32+09:00
Numpyとの比較で学ぶJuliaのベクトル・行列の記法
Abstract Juliaのベクトル・行列とNumpy.ndarrayの記法を比較してまとめました。(備忘録) 本記事では主にPythonのNumpyをよく使っていた人を対象に、「Juliaではこうやって書く」を紹介することになります。 とはいえ、記事を書いている本人はJuliaを初めて1ヶ月も経っていないので、間違いなどありましたら指摘してもらえたら嬉しいです。 環境・前提 OS: Windows 10 Python: 3.7.9 Numpy: 1.20.3 Julia: 1.6.2 Pythonのコードではimport numpy as npを前提にします。 Juliaでは以下のパッケージを用いることがあります。 LinearAlgebra Statistics 必要ならPkg.addしてください。 また文法についてはほぼ触れません。 indexのスタート Numpy(Python)ではインデックスは0スタートです。 一方でJuliaでは1スタートです。地味に注意。 1次元(Vector) 1次元の場合、indexの始まりに注意すれば問題ならないかと。 宣言・初期化 JuliaではNumpy.array()みたいなのは使わない。 Arrayコンストラクタで作ることはできるが・・使うのかなぁ? ベクトル宣言.jl julia> x = [1, 2, 3] 3-element Vector{Int64}: 1 2 3 # 各要素が0で長さが4のベクトル。型はFloat64がデフォルト。 julia> x = zeros(4) 4-element Vector{Float64}: 0.0 0.0 0.0 0.0 # 各要素の型がInt8で値が0、長さ5のベクトル julia> x = zeros(Int8, 5) 5-element Vector{Int8}: 0 0 0 0 0 julia> x = ones(5) 5-element Vector{Float64}: 1.0 1.0 1.0 1.0 1.0 # start:end を使って等差数列(交差は1) Pythonのrangeと異なり、endを含む点は注意。 julia> x = collect(0:5) 6-element Vector{Int64}: 0 1 2 3 4 5 # start:step:end で生成 julia> x = Vector(0:2:5) 3-element Vector{Int64}: 0 2 4 他にも色々ある。引数は上記と変わらない。 またMatrixでも次元の指定部分を変えるだけ。 関数 返り値 trues 各要素がtrue falses 各要素がfalse rand 各要素が区間[0, 1)から生成される乱数(各要素は無相関) randn 各要素が標準正規分布から生成される乱数(各要素は無相関) length, size, shape Numpyお馴染みのアトリビュート達。 長さとか.py x = np.array([1, 2, 3]) len(x) #> 3 x.shape #> (3, ) x.size #> 3 Juliaはshapeに相当するものがsize()だと思われる。 shape()はなさそう?(要出典) 長さとか.jl x = [1, 2, 3] length(x) #> 3 size(x) #> (3,) なお、Pythonではxは1次元ndarray(?)であり、行列と演算するためにはx.reshape((-1, 1))などで縦/横ベクトルにしなければならない。 一方でJuliaでは1次元配列は通常縦ベクトルで扱われる。 詳しくは後半の行列の演算で述べる。 基本的なアクセス ここは簡単。 アクセス.py x = np.array([1, 2, 3]) x[0] #> 1 x[1] #> 2 x[3] #> 3 # 末尾のアクセスは-1で可能 x[-1] #> 3 アクセス.jl julia> x = [1, 2, 3] julia> x[1] 1 julia> x[2] 2 julia> x[3] 3 # Juliaでは末尾のアクセスにはendというキーワード?を用いる。 julia> x[end] 3 Pythonではお馴染みのスライスもJuliaではサポートしている。 スライス.py >>> a = np.array([1, 2, 3, 4, 5]) >>> a[1:3] array([2, 3]) >>> a[2:] array([3, 4, 5]) indexの違いにより、同じ結果を得るためにはスライスのindexに注意しなければならない。 スライス.jl julia> a = [1:5;] 5-element Vector{Int64}: 1 2 3 4 5 julia> a[2:3] 2-element Vector{Int64}: 2 3 # Pythonと違い、最後の成分の省略はエラー julia> a[3:] ERROR: syntax: missing last argument in "3:" range expression # endで最後の指定をする julia> a[3:end] 3-element Vector{Int64}: 3 4 5 イテレーション Juliaは単純なイテレーションでも色々あるらしい。 イテレーション.py x = np.array([1, 2, 3]) for i in range(x.shape[0]): print(x[i]) ''' 1 2 3 ''' イテレーション.jl x = [1, 2, 3] for i in 1:length(x) println(x[i]) end #= 1 2 3 =# # ドキュメントではこっちの方が推奨? for i in eachindex(x) println(x[i]) end #= 1 2 3 =# 基本演算 Numpyでは、形状が同じなら、基本的には要素ごとの演算で+各種演算子が定義されている。 演算.py >>> x = np.array([1, 2, 3]) >>> y = np.array([7, 8, 9]) >>> x + y np.array([ 8 10 12]) >>> x - y np.array([-6 -6 -6]) >>> x * y np.array([ 7 16 27]) >>> x / y np.array([0.14285714 0.25 0.33333333]) >>> x ** y np.array([ 1 256 19683]) 一方で、Juliaでは各要素に対する演算は、.を各種演算子に付け加えることで実現する。 演算.jl julia> x = [1, 2, 3]; julia> y = [7, 8, 9]; julia> x .+ y 3-element Vector{Int64}: 8 10 12 julia> x .- y 3-element Vector{Int64}: -6 -6 -6 julia> x ./ y 3-element Vector{Float64}: 0.14285714285714285 0.25 0.3333333333333333 julia> x .* y 3-element Vector{Int64}: 7 16 27 julia> x .^ y 3-element Vector{Int64}: 1 256 19683 なお、関数に対しても同様に.を必要とする。 関数.jl julia> x = [1, 2, 3]; julia> f(x) = x^2 + 1 f (generic function with 1 method) # 関数の次に . をつけることで、作用を各要素にする julia> f.(x) 3-element Vector{Int64}: 2 5 10 # `@.`マクロを使えばその行の関数を全て各要素に作用するようにできる。 julia> @. sin(cos(x)) 3-element Vector{Float64}: 0.5143952585235492 -0.4042391538522658 -0.8360218615377305 Numpyでの内積はこんな感じ。 2次元ndarrayとして扱う場合は以下の方法ではダメ。 内積.py >>> x = np.array([1, 2, 3]) >>> y = np.array([7, 8, 9]) >>> np.dot(x, y) 50 Juliaではdot関数のあるLinearAlgebraパッケージが必要。 内積.jl >>> using LinearAlgebra >>> x = [1, 2, 3]; >>> y = [7, 8, 9]; >>> dot(x, y) 50 # 転置 ' を使えば以下でも可能。行列の掛け算*は省略可能なことも使っている。 >>> x'y 50 比較 Numpyでは、==, !=演算子によってTrue, FalseのNumpy配列が返却される。 なお以下の議論は大体==で行うが、!=, >, <, >=, <=などでも同じ。 比較.py >>> a = np.array([1, 2, 3]) >>> b = np.array([3, 2, 1]) >>> a == 1 array([ True, False, False]) >>> a == b array([False, True, False]) # サイズが違うと怒られる。 >>> c = np.array([1, 2, 3, 4]) >>> a == c __main__:1: DeprecationWarning: elementwise comparison failed; this will raise an error in the future. False Juliaでは、前項同様に.==を用いるようだ。 ==によってエラーを吐かないっぽいので、注意が必要かもしれない。 比較.jl julia> a = [1, 2, 3]; julia> b = [3, 2, 1]; # 各要素と右辺の2を比較 julia> a .== 2 3-element BitVector: 0 1 0 # これはエラーにならず、falseが返ってくることに注意。むしろエラーしてくれ? julia> a == 2 false # Vector同士でも==の比較ができてしまう。 julia> a == b false # .==を使うと、Numpyみたいな結果が得られる。 julia> a .== b 3-element BitVector: 0 1 0 # all, anyはNumpyっぽく使える。ただしBitVectorでないとエラーを吐く。 julia> all(a) ERROR: TypeError: non-boolean (Int64) used in boolean context # .==, .>等で比較した結果の配列を渡す julia> all(a .> 2) false # anyも同様 julia> any(a .== 2) true なおJuliaのany, allにはコールバック関数を渡す方法もあるようだ。 適当に実験してみよう!(本当はベンチマークとか使うべきなんだろうけど・・・) julia> a = [1:100000]; julia> @time all(a .== 5) 0.243744 seconds (321.17 k allocations: 17.518 MiB, 99.66% compilation time) false julia> @time all(x -> x == 5, a) 0.055989 seconds (59.71 k allocations: 3.653 MiB, 112.40% compilation time) false julia> @time any(a .== 5) 0.000046 seconds (4 allocations: 192 bytes) false julia> @time any(x -> x == 5, a) 0.056032 seconds (56.57 k allocations: 3.428 MiB, 99.72% compilation time) false allではコールバック関数を渡す方法の方が圧倒的に速い。 一方でanyは評価済みの配列を渡す方法が圧倒的に速い。 allについては公式ドキュメントの説明によると Determine whether predicate p returns true for all elements of itr, returning false as soon as the first item in itr for which p returns false is encountered (short-circuiting). と書いてある。いわゆる短絡評価によるものらしい。 a .== 5は各要素全てに評価を行ってから渡している。そりゃあ遅い。 anyについても、 Determine whether predicate p returns true for any elements of itr, returning true as soon as the first item in itr for which p returns true is encountered (short-circuiting). と書いてあるので、短絡評価をしているはずなのだが・・・・ allでは最初の要素がfalseなので、コールバック関数が呼ばれるのは初回だけ。 一方でanyは5番目の要素まではコールバック関数が呼ばれるからだろうか。 理由わかる方いたらご教授願います。 最大最小 Numpyではmin, maxでおkだった。 最大最小.py >>> x = np.array([1, 5, 4, 9, -2]) >>> np.min(x) -2 >>> np.max(x) 9 一方でJuliaの場合、最大最小に関する組み込み関数が2種類ある。 Juliaで配列の最小値を計算する時はminよりminimumの方が圧倒的に速いです!!の検証結果及びコメント欄を参考にしてほしい。なおnp.minimum, np.maximumとは名前がお味だけで、全く動作は異なるので注意。 最大最小.jl julia> x = [1, 5, 4, 9, -2]; # こっちの方が良い? julia> minimum(x) -2 julia> maximum(x) 9 # min()でも可能だが、...を使って展開する必要がある。展開が遅いらしいので非推奨?(上記記事のコメント欄参照) julia> min(x...) -2 julia> max(x...) 9 ソート Numpyではnp.sortを使えばヨシ。indexのソートを返すargsortの例も置いておく。 x.sort()だと自身を書き換える破壊的メソッドなので注意。 ソート.py >>> x = np.array([1, 5, 4, 9, -2]) >>> np.sort(x) np.array([-2 1 4 5 9]) # 降順の場合はスライスで反転させる。 >>> np.sort(x)[::-1] np.array([ 9 5 4 1 -2]) # argsort >>> np.argsort(x) np.array([4 0 2 1 3]) Juliaでもsort関数が定義されている。 また、indexの結果を返すnp.argsort的なものもちゃんとある。 sort!関数では引数の配列をソートする破壊的メソッドなので注意。 ソート.jl julia> x = [1, 5, 4, 9, -2]; julia> sort(x) (x) = [-2, 1, 4, 5, 9] julia> sort(x, rev=true) 5-element Vector{Int64}: -2 1 4 5 9 # 便利機能。byに関数を渡して、その値でソートできる。 julia> sort(x, by=abs) 5-element Vector{Int64}: 1 -2 4 5 9 # このsortpermがargsortに相当する。 perm = sortperm(x) 5-element Vector{Int64}: 5 1 3 2 4 # indexの並び替えを指定した配列を使えば、それに従って並び替えることができる。 julia> x[perm] 5-element Vector{Int64}: -2 1 4 5 9 統計的な量 単純な和、平均値や分散など。 Numpyでnp.mean等が用意されている。 統計.py >>> x = [1, 5, 4, 9, -2] >>> np.sum(x) 17 >>> np.mean(x) 3.4 >>> np.std(x) 3.7202150475476548 一方で、Juliaの場合はStatisticsパッケージが必要となる。 統計.jl julia> x = [1, 5, 4, 9, -2]; julia> using Statistics # sumは組込み関数。 julia> sum(x) 17 # 平均 julia> mean(x) 3.4 # 標準偏差 julia> std(x) 4.159326868617084 Statisticパッケージは、その他便利な機能が沢山あるようなので、ドキュメントを読んでみるとよいだろう。 2次元(Matrix) 宣言・初期化 NumpyではPythonの2次元配列→Numpyの2次元配列(行列)といった風に作成する。 全要素が0, 1の配列はnp.zeros, np.onesを使うし、単位行列はnp.eye, np.identityを使う。 宣言初期化.py >>> A = np.array([[1, 2, 3], [4, 5, 6]]) >>> A array([[1, 2, 3], [4, 5, 6]]) >>> B = np.zeros((2, 3)) >>> B array([[0., 0., 0.], [0., 0., 0.]]) >>> I = np.identity(3) >>> I array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) Juliaでは2次元配列はデフォルト行列となっている(要出典)。 行列の宣言に注意すれば、生成する関数はVectorの場合とほぼ同様。形状を指定する引数ndimを変えればよい。 また単位行列はLinearAlgebraパッケージに定数Iとして存在している。 宣言初期化.jl julia> A = [1 2 3; 4 5 6] 2×3 Matrix{Int64}: 1 2 3 4 5 6 # 形は順番に引数に渡してもよいし julia> B = zeros(2, 3) 2×3 Matrix{Float64}: 0.0 0.0 0.0 0.0 0.0 0.0 # タプルの形で渡してもよい julia> C = ones((3, 3)) 3×3 Matrix{Float64}: 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 julia> using LinearAlgebra # Iは単位行列かのようにふるまう julia> C + I 3×3 Matrix{Float64}: 2.0 1.0 1.0 1.0 2.0 1.0 1.0 1.0 2.0 # 正方行列と演算しないとエラー julia> A + I ERROR: DimensionMismatch("matrix is not square: dimensions are (2, 3)") Vectorの初期化でも紹介したいtrues, falsesなども同様に使える。 length, size, shape Numpyでお馴染みのshape, size。 長さとか.py >>> A = np.zeros((2, 2)) >>> A array([[0., 0.], [0., 0.]]) >>> len(A) 2 >>> A.shape (2, 2) >>> A.size 4 Vectorの時と同様。 Matrixの場合lengthは要素の数で、sizeが2×2などの形状を返す。 長さとか.jl julia> A = zeros((2, 2)) 2×2 Matrix{Float64}: 0.0 0.0 0.0 0.0 julia> length(A) 4 julia> size(A) (2, 2) reshape Numpyでもshapeを変換するためにnp.reshape, ndarray.reshapeを使えた。 reshape.py >>> A = np.array([[1, 2, 3], [4, 5, 6]]) >>> A array([[1, 2, 3], [4, 5, 6]]) >>> B = np.reshape(A, (3, 2)) >>> B array([[1, 2], [3, 4], [5, 6]]) Juliaでは組込みでreshape関数がある。 またJuliaの多くの関数では、形状を指定する際タプルで渡してもよいし、順番に引数に渡してもよい(可変長引数)のはNumpyとの大きな違いであろう。 reshape.jl # ones((2, 3))でもよい julia> A = ones(2, 3) 2×3 Matrix{Float64}: 1.0 1.0 1.0 1.0 1.0 1.0 # reshape(A, 3, 2)でもよい julia> B = reshape(A, (3, 2)) 3×2 Matrix{Float64}: 1.0 1.0 1.0 1.0 1.0 1.0 基本的なアクセス Numpyの場合は、基本的にはA[行][列]の形でアクセスし、A[行]では列が返ってきた。またスライスの利用で行も取得できた。 アクセス.py >>> A = np.array([[1, 2, 3], [4, 5, 6]]) >>> A array([[1, 2, 3], [4, 5, 6]]) # 要素のアクセス >>> A[0][1] 2 # 列のアクセス >>> A[0] array([1, 2, 3]) >>> A[1] array([4, 5, 6]) # 行のアクセス >>> A[:, 0] array([1, 4]) 一方でJuliaでも色々アクセスの仕方がある。 最も基本的なアクセスの仕方は、Vectorのようなアクセス方法と、行と列で指定する2種類がある。 それぞれLinear indexing, Cartesian indexingと呼ぶらしい。 アクセス.jl julia> A = [1 2 3; 4 5 6] 2×3 Matrix{Int64}: 1 2 3 4 5 6 # Numpyと違い、要素が返ってくる julia> A[1] 1 # しかも列指向であるので、2ではなく4が返ってくる! julia> A[2] 4 # [行, 列]で指定すると、Numpyっぽい julia> A[1, 2] 2 列指向を完全に理解しているわけではないが、今回の行列の場合以下のような順番になる。 よってA[3] = 2であり、`A[4] = 5, A[5] = 3, A[6] = 6となる。 イテレーション Numpyでfor文ってよく考えたら速度的にNGだった気が Numpyでは単純にやると行ごとにイテレーションする。 イテレーション.py A = np.array([[1, 2, 3], [4, 5, 6]]) for a in A: print(f"a = {a}, type: {type(a)}") ''' a = [1 2 3], type: <class 'numpy.ndarray'> a = [4 5 6], type: <class 'numpy.ndarray'> ''' 一方でJuliaでは成分ごとに、列指向の順でイテレーションされる。 ちなみにJuliaでは場合によってはforで計算する方が速いこともあるとか(要出典)。 イテレーション.jl A = [ 1 2 3; 4 5 6 ] for a in A println("a = $(a), type: $(typeof(a))") end #= a = 1, type: Int64 a = 4, type: Int64 a = 2, type: Int64 a = 5, type: Int64 a = 3, type: Int64 a = 6, type: Int64 =# その他にも、色々とイテレーションの方法はある。 イテレーション2.jl # 列ごとにイテレーション for a in eachcol(A) println("a = $(a), type: $(typeof(a))") end #= a = [1, 4], type: SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true} a = [2, 5], type: SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true} a = [3, 6], type: SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true} =# # 行ごとにイテレーション for a in eachrow(A) println("a = $(a), type: $(typeof(a))") end #= a = [1, 2, 3], type: SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true} a = [4, 5, 6], type: SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true} =# # indexを順番にイテレーション for a in eachindex(A) println("a = $(a), type: $(typeof(a))") end #= a = 1, type: Int64 a = 2, type: Int64 a = 3, type: Int64 a = 4, type: Int64 a = 5, type: Int64 a = 6, type: Int64 =# 他にも様々な方法があるようだが、配列要素の参照、色々を参考にしてもらいたい。 基本演算 ここでは行列同士の演算を見ていく。 Numpyでは通常の演算子+, -等が使えた。 演算.py >>> A = np.array([[1, 2, 3], [4, 5, 6]]) >>> B = np.array([[7, 8, 9], [9, 8, 7]]) # Numpyでは基本的には各要素同士の演算になる。 >>> A + B array([[ 8, 10, 12], [13, 13, 13]]) >>> A - B array([[-6, -6, -6], [-5, -3, -1]]) >>> A * B array([[ 7, 16, 27], [36, 40, 42]]) >>> A / B array([[0.14285714, 0.25 , 0.33333333], [0.44444444, 0.625 , 0.85714286]]) >>> A ** B array([[ 1, 256, 19683], [262144, 390625, 279936]], dtype=int32) Juliaでは、+, -は通常の行列同士の足し算・引き算となる。 Vectorと同様に.+, .-などで各要素に同士の演算を行う。 演算.jl julia> A = [1 2 3; 4 5 6]; julia> B = [7 8 9; 9 8 7]; # 行列同士の足し算 julia> A + B 2×3 Matrix{Int64}: 8 10 12 13 13 13 # 要素同士の足し算を強調? julia> A .+ B 2×3 Matrix{Int64}: 8 10 12 13 13 13 # 行列同士の引き算 julia> A - B 2×3 Matrix{Int64}: -6 -6 -6 -5 -3 -1 # 要素同士の引き算強調 julia> A .- B 2×3 Matrix{Int64}: -6 -6 -6 -5 -3 -1 # *は行列の掛け算。今回の例(A, B)ではサイズが合わずにエラーとなる。 julia> A * B ERROR: DimensionMismatch("matrix A has dimensions (2,3), matrix B has dimensions (2,3)") # 要素同士の掛け算(アダマール積)は .* でできる。 julia> A .* B 2×3 Matrix{Int64}: 7 16 27 36 40 42 # A / B == A * inv(B) であるはずなのだが・・・(invは逆行列の計算) julia> A / B 2×2 Matrix{Float64}: 0.625 -0.375 0.8125 -0.1875 # 要素同士の割り算 julia> A ./ B 2×3 Matrix{Float64}: 0.142857 0.25 0.333333 0.444444 0.625 0.857143 # 行列同士のべき乗は定義されていない julia> A ^ B ERROR: MethodError: no method matching ^(::Matrix{Int64}, ::Matrix{Int64}) # 要素同士のべき乗はできる。 julia> A .^ B 2×3 Matrix{Int64}: 1 256 19683 262144 390625 279936 Juliaでは要素同士の演算を.で強調できるのはNumpyと明確に異なる点であろう。 関数に対しても、Vectorと同様に.で要素同士の演算にできる。 ほぼ同じなのでここは省略します。 転置・トレース・行列式・逆行列 Numpyでは転置はndarray.Tで取得できた。 行列式などはnp.linalgモジュールを使えば良かった。 >>> A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) >>> A array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 転置 >>> A.T array([[1, 4, 7], [2, 5, 8], [3, 6, 9]]) # トレース >>> np.trace(A) 15 # 行列式 >>> np.linalg.det(A) 0.0 # 逆行列 >>> B = np.identity(3) >>> np.linalg.inv(B) array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) Juliaでは、転置は'或いはtranspose関数で可能。 またusing LinearAlgebraしておく。 julia> using LinearAlgebra julia> A = Matrix(3*I, 3, 3) 3×3 Matrix{Int64}: 3 0 0 0 3 0 0 0 3 # 転置 julia> A' 3×3 adjoint(::Matrix{Int64}) with eltype Int64: 3 0 0 0 3 0 0 0 3 # transpose関数 julia> transpose(A) 3×3 transpose(::Matrix{Int64}) with eltype Int64: 3 0 0 0 3 0 0 0 3 # tr関数でtraceを取得 julia> tr(A) 9 # 行列式もdet関数 julia> det(A) 27.0 # 逆行列はinv関数 julia> inv(A) 3×3 Matrix{Float64}: 0.333333 0.0 0.0 0.0 0.333333 0.0 0.0 0.0 0.333333 eltypeは恐らくelement typeの略です。 'とtransposeの結果の型が微妙に違う気がする・・・。 他にも便利な機能が沢山あるため、Linear Algebraを読むとよいだろう。 比較 Numpyでは1次元同様、各要素を比較したTrue, FalseのNumpy配列を返すのだった。 比較.py >>> A = np.array([[1, 2, 3], [4, 5, 6]]) >>> B = np.array([[3, 2, 1], [6, 5, 4]]) >>> A == B array([[False, True, False], [False, True, False]]) >>> A < B array([[ True, False, False], [ True, False, False]]) # サイズが違うと怒られるのではなく、単にFalseが返ってくる?エラー吐いてほしいな。 >>> C = np.ones((2, 2)) >>> A == C False Juliaでは、MatrixでもVectorと同じ挙動をする。 つまりNumpyのような要素同士の比較を行うには.==, .<などを用いる。 比較.jl julia> A = [1 2 3; 4 5 6]; julia> B = [3 2 1; 6 5 4]; julia> A .== B 2×3 BitMatrix: 0 1 0 0 1 0 julia> A .< B 2×3 BitMatrix: 1 0 0 1 0 0 # 単純な == では、all(A .== B)と同じと思われる。 julia> A == B false # all, anyもVectorの時とほぼ同様。 julia> any(A .== 2) true 最大最小 Numpyでもnp.max, np.minが使えた。 axisキーワードで軸に沿って最大最小を取得することもできた。 最大最小.py >>> A = np.array([[1, 2, 3], [4, 5, 6]]) # 行方向に関して最小値 >>> np.min(A, axis=1) array([1, 4]) # 列方向に関して最小値 >>> np.min(A, axis=0) array([1, 2, 3]) # 全要素で最小値 >>> np.min(A) 1 Juliaでは、minimum, maximum関数のdimsキーワード引数によって軸に沿って最小最大を取得できる。 Numpyライクで良いですね。 julia> A = [1 2 3; 4 5 6]; # dims=1は列方向。 julia> minimum(A, dims=1) 1×3 Matrix{Int64}: 1 2 3 # dims=2は行方向。 julia> minimum(A, dims=2) 2×1 Matrix{Int64}: 1 4 # 全体の最小値 julia> minimum(A) 1 # 最大値も同様である。 julia> maximum(A, dims=1) 1×3 Matrix{Int64}: 4 5 6 なおVectorの際min(a...)といった記法があったが、行列に対してはA...がエラーになり、不可能であった。 展開は遅いらしいことを考慮すれば、素直にminimum, maximumでよいだろう。 ソート Numpyのsortでもaxisキーワード引数で軸ごとにソートできた。 ソート.py >>> A = np.array([[5, 1, 6], [4, 8, 2]]) >>> A array([[5, 1, 6], [4, 8, 2]]) # axisを省略した場合はaxis=-1, 2次元ではaxis=1になる。 >>> np.sort(A) array([[1, 5, 6], [2, 4, 8]]) # axis=1, つまり行方向にソート >>> np.sort(A, axis=1) array([[1, 5, 6], [2, 4, 8]]) # axis=1, つまり行方向にソートして逆順(降順) >>> np.sort(A, axis=1)[:, ::-1] array([[6, 5, 1], [8, 4, 2]]) # axis=0, つまり列方向にソート >>> np.sort(A, axis=0) array([[4, 1, 2], [5, 8, 6]]) # axis=0, つまり列方向にソートして逆順(降順) >>> np.sort(A, axis=0)[::-1, :] array([[5, 8, 6], [4, 1, 2]]) JuliaではVector同様sort関数を使う。 というか最大最小もソートもdims引数以外Vectorと同じである。 julia> A = [5 1 6; 4 8 2] 2×3 Matrix{Int64}: 5 1 6 4 8 2 # Numpyと違い、行列の場合はdimsの指定が必須なようだ。 julia> sort(A) ERROR: UndefKeywordError: keyword argument dims not assigned # dims = 1, つまり行方向にソート julia> sort(A, dims=1) 2×3 Matrix{Int64}: 4 1 2 5 8 6 # 降順ソートは rev = trueで。 Numpyより簡単。 julia> sort(A, dims=1, rev=true) 2×3 Matrix{Int64}: 5 8 6 4 1 2 # dims = 2, つまり列方向にソート julia> sort(A, dims=2) 2×3 Matrix{Int64}: 1 5 6 2 4 8 # 列方向にソート(降順) julia> sort(A, dims=2, rev=true) 2×3 Matrix{Int64}: 6 5 1 8 4 2 また、Vectorの時同様byキーワードに関数を渡すことで、その返り値でソートできる。 なおVectorでも紹介したsortpermは行列では定義されていないらしい。 なんでだろう。 ソート2.jl julia> B = [-5 1 6; 4 -8 2] 2×3 Matrix{Int64}: -5 1 6 4 -8 2 # 二乗した値が大きい順に、列方向にソート julia> sort(B, dims=1, by=x->x^2) 2×3 Matrix{Int64}: 4 1 2 -5 -8 6 # sortpermは行列では定義されていないらしい。 julia> sortperm(A, dims=1) ERROR: MethodError: no method matching sortperm(::Matrix{Int64}; dims=1) 統計的な量 さすがにNumpy版の記述は飽きたので省略 Vector版のセクションと最大最小・ソートの際とほぼ同じ。 統計.jl julia> A = [5 1 6; 4 8 2] 2×3 Matrix{Int64}: 5 1 6 4 8 2 # 和:sum は dims 無指定では全成分の和 julia> sum(A) 26 # dims = 1, 列方向の和 julia> sum(A, dims=1) 1×3 Matrix{Int64}: 9 9 8 # dims = 2, 行方向の和 julia> sum(A,dims=2) 2×1 Matrix{Int64}: 12 14 # 平均:mean はdims無指定で全要素の平均 julia> mean(A) 4.333333333333333 # dims = 1, つまり列方向の平均 julia> mean(A, dims=1) 1×3 Matrix{Float64}: 4.5 4.5 4.0 # dims = 2, つまり行方向の平均 julia> mean(A, dims=2) 2×1 Matrix{Float64}: 4.0 4.666666666666667 # 標準偏差:std はdims無指定で全要素の標準偏差 julia> std(A) 2.581988897471611 # dims = 1, つまり列方向の標準偏差 julia> std(A, dims=1) 1×3 Matrix{Float64}: 0.707107 4.94975 2.82843 # dims = 2, つまり行方向の標準偏差 julia> std(A, dims=2) 2×1 Matrix{Float64}: 2.6457513110645907 3.055050463303893 ベクトルと行列の演算 ここではVectorとMatrix同士の演算のNumpyと比較しながら見ていく。 Vectorの転置と行列のかけ算 Numpyの1次元配列は、良くも悪くもベクトル。 >>> x = np.array([1, 2, 3]) >>> A = np.array([[1, 0, 0], [1, 0, 1], [0, 1 ,1]]) # xは転置できない shapeが3 * 1 ではないため。 >>> x.shape (3,) >>> x.T.shape (3,) >>> y = A.dot(x) >>> y array([1, 4, 5]) >>> y = x.dot(A) >>> y array([3, 3, 5]) 上記の計算結果は以下のように解釈できる。 A.dot(x) = \begin{bmatrix} 1 & 0 & 0 \\ 1 & 0 & 1 \\ 0 & 1 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 2 \\ 3 \\ \end{bmatrix} = \begin{bmatrix} 1 \\ 4 \\ 5 \\ \end{bmatrix} x.dot(A) = \begin{bmatrix} 1 & 2 & 3 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 1 & 0 & 1 \\ 0 & 1 & 1 \end{bmatrix} = \begin{bmatrix} 3 & 3 & 5 \end{bmatrix} いやまぁ・・・あのさぁ・・・ なお計算結果のy.shapeは両方同じで(3, )となる。 便利かも知れないが、少し気持ち悪い。勿論これを避けるには、reshapeで2次元配列として扱うようにすればよい。 一方でJuliaは基本的にVectorは縦ベクトルである。 julia> x = [1, 2, 3]; julia> A = [1 0 0; 1 0 1; 0 1 1] 3×3 Matrix{Int64}: 1 0 0 1 0 1 0 1 1 # JuliaではVectorも転置できる。なぜなら縦ベクトルだから。 julia> x' 1×3 adjoint(::Vector{Int64}) with eltype Int64: 1 2 3 # 想定通りの計算結果 julia> A * x 3-element Vector{Int64}: 1 4 5 # これはエラー。 julia> x * A ERROR: DimensionMismatch("matrix A has dimensions (3,1), matrix B has dimensions (3,3)") # こっちは転置したから問題ない。返り値もしっかり1 * 3になっている。 julia> x' * A 1×3 adjoint(::Vector{Int64}) with eltype Int64: 3 3 5 ベクトルと行列の演算では、あらかじめ縦ベクトルと割り切っているJuliaの方がミスを生み出しにくそうで好ましく感じた。 ブロードキャスト Numpyでは、演算する際に形状が異なっていた時、適切に形状を変化させて演算する特徴がある。 ブロードキャスト.py >>> A = np.zeros((3, 3)) >>> A array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]) >>> a = np.array([1, 2, 3]) # aが縦にコピーされる >>> A + a array([[1., 2., 3.], [1., 2., 3.], [1., 2., 3.]]) Juliaでもそのような機能はある。Numpyと異なり、ベクトルは列(縦)ベクトルであるため、挙動が一見すると異なる。でもそこだけ注意すれば、Numpyと同じように扱えるであろう。 julia> A = zeros(3, 3) 3×3 Matrix{Float64}: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 # カンマ区切りで宣言すると、これは縦ベクトル。 julia> a_vec = [1, 2, 3] 3-element Vector{Int64}: 1 2 3 # 縦ベクトルが横にコピーされる julia> A .+ a_vec 3×3 Matrix{Float64}: 1.0 1.0 1.0 2.0 2.0 2.0 3.0 3.0 3.0 # スペースまたはtab区切りで宣言するとこれは行列。 julia> a_mat = [1 2 3] 1×3 Matrix{Int64}: 1 2 3 # 縦にコピーされて足される julia> A .+ a_mat 3×3 Matrix{Float64}: 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 # ちなみに .+ じゃないとエラーになる。 julia> A + a ERROR: DimensionMismatch("dimensions must match: a has dims (Base.OneTo(3), Base.OneTo(3)), b has dims (Base.OneTo(1), Base.OneTo(3)), mismatch at 1") # 掛け算などでも同様。 julia> B = ones(3, 3) 3×3 Matrix{Float64}: 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 julia> B .* a 3×3 Matrix{Float64}: 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 ブロードキャストのルールはほとんどNumpyと同じ模様。 Numpyの便利ルールがそのまま使えるのはありがたい。 その他豆知識 Numpyとは関係ない、便利機能や補足のセクションです。 カンマ区切りやタブ・スペース区切りの違い ;区切りとスペース区切りは、ベクトル宣言と行列宣言の違いではないみたい。 ;区切り→垂直に結合 スペース区切り→水平に結合 という意味。この説明はv1.6のArrayの公式ドキュメントによれば If the arguments inside the square brackets are separated by semicolons (;) or newlines instead of commas, then their contents are vertically concatenated together instead of the arguments being used as elements themselves. 適当に和訳してみると、「[]の中で、セミコロン(;)または改行されているものは、垂直方向に結合されます。また仮にカンマ区切りであれば、そのまま要素として使われます。」 ふーん?以下のプログラム例が載っていた。 julia> [1:2, 4:5] # Has a comma, so no concatenation occurs. The ranges are themselves the elements 2-element Vector{UnitRange{Int64}}: 1:2 4:5 julia> [1:2; 4:5] 4-element Vector{Int64}: 1 2 4 5 julia> [1:2 4:5 6] 5-element Vector{Int64}: 1 2 4 5 6 Similarly, if the arguments are separated by tabs or spaces, then their contents are horizontally concatenated together. 「同様に、タブやスペース区切りの場合は、水平方向に結合されます。」 なるほどね(?)以下も公式ドキュメントより。 julia> [1:2 4:5 7:8] 2×3 Matrix{Int64}: 1 4 7 2 5 8 julia> [[1,2] [4,5] [7,8]] 2×3 Matrix{Int64}: 1 4 7 2 5 8 julia> [1 2 3] # Numbers can also be horizontally concatenated 1×3 Matrix{Int64}: 1 2 3 なるほど。 まとめ Python, Numpy使っていた方(特に自分)が、Juliaでの演算の違いの参考になったら幸いです。 文体がバラバラですね・・・本当は統一すべきですが、気が向いたら統一しておきます。 間違い等ありましたら、コメントよろしくお願いします。 参考資料等 Juliaで最低限やっていくための配列操作まとめ 演算以外の、配列操作に関する情報量と密度は随一かと。 逆引きJulia 配列要素の参照、色々 行列のアクセスに関して、かなりの数を網羅している。
- 投稿日:2021-08-15T23:02:35+09:00
仮想通貨の自動売買チュートリアル。環境構築
この記事では以下のチュートリアル実行に必要な環境構築を行います。 初心者向けの記事です。 上級者ならこの記事を読まなくても環境構築できると思います。 前提知識 プログラミングの基礎知識 コマンドラインを使える gitを少し知っている dockerを少し知っている 英語 (または自動翻訳、または日本語の情報を探せる) 開発環境の用意 OS OSはWindows, Mac, Linuxどれでも開発できます。 MacかLinuxだとコマンドラインが使いやすいです。 私はMacで開発しています。 エディタの用意 ソースコードを編集するためにエディタが必要です。 OSに標準でついているエディタでも一応できますが、効率が悪いです。 私はIntelliJ IDEAを買って使ってます。 Visual Studio CodeやSublimeなどは無料で使えるらしいです。 https://smartlife-weblog.com/programing/recommend-visual-studio-code.html コマンドライン環境の用意 gitやdockerを使うためにコマンドライン環境が必要です。 MacとLinuxは元から使えます。 Windowsはいくつか方法があるみたいです。 ぐぐってみてください。 でも、Macを買うのが早いと思います。 オープンソース系のツールはほとんどLinuxを想定して作られていて、 Macはギリギリ動く、Windowsはほとんど想定されていないような気がします。 なのでWindowsを使うとマイナーなツールを使ったときにトラブルに巻き込まれやすいです。 中古のMacは10万円(0.02BTC)以下で買えるみたいです。 https://www.pc-koubou.jp/pc/used_mac_book_pro.php 最近のMacはIntelのCPUではないMacがあるみたいです。 IntelのCPUでないと互換性のトラブルがあるかもしれません(例: https://fixel.co.jp/blog/docker-m1mac/)。 私はIntelのものを使っています。 ローカル環境構築 ローカル環境は自分のパソコンのことです。 本番でボットを動かすためのサーバーなどと区別するためにそう呼びます。 gitのインストール gitはバージョン管理ツールです。 プログラミングをやるなら必須のツールです。 この記事ではチュートリアルのソースコードをダウンロードするために使います。 ローカル環境にgitをインストールします。 gitのインストール方法 色々なインストール方法がありますが、 私はMacで以下のコマンドでインストールしました。 brew install git brewは色々なものをかんたんにインストールできるコマンドです。 brewは以下でインストールできます。 https://brew.sh/index_ja dockerのインストール dockerはかんたんにアプリを動かすためのツールです。 この記事ではJupyter(ブラウザ上でpythonソースコードを編集したり実行したりできるツール)を動かすために使います。 ローカル環境にdockerをインストールします。 dockerのインストール方法 私はMacで以下の方法でインストールしました。 Install Docker Desktop on Mac docker-composeのインストール docker-composeはdockerが使いやすくなるツールです。 ローカル環境にdocker-composeをインストールします。 docker-composeのインストール方法 Macの場合、Docker Desktop for Macをインストールしたら自動でインストールされていました。 チュートリアルリポジトリのクローン 以下のチュートリアルリポジトリをgit clone(ダウンロード)します。 https://github.com/richmanbtc/mlbot_tutorial 以下のコマンドでクローンできます。 git clone https://github.com/richmanbtc/mlbot_tutorial.git 実行すると、 コマンドを実行したディレクトリにmlbot_tutorialというディレクトリが作られて、中にリポジトリの内容がダウンロードされます。 Jupyterの起動 Jupyterはブラウザ上でpythonをインタラクティブに実行できるツールです。 ソースコードを少しずつ編集しながら試行錯誤できるので、 bot研究に向いています。 mlbot_tutorialディレクトリへ移動後、以下のコマンドでJupyterを起動できます。 docker-compose up -d ブラウザで http://localhost:8888 を開くとJupyterが開きます。 これでbot研究の環境が整いました。 Jupyterが起動する仕組みはmlbot_tutorial内のDockerfileとdocker-compose.ymlを見てください。 チュートリアルを開く Jupyter上でworkディレクトリ内のtutorial.ipynbを開いてください。 以下のリンクでも開けます。 http://localhost:8888/lab/tree/work/tutorial.ipynb ここからボット研究をします。
- 投稿日:2021-08-15T22:59:51+09:00
pythonで因果推論 ~データセット紹介~
はじめに 因果推論の実験に使えるデータセットをpythonでの読み込み方法とともにまとめた記事です。 想定している読者 主にRubin因果モデルに従った手法による因果推論を実装したい それぞれのデータセットの特徴を知りたい (pythonで)すぐに自分の環境でデータセットを読み込みたい 因果推論とデータセット データセット紹介の前に、少しだけ表題の解説を行います。基礎的な部分なので、読み飛ばしていただいて大丈夫です。 因果推論の手法の評価実験では、通常のテーブルデータを用いることは少なく、多くの場合、因果推論専用のデータセットによって手法の評価を行います。その理由は簡潔に言うと、「通常のデータには正解データが無いから」であり、これは「因果推論の根本問題」という問題と関連します。 因果推論の根本問題とは、ある介入を行った場合と行わなかった場合の両方の効果を同時には観測できないと言う問題です。例えば、薬の投与をした人からは、薬を投与した場合の効果しか観測されず、薬を投与しなかった場合の効果は観測されない、同様に、薬を投与しなかった人からは、薬を投与した場合の効果が観測されないという問題です。 この問題のため、通常は真の因果効果をデータから算出することはできないという問題点があります。そのため、通常のデータに因果推論の手法を適用しても、手法の精度が良いか分からず、手法間の比較を行うことができません。 以上の問題点から、因果推論の手法の性能評価実験では、「因果効果が分かる形」のデータセットが用いられます。 ここからは、(一部例外がありますが、)因果推論の手法の性能評価実験に使えるデータセットについて紹介していきます。 1. IHDPデータセット 概要 よく採用されているベンチマーク・データセット Infant Health and Development Programという、低出生体重児や未熟児を対象としたランダム化比較試験(RCT)を元に生成されたデータセット 変数 共変量:出生時の体重・頭囲、母親の年齢・教育・薬物・アルコールなどの25の変数 介入:「乳児が質の高い保育と専門家による家庭訪問を受けること」 効果:乳児の認知テストのスコア NPCIパッケージを用いて効果をシミュレーションすることができる 介入あり、なしの効果を同時に入手できる シミュレーションには複数の設定がある 読み込み ihdp.py df = pd.DataFrame() for i in range(1, 10): data = pd.read_csv('https://raw.githubusercontent.com/AMLab-Amsterdam/CEVAE/master/datasets/IHDP/csv/ihdp_npci_' + str(i) + '.csv', header=None) df = pd.concat([data, df]) cols = ["treatment", "y_factual", "y_cfactual", "mu0", "mu1"] + [i for i in range(25)] df.columns = cols df.index = range(len(df)) df treatmentは介入の有無 y_factualは観測された効果、y_cfactualは観測されなかった効果 mu0は介入なしの場合の効果の期待値、mu1は介入ありの場合の効果の期待値 2. Jobsデータセット 概要 Landoleの行った職業訓練の有無による収入への因果効果を確かめるための実験 論文リンク 変数 共変量:年齢、教育、民族、1974,75年の収入などの8つの変数 介入:職業訓練への参加の有無 効果:1978年の収入 Landoleらの行ったRCT(ランダム化比較実験)のデータと、職業訓練を受けていない人のRCTでないデータがある 前者を用いることで、職業訓練の因果効果を知ることができる 後者を前者と結合することで不均衡なデータを作れる 読み込み jobs.py # RCTでないデータ df_cps1 = pd.read_stata('https://users.nber.org/~rdehejia/data/cps_controls.dta') # RCTデータ df_nsw = pd.read_stata('https://users.nber.org/~rdehejia/data/nsw_dw.dta') df = pd.concat([df_nsw[df_nsw[z_column] == 1], df_cps1], ignore_index=True) df 2行目のcps_controlsの末尾に2, 3をつけることで、別のボリュームのデータを得られる cps_controls.dta:15992行 cps_controls2.dta:2369行 cps_controls3.dta:429行 詳細な説明はhttps://users.nber.org/~rdehejia/nswdata2.html 参照 3. TWINSデータセット 概要 1989年から1991年の間に米国で出生した双子のデータをもとに構築されたデータセット 変数 共変量:妊娠週数、妊娠中のケアの質、妊娠危険因子(貧血、アルコール使用、タバコ使用など)、居住地などの50の変数 介入:体重の重い方の双子 効果:1年後志望しているかどうか 介入を受けた場合(重い方の双子)の効果、受けなかった場合(軽い方の双子)の効果の両方が観測される このデータセットにおける因果効果は「双子の重い方と軽い方の死亡率の差」を示している 「介入をどちらかだけ割り当てる」という概念がないため、この割り当ては通常は自分で定義する 読み込み twins.py # 共変量のdf df_x = pd.read_csv('https://raw.githubusercontent.com/AMLab-Amsterdam/CEVAE/master/datasets/TWINS/twin_pairs_X_3years_samesex.csv', index_col=0) df_x.drop(['Unnamed: 0.1', 'infant_id_0', 'infant_id_1'], axis=1, inplace=True) # 効果のdf df_y = pd.read_csv('https://raw.githubusercontent.com/AMLab-Amsterdam/CEVAE/master/datasets/TWINS/twin_pairs_Y_3years_samesex.csv', index_col=0) # 結合 df = pd.concat([df_y, df_x], axis=1) # 割り当ての追加(自分で割り当てを定義する) # 今回は、ランダム割り当て(本来は、共変量から割り当てを決定する関数を作成する必要あり) df['treat'] = np.random.randint(0, 2, (len(df), 1)) df 欠損値の処理をする必要あり mort_0が非介入の場合の死亡、mort_1が介入の場合の死亡 pd.read_csv('https://raw.githubusercontent.com/AMLab-Amsterdam/CEVAE/master/datasets/TWINS/twin_pairs_T_3years_samesex.csv', index_col=0)とすることで、体重に関する情報を得ることができる 岩波データサイエンス 概要 『岩波データサイエンスvol.3』で取り上げられているCM接触のアプリゲームプレイ因果効果についてのデータセット 変数 共変量:居住地、年齢、性別、職業などの31個の変数 介入:(アプリゲームの)cmを見たかどうか 効果:アプリゲームのプレイ時間 注意:このデータセットは、正解データがあるわけではないので、手法の評価を行うのは難しい 読み込み iwanami.py df = pd.read_csv('https://raw.githubusercontent.com/iwanami-datascience/vol3/master/kato%26hoshino/q_data_x.csv') df 詳しい説明はサポートページ参照 (注)このデータセットは、両方の効果を観測できているわけではないので手法の評価に使えるわけではない おわりに 本記事では、因果推論の実験に使えるデータセットをまとめました。 参考文献
- 投稿日:2021-08-15T22:11:41+09:00
ゲーミングPCで食い下がる科学技術計算
コードの中身はどうでもいいからGPUでどれくらい速くなるか知りたいという方は下までスキップして下さい. 環境 ツクモで17万くらいで購入したゲーミングPCを使います. OS: Windows 10 Home CPU: Intel Core i9-9900KF RAM: 16.0 GB GPU: GeForce RTX 2070 SUPER VRAM: 8.0 GB Python 3.8.2 numpy 1.19.1 scipy 1.5.2 PyTorch 1.9.0+cu111 CUDA 11.1 目的 scipy.sparseだと計算時間がかかりすぎて効率が悪かったのでGPUが使えるPyTorchで高速化したかった. GPU環境の(再)構築 久々に使おうとしたらつまづいたので簡単にメモ.初めて導入するのであればVisual Stadioが必要なのでこちらも参考. torch.cuda.is_available()がTrueなのを確認. Falseなら環境の(再)構築が必要.以前に構築済みでダメになっているのならドライバの更新時にCUDAとバージョンが合わなくなったと思われる. nvcc -VでCUDAのバージョンを確認する.Pytorchの公式インストールガイドのバージョンと一致するか確かめる. 不一致の場合,念のために古いCUDAとcuDNNを削除しておく.コントロールパネルから可能. 必要なバージョンのCUDAをNVIDIA公式から取ってくる.安易に取ってくると最新版となりPyTorchと合わなかったのでアーカイブから選ぶこと. CUDAに合わせてcuDNNをダウンロード.こちらもアーカイブから選ぶ. cuDNNをパスが通ってる場所に置く. PyTorchインストールガイドのコマンドをコピペして実行.最初の'torch.cuda.is_available()`が'True'になることを確認. 問題設定 以前投稿したように2次元格子点をバネで繋いだ時の運動をルンゲクッタ法で解きます.密行列で計算すると一辺の原子数$N$に対して$O \left( N^4 \right)$の要素数になるはずです.疎行列で$O \left( N^2 \right)$のはず.分子の速度(というか平均運動エネルギー)で温度を議論するにはボルツマン分布を再現できている必要があるので原子数はなるべく増やしたいですが,この通りちょっと原子数を増やすと計算量があっといまに膨れ上がってしまいます.こうなると時間はかかるしメモリに乗り切らないしで困りました.(原子数個の速度で温度を議論しているような文献には身構えて下さい.分布の幅は結構広いです.「その速度は違う温度でも〇%の確率で得られますが,その議論は本当に正しいのですか?」と言えるとかっこいいですね.) さて計算速度を向上させる方法は主に2つあります.1つ目がより高速なマシンを使うこと,2つ目が計算量を減らすことですね.メモリ問題にしたって同じです.アプローチをハードとソフトのどちらからかけるかです.今回はハード面はGPUの活用,ソフト面は疎行列の活用です.・・・・・・と言いながらソフト面は以前の投稿でも既に実行済みなので,改めて密行列の場合も計算してみて比較します. なお残念ながらGPUを活用した場合に疎行列がうまく扱えなかったため,そちらを探されている方は諦めて下さい.これはPyTorchにしてもTensorFlowにしても同じようで,スパーステンソルはbroadcastが定義されていなかったり,通常のテンソルとのアダマール積が定義されていなかったためです.従ってタイトルの通りゲーミングPCを使う場合は簡単にVRAMが足りなくなります.行列積は使わないのでブロックごとにGPUに投げればいいだけかもしれませんが試してません. 解法 まずCPUで密行列を計算するコードです.長くなるのでコメントは全消しです.説明が必要であれば以前の投稿を確認願います. cpu_dense.py from pathlib import Path import numpy as np from matplotlib import pyplot as plt class RungeKutta(object): def __init__(self, dt, grad): self.dt = dt self.grad = grad def calc_k(self, t, y, **kwargs): k1 = self.grad(t, y, **kwargs) k2 = self.grad(t + self.dt/2, y + self.dt/2*k1, **kwargs) k3 = self.grad(t + self.dt/2, y + self.dt/2*k2, **kwargs) k4 = self.grad(t + self.dt, y + self.dt*k3, **kwargs) return k1, k2, k3, k4 def calc(self, t, y, **kwargs): k1, k2, k3, k4 = self.calc_k(t, y, **kwargs) y_new = y + self.dt/6*(k1 + 2*k2 + 2*k3 + k4) t_new = t + self.dt return t_new, y_new def hooke(t, v, **kwargs): X = kwargs['X'] n = X.shape[0] R = [] r = 0 for i in range(n): xi = kwargs['C']*(X[i].reshape(-1,1)) xj = kwargs['C']*(X[i].reshape(1,-1)) R.append(xi-xj) #r += (xi-xj).power(2) r += (xi-xj)**2 L = kwargs['C']*(kwargs['L']) r = np.sqrt(r) f = kwargs['K']*(r-L) F = [] for i in range(n): F.append( np.nan_to_num(f*(R[i])/r) .sum(axis=-1) .reshape(X[i].shape) /kwargs['M'] ) F = -np.array(F) return F def velosity(t, x, **kwargs): return kwargs['V'] def boundary(t, phi, a, k, m): omega = np.sqrt(k/m) x = np.cos(omega*t + phi)*a*np.array([[0.5, 1]]) vx = -omega*np.sin(omega*t + phi)*a*np.array([[0.5, 1]]) return x, vx def main(Nx=5, Ny=3, dt=1.e-16, upto=1.e-12, k=1.e3, l=1.e-10, m=1.e-25, a=3.e-12): t = 0 I = np.ones(Nx*Ny).reshape(Ny,Nx) E = (np.eye(Nx*Ny)) Iu = I.copy() Iu[0,:]=0 Iu = Iu.reshape(-1,1) Il = I.copy() Il[:,0] = 0 Il = Il.reshape(-1,1) Ir = I.copy() Ir[:,-1] = 0 Ir = Ir.reshape(-1,1) Id = I.copy() Id[-1,:] = 0 Id = Id.reshape(-1,1) C = (np.roll(E*Iu, -Nx, axis=1) + np.roll(E*Il, -1, axis=1) + np.roll(E*Ir, 1, axis=1) + np.roll(E*Id, Nx, axis=1) ) K = C*k L = C*l M = np.ones(Nx*Ny).reshape(Ny,Nx)*m x = np.arange(1,Nx+1)*l y = np.arange(Ny,0,-1)*l x, y = np.meshgrid(x,y) xb0 = x.copy() yb0 = y.copy() dx = np.random.randn(Ny,Nx)*l/300 dy = np.random.randn(Ny,Nx)*l/300 dx[:,0] = 0 dx[:,-1] = 0 x = x + dx y = y + dy phi = np.random.rand(Ny*2).reshape(Ny, 2)*2*np.pi xb, vb = boundary(0, phi, a, k, m) x[:,0] += xb[:,0] x[:, -1] += xb[:,1] x = np.stack([x, y]) v = np.zeros_like(x) v[0,:,0] += vb[:,0] v[0,:,-1] += vb[:,1] V = RungeKutta(dt, hooke) X = RungeKutta(dt, velosity) folder = Path( f'dense_k{k}_l{l}_m{m}_a{a}_dt{dt}' ) folder.mkdir(exist_ok=True) wd = folder/f'Nx{Nx}_Ny{Ny}_time{upto}' wd.mkdir(exist_ok=True) res_t = open(wd/f'time.txt', 'w') res_vx = open(wd/f'velosity_x.txt', 'w') res_vy = open(wd/f'velosity_y.txt', 'w') res_x = open(wd/f'x.txt', 'w') res_y = open(wd/f'y.txt', 'w') fig, ax = plt.subplots() ax.scatter(x[0], x[1], c='k') fig.savefig(wd/'Init.png', bbox_inches='tight') np.savetxt(res_t, np.array([t])) np.savetxt(res_vx, v[0]) np.savetxt(res_vy, v[1]) np.savetxt(res_x, x[0]) np.savetxt(res_y, x[1]) for txt in [res_t, res_vx, res_vy, res_x, res_y]: txt.write('\n') while t<=upto: t_new, v_new = V.calc( t, v, X=x, K=K, L=L, M=M, C=C ) xb ,vb = boundary(t_new, phi, a, k, m) xb[:,0] += xb0[:,0] xb[:,-1] += xb0[:, -1] v_new[0, :, 0] = vb[:,0] v_new[0, :, -1] = vb[:,1] v_new[1, :, 0] = 0 v_new[1, :, -1] = 0 t_new, x_new = X.calc(t, x, V=v) x_new[0, :, 0] = xb[:,0] x_new[0, :, -1] = xb[:,1] x_new[1, :, 0] = yb0[:, 0] x_new[1, :, -1] = yb0[:, -1] t = t_new v = v_new x = x_new np.savetxt(res_t, np.array([t])) np.savetxt(res_vx, v[0]) np.savetxt(res_vy, v[1]) np.savetxt(res_x, x[0]) np.savetxt(res_y, x[1]) for txt in [res_t, res_vx, res_vy, res_x, res_y]: txt.write('\n') res_t.close() res_vx.close() res_vy.close() res_x.close() res_y.close() return wd 続いてCPU上で疎行列を使うコードです.変更があるhooke関数は下記の通りです.main関数はnp.rollを使ってCを定義した後にC = csr_matrix(C)を追加してあります.というか以前投稿した記事のコードはこっちです. cpu_sparse.py from scipy.sparse import csr_matrix def hooke(t, v, **kwargs): X = kwargs['X'] n = X.shape[0] R = []#scipy.sparse CANNOT handle tensor. r = 0 for i in range(n): xi = kwargs['C'].multiply(X[i].reshape(-1,1)) xj = kwargs['C'].multiply(X[i].reshape(1,-1)) R.append(xi-xj) r += (xi-xj).power(2) r = r.sqrt() f = kwargs['K'].multiply(r-kwargs['L']) F = [] for i in range(n): F.append( np.nan_to_num(f.multiply(R[i])/r) .sum(axis=-1) .reshape(X[i].shape) /kwargs['M'] ) F = -np.array(F) return F 最後にPyTorchでGPUを使うコードです.なんとかしてスパーステンソルを使おうとしましたが諦めました.普通のテンソルであればbroadcastは問題なく機能してくれます. class RungeKuttaはやはり変更なしなので省略です.ほとんどnumpy-likeに書けるので楽ですが,随所にGPUを使うための記述と倍精度を指定するための記述があります.torch.tensorの実数は単精度が標準になっていますが,numpyから変換した場合は倍精度になります.最大の注意点はnp.meshgridはindexing=xyが基本になってますが,torch.meshgridではindexing=ijでしか出力できないことです(パラメータが存在しません). gpu_dense.py import torch def hooke(t, v, **kwargs): X = kwargs['X'] n = X.shape[0] xi = kwargs['C'].multiply( X.reshape(2,kwargs['C'].shape[0],1) ) xj = kwargs['C'].multiply( X.reshape(2,1,kwargs['C'].shape[0]) ) R = xi-xj del xi, xj r = R**2 r = r.sqrt() f = kwargs['k']*kwargs['C']*(r-kwargs['l']) F = -(torch.nan_to_num(f*R/r) .sum(axis=-1) .reshape(X.shape) /kwargs['m'] ) return F def velosity(t, x, **kwargs): return kwargs['V'] def boundary(t, phi, a, k, m): device = torch.device( 'cuda:0' if torch.cuda.is_available() else 'cpu' ) omega = np.sqrt(k/m) x = (torch.cos(omega*t + phi)* a*torch.tensor([0.2, 1],device=device) ) vx = (-omega*torch.sin(omega*t + phi)* a*torch.tensor([0.2, 1],device=device) ) return x, vx def main(Nx=5, Ny=3, dt=1.e-16, upto=1.e-12, k=1.e3, l=1.e-10, m=1.e-25, a=3.e-12): device = torch.device( 'cuda:0' if torch.cuda.is_available() else 'cpu' ) print('device:', device) t = 0 E = torch.eye(Ny*Nx, device=device, dtype=torch.float64) Iu = torch.ones( Ny*Nx, device=device, dtype=torch.float64 ).reshape(Ny,Nx) Iu[0,:]=0 Iu = Iu.reshape(-1,1) Il = torch.ones( Ny*Nx, device=device, dtype=torch.float64 ).reshape(Ny,Nx) Il[:,0] = 0 Il = Il.reshape(-1,1) Ir = torch.ones( Ny*Nx, device=device, dtype=torch.float64 ).reshape(Ny,Nx) Ir[:,-1] = 0 Ir = Ir.reshape(-1,1) Id = torch.ones( Ny*Nx, device=device, dtype=torch.float64 ).reshape(Ny,Nx) Id[-1,:] = 0 Id = Id.reshape(-1,1) C = (torch.roll(E*Iu, -Nx, dims=1) + torch.roll(E*Il, -1, dims=1) + torch.roll(E*Ir, 1, dims=1) + torch.roll(E*Id, Nx, dims=1) ) #C = torch.stack([C for i in range(len(X.shape[0]))]).to_sparse() x = np.arange(0,Nx)*l y = np.arange(Ny-1,-1,-1)*l #unlike np.meshgrid, torch.meshgrid afford tensor as indexing='ij' only x, y = np.meshgrid(x, y, indexing='xy') x = torch.from_numpy(x).to(device) y = torch.from_numpy(y).to(device) xb0 = torch.arange(0,Nx,device=device)*l yb0 = torch.arange(Ny-1,-1,-1,device=device)*l dx = torch.randn(Ny,Nx,device=device)*l/10 dy = torch.randn(Ny,Nx,device=device)*l/10 dx[:,0] = 0 dx[:,-1] = 0 x = x + dx y = y + dy phi = torch.rand( Ny*2,device=device ).reshape(Ny, 2)*2*np.pi xb, vb = boundary(0, phi, a, k, m) x[:,0] += xb[:,0] x[:, -1] += xb[:,1] x = torch.stack([x, y]) v = torch.zeros_like(x) v[0,:,0] += vb[:,0] v[0,:,-1] += vb[:,1] V = RungeKutta(dt, hooke) X = RungeKutta(dt, velosity) folder = Path( f'pytorch_k{k}_l{l}_m{m}_a{a}_dt{dt}' ) folder.mkdir(exist_ok=True) wd = folder/f'Nx{Nx}_Ny{Ny}_time{upto}' wd.mkdir(exist_ok=True) res_t = open(wd/f'time.txt', 'w') res_vx = open(wd/f'velosity_x.txt', 'w') res_vy = open(wd/f'velosity_y.txt', 'w') res_x = open(wd/f'x.txt', 'w') res_y = open(wd/f'y.txt', 'w') fig, ax = plt.subplots() ax.scatter(x[0].to('cpu'), x[1].to('cpu'), c='k') fig.savefig(wd/'Init.png', bbox_inches='tight') np.savetxt(res_t, np.array([t])) np.savetxt(res_vx, v[0].to('cpu').numpy()) np.savetxt(res_vy, v[1].to('cpu').numpy()) np.savetxt(res_x, x[0].to('cpu').numpy()) np.savetxt(res_y, x[1].to('cpu').numpy()) for txt in [res_t, res_vx, res_vy, res_x, res_y]: txt.write('\n') while t<=upto: t_new, v_new = V.calc( t, v, X=x, k=m, l=l, m=m, C=C ) xb ,vb = boundary(t_new, phi, a, k, m) xb[:,0] += xb0[0] xb[:,-1] += xb0[-1] v_new[0, :, 0] = vb[:,0] v_new[0, :, -1] = vb[:,1] v_new[1, :, 0] = 0 v_new[1, :, -1] = 0 t_new, x_new = X.calc(t, x, V=v) x_new[0, :, 0] = xb[:,0] x_new[0, :, -1] = xb[:,1] x_new[1, :, 0] = yb0 x_new[1, :, -1] = yb0 t = t_new v = v_new x = x_new np.savetxt(res_t, np.array([t])) np.savetxt(res_vx, v[0].to('cpu').numpy()) np.savetxt(res_vy, v[1].to('cpu').numpy()) np.savetxt(res_x, x[0].to('cpu').numpy()) np.savetxt(res_y, x[1].to('cpu').numpy()) for txt in [res_t, res_vx, res_vy, res_x, res_y]: txt.write('\n') res_t.close() res_vx.close() res_vy.close() res_x.close() res_y.close() return wd あとはこいつらに縦横の原子数を順番に投げて計算時間を比較します. compare.py import time import cpu_dense import cpu_sparse import gpu_dense dt = 1.e-16 upto = 1.e-13 k = 1.e2 l = 1.e-10 m = 3.e-25 a = l*0.1 Nx = [5, 50, 50, 100, 100] Ny = [3, 3, 30, 30, 50] for nx, ny in zip(Nx, Ny): for main in [cpu_dense, cpu_sparse, gpu_dense]: start = time.perf_counter() wd = main.main(nx, ny, dt, upto, k, l, m, a) end = time.perf_counter() with open(wd/f'{end - start}.txt', 'w') as f: f.write(f'it took {end - start}.') そして計算時間を比較したグラフが下です.横軸が総原子数,縦軸は計算にかかった時間です.ゲーミングPCだけでなく手持ちのMac MiniでもCPU計算をさせてみました. GPUの圧勝.CPUだと2時間以上かかりそうなN=8000の条件でも6分で終わりました.インテル終わってる.CPU上での疎行列化も計算時間を半減させる琴葉できてますね.numpy-likeに簡単に扱えるので遊びで物理計算したい程度ならゲーミングPCのGPUでガンガン試せそうです.スパーステンソルが扱えないので変な工夫とか要らないのもメリットに思えてきます. じゃあ何でもGPUでいいかと言うと世の中そんなに甘くないです.グラフが途中で切れてるので察しがつくかと思いますが,GPUはVRAMの限界がN=8000で来てしまいました.2次元格子点の距離を保持させたテンソルの要素数は$2\times8000^2$です.これ以上のサイズで計算したければメモリに余裕のあるCPUを使わざるをえません.残念. 結論 VRAMが溢れない限りはスパース化できてなくてもGPU使った方が絶対にいい.マジで泣きながらCPU使いましょう. Continue...? ブロック化してバッチ処理するかも
- 投稿日:2021-08-15T21:57:38+09:00
機械学習、Pythonに関する個人的Tips
(yyyy.mm.dd追記) はじめに 機械学習絡みでPythonのことでよく検索することを個人的な備忘録がてら残します。 「前に検索したなこれ・・・」と思ったものは逐一追加していきます。 Pandas関連 表示行、列を変えたい import pandas as pd # 現在の最大表示列数 pd.get_option("display.max_columns") # 最大表示列数を10列に指定 pd.set_option('display.max_columns', 10) # 最大表示列数を無制限に pd.set_option('display.max_columns', None) # 現在の最大表示行数 pd.get_option("display.max_rows") # 最大表示行数を10行に指定 pd.set_option('display.max_rows', 10) # 最大表示行数を無制限に pd.set_option('display.max_rows', None) その他
- 投稿日:2021-08-15T20:56:24+09:00
~ 再帰 ~ チートシート
目次 再帰の例 注意点 はじめに チートシートの扱いついてはここを読んでください 再帰の例 recursion.py def sum(num): if num == 1: return 1 else: return sum(num-1)+num ans.py print(sum(100)) >>> 5050 1からnumまでの整数の和を求める関数 関数の中でその関数自身を呼び出すことで、ループして計算することができる 注意点 ans.py print(sum(10000)) >>> Traceback (most recent call last): >>> File "./Main.py", line 6, in <module> >>> print(sum(10000)) >>> File "./Main.py", line 5, in sum >>> return sum(num-1)+num >>> File "./Main.py", line 5, in sum >>> return sum(num-1)+num >>> File "./Main.py", line 5, in sum >>> return sum(num-1)+num >>> [Previous line repeated 1887 more times] >>> File "./Main.py", line 2, in sum >>> if num == 1: >>> RecursionError: maximum recursion depth exceeded 再帰の上限回数が決まっており、それを超えるとエラーを吐く(デフォルトでは1000回まで) 上限回数以上の再帰を行いたい場合は、以下のおまじないが必要 recursion.py import sys sys.setrecursionlimit(max_num) # 設定する上限回数をmax_numに入れる しかし再帰の回数が多くなると計算時間が劇的に大きくなるので、そもそも再起を使わず普通にfor文とかで同じ計算をした方がよい
- 投稿日:2021-08-15T20:42:09+09:00
Pythonではじめる機械学習 part2
前回の内容 Pythonではじめる機械学習 part1 *間違いや指摘があれば、ぜひコメント下さい! 1章(後半) 概要 主に機械学習に用いられるライブラリ群の紹介 scikit-learn さまざまな機械学習用のライブラリが入ったパッケージ 参考書では、基本的にscikit-learnに梱包されたアルゴリズムを題材に学習を進めていく仕組みとなっている。 何か一つを指すと言うよりも、機械学習ライブラリの集合という認識 Numpy 科学技術計算用のライブラリ 多次元配列や、線形代数、フーリエ変換など高度な数学式をプログラムから実行したいときに利用される。 scikit-learnのI/FはNumpy配列が前提になっている。 matplotlib 科学技術計算向けのグラフ描画ライブラリ テストデータや解析結果を確認する際に、これを利用するとグラフィカルに表現してくれる。 初めて実行すると結構感動する。 pandas 解析を行う際に、対象データをDataFrameというオブジェクトに落とし込んで表現することを可能にするライブラリ 生成したデータは、SQLライクにアクセスできるほか、各種フォーマット(.csv, .xslx)からDataFrameを作成するためのI/Fも提供している。 基本的にライブラリ群の紹介なので、今回所感は割愛します! 次回から、機械学習らしい内容に入っていきます。
- 投稿日:2021-08-15T20:29:35+09:00
numpyで二重の重ね合わせ処理をする ~透視メガネを作る~
はじめに 透視メガネは男のロマン。ドラえもんを思い浮かべるかTo LOVEるを連想するかインチキ通販の苦い思い出を蘇らせるかは人によって異なる。 これをPythonで実装してみよう。といってもディープラーニングを駆使して着衣の奥にある見えない裸体をAIに生成させるわけではなく、あらかじめ脱衣画像を用意しておく必要があるわけだが。空間を切り裂くプログラムとほぼ同内容だ。 多くのテレビゲームには登場キャラクターの立ち絵がある。恋愛要素のあるアドベンチャーゲームなどでは制服や私服や水着で同一のポーズの画像が用意されていることも少なくない。空間を切り裂くプログラムではそういうのを使ってアウトプットを作りたかったのだが、残念なことにそのようなゲームを持っていなかった。当時は。 しかし今は違う。今の私はかわいい子をひん剥くことができる。 かわいい子の名前はリンク。そう、ブレスオブザワイルドの。去年の12月にようやくSwitchを買ったのよね。 着衣 linkA.jpg 脱衣 linkB.jpg ソース 透視を実装するにはスプライトの処理を2回おこなうのとは別の、二重の重ね合わせが必要だ。 それを実装するにあたりこれまでスプライトの勉強でこだわってきたマスク処理でなくnumpy.where()を使うことにした。これならば複数の透明色を設定できる。 ちなみに特撮ではブルーとグリーンのいる戦隊ヒーローは以下のような苦労をしているそうな。 透視メガネの画像は自分で作った。アニメアニメした画像で同様のことをする際は別の色にしたほうがよいが、その際は注意が必要だ(後述)。 glass.png sukesuke.py import cv2 import numpy as np def put_sprite(event, x, y, flags, param): if event == cv2.EVENT_MOUSEMOVE: img = sukesuke(imgA, imgB, glass, (x, y), home=(130,130)) cv2.imshow(winname, img) def sukesuke(img1, img2, glass, pos, home=(0,0)): result = img1.copy() imgH, imgW = result.shape[:2] gH, gW = glass.shape[:2] x, y = pos xc, yc = home x, y = x-xc, y-yc x1, y1 = max(x, 0), max(y, 0) x2, y2 = min(x+gW, imgW), min(y+gH, imgH) if not ((-gW < x < imgW) and (-gH < y < imgH)): return img1 outer_color = (255,255,255) inner_color = (0,0,0) img1_roi = img1[y1:y2, x1:x2] img2_roi = img2[y1:y2, x1:x2] glass_roi = glass[y1-y:y2-y, x1-x:x2-x] tmp = np.where(glass_roi==outer_color, img1_roi, glass_roi) tmp = np.where(glass_roi==inner_color, img2_roi, tmp) result[y1:y2, x1:x2] = tmp return result imgA = cv2.imread("linkA.jpg") imgB = cv2.imread("linkB.jpg") glass = cv2.imread("glass.png") winname = "sukesuke" cv2.namedWindow(winname) cv2.setMouseCallback(winname, put_sprite) cv2.imshow(winname, imgA) while True: if cv2.waitKey(1) & 0xFF == 27: # esc break cv2.destroyAllWindows() これにより以下のようにじっくりねっとりと透視メガネを楽しむことができる。 結果 疑問 ところが、透視メガネの色を変更させると期待通りに動かなくなってしまうことがある。もちろん最新のnumpy==1.21.1での話だ。 これは一体どういうことだろう(まさかouter_colorやinner_colorの変更を間違えているということはないと信じたい)。 sukesuke.pyの一部を変更 def sukesuke(img1, img2, glass, pos, home=(0,0)): # 中略 outer_color = (255, 0, 0) # 必要に応じて色指定を変更する inner_color = (0, 0, 255) # 必要に応じて色指定を変更する # 中略 tmp = np.where(glass_roi==outer_color, img1_roi, glass_roi) cv2.imshow("tmp1", tmp) # 追加する tmp = np.where(glass_roi==inner_color, img2_roi, tmp) cv2.imshow("tmp2", tmp) # 追加する result[y1:y2, x1:x2] = tmp return result glass tmp1(途中の姿) tmp2(最終の姿) 背景=(255,255,255)レンズ=(0,0,0)縁=(208,99,244) 背景:正しく着衣レンズ:正しい色縁:正しい色 背景:正しく着衣レンズ:正しく裸縁:正しい色 背景=(255,255,255)レンズ=(0,0,0)縁=(0,255,0) 背景:正しく着衣レンズ:正しい色縁:合成 背景:正しく着衣レンズ:正しく裸縁:合成 背景=(255,0,0)レンズ=(0,0,255)縁=(255,255,255) 背景:正しく着衣レンズ:合成縁:合成 背景:合成レンズ:正しく裸縁:合成 リンクきゅんの股間ばかり注視してしまいたいへん恐縮だが、この辺りがもっとも違いが分かりやすいので勘弁してほしい。 numpy.where(condition, x, y)はconditionによってxもしくはyのどちらかを返すという関数のはずなのだが、なぜかxとyの演算が発生してしまっているようだ。 本件について知見のある方はぜひ教えてください。 終わりに 世間一般から4年以上遅れているが、女装リンク、いい…。
- 投稿日:2021-08-15T20:10:48+09:00
教科書に載っていないPython定数倍高速化Tips
0. はじめに みなさん、こんにちは。競技プログラミングを楽しんでいますか? 私は主にAtCoderで遊んでいて、使用言語はPythonです。 Pythonといえば、近年、機械学習関連の機運が高まったことにより注目されている言語であり、AtCoderでも競技プログラミングの主要言語であるC++に次いで人気の高い言語となっています。 しかし、一方でPythonはインタープリタ言語であり、「実行速度の遅い言語の代表格」として語られることも多いです。 競技プログラミングでは問題毎に決められた実行時間制限を満たすために、計算量がオーダーレベルで改善される効率的なアルゴリズムを考えることが主なタスクとなることが多いですが、Pythonでは良いアルゴリズムを考えても、その書き方が AC (正解)と TLE (実行時間超過) の分かれ目となる場合もあります。いわゆる定数倍高速化もPythonで競技プログラミングをやる場合には重要になります。そのため、インターネットで検索するとPythonの定数倍高速化について書かれた記事もいくつか出てきます。 実例から学ぶ Python競技プログラミングの定数倍高速化シリーズ1:徒競走 競技プログラミングにおけるPython定数倍高速化Tips Pythonで競プロをしよう!〜入門者が知っておくべきTips〜 本記事では、数あるPython高速化Tips記事にも登場しないであろう定数倍高速化を紹介します。 本記事を読んで高速化テクニックを身につけ、「定数倍でTLE」とはおさらばしましょう!! ※注意 この記事はネタ記事です。本記事を読んで得られる情報が全くないということはないとは思いますが、「定数倍でTLEをしないために高速化テクニックを身につけたい」という目的を達成するためにはあまり役に立たないかもしれません。このような目的のためには上で紹介したような記事を読むことをお勧めします。 本記事での実行時間の計測にはAtCoderのコードテストPython(3.8.2)を使用しています。 1. 値のswap, rotate 1つ目は、値のswap, rotateです。 多くのプログラミング言語では、2つの変数の値を交換(swap)するためには、一時的な変数を用意しそこに片方の値を保存するという手続きを記述することになります。 a = 1 b = 2 print(a, b) # 1 2 # swap tmp = a a = b b = tmp print(a, b) # 2 1 しかし、Pythonでは一時的な変数を用意することなくswapを記述できます。 a = 1 b = 2 print(a, b) # 1 2 # swap a, b = b, a print(a, b) # 2 1 このような変数の値の交換は2つの変数だけではなく、3つ以上の変数についても同様に行うことができます。 a = 1 b = 2 c = 3 d = 4 e = 5 print(a, b, c, d, e) # 1 2 3 4 5 # 値の交換 a, b, c, d, e = c, e, d, a, b print(a, b, c, d, e) # 3 5 4 1 2 便利ですね! ですが、ここに落とし穴があります。 変数の数が 2 ~ 4 個の場合について一時変数tmpを使う場合と使わない場合で実行時間を計測しました。 例えば変数が4個の場合のコードは以下の通りです。 def main(): a, b, c, d = 0, 1, 2, 3 for _ in range(10**7): # tmpなし a, b, c, d = b, c, d, a # tmpあり #tmp = a #a, b, c = b, c, d #d = tmp if __name__ == '__main__': main() 結果は次の表のようになりました。 変数の数 tmpなし tmpあり 2 $255 \;\rm{ms}$ $294 \;\rm{ms}$ 3 $311 \;\rm{ms}$ $334 \;\rm{ms}$ 4 $534 \;\rm{ms}$ $376 \;\rm{ms}$ 変数が2個、3個の時にはtmpを使わない方が速く、4個の時にはtmpを使った方が速くなりました。 なぜこのようなことが起こるのでしょうか?これを解明するためにPython標準のdisモジュールを使ってPython内部の動きを見てみます。 まずは、2変数の場合です。以下のコードを実行し、tmpなしでswapする場合の内部の挙動を可視化します。 import dis def f(a, b): a, b = b, a dis.dis(f) これを実行すると以下のようになると思います。 4 0 LOAD_FAST 1 (b) 2 LOAD_FAST 0 (a) 4 ROT_TWO 6 STORE_FAST 0 (a) 8 STORE_FAST 1 (b) 10 LOAD_CONST 0 (None) 12 RETURN_VALUE 注目するのは3行目で、ROT_TWOという操作が行われています。2変数の場合に一時変数tmpを使用しない方が高速だったのはROT_TWOという、2変数のswapに適した命令コードが存在したからです。 次に3変数の場合もみていきましょう。先ほどと同様に以下のコードを実行します。 import dis def f(a, b, c): a, b, c = b, c, a dis.dis(f) 結果は次のようになります。 4 0 LOAD_FAST 1 (b) 2 LOAD_FAST 2 (c) 4 LOAD_FAST 0 (a) 6 ROT_THREE 8 ROT_TWO 10 STORE_FAST 0 (a) 12 STORE_FAST 1 (b) 14 STORE_FAST 2 (c) 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 4、5行目でROT_THREE、ROT_TWOという操作が行われています。3変数の場合にも値の交換に適した命令コードが存在するので一時変数tmpを用いない方が高速に実行されたわけです。 では、4変数の場合はどうでしょうか。 以下のコードを実行します。 import dis def f(a, b, c, d): a, b, c, d = b, c, d, a dis.dis(f) 結果は次のようになります。 4 0 LOAD_FAST 1 (b) 2 LOAD_FAST 2 (c) 4 LOAD_FAST 3 (d) 6 LOAD_FAST 0 (a) 8 BUILD_TUPLE 4 10 UNPACK_SEQUENCE 4 12 STORE_FAST 0 (a) 14 STORE_FAST 1 (b) 16 STORE_FAST 2 (c) 18 STORE_FAST 3 (d) 20 LOAD_CONST 0 (None) 22 RETURN_VALUE 4変数の場合には、ROT_TWOやROT_THREEではなく、BUILD_TUPLE、UNPACK_SEQUENCEという操作が行われます。 すなわち4変数の場合には「右辺を一度タプルにして、それをアンパックして左辺に代入する」という操作が行われています。 一方、一時変数tmpを用いた場合には def f(a, b, c, d): tmp = a a, b, c = b, c, d d = tmp で、a, b, c = b, c, dの部分は3変数の値の交換になるので先ほど見たように命令コードROT_TWOやROT_THREEを用いた操作が行われます。 つまり、4変数の場合には「タプルを作成しそれをアンパックする」という操作が重いため、一時変数tmpを用いた場合より用いない場合の方が実行速度が遅くなってしまったわけです。 では、5変数以上の場合にはどうでしょうか?この場合、tmpを使っても4変数以上の値の交換になるので、3変数以下の「速い操作」が行われず、さらにtmpを用意する手間もかかるのでtmpを使わない方が速くなります。 しかし、$N$ 変数の値の交換において、tmpを使う場合には $N-1$ 変数の交換を同時に行う必要はありません。そこで $N-1$ 変数の交換を 3変数以下の交換に分割します。すると、「速い操作」が行われるので、tmpを使わない場合よりも高速に実行することができます。 実際にこの方法が上手くいくことを確認してみましょう。題材は競技プログラミングでよく現れる「10変数の交換を $10^7$ 回繰り返す」手続きです。(誰もが INF 回書いた手続きだと思います) ベースとなるコードは以下のものです。 def main(): a, b, c, d, e, f, g, h, i, j = list(range(10)) for _ in range(10**7): # ここに交換の操作を書く if __name__ == '__main__': main() まずはtmpを使わない方法です。 交換の操作は a, b, c, d, e, f, g, h, i, j \ = b, c, d, e, f, g, h, i, j, a で、実行時間は $834 \;\rm{ms}$ でした。 次に、tmpを使う方法です。 「9変数の交換」を「3変数の交換を3回」に分割します。 交換の操作は tmp = a a, b, c = b, c, d d, e, f = e, f, g g, h, i = h, i, j j = tmp で、実行時間は $624 \;\rm{ms}$ となり、なんと $\boldsymbol{200 \;\rm{ms}}$ もの改善に成功しました。 ちなみに、「出来るだけ2変数のswapに分割」をすると交換の操作は tmp = a a, b = b, c c, d = d, e e, f = f, g g, h, i = h, i, j j = tmp で実行時間は $602 \;\rm{ms}$ となり、さらに高速化できます。 本章で分かったこと: 変数の値の同時交換は3変数までにしましょう 2. 条件式 2つ目は条件式についてです。 条件式は、一つの条件からなる単純なものから、複数の条件を複合した複雑なものまで様々なものを書きますが、本章では「3つの数値の比較」に焦点を当ててみます。 具体的には、「3つの数値a, b, cについて、bが半開区間 $[a, c)$ に含まれるかどうか」という条件式を考えます。この条件はもちろん「b が a以上でかつbがc未満」と等価なので多くのプログラミング言語では a <= b and b < c のように書くと思います。 Pythonではより直感的に a <= b < c と書くこともでき、非常に便利です。 ですが、これもまた落とし穴になっています。それぞれの実行時間を次のコードで計測してみます。 def main(): a, b, c = 0, 1, 2 for _ in range(10**7): a <= b and b < c #a <= b < c if __name__ == '__main__': main() 結果は下表のようになりました。 a <= b and b < c a <= b < c $480 \;\rm{ms}$ $501 \;\rm{ms}$ なんと $10^7$ 回の条件判定で $20 \;\rm{ms}$ もの差がつきました。 なぜこのような結果になったのか、やはりdisモジュールを使ってみていきましょう。 まずはa <= b and b < cの場合です。 import dis def f(a, b, c): a <= b and b < c dis.dis(f) を実行すると 4 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 COMPARE_OP 1 (<=) 6 JUMP_IF_FALSE_OR_POP 14 8 LOAD_FAST 1 (b) 10 LOAD_FAST 2 (c) 12 COMPARE_OP 0 (<) >> 14 POP_TOP 16 LOAD_CONST 0 (None) 18 RETURN_VALUE となります。 この手続きを日本語で書いてみると以下のようになります。 aへの参照を取得 bへの参照を取得 直近2つの取得された値を <= で比較 Falseなら指定位置(手続き8)までジャンプしTrueなら結果を破棄 (andの部分) bへの参照を取得 cへの参照を取得 直近2つの取得された値を < で比較 結果を破棄(今回の場合は条件判定をするだけなので) 予想通りというか、実際にコーディングした通りの挙動をしていますね。 次はa <= b < cの場合です。 import dis def f(a, b, c): a <= b < c dis.dis(f) を実行すると 4 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 DUP_TOP 6 ROT_THREE 8 COMPARE_OP 1 (<=) 10 JUMP_IF_FALSE_OR_POP 18 12 LOAD_FAST 2 (c) 14 COMPARE_OP 0 (<) 16 JUMP_FORWARD 4 (to 22) >> 18 ROT_TWO 20 POP_TOP >> 22 POP_TOP 24 LOAD_CONST 0 (None) 26 RETURN_VALUE となります。 こちらも日本語で手続きを記述してみましょう。 aへの参照を取得 bへの参照を取得 直近の取得した参照(b)を複製 直近3つを順番をrotate(これで直近2つはaとbになる) 直近2つの取得された値を <= で比較 Falseなら指定位置(手続き8)までジャンプしTrueなら結果を破棄 (andに相当) cへの参照を取得 直近2つの取得された値を < で比較 結果を破棄 a <= b and b < cの場合を比較すると、「bへの参照を取得」が1回減り、「bの複製」と「直近3つを順番をrotate」という操作が新たに加わったことがわかります。つまり、この違いが $20 \;\rm{ms}$ の差を生み出していたわけです。 $10^7$ 回で $20 \;\rm{ms}$ ということは1回あたり $2 \times 10^{-6}\;\rm{ms}$ です。 「b を2回書き、間をandで繋げる」という手間をかけるだけで1回あたり $2 \times 10^{-6}\;\rm{ms}$ もの実行時間の短縮ができるわけですから、どちらの記法で書けば良いかは明白ですね! 本章で分かったこと: 条件式の変数は自分の手で複製しましょう 3. アンパック代入 with アスタリスク 最後はアスタリスク付きのアンパック代入についてです。 プログラミングをしていると、「リストの先頭の値と二番目以降を分けたい」と思うことが往々にしてあるかもしれません。そんな時にはアスタリスク付きのアンパック代入が使えます。 A = [0, 1, 2, 3] a, *b = A print(a) # 0 print(b) # [1, 2, 3] より一般的には、先頭1つだけでなく、先頭と末尾からいくつかずつとそれ以外を分けることができます。 A = [0, 1, 2, 3, 4, 5, 6, 7] a, b, *c, d, e, f = A print(a) # 0 print(b) # 1 print(c) # [2, 3, 4] print(d) # 5 print(e) # 6 print(f) # 7 これもまた便利な記法ですが、やはり便利な記法には落とし穴が存在します。 以下のコードで同じ結果をもたらす2つの方法を比較してみます。 def main(): A = [0, 1, 2, 3, 4] for _ in range(10**7): a, b = A[0], A[1:] #a, *b = A if __name__ == '__main__': main() 結果は下表のようになりました。 a, b = A[0], A[1:] a, *b = A $921 \;\rm{ms}$ $1162 \;\rm{ms}$ これまでと同様に、なぜこうなったのか内部をみていきましょう。 まず、a, b = A[0], A[1:]の場合です。 import dis A = [0, 1, 2, 3, 4] def f(A): a, b = A[0], A[1:] dis.dis(f) を実行すると 5 0 LOAD_FAST 0 (A) 2 LOAD_CONST 1 (0) 4 BINARY_SUBSCR 6 LOAD_FAST 0 (A) 8 LOAD_CONST 2 (1) 10 LOAD_CONST 0 (None) 12 BUILD_SLICE 2 14 BINARY_SUBSCR 16 ROT_TWO 18 STORE_FAST 1 (a) 20 STORE_FAST 2 (b) 22 LOAD_CONST 0 (None) 24 RETURN_VALUE となります。 手続きを日本語にすると、 Aへの参照を取得 0(aを得るためのindex)を取得 直近2つ(A, 0)からA[0]を取得 Aへの参照を取得 1とNoneを取得 直近2つからスライス 1:Noneを作成 直近2つ(A, 1:None)からA[1:]を取得 A[0]、A[1:]を適切に並び替えて a、bに代入 となります。 次に、a, *b = Aの場合をみていきます。 import dis A = [0, 1, 2, 3, 4] def f(A): a, *b = A dis.dis(f) を実行すると 5 0 LOAD_GLOBAL 0 (A) 2 UNPACK_EX 1 4 STORE_FAST 0 (a) 6 STORE_FAST 1 (b) 8 LOAD_CONST 0 (None) 10 RETURN_VALUE となります。 先ほどは無かったUNPACK_EXという命令コードがあり、これが全てを行なっているようです。 つまり、このUNPACK_EXが重いため、a, *b = Aは実行速度が遅くなってしまったわけです。 では、アンパック代入を一切使用しなければ良いかというとそうでもありません。 気付いた方もいるかと思いますが、a, b = A[0], A[1:]の場合においてA[0]とA[1:]を取得した後の手続きは、まさに第1章でみた「複数の値の複数の変数への代入」そのものです。つまり、4変数以上になると命令コードBUILD_TUPLE、UNPACK_SEQUENCEが実行されてしまいます。 実際に試してみます。 import dis A = [0, 1, 2, 3, 4] def f(A): a, b, c, d = A[0], A[1], A[2], A[3:] dis.dis(f) を実行すると 5 0 LOAD_FAST 0 (A) 2 LOAD_CONST 1 (0) 4 BINARY_SUBSCR 6 LOAD_FAST 0 (A) 8 LOAD_CONST 2 (1) 10 BINARY_SUBSCR 12 LOAD_FAST 0 (A) 14 LOAD_CONST 3 (2) 16 BINARY_SUBSCR 18 LOAD_FAST 0 (A) 20 LOAD_CONST 4 (3) 22 LOAD_CONST 0 (None) 24 BUILD_SLICE 2 26 BINARY_SUBSCR 28 BUILD_TUPLE 4 30 UNPACK_SEQUENCE 4 32 STORE_FAST 1 (a) 34 STORE_FAST 2 (b) 36 STORE_FAST 3 (c) 38 STORE_FAST 4 (d) 40 LOAD_CONST 0 (None) 42 RETURN_VALUE となり、28、30の部分で確かにBUILD_TUPLE、UNPACK_SEQUENCEが実行されていることがわかります。そのため、この方法ではA[0], A[1]...の取得 + BUILD_TUPLE、UNPACK_SEQUENCEの手間がかかるので、UNPACK_EXのみで完結するa, b, c, *d = Aよりも実行速度が遅くなってしまいます。 一応、以下のコードで実行時間を計測してみます。 def main(): A = [0, 1, 2, 3, 4] for _ in range(10**7): a, b, c, d = A[0], A[1], A[2], A[3:] #a, b, c, *d = A if __name__ == '__main__': main() 結果は下表のようになりました。 a, b, c, d = A[0], A[1], A[2], A[3:] a, b, c, *d = A $1424 \;\rm{ms}$ $1218 \;\rm{ms}$ 確かに、アンパック代入を使った方が速くなっていますね! 本章で分かったこと: 3つ以下の変数へのアスタリスク付きのアンパック代入はやめましょう 4. おわりに いかがだったでしょうか? 最後にもう一度、ポイントをまとめておきます。 変数の値の同時交換は3変数までにしましょう 条件式の変数は自分の手で複製しましょう 3つ以下の変数へのアスタリスク付きのアンパック代入はやめましょう どのテクニックも、「Pythonらしさ」、「快適さ」を犠牲にすることで驚くほど(わずかな)実行時間の改善ができます。 これらを駆使してぜひよい競プロライフをお過ごしください。 最後まで読んでいただきありがとうございました。おかしなところがありましたらコメント等でお教えください。
- 投稿日:2021-08-15T19:20:22+09:00
SESAME3をWEB API経由で操作する
いろいろあって会社オフィスの1FにSESAME3を導入しました。 SESAME3+スマホの組み合わせでも十分便利だけど、やっぱりWEB API使いたい! というわけで始めました。 (1Fだけはスマホで開けてね、って言っても社内から反発ありそうだし…) さほど難解なところもないので、公式ページを見ながら進めれば大丈夫です。 (載せたコードもほぼ公式そのままです) 導入環境 Raspberry Pi 4 Ubuntu Server 20.04 LTS Python 3.9.6 SESAME3 SESAME3用 Wifiモジュール WEB APIに必要な情報(SESAME3) API Key(APIキー) Secret Key(秘密鍵) UUID API Key まずCandyHOUSEダッシュボードにアクセスして、API Keyを入手しましょう。 以下画面にメールアドレスを入力すると、4桁の認証コードがメールで届きます。 届いた認証コードを入力してログインします。 ログイン後に表示される「API Key」の中の文字列をコピーしておきましょう。 (Client IDはいまのところ使い道がわからないです…) Secret Key 秘密鍵の確認はちょっと面倒です。(もっと良い方法あれば教えてほしい) SESAMEの専用アプリ「セサミ、ひらけごま!」を利用します。 「セサミ」などでストア検索するとアプリが2つ出てきますが、SESAME3では「セサミ、ひらけごま!」を使います。 もうひとつの「SESAME セサミ」は旧バージョンのSESAMEで利用するようです。 ※リンクはiPhoneアプリです。Android版はこちら。 アプリを起動して、SESAME3を登録したあとにメニュー画面から鍵のシェアをおこなうとQRコードが表示されます。 スマホ間で鍵を共有するときに利用するQRコードですが、これをスマホのQRコードリーダなどで読み取ると ssm://UI?t=sk&sk=xxxxxxxxxxxxxxxxxxxxx&l=0&n=xxxxxxxxxxx のような文字列が取得できるので、ここからskの部分を抜き取ります。(秘密鍵) パースをかけたりして抜き出すかたもいるようですが、自分は目視で抜き取りました。 分解するとこんな感じ t=sk sk=xxxxxxxxxxxxxxxxxxxxx l=0 n=xxxxxxxxxxx のちほどこのskをCMACで暗号化して利用しますので、文字列をコピーしておきましょう。 (これをそのままGithubに乗せてるっぽいの見かけるけど大丈夫なんかな…?) UUID UUIDもSecret Keyと同じく専用アプリのメニュー内から確認できます。 メニューの一番下に表示されます。 こちらもUUID部分の文字列をコピーしておきましょう。 WEB API動作 URL : https://app.candyhouse.co/api/sesame2/ SESAME3のステータスを取得する 先程ダッシュボードで取得したAPI Keyをヘッダー(x-api-key)に入れて、GETすると現在のステータスが確認できます。 URL末尾のUUIDには、アプリで取得した自分のSESAME3のUUIDを入れます。 % curl -H "x-api-key":"[myapikey]" https://app.candyhouse.co/api/sesame2/[UUID] {"batteryPercentage":100,"batteryVoltage":6.0316715542522,"position":-121,"CHSesame2Status":"locked","timestamp":1628937684,"wm2State":true}% バッテリー情報、サムターンのポジション、施錠状態などが確認できます。 (施錠・解錠ログを取得するAPIもありますが、今回は割愛) 解錠・施錠 公式ページを参照するとBODYにcmd、history、signをつけてPOSTすれば良いとのこと。 公式ページから抜粋 POST: "https://app.candyhouse.co/api/sesame2/${sesame_id}/cmd" BODY: { cmd: cmd, history: base64_history, sign: sign } ※上の「${sesame_id}」はUUIDのこと BODY 公式説明 備考 cmd 開閉コマンドコード toggle:88、lock:82、unlock:83 history 履歴 操作ログを見たときに表示される名前 sign 署名 秘密鍵をAES-CMACで暗号化した署名 ※toggleって何に使うんですかね… Pythonからアクセス 以下、Python3での動作です。 pysesame3というSESAME3専用ライブラリもありますが、今回は利用していません。 ライブラリ 今回使用するライブラリをインポートします。 事前にpipでインストールしておきましょう。 pip install pycryptodome requests import datetime, base64, requests, json from Crypto.Hash import CMAC from Crypto.Cipher import AES パラメータ(APIキー、秘密鍵、UUID) 先程取得した各種パラメータを使います。 # 各種パラメータ uuid = 'my-uuid' secret_key = 'my-secret_key' api_key = 'my-api_key' BODY まずは開閉のコマンドcmdに値を入れます。 今回はlock:82で施錠します。 # cmd cmd = 82 続いてhistoryは自分でわかりやすい名前をつければ良いだけなので、「WEB API」としました。 複数ノードからAPIを叩く場合には、判別できるように名前を区別したほうがよいかもしれません。 base64でエンコードしてからbodyに入れます # history history = 'WEB API' base64_history = base64.b64encode(bytes(history, 'utf-8')).decode() 署名部分は秘密鍵をCMACを利用して暗号化します。 タイムスタンプを追加してから、HEXのダイジェスト値を作ります。 # sign cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES) ts = int(datetime.datetime.now().timestamp()) message = ts.to_bytes(4, byteorder='little') message = message.hex()[2:8] cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES) cmac.update(bytes.fromhex(message)) sign = cmac.hexdigest() あとはAPIリクエストをセットして完了です。 # API url = f'https://app.candyhouse.co/api/sesame2/{uuid}/cmd' body = { 'cmd': cmd, 'history': base64_history, 'sign': sign } res = requests.post(url, json.dumps(body), headers=headers) コード全体 実際の運用では、クラスオブジェクトにしてレスポンスの確認やエラー処理などを追加していますが、単体の動作確認だけであればこのコードだけでも十分です。 import datetime, base64, requests, json from Crypto.Hash import CMAC from Crypto.Cipher import AES # 各種パラメータ uuid = 'my-uuid' secret_key = 'my-secret_key' api_key = 'my-api_key' # ヘッダーの設定 headers = {'x-api-key': api_key} # cmd cmd = 82 # 施錠する場合は「82」、解錠する場合は「83」 # history history = 'WEB API' # とりあえず「WEB API」と名付ける base64_history = base64.b64encode(bytes(history, 'utf-8')).decode() # sign cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES) ts = int(datetime.datetime.now().timestamp()) message = ts.to_bytes(4, byteorder='little') message = message.hex()[2:8] cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES) cmac.update(bytes.fromhex(message)) sign = cmac.hexdigest() # API url = f'https://app.candyhouse.co/api/sesame2/{uuid}/cmd' body = { 'cmd': cmd, 'history': base64_history, 'sign': sign } res = requests.post(url, json.dumps(body), headers=headers) 注意すること 詳細は公開されていないようですが、WEB API利用数に制限があるようです。 正確な数字はわかりませんが、1アカウントにつき900-1000回くらいのようです。1日放置したら制限は解除されていました。 CANDYHOUSE様に問い合わせたところ、WEB APIの有料化が予定されていて、その後有料ユーザは利用制限が解除されるそうです。 現時点でWEB API利用は無料です。(2021年8月時点) 利用頻度が高い場合は注意です。締め出されたり閉じ込められたりします…。
- 投稿日:2021-08-15T19:13:20+09:00
【Python演算処理】パスカルの三角形+虚数=四元数?
「パスカルの三角形」の世界と異なり、Pythonの演算世界はほとんど三次元以上の配列と無関係に構築されている様に見えます。 【Python演算処理】微分の概念を高校数学段階からグレードアップ? 【Python演算処理】環論に立脚した全体像再構築②同値関係の再習 【Python演算処理】三次元配列を扱えるのはnumpyと…xarray? 一体どういう事なんでしょうか? 詳しく見ていく事にしましょう。 「パスカルの三角形」の世界と「微積分」の世界 その作業は数学発展史全体を振り返るのとほぼ同値関係となります。 【Python演算処理】環論に立脚した全体像再構築②同値関係の再習 「パスカルの三角形」の世界 【Python演算処理】パスカルの三角形と二項定理または二項展開 概ね中世において、それは当時はまだまだ後進地域に過ぎなかった欧州(まだ計算に不向きなローマ数字しか知らなかった)ではなく最先端文化圏だった「地中海-環ユーラシア大陸南岸エリア」のアラビア数学やインド数学の世界におけるコンセンサスだったのです。 日本の和算もこの辺りについては惜しいところまで迫っていたりする。神谷徳昭/鈴木大郎「パスカルの3角形と和算」 竹之内脩「和算における行列式について」 指数-3の時(項配分1-3-3-1) \frac{1}{(x+1)^3} = \frac{1}{x^3+3x^2+3x+1} 指数-2の時(項配分1-2-1) \frac{1}{(x+1)^2} = \frac{1}{x^2+2x+1} 指数-1の時(項配分1-1) \frac{1}{(x+1)^1} = \frac{1}{x+1} x=$e^{-x}$と代入すると標準シグモイド関数(Standard Sigmoid Function)$\frac{1}{1+e^{-x}}$となる。y(0→1),x(-∞→+∞)を範囲としx=0の時、y=$\frac{1}{2}$となる。シグモイド関数 - Wikipedia 指数0の時(項配分1) \frac{(x+1)}{(x+1)} = 1\\ その行列表現:[1] 指数1の時(項配分1-1) (x+1)^1=x+1\\ その行列表現:[[x,1]]\\ ないしは\left[\begin{matrix}\left[x\right]\\\left[1\right]\end{matrix}\right] x=1の時、2 その積(両者は転置関係にあるので積が取れる→二次形式)【Python演算処理】行列演算の基本③パスカルの三角形から二次形式へ \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}1 & 1\\1 & 1\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}x \left(x + 1\right) + x + 1\end{matrix}\right]\\ =[x^2+2x+1]=[(x+1)^2] 「マスキング」操作 \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}1 & 0\\0 & 0\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}x^{2}\end{matrix}\right]\\ \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}0 & 1\\0 & 0\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}x\end{matrix}\right]\\ \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}0 & 0\\1 & 0\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}x\end{matrix}\right]\\ \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}0 & 0\\0 & 1\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}1\end{matrix}\right]\\ \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}1 & 0\\0 & 1\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}x^{2} + 1\end{matrix}\right]\\ \left[\begin{matrix}x & 1\end{matrix}\right]\left[\begin{matrix}0 & 1\\1 & 0\end{matrix}\right]\left[\begin{matrix}x\\1\end{matrix}\right]=\left[\begin{matrix}2 x\end{matrix}\right] import sympy as sp x = sp.symbols('x') #Rows a=sp.Matrix([[x,1]]) #Columns b=sp.Matrix([[x],[1]]) a00=sp.Matrix([[1,1],[1,1]]) a11=sp.Matrix([[1,0],[0,0]]) a12=sp.Matrix([[0,1],[0,0]]) a21=sp.Matrix([[0,0],[1,0]]) a22=sp.Matrix([[0,0],[0,1]]) adp=sp.Matrix([[1,0],[0,1]]) adm=sp.Matrix([[0,1],[1,0]]) sp.init_printing() display(a) display(a00) display(a11) display(a12) display(a21) display(a22) display(adp) display(adm) display(b) display(sp.simplify(a*a00*b)) print(sp.latex(a)+sp.latex(a00)+sp.latex(b)+"="+sp.latex(sp.simplify(a*a00*b))) display(sp.simplify(a*a11*b)) print(sp.latex(a)+sp.latex(a11)+sp.latex(b)+"="+sp.latex(sp.simplify(a*a11*b))) display(sp.simplify(a*a12*b)) print(sp.latex(a)+sp.latex(a12)+sp.latex(b)+"="+sp.latex(sp.simplify(a*a12*b))) display(sp.simplify(a*a21*b)) print(sp.latex(a)+sp.latex(a21)+sp.latex(b)+"="+sp.latex(sp.simplify(a*a21*b))) display(sp.simplify(a*a22*b)) print(sp.latex(a)+sp.latex(a22)+sp.latex(b)+"="+sp.latex(sp.simplify(a*a22*b))) display(sp.simplify(a*adp*b)) print(sp.latex(a)+sp.latex(adp)+sp.latex(b)+"="+sp.latex(sp.simplify(a*adp*b))) display(sp.simplify(a*adm*b)) print(sp.latex(a)+sp.latex(adm)+sp.latex(b)+"="+sp.latex(sp.simplify(a*adm*b))) 指数2の時(項配分1-2-1) (x+1)^2=x^2+2x+1\\ その行列表現:\left[\begin{matrix}\left[ x^{2}, \ x\right] & \left[ x, \ 1\right]\end{matrix}\right]\\ ないしは\left[\begin{matrix}\left[ x^{2}, \ x\right]\\\left[ x, \ 1\right]\end{matrix}\right] 指数3の時(項配分1-3-3-1) (x+1)^3=x^3+3x^2+3x+1\\ その行列表現:\left[\begin{matrix}\left[ x^{3}, \ x^{2}\right] & \left[ x^{2}, \ x^{2}\right]\\\left[ x, \ x\right] & \left[ x, \ 1\right]\end{matrix}\right] 平方関係が崩れるせいもあって二次形式の延長線上で以下の様な演算の導出が難しい。 \left[\begin{matrix}\left[ x^{3}, \ x^{2}\right] & \left[ x^{2}, \ x^{2}\right]\\\left[ x, \ x\right] & \left[ x, \ 1\right]\end{matrix}\right] \to x^3+3x^2+3x+1 = (x+1)^3\\ 三次元配列の概念を使わないとこういうマスキングも切れない。 \left[\begin{matrix}\left[ a^{3}, \ a^{2}\right] & \left[ a^{2}, \ a^{2}\right]\\\left[ a, \ a\right] & \left[ a, \ 1\right]\end{matrix}\right]・\left[\begin{matrix}\left[ 1, \ 0\right] & \left[ 0, \ 0\right]\\\left[ 0, \ 0\right] & \left[ 0, \ 0\right]\end{matrix}\right]=a^3…①\\ \left[\begin{matrix}\left[ a^{3}, \ a^{2}\right] & \left[ a^{2}, \ a^{2}\right]\\\left[ a, \ a\right] & \left[ a, \ 1\right]\end{matrix}\right]・\left[\begin{matrix}\left[ 0, \ 1\right] & \left[ 1, \ 1\right]\\\left[ 0, \ 0\right] & \left[ 0, \ 0\right]\end{matrix}\right]=3a^2…②\\ \left[\begin{matrix}\left[ a^{3}, \ a^{2}\right] & \left[ a^{2}, \ a^{2}\right]\\\left[ a, \ a\right] & \left[ a, \ 1\right]\end{matrix}\right]・\left[\begin{matrix}\left[ 0, \ 0\right] & \left[ 0, \ 0\right]\\\left[ 1, \ 1\right] & \left[ 1, \ 0\right]\end{matrix}\right]=3a…③\\ \left[\begin{matrix}\left[ a^{3}, \ a^{2}\right] & \left[ a^{2}, \ a^{2}\right]\\\left[ a, \ a\right] & \left[ a, \ 1\right]\end{matrix}\right]・\left[\begin{matrix}\left[ 0, \ 0\right] & \left[ 0, \ 0\right]\\\left[ 0, \ 0\right] & \left[ 0, \ 1\right]\end{matrix}\right]=1…④ そう、全体像を俯瞰すると、おそらく(欧州参入前の)中世数学はこういう辺りで袋小路に入ってしまっていたのです。 「微積分」の世界 一方、欧州で発達した近代解析学は偏微分方程式を繰り返し解き続ける事で(二次元配列まで使えれば事足りる)連立一時方程式に書き換えてしまいました。このパラダイムシフトが三次元配列の需要を大幅に下げた最大要因だったとも推察される状況です。 高校数学からヤコビアンに至るまで {\int U(x,y,z)dV=\int_{0}^{2π}\int_{0}^{π}\int_{0}^{1}U(r,θ,φ)r^2\sinθdrdθdφ\\ =\int_{0}^{2π}\int_{0}^{π}\int_{0}^{1}\begin{vmatrix} \frac{∂x}{∂r} & \frac{∂x}{∂θ} & \frac{∂x}{∂φ}\\ \frac{∂y}{∂r} & \frac{∂y}{∂θ} & \frac{∂y}{∂φ}\\ \frac{∂z}{∂r} & \frac{∂z}{∂θ} & \frac{∂z}{∂φ} \end{vmatrix} drdθdφ } 【Python演算処理】特異点を巡る数理の自分なりのまとめ。 それではこの間に一体何が起こったのでしょうか? 間をつなぐ「複素数の世界」 「パスカルの三角形」や「複式簿記」の概念がルネサンス期(14世紀~16世紀)に(フィレンツェに事実上併合されたピサ、オスマン帝国とレパント交易の支配権を巡って争ったジェノヴァやヴェネツィアといった)イタリア海洋諸国経由で地中海文化圏から欧州文化圏に伝わった時起こった最大のパラダイムシフトは(印刷物が広域に頒布される様になった出版革命を背景とする)それらの知識の「特定集団の家芸」から「(その枠を超えて世界を結ぶ)読書階層のネットワーク的コンセンサス」への変遷だったのです。十字軍運動とヴェネツィアの覇権 ①一般に複式簿記の概念の大源流はヴェネツィア共和国で出版されたルカ・パチョーリ(Fra Luca Bartolomeo de Pacioli、1445年~1517年)の著書「スムマ(Summa de arithmetica, geometria, proportioni et proportionalita=算術、幾何、比および比例に関する全集,1494年)」における複式簿記概念の紹介であったとされるが、まずここに以下の二つの歴史的画期が見て取れる。 それが、これまで地中海商人が欧州内陸部商人に優越し続ける為にあえて秘匿されてきた秘密情報の暴露だったという事。そしてそれはオランダにおける期間会計概念への発展を通じて欧州経済界にパラダイムシフトを起こしたばかりか、最終的には(当時の欧州諸国が戦争を継続する為に)国家経営手段として採用され「(辺境部に現れて中央集権体制を脅かす遊牧民や海賊といった蛮族集団の脅威を確実に取り除くのに)必要にして充分なだけ火器と機動力を装備した常備軍を中央集権的官僚制が徴税によって賄う主権国家体制(Civitas sui Iuris)間の国際協調体制」を登場させたという事。これをもって欧州近世なる歴史区分の本格稼働が始まる。【欧州中心史観以前の世界】火砲の集中運用手段としての常備軍の発達しばしば三十年戦争(1618年~1648年)を終わらせたヴェストファーレン条約(1648年)が重要な契機として挙げられるが、清教徒革命(狭義1642年~1649年、広義1639年~1660年)の渦中にあった英国も、当時はまだまだ氷に閉ざされた辺境に過ぎなかったロシアもこれに参加してはいない。特にロシアが本格的に国家としての影響力を発揮する様になるのは(フランス同様、三十年戦争において元来のプロテスタント陣営もカソリック陣営も差し置いて勝者となった)スウェーデンを本格的に破ってバルト海進出を果たした大北方戦争(1700年~1721年)以降となる。 こうして様々な方面にパラダイムシフトを引き起こした複式簿記概念の根幹が、実は(中世アラビア数学が発展させた)代数(Algebra=アラビア語由来)の概念に立脚していたという事。【Python演算処理】行列演算の基本④大源流における記述統計学との密接な関連性?山本義隆「小数と対数の発見(2018年)」は、小数点下を嫌う会計の世界の伝統が数学の世界における「少数の発見」を遅らせた可能性を指摘する一方でスペインからの独立を目して八十年戦争(1568年~1609年,1621年~1648年)を戦い抜いたオランダにおける(戦争継続の為の)合理主義追及の産物、すなわち「十進法(De Thiende,1585年,十進数による小数の理論を提唱)」や「数学覚書(Ghedachtenissen,1605年,年次期間損益計算書や精算表について解説し国家の財政管理にも複式簿記を導入することを提案)」を著したシモン・ステヴィン(Simon Stevin、1548年~1620年)」と彼を抜擢したオランダ総督マウリッツ(Maurits van Nassau,1567年~1625年)を高く評価する。小数の発見の数学史:天文学との関係 ②一方虚数(Imaginal)の概念はジェロラモ・カルダーノ(Gerolamo Cardano、1501年~1576年)が著書「偉大なる術(アルス・マグナ,羅Ars magna de Rebus Algebraicis,1545年) の中で示した三次方程式の解の公式や四次方程式の解法とされているが、このうち三次方程式の解の公式は数秘術の大家ニコロ・フォンタナ・タルタリア(Niccolò Fontana "Tartaglia",1499年/1500年~1557年)から聞き出した秘術を暴露した内容、四次方程式の解法は弟子のルドヴィコ・フェラーリ(Ludovico Ferrari, 1522年~1565年)の研究内容の紹介だったのである。【数理考古学】三次方程式から虚数へ。 ③その一方で英国におけるジョン・ネイピア(John Napier,1550年~1617年)とヘンリー・ブリッグス(Henry Briggs,1561年~1630年)の手になる最初の常用対数表(Table of Common Logarithms,1624年)が刊行されてその原理に基づく計算尺や歯車式計算機(コンピューターの大源流)ると、天文学や測量術や航海術の分野の発展が加速する。 【数理考古学】常用対数表を使った計算 世界地図の記法として著名なメルカトル図法(Mercator Projection)と正距方位図法(Azimuthal Equidistant Projection)も、ここに登場する「指数写像・対数画像」の概念に立脚する。【Python画像処理】メルカトル図法と正距方位図法 大航海時代(16世紀中旬~17世紀中旬)は「騎士道修道会国家」ポルトガル王国の十字軍運動の延長線上に現れ、オスマン帝国とヴェネツィアのレパント交易独占を快く思わないイタリア人からの資金と人員と装備の供給によって加速したが、最終的には欧州の経済的中心を大西洋沿岸に推移させ地中海沿岸経済圏全体の没落を招いてしまう。そしてその一方でクロムウェル護国卿時代(1653年~1659年)から(欧州内戦から距離を置いて新大陸での展開に重きを置く)貿易革命に転じた英国がじわじわと台頭を開始するのである。フランス絶対王政や新生プロイセン公国を台風の目玉として展開する欧州内戦の時代を尻目に…十字軍国家としてのポルトガル王朝 ④こうして啓蒙君主達が続々と諸国の有識者を招聘する様になった宮廷文化最盛期、しかしむしろ英国人数学者アイザック・ニュートン(Sir Isaac Newton、1642年~1727年)が微積分や万有引力のアイディアをまとめたのがペスト流行に伴うケンブリッジ大学閉鎖期(1665年~1666年)、ドイツ人数学者ゴットフリート・ライプニッツ(Gottfried Wilhelm Leibniz, 1646年~1716年)が並行して微積分の概念を発展させたのが主にパリ出張中に最初の主君だったマインツ選帝侯が亡くなり、カレンベルク侯ヨハン・フリードリヒ により顧問官兼図書館長へと任ぜられハノーファーに移住するまでの失職期間(1668年~1668年)だった辺りが興味深い。 【数理考古学】とある円周率への挑戦? 【数理考古学】解析学史に「虚数概念」をもたらした交代級数 イエズス会の軍隊式教育で育てられ、オランダ軍に入隊してその合理主義精神を学んで「方法序説(Discours de la méthode, 1637年)」を著したフランス人数学者ルネ・デカルト(René Descartes、1596年~1650年)が寿命を縮めたのはスウェーデン女王クリスティーナに招聘されて早起きを強要されたからからと言われる一方、スコットランドの様な「僻地」でジェームズ・グレゴリー(James Gregory,1638年~1675年)がグレゴリー級数を、コリン・マクローリン(Colin Maclaurin,1698~1746年)が特に誰からもパトロネージュを受ける事なくテイラー級数の応用例たるマクローリン級数を研究し、「ただの計算マシーンに過ぎない」と啓蒙君主に嫌われ「僻地の新興国」帝政ロシアに就職先を求めたスイス人数学者レオンハルト・オイラー(Leonhard Euler,1707年~1783年)がこれにさらに複素数の概念を追加して対数概念と三角関数概念を統合するオイラーの公式(Eulerian Formula)$e^{iθ}=\cos(θ)+\sin(θ)i$を完成させた。ジェームス・グレゴリー - Wikipediaコリン・マクローリン - Wikipediaレオンハルト・オイラー - Wikipediaこうして全体像を俯瞰すると、どうしても「周辺性」というキーワードが念頭に浮かんでしまう。そう、当時の数学の発展は絶対王政から直接後援を受けた啓蒙主義的文化人そのものが推進した訳ではなかったのである。ただし、こうした先人の活躍も受けてスコットランドにはスコットランド啓蒙主義(The Scottish Enlightenment1740年代~1790年代)が台頭する展開を迎える。そしてロシア宮廷も次第に文化発信力を高めていく。スコットランド啓蒙主義(The Scottish Enlightenment) 当時のこの方面の数学の発展速度は歯痒いほど緩慢に見えるが、ガウス(Johann Carl Friedrich Gauß,1777年~1855年)が複素平面(Complex Plane)概念を本格的に提唱するのはフランス革命期(1789年~1795年)に続いたナポレオン戦争期(1799年~1815年)の1811年であり(ただし同概念について1797年にCaspar Wesselが書簡で言及しており、Jean-Robert Argandも1806年に同様の手法を用いている)、それなしの研究だったと考えると十分納得がいくのである。なまじ新大陸やインドでの事業展開が順調過ぎたせいで植民地経営に奢りが出た英国はアメリカ独立戦争(1775年~1783年)に直面した。しかし王侯貴族の国民の不満を外部に逸らし続ける」戦争の種が尽きたフランス絶対王政もフランス革命とナポレオン戦争による国民の$\frac{1}{5}$消失とと莫大な経済的損失のせいで産業革命導入が半世紀以上も遅れ、大英帝国に大差をつけられ、二級国家へと転落してしまう。スコットランド啓蒙主義も、その巻き添えとなって壊滅。かかる暗黒の歴史はオカルトに傾倒して「黄金の夜明け団(Hermetic Order of the Golden Dawn)」にも参画したフィオナ・マクラウド(Fiona Macleod,1855年~1905年)らが主導したケルティック・ルネサンスへも確実に昏い影を落としていく。ウィリアム・シャープ (作家) - Wikipedia黄金の夜明け団 - Wikipedia それではここで「パスカルの三角形」を極座標系(r,φ,θ.…)と関連付けた複素関数$e^{ix}$の添字と考えてみましょう。オイラーの公式を用いて二項定理からピタゴラスの定理を導出する際に使う方法の応用です。 【数理考古学】ピタゴラスの定理あるいは三平方の定理からの出発 1=e^0=e^{iθ-iθ}=e^{iθ}e^{-iθ}\\ =(\cos(θ)+\sin(θ)i)(\cos(θ)-\sin(θ)i)\\ =cos(θ)^2+sin(θ)i-sin(θ)i-sin(θ)i^2\\ =cos(θ)^2+sin(θ)^2\\ 【Python演算処理】環論に立脚した全体像再構築②同値関係の再習 指数0の時(項配分1) e^{0}= 1\\ その行列表現:[1] 指数1の時(項配分1-1) e^{1-1}=1\\ その行列表現:[[1,-1]]\\ ないしは\left[\begin{matrix}\left[1\right]\\\left[-1\right]\end{matrix}\right] 指数2の時(項配分1-2-1) ここで$i^2=-1$と考え、四次方程式の解を代入すると… 1の冪根 - Wikipedia \left[\begin{matrix}\left[ 1, \ i\right] & \left[ -i, \ -1\right]\end{matrix}\right]\\ ないしは\left[\begin{matrix}\left[ 1, \ i\right]\\\left[ -i, \ -1\right]\end{matrix}\right] なんとなく見た目は上はそれっぽく見えますよね。ただこの様な小手先技はもうこれ以上応用が効かないのです。 $1=-i^2$でなければならない辺りが毎回扱う上での難所となる。微積分ではちゃんと巡回するのだが… この辺りの小手先性はユニタリ行列(Unitary Matrix)の定義にも感じずにはいられない。ユニタリ行列 - Wikipedia 次を満たす複素正方行列Uとして定義される。なおここでIは単位行列、U*は行列Uの随伴行列(U* = U T)となる。 {\displaystyle U^{*}U=UU^{*}=I} 実数のみで構成される行列の随伴は単に転置であるため実ユニタリ行列は直交行列に等しく、直交行列を複素数体へ拡張したものがユニタリ行列ともいえる。 ここでアイルランド出身の数学者ウィリアム・ローワン・ハミルトン(William Rowan Hamilton、1805年~1865年)が虚数単位を3種類導入する四元数(Quaternion)の概念を思いついきます。 【Pythonで球面幾何学】ハミルトンの四元数は何を表しているのか? i^2=j^2=k^2=ijk=-1\\ その乗積表 1 i j k 1 1 i j k i i -1 k -j j j -k -1 i k k j -i -1 import numpy as np import sympy as sp import pandas as pd X1 = np.matrix([ ["1","i","j","k"], ["i","-1","k","-j"], ["j","-k","-1","i"], ["k","j","-i","-1"]]) #x=X1.transpose() x=X1 df=pd.DataFrame(x,columns=["1",'i','j','k'],index=["1",'i','j','k']) sp.init_printing() org=df.to_html() print(org.replace('\n', '')) 要するに以下の関係にあるんですね。 ij=−ji=k jk=−kj=i ki=−ik=j ちなみにこの四元数間で積を求める演算がベクトルにおける内積(Inner Product=スカラー積)と外積(Outer Product=ベクトル積/クロス積)の概念の起源となります。 四元数と行列で見る内積と外積の「内」と「外」 {\begin{align} &(ai+bj+ck)(di+ej+fk) \\ &=ai(di+ej+fk) \\ &\quad +bi(di+ej+fk) \\ &\quad +cj(di+ej+fk) \\ &=ad\underbrace{ii}_{-1}+ae\underbrace{ij}_{k}+af\underbrace{ik}_{-j} \\ &\quad +bd\underbrace{ji}_{-k}+be\underbrace{jj}_{-1}+bf\underbrace{jk}_{i} \\ &\quad +cd\underbrace{ki}_{j}+ce\underbrace{kj}_{-i}+cf\underbrace{kk}_{-1} \\ &=-\underbrace{(ad+be+cf)}_{内積} \\ &\quad +\underbrace{(bf-ce)i+(cd-af)j+(ae-bd)k}_{外積} \end{align} } 行列演算の場合 {\begin{align} &(ai+bj+ck)(di+ej+fk) \\ &=\left(\begin{matrix} i & j & k \end{matrix}\right) \left(\begin{matrix} a \\ b \\ c \end{matrix}\right) \left(\begin{matrix} d & e & f \end{matrix}\right) \left(\begin{matrix} i \\ j \\ k \end{matrix}\right) \\ &=\left(\begin{matrix} i & j & k \end{matrix}\right) \left(\begin{matrix} ad & ae & af \\ bd & be & bf \\ cd & ce & cf \end{matrix}\right) \left(\begin{matrix} i \\ j \\ k \end{matrix}\right) \\ &=-(ad+be+cf) \\ &\quad +(bf-ce)i+(cd-af)j+(ae-bd)k \end{align} } 途中過程で現れる内積(内)と外積(外)の関係 {\left(\begin{matrix} \underbrace{ad}_{内} & \underbrace{ae}_{外} & \underbrace{af}_{外} \\ \underbrace{bd}_{外} & \underbrace{be}_{内} & \underbrace{bf}_{外} \\ \underbrace{cd}_{外} & \underbrace{ce}_{外} & \underbrace{cf}_{内} \end{matrix}\right) } 全部二次元配列で処理出来てしまってますね。ついでにいわゆる回転行列(Rotation Matrix)の場合についても見ておきましょう。 回転行列 - Wikipedia ユークリッド空間内における原点中心の回転変換の表現行列のことである。n次元空間における回転行列は、実数を成分とする正方行列であって、行列式が1のn次直交行列として特徴づけられる。n次元の回転行列全体は特殊直交群(あるいは回転群)SO(n)と呼ばれる群をなす。 {}^{t}\!R=R^{-1},\;\det R=1. 二次元の回転行列 原点中心に θ 回転して点 (x, y) が (x ', y ') に写るとすると、図形的考察または三角関数の加法定理より、x ', y ' は以下のように表される。 【初心者向け】「加法定理の幾何学的証明」に挑戦。 x'=x\cos \theta -y\sin \theta \\ y'=x\sin \theta +y\cos \theta このことを行列の積で表すと以下となる。 {\begin{bmatrix}x'\\y'\end{bmatrix}}={\begin{bmatrix}\cos \theta &-\sin \theta \\\sin \theta &\cos \theta \\\end{bmatrix}}{\begin{bmatrix}x\\y\end{bmatrix}} import sympy as sp θ,x,y = sp.symbols("θ,x,y") #Rows a=sp.Matrix([[sp.cos(θ),-sp.sin(θ)],[sp.sin(θ),sp.cos(θ)]]) #Columns b=sp.Matrix([[x],[y]]) sp.init_printing() display(a*b) print(sp.latex(a)+sp.latex(b)+"="+sp.latex(sp.simplify(a*b))) display(a.T) print("{}^T"+sp.latex(a)+"="+sp.latex(sp.simplify(a.T))) display(a**-1) print(sp.latex(a)+"^{-1}="+sp.latex(a**-1)+"=" + sp.latex(sp.simplify(a**-1))) display(a.det()) print(sp.latex(a)+sp.latex(b)+".det="+sp.latex(a.det())+"=" + sp.latex(sp.simplify(a.det()))) \left[\begin{matrix}\cos{\left(θ \right)} & - \sin{\left(θ \right)}\\\sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]\left[\begin{matrix}x\\y\end{matrix}\right]=\left[\begin{matrix}x \cos{\left(θ \right)} - y \sin{\left(θ \right)}\\x \sin{\left(θ \right)} + y \cos{\left(θ \right)}\end{matrix}\right]\\ {}^T\left[\begin{matrix}\cos{\left(θ \right)} & - \sin{\left(θ \right)}\\\sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]=\left[\begin{matrix}\cos{\left(θ \right)} & \sin{\left(θ \right)}\\- \sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]\\ \left[\begin{matrix}\cos{\left(θ \right)} & - \sin{\left(θ \right)}\\\sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]^{-1}=\left[\begin{matrix}- \frac{\sin^{2}{\left(θ \right)}}{\cos{\left(θ \right)}} + \frac{1}{\cos{\left(θ \right)}} & \sin{\left(θ \right)}\\- \sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]=\left[\begin{matrix}\cos{\left(θ \right)} & \sin{\left(θ \right)}\\- \sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]\\ \left[\begin{matrix}\cos{\left(θ \right)} & - \sin{\left(θ \right)}\\\sin{\left(θ \right)} & \cos{\left(θ \right)}\end{matrix}\right]\left[\begin{matrix}x\\y\end{matrix}\right].det=\sin^{2}{\left(θ \right)} + \cos^{2}{\left(θ \right)}=1 従って2次元空間ユークリッド空間においては原点中心のθ回転(反時計回りを正とする)を以下の転行列の形で表す事が出来る。 R(\theta )={\begin{bmatrix}\cos \theta &-\sin \theta \\\sin \theta &\cos \theta \\\end{bmatrix}} 逆回転は、回転角が −θ になるだけなので以下となる。 R(-\theta )={\begin{bmatrix}\cos(-\theta )&-\sin(-\theta )\\\sin(-\theta )&\cos(-\theta )\\\end{bmatrix}}={\begin{bmatrix}\cos \theta &\sin \theta \\-\sin \theta &\cos \theta \\\end{bmatrix}} また行列の指数関数を用いた以下の表示もある。 {\displaystyle R(\theta )=\exp \left(\theta {\begin{bmatrix}0&-1\\1&0\end{bmatrix}}\right)}{\displaystyle R(\theta )=\exp \left(\theta {\begin{bmatrix}0&-1\\1&0\end{bmatrix}}\right)} 3次元の回転行列 3次元空間でのx軸、y軸、z軸周りの回転を表す回転行列は、それぞれ次の通りとなる: R_{x}(\theta )={\begin{bmatrix}1&0&0\\0&\cos \theta &-\sin \theta \\0&\sin \theta &\cos \theta \\\end{bmatrix}}\\ R_{y}(\theta )={\begin{bmatrix}\cos \theta &0&\sin \theta \\0&1&0\\-\sin \theta &0&\cos \theta \\\end{bmatrix}}\\ R_{z}(\theta )={\begin{bmatrix}\cos \theta &-\sin \theta &0\\\sin \theta &\cos \theta &0\\0&0&1\end{bmatrix}} ここで回転の方向は$R_{x}$はy軸をz軸に向ける方向、$R_{y}$はz軸をx軸に向ける方向、$R_{z}$はx軸をy軸に向ける方向となる。一般の回転行列も、これら3つの各軸周りの回転行列$R_{x},R_{y},R_{z}$の積によって得ることができる。 例えば$R_{z}(\gamma )R_{x}(\beta )R_{y}(\alpha )$は、yxz系で表したときのオイラー角が α, β, γ であるような回転を表す。 任意の軸周りの回転 任意の回転行列は、ある軸${\mathbf {n}}$まわりの角度$\theta$の回転という形に表示できる。このような回転行列はロドリゲスの回転公式により以下の様に表示出来る。 {\displaystyle R_{\mathbf {n} }(\theta )={\begin{bmatrix}\cos \theta +n_{x}^{2}\left(1-\cos \theta \right)&n_{x}n_{y}\left(1-\cos \theta \right)-n_{z}\sin \theta &n_{z}n_{x}\left(1-\cos \theta \right)+n_{y}\sin \theta \\n_{x}n_{y}\left(1-\cos \theta \right)+n_{z}\sin \theta &\cos \theta +n_{y}^{2}\left(1-\cos \theta \right)&n_{y}n_{z}\left(1-\cos \theta \right)-n_{x}\sin \theta \\n_{z}n_{x}\left(1-\cos \theta \right)-n_{y}\sin \theta &n_{y}n_{z}\left(1-\cos \theta \right)+n_{x}\sin \theta &\cos \theta +n_{z}^{2}\left(1-\cos \theta \right)\\\end{bmatrix}}} また、任意のベクトル${\mathbf {r}}$へのその作用は以下の様に書ける。 {\displaystyle R_{\mathbf {n} }(\theta )\mathbf {r} =\mathbf {r} \cos \theta +\mathbf {n} (\mathbf {n} \cdot \mathbf {r} )(1-\cos \theta )+(\mathbf {n} \times \mathbf {r} )\sin \theta } やはり全て二次元配列で扱える範囲。なので、ますます三次元配列の需要が遠のいてしまったという印象があります。かなり竜頭蛇尾な感じで終わってしまいましたが、とりあえずそんな感じで以下続報…
- 投稿日:2021-08-15T18:59:54+09:00
素人はkaggleで勝てない
執筆の経緯 夢をぶち壊すような記事でごめんなさい 初めてのkaggleに挑戦してみて色々と考えたことがありました。 初めてながらもデータサイエンスで成績を残すことがどのくらい難しいのかわかったので ややポエムチックな記事ですがアウトプットしたいと思います。 メダルの難易度を知りたくて調べると出てくる 『初心者だけどメダル取れました』みたいな感じの記事を 真に受けてしまうひとがいないよう警鐘の意を込めたいという気持ちがあります。 kaggleなんてやるなって意味じゃありません、地道な努力を積み重ねて素人から中級者へ、 そこから上級への壁をぶち壊していつかはメダル獲得をしたいねという話をするのが目的です。 ちなみに初参加のコンペはこちらです。 https://www.kaggle.com/c/commonlitreadabilityprize/overview 2066/3633という順位でまずは上位半分は目指せるように頑張りたいなという感じです。 なぜ素人には勝てないのか そもそもこのデータサイエンスの分野が広大な地図になっていることが原因です。 扱うデータで数値、自然言語、画像があり、 モデルをとってもGBDT、ランダムフォレスト、サポートベクターマシーン...etc さらに特徴量抽出の方法を考えたりなんて多方向へ学習のエネルギーを向けないといけないので、 よっぽど時間がある人でないとコンペ期間で理解しきって自分なりの最強モデルを作成だなんて無理があります。 いくら勉強して他の方のコードなど参照しても全然霧が晴れないんですよ、 勉強してキャッチアップできた部分があったとしても霧が深まることもあります... なんだか深い森に迷い込んでしまったような 素人がkaggleに参入するとこんなもどかしさを味わうと思います。 おそらくこの霧が晴れないうちはメダル獲得なんて遠い話です。 よくある素人でもメダルとれたよみたいな記事 上位者が公開しているノートブックがあるのでそういったものを見てディスカッションを深く理解すれば初参加でメダル獲得も夢じゃないみたいな話を見たことが何度かあったのでちゃんと勉強すれば私にも...って淡い期待を抱いていました。 そもそもそういう方のバックボーンを見ると元々データサイエンスを実務でやってはいて kaggleに参入しての分析は初めてだっただけとかゴリゴリの理系数学マンだったとか それなりに持つもの持っている方が多い印象です。(素人の皮被った上級者ですね笑) まぁこれくらいだったらいいのですが、そういうの昔の記事が多くないですかね? 最近のkaggle事情を聞くと以前に増して複雑なデータを扱う機会が増えたと聞きます。 今は前処理が大変だったり、特徴量抽出をよく考えて工夫しないといけないとか、 より実践的なデータ分析を行わなければならないと聞いたことがあります。 逆に考えるとkaggle頑張ればより実務経験として認めて貰えるようになってきた? 素人でもメダル取れるとか、あまり背景知識なんてなくてもkaggleでアウトプットしてたらメダルとれちゃうぞなんて話は鵜のみしちゃいけないって思います。 それなりの覚悟が必要 私は2年ほど緩く勉強してきた身だったので、 参入前はなんとなく真面目に数か月頑張ればメダルとれるっしょみたいな気持ちでした。 でも1か月頑張ってみて到底納得のいく成果が出せず完全に見くびっていたなという感じです。 私のような軽い気持ちで始めてしまうと心がポッキリ行きます、ポッキリと(笑) ただ、メダルなんて遠いとしても、1か月前の自分と執筆中の自分は明らかにスキルが違うという自負があります。スコアを上げたいと思って試行錯誤をしたのは非常に濃密なアウトプットでした。 参考書とにらめっこして勉強してきた自分からは1皮2皮剥けましたね! それでも貴方はkaggleをやるのか 以上kaggleをやってテキトーにメダルとって転職できるっしょみたいな、 アメリカンドリームには期待できないぞという話でした。 今現在データサイエンスの分野は非常に注目され発展も目覚ましいので、 扱うデータはドンドン複雑化されコンペで求められる技能水準もガンガン高くなっています。
- 投稿日:2021-08-15T18:36:04+09:00
causalmlで因果推論 ~人工データ生成とベースライン検証~
はじめに pythonの因果推論ライブラリcausalmlの人工データ生成、ベースライン検証機能が便利だったので紹介する記事です。 自作の関数で人工データを作成し、causalmlに検証を行ってもらうコードについても解説しています。 因果推論と人工データ 因果推論では、しばしば人工データが用いられます。なぜなら、因果推論の根本問題という、介入または非介入の片方のデータしか得られない問題のため、通常のデータセットから因果推論の手法の性能を検証することは難しいからです。 そのため、手法間の性能の比較を行うために、シミュレーションにより生成された人工データを用いることはお手軽かつ有効性の高い方法です。 本記事では、causalml.dataset.synthetic_data()による人工データ生成と、causalml.dataset.get_synthetic_summary()によるベースライン手法の適用、可視化についての紹介を行います。 人工データ生成 まず、人工データの生成方法について紹介します。 causalml.dataset.synthetic_data()による人工データ生成は、Nie 2017の実験で用いられた4つの設定でのシミュレーションを行うことができます。 causalml.dataset.synthetic_data(mode=1)などと記述することで人工データを生成できます。 データ生成過程 Nie 2017, causalmlのソースコードを参考に作成 表記 共変量:$X_{i}$ データ数、共変量数は自分で設定できる 共変量のうち、効果に影響するのは最初の5つのみ 以下の式により生成される $$X_i = \mathrm{Unif}(0, 1)^d ~~~(\rm{mode=1}) $$ $$X_i = \mathcal{N}(0, I_{d\times d}) ~~~ (\rm{mode\ne1})$$ 割り当て:$W_i$ $i$に介入が割り当てられるかどうか 傾向スコア$e(X_i)$のベルヌーイ分布により決定される $$W_i|X_i \sim \mathrm{Bernoulli}(e(X_i))$$ 効果:$Y_i$ 共変量$X_{i}$と割り当て$W_i$によって決定される効果 以下の式により算出される ベースライン主効果:$b(X_i)$ どのユーザーにも共通の効果 治療効果関数:$\tau(X_i)$ 介入を行った場合と行わなかった場合の差を表す関数 因果推論では、この項の大きさを推定したい 誤差:$\sigma\varepsilon_i$ ノイズレベル$\sigma$と$\mathcal{N}(0, 1)$の乗算によって生成される $$Y_i=b(X_i)+(W_i-0.5)\tau(X_i)+\sigma\varepsilon_i$$ ※以下、便宜上 $X_i=x, X_{ij}=x_j $と記載 設定1:simulate_nuisance_and_easy_treatment $$b(x)=\sin(π x_0 x_1) + 2(x_2 - 0.5)^2 + x_3 +0.5x_4 $$ $$e(x)=\mathrm{trim}_{0.1}\sin(π x_0 x_1)$$ $$\tau(x)=(x_0 + x_1)/2$$ 設定2:simulate_randomized_trial $$b(x)=\max(0, x_0+x_1+x_2) + \max(x_3+x_4)$$ $$e(x)=1/2$$ $$\tau(x)=x_0 + \log(1 + e^{x_1})$$ 設定3:simulate_easy_propensity_difficult_baseline $$b(x)=2\log(1 + e^{x_0+x_1+x_2})$$ $$e(x)=\frac{1}{1+e^{x_1}+e^{x_2}}$$ $$\tau(x)=1$$ 設定4:simulate_unrelated_treatment_control $$b(x)=\max(0, x_0+x_1+x_2) + \max(x_3+x_4)$$ $$e(x)=\frac{1}{1+e^{-x_1}+e^{-x_2}}$$ $$\tau(x)=\max(0, x_0+x_1+x_2) + \max(x_3+x_4)$$ サンプルコード from causalml.dataset import synthetic_data # 設定1で人工データを生成 # サンプル数:1000, 共変量数:5, σ=1.0 # numpy配列が返される y, X, treatment, tau, b, e = synthetic_data(mode=1, n=1000, p=5, sigma=1.0) ベースライン検証 causalmlには、因果推論の複数の手法を一括で人工データに適用してくれるget_synthetic_summary()関数があるので、解説します。 検証してくれる手法 この記事ではそれぞれの手法についての詳しい解説はしません。 メタ学習 4種類のlearnerと2種類の学習機を組み合わせた、計8種類の手法を検証 learner S-learner T-learner X-learner R-learner 学習機 ロジスティック回帰 (LR) XGBoost causal tree 上記以外の手法を試したい場合は、ナイーブに実装する必要がありそうです。 指標 3種類の指標で、それぞれの手法の評価を行います。 Abs % Error of ATE:平均処置効果の誤差の%の絶対値 MSE:平均二乗誤差 KL Divergense:2つの分布の差異を計る尺度 一回のシミュレーションの予測結果を図示 # まとめて 'from causalml.dataset import * ' でもよい from causalml.dataset import get_synthetic_preds, simulate_nuisance_and_easy_treatment, scatter_plot_single_sim import matplotlib.pyplot as plt # 設定1(simulate_nuisance_and_easy_treatment)でn=1000のデータを生成 # デフォルトのベースライン手法で予測を行う single_sim_preds = get_synthetic_preds(simulate_nuisance_and_easy_treatment, n=1000) # 散布図によりそれぞれの性能を比較 scatter_plot_single_sim(single_sim_preds) # グラフの文字の被りをなくす plt.tight_layout() plt.show() シンプルなコードで上図のように、複数の手法の結果を図示することができます。 今回のコードの場合は、まず設定1で1000個の人工データを生成し、デフォルトの9手法で予測を行い、それぞれの結果を散布図で図示しました。 散布図からは LRのS-Learnerの予測値はほぼ0.8弱である XGBを用いたメタ学習は予測値のばらつきが大きい LRを用いたメタ学習はS-Learner以外は大方予測値の順位は正しそう Causal Treeは分岐が不十分で、上手く予測できていない といったことが読み取れます。 複数回シミュレーションを実施し、各手法の精度を比較 # まとめて 'from causalml.dataset import * ' でもよい from causalml.dataset import get_synthetic_summary, scatter_plot_summary, bar_plot_summary # 設定1(simulate_nuisance_and_easy_treatment)でn=1000のデータをk=10回生成 num_simulations = 10 preds_summary = get_synthetic_summary(simulate_nuisance_and_easy_treatment, n=1000, k=num_simulations) 以上のコードにより、「設定1で1000個の人工データを生成し、デフォルトの9手法で予測を行う」という過程を10回繰り返し、pred_summaryに格納しています。ちなみに、pred_summaryは以下のようなデータフレームとなっています。 以下のようにして、それぞれの手法の性能を散布図、棒グラフで図示することもできます。 # それぞれの手法の性能の散布図を作成 scatter_plot_summary(preds_summary, k=num_simulations) # それぞれの手法の性能の棒グラフを作成 bar_plot_summary(preds_summary, k=num_simulations) 自作の関数による人工データの検証 最後に、自作の関数で人工データを作成し、causalmlに検証を行ってもらう方法についても紹介します。 人工データを生成する関数 今回は、設定1を参考にした以下の関数により人工データを生成します。 $$X_i = \mathrm{Unif}(0, 1)^d ~~~(\rm{mode=1}) $$ $$Y_i=b(X_i)+(W_i-0.5)\tau(X_i)+\sigma\varepsilon_i$$ $$b(x)=\cos(π x_0 x_1 x_2) + 2(x_3 - 0.5)^3 + x_4^2 +0.5x_5 $$ $$e(x)=\mathrm{trim}_{0.1}\sin(π x_0 x_1)$$ $$\tau(x)=\frac{x_0 + x_1 + x_2}{3}$$ import numpy as np from scipy.special import expit, logit # 自作の人工データ生成関数 def simulate_original_treatment(n=1000, p=5, sigma=1.0, adj=0.): X = np.random.uniform(size=n*p).reshape((n, -1)) b = np.cos(np.pi * X[:, 0] * X[:, 1] * X[:, 2]) + 2 * (X[:, 3] - 0.5) ** 3 + X[:, 4] ** 2 + 0.5 * X[:, 4] eta = 0.1 e = np.maximum(np.repeat(eta, n), np.minimum(np.sin(np.pi * X[:, 0] * X[:, 1]), np.repeat(1-eta, n))) e = expit(logit(e) - adj) tau = (X[:, 0] + X[:, 1] + X[:, 2]) / 3 w = np.random.binomial(1, e, size=n) y = b + (w - 0.5) * tau + sigma * np.random.normal(size=n) return y, X, w, tau, b, e causalmlによるベースライン検証 これまでと同様にして検証を行うことができます。 一回のシミュレーションの予測結果を図示 # 自作の関数(simulate_original_treatment)でn=1000のデータを生成 # デフォルトのベースライン手法で予測を行う single_sim_preds = get_synthetic_preds(simulate_original_treatment, n=1000) # 散布図によりそれぞれの性能を比較 scatter_plot_single_sim(single_sim_preds) # グラフの文字の被りをなくす plt.tight_layout() 先程の設定1よりも学習がうまくいっていないことがわかります。 複数回シミュレーションを実施し、各手法の精度を比較 # 自作の関数(simulate_original_treatment)でn=1000のデータをk=10回生成 num_simulations = 10 preds_summary = get_synthetic_summary(simulate_original_treatment, n=1000, k=num_simulations) # それぞれの手法の性能の散布図を作成 scatter_plot_summary(preds_summary, k=num_simulations) # それぞれの手法の性能の棒グラフを作成 bar_plot_summary(preds_summary, k=num_simulations) plt.tight_layout() 精度はともかく、比較は行えました。 おわりに pythonの因果推論ライブラリcausalmlの人工データ生成、ベースライン検証機能について紹介しました。 参考文献 causalml documentation causalml.dataset ソースコード
- 投稿日:2021-08-15T17:33:01+09:00
Tkinterでダイアログをカスタマイズする方法
概要 GUIオブジェクトを作っていく中で、既に用意されているダイアログだけでなく、 ユーザー自身でカスタマイズしたダイアログを使用したくなる場面があります。 カスタマイズ可能なダイアログとして、Tkinterにはsimpledialog.Dialogがあります。 カスタマイズ方法について、本記事で解説いたします。 環境 Python3系 デフォルト状態のダイアログ 何もカスタマイズしない状態では、simpledialog.DialogにOKとCancelのボタンしかありません。 上記はrootウィンドウです。ダイアログ表示ボタンを押すと以下のダイアログが表示されます。 import tkinter from tkinter import * from tkinter import simpledialog if __name__ == "__main__": root = tkinter.Tk() def display_dialog(): simpledialog.Dialog(root) button = Button(root) button["text"] = "ダイアログ表示" button["command"] = display_dialog button.grid(column=0, row=0, padx=10, pady=10) root.mainloop() ダイアログの基本的なカスタマイズ方法 任意のオブジェクトをダイアログへ埋め込む ダイアログには基本的なオブジェクト(ラベル、エントリー、コンボボックス、ボタンなど)を埋め込むことができます。 以下のコードではエントリーオブジェクトを埋め込みます。 import tkinter from tkinter import * from tkinter import simpledialog class CustomDialog(simpledialog.Dialog): def __init__(self, master, title=None) -> None: super(CustomDialog, self).__init__(parent=master, title=title) def body(self, master) -> None: """ Dialogオブジェクトへ配置するオブジェクトを定義する。 Parameters ---------- master: Dialogオブジェクトの親オブジェクト Returns ------- None """ entry: Entry = Entry(master) entry.grid(column=0, row=0) if __name__ == "__main__": root = tkinter.Tk() def display_dialog(): CustomDialog(root) button = Button(root) button["text"] = "ダイアログ表示" button["command"] = display_dialog button.grid(column=0, row=0, padx=10, pady=10) root.mainloop() 任意のオブジェクトをダイアログへ埋め込むために以下を実施しています。 simpledialog.Dialogを継承したCustomDialogというクラスを作成します。 ダイアログを初期化するために、simpledialog.Dialogの__init__メソッドを実行します(super(CustomDialog, self).init(parent=master, title=title))。 ダイアログへ配置するオブジェクトを、bodyメソッドへ記載します。 simpledialog.Dialogに実装されているbodyメソッドをオーバーライドすることで、ユーザーが自由にダイアログ内のオブジェクトを定義できます。 OKボタンが押された後の処理 OKボタンが押された後、何らかの処理を行いたい場合があると思います。 例えば、エントリーオブジェクトに入力された文字列を出力したい場合などです。 そのような場合にはapplyメソッドをオーバーライドします。 以下のコードではエントリーオブジェクト内の文字列を取得して、コンソールへ出力します。 import tkinter from tkinter import * from tkinter import simpledialog class CustomDialog(simpledialog.Dialog): def __init__(self, master, title=None) -> None: super(CustomDialog, self).__init__(parent=master, title=title) def body(self, master) -> None: """ Dialogオブジェクトへ配置するオブジェクトを定義する。 Parameters ---------- master: Dialogオブジェクトの親オブジェクト Returns ------- None """ self.entry: Entry = Entry(master) self.entry.grid(column=0, row=0) def apply(self) -> None: """ このDialogオブジェクトが破棄される際に実行される処理を定義する。 Parameters ---------- Returns ------- None """ print(self.entry.get()) if __name__ == "__main__": root = tkinter.Tk() def display_dialog(): CustomDialog(root) button = Button(root) button["text"] = "ダイアログ表示" button["command"] = display_dialog button.grid(column=0, row=0, padx=10, pady=10) root.mainloop() バリデーション OKボタンが押され、ダイアログが削除される際にバリデーション(検証)を行うこともできます。 validateメソッドをオーバーライドします。 例えば、エントリーオブジェクトに入力した文字列の文字数が不足している場合に、エラーメッセージを出すことが可能です。 import tkinter from tkinter import * from tkinter import simpledialog class CustomDialog(simpledialog.Dialog): def __init__(self, master, title=None) -> None: super(CustomDialog, self).__init__(parent=master, title=title) def body(self, master) -> None: """ Dialogオブジェクトへ配置するオブジェクトを定義する。 Parameters ---------- master: Dialogオブジェクトの親オブジェクト Returns ------- None """ self.label: Label = Label(master) self.label["text"] = "文字数が足りません。" self.label["fg"] = "red" self.entry: Entry = Entry(master) self.entry.grid(column=0, row=0) def apply(self) -> None: """ このDialogオブジェクトが破棄される際に実行される処理を定義する。 Parameters ---------- Returns ------- None """ print(self.entry.get()) def validate(self) -> bool: """ このDialogオブジェクトが破棄される際に行う検証を定義する。 Parameters ---------- Returns ------- None """ if len(self.entry.get()) > 4: return True else: self.label.grid(column=0, row=1) return False if __name__ == "__main__": root = tkinter.Tk() def display_dialog(): CustomDialog(root) button = Button(root) button["text"] = "ダイアログ表示" button["command"] = display_dialog button.grid(column=0, row=0, padx=10, pady=10) root.mainloop() ダイアログの少し発展的なカスタマイズ方法 背景色変更 ダイアログの背景色を変更したい場合は、__init__メソッドをオーバーライドします。 import tkinter from tkinter import * from tkinter import simpledialog class CustomDialog(simpledialog.Dialog): def __init__(self, master, title=None) -> None: parent = master ''' ダイアログの初期化 背景色を変えるためにオーバーライドしている。 ''' Toplevel.__init__(self, parent, bg="red") # 背景色の変更 self.withdraw() # remain invisible for now # If the master is not viewable, don't # make the child transient, or else it # would be opened withdrawn if parent.winfo_viewable(): self.transient(parent) if title: self.title(title) self.parent = parent self.result = None body = Frame(self) self.initial_focus = self.body(body) body.pack(padx=5, pady=5) self.buttonbox() if not self.initial_focus: self.initial_focus = self self.protocol("WM_DELETE_WINDOW", self.cancel) if self.parent is not None: self.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50)) self.deiconify() # become visible now self.initial_focus.focus_set() # wait for window to appear on screen before calling grab_set self.wait_visibility() self.grab_set() self.wait_window(self) if __name__ == "__main__": root = tkinter.Tk() def display_dialog(): CustomDialog(root) button = Button(root) button["text"] = "ダイアログ表示" button["command"] = display_dialog button.grid(column=0, row=0, padx=10, pady=10) root.mainloop() 参考資料 simpledialogのソースコード How to set background color of tk.simpledialog? 質問者:Tasmotanizer 参考にした回答 回答者:TheFluffDragon9 Python Tkinter のダイアログボックスをカスタマイズしてみた
- 投稿日:2021-08-15T17:30:56+09:00
Pythonでエラストテネスの篩を使って対象の数までの素数の個数を求めてみる
Pythonでエラストテネスの篩で対象の数までの素数の個数を求めてみる アルゴリズムの勉強の一環でエラストテネスの篩を使って対象の素数の個数を求めるツールを組んでみました。世の中に色々と出尽くしている感はありますが、自分なりに考えてやってみた次第です。 実際のソースコード sieve_of_eratosthenes.py.py import sys import math import time def main(): print('start!' ) start = time.time() args = sys.argv if len(args) == 1: print('Please specify an argument.') exit() if args[1].isdigit() == True: erastosthenes(int(args[1])) else: print('Please specify a numerical value for the argument.') elapsed_time = time.time() - start print ("elapsed_time:{0}".format(elapsed_time) + "[sec]") print('end!') def erastosthenes(number): print('check number: ' + str(number)) if number == 1: print('prime number is nothing.') return # 平方根にして整数化する、これが検索のMAX max_limit = int(math.sqrt(number)) #print('Max: ' + str(max_limit)) # 3以上の奇数のリストを作る(2より大きい偶数は素数でないので無視) odd_numbers = [i + 1 for i in range(2, number, 2)] #print('odd_numbers: ' + str(odd_numbers)) # max_limitまでの奇数のリスト max_limit_odd_numbers = [i + 1 for i in range(2, max_limit, 2)] #print('max_limit_odd_numbers: ' + str(max_limit_odd_numbers)) # max_limitから素数だけにする max_limit_prime_numbers = make_prime_list(max_limit_odd_numbers, max_limit_odd_numbers) #print('max_limit_prime_numbers: ' + str(max_limit_prime_numbers)) # 返却用のリスト(2は素数なので入れておく) ret_prime_list = [2] + make_prime_list(odd_numbers, max_limit_prime_numbers) #print(ret_prime_list) print('prime number quantity is :' + str(len(ret_prime_list))) def make_prime_list(num_list1, num_list2): prime_list = [] for num1 in num_list1: add_flag = True for num2 in num_list2: if num1 <= num2: add_flag = True break if num1 % num2 == 0: add_flag = False break if add_flag == True: prime_list.append(num1) return prime_list if __name__ == "__main__": main() 実行結果例 root@070f0854c823:/code# python sieve_of_eratosthenes.py 1000000 start! check number: 1000000 prime number quantity is :78498 elapsed_time:3.1015725135803223[sec] end! root@070f0854c823:/code#
- 投稿日:2021-08-15T16:59:42+09:00
DHCP Discoverのパケットをscapyで解析してクライアントのホスト名を表示する
はじめに 多くのブロードバンドルーターはDHCP ( Dynamic Host Configuration Protocol ) を用いてネットワーク機器に自動的にIPアドレスを割り当てる。DHCPでは初めにDHCPクライアント(ネットワーク機器)がDHCPサーバー(ブロードバンドルーター)に問い合わせをするためにLAN内の全ての宛先(ブロードキャスト)にDHCP Discoverメッセージを送信する。多くの場合、このDHCP Discoverメッセージの中のオプションにクライアントのホスト名が含まれる。このホスト名を抽出することが今回の目標である。 環境 Raspberry Pi OS (32-bit) Python 3.7.3 scapy==2.4.5 DHCP Discoverメッセージ(DHCPクライアント->DHCPサーバー)をtcpdumpで取得する sudo apt-get install tcpdump tcpdumpをインストール sudo tcpdump -i wlan0 dst port 67 -w test.pcap -i wlan0: 監視するネットワークインターフェースを指定。wlan0は無線LAN。 dst port 67: 宛先ポートが67であれば真。67はDHCPクライアントがDHCPサーバ宛にパケットを送る際のポート番号。 -w test.pcap: test.pcapというファイル名でログを保存。 scapyでパケットを解析しホスト名を表示する pip3 install --pre scapy[basic] pipでscapyをインストール。 parse_dhcp_discover_packet.py from scapy.all import * from datetime import datetime pcap_path = "test.pcap" packets = rdpcap(pcap_path) for packet in packets: try: options = packet[DHCP].options for option in options: if option[0] == 'hostname': hostname = option[1].decode() print('Time: {} | Hostname: {}'.format(datetime.fromtimestamp(packet.time), hostname)) except IndexError as e: print(e) python3 parse_dhcp_discover_packet.py パケットが送信された時刻とDHCPクライアントのホスト名が表示される。 上記のコードを補足 from scapy.all import * from datetime import datetime 必要なモジュールをインポート。 pcap_path = "test.pcap" packets = rdpcap(pcap_path) tcpdumpで取得したログ(test.pcap)をrdpcapで読み込む。 <Ether ... |<IP ... |<UDP ... |<BOOTP ... |<DHCP options=[... hostname=[ホスト名] ...] |>>>>> packetは上記のように構成されている。packet[DHCP]とすることでDHCPの情報のみ取得できる。 packet[DHCP].optionsでoptionsを取得する。optionsはiterableなのでforでoptionを一つ一つ取り出す。 optionのキーoption[0]が'hostname'であるとき、option[1].decode()がホスト名となる。 参考 TCP/IP - DHCP: https://www.infraexpert.com/study/tcpip13.html Download and Installation: https://scapy.readthedocs.io/en/latest/installation.html Scapy入門: https://qiita.com/Howtoplay/items/4080752d0d8c7a9ef2aa Scapy 2.4.5 documentation: https://scapy.readthedocs.io/en/latest/api/scapy.utils.html Usage: https://scapy.readthedocs.io/en/latest/usage.html
- 投稿日:2021-08-15T16:38:15+09:00
2変量正規分布
1.目的と結果 【目的】 2つの分布(ここでは平均0、分散1の正規分布)の散布図と相関係数の値の関係がざっくりわかるGIFファイルをつくること。 【結果】 以下のようにできた。 2.乱数の準備 それぞれ独立に平均0,分散1の正規分布に従う乱数$x_1,x_2$を生成。 (ここで分散1の分布を扱うので、以下では共分散算出値=相関係数として扱う) import numpy as np import pandas as pd np.random.seed(1) #乱数を再現できるようにnp.random.seed(1)としておく df=pd.DataFrame() #DataFrameに複数サンプルデータを格納する df['x_1'] = np.random.normal(0, 1, 2000) df['x_2'] = np.random.normal(0, 1, 2000) 3.乱数の変換 上で生成した相関のない乱数$x_1,x_2$を以下(1)式のような変数変換で相関のあるデータ$y_1,y_2$を生成。 \begin{eqnarray} y_1 &=& x_1 \tag{1} \\ y_2 &=& x_1 \sin{\theta} + x_2 \cos{\theta} \tag{2} \end{eqnarray} ここでは$y_2$の分散を1にするため、式(2)右辺の$x_1$、$x_2$のそれぞれの係数を$\sin{\theta}$、$\cos{\theta}$としている。 「4.グラフ(gifファイル)作成」のグラフでは$\theta$を振ってグラフの分布を変えている。 相関係数は(1)、(2)より$\sin{\theta}$となる。 (gifファイル出力の「相関係数(計算値)」は$\sin{\theta}$。なお乱数から相関係数を算出した結果は「相関係数(実測値)」と暫定的に名付けた) 4.グラフ(gifファイル)作成 以下のコードでgifファイルを作成。 import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.set_aspect('equal') def plot(deg): plt.cla() theta = 2*np.pi*(10*deg)/360 y_1 = df['x_1'] # 式(1)部分 y_2 = np.sin(theta)*df['x_1'] + np.cos(theta)*df['x_2']# 式(2)部分 corr_cal = np.sin(theta) #グラフタイトルの相関係数(計算値)の値 corr_sample =y_1.corr(y_2) #グラフタイトルの相関係数(実測値)の値 plt.plot(y_1, y_2,"o", c='blue',alpha =0.1) plt.xlim(-4,4) plt.ylim(-4,4) plt.xlabel('$y_1$') plt.ylabel('$y_2$') plt.grid() plt.title('θ={}°、相関係数(計算値)={:.2f}、相関係数(実測)={:.2f}' .format(deg*10,corr_cal,corr_sample),fontname="Meiryo") # アニメーション作成 anim = FuncAnimation(fig, plot, frames=36,interval=750) anim.save('相関θ依存.gif', writer='Pillow') ここから出力される相関θ依存.gifがつくりたかったgifファイル。 今回は正規分布に従う乱数で行ったが、 df['x_1'] = np.random.normal(0, 1, 2000)とdf['x_2'] = np.random.normal(0, 1, 2000)を変えれば 他の分布についても同様のことができるはず。 6.参考URL 以下のサイトを勉強し、参考にさせていただきました。 ・[Python/matplotlib] FuncAnimationを理解して使う ・matplotlibのanimationで一枚ごとに違うタイトルを付けたい 以上。
- 投稿日:2021-08-15T16:38:15+09:00
散布図と相関係数、ざっくり目視確認
1.目的と結果 【目的】 2つの分布(ここでは平均0、分散1の正規分布)の散布図と相関係数の値の関係がざっくりわかるGIFファイルをつくること。 【結果】 以下のようにできた。 2.乱数の準備 それぞれ独立に平均0,分散1の正規分布に従う乱数$x_1,x_2$を生成。 (ここで分散1の分布を扱うので、以下では共分散算出値=相関係数として扱う) import numpy as np import pandas as pd np.random.seed(1) #乱数を再現できるようにnp.random.seed(1)としておく df=pd.DataFrame() #DataFrameに複数サンプルデータを格納する df['x_1'] = np.random.normal(0, 1, 2000) df['x_2'] = np.random.normal(0, 1, 2000) 3.乱数の変換 上で生成した相関のない乱数$x_1,x_2$を以下(1)式のような変数変換で相関のあるデータ$y_1,y_2$を生成。 \begin{eqnarray} y_1 &=& x_1 \tag{1} \\ y_2 &=& x_1 \sin{\theta} + x_2 \cos{\theta} \tag{2} \end{eqnarray} ここでは$y_2$の分散を1にするため、式(2)右辺の$x_1$、$x_2$のそれぞれの係数を$\sin{\theta}$、$\cos{\theta}$としている。 「4.グラフ(gifファイル)作成」のグラフでは$\theta$を振ってグラフの分布を変えている。 相関係数は(1)、(2)より$\sin{\theta}$となる。 (gifファイル出力の「相関係数(計算値)」は$\sin{\theta}$。なお乱数から相関係数を算出した結果は「相関係数(実測値)」と暫定的に名付けた) 4.グラフ(gifファイル)作成 以下のコードでgifファイルを作成。 import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.set_aspect('equal') def plot(deg): plt.cla() theta = 2*np.pi*(10*deg)/360 y_1 = df['x_1'] # 式(1)部分 y_2 = np.sin(theta)*df['x_1'] + np.cos(theta)*df['x_2']# 式(2)部分 corr_cal = np.sin(theta) #グラフタイトルの相関係数(計算値)の値 corr_sample =y_1.corr(y_2) #グラフタイトルの相関係数(実測値)の値 plt.plot(y_1, y_2,"o", c='blue',alpha =0.1) plt.xlim(-4,4) plt.ylim(-4,4) plt.xlabel('$y_1$') plt.ylabel('$y_2$') plt.grid() plt.title('θ={}°、相関係数(計算値)={:.2f}、相関係数(実測)={:.2f}' .format(deg*10,corr_cal,corr_sample),fontname="Meiryo") # アニメーション作成 anim = FuncAnimation(fig, plot, frames=36,interval=750) anim.save('相関θ依存.gif', writer='Pillow') ここから出力される相関θ依存.gifがつくりたかったgifファイル。 今回は正規分布に従う乱数で行ったが、 df['x_1'] = np.random.normal(0, 1, 2000)とdf['x_2'] = np.random.normal(0, 1, 2000)を変えれば 他の分布についても同様のことができるはず。 6.参考URL 以下のサイトを勉強し、参考にさせていただきました。 ・[Python/matplotlib] FuncAnimationを理解して使う ・matplotlibのanimationで一枚ごとに違うタイトルを付けたい 以上。
- 投稿日:2021-08-15T16:29:16+09:00
kivyMDチュートリアル其の参什 Components - TapTargetView篇
ハローハロー、お勤めご苦労様です。 なんか2国間のあいさつをした(出来ていない)ところで、いかがお過ごしでしょうか。 すっかりお盆の時期になりましたね。こういったときは祝日にするべきだ(過激論)と 思いますが、お仕事されている方は意外といるのではないでしょうか。はい、私は 言うまでもないですね。 週初めとかはなんか天気良かったのに、急に週末天気がグズついてきましたね。 体調管理はしっかりしたいものですね。何かこういう管理をしてるよという健康 オタクのかた、コメントをお待ちしています。 ということで、このKivymdに関する投稿はお休みしないので、ご心配なく(誰も していない)。というわけで今週も空元気でやっていきますね。今日はTapTarget- View篇となります。 TapTargetView まぁ、恒例行事のようにリンクはスキップするのですが、なにやらいつもと様子が 異なります。マテリアルデザインのリンク先がアーカイブ扱いとなっていますね。 それだけ古いコンポーネントだよということでしょうか。 アーカイブになっている理由などは分かりませんでしたが、詳細な仕様などが書か れていることは変わりありません。気になる方は見て頂ければと思います。 KivyMDの方ではこのように書かれています。 Provide value and improve engagement by introducing users to new features and functionality at relevant moments. つまりどういうことだってばよ、と私はなったのですぐさま依頼しました。 依頼結果はこちらとなります。 適切なタイミングでユーザーに新機能を紹介することで、価値を提供し、 エンゲージメントを向上させます。 なるほど...うぅん?となってしまいましたが、少し抽象的な表現となる でしょうか。 実際に今のGoogleのマップアプリだと、当然っちゃ当然ですが機能としては ありません。なのでマテリアルデザインでもアーカイブとなるのはそのためで しょうか。少しレガシーな機能ということでしょうかね。 機能自体の概略としては、アイコン付きのボタンなどがあり、それを押すと テキストなどのコンテンツが表示されそれ自体が「新機能を紹介することで 、価値を提供し、エンゲージメントを向上」させるということでしょうか。 主に新機能を取り込んだ用というかそのような位置付けになります。 まぁ、要は見てみないとなんだかよくわからんとなりそうなので、さっそく 見ていきましょう。 Usage はい、使用方法ですね。こちらもサンプルがあるので見ていきましょう。 xxx/taptargetview.py from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.taptargetview import MDTapTargetView KV = ''' Screen: MDFloatingActionButton: id: button icon: "plus" pos: 10, 10 on_release: app.tap_target_start() ''' class TapTargetViewDemo(MDApp): def build(self): screen = Builder.load_string(KV) self.tap_target_view = MDTapTargetView( widget=screen.ids.button, title_text="This is an add button", description_text="This is a description of the button", widget_position="left_bottom", ) return screen def tap_target_start(self): if self.tap_target_view.state == "close": self.tap_target_view.start() else: self.tap_target_view.stop() TapTargetViewDemo().run() import文 で、さっそくimport文に入りますが、以下に抜粋を載せておきます。 from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.taptargetview import MDTapTargetView BuilderやMDAppモジュールはいつも出てくるものですよね。また、MDTap- TargetViewはTapTargetViewDemoクラス側で使用するので、ここでimport しています。 kv側 ここは特にとりとめがないところですが、一応クラス側にもつながるところなので 再度以下に該当部分を抜粋しておきます。 KV = ''' Screen: MDFloatingActionButton: id: button icon: "plus" pos: 10, 10 on_release: app.tap_target_start() ''' ここではMDFloatActionButton1個が定義されているのみになります。Buttonに ついては以下でチュートリアルを済ませているので、参考までに見ていただければ。 分かりにくっとなった方は公式マニュアルの該当ページをご覧ください。 代わり映えのあるところで言うと、idとon_releaseプロパティがあるくらいになり ますかね。これらは後のクラス側でも出てくるので、詳細は後ほど。あと、posプロ パティはこれまた後ほど触れるかもしれませんが、絶対座標方式で位置を定義する ものになります。 クラス側 ここが結構、コンポーネントのキモとなるのでじっくり見ていきましょう。 コードを抜粋します。 class TapTargetViewDemo(MDApp): def build(self): screen = Builder.load_string(KV) self.tap_target_view = MDTapTargetView( widget=screen.ids.button, title_text="This is an add button", description_text="This is a description of the button", widget_position="left_bottom", ) return screen def tap_target_start(self): if self.tap_target_view.state == "close": self.tap_target_view.start() else: self.tap_target_view.stop() まずはbuildメソッドから。基本的な使い方としてはMDTapTargetViewインスタンス を直接TapTargetViewDemoのプロパティとして格納する形となります。これは特に 初めてというわけでもなく、Dialogとかもその形でしたね。 あとは、MDTapTargetViewインスタンスを生成するときに、引数として使用したい オプションなどをじゃんじゃん入れ込んでいきます。サンプルとしては、title_text、 description_text、widget_positionなどがありますね。 *_textに関しては、なんとなく分かるかと思いますが、widgetに関してはTapTarget- Viewを発動させたい起点となる他のコンポーネントを基本的には指定します。この場合は MDFloatingActionButtonになっていますね。 widget_positionはコンテンツを入れ込んだ外円をどこに表示させたいかということを 考慮する必要があります。まぁ、端的に言うと画面上で起点となるコンポーネント(この 場合ボタン)がどこにあるかということになりそうですが。今回のボタンは画面左下に 配置させたので、外円は右上に表示させないと見ることができません。ということで、 "left_bottom"を値として入れているということがあります。 残るは、tap_target_startメソッドですが、これはkv側でもありましたね。そう です、気づいた方は大勢いらっしゃるかもしれませんが、これはon_release(kv側) プロパティのコールバックメソッドになります。 中身も至って単純ですね。先程インスタンス化したtap_target_viewの状態を見て それを元に条件分けをしていることになります。ボタンが押されたときに、クローズ していれば、オープンするように。オープンしているときは、また逆のように。 結果 ということで、さっそく結果の方を見てみましょう。論より証拠を。 マニュアルから至って変わりない様子ですが、ちゃんと機能としては使用することが 出来ます。外円も右上にちゃんと表示されていますね。タイトルと説明文も意図通り 表示されています。問題はなさそうですね。 Breeding というわけで、今日はここまで! という風に打ち切る予定でしたが、少し気になることもあり、色々見てみたいという ことでここからはオリジナル節となります。 Breedingは動物に対する進化、品種改良(最初に検索したのはこちら)という意味で ありますが、ここでもコードは動物でしょ?(意味不明)ということから名付けられ ました。 というか品種改良って生物だけかなと思っていましたが、どうやらそのようではない みたいです。 まぁ、重要なところはこの後のコードになりますが、少し変更を加えたコードをさっ そく載せておきます。 xxx/taptargetview_breeding.py from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.taptargetview import MDTapTargetView KV = ''' Screen: MDFloatingActionButton: id: button_left_top icon: "plus" pos: 50, 480 on_release: app.tap_target_view_lt.start() if app.tap_target_view_lt.state == "close" else app.tap_target_view_lt.stop() (略) ''' class TapTargetViewDemo(MDApp): def build(self): screen = Builder.load_string(KV) self.tap_target_view_lt = MDTapTargetView( widget=screen.ids.button_left_top, title_text="Title text", title_text_size="36sp", description_text="Description text", description_text_color=[1, 0, 0, 1], widget_position="left_top", ) (略) return screen TapTargetViewDemo().run() 簡単にどんな変更を加えたかということを言いますと、まずボタンを8個ほど追加を しています。これはUsageであったMDTapTargetViewオブジェクトのwidget_po- sitionを全て列挙できるかどうかということを見ています。left-top、topだとか の定義していなかったものを全て指定してみたということですね。 お次はというと、まずUsageでMDTapTargetViewオブジェクトの指定していたオプ ションをベースとします。分かりやすくするために以下に再掲しておきます。 self.tap_target_view = MDTapTargetView( widget=screen.ids.button, // #以下のプロパティは各ボタンによりけり title_text="This is an add button", description_text="This is a description of the button", widget_position="left_bottom", ) widget(_position)は、作成するボタンによりけりなので可変ということは分かり やすいとは思いますが、これら4つのプロパティをこのままにしておいて値だけを変更 したものをベースとします。 そこからさらにタイトルの文字サイズ、色だとかを変更したものを9個全て定義してみま した。具体的な仕様としては以下のようになります。 Screen: MDFloatingActionButton_left_top: - タイトルテキストサイズを36dpに変更 - 説明文の色を赤文字に変更 MDFloatingActionButton_top: - 外円を赤色に変更 MDFloatingActionButton_right_top: - target_circle(内円と言った方がいいでしょうか)の色を緑に変更 MDFloatingActionButton_left: - 外円の半径を150dpに変更 MDFloatingActionButton_center: - タイトルの位置を左上に設定(プロパティ定義は必須) MDFloatingActionButton_right: - 内円の半径を30dpに変更 MDFloatingActionButton_left_bottom: - タイトルの太文字をFalseに変更 MDFloatingActionButton_bottom: - 説明文のサイズを5dpに変更 MDFloatingActionButton_right_bottom: - 説明文の文字を太文字に変更 ふー、大変。なぜyaml形式にしたのかと言われると真っ先に出てくるのはやってみた かったということですねw という冗談みたいなことは置いておいて、ざっとの仕様はこんな感じになります。 書いてみて、結構kv言語とyamlは相性がいいなぁと思ったことも置いておいて、 チュートリアルでスクショが貼ってあったものは全て見ているかと思います。 漏れていたらすみません・・ 注目ポイントとしては、センター配置のときにタイトルの位置を固定化しなければ いけないという仕様ですね。これがないと動きません。私もまんまと引っかかり ました。チュートリアルにも以下のように記載があります。 If you use the widget_position = "center" parameter then you must definitely specify the title_position. from Widget position - widget_position="center" ですって。あ、あと言い忘れていましたが、コールバックメソッドはプロパティ の値として定義しているということも変更点になります。分かりにくいという方は クラス側の方でbuildメソッド以外のメソッドが亡くなったと思ってもらえれば. ということで、結果を見てみたいという方が大半と思うので、さっそく結果の方に 移ります。あ、コードの方はGitHubにプッシュするようにしますので、ご安心を。 というか全文見たい方はそちらをご参照ください。 結果 お待ちかね、結果になります。 ここは動画キャプチャでの案内になります。 なんか画質も荒いし、撮り方も下手くそですね。。もう少し練習しますね。何か良い 方法をまとめたページどこかに落ちていないかな。 ということは置いておいて、yamlでまとめた仕様を具現化したものが上記キャプチャ になります。照らし合わせて見ていただくと、スッと入ってきやすいのではと思って おります。 [追記 2021/08/15 17:22] 少し書き忘れていたことがあって、以前にも述べていたことかもしれませんがpos_size プロパティは使用しないほうがいいかもしれません。なぜならtablet表示などでは以下の ように位置ずれが発生するためになります。 API - kivymd.uix.taptargetview まとめに入る前に、これも恒例行事ですが使用したAPIをまとめておきます。 class kivymd.uix.taptargetview.MDTapTargetView(**kwargs) Rough try to mimic the working of Android’s TapTargetView. 自身の英語力ではちんぷんかんぷんであったので、依頼してみました。結果を見た 瞬間にあぁなるほどwという気持ちになりましたね。 AndroidのTapTargetViewの動作を真似てみました。 widget Widget to add TapTargetView upon. widget is an ObjectProperty and defaults to None. オブジェクトをインスタンス化をするときに引数で指定していたプロパティでしたね。 Usage・Breedingどちらもボタンのid値を指定していました。 outer_radius Radius for outer circle. outer_radius is an NumericProperty and defaults to dp(200). 外円の半径です。Breedingでは左にボタンを配置したところで指定していました。 デフォルトは200dpで、Breedingでは150dpに変更していました。 outer_circle_color Color for the outer circle in rgb format. outer_circle_color is an ListProperty and defaults to theme_cls.primary_color. 外円の色指定です。Breedingでは上にボタンを配置したところで指定していました。 リストプロパティでデフォルトテーマカラーを引き継いでいます。Breedingでは(1,0,0) つまりは赤に変更していました。 target_radius Radius for target circle. target_radius is an NumericProperty and defaults to dp(45). ターゲット円、つまりは内円の半径になります。Breedingでは右配置のボタンで 間接的に指定していました。デフォルトは45dpで、Breeding時の指定は30dpでした。 target_circle_color Color for target circle in rgb format. target_circle_color is an ListProperty and defaults to [1, 1, 1]. これまた内円で、その色指定ですね。今回は右上配置のボタンに関連して、(0,1,0) つまり緑色に指定していました。プロパティの仕様、デフォルト値は上記の通りです。 title_text Title to be shown on the view. title_text is an StringProperty and defaults to ‘’. これは言わずもがなかもですね。タイトルのデフォルト値は何も入っていないのでご注意を。 title_text_size Text size for title. title_text_size is an NumericProperty and defaults to dp(25). 今度はタイトルのサイズになります。Breedingでは左上配置のボタンに関連して 36dpと指定していました。プロパティの仕様、デフォルト値は上記の通りです。 title_text_color Text color for title. title_text_color is an ListProperty and defaults to [1, 1, 1, 1]. 今度はタイトルの色指定になります。Breedingでは指定はしていませんでしたが、これも 指定できるよーという周知も込めて。プロパティの仕様、デフォルト値は上記の通りです。 title_text_bold Whether title should be bold. title_text_bold is an BooleanProperty and defaults to True. タイトルの太文字選択ですね。これは今回使用していませんが、デフォルトは上記の 通りとなっているので注意が必要そうです。 description_text Description to be shown below the title (keep it short). description_text is an StringProperty and defaults to ‘’. description_text_size Text size for description text. description_text_size is an NumericProperty and defaults to dp(20). description_text_color Text size for description text. description_text_color is an ListProperty and defaults to [0.9, 0.9, 0.9, 1]. description_text_bold Whether description should be bold. description_text_bold is an BooleanProperty and defaults to False. これら4つはタイトルと同じで、その説明文版と見てもらって良いかもしれません。 これらはUsage・Breedingともに使用している・していないということがあり ますが、ウォーリーを探せみたいにどこで使用されているか探してみるのもよい トレーニングになるかもしれません。# 省エネモード突入! widget_position Sets the position of the widget on the outer_circle. Available options are ‘left’, ‘right’, ‘top’, ‘bottom’, ‘left_top’, ‘right_top’, ‘left_bottom’, ‘right_bottom’, ‘center’. widget_position is an OptionProperty and defaults to ‘left’. 一言で表すなら、外円を表示させるウィジェットがどこにあるかという位置決め ですが、このデフォルトは'left'なことは注意が必要そうです。 title_position Sets the position of :attr~title_text on the outer circle. Only works if :attr~widget_position is set to ‘center’. In all other cases, it calculates the :attr~title_position itself. Must be set to other than ‘auto’ when :attr~widget_position is set to ‘center’. Available options are ‘auto’, ‘left’, ‘right’, ‘top’, ‘bottom’, ‘left_top’, ‘right_top’, ‘left_bottom’, ‘right_bottom’, ‘center’. title_position is an OptionProperty and defaults to ‘auto’. 今度はタイトルの位置決めになりますね。デフォルトは'auto'なので特に気にする ことはないのかと思われるかもしれません。先述の通りですがwidget_position プロパティをcenterに配置したときはこちらのプロパティが必須項目となることに 注意が必要です。 state State of MDTapTargetView. state is an OptionProperty and defaults to ‘close’. stop(self, *args) Starts widget close animation. start(self, *args) Starts widget opening animation. MDTapTargetViewインスタンスの状態及び併せ持つメソッドになります。 注意としてはデフォルト時はこれが閉じた状態にあるという点になります。 まとめ いやー、長い!打ち切っとけばよかった! という本当か分からない心の内は閉まっておくのがベストですね。 さぁ、どうでもよいことは置いておいて、いかがだったでしょうか。 試してみると分かるかもしれませんが、結構デフォルトは計算されているので 特にカスタマイズ要件などがない限りは、そのまま使用した方がいいかもですね。 実行結果の下ボタン開いたときなんかは、説明文が見えないしで。。 ということで、長文になったことは思ってもみなかったことですが、参考になれば この上なく嬉しいです。ということで今日はここまで! 来週は順番通りTextField篇となります。終わりも見えてきて、これをやると結構 アプリ開発が本格的にスタートできるのではという感じになってきます。バージョン アップが先だけどね。ということで、来週もお楽しみにー。 それでは、ごきげんよう。 参照 Components » TapTargetView https://kivymd.readthedocs.io/en/latest/components/taptargetview/ DeepL 翻訳ツール https://www.deepl.com/ja/translator
- 投稿日:2021-08-15T16:23:50+09:00
pythonで平衡二分木を使った辞書を作ってみた(平衡二分木+defaultdict相当)
はじめに pythonのset(集合)やdict(辞書)は値の検索削除がO(1)と高速ですが、要素の大きさに順序を持つことができず、順序を気にしながらの実装を愚直にすると計算時間が大きくかかってしまうなどの課題があります。一方でc++では、順序付き集合のsetや順序付きの連想配列のmap(辞書相当)が標準で整備されており、競技プログラミングなど時間制約がタイトな場合では、標準ライブラリーの差でもpython勢は不利になることが多いです。そこで今回はc++のmapに相当する平衡二分木を用いた辞書の実装例を紹介したいと思います。 参考記事 今回は平衡二分木部分に関しては出来合いのものを用いました。 Pythonで非再帰AVL木 - 競プロ記録 他 (非再帰での実装となっておりpythonでも高速に動くためとても重宝しております) こちらで紹介されている記事の実装例にいくつか機能を付与する形で実装しました。 コードの紹介 コードがとても長くなってしまっているので、関数及び使用方法について次項で重点的に説明しようと思います。 コードの表示はこちらをクリック AVLTree_dict.py #非再帰の平衡二分木 import copy class Node: """ノード Attributes: key (any): ノードのキー。比較可能なものであれば良い。(1, 4)などタプルも可。 val (any): ノードの値。 lch (Node): 左の子ノード。 rch (Node): 右の子ノード。 bias (int): 平衡度。(左部分木の高さ)-(右部分木の高さ)。 size (int): 自分を根とする部分木の大きさ """ def __init__(self, key, val): self.key = key self.val = val self.lch = None self.rch = None self.bias = 0 self.size = 1 class AVLTree: """非再帰AVL木 Attributes: root (Node): 根ノード。初期値はNone。 valdefault (any): ノード値のデフォルト値。デフォルトではNone。(数値、リストなど可) """ def __init__(self,valdefault=None): self.root = None self.valdefault = valdefault def rotate_left(self, v): u = v.rch u.size = v.size v.size -= u.rch.size + 1 if u.rch is not None else 1 v.rch = u.lch u.lch = v if u.bias == -1: u.bias = v.bias = 0 else: u.bias = 1 v.bias = -1 return u def rotate_right(self, v): u = v.lch u.size = v.size v.size -= u.lch.size + 1 if u.lch is not None else 1 v.lch = u.rch u.rch = v if u.bias == 1: u.bias = v.bias = 0 else: u.bias = -1 v.bias = 1 return u def rotateLR(self, v): u = v.lch t = u.rch t.size = v.size v.size -= u.size - (t.rch.size if t.rch is not None else 0) u.size -= t.rch.size + 1 if t.rch is not None else 1 u.rch = t.lch t.lch = u v.lch = t.rch t.rch = v self.update_bias_double(t) return t def rotateRL(self, v): u = v.rch t = u.lch t.size = v.size v.size -= u.size - (t.lch.size if t.lch is not None else 0) u.size -= t.lch.size + 1 if t.lch is not None else 1 u.lch = t.rch t.rch = u v.rch = t.lch t.lch = v self.update_bias_double(t) return t def update_bias_double(self, v): if v.bias == 1: v.rch.bias = -1 v.lch.bias = 0 elif v.bias == -1: v.rch.bias = 0 v.lch.bias = 1 else: v.rch.bias = 0 v.lch.bias = 0 v.bias = 0 def insert(self, key, val=None): """挿入 指定したkeyを挿入する。valはkeyのノード値。 Args: key (any): キー。 val (any): 値。(指定しない場合はvaldefaultが入る) Note: 同じキーがあった場合は上書きする。 """ if val == None: val = copy.deepcopy(self.valdefault) if self.root is None: self.root = Node(key, val) return v = self.root history = [] while v is not None: if key < v.key: history.append((v, 1)) v = v.lch elif v.key < key: history.append((v, -1)) v = v.rch elif v.key == key: v.val = val return p, pdir = history[-1] if pdir == 1: p.lch = Node(key, val) else: p.rch = Node(key, val) while history: v, direction = history.pop() v.bias += direction v.size += 1 new_v = None b = v.bias if b == 0: break if b == 2: u = v.lch if u.bias == -1: new_v = self.rotateLR(v) else: new_v = self.rotate_right(v) break if b == -2: u = v.rch if u.bias == 1: new_v = self.rotateRL(v) else: new_v = self.rotate_left(v) break if new_v is not None: if len(history) == 0: self.root = new_v return p, pdir = history.pop() p.size += 1 if pdir == 1: p.lch = new_v else: p.rch = new_v while history: p, pdir = history.pop() p.size += 1 def delete(self, key): """削除 指定したkeyの要素を削除する。 Args: key (any): キー。 Return: bool: 指定したキーが存在するならTrue、しないならFalse。 """ v = self.root history = [] while v is not None: if key < v.key: history.append((v, 1)) v = v.lch elif v.key < key: history.append((v, -1)) v = v.rch else: break else: return False if v.lch is not None: history.append((v, 1)) lmax = v.lch while lmax.rch is not None: history.append((lmax, -1)) lmax = lmax.rch v.key = lmax.key v.val = lmax.val v = lmax c = v.rch if v.lch is None else v.lch if history: p, pdir = history[-1] if pdir == 1: p.lch = c else: p.rch = c else: self.root = c return True while history: new_p = None p, pdir = history.pop() p.bias -= pdir p.size -= 1 b = p.bias if b == 2: if p.lch.bias == -1: new_p = self.rotateLR(p) else: new_p = self.rotate_right(p) elif b == -2: if p.rch.bias == 1: new_p = self.rotateRL(p) else: new_p = self.rotate_left(p) elif b != 0: break if new_p is not None: if len(history) == 0: self.root = new_p return True gp, gpdir = history[-1] if gpdir == 1: gp.lch = new_p else: gp.rch = new_p if new_p.bias != 0: break while history: p, pdir = history.pop() p.size -= 1 return True def member(self, key): """キーの存在チェック 指定したkeyがあるかどうか判定する。 Args: key (any): キー。 Return: bool: 指定したキーが存在するならTrue、しないならFalse。 """ v = self.root while v is not None: if key < v.key: v = v.lch elif v.key < key: v = v.rch else: return True return False def getval(self, key): """値の取り出し 指定したkeyの値を返す。 Args: key (any): キー。 Return: any: 指定したキーが存在するならそのオブジェクト。存在しなければvaldefault """ v = self.root while v is not None: if key < v.key: v = v.lch elif v.key < key: v = v.rch else: return v.val self.insert(key) return self[key] # def lower_bound(self, key): """下限つき探索 指定したkey以上で最小のキーを見つける。[key,inf)で最小 Args: key (any): キーの下限。 Return: any: 条件を満たすようなキー。そのようなキーが一つも存在しないならNone。 """ ret = None v = self.root while v is not None: if v.key >= key: if ret is None or ret > v.key: ret = v.key v = v.lch else: v = v.rch return ret def upper_bound(self, key): """上限つき探索 指定したkey未満で最大のキーを見つける。[-inf,key)で最大 Args: key (any): キーの上限。 Return: any: 条件を満たすようなキー。そのようなキーが一つも存在しないならNone。 """ ret = None v = self.root while v is not None: if v.key < key: if ret is None or ret < v.key: ret = v.key v = v.rch else: v = v.lch return ret def find_kth_element(self, k): """小さい方からk番目の要素を見つける Args: k (int): 何番目の要素か(0オリジン)。 Return: any: 小さい方からk番目のキーの値。 """ v = self.root s = 0 while v is not None: t = s+v.lch.size if v.lch is not None else s if t == k: return v.key elif t < k: s = t+1 v = v.rch else: v = v.lch return None def getmin(self): ''' Return: any: 存在するキーの最小値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.lch if v == None: break return ret.key def getmax(self): ''' Return: any: 存在するキーの最大値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.rch if v == None: break return ret.key def popmin(self): ''' 存在するキーの最小値をpopする Return: any: popした値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.lch if v == None: break del self[ret.key] return ret.key def popmax(self): ''' 存在するキーの最大値をpopする Return: any: popした値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.rch if v == None: break del self[ret.key] return ret.key def popkth(self,k): ''' 存在するキーの小さい方からk番目をpopする Return: any: popした値 ''' key = self.find_kth_element(k) del self[key] return key def get_key_val(self): ''' Return: dict: 存在するキーとノード値をdictで出力 ''' retdict = dict() for i in range(len(self)): key = self.find_kth_element(i) val = self[key] retdict[key] = val return retdict def values(self): for i in range(len(self)): yield self[self.find_kth_element(i)] def __iter__(self): # Nlog(N) for i in range(len(self)): yield self.find_kth_element(i) def __contains__(self, key): return self.member(key) def __getitem__(self, key): return self.getval(key) def __setitem__(self, key, val): return self.insert(key, val) def __delitem__(self, key): return self.delete(key) def __bool__(self): return self.root is not None def __len__(self): return self.root.size if self.root is not None else 0 関数の説明 ・rotate_left - update_bias_double: 平衡二分木を実現するうえで最も大事な部分です(平衡を保つための回転などを行っています)。外部で呼び出す必要はないので、使用上は気にしなくていい部分です。 ・insert(key,val=None): keyを平衡二分木に挿入します。valはkeyのノード値(value)です(辞書のkey,valueと同等) ・delete(key): 指定したkeyの要素を削除します。 ・menber(key): 指定したkeyの有無を判定します。 ・getval(key): 指定したkeyの値のノード値を返します。(dict[key]と同等) ・lower_bound(key): [key,inf)で最小のものを返します。 ・upper_bound(key): [-inf,key)で最大のものを返します。 ・find_kth_element(k): 小さい方からk番目のkeyを返します(最小を0番目とする) 以下元記事にはなかった追加機能です。 ・getmin(): 最小のkeyを返します ・getmax(): 最大のkeyを返します ・popmin(): 最小のkeyを返し、削除します。(heapqと同じ操作が実現可能) ・popmax(): 最大のkeyを返し、削除します。 ・popkth(k): 小さい方からk番目のkeyを返し、削除します。 ・get_key_val(): 存在するkeyとノード値(value)をdictで返します。 ・values(): すべてのノード値(value)を返します。 その他に、使用感をdictに合わせるために特殊メゾットを定義しています。 使用例 注意点としてはkeyは大小比較可能なのものである必要があるというところです(数値、文字列、tupleなどは可能) ・普通の平衡二分木としての動作 a.py AVL = AVLTree() AVL.insert(10) AVL.insert(20) AVL.insert(30) AVL.insert(40) AVL.insert(50) print(AVL.lower_bound(15)) # 20 print(AVL.find_kth_element(2)) # 30 print(40 in AVL) # True del AVL[40] # AVL.delete(40)と等価 print(40 in AVL) # False print(list(AVL)) # [10, 20, 30, 50] print(AVL.popmin()) # 10 print(AVL.popkth(1)) # 20,30,50のうち1番目(0オリジン)の30 # 30 print(list(AVL)) # [20, 50] print(len(AVL)) # 2 ・辞書っぽい使い方 b.py AVL1 = AVLTree() AVL1['a'] = 'A' AVL1['b'] = 'B' AVL1['f'] = 'C' AVL1['aa'] = 'AA' print(list(AVL1)) # ['a', 'aa', 'b', 'f'] print(AVL1.get_key_val()) # {'a': 'A', 'aa': 'AA', 'b': 'B', 'f': 'C'} print(AVL1.getmax()) # f print(AVL1.upper_bound('e')) # b ・collections.defaultdict相当の動作 c.py # はじめにvaldefaultを指定することでdefaultdictに相当する処理が可能。 AVL2 = AVLTree(valdefault=[]) AVL2[20].append(2) AVL2[20].append(3) AVL2[20].append(6) AVL2[30].append(5) AVL2[40].append(1) AVL2[40].append(2) print(AVL2.get_key_val()) # {20: [2, 3, 6], 30: [5], 40: [1, 2]} print(AVL2[20].pop()) # 6 print(40 in AVL2) # True print(50 in AVL2) # False print(AVL2.popmax()) # 40 AVL2[50].append(5) AVL2[50].append(6) print(AVL2.get_key_val()) # {20: [2, 3], 30: [5], 50: [5, 6]} ・collections.Counter相当の動作 d.py AVL3 = AVLTree(valdefault=0) AVL3[30] += 3 AVL3[40] += 2 AVL3[50] += 1 print(AVL3.get_key_val()) # {30: 3 40: 2, 50: 1} AVL3[50] += 5 print(AVL3.get_key_val()) # {30: 3, 40: 2, 50: 6} print(list(AVL3.values())) # [3, 2, 6] while AVL3: key = AVL3.getmin() print(key,AVL3[key]) AVL3.popmin() # 30 3 # 40 2 # 50 6 競技プログラミングでの使用例 ・atcoder ABC140F 提出結果 ・atcoder AGC005B 提出結果 ・defaultdictとしての例: atcoder diverta 2019 Programming Contest 2 提出結果 最後に LGTMしていただけると大変励みになりますので参考になった方いましたらよろしくお願いいたします。
- 投稿日:2021-08-15T16:23:50+09:00
pythonで平衡二分木を使った辞書を作ってみた(平衡二分木+defaultdict, C++のmap相当)
はじめに pythonのset(集合)やdict(辞書)は値の検索削除がO(1)と高速ですが、要素の大きさに順序を持つことができず、順序を気にしながらの実装を愚直にすると計算時間が大きくかかってしまうなどの課題があります。一方でc++では、順序付き集合のsetや順序付きの連想配列のmap(辞書相当)が標準で整備されており、競技プログラミングなど時間制約がタイトな場合では、標準ライブラリーの差でもpython勢は不利になることが多いです。そこで今回はc++のmapに相当する平衡二分木を用いた辞書の実装例を紹介したいと思います。 参考記事 今回は平衡二分木部分に関しては出来合いのものを用いました。 Pythonで非再帰AVL木 - 競プロ記録 他 (非再帰での実装となっておりpythonでも高速に動くためとても重宝しております) こちらで紹介されている記事の実装例にいくつか機能を付与する形で実装しました。 コードの紹介 コードがとても長くなってしまっているので、関数及び使用方法について次項で重点的に説明しようと思います。 コードの表示はこちらをクリック AVLTree_dict.py #非再帰の平衡二分木 import copy class Node: """ノード Attributes: key (any): ノードのキー。比較可能なものであれば良い。(1, 4)などタプルも可。 val (any): ノードの値。 lch (Node): 左の子ノード。 rch (Node): 右の子ノード。 bias (int): 平衡度。(左部分木の高さ)-(右部分木の高さ)。 size (int): 自分を根とする部分木の大きさ """ def __init__(self, key, val): self.key = key self.val = val self.lch = None self.rch = None self.bias = 0 self.size = 1 class AVLTree: """非再帰AVL木 Attributes: root (Node): 根ノード。初期値はNone。 valdefault (any): ノード値のデフォルト値。デフォルトではNone。(数値、リストなど可) """ def __init__(self,valdefault=None): self.root = None self.valdefault = valdefault def rotate_left(self, v): u = v.rch u.size = v.size v.size -= u.rch.size + 1 if u.rch is not None else 1 v.rch = u.lch u.lch = v if u.bias == -1: u.bias = v.bias = 0 else: u.bias = 1 v.bias = -1 return u def rotate_right(self, v): u = v.lch u.size = v.size v.size -= u.lch.size + 1 if u.lch is not None else 1 v.lch = u.rch u.rch = v if u.bias == 1: u.bias = v.bias = 0 else: u.bias = -1 v.bias = 1 return u def rotateLR(self, v): u = v.lch t = u.rch t.size = v.size v.size -= u.size - (t.rch.size if t.rch is not None else 0) u.size -= t.rch.size + 1 if t.rch is not None else 1 u.rch = t.lch t.lch = u v.lch = t.rch t.rch = v self.update_bias_double(t) return t def rotateRL(self, v): u = v.rch t = u.lch t.size = v.size v.size -= u.size - (t.lch.size if t.lch is not None else 0) u.size -= t.lch.size + 1 if t.lch is not None else 1 u.lch = t.rch t.rch = u v.rch = t.lch t.lch = v self.update_bias_double(t) return t def update_bias_double(self, v): if v.bias == 1: v.rch.bias = -1 v.lch.bias = 0 elif v.bias == -1: v.rch.bias = 0 v.lch.bias = 1 else: v.rch.bias = 0 v.lch.bias = 0 v.bias = 0 def insert(self, key, val=None): """挿入 指定したkeyを挿入する。valはkeyのノード値。 Args: key (any): キー。 val (any): 値。(指定しない場合はvaldefaultが入る) Note: 同じキーがあった場合は上書きする。 """ if val == None: val = copy.deepcopy(self.valdefault) if self.root is None: self.root = Node(key, val) return v = self.root history = [] while v is not None: if key < v.key: history.append((v, 1)) v = v.lch elif v.key < key: history.append((v, -1)) v = v.rch elif v.key == key: v.val = val return p, pdir = history[-1] if pdir == 1: p.lch = Node(key, val) else: p.rch = Node(key, val) while history: v, direction = history.pop() v.bias += direction v.size += 1 new_v = None b = v.bias if b == 0: break if b == 2: u = v.lch if u.bias == -1: new_v = self.rotateLR(v) else: new_v = self.rotate_right(v) break if b == -2: u = v.rch if u.bias == 1: new_v = self.rotateRL(v) else: new_v = self.rotate_left(v) break if new_v is not None: if len(history) == 0: self.root = new_v return p, pdir = history.pop() p.size += 1 if pdir == 1: p.lch = new_v else: p.rch = new_v while history: p, pdir = history.pop() p.size += 1 def delete(self, key): """削除 指定したkeyの要素を削除する。 Args: key (any): キー。 Return: bool: 指定したキーが存在するならTrue、しないならFalse。 """ v = self.root history = [] while v is not None: if key < v.key: history.append((v, 1)) v = v.lch elif v.key < key: history.append((v, -1)) v = v.rch else: break else: return False if v.lch is not None: history.append((v, 1)) lmax = v.lch while lmax.rch is not None: history.append((lmax, -1)) lmax = lmax.rch v.key = lmax.key v.val = lmax.val v = lmax c = v.rch if v.lch is None else v.lch if history: p, pdir = history[-1] if pdir == 1: p.lch = c else: p.rch = c else: self.root = c return True while history: new_p = None p, pdir = history.pop() p.bias -= pdir p.size -= 1 b = p.bias if b == 2: if p.lch.bias == -1: new_p = self.rotateLR(p) else: new_p = self.rotate_right(p) elif b == -2: if p.rch.bias == 1: new_p = self.rotateRL(p) else: new_p = self.rotate_left(p) elif b != 0: break if new_p is not None: if len(history) == 0: self.root = new_p return True gp, gpdir = history[-1] if gpdir == 1: gp.lch = new_p else: gp.rch = new_p if new_p.bias != 0: break while history: p, pdir = history.pop() p.size -= 1 return True def member(self, key): """キーの存在チェック 指定したkeyがあるかどうか判定する。 Args: key (any): キー。 Return: bool: 指定したキーが存在するならTrue、しないならFalse。 """ v = self.root while v is not None: if key < v.key: v = v.lch elif v.key < key: v = v.rch else: return True return False def getval(self, key): """値の取り出し 指定したkeyの値を返す。 Args: key (any): キー。 Return: any: 指定したキーが存在するならそのオブジェクト。存在しなければvaldefault """ v = self.root while v is not None: if key < v.key: v = v.lch elif v.key < key: v = v.rch else: return v.val self.insert(key) return self[key] # def lower_bound(self, key): """下限つき探索 指定したkey以上で最小のキーを見つける。[key,inf)で最小 Args: key (any): キーの下限。 Return: any: 条件を満たすようなキー。そのようなキーが一つも存在しないならNone。 """ ret = None v = self.root while v is not None: if v.key >= key: if ret is None or ret > v.key: ret = v.key v = v.lch else: v = v.rch return ret def upper_bound(self, key): """上限つき探索 指定したkey未満で最大のキーを見つける。[-inf,key)で最大 Args: key (any): キーの上限。 Return: any: 条件を満たすようなキー。そのようなキーが一つも存在しないならNone。 """ ret = None v = self.root while v is not None: if v.key < key: if ret is None or ret < v.key: ret = v.key v = v.rch else: v = v.lch return ret def find_kth_element(self, k): """小さい方からk番目の要素を見つける Args: k (int): 何番目の要素か(0オリジン)。 Return: any: 小さい方からk番目のキーの値。 """ v = self.root s = 0 while v is not None: t = s+v.lch.size if v.lch is not None else s if t == k: return v.key elif t < k: s = t+1 v = v.rch else: v = v.lch return None def getmin(self): ''' Return: any: 存在するキーの最小値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.lch if v == None: break return ret.key def getmax(self): ''' Return: any: 存在するキーの最大値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.rch if v == None: break return ret.key def popmin(self): ''' 存在するキーの最小値をpopする Return: any: popした値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.lch if v == None: break del self[ret.key] return ret.key def popmax(self): ''' 存在するキーの最大値をpopする Return: any: popした値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.rch if v == None: break del self[ret.key] return ret.key def popkth(self,k): ''' 存在するキーの小さい方からk番目をpopする Return: any: popした値 ''' key = self.find_kth_element(k) del self[key] return key def get_key_val(self): ''' Return: dict: 存在するキーとノード値をdictで出力 ''' retdict = dict() for i in range(len(self)): key = self.find_kth_element(i) val = self[key] retdict[key] = val return retdict def values(self): for i in range(len(self)): yield self[self.find_kth_element(i)] def keys(self): for i in range(len(self)): yield self.find_kth_element(i) def items(self): for i in range(len(self)): key = self.find_kth_element(i) yield key,self[key] def __iter__(self): return self.keys() def __contains__(self, key): return self.member(key) def __getitem__(self, key): return self.getval(key) def __setitem__(self, key, val): return self.insert(key, val) def __delitem__(self, key): return self.delete(key) def __bool__(self): return self.root is not None def __len__(self): return self.root.size if self.root is not None else 0 関数の説明 関数 説明 rotate_left - update_bias_double 平衡二分木を実現するうえで最も大事な部分です(平衡を保つための回転などを行っています)。外部で呼び出す必要はないので、使用上は気にしなくていい部分です。 insert(key,val=None) keyを平衡二分木に挿入します。valはkeyのノード値(value)です(辞書のkey,valueと同等) delete(key) 指定したkeyの要素を削除します。 menber(key) 指定したkeyの有無を判定します。 getval(key) 指定したkeyの値のノード値を返します。(dict[key]と同等) lower_bound(key) [key,inf)で最小のものを返します。 upper_bound(key) [-inf,key)で最大のものを返します。 find_kth_element(k) 小さい方からk番目のkeyを返します(最小を0番目とする) 以下元記事にはなかった追加機能です。 関数 説明 getmin() 最小のkeyを返します。 getmax() 最大のkeyを返します。 popmin() 最小のkeyを返し、削除します。(heapqと同じ操作が実現可能) popmax() 最大のkeyを返し、削除します。 popkth(k) 小さい方からk番目のkeyを返し、削除します。 get_key_val() 存在するすべてのkeyとノード値(value)をdictで返します。 values() すべてのノード値(value)を返します。(dictのvalues()と同等) keys() すべてのkeyを返します。(dictのkeys()と同等) items() すべてのkeyとノード値(value)を返します。(dictのitems()と同等) 計算量はget_key_val(),values()がO(Nlog(N))、他はO(log(N))と高速です。 その他に、使用感をdictに合わせるために特殊メゾットを定義しています。 使用例 注意点としてはkeyは大小比較可能なのものである必要があるというところです(数値、文字列、tupleなどは可能) ・普通の平衡二分木としての動作 a.py AVL = AVLTree() AVL.insert(10) AVL.insert(20) AVL.insert(30) AVL.insert(40) AVL.insert(50) print(AVL.lower_bound(15)) # 20 print(AVL.find_kth_element(2)) # 30 print(40 in AVL) # True del AVL[40] # AVL.delete(40)と等価 print(40 in AVL) # False print(list(AVL)) # [10, 20, 30, 50] print(AVL.popmin()) # 10 print(AVL.popkth(1)) # 20,30,50のうち1番目(0オリジン)の30 # 30 print(list(AVL)) # [20, 50] print(len(AVL)) # 2 ・辞書っぽい使い方 b.py AVL1 = AVLTree() AVL1['a'] = 'A' AVL1['b'] = 'B' AVL1['f'] = 'C' AVL1['aa'] = 'AA' print(list(AVL1)) # ['a', 'aa', 'b', 'f'] print(AVL1.get_key_val()) # {'a': 'A', 'aa': 'AA', 'b': 'B', 'f': 'C'} print(AVL1.getmax()) # f print(AVL1.upper_bound('e')) # b ・collections.defaultdict相当の動作 c.py # はじめにvaldefaultを指定することでdefaultdictに相当する処理が可能。 AVL2 = AVLTree(valdefault=[]) AVL2[20].append(2) AVL2[20].append(3) AVL2[20].append(6) AVL2[30].append(5) AVL2[40].append(1) AVL2[40].append(2) print(AVL2.get_key_val()) # {20: [2, 3, 6], 30: [5], 40: [1, 2]} print(AVL2[20].pop()) # 6 print(40 in AVL2) # True print(50 in AVL2) # False print(AVL2.popmax()) # 40 AVL2[50].append(5) AVL2[50].append(6) print(AVL2.get_key_val()) # {20: [2, 3], 30: [5], 50: [5, 6]} ・collections.Counter相当の動作 d.py AVL3 = AVLTree(valdefault=0) AVL3[30] += 3 AVL3[40] += 2 AVL3[50] += 1 print(AVL3.get_key_val()) # {30: 3 40: 2, 50: 1} AVL3[50] += 5 print(AVL3.get_key_val()) # {30: 3, 40: 2, 50: 6} print(list(AVL3.values())) # [3, 2, 6] for key,val in AVL3.items(): print(key,val) # 30 3 # 40 2 # 50 6 while AVL3: key = AVL3.getmin() print(key,AVL3[key]) AVL3.popmin() # 30 3 # 40 2 # 50 6 競技プログラミングでの使用例 ・atcoder ABC140F 提出結果 ・atcoder AGC005B 提出結果 ・defaultdictとしての例: atcoder diverta 2019 Programming Contest 2 提出結果 最後に LGTMしていただけると大変励みになりますので参考になった方いましたらよろしくお願いいたします。
- 投稿日:2021-08-15T16:23:50+09:00
pythonで平衡二分木を使った辞書を作ってみた(平衡二分木+defaultdict, C++ : std::map相当)
はじめに pythonのset(集合)やdict(辞書)は値の検索削除がO(1)と高速ですが、要素の大きさに順序を持つことができず、順序を気にしながらの実装を愚直にすると計算時間が大きくかかってしまうなどの課題があります。一方でc++では、順序付き集合のsetや順序付きの連想配列のmap(辞書相当)が標準で整備されており、競技プログラミングなど時間制約がタイトな場合では、標準ライブラリーの差でもpython勢は不利になることが多いです。そこで今回はc++のmapに相当する平衡二分木を用いた辞書の実装例を紹介したいと思います。 参考記事 今回は平衡二分木部分に関しては出来合いのものを用いました。 Pythonで非再帰AVL木 - 競プロ記録 他 (非再帰での実装となっておりpythonでも高速に動くためとても重宝しております) こちらで紹介されている記事の実装例にいくつか機能を付与する形で実装しました。 コードの紹介 コードがとても長くなってしまっているので、関数及び使用方法について次項で重点的に説明しようと思います。 コードの表示はこちらをクリック AVLTree_dict.py #非再帰の平衡二分木 import copy class Node: """ノード Attributes: key (any): ノードのキー。比較可能なものであれば良い。(1, 4)などタプルも可。 val (any): ノードの値。 lch (Node): 左の子ノード。 rch (Node): 右の子ノード。 bias (int): 平衡度。(左部分木の高さ)-(右部分木の高さ)。 size (int): 自分を根とする部分木の大きさ """ def __init__(self, key, val): self.key = key self.val = val self.lch = None self.rch = None self.bias = 0 self.size = 1 class AVLTree: """非再帰AVL木 Attributes: root (Node): 根ノード。初期値はNone。 valdefault (any): ノード値のデフォルト値。デフォルトではNone。(数値、リストなど可) """ def __init__(self,valdefault=None): self.root = None self.valdefault = valdefault def rotate_left(self, v): u = v.rch u.size = v.size v.size -= u.rch.size + 1 if u.rch is not None else 1 v.rch = u.lch u.lch = v if u.bias == -1: u.bias = v.bias = 0 else: u.bias = 1 v.bias = -1 return u def rotate_right(self, v): u = v.lch u.size = v.size v.size -= u.lch.size + 1 if u.lch is not None else 1 v.lch = u.rch u.rch = v if u.bias == 1: u.bias = v.bias = 0 else: u.bias = -1 v.bias = 1 return u def rotateLR(self, v): u = v.lch t = u.rch t.size = v.size v.size -= u.size - (t.rch.size if t.rch is not None else 0) u.size -= t.rch.size + 1 if t.rch is not None else 1 u.rch = t.lch t.lch = u v.lch = t.rch t.rch = v self.update_bias_double(t) return t def rotateRL(self, v): u = v.rch t = u.lch t.size = v.size v.size -= u.size - (t.lch.size if t.lch is not None else 0) u.size -= t.lch.size + 1 if t.lch is not None else 1 u.lch = t.rch t.rch = u v.rch = t.lch t.lch = v self.update_bias_double(t) return t def update_bias_double(self, v): if v.bias == 1: v.rch.bias = -1 v.lch.bias = 0 elif v.bias == -1: v.rch.bias = 0 v.lch.bias = 1 else: v.rch.bias = 0 v.lch.bias = 0 v.bias = 0 def insert(self, key, val=None): """挿入 指定したkeyを挿入する。valはkeyのノード値。 Args: key (any): キー。 val (any): 値。(指定しない場合はvaldefaultが入る) Note: 同じキーがあった場合は上書きする。 """ if val == None: val = copy.deepcopy(self.valdefault) if self.root is None: self.root = Node(key, val) return v = self.root history = [] while v is not None: if key < v.key: history.append((v, 1)) v = v.lch elif v.key < key: history.append((v, -1)) v = v.rch elif v.key == key: v.val = val return p, pdir = history[-1] if pdir == 1: p.lch = Node(key, val) else: p.rch = Node(key, val) while history: v, direction = history.pop() v.bias += direction v.size += 1 new_v = None b = v.bias if b == 0: break if b == 2: u = v.lch if u.bias == -1: new_v = self.rotateLR(v) else: new_v = self.rotate_right(v) break if b == -2: u = v.rch if u.bias == 1: new_v = self.rotateRL(v) else: new_v = self.rotate_left(v) break if new_v is not None: if len(history) == 0: self.root = new_v return p, pdir = history.pop() p.size += 1 if pdir == 1: p.lch = new_v else: p.rch = new_v while history: p, pdir = history.pop() p.size += 1 def delete(self, key): """削除 指定したkeyの要素を削除する。 Args: key (any): キー。 Return: bool: 指定したキーが存在するならTrue、しないならFalse。 """ v = self.root history = [] while v is not None: if key < v.key: history.append((v, 1)) v = v.lch elif v.key < key: history.append((v, -1)) v = v.rch else: break else: return False if v.lch is not None: history.append((v, 1)) lmax = v.lch while lmax.rch is not None: history.append((lmax, -1)) lmax = lmax.rch v.key = lmax.key v.val = lmax.val v = lmax c = v.rch if v.lch is None else v.lch if history: p, pdir = history[-1] if pdir == 1: p.lch = c else: p.rch = c else: self.root = c return True while history: new_p = None p, pdir = history.pop() p.bias -= pdir p.size -= 1 b = p.bias if b == 2: if p.lch.bias == -1: new_p = self.rotateLR(p) else: new_p = self.rotate_right(p) elif b == -2: if p.rch.bias == 1: new_p = self.rotateRL(p) else: new_p = self.rotate_left(p) elif b != 0: break if new_p is not None: if len(history) == 0: self.root = new_p return True gp, gpdir = history[-1] if gpdir == 1: gp.lch = new_p else: gp.rch = new_p if new_p.bias != 0: break while history: p, pdir = history.pop() p.size -= 1 return True def member(self, key): """キーの存在チェック 指定したkeyがあるかどうか判定する。 Args: key (any): キー。 Return: bool: 指定したキーが存在するならTrue、しないならFalse。 """ v = self.root while v is not None: if key < v.key: v = v.lch elif v.key < key: v = v.rch else: return True return False def getval(self, key): """値の取り出し 指定したkeyの値を返す。 Args: key (any): キー。 Return: any: 指定したキーが存在するならそのオブジェクト。存在しなければvaldefault """ v = self.root while v is not None: if key < v.key: v = v.lch elif v.key < key: v = v.rch else: return v.val self.insert(key) return self[key] # def lower_bound(self, key): """下限つき探索 指定したkey以上で最小のキーを見つける。[key,inf)で最小 Args: key (any): キーの下限。 Return: any: 条件を満たすようなキー。そのようなキーが一つも存在しないならNone。 """ ret = None v = self.root while v is not None: if v.key >= key: if ret is None or ret > v.key: ret = v.key v = v.lch else: v = v.rch return ret def upper_bound(self, key): """上限つき探索 指定したkey未満で最大のキーを見つける。[-inf,key)で最大 Args: key (any): キーの上限。 Return: any: 条件を満たすようなキー。そのようなキーが一つも存在しないならNone。 """ ret = None v = self.root while v is not None: if v.key < key: if ret is None or ret < v.key: ret = v.key v = v.rch else: v = v.lch return ret def find_kth_element(self, k): """小さい方からk番目の要素を見つける Args: k (int): 何番目の要素か(0オリジン)。 Return: any: 小さい方からk番目のキーの値。 """ v = self.root s = 0 while v is not None: t = s+v.lch.size if v.lch is not None else s if t == k: return v.key elif t < k: s = t+1 v = v.rch else: v = v.lch return None def getmin(self): ''' Return: any: 存在するキーの最小値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.lch if v == None: break return ret.key def getmax(self): ''' Return: any: 存在するキーの最大値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.rch if v == None: break return ret.key def popmin(self): ''' 存在するキーの最小値をpopする Return: any: popした値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.lch if v == None: break del self[ret.key] return ret.key def popmax(self): ''' 存在するキーの最大値をpopする Return: any: popした値 ''' if len(self) == 0: raise('empty') ret = None v = self.root while True: ret = v v = v.rch if v == None: break del self[ret.key] return ret.key def popkth(self,k): ''' 存在するキーの小さい方からk番目をpopする Return: any: popした値 ''' key = self.find_kth_element(k) del self[key] return key def get_key_val(self): ''' Return: dict: 存在するキーとノード値をdictで出力 ''' retdict = dict() for i in range(len(self)): key = self.find_kth_element(i) val = self[key] retdict[key] = val return retdict def values(self): for i in range(len(self)): yield self[self.find_kth_element(i)] def keys(self): for i in range(len(self)): yield self.find_kth_element(i) def items(self): for i in range(len(self)): key = self.find_kth_element(i) yield key,self[key] def __iter__(self): return self.keys() def __contains__(self, key): return self.member(key) def __getitem__(self, key): return self.getval(key) def __setitem__(self, key, val): return self.insert(key, val) def __delitem__(self, key): return self.delete(key) def __bool__(self): return self.root is not None def __len__(self): return self.root.size if self.root is not None else 0 関数の説明 関数 説明 rotate_left - update_bias_double 平衡二分木を実現するうえで最も大事な部分です(平衡を保つための回転などを行っています)。外部で呼び出す必要はないので、使用上は気にしなくていい部分です。 insert(key,val=None) keyを平衡二分木に挿入します。valはkeyのノード値(value)です(辞書のkey,valueと同等) delete(key) 指定したkeyの要素を削除します。 menber(key) 指定したkeyの有無を判定します。 getval(key) 指定したkeyの値のノード値を返します。(dict[key]と同等) lower_bound(key) [key,inf)で最小のものを返します。 upper_bound(key) [-inf,key)で最大のものを返します。 find_kth_element(k) 小さい方からk番目のkeyを返します(最小を0番目とする) 以下元記事にはなかった追加機能です。 関数 説明 getmin() 最小のkeyを返します。 getmax() 最大のkeyを返します。 popmin() 最小のkeyを返し、削除します。(heapqと同じ操作が実現可能) popmax() 最大のkeyを返し、削除します。 popkth(k) 小さい方からk番目のkeyを返し、削除します。 get_key_val() 存在するすべてのkeyとノード値(value)をdictで返します。 values() すべてのノード値(value)を返します。(dictのvalues()と同等) keys() すべてのkeyを返します。(dictのkeys()と同等) items() すべてのkeyとノード値(value)を返します。(dictのitems()と同等) 計算量はget_key_val(),values(),keys(),items()がO(Nlog(N))、他はO(log(N))と高速です。 その他に、使用感をdictに合わせるために特殊メゾットを定義しています。 使用例 注意点としてはkeyは大小比較可能なのものである必要があるというところです(数値、文字列、tupleなどは可能) ・普通の平衡二分木としての動作 a.py AVL = AVLTree() AVL.insert(10) AVL.insert(20) AVL.insert(30) AVL.insert(40) AVL.insert(50) print(AVL.lower_bound(15)) # 20 print(AVL.find_kth_element(2)) # 30 print(40 in AVL) # True del AVL[40] # AVL.delete(40)と等価 print(40 in AVL) # False print(list(AVL)) # [10, 20, 30, 50] print(AVL.popmin()) # 10 print(AVL.popkth(1)) # 20,30,50のうち1番目(0オリジン)の30 # 30 print(list(AVL)) # [20, 50] print(len(AVL)) # 2 ・辞書っぽい使い方 b.py AVL1 = AVLTree() AVL1['a'] = 'A' AVL1['b'] = 'B' AVL1['f'] = 'C' AVL1['aa'] = 'AA' print(list(AVL1)) # ['a', 'aa', 'b', 'f'] print(AVL1.get_key_val()) # {'a': 'A', 'aa': 'AA', 'b': 'B', 'f': 'C'} print(AVL1.getmax()) # f print(AVL1.upper_bound('e')) # b ・collections.defaultdict相当の動作 c.py # はじめにvaldefaultを指定することでdefaultdictに相当する処理が可能。 AVL2 = AVLTree(valdefault=[]) AVL2[20].append(2) AVL2[20].append(3) AVL2[20].append(6) AVL2[30].append(5) AVL2[40].append(1) AVL2[40].append(2) print(AVL2.get_key_val()) # {20: [2, 3, 6], 30: [5], 40: [1, 2]} print(AVL2[20].pop()) # 6 print(40 in AVL2) # True print(50 in AVL2) # False print(AVL2.popmax()) # 40 AVL2[50].append(5) AVL2[50].append(6) print(AVL2.get_key_val()) # {20: [2, 3], 30: [5], 50: [5, 6]} ・collections.Counter相当の動作 d.py AVL3 = AVLTree(valdefault=0) AVL3[30] += 3 AVL3[40] += 2 AVL3[50] += 1 print(AVL3.get_key_val()) # {30: 3, 40: 2, 50: 1} AVL3[50] += 5 print(AVL3.get_key_val()) # {30: 3, 40: 2, 50: 6} print(list(AVL3.values())) # [3, 2, 6] for key,val in AVL3.items(): print(key,val) # 30 3 # 40 2 # 50 6 while AVL3: key = AVL3.getmin() print(key,AVL3[key]) AVL3.popmin() # 30 3 # 40 2 # 50 6 競技プログラミングでの使用例 ・atcoder ABC140F 提出結果 ・atcoder AGC005B 提出結果 ・defaultdictとしての例: atcoder diverta 2019 Programming Contest 2 提出結果 最後に LGTMしていただけると大変励みになりますので参考になった方いましたらよろしくお願いいたします。
- 投稿日:2021-08-15T15:46:28+09:00
データサイエンス100本ノック(構造化データ加工編)をやってみた part8(P-069~P-078)
この記事はデータサイエンスを勉強しながら、データサイエンス協会が提供するデータサイエンス100本ノック(構造化データ加工編)を解く過程を自分用にまとめたものです。 P-001~P-016 part1 P-017~P-022 part2 P-023~P-031 part3 P-032~P-039 part4 P-040~P-051 part5 P-052~P-062 part6 P-063~P-068 part7 P-069~P-078 part8 日付の加算・減算 relativedelta() datetime型 → UNIX時間 timestamp() 曜日をint型で返す weekday() ランダムに抽出 sample() 層化抽出 train_test_split(stratify=) P-069 P-069: レシート明細データフレーム(df_receipt)と商品データフレーム(df_product)を結合し、顧客毎に全商品の売上金額合計と、カテゴリ大区分(category_major_cd)が"07"(瓶詰缶詰)の売上金額合計を計算の上、両者の比率を求めよ。抽出対象はカテゴリ大区分"07"(瓶詰缶詰)の購入実績がある顧客のみとし、結果は10件表示させればよい。 p069.py df_tmp_1 = pd.merge(df_receipt, df_product, how='inner', on='product_cd').groupby('customer_id').amount.sum().rename('amount_sum').reset_index() df_tmp_2 = pd.merge(df_receipt, df_product.query('category_major_cd == "07"'), how='inner', on='product_cd').groupby('customer_id').amount.sum().rename('07_amount_sum').reset_index() df_tmp_3 = pd.merge(df_tmp_1, df_tmp_2, how='inner', on='customer_id') df_tmp_3['07_rate'] = df_tmp_3['07_amount_sum'] / df_tmp_3['amount_sum'] df_tmp_3.head(10) P-070 drop_duplicates() P-070: レシート明細データフレーム(df_receipt)の売上日(sales_ymd)に対し、顧客データフレーム(df_customer)の会員申込日(application_date)からの経過日数を計算し、顧客ID(customer_id)、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い(なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意)。 p070.py df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']], how='inner', on='customer_id') df_tmp = df_tmp.drop_duplicates() df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str')) df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date']) df_tmp['lapsed_date'] = df_tmp['sales_ymd'] - df_tmp['application_date'] df_tmp.head(10) drop_duplicates():重複した行を削除 duplicated()を使うなら、論理演算子を用いて2行目を以下のように変更すればいい。 df_tmp = df_tmp[~df_tmp.duplicated()] P-071 relativedelta() P-071: レシート明細データフレーム(df_receipt)の売上日(sales_ymd)に対し、顧客データフレーム(df_customer)の会員申込日(application_date)からの経過月数を計算し、顧客ID(customer_id)、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い(なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意)。1ヶ月未満は切り捨てること。 p071.py df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']], how='inner', on='customer_id') df_tmp = df_tmp.drop_duplicates() df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str')) df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date']) df_tmp['lapsed_month'] = df_tmp[['sales_ymd', 'application_date']].apply(lambda x: relativedelta(x[0], x[1]).years * 12 + relativedelta(x[0], x[1]).months ,axis=1) df_tmp.head(10) relativedelta(datetime1, datetime2):引数で渡したdatetime型の差分を計算してくれる。 結果をyears、months、daysで返してくれるため、yearsには12を掛けて月に変換している。 P-072 P-072: レシート明細データフレーム(df_receipt)の売上日(sales_ymd)に対し、顧客データフレーム(df_customer)の会員申込日(application_date)からの経過年数を計算し、顧客ID(customer_id)、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い。(なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意)。1年未満は切り捨てること。 p072.py df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']], how='inner', on='customer_id') df_tmp = df_tmp.drop_duplicates() df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str')) df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date']) df_tmp['lapsed_years'] = df_tmp[['sales_ymd', 'application_date']].apply(lambda x: relativedelta(x[0], x[1]).years, axis=1) df_tmp.head(10) P-073 timestamp() P-073: レシート明細データフレーム(df_receipt)の売上日(sales_ymd)に対し、顧客データフレーム(df_customer)の会員申込日(application_date)からのエポック秒による経過時間を計算し、顧客ID(customer_id)、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い(なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意)。なお、時間情報は保有していないため各日付は0時0分0秒を表すものとする。 p073.py df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']], how='inner', on='customer_id') df_tmp = df_tmp.drop_duplicates() df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str')) df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date']) df_tmp['lapsed_date_unix'] = df_tmp['sales_ymd'].apply(lambda x: x.timestamp()) - df_tmp['application_date'].apply(lambda x: x.timestamp()) df_tmp.head(10) timestamp():datetimeオブジェクトをUNIX時間に変換 P-074 weekday() P-074: レシート明細データフレーム(df_receipt)の売上日(sales_ymd)に対し、当該週の月曜日からの経過日数を計算し、売上日、当該週の月曜日付とともに表示せよ。結果は10件表示させれば良い(なお、sales_ymdは数値でデータを保持している点に注意)。 074.py df_tmp = df_receipt[['customer_id', 'sales_ymd']] df_tmp = df_tmp.drop_duplicates() df_tmp['sales_ymd'] = pd.to_datetime(df_receipt['sales_ymd'].astype('str')) df_tmp['this_monday'] = df_tmp['sales_ymd'].apply(lambda x: x - relativedelta(days=x.weekday())) df_tmp['lapsed_date'] = df_tmp['sales_ymd'] - df_tmp['this_monday'] df_tmp.head(10) weekday():datetimeオブジェクトの曜日を整数値(int型)で返す。(月曜日は0、日曜日は6) ※weekday()と似たような関数 isoweekday():月曜日が1、日曜日が7の整数値を返す day_name():曜日を文字列で返す。('Monday', 'Tuesday',...) P-075 sample() P-075: 顧客データフレーム(df_customer)からランダムに1%のデータを抽出し、先頭から10件データを抽出せよ。 p075.py df_customer.sample(frac=0.01).head(10) sample():行または列をランダムに抽出する。 引数 説明 n 抽出する数を指定できる。 frac 抽出する割合を指定(1なら100%、0.01なら1%) random_state 乱数シードを固定 replace Trueで重複を許可(デフォルトはFalse) axis 1で列をランダムに取得(デフォルトは0、列) P-076 train_test_split() P-076: 顧客データフレーム(df_customer)から性別(gender_cd)の割合に基づきランダムに10%のデータを層化抽出データし、性別ごとに件数を集計せよ。 p076.py _, df_tmp = train_test_split(df_customer, test_size=0.1, stratify=df_customer['gender_cd']) df_tmp.groupby('gender_cd').customer_id.count().reset_index() train_test_split():scikit-learnの関数で配列やリストを2分割できる。 主に、機械学習においてデータを訓練用とテスト用に分割してホールドアウト検証を行う際に用いることが多い。 引数stratifyに均等に分割させたいデータを指定すると、そのデータの値の比率が元のデータと一致するように分割される。 問題のケースではサンプル数が少ないgender_cdが0と9のデータを逃さないために比率を固定している。 P-077 P-077: レシート明細データフレーム(df_receipt)の売上金額(amount)を顧客単位に合計し、合計した売上金額の外れ値を抽出せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。なお、ここでは外れ値を平均から3σ以上離れたものとする。結果は10件表示させれば良い。 p077.py df_tmp = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index() amount_mean = df_tmp['amount'].mean() amount_std = np.std(df_tmp['amount']) df_tmp[df_tmp['amount'] >= amount_mean + amount_std*3].head(10) preprocessing.scale()を使って平均を0、標準偏差を1に標準化して行う場合 p077.py df_tmp = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index() df_tmp['amount_ss'] = preprocessing.scale(df_tmp['amount']) df_tmp.query('abs(amount_ss) >= 3').head(10) P-078 P-078: レシート明細データフレーム(df_receipt)の売上金額(amount)を顧客単位に合計し、合計した売上金額の外れ値を抽出せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。なお、ここでは外れ値を第一四分位と第三四分位の差であるIQRを用いて、「第一四分位数-1.5×IQR」よりも下回るもの、または「第三四分位数+1.5×IQR」を超えるものとする。結果は10件表示させれば良い。 p078.py df_tmp = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index() amount_25 = df_tmp['amount'].quantile(0.25) amount_75 = df_tmp['amount'].quantile(0.75) iqr = amount_75 - amount_25 lower_lim = amount_25 - 1.5 * iqr upper_lim = amount_75 + 1.5 * iqr df_tmp[(lower_lim > df_tmp['amount']) | (upper_lim < df_tmp['amount'])].head(10) 上のように書いたが、最後の行はquery()を使って書いた方がスッキリする。 df_tmp.query('amount < @lower_lim or @upper_lim < amount').head(10)
- 投稿日:2021-08-15T14:28:21+09:00
【デジタル採点】採点斬り2021verをPythonで作ってみた
はじめに 教員にとって、採点は時間がかかる業務の一つです。高校教員で必修科目を担当する場合には、40人×8クラスの採点を一人で行わなくてはいけません。 マークシート式で試験を実施すると、採点時間が大きく削減されます。しかし定期試験では作図問題や論述問題も問いたいです。 記述採点の効率化のためには、竹内俊彦氏作成の採点革命や島守睦美氏作成の採点斬りなどの素晴らしいフリーソフトがありましたが、いずれも2000年代初頭のソフトで開発は中断されているため、実行のためにVBランタイムの導入が必要だったり、いくつかのバグが残っていたりと、2021年現在では課題があるのも事実です。 そこで、記述問題のデジタル採点を可能にするソフトウェアを作成しました。 作ったもの 採点斬り2021 できること 解答用紙の設問領域をマウスで選択します。 それぞれの問題が斬りとられて、採点画面に表示されます。 数字キーで点数を入力します。 最新版では、数字の入力規則のためにチェックボックスがあります。 Excelに出力ボタンを押すと、Excelファイルが出力されます。名前領域を画像として出力するので、名前と対応するように横に生徒番号等を入力しておけば、Vlookup関数等で直接データを扱えます。 また、元画像に点数を書き込んだファイルを出力することもできます。 処理の概要 初期設定をすると、setting/inputフォルダとsetting/outputフォルダと、setting/ini.csvが作成されます。 解答用紙の切り取り範囲は、ini.csvに書き込んでいき、範囲確定を押すとini.csvをtrimData.csvに名前を変えて、切り取る座標を保存します。 setting/trimData.csvの座標データをもとに、setting/inputの中の画像をPillowで連続トリミングします。 setting/outputの下に設問ごとにQ000Xというフォルダを作成し、切り抜いた画像を保存します。 採点作業では、Q000Xの中にQ000X/n(nは点数)という名前のフォルダを新規作成し、画像をその中に振り分けて保存していきます。 フォルダ構造に基づいて、Excel出力や採点結果画像の出力を行います。 コードの概要 TkinterでUIを作成しました。 Pyinstallerで実行ファイルにコンパイルしました。 Pillowで画像の読み込み、トリミング、文字の記入を実装しています。 openpyxlでExcelへの書き込みを実装しています。 tkinterで画像を表示するためには、pillowのimgオブジェクトはグローバル変数にしないといけないようです。低レイヤは詳しくないですが、切り取った画像などがメモリを圧迫するのは良くない気がしたので、配列の初期化→.appendで処理を書くようにしてみました。この辺については、詳しい方のアドバイスをもとに高速化を実現したいです。 採点の進捗をどう保存していくかについては工夫しました。ini.csvにログを残していこうかとも思いましたが、エラー回避を書くのがめんどくさかったので、ini.csvへの設定の保存は最小限にして、進捗等はフォルダ構造で判別するように工夫しました。Excelに出力や、採点結果画像の出力の際には、flagを立てながらos.walkでフォルダ構造を解析して、点数を出力しています。 今後の展望 Pythonで作成しているので、機械学習と相性が良いはずです。pyocr等のライブラリと連携して、自動採点ができるといいなあという野望はあります。 採点斬り2021のリンクでは、ソースコードもすべて公開しています。ぜひ、改造、改良をしてください。 著作権は放棄していません。改造する場合は、ライセンスをご確認ください。 参考にしたサイト 【Python】簡易的な仕分け機能付き画像ビューワー作ってみた 【python】マウスドラッグで画像から範囲指定する How to make a tkinter canvas rectangle transparent? 【python, pyinstaller】画像や音楽などの外部ファイルも一括でexe化して配布する
- 投稿日:2021-08-15T13:55:01+09:00
SVM 原理 簡単に
はじめに SVMの原理を極力簡単に、見返した時にわかるように記載していく。 線形分離可能(ハードマージン) この直線を $$w^TX_i+b$$ とする。 この時データ群K1, k2において $$K_1 : w^TX_i+b>0$$ $$K_2 : w^TX_i+b<0$$ が成立する。 負の数を嫌い、ラベル変数$t_i$を導入する $$K_1 : t_i=1$$ $$K_2 : t_i=-1$$ ラベル変数の導入によって全てのデータで $$t_i(w^TX_i+b)>0$$ が成立する。 ここでこの直線と各点までの距離は点と直線の距離の計算式により $$d = \frac{|w_1x_1+w_2x_2+w_3x_3....|}{\sqrt{w_1^2+w_2^2+w_3^2....}}=\frac{|w_i^TX_i+b|}{||w_i||}$$ と書き表せる。 ここでマージンMは上記直線と最寄りの点において $$M=\frac{w_i^TX_i+b}{||w_i||}=\frac{-(w_i^TX_i+b)}{||w_i||}$$ のどちらかになる。(どちらも正の値なので絶対値は取れる。) なのでここで求めたいMを最大化する式は $$maxM : M<=\frac{t_i(w_i^TX_i+b)}{||w_i||}$$ で表すことができる。 ここで両辺をMで割ると $$1<=\frac{t_i(w_i^TX_i+b)}{M||w_i||}$$ となり、等号が成立し、なおかつ $$\tilde{w}=\frac{w}{M||w||}$$ $$\tilde{b}=\frac{b}{M||w||}$$ とおくのであれば $$1=t_i(\tilde{w_i^T}X_i+\tilde{b})$$ が成立する。 この式を $$maxM : M<=\frac{t_i(w_i^TX_i+b)}{||w_i||}$$に代入すると $$maxM : M=\frac{1}{||w||}$$が得られる。 最大化問題を最小化問題に変更し、なおかつ唯一解をもつ形に変更すると $$minM : M=\frac{1}{2}||w||^2$$ が得られる。 過程が違うだけで、やることは線形回帰と同じ。 線形分離不可能(ソフトマージン) 判別できるデータは$\epsilon_i$は0となる。(イメージ何もしなくても分別ができるため) $$t_i(w^T X_i+b)\geq1-\epsilon_i$$ $\epsilon_i$も数が大きいとその回帰直線は分別に向いていないこととなる。 そのため、両者のバランスをとって最適化することが必要となる。 結果最適化式は $$min(w,\epsilon) : \frac{1}{2}||w||^2 + C\sum_{i=1}^n \epsilon_i$$ となる。 両者のバランスをとりながら最適化していく。 Cはペナルティの大きさ。 ラグランジュ乗数を求める。 難しいのでハードマージンの場合を考える。 ソフトマージンの場合、式に$C\sum_{i=1}^n \epsilon_i$が足されていると考えて問題ない。 今回解きたい式は $\frac{1}{2}||w||^2$ を $y_{i}(w^{T}x_{i})\geq 1$ という条件の元で最小化する $w$ を求める問題 だと考えるとラグランジュの未定乗数法を使用して双対問題として解くことができる。 ラグランジュの未定乗数法について詳しくはこちら この式をラグランジュ関数に置き換える $$L(w, \lambda) = \frac{1}{2}||w||^2-\sum_{i=1}^n \lambda_i(y_i(w^T x_i)-1)$$ これを解いて求まる解は元式を最小化する。この式を解くと $$w = \sum_{i_1}^n \lambda_i y_i x_i$$ が求められて代入すると $$L(\lambda_i) = \sum_{j=1}^n\lambda_i - \sum_{i=1}^n\sum_{j=1}^n\lambda_i \lambda_j y_i y_j k(x_i,x_j)$$ がもとまる。 最急降下法に代入 そもそも上の式がどう出てきてるのか調べてみる。 最急降下法の式としては $$\theta^{new} = \theta-\alpha\frac{d (目的関数)}{d\theta}$$ $\sum$の中身は求めたい物の微分になる もともとラグランジュの未定乗数法の求め方としては $$L(\lambda_i) = \sum_{j=1}^n\lambda_i - \sum_{i=1}^n\sum_{j=1}^n\lambda_i \lambda_j y_i y_j k(x_i,x_j)$$ この式を $\lambda_i$で微分すると $$\frac{d L(\lambda_i)}{d\lambda_i} = 1 - \sum_{j=1}^n\lambda_j y_i y_j k(x_i,x_j)$$ の式が得られる。 これを最急降下法の式に代入したものが $$\lambda_i^{new} = \lambda_i-\alpha(1-\sum_{j=1}^n\lambda_i y_i y_j k(x_i,x_j))$$ になる。 そもそもこの式は $$L(\lambda_i) = \sum_{j=1}^n\lambda_i - \sum_{i=1}^n\sum_{j=1}^n\lambda_i \lambda_j y_i y_j k(x_i,x_j)$$ を$\lambda_i$で最小化する時の$\lambda_i$を求める式になるが、そもそも求めたかったのは 上の式をを最小化する$\lambda_i, \lambda_j$だったはず。 この問題は$\lambda_i$を最大化する問題に書き換えることができる。(説明は省略) (数学的なトリックらしい...) そのため、降っていくイメージではなく登っていくイメージに変わるので(最大値を求める式にかわる) $$\lambda_i^{new} = \lambda_i+\alpha(1-\sum_{j=1}^n\lambda_i y_i y_j k(x_i,x_j))$$ の式を解くことと同義となる。 実際にスクラッチしてみた import numpy as np class ScratchSVMClassifier(): """ SVM分類器のスクラッチ実装 Parameters ---------- num_iter : int イテレーション数 lr : float 学習率 kernel : str カーネルの種類。線形カーネル(linear)か多項式カーネル(polly) threshold : float サポートベクターを選ぶための閾値 verbose : bool 学習過程を出力する場合はTrue Attributes ---------- self.n_support_vectors : int サポートベクターの数 self.index_support_vectors : 次の形のndarray, shape (n_support_vectors,) サポートベクターのインデックス self.SV_X_ndarray : 次の形のndarray, shape(n_samples, n_feature) サポートベクターの特徴量 self.SV_y_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラベル self.SV_ lag_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラグランジュ乗数 """ def __init__(self, num_iter, lr, kernel='linear', threshold=1e-5, verbose=False): # ハイパーパラメータを属性として記録 self.iter = num_iter self.lr = lr self.kernel = kernel self.threshold = threshold self.verbose = verbose # 最大化する関数(勾配降下の逆) def _gradient_descent(self, X, y, lag): """ ラグランジュ関数の最大化を行う Parameters ---------- X : 次の形のndarray, shape (n_samples, n_features) 訓練データ y : 次の形のndarray, shape (n_samples, 1) 目的データ lag : 次の形のndarray, shape(n_samples, 1) ラグランジュ乗数 lr : float 学習率 Returns ------- 次の形のndarray, shape (n_samples, 1) 更新後のラグランジュ関数 """ sigma = (lag * y * X).sum(axis=0) lag = lag + self.lr * (1 - ((y * X) @ sigma).reshape((X.shape[0], 1))) for i in range(lag.shape[0]): if lag[i] < 0: lag[i] = 0 # sigma = 1 * n_features # y * X = n_sample * n_futures return lag def get_SV(self, X, y, lag): """ サポートベクターの数及びサポートベクターを取得する parameters ---------------------- lag : 次の形のndarray, shape(n_samples, 1) ラグランジュ乗数 C : float サポートベクターの閾値 returns ---------------------- SV_X_ndarray : 次の形のndarray, shape(n_samples, n_feature) サポートベクターの特徴量 SV_y_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラベル SV_ lag_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラグランジュ乗数 """ index_list = [] count = 0 # 閾値より高いラグランジュ乗数の個数 count_sv = (lag > self.threshold).sum() SV_X_ndarray = np.zeros((count_sv, X.shape[1])) SV_y_ndarray = np.zeros((count_sv, 1)) SV_lag_ndarray = np.zeros((count_sv, 1)) for i in range(lag.shape[0]): if lag[i] > self.threshold: # index番号を保存 index_list.append(i) # 閾値より高いサポートベクターのパラメータを保存 SV_X_ndarray[count] = X[i] SV_y_ndarray[count] = y[i] SV_lag_ndarray[count] = lag[i] count += 1 # 使わないのでインスタンス化 self.n_support_vectors = count_sv self.index_support_vectors = index_list return SV_X_ndarray, SV_y_ndarray, SV_lag_ndarray def fit(self, X, y): """ SVM分類器を学習する。検証データが入力された場合はそれに対する精度もイテレーションごとに計算する。 Parameters ---------- X : 次の形のndarray, shape (n_samples, n_features) 訓練データの特徴量 y : 次の形のndarray, shape (n_samples, ) 訓練データの正解値 """ # 初期値の設定 lag = np.zeros((X.shape[0], 1)) lag_history = np.zeros((self.iter, X.shape[0])) # yの設定 y = y.reshape((y.shape[0], 1)) # 学習 for n in range(self.iter): lag = self._gradient_descent(X, y, lag) lag_history[n] = lag.T # SVの決定 self.SV_X_ndarray, self.SV_y_ndarray, self.SV_lag_ndarray = self.get_SV(X, y, lag) if self.verbose: #verboseをTrueにした際は学習過程を出力 print(lag_history) def predict(self, test_X): """ SVM分類器を使いラベルを推定する。 Parameters ---------- X : 次の形のndarray, shape (n_samples, n_features) サンプル Returns ------- 次の形のndarray, shape (n_samples, 1) SVM分類器による推定結果 """ f = np.zeros((test_X.shape[0], 1)) # テストデータ分推定の実行 for n in range(test_X.shape[0]): f[n] = (self.SV_y_ndarray * self.SV_lag_ndarray * (self.SV_X_ndarray @ test_X[n:n+1].T)).sum() if f[n] > 0: f[n] = 1 else: f[n] = -1 return f 終わりに 上のコードは問題なく公式のSVCと同等の分類をした。(Irisデータセットでの分類) # 公式 from sklearn.svm import SVC svc = SVC(kernel='linear', random_state=0) svc.fit(X_75, y_75) svc_pred = svc.predict(X_25) from sklearn.metrics import classification_report print(classification_report(y_25, svc_pred)) precision recall f1-score support -1 1.00 1.00 1.00 63 1 1.00 1.00 1.00 62 accuracy 1.00 125 macro avg 1.00 1.00 1.00 125 weighted avg 1.00 1.00 1.00 125 # 自作 print(classification_report(y_25, my_svc.predict(X_25))) precision recall f1-score support -1 1.00 1.00 1.00 63 1 1.00 1.00 1.00 62 accuracy 1.00 125 macro avg 1.00 1.00 1.00 125 weighted avg 1.00 1.00 1.00 125
- 投稿日:2021-08-15T13:55:01+09:00
SVM 原理 極限まで簡単に解説
はじめに SVMの原理を極力簡単に、見返した時にわかるように記載していく。 線形分離可能(ハードマージン) この直線を $$w^TX_i+b$$ とする。 この時データ群K1, k2において $$K_1 : w^TX_i+b>0$$ $$K_2 : w^TX_i+b<0$$ が成立する。 負の数を嫌い、ラベル変数$t_i$を導入する $$K_1 : t_i=1$$ $$K_2 : t_i=-1$$ ラベル変数の導入によって全てのデータで $$t_i(w^TX_i+b)>0$$ が成立する。 ここでこの直線と各点までの距離は点と直線の距離の計算式により $$d = \frac{|w_1x_1+w_2x_2+w_3x_3....|}{\sqrt{w_1^2+w_2^2+w_3^2....}}=\frac{|w_i^TX_i+b|}{||w_i||}$$ と書き表せる。 ここでマージンMは上記直線と最寄りの点において $$M=\frac{w_i^TX_i+b}{||w_i||}=\frac{-(w_i^TX_i+b)}{||w_i||}$$ のどちらかになる。(どちらも正の値なので絶対値は取れる。) なのでここで求めたいMを最大化する式は $$maxM : M<=\frac{t_i(w_i^TX_i+b)}{||w_i||}$$ で表すことができる。 ここで両辺をMで割ると $$1<=\frac{t_i(w_i^TX_i+b)}{M||w_i||}$$ となり、等号が成立し、なおかつ $$\tilde{w}=\frac{w}{M||w||}$$ $$\tilde{b}=\frac{b}{M||w||}$$ とおくのであれば $$1=t_i(\tilde{w_i^T}X_i+\tilde{b})$$ が成立する。 この式を $$maxM : M<=\frac{t_i(w_i^TX_i+b)}{||w_i||}$$に代入すると $$maxM : M=\frac{1}{||w||}$$が得られる。 最大化問題を最小化問題に変更し、なおかつ唯一解をもつ形に変更すると $$minM : M=\frac{1}{2}||w||^2$$ が得られる。 過程が違うだけで、やることは線形回帰と同じ。 線形分離不可能(ソフトマージン) 判別できるデータは$\epsilon_i$は0となる。(イメージ何もしなくても分別ができるため) $$t_i(w^T X_i+b)\geq1-\epsilon_i$$ $\epsilon_i$も数が大きいとその回帰直線は分別に向いていないこととなる。 そのため、両者のバランスをとって最適化することが必要となる。 結果最適化式は $$min(w,\epsilon) : \frac{1}{2}||w||^2 + C\sum_{i=1}^n \epsilon_i$$ となる。 両者のバランスをとりながら最適化していく。 Cはペナルティの大きさ。 ラグランジュ乗数を求める。 難しいのでハードマージンの場合を考える。 ソフトマージンの場合、式に$C\sum_{i=1}^n \epsilon_i$が足されていると考えて問題ない。 今回解きたい式は $\frac{1}{2}||w||^2$ を $y_{i}(w^{T}x_{i})\geq 1$ という条件の元で最小化する $w$ を求める問題 だと考えるとラグランジュの未定乗数法を使用して双対問題として解くことができる。 また、今回用いるカーネルは線形カーネルを用いる。(計算が簡単なため) カーネルについては昔の記事に書いたのでそちらを参照ください。 https://qiita.com/hannnari0918/items/c73ca2bc5eee70dc741e ラグランジュの未定乗数法について詳しくはこちら この式をラグランジュ関数に置き換える $$L(w, \lambda) = \frac{1}{2}||w||^2-\sum_{i=1}^n \lambda_i(y_i(w^T x_i)-1)$$ これを解いて求まる解は元式を最小化する。この式を解くと $$w = \sum_{i_1}^n \lambda_i y_i x_i$$ が求められて代入すると $$L(\lambda_i) = \sum_{j=1}^n\lambda_i - \sum_{i=1}^n\sum_{j=1}^n\lambda_i \lambda_j y_i y_j k(x_i,x_j)$$ がもとまる。 最急降下法に代入 そもそも上の式がどう出てきてるのか調べてみる。 最急降下法の式としては $$\theta^{new} = \theta-\alpha\frac{d (目的関数)}{d\theta}$$ $\sum$の中身は求めたい物の微分になる もともとラグランジュの未定乗数法の求め方としては $$L(\lambda_i) = \sum_{j=1}^n\lambda_i - \sum_{i=1}^n\sum_{j=1}^n\lambda_i \lambda_j y_i y_j k(x_i,x_j)$$ この式を $\lambda_i$で微分すると $$\frac{d L(\lambda_i)}{d\lambda_i} = 1 - \sum_{j=1}^n\lambda_j y_i y_j k(x_i,x_j)$$ の式が得られる。 これを最急降下法の式に代入したものが $$\lambda_i^{new} = \lambda_i-\alpha(1-\sum_{j=1}^n\lambda_i y_i y_j k(x_i,x_j))$$ になる。 そもそもこの式は $$L(\lambda_i) = \sum_{j=1}^n\lambda_i - \sum_{i=1}^n\sum_{j=1}^n\lambda_i \lambda_j y_i y_j k(x_i,x_j)$$ を$\lambda_i$で最小化する時の$\lambda_i$を求める式になるが、そもそも求めたかったのは 上の式をを最小化する$\lambda_i, \lambda_j$だったはず。 この問題は$\lambda_i$を最大化する問題に書き換えることができる。(説明は省略) (数学的なトリックらしい...) そのため、降っていくイメージではなく登っていくイメージに変わるので(最大値を求める式にかわる) $$\lambda_i^{new} = \lambda_i+\alpha(1-\sum_{j=1}^n\lambda_i y_i y_j k(x_i,x_j))$$ の式を解くことと同義となる。 実際にスクラッチしてみた import numpy as np class ScratchSVMClassifier(): """ SVM分類器のスクラッチ実装 Parameters ---------- num_iter : int イテレーション数 lr : float 学習率 kernel : str カーネルの種類。線形カーネル(linear)か多項式カーネル(polly) threshold : float サポートベクターを選ぶための閾値 verbose : bool 学習過程を出力する場合はTrue Attributes ---------- self.n_support_vectors : int サポートベクターの数 self.index_support_vectors : 次の形のndarray, shape (n_support_vectors,) サポートベクターのインデックス self.SV_X_ndarray : 次の形のndarray, shape(n_samples, n_feature) サポートベクターの特徴量 self.SV_y_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラベル self.SV_ lag_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラグランジュ乗数 """ def __init__(self, num_iter, lr, kernel='linear', threshold=1e-5, verbose=False): # ハイパーパラメータを属性として記録 self.iter = num_iter self.lr = lr self.kernel = kernel self.threshold = threshold self.verbose = verbose # 最大化する関数(勾配降下の逆) def _gradient_descent(self, X, y, lag): """ ラグランジュ関数の最大化を行う Parameters ---------- X : 次の形のndarray, shape (n_samples, n_features) 訓練データ y : 次の形のndarray, shape (n_samples, 1) 目的データ lag : 次の形のndarray, shape(n_samples, 1) ラグランジュ乗数 lr : float 学習率 Returns ------- 次の形のndarray, shape (n_samples, 1) 更新後のラグランジュ関数 """ sigma = (lag * y * X).sum(axis=0) lag = lag + self.lr * (1 - ((y * X) @ sigma).reshape((X.shape[0], 1))) for i in range(lag.shape[0]): if lag[i] < 0: lag[i] = 0 # sigma = 1 * n_features # y * X = n_sample * n_futures return lag def get_SV(self, X, y, lag): """ サポートベクターの数及びサポートベクターを取得する parameters ---------------------- lag : 次の形のndarray, shape(n_samples, 1) ラグランジュ乗数 C : float サポートベクターの閾値 returns ---------------------- SV_X_ndarray : 次の形のndarray, shape(n_samples, n_feature) サポートベクターの特徴量 SV_y_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラベル SV_ lag_ndarray : 次の形のndarray, shape(n_samples, 1) サポートベクターのラグランジュ乗数 """ index_list = [] count = 0 # 閾値より高いラグランジュ乗数の個数 count_sv = (lag > self.threshold).sum() SV_X_ndarray = np.zeros((count_sv, X.shape[1])) SV_y_ndarray = np.zeros((count_sv, 1)) SV_lag_ndarray = np.zeros((count_sv, 1)) for i in range(lag.shape[0]): if lag[i] > self.threshold: # index番号を保存 index_list.append(i) # 閾値より高いサポートベクターのパラメータを保存 SV_X_ndarray[count] = X[i] SV_y_ndarray[count] = y[i] SV_lag_ndarray[count] = lag[i] count += 1 # 使わないのでインスタンス化 self.n_support_vectors = count_sv self.index_support_vectors = index_list return SV_X_ndarray, SV_y_ndarray, SV_lag_ndarray def fit(self, X, y): """ SVM分類器を学習する。検証データが入力された場合はそれに対する精度もイテレーションごとに計算する。 Parameters ---------- X : 次の形のndarray, shape (n_samples, n_features) 訓練データの特徴量 y : 次の形のndarray, shape (n_samples, ) 訓練データの正解値 """ # 初期値の設定 lag = np.zeros((X.shape[0], 1)) lag_history = np.zeros((self.iter, X.shape[0])) # yの設定 y = y.reshape((y.shape[0], 1)) # 学習 for n in range(self.iter): lag = self._gradient_descent(X, y, lag) lag_history[n] = lag.T # SVの決定 self.SV_X_ndarray, self.SV_y_ndarray, self.SV_lag_ndarray = self.get_SV(X, y, lag) if self.verbose: #verboseをTrueにした際は学習過程を出力 print(lag_history) def predict(self, test_X): """ SVM分類器を使いラベルを推定する。 Parameters ---------- X : 次の形のndarray, shape (n_samples, n_features) サンプル Returns ------- 次の形のndarray, shape (n_samples, 1) SVM分類器による推定結果 """ f = np.zeros((test_X.shape[0], 1)) # テストデータ分推定の実行 for n in range(test_X.shape[0]): f[n] = (self.SV_y_ndarray * self.SV_lag_ndarray * (self.SV_X_ndarray @ test_X[n:n+1].T)).sum() if f[n] > 0: f[n] = 1 else: f[n] = -1 return f 終わりに 上のコードは問題なく公式のSVCと同等の分類をした。(Irisデータセットでの分類) # 公式 from sklearn.svm import SVC svc = SVC(kernel='linear', random_state=0) svc.fit(X_75, y_75) svc_pred = svc.predict(X_25) from sklearn.metrics import classification_report print(classification_report(y_25, svc_pred)) precision recall f1-score support -1 1.00 1.00 1.00 63 1 1.00 1.00 1.00 62 accuracy 1.00 125 macro avg 1.00 1.00 1.00 125 weighted avg 1.00 1.00 1.00 125 # 自作 print(classification_report(y_25, my_svc.predict(X_25))) precision recall f1-score support -1 1.00 1.00 1.00 63 1 1.00 1.00 1.00 62 accuracy 1.00 125 macro avg 1.00 1.00 1.00 125 weighted avg 1.00 1.00 1.00 125
- 投稿日:2021-08-15T12:40:33+09:00
夏休みなのでStreamlitで機械学習アプリを作成してみた
はじめに 仕事もお盆休みということで夏休みの自由研究ではないですが、StreamlitというPython Webフレームワークを用いた簡易なWebアプリケーション作成にトライしたので記事にまとめました。百聞は一見にしかずですので最初に実際に作成した機械学習アプリの動画を貼っておきます。 目次 Streamlitとは 代表的なWebフレームワークとの比較 Streamlitでできること デプロイ方法 作成した機械学習アプリ 作成においてハマった点 Streamlitを使って感じた課題 最後に Streamlitとは Python Webフレームワークのひとつであり、Streamlitを用いることでWeb開発で通常必要なHTMLやCSSでのフロントエンドの記述が不要で、Pythonスクリプトのみで良い感じのアプリを開発することが可能です。 代表的なWebフレームワークとの比較 PythonベースのWebフレームワークは数多くありますが、上述の通り、Pythonスクリプトのみで開発できること、また、後述しますがアプリ作成後の本番環境へのデプロイも簡易に可能なことがStreamlitのメリットになります。短所としては、細かい見た目の変更ができないことです。 私の認識では以下のような整理になります。 Framework 概要 Python HTML CSS※ デプロイ 学習コスト Django フルスタックフレームワーク、豊富な機能追加が可能、アプリ構成が複雑 〇 〇 〇 普通 高 Flask マイクロフレームワーク、手軽に開発が可能 〇 〇 〇 簡易 中 FastAPI マイクロフレームワーク、純正ライブラリ・ドキュメントが充実 〇 〇 〇 簡易 中 Streamlit Pythonのみで開発が可能、アプリ構成がシンプル、デプロイが簡単 〇 超簡易 低 ※ 正確にはDjangoなどでもCSSの用意は必須ではないです(ないと見た目はダサくなりますが) Djangoはユーザ認証機能、アプリの管理機能などもフレームワークに標準機能として搭載されているため、本格的なWebサービスや業務アプリケーションなどの開発をしたい場合に向いており、Streamlitはアプリのプロトタイプ開発に向いているように思います(アプリ開発初期に社内でデモを見せたいときに飛び道具的に使用するなど)。 Streamlitでできること 文字の表示(Markdown記法も可能) データフレーム、グラフ(Plotlyにも対応)、画像の表示 ボタン、スライダーなどのウィジェットの表示 その他 データフレームを画面表示させる場合にHTMLの面倒な記述なしに表示できるのは非常に便利だと思いました。また、グラフ表示についてはPlotlyも対応しているのでインタラクティブなグラフの表示も可能です。 ウィジェットを使用する場合の記述方法と表示例については以下のサンプルアプリにまとめましたのでご覧下さい。 デプロイ方法 デプロイする方法についてはStreamlit SharingというStreamlitで作成したアプリを無償でデプロイできるサービスを利用します。詳細は以下の記事にまとめられているのでご参照ください。慣れれば5分程度でデプロイすることも可能だと思います。 HerokuやAWS、Azureにデプロイする方法もありますので参考記事も載せておきます。 (PythonAnywhreにデプロイしている記事は見たことがないです)。 Streamlitで作ったWebアプリをHerokuにデプロイする StreamlitでPythonデータ分析ダッシュボードをサクッと作ってAWSにデプロイ Azure Web AppsとStreamlitでデータを可視化して画面を公開する 作成した機械学習アプリ ソースコード アプリの内容ですが、PyCaretライブラリを用いた複数モデルでの予測精度比較やPandas-Profilingライブラリを用いたデータフレームのEDAなどを実施することが可能なアプリです。ただし、デプロイ先のStreamlit Sharingから割り当てられるRAMリソースの関係でPyCaretは最後までは実行ができません。 また、今回PyCaretの「compare_model()」をStreamlit上で使用していますがそのままではStreamlit上で上手く動作しなかったため、ライブラリのcompare_model関数の中身をカスタマイズしています。 作成においてハマった点 今回、アプリを作成するにあたりStreamlitのbutton widgetの使用における意図しない挙動の修正に時間を費やしました。具体的には、以下の動画のような挙動です。アプリ上でbuttonを押下すると、新しいウィジェットAが出現して、ウィジェットAで次の操作を行うという作りにしていましたが、なぜかウィジェットAでの操作を終えた瞬間にbuttonの操作前の段階にページがリロードされてしまうという状況でした(動画の「Session Stateの設定なし」での挙動)。 原因としては、button widgetは押された瞬間のみTrueになりますが、その後Falseに戻るという仕様が原因でした。この仕様のため、button以降にウィジェットAで操作をした時点でbuttonはFalseに戻ります。Streamlitはコード内の変数に変化があった場合リロードされるため、今回はbuttonのところまで戻ってしまう状態でした。 対策として以下の記事の通り、StreamlitのSession State機能で変数の状態を保持することで問題を解決することができました。 Streamlitを使って感じた課題 今回、Streamlitを用いたアプリの作成から、本番環境までのデプロイを実施しました。簡易にアプリ作成ができる点は非常に便利ですが、以下の点については課題だと感じました。 Streamlit Sharingにデプロイする場合、割り当てられるRAMリソースが少ない(1GB)。今回作成したアプリの場合、RAMが少ないため、PyCaretのような重い機械学習処理が発生するものについては本番環境では最後まで動作しない状況です。 Streamlit Sharingにデプロイする場合、ソースコードはGitHub上でパブリックにしないといけない。そのため、業務用途では使用できない。 上記の2点の課題を解決するためには、現状ではHerokuやAWSなどにデプロイするしかないと思われます(ただし、Herokuの2.5GB RAMプランの場合、$250/月かかります)。今後、Streamlit Sharingにも有償でRAMリソースを選択できるプランができるととても良いと思いました。 参考 pythonで簡単なWebアプリ作成:streamlitの使い方 【PyCaret】ローコードで前処理からモデル作成まで 最後に これまでDjangoやFlaskを使ったことはありましたが、Streamlitを用いれば用途は限られますがより簡易にWebアプリの作成ができると感じました。利用するにあたっても覚えなくてはいけないこともさほど多くなく数時間学習すればすぐに使えるようになるため、学習コストが低い点も魅力的です。 Pythonを触れる人でアプリ開発に興味がある人はぜひ一度触ってみて下さい。
- 投稿日:2021-08-15T12:40:33+09:00
Python WebフレームワークのStreamlitで機械学習アプリを作成してみた
はじめに お盆休みでまとまった時間があったため、今回、前々から気になっていたStreamlitというPython Webフレームワークを用いた簡易なWebアプリケーション作成にトライしてみたので記事にまとめました。百聞は一見にしかずですので最初に実際に作成した機械学習アプリの動画を貼っておきます。 目次 Streamlitとは 代表的なWebフレームワークとの比較 Streamlitでできること デプロイ方法 作成した機械学習アプリ 作成においてハマった点 Streamlitを使って感じた課題 最後に Streamlitとは Python Webフレームワークのひとつであり、Streamlitを用いることでWeb開発で通常必要なHTMLやCSSでのフロントエンドの記述が不要で、Pythonスクリプトのみで良い感じのアプリを開発することが可能です。 代表的なWebフレームワークとの比較 PythonベースのWebフレームワークは数多くありますが、上述の通り、Pythonスクリプトのみで開発できること、また、後述しますがアプリ作成後の本番環境へのデプロイも簡易に可能なことがStreamlitのメリットになります。短所としては、細かい見た目の変更ができないことです。 私の認識では以下のような整理になります。 Framework 概要 Python HTML CSS※ デプロイ 学習コスト Django フルスタックフレームワーク、豊富な機能追加が可能、アプリ構成が複雑 〇 〇 〇 普通 高 Flask マイクロフレームワーク、手軽に開発が可能 〇 〇 〇 簡易 中 FastAPI マイクロフレームワーク、純正ライブラリ・ドキュメントが充実 〇 〇 〇 簡易 中 Streamlit Pythonのみで開発が可能、アプリ構成がシンプル、デプロイが簡単 〇 超簡易 低 ※ 正確にはDjangoなどでもCSSの用意は必須ではないです(ないと見た目はダサくなりますが) Djangoはユーザ認証機能、アプリの管理機能などもフレームワークに標準機能として搭載されているため、本格的なWebサービスや業務アプリケーションなどの開発をしたい場合に向いており、Streamlitはアプリのプロトタイプ開発に向いているように思います(アプリ開発初期に社内でデモを見せたいときに飛び道具的に使用するなど)。 Streamlitでできること 文字の表示(Markdown記法も可能) データフレーム、グラフ(Plotlyにも対応)、画像の表示 ボタン、スライダーなどのウィジェットの表示 その他 データフレームを画面表示させる場合にHTMLの面倒な記述なしに表示できるのは非常に便利だと思いました。また、グラフ表示についてはPlotlyも対応しているのでインタラクティブなグラフの表示も可能です。 ウィジェットを使用する場合の記述方法と表示例については以下のサンプルアプリにまとめましたのでご覧下さい。 デプロイ方法 デプロイする方法についてはStreamlit SharingというStreamlitで作成したアプリを無償でデプロイできるサービスを利用します。詳細は以下の記事にまとめられているのでご参照ください。慣れれば5分程度でデプロイすることも可能だと思います。 HerokuやAWS、Azureにデプロイする方法もありますので参考記事も載せておきます。 (PythonAnywhreにデプロイしている記事は見たことがないです)。 Streamlitで作ったWebアプリをHerokuにデプロイする StreamlitでPythonデータ分析ダッシュボードをサクッと作ってAWSにデプロイ Azure Web AppsとStreamlitでデータを可視化して画面を公開する 作成した機械学習アプリ ソースコード アプリの内容ですが、PyCaretライブラリを用いた複数モデルでの予測精度比較やPandas-Profilingライブラリを用いたデータフレームのEDAなどを実施することが可能なアプリです。ただし、デプロイ先のStreamlit Sharingから割り当てられるRAMリソースの関係でPyCaretは最後までは実行ができません。 また、今回PyCaretの「compare_model()」をStreamlit上で使用していますがそのままではStreamlit上で上手く動作しなかったため、ライブラリのcompare_model関数の中身をカスタマイズしています。 作成においてハマった点 今回、アプリを作成するにあたりStreamlitのbutton widgetの使用における意図しない挙動の修正に時間を費やしました(2日間くらい頭を抱えました爆)。具体的には、以下の動画のような挙動です。アプリ上でbuttonを押下すると、新しいウィジェットAが出現して、ウィジェットAで次の操作を行うという作りにしていましたが、なぜかウィジェットAでの操作を終えた瞬間にbuttonの操作前の段階にページがリロードされてしまうという状況でした(動画の「Session Stateの設定なし」での挙動)。 原因としては、button widgetは押された瞬間のみTrueになりますが、その後Falseに戻るという仕様が原因でした。この仕様のため、button以降にウィジェットAで操作をした時点でbuttonはFalseに戻ります。Streamlitはコード内の変数に変化があった場合リロードされるため、今回はbuttonのところまで戻ってしまう状態でした。 対策として以下の記事の通り、StreamlitのSession State機能で変数の状態を保持することで問題を解決することができました。 Streamlitを使って感じた課題 今回、Streamlitを用いたアプリの作成から、本番環境までのデプロイを実施しました。簡易にアプリ作成ができる点は非常に便利ですが、以下の点については課題だと感じました。 Streamlit Sharingにデプロイする場合、割り当てられるRAMリソースが少ない(1GB)。今回作成したアプリの場合、RAMが少ないため、PyCaretのような重い機械学習処理が発生するものについては本番環境では最後まで動作しない状況です(データ分析結果をグラフ表示する等、RAMをあまり消費しない用途であればまったく問題なく使えます)。 Streamlit Sharingにデプロイする場合、ソースコードはGitHub上でパブリックにしないといけない。そのため、業務用途では使用できない。 上記の2点の課題を解決するためには、現状ではHerokuやAWSなどにデプロイするしかないと思われます(ただし、Herokuの2.5GB RAMプランの場合、$250/月かかります)。今後、Streamlit Sharingにも有償でRAMリソースを選択できるプランができるととても良いと思いました。 参考 pythonで簡単なWebアプリ作成:streamlitの使い方 【PyCaret】ローコードで前処理からモデル作成まで 最後に これまでDjangoやFlaskを使ったことはありましたが、Streamlitを用いれば用途は限られますがより簡易にWebアプリの作成ができると感じました。利用するにあたっても覚えなくてはいけないこともさほど多くなく数時間学習すればすぐに使えるようになるため、学習コストが低い点も魅力的です。 Pythonを触れる人でアプリ開発に興味がある人はぜひ一度使ってみて下さい。
- 投稿日:2021-08-15T12:22:19+09:00
Python で文字列の末尾の改行を削除する
急ぐ人向け str_line.rstrip('\r\n') を使いましょう。 line_content = string_line.rstrip('\r\n') (注: Perl の chomp と異なり、元の文字列を保持する非破壊操作です。) 以下、この記事を書いた理由(愚痴) 誰もが通ると思われる道だが、きっかけは「文字列を splitlines してから iterate するの、効率悪いんじゃね?」って思ったこと。 for line in string_content.splitlines(): print(line) google って調べて、 stack overflow の質問と回答にたどり着いた。 そこの回答を見て、「改行が \r\n の場合に対応するにはどうしたらいいか」 で Bing で「python chomp」で検索し、「rstrip() 使うといい」という Blog記事を見つけ、そのまま実装してしまった。 # 注:問題ありコード def bad_line_iter(string_content): stri = io.StringIO(string_content) while True: nl = stri.readline() if nl != '': yield nl.rstrip() else: return プログラム組み込み終わってテストする。 いくつか試していて想定外の動きをするものがあった。 デバッグ出力入れて調べたらiterator が空白のみの行を空文字列にしていた。 コードを見返す。・・・ rstrip って、名前からして右空白削除の関数やん。 調べたら実際にそうだった。で、末尾の連続する特定の文字群を削除する場合には rstip に引数に与えればよい。 そういうわけで修正。 def line_iter(string_content): stri = io.StringIO(string_content) while True: nl = stri.readline() if nl != '': yield nl.rstrip(’\r\n’) else: return で、改めて「python chomp」や「python 改行削除」で検索したのだが、 rstrip(’\r\n’) を挙げているものを(少なくとも上位では)見つけられなかった。(2021年8月15日現在) そういうわけで、記事にしました。 なお、途中にでてきた Perl の chomp は破壊的関数(元も文字列を書きかえる)だが、Python の rstip は非破壊関数(元の文字列が保持される)点には注意。 >>> l = 'aaa \r\n' >>> l 'aaa \r\n' >>> print(l) aaa >>> l.rstrip() 'aaa' >>> l 'aaa \r\n' >>> l.rstrip('\r\n') 'aaa ' >>> l.rstrip('\n') 'aaa \r' >>> print(l.rstrip('\n')) aaa >>> print(l.rstrip('\r\n')) aaa
- 投稿日:2021-08-15T11:31:18+09:00
PandasのDataFrameやSeriesで重複要素を取り除く方法
初投稿かつ備忘録として残しておく。 といっても、参考になったサイトのURLを残すだけ。 〇参考URL ・https://qiita.com/kira4845/items/6798e54eb76b15329e3c 〇やりたいこと ・データフレームdfの特定の列col_aの重複要素を削除したい。 ・重複のないデータフレームをdf2とする。 ・df=(col_a , col_b , col_c, 0 , 0 , 0 , 1 , 2 , 0 , 0 , 5 , 3 ) ・df2=(col_a , col_b , col_c, 0 , 0 , 0 , 1 , 2 , 0 ) 〇解決方法 ・drop_duplicatesというメソッドを使用する。 df2 = df.drop_duplicates(subset="col_a")