sklearnに頼らずRidge回帰を自力で書いてみて正則化項の影響を考えてみるテスト
[mathjax] タイトルの通り。Losso回帰と違って損失関数を偏微分するだけで出来そうなのでやってみる。 Ridge回帰は線形回帰の1種だけれども、損失関数として最小二乗法をそのまま使わず、 (L_2)ノルムの制約を付けたものを使う((L_2)正則化)。 データとモデル 教師データ(boldsymbol{y})、訓練データ(boldsymbol{x})があるとする。 (または目的変数(boldsymbol{y})、説明変数(boldsymbol{x})があるとする。) 例えば(p)次の属性データが(n)個あり、それらと結果の対応が分かっている状況。 begin{eqnarray} boldsymbol{y} &=& begin{pmatrix} y_1 \\ y_2 \\ vdots \\ y_p end{pmatrix} , boldsymbol{x} &=& begin{pmatrix} x_{11} & x_{21} & cdots & x_{n1} \\ x_{12} & x_{22} & cdots & x_{n2} \\ vdots & vdots & ddots & vdots \\ x_{1p} & x_{2p} & cdots & x_{np} end{pmatrix} end{eqnarray} モデルは以下。特徴ベクトル(boldsymbol{w})は訓練データの重み。 特徴空間において損失を最小化する特徴ベクトルを求める問題。 begin{eqnarray} boldsymbol{y} &=& boldsymbol{w} boldsymbol{x} + k \\ boldsymbol{w} &=& begin{pmatrix} w_1 & w_2& cdots &w_p end{pmatrix} end{eqnarray} 損失関数 普通の2乗損失に正則化項((L_2)ノルムを定数倍した値)を付けたものを損失関数として利用する。 正則化項の係数はハイパーパラメータとして調整する値。逆数なのはsklearnに従う。 begin{eqnarray} L(boldsymbol{w}) = |boldsymbol{y} - boldsymbol{w} boldsymbol{x}|^2 +C |boldsymbol{w}|^2 end{eqnarray} 特徴ベクトルは以下。(mathjaxでargminが出せない...) begin{eqnarray} newcommand{argmin}[1]{underset{#1}{operatorname{arg},operatorname{min}};} boldsymbol{w} = argmin w L(boldsymbol{w}) = argmin w |boldsymbol{y} - boldsymbol{w} boldsymbol{x}|^2 + C |boldsymbol{w}|^2 end{eqnarray} 特徴ベクトルを求める 勾配=0と置けば上の式の解を得られる。 損失関数が微分可能だからできる技。 begin{eqnarray} frac{partial L(boldsymbol{w})}{partial boldsymbol{w}} &=& 2 boldsymbol{w}^T (boldsymbol{y} - boldsymbol{w} boldsymbol{x}) + C boldsymbol{w} \\ &=& 0 end{eqnarray} 変形する。 begin{eqnarray} 2 boldsymbol{x}^T (boldsymbol{x}boldsymbol{w}-boldsymbol{y}) + C boldsymbol{w} &=& 0 \\ boldsymbol{x}^T (boldsymbol{x}boldsymbol{w}-boldsymbol{y}) + C boldsymbol{w} &=& 0 \\ boldsymbol{x}^T boldsymbol{x} boldsymbol{w} -boldsymbol{x}^T boldsymbol{y} + Cboldsymbol{w} &=& 0 \\ (boldsymbol{x}^T boldsymbol{x} +C E) boldsymbol{w} &=& boldsymbol{x}^T boldsymbol{y} \\ boldsymbol{w} &=& (boldsymbol{x}^T boldsymbol{x} + C E)^{-1} boldsymbol{x}^T boldsymbol{y} end{eqnarray} テストデータを作る 練習用にsklearnのbostonデータを使ってみる。 ボストンの住宅価格が目的変数、属性データが説明変数として入ってる。 import pandas as pd import numpy as np from pandas import Series,DataFrame import matplotlib.pyplot as plt from sklearn.datasets import load_boston boston = load_boston() boston_df = DataFrame(boston.data) boston_df.columns = boston.feature_names print(boston_df.head()) boston_df[\"PRICE\"] = DataFrame(boston.target) # CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT PRICE # 0 0.00632 18.0 2.31 0.0 0.538 6.575 65.2 4.0900 1.0 296.0 15.3 396.90 4.98 24.0 # 1 0.02731 0.0 7.07 0.0 0.469 6.421 78.9 4.9671 2.0 242.0 17.8 396.90 9.14 21.6 # 2 0.02729 0.0 7.07 0.0 0.469 7.185 61.1 4.9671 2.0 242.0 17.8 392.83 4.03 34.7 # 3 0.03237 0.0 2.18 0.0 0.458 6.998 45.8 6.0622 3.0 222.0 18.7 394.63 2.94 33.4 # 4 0.06905 0.0 2.18 0.0 0.458 7.147 54.2 6.0622 3.0 222.0 18.7 396.90 5.33 36.2 散布図行列を表示してみる。 PRICEと関係がありそうなZN,RM,AGE,DIS,LSTATの5個を使ってみる。 pg = sns.pairplot(boston_df) plt.show() pg.savefig(\'boston_fig.png\') 特徴ベクトルを自力で計算する これを自力で計算してみる。(C=0.01)、(C=0)、(C=100)としてみた。 begin{eqnarray} boldsymbol{w} &=& (boldsymbol{x}^T boldsymbol{x} + C E)^{-1} boldsymbol{x}^T boldsymbol{y} end{eqnarray} X_df = boston_df.drop(columns=[\'CRIM\',\'INDUS\',\'CHAS\',\'NOX\',\'RAD\',\'TAX\',\'PTRATIO\',\'B\',\'PRICE\']) X = X_df.values y = boston.target.T C1 = 0.01 C2 = 0 C3 = 100 e = np.identity(5) w1 = np.dot( np.linalg.inv(np.dot(X.T , X) + C1 * e), np.dot(X.T,y)) w2 = np.dot( np.linalg.inv(np.dot(X.T , X) + C2 * e), np.dot(X.T,y)) w3 = np.dot( np.linalg.inv(np.dot(X.T , X) + C3 * e), np.dot(X.T,y)) print(w1) # [ 0.05338557 5.40396159 -0.01209002 -0.83723303 -0.63725397] print(w2) # [ 0.05338539 5.40403743 -0.01209427 -0.83728837 -0.63725093] print(w3) # [ 0.05612977 4.76664789 0.02374402 -0.38576708 -0.66137596] (C=0)のとき、つまり最小二乗法のとき。 sklearnを使う sklearnのridge回帰モデルを使うと以下みたいになる。 from sklearn.linear_model import Ridge from sklearn.model_selection import train_test_split Xf_train,Xf_test,yf_train,yf_test = train_test_split(X,y,random_state=0) ridge = Ridge().fit(Xf_train,yf_train) print(f\"accuracy for training data:{ridge.score(Xf_train,yf_train):.2}\") print(f\"accuracy for test data:{ridge.score(Xf_test,yf_test):.2f}\") # accuracy for training data:0.68 # accuracy for test data:0.58 print(ridge.coef_) # [ 0.06350701 4.3073956 -0.02283312 -1.06820241 -0.73188192] 出てきた特徴ベクトルを並べてみる 自力で計算したものとsklearnに計算してもらったものを並べてみる。 似てるのか似ていないのかよくわからない .. けど、RMの寄与度が高いというのは似ている。 # 自力で計算 (C=100) # [ 0.05612977 4.76664789 0.02374402 -0.38576708 -0.66137596] # sklearnで計算 # [ 0.06350701 4.3073956 -0.02283312 -1.06820241 -0.73188192] 自力で計算したモデルの正答率を求めてみないとなんとも... そして、正規化項の係数の大小がどう影響するのか、あまり良くわからなかった..。 (L_2)ノルムの制約を付けると、パラメタの大小が滑らかになると言いたかったのだけども。 あと、訓練データに対して68%、テストデータに対して58%という感じで、 大して成績が良くない...。
NumPy uniqe, File I/O
集合関数 集合関数。ndarrayから重複を取り除きsortした結果を返す。 2dであってもその中から要素を抜き出して1dにする。 hoges = np.array([\"hoge\",\"fuga\",\"hoge\",\"fuga\"]) print(np.unique(hoges)) # [\'fuga\' \'hoge\'] fugas = np.array([[\"hoge\",\"fuga\",\"hoge\",\"fuga\"],[\"hoge2\",\"fuga2\",\"hoge2\",\"fuga2\"]]) print(np.unique(fugas)) # [\'fuga\' \'fuga2\' \'hoge\' \'hoge2\'] ファイルI/O pandasを使わずとも、NumPyだけでファイルI/Oができる。 以下でhoges.npyという無圧縮バイナリファイルが作られる。 それを読み込んで出力する。 hoges = np.array([\"hoge\",\"fuga\",\"hoge\",\"fuga\"]) np.save(\'hoges.npy\',hoges) fugas = np.load(\'hoges.npy\') print(fugas) # [\'hoge\' \'fuga\' \'hoge\' \'fuga\'] 複数の配列を同時に書き込むこともできる。 キーワードを指定して書き込む。キーワードを指定して1つずつ読み込む。 読み込む時はキーワードを指定して参照したときに遅延ロードされる。 hoges = np.array([\"hoge1\",\"fuga1\",\"hoge1\",\"fuga1\"]) fugas = np.array([\"hoge2\",\"fuga2\",\"hoge2\",\"fuga2\"]) np.savez(\'hogefuga.npz\', hoges=hoges, fugas=fugas) hogefugas = np.load(\'hogefuga.npz\') hoges_l = hogefugas[\'hoges\'] fugas_l = hogefugas[\'fugas\'] print(hgoes_l) # [\'hoge1\' \'fuga1\' \'hoge1\' \'fuga1\'] print(fugas_l) # [\'hoge2\' \'fuga2\' \'hoge2\' \'fuga2\']
線形サポートベクトル分類器で画像認識するテスト
線形サポートベクトル分類器で画像認識する流れを理解したので、 定着させるために記事にしてみます。 当然、モデルの数学的な理解がないとモデルを解釈することは不可能だし、 正しいハイパーパラメータを設定することも不可能なので、数学的な理解は不可欠。 NumPy、pandas、matplotlibに慣れないと、そこまで行くのに時間がかかります。 こちらはPythonプログラミングの領域なので、数こなして慣れる他ないです。 機械学習用のサンプル画像で有名なMNISTを使ってNumPy、pandasの練習。 手書き文字認識用の画像データを読み込んでみる。サイズは28x28。各々1byte。 MNISTの手書き文字認識画像の読み込み まず読み込んでみて、データの形を出力してみる。 X_trainは、要素が3個のTupleが返る。3次。 1番外が60000。28x28の2次のndarrayが60000個入っていると読む。 1枚目の画像データはX_train[0]によりアクセスできる。 import tensorflow as tf minst = tf.keras.datasets.mnist (X_train,y_train),(X_test,y_test) = mnist.load_data() print(X_train.shape) # (60000, 28, 28) y_trainは要素が1個のTupleが返る。1次。 1枚目から60000枚目までの画像が0から9のいずれに分類されたかが入っている。 y_train[0]が4なら、1枚目の画像が4に分類された、という意味。 print(y_train.shape) # (60000,) データセットの選択 X_train,y_train、X_test,y_testから、値が5または8のものだけのViewを取得する。 そのために、まず値が5または8のものだけのインデックスを取得する。 NumPyのwhereはndarrayのうち条件を満たす要素のインデックスを返す。 X_trainに入っている60000件の2d arrayのうち、 値が5または8のインデックス(0-59999)を取得するのは以下。 index_train = np.where((X_train==5)|(X_train==8)) print(index_train) # (array([ 0, 11, 17, ..., 59995, 59997, 59999]),) index_test = np.where((X_test==5)|(X_test==8)) print(index_test) # (array([ 8, 15, 23, ..., 9988, 9991, 9998]),) インデックスを使って絞り込む。 X_train,y_train = X_train[index_train],y_train[index_train] X_test,y_test = X_test[index_test],y_test[index_test] print(X_train.shape) # (11272, 28, 28) print(X_test.shape) # (1866, 28, 28) 前処理 0-255の間の値を0-1の間の値に変換する(正規化)。 28x28の画像(2darray)を1x784(1darray)に整形する(平坦化)。 X_train,X_test = X_train / 255.0, X_test / 255.0 X_train = X_train.reshape(X_train.shape[0], X_train.shape[1] * X_train.shape[2]) X_test = X_test.reshape(X_test.shape[0], X_test.shape[1] * X_test.shape[2]) ベストなハイパーパラメータの選択 線形サポートベクトル分類器を作成する。 from sklearn.svm import LinearSVC linsvc = LinearSVC(loss=\"squared_hinge\",penalty=\"l1\",dual=False) 線形サポートベクトル分類器のハイパーパラメータCの選択 逆正則化パラメータCをGridSearchCVで探す。MBP2013Laterで学習(fit)に5分くらいかかった。 GridSearchCVからはC=0.2がbestと返ってくる。 from sklearn.model_selection import GridSearchCV param_grid = {\"C\":[0.025,0.05,0.1,0.2,0.4]} model = GridSearchCV(estimator=linsvc, param_grid=param_grid,cv=5,scoring=\"accuracy\",return_train_score=True) model.fit(X_train,y_train) print(model.cv_results_[\"mean_train_score\"]) # array([0.96291693, 0.96775192, 0.97059085, 0.97340754, 0.97626859]) print(model.cv.results_[\"mean_test_score\"]) # array([0.95626331, 0.95990064, 0.96158623, 0.9625621 , 0.96105394]) print(model.best_params_) # {\'C\': 0.2} 学習、精度評価 C=0.2を使って新しく学習させる。 linsvc = LinearSVC(loss=\"squared_hinge\",penalty=\"l1\",dual=False,C=0.2) linsvc.fit(X_train,y_train) 訓練データ、テストデータに対して正答率を求める。 訓練データについて97.2%、テストデータについて96.2%。 過学習すると訓練データが高くテストデータが低くなる。 from sklearn.metrics import accuracy_score pred_train = linsvc_best.predict(X_train) acc = accuracy_score(y_true = y_train,y_pred = pred_train) print(acc) # 0.9723207948899929 pred_test = linsvc_best.predict(X_test) acc = accuracy_score(y_true = y_test,y_pred = pred_test) print(acc) # 0.9619506966773848 モデルの解釈可能性 [mathjax] 線形SVMの決定境界(f(x))の係数をヒートマップっぽく表示して、どの係数を重要視しているかを確認する。 基本的に真ん中に画像が集まっているので、28x28の隅は使わないのが正しそう。 正則化パラメータによって係数の大きさを制御しているため、正則化パラメータを変えると係数が変わる。 今回のは(L_1)正則化なので、係数が0のものが増える..らしい(..別途調べる..)。 (f(x) = w_0 + w_1 x_1 + w_2 x_2 + cdots w_{784} x_{784}) import matplotlib.pyplot as plt weights = linsvc_best.coef_ plt.imshow(weights.reshape(28,28)) plt.colorbar() plt.show()
NumPy vector operations, universal functions, matplotlib, 3項演算, 次元削減
universal functions ndarrayの全ての要素に対して基本的な計算を実行する。 以下オペランドが1つの単項universal functions。 abs,sqrt,square,exp,log,sign,ceil,floor,rint,modf,isnan,sin,cos,arcsin,arccosなどがある。 array = np.arange(10) print(array) # [0 1 2 3 4 5 6 7 8 9] sqrt = np.sqrt(array) print(sqrt) # [0. 1. 1.41421356 1.73205081 2. 2.23606798 # 2.44948974 2.64575131 2.82842712 3. ] exp = np.exp(array) print(exp) # [1.00000000e+00 2.71828183e+00 7.38905610e+00 2.00855369e+01 # 5.45981500e+01 1.48413159e+02 4.03428793e+02 1.09663316e+03 # 2.98095799e+03 8.10308393e+03] 以下、オペランドが2つの2項universal functions。 いずれかのうち最大の値を残すmaximum()。 add,subtract,divide,power,maximum,minimum,copysign,greater,lessなどがある。 x = np.random.randn(10) y = np.random.randn(10) print(x) # [ 1.3213258 0.12423666 -1.45665939 -1.49766467 -0.6129116 2.00056744 # -0.00816571 0.63247747 0.29497652 0.80000291] print(y) # [-0.76739214 0.95151629 0.03208859 0.40641677 0.82635027 1.01773826 # 0.75601178 0.25200147 1.59929321 0.6251983 ] z = np.maximum(x,y) print(z) # [1.3213258 0.95151629 0.03208859 0.40641677 0.82635027 2.00056744 # 0.75601178 0.63247747 1.59929321 0.80000291] [mathjax] matplotlibにndarrayを引数として渡せば簡単にプロットできる。 (z=sqrt{x^2+y^2})をプロットしてみる。 import numpy as np import matplotlib.pyplot as plt points = np.arange(-5,5,0.01) xs,ys = np.meshgrid(points,points) z = np.sqrt(xs**2 +ys**2) plt.imshow(z, cmap=plt.cm.gray) plt.colorbar() plt.title(\"Image plot\") plt.show() 3項演算子 where マスクの論理値に従って2つのndarrayのうちいずれかの値を選択してリストに書く。 3項演算子を使ってPythonのlistに入れる方法は以下。 xa,xbはndarrayだが最終的なr1はPythonオブジェクト。 import numpy as np xa = np.array([1,2,3,4,5]) xb = np.array([6,7,8,9,10]) cnd = np.array([True,True,False,False,False]) r1 = [(x if c else y) for x,y,c in zip(xa,xb,cnd)] print(r1) 対して、ndarrayに対して直に3項演算子を実行するwhereがある。 import numpy as np xa = np.array([1,2,3,4,5]) xb = np.array([6,7,8,9,10]) cnd = np.array([True,True,False,False,False]) r2 = np.where(cnd,xa,xb) print(r2) 数学関数,統計関数,次元削減 (n)次のndarrayをある軸について集計して(n-1)次のndarrayにする。 集計方法としていくつかの数学関数、統計関数が用意されている。 以下5x4(2次)のndarrayについて、それぞれの列について平均を取り4列(1次)のndarrayにしている。 さらに列の平均を取りスカラーにしている。 import numpy as np ary = np.random.randn(5,4) print(ary) # [[-1.84573174 1.84169514 1.43012623 -0.5416877 ] # [-1.03660701 0.63504086 -0.12239017 -0.77822113] # [ 0.1711323 -0.16660851 -0.7928288 1.17582814] # [-0.29302267 -0.23316282 1.70611457 0.53870384] # [-0.46513289 -1.12207588 0.01930695 0.49635739]] print(ary.mean(axis=0)) # [-0.6938724 0.19097776 0.44806576 0.17819611] print(ary.mean(axis=1)) # [ 0.22110048 -0.32554436 0.09688078 0.42965823 -0.26788611] print(ary.mean()) # 0.030841804893752683
NumPy ndarray assignment, vector operation, indexing, slicing, bool indexing, transposition
大規模高速計算を前提にC言語との接続を前提にしていて、配列処理に寄せることになる。 ndarrayで確保するメモリはPythonとは別(プロセス?)で確保される。 一通り流してみる。 shape()で配列の形を応答する。2行3列。 import numpy as np data = np.random.randn(2,3) shape = data.shape print(shape) print(data) # (2, 3) # [[ 0.79004157 0.45749364 0.90854549] # [-1.91791968 2.80050094 -0.60338724]] ndarrayを作る ndarrayを作る方法は以下。 data1 = [1,2,3,4,5] data2 = [6,7,8,9,10] data = np.array([data1,data2]) print(data) # [[ 1 2 3 4 5] # [ 6 7 8 9 10]] rng = np.arange(5) print(rng) # [0 1 2 3 4] ones = np.ones((5,5)) print(ones) # [[1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.] # [1. 1. 1. 1. 1.]] # 零行列 zeros = np.zeros((3,5)) print(zeros) # [[0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0.]] # 未初期化の配列確保 empties = np.empty((5,3)) print(empties) # [[-1.72723371e-077 -1.72723371e-077 2.24419447e-314] # [ 2.24421423e-314 2.24421423e-314 2.24563072e-314] # [ 2.24421559e-314 2.24563072e-314 2.24421570e-314] # [ 2.24563072e-314 2.24421558e-314 2.24563072e-314] # [ 2.24421562e-314 2.24563072e-314 2.24421577e-314]] # 指定値で埋める fulls = np.full((2,3),5) print(full) # [[5 5 5] # [5 5 5]] # 単位行列 identities = np.identity(5) print(identities) # [[1. 0. 0. 0. 0.] # [0. 1. 0. 0. 0.] # [0. 0. 1. 0. 0.] # [0. 0. 0. 1. 0.] # [0. 0. 0. 0. 1.]] ndarrayのデータ型 ndarrayで確保されるメモリのデータ型。 実際に型に従ってメモリが確保されているため、簡単にCに渡せる。 ary = np.array((1,2,3),dtype=np.float64) print(ary) # [1. 2. 3.] # float64をint32でキャスト ary_int = ary.astype(np.int32) print(ary_int) # [1 2 3] # キャストできないとコケる ary_str = np.array([\'hoge\',\'fuga\']) ary_str_int = ary_str.astype(np.int32) # ValueError: invalid literal for int() with base 10: \'hoge\' ベクトル演算 配列に寄せる醍醐味。Pythonに数値計算用のオペランドが用意されていることがあって、 割と自然に書ける。 ary = np.array([[1,2,3],[4,5,6]]) print(ary * ary) # [[ 1 4 9] # [16 25 36]] print(ary - ary) # [[0 0 0] # [0 0 0]] print(ary * 2) # [[ 2 4 6] # [ 8 10 12]] print(ary ** 2) # [[ 1 4 9] # [16 25 36]] スライスとView 巨大なメモリへのアクセス高速化のために、np.arrayに対するスライスによるアクセスは、 同じメモリを指すViewを返す。Viewに対する操作は元のメモリを変更する。 Copyする場合は明示的にCopyをする必要がある。 ary = np.arange(10) print(ary) # [0 1 2 3 4 5 6 7 8 9] ary[5] = 500 print(ary) # [ 0 1 2 3 4 500 6 7 8 9] ary[3:5] = 999 print(ary) # [ 0 1 2 900 900 500 6 7 8 9] copied = ary.copy() print(copied) # [ 0 1 2 900 900 500 6 7 8 9] 2次元のnp.array。要素へのアクセスの仕方は2通り。 ary2d = np.array([[1,2,3],[10,20,30]]) print(ary2d) # [[ 1 2 3] # [10 20 30]] print(ary2d[1]) # [10 20 30] print(ary2d[1][0]) # 10 print(ary2d[1,0]) # 10 n次元arrayへスカラーでインデックス参照するとn-1次元が戻る。 スライス参照はn次元が戻る。 ary2d = np.array([[1,2,3],[10,20,30],[100,200,300]]) print(ary2d[1]) # [10 20 30] print(ary2d[:2]) # [[ 1 2 3] # [10 20 30]] print(ary2d[1,:2]) # [10,20] Viewの選択 ndarrayから欲しいViewを選択するために色々と条件をつけられる。 例えば、bool index参照。 data = np.random.randn(7,4) print(data) # [[-0.69179761 -1.30790477 1.7224557 -0.67436315] # [ 0.45457462 0.24713663 -0.84619583 -0.31182853] # [-1.36397651 0.51770088 -1.8459593 -1.75146057] # [ 2.38626251 -0.4747874 -0.49951212 0.61803437] # [ 1.00048197 1.21838773 -0.4828001 0.9952139 ] # [ 0.17838262 1.687342 0.81501139 -1.12800811] # [ 0.65216988 -2.57185067 0.29802975 0.28870091]] recs = np.array([\'apple\',\'orange\',\'banana\',\'mountain\',\'river\',\'moon\',\'snow\']) print(recs==\'mountain\') # [False False False True False False False] print(data[recs==\'mountain\']) # [[ 2.38626251 -0.4747874 -0.49951212 0.61803437]] reshape reshape()を使って行列の形を変える。例えば1x15のndarrayを3x5のndarrayに変換。 もちろんCopyではなくView。これは頻出っぽい。 ちなみに、転値は専用のメソッド(T)が用意されている。 data1 = np.arange(15) print(data1) # [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14] data2 = data1.reshape(3,5) print(data2) # [[ 0 1 2 3 4] # [ 5 6 7 8 9] # [10 11 12 13 14]] data3 = data2.T print(data3) # [[ 0 5 10] # [ 1 6 11] # [ 2 7 12] # [ 3 8 13] # [ 4 9 14]]
Dictionary
Dictionaryの基本 いわゆるKey-Valueのことを\"Mapping\"と呼ぶ。 PythonのDictionaryはハッシュで実現されている。 キーにはハッシュ可能オブジェクトを指定可能。ハッシュ可能=更新不可、という意味ではない。 例えばオブジェクトのインスタンスのように中身が変わっても枠が変わらなければOK。 rgbs = { (255,0,0): \'red\', (0,255,0): \'green\', (0,0,255): \'blue\' キーは重複不可。同じキーを登録すると前のKeyValueが上書きされる。 kv = { \'hoge\': 100, \'fuga\': 200 } print(kv) # {\'hoge\': 100, \'fuga\': 200} kv[\'hoge\'] = 300 print(kv) # {\'hoge\': 300, \'fuga\': 200} DictionaryのCRUD d = {\'hoge\':100,\'fuga\':200} # Create print(d[\'hoge\']) # Read d[\'hoge\'] = 300 # Update del d[\'hoge\'] # Delete d2 = dict([(1,\'hoge\'),(2,\'fuga\')]) print(d2) # {1: \'hoge\', 2: \'fuga\'} d3 = dict(hoge1=\'hoge1\',hoge2=\'hoge2\',hoge3=\'hoge3\') print(d3) # {\'hoge1\': \'hoge1\', \'hoge2\': \'hoge2\', \'hoge3\': \'hoge3\'} 辞書内包 List,Tupleと同様に内包式を書ける。 s = [\"Hoge1\",\"Hoge2\",\"Hoge3\"] k1 = {} for v in s: k1[v] = v.lower() k2 ={v.lower() for v in s} Operator Dictionaryに対する演算。 d = {\"hoge\":100} print(\'hoge\' in d) # True print(\'fuga\' not in d) # True print(len(d)) # 1 iteration中に別のkeyへのアクセスは不可 こういうことは避けるべきだけれども、実際にやるとRuntimeエラー。 d = {\'hoge1\':100,\'hoge2\':200,\'hoge3\':300} for key in d: d[\'hoge4\'] = 500 # RuntimeError: dictionary changed size during iteration ViewObject キーの集合、値の集合を参照するためのデータ構造。 Viewというからには、Dictionary内に含まれる実体を参照している。 ViewObjectは集合演算をサポートしている。 d = {\'hoge1\':100,\'hoge2\':200,\'hoge3\':300} view1 = d.keys() for key in view1: print(key) # hoge1 hoge2 hoge3 del d[\'hoge2\'] for key in view1: print(key) # hoge1 hoge3 view_x = view1 - [\'hoge1\',\'hoge2\'] print(view_x) # {\'hoge3\'} view2 = d.values() for value in view2: print(value) # 100 200 300
String
Stringの基本 他の型をStringに変換する。 a = 100 b = 3.14 c = str(a) + \'/\' + str(b) print(c) # 100/3.14 スライスで参照できる。 a = \"hogehoge\" print(a[3:5]) # eh Stringのmethod達 Stringは更新不能。method達は自分自身を破壊しない。 戻り値として新しいStringオブジェクトが戻るのみ。 大文字小文字,数値アルファベットの判定はUnicode標準に従う。 # coding: utf-8 # Your code here! print(\"Hogehoge\".capitalize()) # Hogehoge print(\"AiUeO\".casefold()) # aiueo print(\"hoge\".center(10,\'-\')) # ---hoge--- print(\"hogehoge\".count(\'h\')) # 2 print(\"hogehoge\".encode()) # b\'hogehoge\' print(\"hogehoge\".endswith(\'hoge\')) # True print(\"hoge\".find(\"ge\")) # 2 print(\"{0} is {1}\".format(\"hoge\",\"fuga\")) # hoge is fuga print(\"hogehoge\".index(\"geh\")) print(\"hoge\".isalnum()) # True print(\"1234\".isalnum()) # True print(\"()\".isalnum()) # False print(\"hoge\".isalpha()) # True print(\"1234\".isalpha()) # False print(\"1234\".isdecimal()) # True print(\"12ab\".isdecimal()) # False print(\"hoge\".isidentifier()) # True print(\"3ab\".isidentifier()) # Flase print(\"Hoge\".islower()) # False print(\"hoge\".islower()) # True print(\"1234\".isnumeric()) # True print(\"12ab\".isnumeric()) # False print(\"t\".isprintable()) # False print(\"hoge \".isspace()) # False print(\" \".isspace()) # True print(\"hoge\".istitle()) # False print(\"This Is My Hoge\".istitle()) # True print(\"--\".join([\'1\',\'2\',\'3\',\'4\'])) # 1--2--3--4 print(\"\".join([\'spam\',\'ham\',\'egg\'])) # spamhamegg print(\"hoge\".ljust(10,\'*\')) # hoge****** print(\"This is hoge\".lower()) # this is hoge print(\" hoge \".lstrip()) # hoge print(\"hoge1 hoge2 hoge3\".replace(\"hoge1\",\"fuga1\")) # fuga1 hoge2 hoge3 print(\"12345678\".rfind(\'6\')) # 5 print(\"12345678\".rindex(\'6\')) # 5 print(\"hoge\".rjust(10,\'*\')) # ******hoge print(\"hoge/fuga\".rpartition(\'/\')) # (\'hoge\', \'/\', \'fuga\') print(\"hoge*fuga\".rpartition(\'/\')) # (\'\', \'\', \'hoge*fuga\') print(\"hoge fuga hoge\".rsplit()) # [\'hoge\', \'fuga\', \'hoge\'] print(\"hoge \".rstrip()) # hoge print(\"hoget\".rstrip()) # hoge print(\"hoge hoge hoge\".split()) # [\'hoge\', \'hoge\', \'hoge\'] print(\"hoge1rhoge2nhoge3rn\".splitlines()) # [\'hoge1\', \'hoge2\', \'hoge3\'] print(\"hoge1rhoge2nhoge3rn\".splitlines(keepends=True)) # [\'hoge1r\', \'hoge2n\', \'hoge3rn\'] print(\"aiueokakikukeko\".startswith(\"aiueo\")) # True print(\" hoge \".strip()) # hoge print(\"AiUeO\".swapcase()) # aIuEo print(\"hoge hoge hoge\".title()) # Hoge Hoge Hoge print(\"hogehgoehgoe\".upper()) # HOGEHGOEHGOE print(\"hoge\".zfill(10)) # 000000hoge print(\"-foo\".zfill(10)) # -000000foo
Collection…Sequence,Slice,List,Tupple
Sequence,Slice 初めてPythonを触ると地味に取っつきにくい構文が現れるけども。 序数によるアクセスの他、範囲を指定してリストを得られる。 負の序数を指定すると後ろから数える。負のスライスも可 スライスの開始位置、終了位置はSequenceの前の方->後の方の順に書く。 長さがゼロ、またはマイナスの場合、空のリストを返す。 L = [0,1,2,3,4] a = L[1:3] print(a) # [1,2] b = L[-1:-3] print(b) # [2,3] k = K[3:1] print(k) # [] 増分値を指定することもできる。 L = [0,1,2,3,4] c = L[0:5:2] print(c) # [0,2] スライス自体もオブジェクト L = [0,1,2,3,4] s = slice(0,3) d = L[s] print(d) # [0,1,2] s2 = slice(0,None,1) print(L[s2]) # [0,1,2,3,4] スライスを構成する3要素は省略できる。 増分値に負を与えると逆順になるのを利用して要素を反転できる。 L = [0,1,2,3,4] print(L[::]) # = L[0:4:1] = [0,1,2,3,4] print(L[:]) # = L[0:4:1] = [0,1,2,3,4] print(L[::2]) # = L[0:4:2] = [0,2] print(L[::-1]) # = [4,3,2,1,0] スライスを指定して代入できる。 L = [0,1,2,3,4] L[1,4] = [\'hoge\',\'fuga\'] print(L) # [0,\'hoge\',\'fuga\',4] スライスを指定して削除できる。リストから削除するだけで要素自体を破棄しない。 L = [0,1,2,3,4] del L[0,2] print(L) # [3,4] 算術演算子、比較演算子、メンバーシップ演算子が用意されている。 Q = [1,2] * 3 print(Q) # [1,2,1,2,1,2] R1 = [1,2] R2 = [3,4] RX = R1 < R2 print(RX) # True P = [0,1,2,3,4] PX = 7 in P print(PX) # False PXX = 15 not in P print(PXX) # True Sequenceの長さはlen()で取得。 L = [0,1,2,3,4] print(len(L)) # 5 List リストの基本的な使い方は以下。 L1 = [0,1,2,3,4,5] L2 = [] for E in L1: L2.append(E) print(L2) # [0,1,2,3,4,5] 上記は以下のように1回で書ける。リスト内包とか言うらしい。 L1 = [0,1,2,3,4,5] L2 = [value for value in L1] print(L2) # [0,1,2,3,4,5] リスト内包には条件をつけられる。例えば偶数だけを集めるのは以下。 L1 = [0,1,2,3,4,5] L2 = [value for value in L1 if value % 2 == 0] # [0,2,4] iteratableを分解できる。 L = [(\'a\',1),(\'b\',2),(\'c\',3)] L1 = [c*i for c,i in L] print(L1) # [\'a\', \'bb\', \'ccc\'] iteratableの分解時に要素をリストで受けられる。 以下、タプルの第2要素以降をリストで受けて総和を求めて第1要素に掛けている。 L = [(1,2),(3,4,5),(6,7,8,9)] L1 = [car*sum(cdr) for car,*cdr in L] print(L1) # [2, 27, 144] リスト内包を入れ子にすることもできる。 years = [2013,2014] months = [1,2,3,4,5,6,7,8,9,10,11,12] dest1 = [] for year in years: for month in months: dest1.append((year,month)) print(dest1) dest2 = [(year,month) for year in years for month in months] print(dest2) Listオブジェクトの代表的なメソッド達。 L1 = [0,1,2,3,4] L1.append(5) print(L1) # [0,1,2,3,4,5] L2 = L1.copy() print(id(L1)) # 140415098962824 print(id(L2)) # 140415098984840 L2.clear() print(L2) # [] print(L1.count(2)) # 1 L3 = [\'a\',\'b\',\'c\'] L1.extend(L3) print(L1) # [0, 1, 2, 3, 4, 5, \'a\', \'b\', \'c\'] L4 = L1.index(0) print(L4) # 0 L1.insert(4,\'hoge\') print(L1) # [0, 1, 2, 3, \'hoge\', 4, 5, \'a\', \'b\', \'c\'] p = L1.pop(3) print(p) # 3 print(L1) # [0, 1, 2, \'hoge\', 4, 5, \'a\', \'b\', \'c\'] L1.remove(4) print(L1) # [0, 1, 2, \'hoge\', 5, \'a\', \'b\', \'c\'] L1.reverse() print(L1) # [\'c\', \'b\', \'a\', 5, \'hoge\', 2, 1, 0] Tuple 更新不能なリスト。要素を列挙するだけでTupleを作れる。更新できない以外はListと変わらない感じ。 \"更新できない\"という特徴があるため、辞書のキーとして使うことができる。 リストは要素としてそれぞれ独立した値が設定されることを想定。 タプルは全体で1つの要素になることを想定。タプルは「複数の要素から構成される値」。 \"更新できない\"という特徴は、メモリ割り当て時に動的確保を想定しなくて良いため、 リストよりもタプルの方がメモリ使用量を削減できるし、GCの負荷軽減に寄与する。 t1 = (1,2,3) print(t1) # (1,2,3) t2 = 3,4,5 print(t2) # (3, 4, 5) t3 = 1, print(t3) # (1,)
NP困難な分類問題を代理損失の最小化に帰着させる話
[mathjax] 機械学習の分類問題の中心にある決定境界の決定方法について かなり要領を得た説明を聞いて理解が2段階くらい先に進んだのでまとめてみます。 データが与えられただけの状態から決定境界を決める問題はNP困難ですが 別の問題に帰着させることで解を得る、というのが基本的なアイデアです。 分類の正誤とその度合いを一度に表現できるマージンを定義し、 マージンを使って与えた代理損失を最小にする問題にします。 分類問題を代理損失の最小化に帰着させるのですね。 任意の決定境界を決める問題は線形分類であってもNP困難 2値のラベルA,B付きの2次のデータポイントが与えられたとして、 入力空間(X1-X2)におけるA,Bの分離境界(decision boundary)を求める問題が\"分類\"。 直線で分離境界を書くとして、それを求めるための最も愚直な方法は以下のようなもの。 その分離境界によりデータポイントが正しく分類出来ていれば1をカウントする。 正しく分類出来ていなければ0をカウントする。 全データポイントにおける正答率を求める。 正答率が最大になるような決定境界を求める。 そもそも分離境界は直線でなくても良いのに、あえて直線ですよ、と仮定をしたとしても、 分離境界が完全に自由で、全データに対して正答率を求めないといけない。 上記の問題の計算量は(mathcal{O}(n^3))では済まない。NP困難。 計算できるように改善 分離境界の初期値を決めて、そこから正答率が良くなる方向に少しずつずらしていこうにも、 \"正しく分類されている\"=1,\"分類されていない\" =0 は、少しの変化に影響されない。 正しい=1/正しくない=0、という損失とは別の損失を作って、 その損失を使った別の問題を解くことを、上記を問題を解くことに帰着させる。 決定境界の変化に敏感な損失を作る サンプルサイズが十分大きいとき、1.で作った損失による学習結果が、「正しく分類」「正しくない分類」という損失の学習結果と一致する margin 線形分類において、分離境界(f(x_1,x_2,cdots,x_n)=w_0+w_1x_1+w_2x_2+cdots+w_nx_n)とする。 この多項式と分離の正誤、正誤の度合いは以下のように決まる。 分類の正誤は(f(x_1,x_2,cdots,x_n))の符号が決める。 分類の正誤の度合いは(f(x_1,x_2,cdots,x_n))の絶対値が決める。 (f(x_1,x_2,cdots,x_n))が正の場合、決定境界から近い場所にあるデータポイントは もしかしたら誤って分類してしまったものかもしれない。 決定境界から遠い場所にあるデータポイントは近いものよりは正しく分類しているかもしれない。 同様に(f(x_1,x_2,cdots,x_n))が負の場合、決定境界から近い場所にあるデータポイントは もしかしたら正しい分類かもしれないし、遠いデータポイントはより近いものより間違っている可能性が高い。 この事実を1つの式で表す。 データポイントには出力ラベル(y=pm 1)が付いているものとする。 判別関数を(f(x_1,x_2,cdots,x_n))とする。決定境界は(f(x_1,x_2,cdots,x_n)=0) begin{eqnarray} m = yf(x_1,x_2,cdots,x_n) end{eqnarray} ラベル1を-1と分類した場合(f(x_1,x_2,cdots,x_n)<0)。 同様に-1を1と分類した場合も(f(x_1,x_2,cdots,x_n)<0)。 つまり、誤分類したときにラベルと判別関数の符号が異なり(m0)となる。 ということで、(m)をマージン(margin)と呼ぶ。 サポートベクトル marginが最大になるように各データポイントの中にある決定境界を決めていく。 全てのデータポイントについて距離を計算する必要はなく、決定境界と距離が一番近いデータポイントとの 距離を最大化すれば良いらしい。(それが一番近いかどうかはいずれにせよ距離を求める必要がありそうだけど..) marginが最大になるように決めた決定境界と距離が最も近いデータポイントをサポートベクトルと言うらしい。 マージンを使った損失 最初に戻ると、決定境界の変化に敏感な損失を作ることが目的だった。 マージンが正の方向に大きいほど正しい分類であると言えるし、 マージンが負の方向に大きいほど誤った分類であると言えるけれども、 正しい度合いが高ければ小、誤りの度合いが高ければ大、となる損失を考えることで、 誤った方向に決定境界を修正すれば敏感に値が上昇する損失にすることができる。 (正しい方向に移動しても変わらない。) 横軸にマージン、縦軸に損失を取ったとして、以下のような損失(h(m))を考える。 もちろん、(m = yf(x_1,x_2,cdots,x_n))。 (m=1)より大きいマージンについては損失が0。(m=1)より小さいマージンについて線形に増加する。 (m=1)を境にヒンジの形をしているのでhinge損失という名前が付いてる。 begin{eqnarray} h(m) = max(0,1-m) end{eqnarray}
Reference,unpack,同時代入,変数の削除,3項演算子
Reference 変数はオブジェクトの参照。ということなので、オブジェクトのidを確認してみる。 hoge = [1,2,3,4] fuga = hoge print(hoge) # [1,2,3,4] print(fuga) # [1,2,3,4] print(id(hoge)) # 4359573320 print(id(fuga)) # 4359573320 hoge.append(5) print(hoge) # [1,2,3,4,5] print(fuga) # [1,2,3,4,5] 代入したときにどうなるか。新しい箱が作られてそちらを指すようになる。 foo = 1 bar = foo print(foo) # 1 print(bar) # 1 print(id(foo)) # 4359573320 print(id(bar)) # 4359573320 foo = 2 print(foo) # 2 print(bar) # 1 print(id(foo)) # 4476302432 print(id(bar)) # 4359573320 Unpack,同時代入 代入の右辺にiterableを置き、左辺に複数の変数を置くと、 左辺の各変数にiterableの各要素が代入される。 数が合わないとエラー。 foo = [1,2,3,4,5] a1,a2,a3,a4,a5 = foo print(a1) # 1 print(a2) # 2 print(a3) # 3 print(a4) # 4 print(a5) # 5 左辺にリストを置けるが、Unpackの評価においてリストも分解される。 c1,[c2,c3],c4 = [1,[2,3],4] print(c1) # 1 print(c2) # 2 print(c3) # 3 print(c4) # 4 リストでなくても同時代入は可能。 d1,d2 = 100,200 print(d1) # 100 print(d2) # 200 左辺と右辺の個数が合わないケース。通常は下記の通りエラーになる。 ただし左辺に*があるとよしなにやってくれる。 bar = [1,2,3] b1,b2 = bar # ValueError: too many values to unpack (expected 2) e1,*e2 = bar print(e1) # 1 print(e2) # [2,3] *f1,f2 = bar print(f1) # [1,2] print(f2) # 3 オブジェクトの削除 オブジェクトは明示的に削除できる。 foobar = 1024 print(id(foobar)) # 4348092368 del(foobar) print(id(foobar)) # NameError: name \'foobar\' is not defined 3項演算子 ?ではなくif..elseで書く。 a if b else c の場合、bが真ならa,偽ならc。 片方だけ評価される(short circuit)。 a = 100 b = 200 y = True x = a if y else b z = a if not y else b print(x) # 100 print(z) # 200