AtomとVSCodeのJupyter Notebook環境を比較した
仕事でよくJupyter Notebookを使ってのデータ解析を行うのですが、最近いちいちブラウザでNotebookを起動するのが面倒になってきました。そこでエディタを使ってJupyter Notebookを使おうと思って色々調べていたら、AtomとVSCodeを使うのがどうやらよさそうだということがわかりました。
そこで、この記事では、両方の環境を使ってみてどちらがより使いやすいか比較してみたいと思います。
環境構築
PythonとJupyter Notebookを使いたい場合には、Anacondaで入れてしまうのが一番簡単です(特にWindowsでは)。
weblabo.oscasierra.net
エディタをインストールする
AtomやVSCodeのインストール方法は下のリンクからダウンロード、インストールするだけです。
atom.io
code.visualstudio.com
Python環境を整える
次に各エディタにPythonの環境を整えていきます。
Atomの場合:
https://hajipro.com/python/atom-python
手順はAtomのほうが圧倒的に楽です。
Jupyter Notebookを使う環境を整えていく
使ってみる
いよいよ使ってみます。どちらのほうが使い勝手が良いでしょうか?
普通に使ってみる
まずは普通に、算術計算に使ってみます。次のコードを実行してみます。
x = 1 y = 2 x + y
両方ともコードブロックはブロックの先頭に#%%
を入れることで表現できます。
グラフを書く
下のコードを実行してみます。
import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 500) y = np.sin(x) plt.figure() plt.plot(x, y) plt.show()
Atom
きれいに出力されました。
機械学習を原理から理解する 回帰
機械学習をただ使うだけでなく、どのような原理で動いているのか理解するために数学的な観点からちゃんとアルゴリズムを見てみます。
前回は線形回帰の記事を書きました。
pyhaya.hatenablog.com
今回は、前回の線形回帰のアルゴリズムを拡張して、高次の回帰をしていきます。
前回の復習(線形回帰)
前回はデータ点を直線で回帰するために、最小二乗法を使った方法について説明しました。ここでは回帰直線を
というように重みパラメータとデータの内積で表現しました。そして、適切な重みを見つけるための指針として正解との二乗誤差
を最小にしようということになり、行列計算によってこのような重みパラメータを得られることを見ました。
高次多項式での回帰
前回の記事だけでも、複数の特徴量の線形和の形での回帰は可能です。今回はさらにバリエーションを増やすために高次多項式での回帰を行います。つまり、回帰曲線は下のような式であらわされます。
見やすさのため変数は1つのものを書いています。また、いつも通りバイアスは重みパラメータ内部に入れています。
理論
実は、理論的にはこの回帰は線形回帰と同一の方法で実行することができます。というのも、(3)式はベクトルの形式で書き直すと
ここでは
です。最後の1はバイアスを重みパラメータの中に取り込んだために出てきた項です。この形は線形回帰でやったものと全く同じです。なので同じアルゴリズムで解くことができます。
添え字は訓練データの番号です。
実装(Python3)
上の理論を見ながらPythonで実装を行ってみます。
import matplotlib.pyplot as plt import numpy as np from sklearn.model_selection import train_test_split from sklearn.datasets import make_regression def Regression(X, y, dim): d = X.shape[1] + 1 A = np.zeros((d+dim-1, d+dim-1)) b = np.zeros((d+dim-1, 1)) for i in range(len(X)): x = [] for j in range(1, dim+1): x += list(X[i]**j) x.append(1) x = np.array(x).reshape((d+dim-1, 1)) A += x.dot(x.T) b += y[i] * x return np.linalg.inv(A).dot(b) if __name__ == "__main__": X, y = make_regression( n_samples=50, n_features=1, noise=4, random_state=42 ) dim = 2 #何次の多項式で回帰を行うか pred = Regression(X, y, dim) x = np.linspace(-2, 1.9, 50) y_pred = np.zeros_like(x) for i in range(0, dim): y_pred += pred[i][0] * x**(i+1) y_pred += pred[dim][0] plt.figure() plt.plot(X, y, 'o') plt.plot(x, y_pred) plt.show()
回帰結果を見てみる
回帰結果を次数を変えてみてみます。まずは二次の場合フィッティング結果は下のようになります。
回帰はうまくいっている印象です。次に次数を増やしてみます。十次のとき
明らかにノイズも拾って回帰してしまっています。これは訓練データを過度に学習してしまっている状況で、過学習状態となっています。
機械学習を原理から理解する 線形回帰
最近始めた機械学習の理論的な話の3つめです。相変わらず線形の話が続きます。過去記事を下に貼っておきます。
pyhaya.hatenablog.com
pyhaya.hatenablog.com
今回は、前回までの分類アルゴリズムとは異なり、回帰アルゴリズムについて書きます。
回帰とは
回帰というのは、データセットがあったときにパラメータと目的変数の間に成り立つ関係式を見つける操作です。実験とかで結果を理論曲線でフィッティングするイメージです。
...もう少し具体的な例を出すと、例えば、パラメータとして、過去1カ月分の降水量と気温、天候が与えられたときにある特定の日に収穫されたリンゴの糖度(目的変数)を予測するとかなると回帰を使います。降水量、気温、天候というデータを組み合わせることでリンゴの糖度を表現するわけです。
線形回帰
線形という表現からもわかるように、回帰の式に一次関数を使うのが線形回帰です。例えばパラメータが1つあって(xと書く)、そこからある変数の値(yと書く)を予測したいとします。いま、学習のためにいくつかデータが与えられていて、下のグラフのように分布しているとします。
これはおおむね線形に分布していることがわかります。これを一変数の一次関数で回帰したのがオレンジの直線になります。
回帰直線を求める
パラメータ
線形回帰をする際に求める必要があるのは次の2種類のパラメータです。一つは重みパラメータそしてもう一つがバイアスです。回帰に使うパラメータが1種類の時には重みパラメータは2次元平面上の直線の傾きを表します。
ここでは与えられている学習データで、がそのデータから計算した目的変数の予測値です。ふつうは、バイアスも重みパラメータの中に取り込んだ下のような形を使います。
ここで,
損失関数
回帰の際には、どれくらいうまく回帰ができているかを知るために損失関数を定義します。回帰によってこの損失関数を最小化することが目標になります。
線形回帰の際によく用いられる損失関数は二乗誤差です。二乗誤差は下のように定義されます。
は学習データの数です。
Pythonで書く
上の計算をPythonのコードに書き下すと下のようになります。
import matplotlib.pyplot as plt import numpy as np from sklearn.model_selection import train_test_split from sklearn.datasets import make_regression def LinearRegression(X, y): A = np.zeros((X.shape[1]+1, X.shape[1] + 1)) b = np.zeros((X.shape[1]+1, 1)) for i in range(len(X)): x = list(X[i]) x.append(1) x = np.array(x).reshape((2, 1)) A += x.dot(x.T) b += y[i] * x return np.linalg.inv(A).dot(b) if __name__ == '__main__': X, y = make_regression( n_samples=50, n_features=1, noise=7, ) w = LinearRegression(X, y) fit_x = np.linspace(-2, 2, 500) fit_y = fit_x * w[0, 0] + w[1, 0] plt.figure() plt.plot(X, y, 'o') plt.plot(fit_x, fit_y) plt.show()
9, 10行目でX.shape[1]+1
のように+1しているのは、バイアスを重みパラメータの中に取り込むために、配列の要素数を1つ増やすためです。
このコードでは、簡単のために変数は1種類にしています。そのため計算結果は簡単に図示することができて、
うまくいっていることが見て取れます。
逆行列が存在しないとき
ここからは、少しだけ発展的な内容になります。
このアルゴリズムでは、計算に逆行列を使っています。しかし、逆行列は必ずしも存在するとは限りません。このようなときにはどのように計算すればよいか考えます。
逆行列が存在しないときというのは、線形代数の知識を使えば固有値に0を含むときであることがわかります。このようなときに行列Aを
のように直交行列と固有値を対角成分にもつ対角行列を使って分解することができます。の対角成分に0が含まれなければ、Dのすべて対角成分をすべて逆数にした行列がDの逆行列になり、実際、
のように、真ん中から次々に単位行列に代わっていきます(直交行列は転置して複素共役をとったと掛けると単位行列になる性質を利用しています。 Wiki参照)。
対角成分に0が含まれていては、このような操作は行うことができません。この時にはを次のように定義します。
要するに、逆数とれるならとって、とれないなら0のままにしておくということです。このようなによって計算されるを
として逆行列の代わりに使えば、
となります。は中の列ベクトルです。最右辺を見てみると、がであるようなについてのによって張られる空間への射影になっていることがわかります。このように空間を制限して考え直すことで、逆行列が存在しない場合においても回帰計算をすることが可能になります。
機械学習を原理から理解する パーセプトロン
内容的には、前回の記事の続き的な感じになります。
pyhaya.hatenablog.com
今回は、二値分類をパーセプトロンを使って実装します。パーセプトロンといえば、ニューラルネットワークやディープラーニングの基本という感じの学習器です。前回に引き続き、理論を説明した後にPythonでの実装を示します。
対象とする問題
いくつかの特徴量が与えられていて、それに対して正解ラベルが+1, -1で与えられているような状況を考えます。考える仮説空間はhalf-spacesクラス(日本語訳がわからない)です。
仮説空間という言葉が出てくると難しく感じますが、予測に使うのは下のような関数です。
は重みとバイアスです。基本的には線形関数で、最終的にsignで符号だけ取り出すことで予測値を算出しています。
どのように解くか
前回の記事では、これを線形計画問題だと考えて解きました。今回は、線形計画に比べると直感的な方法でパラメータを求めていきます。
まず、パラメータをで初期化します。なぜこのような値で初期化するかというと、こうすることで
となって、ひとまず最初のデータに関して学習させることができるためです。
次にやることははこれで最適か調べることです。学習データを(2)式に代入していって、不等式が成り立っているか確かめます。すべての学習データで不等式が成立していればこのパラメータが最適値とすることができます。しかしほとんどの場合はこううまくはいきません。番目のデータで
となってしまった場合を考えましょう。どうすればよいでしょう。要するに、左辺を増やす方向にを更新すればよいです。どうやるかというと、
このとき、(4)式の左辺は
となります。ここで、正解ラベルは±1しかとらないことを使いました。無事左辺を増やせました。このようにして、すべての学習データについて(2)式が成り立つまで値の更新を続けます。
Pythonでの実装
値の更新は、現実問題としてすべての学習データを間違えることなく予測することは不可能なことが多いので、適当なところで打ち切ります。100回の値更新で打ち切るようにしたのが下のプログラムです。
import numpy as np from sklearn.model_selection import train_test_split from sklearn.datasets import make_classification def Perceptron(X, y): X = np.array(X) w = np.zeros(len(X[0])) w = y[0] * X[0] for count in range(100): success = True for i in range(len(X)): if np.sign(w.dot(X[i])) == np.sign(y[i]): continue else: w += y[i]*X[i] success = False break if success: break return w if __name__ == '__main__': #サンプル生成 X, y = make_classification( n_samples=500, n_classes=2, n_features=2, n_redundant=0, class_sep=1.5, ) y = list(map(lambda x: -1 if x == 0 else 1, y)) X = np.array(X) y = np.array(y) X_train, X_test, y_train, y_test = train_test_split(X, y) w = Perceptron(X_train, y_train) accuracy = np.sum(y_test*X_test.dot(w) > 0) print(accuracy / len(X_test))
これを実行すると、だいたい95%程度の精度が出ていることが確認できます。
Kaggleのタイタニックデータの解析
Kaggleの定番データセットといえば「タイタニックの生存者予測」です。今回は生存者の予測を目指して解析を行っていきたいと思います。データの可視化について詳しい説明は前回記事で書いているのでそちらを参照してください。
Titanic: Machine Learning from Disaster | Kaggle
解析環境
- Kaggleのカーネル
データの全体像の把握
データ解析をする際に最も重要なことはもちろんデータを理解することです。まずは大雑把にデータの全体像を見てみましょう。
import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import os import warnings warnings.filterwarnings('ignore') print(os.listdir("../input")) # -> ['train.csv', 'gender_submission.csv', 'test.csv']
train = pd.read_csv('../input/train.csv') test = pd.read_csv('../input/test.csv') train.head()
いくつかの変数があることがわかります。
- PassengerId : 乗客ID
- Survived : 生存ラベル(1: 生存 0:死亡)
- Pclass : 身分(1が最高)
- Name : 乗客氏名
- Sex : 性別
- Age : 年齢
- SibSp : 乗客中の親戚の人数
- Parch : 乗客中の親もしくは子供の人数
- Ticket : チケット番号
- Fare : 乗船料
- Cabin : 客室番号
- Embarked : どこの港で乗船したか
変数の吟味
これらの変数の中で、中心となるのはもちろん'Survived'です。これにほかの変数が影響を与えうるか考えてみます。
'PassengerId', 'Ticket'
'PassengerId'はただの通し番号なので、生存には関係ない気がします。ただ、これが乗客の身分や部屋の位置に関係する可能性は一度調べる必要がありそうです。これは'Ticket'にも言えます。
'Pclass', 'Cabin', 'Fare'
'Pclass'に関しては、漠然と、身分の高い人は安全なところに客室があるのかなと思うので、関係ありそうです。これも後で解析しましょう。同様に'Cabin'と'Fare'も調べてみます。
'Sex', 'Age'
'Sex'や'Age'に関しては、欧米ではレディーファーストの精神が強いので女性や子供が優先的に救助されていた可能性があります。
'Name'
'Name'は生存とは全く関係なさそうに見えます。ただ、上の表を見てみると、乗客の名前には最初に'Mr'や'Mrs'などがついていることがわかります。これはその人の社会的な地位や状態を表しているといえるので、ここの情報だけは使える気がします。
'SibSp', 'Parch'
これらは親戚関係のパラメータですが、正直、これらのパラメータが'Survived'に効いてくるストーリーは思いつきませんでした。相関係数だけ調べて、早々に落としてもいいかもしれません。
'Embarked'
乗船地です。これはその土地の発展状況によって乗客層の経済力を示している可能性があります。
カテゴリ変数を数値へ変換していく
'Name'を変換する
'Name'の変換は少し手間です。まずは正規表現を使って敬称を取り出し、'Title'という名前の新しいカラムを作ります。
train['Title'] = train['Name'].str.extract(r'([A-Za-z]+)\.', expand=False) test['Title'] = test['Name'].str.extract(r'([A-Za-z]+)\.', expand=False)
数値に置き換える
では、'Sex', 'Embarked', 'Title'を数値に変換していきます。これにはsklearn.preprocessing.LabelEncoder
を使います。
from sklearn.preprocessing import LabelEncoder for col in ['Sex', 'Title']: le = LabelEncoder() le.fit(train[col]) train[col] = le.transform(train[col]) le.fit(test[col]) test[col] = le.transform(test[col]) train['Embarked'] = train['Embarked'].map({'S' : 1, 'C' : 2, 'Q' : 3}) test['Embarked'] = train['Embarked'].map({'S' : 1, 'C' : 2, 'Q' : 3})
多分'Embarked'はnull valueがあってうまくいかなかったので普通にmap
を使って置き換えました。
'Name'を落とす
もう名前情報はいらなくなったので落としてしまいましょう。
train.drop('Name', axis=1, inplace=True) test.drop('Name', axis=1, inplace=True)
ここまでやってきて、データの状況は下のようになっています。
Ticketを処理する
現時点ではまだ、'Ticket'が数値量になっていません。しかし、どのように数値に変換してよいのかはっきりしないので、もう少し詳しくこの変数について知ることにします。
いくつかTicketを眺めてみると、数字だけからなるチケット番号と、アルファベットが混ざるものの2つがあることがわかります。まずはこれから分離してみます。
number_ticket = train[train['Ticket'].str.match('\d+')] num_alpha_ticket = train[train['Ticket'].str.match('[A-Z]+.+')]
数字だけのチケット
数字だけのチケットnumber_ticket
についてまず見てみましょう。数字がどんなふうに分布しているか見るために、簡単なプロットをしてみます。
number_ticket['Ticket'] = number_ticket['Ticket'].apply(lambda x: int(x)) number_ticket.sort_values('Ticket', inplace=True) plt.figure() plt.plot(number_ticket['Ticket'], '-o') plt.show()
大きく分けて2つのグループに分かれている様子が見て取れます。ylim
をいじって拡大してみると、チケット番号は以下の5通りに分けられることがわかります。
- 10000以下
i*1e+4
より大きく(i+1)*1e+4
以下 (i=1,2,3)- 300000以上
このグループ分けをしたときに、生存率に差が出るか見てみましょう。
x = [1, 2, 3, 4, 5] lowest_num_ticket = number_ticket[number_ticket['Ticket'] <= 100000] sec_lowest_num_ticket = number_ticket[(number_ticket['Ticket'] > 100000) & (number_ticket['Ticket'] < 200000)] thir_lowest_num_ticket = number_ticket[(number_ticket['Ticket'] > 200000) & (number_ticket['Ticket'] < 300000)] four_lowest_num_ticket = number_ticket[(number_ticket['Ticket'] > 300000) & (number_ticket['Ticket'] < 400000)] high_num_ticket = number_ticket[number_ticket['Ticket'] > 3000000] y = [lowest_num_ticket['Survived'].mean(), sec_lowest_num_ticket['Survived'].mean(), thir_lowest_num_ticket['Survived'].mean(), four_lowest_num_ticket['Survived'].mean(), high_num_ticket['Survived'].mean() ] plt.figure() plt.bar(x, y) plt.xlabel('ticket number') plt.ylabel('Survived') plt.show()
数字が大きいほど生存率が小さくなるという傾向がありそうです。
アルファベットが入ったチケット
こちらは上に比べて分類が面倒です。正規表現を使ってうまくやっていきます。
A_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('A.+')] CA_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('C\.*A\.*.+')] PC_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('PC.+')] PP_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('PP.+')] SOTON_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('SOTON.+')] STON_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('STON.+')] LINE_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('LINE.*')] FC_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('F\.C\.(C\.)*.+')] W_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('W.+')] C_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('C.+')] SC_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('S(\.)*C.+')] SO_ticket = num_alpha_ticket[num_alpha_ticket['Ticket'].str.match('S(\.)*O.+')] other_ticket = num_alpha_ticket[ num_alpha_ticket['Ticket'].str.match( '(Fa)*(P/PP)*(S\.P)*(S\.*W)*.+' ) ] x = [i for i in range(1, 14)] y = [A_ticket['Survived'].mean(), CA_ticket['Survived'].mean(), PC_ticket['Survived'].mean() ,PP_ticket['Survived'].mean(), SOTON_ticket['Survived'].mean(), STON_ticket['Survived'].mean() ,LINE_ticket['Survived'].mean(), FC_ticket['Survived'].mean(), W_ticket['Survived'].mean() ,C_ticket['Survived'].mean(), SC_ticket['Survived'].mean(), SO_ticket['Survived'].mean() ,other_ticket['Survived'].mean() ] plt.figure() plt.bar(x, y) plt.ylabel('survived') plt.show()
このように場合分けが多岐にわたるときには、漏れがないか確認する手段を持っておくことが重要です。今回の場合は、セット型を使ってチェックが可能です。
new_set = set(A_ticket['Ticket']) | set(CA_ticket['Ticket']) |\ set(PC_ticket['Ticket']) | set(PP_ticket['Ticket']) |\ set(SOTON_ticket['Ticket']) | set(STON_ticket['Ticket']) |\ set(LINE_ticket['Ticket']) | set(FC_ticket['Ticket']) |\ set(W_ticket['Ticket']) | set(C_ticket['Ticket']) |\ set(SC_ticket['Ticket']) | set(SO_ticket['Ticket']) |\ set(other_ticket['Ticket']) set(num_alpha_ticket['Ticket']) - new_set
これで計算結果が空のセットになることを確認します。
グラフを見ると、チケットによって生存率には大きな差が出ていることがわかります。
チケットのラベリング
この結果をもとにして、チケットのラベリングを行います。
number_ticket.loc[number_ticket['Ticket'] <= 100000, 'Ticket'] = 14 number_ticket.loc[(number_ticket['Ticket'] > 100000) & (number_ticket['Ticket'] <= 200000), 'Ticket'] = 15 number_ticket.loc[(number_ticket['Ticket'] > 200000) & (number_ticket['Ticket'] <= 300000), 'Ticket'] = 13 number_ticket.loc[(number_ticket['Ticket'] > 300000) & (number_ticket['Ticket'] <= 400000), 'Ticket'] = 5 number_ticket.loc[number_ticket['Ticket'] > 3000000, 'Ticket'] = 6 num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('A.+'), 'Ticket'] = "1" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('C\.*A\.*.+'), 'Ticket'] = "8" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('PC.+'), 'Ticket'] = "16" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('PP.+'), 'Ticket'] = "18" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('SOTON.+'), 'Ticket'] = "3" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('STON.+'), 'Ticket'] = "11" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('LINE.*'), 'Ticket'] = "7" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('F\.C\.(C\.)*.+'), 'Ticket'] = "17" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('W.+'), 'Ticket'] = "4" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('C.+'), 'Ticket'] = "9" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('S(\.)*C.+'), 'Ticket'] = "12" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('S(\.)*O.+'), 'Ticket'] = "2" num_alpha_ticket.loc[num_alpha_ticket['Ticket'].str.match('[^\d](Fa)*(P/PP)*(S\.P)*(S\.*W)*.+'), 'Ticket'] = "10" num_alpha_ticket['Ticket'] = num_alpha_ticket['Ticket'].apply(lambda x: int(x)) train = pd.concat([number_ticket, num_alpha_ticket])
ここまでの処理の結果
ここまでの処理で、各変数の相関係数がどのように変化したか見てみます。
plt.figure(figsize=(10, 8)) sns.heatmap(train.corr(), annot=True, cmap='Reds') plt.show()
TicketとSurvivedに比較的強い相関が出ました。
次回は、これらのデータを使って実際に機械学習をやってみたいと思います。
- 作者: 門脇大輔,阪田隆司,保坂桂佑,平松雄司
- 出版社/メーカー: 技術評論社
- 発売日: 2019/10/09
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
機械学習を原理から理解する 線形分類
最近、機械学習がブームでPythonを使えばだれでも簡単に学習器を作れるようになってきましたね。Pythonはライブラリが充実しているのでモデルについて何も知らなくでも機械学習をできます。私も別に仕事で機械学習を使っているわけではないのでそのような状態に甘んじていたのですが、最近になってさすがに気持ち悪さを感じていたので勉強を始めました。
今回は簡単なモデルとして、分類問題、特に線形の二値分類問題を考えます。
対象とする問題
いくつかの特徴量が与えられていて、それに対して正解ラベルが+1, -1で与えられているような状況を考えます。考える仮説空間はhalf-spacesクラス(日本語訳がわからない)です。
仮説空間という言葉が出てくると難しく感じますが、予測に使うのは下のような関数です。
は重みとバイアスです。基本的には線形関数で、最終的にsignで符号だけ取り出すことで予測値を算出しています。
学習アルゴリズム
与えられている特徴量を、正解ラベルをとします。この時、予測が当たっている状況を数式であらわすと、
符号関数は、少し抽象的です。もう少し扱いやすい形に書き換えましょう。要は両辺で符号さえ合っていればよいので、
としてもよいでしょう。訓練データが全部で個あるような状況を考えると、重みパラメータとバイアスが満たすべき条件は、
となります。さらに条件を詰めます。まず、バイアスを重みパラメータの中に取り込んでしまいます。
次に重みパラメータをの最小値で規格化(?)します。
これをつかうことで、条件はもう少し厳しくすることができて、
これが求めるべきパラメータの満たすべき条件です。
どのように解くか
問題はこれをどう解くかです。これはよく見ると、次のような問題と同じであることがわかります。
maximize 0
s. t.
線形計画問題です。今回は目的関数はどうでもよくて、制約条件のみが問題になります。この問題の解き方は探せば見つかって、シンプレックス法(単体法)というものが有名です。
Pythonで実装する
では、Pythonで実装します。線形計画法のところはめんどくさくなってライブラリ使いました。ごめんなさい。
import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split if __name__ == '__main__': #サンプル生成 X, y = make_classification( n_samples=500, n_classes=2, n_features=2, n_redundant=0, class_sep=3, ) y = list(map(lambda x: -1 if x==0 else 1, y)) X_train, X_test, y_train, y_test = train_test_split(X, y)
分類サンプルは、Scikit-learnのmake_classification
を使って生成しました。今回は簡単のためにサンプル数は500個、クラス数は2(二値分類)、特徴量は2種類で重要性が少ないものは0個としました。
学習アルゴリズムの本体は下のように定義します。
import pulp def solve(X, y): lp = pulp.LpProblem('lp', pulp.LpMaximize) # 解く問題を作る w1 = pulp.LpVariable('w1', 0) #重みパラメータ1 w2 = pulp.LpVariable('w2', 0) #重みパラメータ2 w3 = pulp.LpVariable('b', 0) #バイアス lp += 0 #目的関数 for i in range(len(X)): lp += y[i] * (X[i, 0] * w1 + X[i, 1] * w2 + w3) >= 1 #制約条件 lp.solve() #解く return w1.value(), w2.value(), w3.value() #各パラメータを返す
pulp
という線形計画問題を解くためのライブラリがありますのでありがたく使わせてもらいます。
学習がうまくいっているか確かめるために、テストデータを使って精度の確認を行います。
w1, w2, b = solve(X, y) acc = 0 for i in range(len(X_test)): pred = X_test[i].dot(np.array([[w1], [w2]])) + b if np.sign(pred) == np.sign(y_test[i]): acc += 1 print(acc/len(X_test))
乱数のシードを指定していないので指向のたびに値は変わりますが、おおむね9割程度の精度は出ています。
今回は、非常に単純なモデルについて書きました。その結果、構造は簡単でも9割程度の精度が出るということが確認できました(class_sep
大きくしているのでもう少し精度出てもよいと思うが...)。
ABC012 B 入浴時間 を解いた
今回は、AtCoder Beginners Contest 012 のB問題を解きましたのでまとめておきます。この問題は特別アルゴリズム等が必要なわけではなく、難しくもないのですが、ゼロ埋めが必要で、私がこれをよく忘れるので、備忘録的な感じで書きました。
beta.atcoder.jp
問題文
問題は下のようになっています。
高橋君は、お風呂で湯船に浸かった秒数を数える習慣があります。
今日は、高橋君は湯船でN秒まで数えました。
しかし、秒だと解りにくいので、何時間何分何秒、という形に直したいです。
秒数Nが与えられるので、hh:mm:ss の形式に変換しなさい。入力は以下の形式で標準入力から与えられる。
N1行目には、高橋君が湯船に浸かった秒数を表す整数N(0≦N≦86399)が与えられる。
出力
高橋君が湯船に浸かっていた時間を、hh:mm:ssの形式で、1行で出力せよ。出力の末尾には改行をいれること。
解答
算数的な感じです。まず時間から求めて、次に分、秒と求めていけばよいです。解答はゼロ埋め出ないとだめなので、ios
とiomanip
をインクルードしておきます。
#include <iostream> #include <ios> #include <iomanip> using namespace std; int main() { int n; cin >> n; int hour, min, sec; hour = n / 3600; n -= hour * 3600; min = n / 60; n -= min * 60; sec = n; cout << setfill('0') << right << setw(2) << hour << ":"; cout << setfill('0') << right << setw(2) << min << ":"; cout << setfill('0') << right << setw(2) << sec << endl; }
一応解説をしておくと、setfill
で何で埋めるか(今回は0)を指定していて、表示するものを左右どちらに寄せるかを指定しているのが次のright
の部分、そして埋めた後に最終的に何文字にするのかを指定しているのがsetw
になります。なのでsetw(4)
としたときには、hour=3
の時、表示は「0003」と4桁になります。