Go言語で五目並べを作ってみた
はじめに
数か月前、Go言語で書かれたソースコードを読まなければいけなくなり A Tour of Go を読んで勉強したり、 実際にソースコードを読んだりしていました。 それ以来まったくGo言語を使っていなかったのですが、 最近Go言語を使ってみたいと思い、 A Tour of Goを読んで勉強しなおしました。(まだgoroutineの手前までしか読んでないけど)
何か作らないといろいろ忘れてしまいそうなので Go言語で五目並べを実装することにしました。 禁じ手などの実装が面倒なので、 禁じ手なし、5つ以上石を並べれば勝利のシンプルなルールで実装しました。
今回作ったものはこちらです。
実装
まず、盤面の状態を表現する型Board
を定義します。
盤面のサイズは15x15で固定で石は'X'と'O'、石がない場所は'-'で表現することにしたので
1次元のbyte型の配列を使いました。
const ( Length = 15 Size = 225 ChainLength = 5 ) type Board [Size]byte
コンストラクタNewBoard
を定義しました。
配列をすべて'-'で初期化するだけです。
func NewBoard() *Board { var board Board for i := 0; i < Size; i++ { board[i] = '-' } return &board }
石を指定した位置に置くメソッドPlace
を定義しました。
- 石が'X'または'O'であるか
- 指定された位置は盤面内か
- 指定された位置に石がないか
をチェックし、 すべてのチェックが通ったら 配列の指定された位置に石を表現する文字を代入します。
func (b *Board) Place(stone byte, x, y uint8) error { if stone != 'O' && stone != 'X' { return InvalidStoneError(stone) } if x >= Length || y >= Length { return OutOfRangeError{x, y} } pos := x + y*Length if b[pos] != '-' { return AlreadyExistError{x, y} } b[pos] = stone return nil }
最後に指定した位置の石を含む5つ以上の並びを見つけるメソッドIsChain
を定義しました。
Go言語のMin
関数はfloat64型にしか対応していなかったので
自分でuint8型のmin
関数を定義しました。
shift
メソッドはpos
の位置からstep
で指定した方向に同じ石が何個並んでいるか数える関数です。
引数maxIter
で指定した回数以上数えないようにできます。
IsChain
は縦・横・斜め4方向にshift
関数を適用し、
同じ石が5つ並んでいる場所がひとつでもあればtrue
、
ひとつもなければfalse
を返します。
私はプログラム苦手なのでこんな方法しか思いつかないですが、
もっと効率の良い方法があるかもしれません。
func min(vals ...uint8) uint8 { minVal := vals[0] for _, val := range vals[1:] { if val < minVal { minVal = val } } return minVal } func (b *Board) shift(stone byte, pos uint8, step uint8, maxIter int) uint8 { var length uint8 = 0 for i := 0; i < maxIter; i++ { pos += step if b[pos] != stone { break } length++ } return length } func (b *Board) IsChain(x, y uint8) (bool, error) { if x >= Length || y >= Length { err := OutOfRangeError{x, y} return false, err } var ( pos = x + y*Length stone = b[pos] ) if stone != 'O' && stone != 'X' { return false, InvalidStoneError(stone) } steps := [4]uint8{1, Length - 1, Length, Length + 1} for _, step := range steps { var ( length uint8 = 1 maxShifts uint8 = Length - 1 shiftV, shiftH uint8 ) if step != 1 { shiftV = maxShifts - y } else { shiftV = Length } switch step % Length { case 0: shiftH = Length case 1: shiftH = maxShifts - x default: shiftH = x } maxIter := int(min(ChainLength-length, shiftV, shiftH)) length += b.shift(stone, pos, step, maxIter) if shiftV != Length { shiftV = maxShifts - shiftV } if shiftH != Length { shiftH = maxShifts - shiftH } maxIter = int(min(ChainLength-length, shiftV, shiftH)) length += b.shift(stone, pos, -step, maxIter) if length >= ChainLength { return true, nil } } return false, nil }
上で説明した以外にもターミナルに表示するためのコードをいろいろ書きました。
\033[G
とか\033[A
みたいなエスケープシーケンス?といわれるものを使って
石を置くたびに盤面の表示を更新するようにしました。
完成
下のようなプログラムが完成しました。
Go言語の良いところ
文法がシンプルで覚えやすかったです。 Go言語の文法を思い出すためにA Tour of GOを読みましたが、 数日でほとんど読み終わり、 すぐにコードを書き始めることができました。
この記事内で説明していない箇所で構造体の埋め込みを使ったのですが、 便利な機能だと思いました。
おわりに
Go言語でプログラムを書くのははじめてでしたが、 書いていて楽しかったので、 今後も使い続けてGo言語マスターを目指したいです。
RustでPythonモジュールをつくってみた
はじめに
最近スパースモデリングに興味があり、 スパース推定法による統計モデリング を読んでいました。(まだ途中までしか読んでないけど) ちょうどRustで何か書いてみたいと思っていたので この本の中に登場するエラスティックネットをRustで実装しようと思いました。
EvcxrやPolarsなどのライブラリを使えば、Jupyter NotebookでRustを使ったり、 EDAや前処理もできるのですが、 個人的にRustでEDAや前処理するのが辛いなと感じました。 そこでRustで書いたエラスティックネットをPythonから呼び出せるようにしました。
今回つくったものはこちらです。
ライブラリ
Pythonから呼び出せるモジュールをつくるために以下のライブラリを使いました。 ほかにもパラメータ推定のためにrand, ndarray-randを使っています。
- Rustライブラリ
- PyO3: PythonからRustでつくったプログラムを呼び出す用
- rust-numpy: NumPy配列とRustのndarrayを変換する用
- ndarray: Rustで使える多次元配列のライブラリ
- Pythonライブラリ
- setuptools-rust: RustのプログラムをPythonモジュールとしてビルドする用
Cargo.tomlの設定
プログラムのビルドと外部ライブラリを使うためCargo.toml
に以下を追加しました。
cdylib
は共有ライブラリをつくるために必要らしいです。
[lib] crate-type = ["cdylib"] name = "gmelasticnet" # ライブラリの名前 [dependencies] ndarray = "0.15.3" ndarray-rand = "0.14.0" numpy = "0.15.0" pyo3 = { version = "0.15.1", features = ["extension-module"] } rand = "0.8.4"
Rustの実装
RustのプログラムをPythonから使うために Rustのコードを書き換える必要がありました。 PyO3のユーザーガイドを参考にRustコードを書きました。
エラスティックネットのパラメータ推定のコードはsrc/elasticnet.rs
に書きました。
パラメータ推定に使うクラスElasticNet
は
ElasticNet
構造体やそのメソッド定義にアトリビュートをつけることで実装します。
自作モジュールgmelasticnet
にelasticnet
という名前でサブモジュールを追加するために
elasticnet
関数を定義しています。
この関数内でElasticNet
クラスをモジュールに追加しています。
// Pythonに公開する構造体 #[pyclass] struct ElasticNet { scaler: StandardScaler, y_mean: f64, xy_cov: Array1<f64>, x_cov: Array2<f64>, } // Pythonに公開しないメソッド impl ElasticNet { fn n_features(&self) -> usize { self.xy_cov.len() } } // Pythonに公開するメソッド #[pymethods] impl ElasticNet { #[new] //コンストラクタを定義 fn new(y: PyReadonlyArray1<f64>, x: PyReadonlyArray2<f64>) -> Self { // NumPy配列をRustのndarrayに変換 let y = y.as_array(); let x = x.as_array(); // なんかいろいろ計算する Self { scaler, y_mean, xy_cov, x_cov, } } #[args(l1 = "0.1", l2 = "0.1", max_iter = "1000", tolerance = "1e-4")] // デフォルト引数を設定できる fn fit( &self, l1: f64, l2: f64, max_iter: i32, tolerance: f64, random_state: Option<u64>, // Optionの引数はNoneがデフォルト値 model: Option<&ElasticNetParams>, // たしか Option<ElasticNetParams> だとエラーになった ) -> ElasticNetParams { // なんかいろいろ計算する ElasticNetParams::new(self.scaler.clone(), self.y_mean, coef) } } #[pymodule] fn elasticnet(_py: Python, m: &PyModule) -> PyResult<()> { // Rustの構造体をPythonのクラスとして追加 m.add_class::<ElasticNet>()?; m.add_class::<ElasticNetParams>()?; Ok(()) }
src/lib.rs
は以下のとおりです。
上のsrc/elasticnet.rs
に同名のelasticnet
関数を定義したせいで分かりにくいですが、
elasticnet
関数をm.add_wrapped(wrap_pymodule!(elasticnet))
とすることで
gmelasticnet
にelasticnet
をサブモジュールとして追加しています。
mod elasticnet; use elasticnet::*; use pyo3::prelude::*; use pyo3::wrap_pymodule; // ここの関数名はCargo.tomlで設定したライブラリの名前と同じにしないといけないみたい #[pymodule] fn gmelasticnet(_py: Python, m: &PyModule) -> PyResult<()> { // elasticnet関数を渡すことで // elasticnetをgmelasticnetのサブモジュールとして追加 m.add_wrapped(wrap_pymodule!(elasticnet))?; Ok(()) }
ビルド
setuptools-rust
のREADME.md
を参考にいろいろ設定しました。
setup.py
は下のようになりました。
from setuptools import setup from setuptools_rust import Binding, RustExtension setup( name="gmelasticnet", version="0.1.0", rust_extensions=[ RustExtension("gmelasticnet.gmelasticnet", binding=Binding.PyO3) ], packages=["gmelasticnet"], zip_safe=False, )
下のコマンドを実行して自作モジュールをインストールします。
python ./setup.py install
pip list
で確認すると
gmelasticnet 0.1.0
自分で作成したモジュールがインストールされていました!!
おわりに
今回作成したモジュールだとRustのコードで変更した箇所は
- アトリビュートの追加
- モジュール、クラスを追加する関数の定義
- NumPy配列の変換
だけなので、意外と簡単にRustでPythonモジュールをつくることができました。
データサイエンティストになりたいのでタイタニックコンペに挑戦してみた
データサイエンティストになりたいので初心者向けコンペの タイタニック(Titanic - Machine Learning from Disaster)に挑戦してみました。 今回はKaggle Notebook上でPythonを使ってモデルの学習と予測をしました。 LightGBMでGBDT(勾配ブースティング木)、Kerasでニューラルネットワークのモデルを学習し、 それぞれの予測値の平均をとってアンサンブルしました。
分析の準備
必要なモジュールをインポートします。
from pathlib import Path import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.pipeline import make_pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score import lightgbm as lgb from tensorflow import keras from tensorflow.keras import layers
データを読み込みます。 このコンペではtrain.csv、test.csvとgender_submission.csvが与えられます。
data_dir = Path("/kaggle/input/titanic") train = pd.read_csv(data_dir / "train.csv") test = pd.read_csv(data_dir / "test.csv") sample_submission = pd.read_csv(data_dir / "gender_submission.csv")
各カラムの説明は以下のとおりです。
Survival
- 生存したか(0=死亡、1=生存)Pclass
- チケットクラスName
- 名前Sex
- 性別Age
- 年齢SibSp
- タイタニックに乗船した兄弟/配偶者の人数Parch
- タイタニックに乗船した親/子の人数Ticket
- チケット番号Fare
- 運賃Cabin
- 客室番号Embarked
- 乗船港
特徴量の作成
preprocess
関数で前処理をしています。
与えられたデータからTitleとFamilyという特徴量を作りました。
前処理の内容は以下のとおりです。
Title
- Nameに含まれる敬称(Mr、Miss、Mrs、Master、それ以外)を整数値に変換Cabin
- 先頭のアルファベットを抽出し整数値に変換Sex
- 女性=0、男性=1に変換Family
- SibSpとParchの合計Fare
- 分布に偏りがあるのでlog(x+1)
で変換Embarked
- 整数値に変換
LightGBMは入力に欠損値があっても学習できるのでpreprocess
では欠損値補完していません。
ニューラルネットワーク用にカテゴリ変数(Title、Cabin、Embarked)をone-hot encodingしました。
PassengerId、Name、Ticketは不要なので削除しました。
def preprocess(df): df = df.copy() df["Title"] = 0 title_dict = { " Mr\. ": 1, " Miss\. ": 2, " Mrs\. ": 3, " Master\. ": 4, } for pattern, label in title_dict.items(): df.loc[df["Name"].str.contains(pattern), "Title"] = label df["Title"] = pd.Categorical(df["Title"], categories=range(5)) cabin_dict = {"A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7, "T": 8} df["Cabin"] = df["Cabin"].str[0].replace(cabin_dict).fillna(0) df["Cabin"] = pd.Categorical(df["Cabin"], categories=range(9)) df["Sex"] = df["Sex"].replace({"female": 0, "male": 1}) df["Family"] = df["SibSp"] + df["Parch"] df["Fare"] = np.log1p(df["Fare"]) df["Embarked"] = df["Embarked"].fillna(0).replace({"C": 1, "Q": 2, "S": 3}) df["Embarked"] = pd.Categorical(df["Embarked"], categories=range(4)) dummies = pd.get_dummies(df[["Title", "Cabin", "Embarked"]], drop_first=True) df = pd.concat([df, dummies], axis=1) df = df.drop(columns=["PassengerId", "Name", "Ticket"]) return df _train = preprocess(train) train_X = _train.drop(columns="Survived") train_y = _train["Survived"] test_X = preprocess(test)
モデルの学習・予測
今回はGBDTとニューラルネットワークの2つのモデルを作成し、 予測値の平均をとってアンサンブルします。
GBDT
GBDTの学習・予測は以下のとおりです。 検証データに対する正解率は0.843でした。
cols = ["Pclass", "Sex", "Age", "Fare", "Cabin", "Embarked", "Title", "Family"] train_X_lgb, val_X_lgb, train_y_lgb, val_y_lgb = train_test_split( train_X[cols], train_y, test_size=0.3, shuffle=True, stratify=train_y, random_state=42, ) test_X_lgb = test_X[cols] # データセット作成 train_dataset = lgb.Dataset(train_X_lgb, train_y_lgb) val_dataset = lgb.Dataset(val_X_lgb, val_y_lgb) # ハイパーパラメータの設定 params = { "objective": "binary", "num_leaves": 7, "learning_rate": 0.1, "bagging_fraction": 0.9, "bagging_freq": 1, "lambda_l1": 0.5, "lambda_l2": 1, "seed": 42, } categorical_features = ["Cabin", "Embarked", "Title"] # 学習 model = lgb.train( params, train_dataset, num_boost_round=100, valid_sets=[train_dataset, val_dataset], early_stopping_rounds=10, categorical_feature=categorical_features, verbose_eval=10, ) # 予測 val_pred_lgb = model.predict(val_X_lgb) test_pred_lgb = model.predict(test_X_lgb) # 評価 acc = accuracy_score(val_y_lgb, np.where(val_pred_lgb > 0.5, 1, 0)) print("Accuracy:", acc)
ニューラルネットワーク
build_model
でニューラルネットワークのモデルを構築しています。
one-hot encodingしたカテゴリ変数を選択しています。
ニューラルネットワークは欠損値を扱えない、スケーリングが必要なのでSimpleImputer
で欠損値補完、StandardScaler
で標準化しています。
検証データに対する正解率は0.832でした。
def build_model(num_inputs): model = keras.Sequential([ layers.Dense(64, activation="relu"), layers.Dropout(0.1), layers.Dense(32, activation="relu"), layers.Dropout(0.1), layers.Dense(1, activation="sigmoid"), ]) model.compile( optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"], ) return model cols = [ "Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Title_1", "Title_2", "Title_3", "Title_4", "Cabin_1", "Cabin_2", "Cabin_3", "Cabin_4", "Cabin_5", "Cabin_6", "Cabin_7", "Cabin_8", "Embarked_1", "Embarked_2", "Embarked_3", ] scaler = make_pipeline( StandardScaler(), SimpleImputer(), ) train_X_nn = scaler.fit_transform(train_X[cols]) train_X_nn, val_X_nn, train_y_nn, val_y_nn = train_test_split( train_X_nn, train_y, test_size=0.3, shuffle=True, stratify=train_y, random_state=43, ) test_X_nn = scaler.transform(test_X[cols]) # 学習 model = build_model(len(cols)) early_stopping = keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True) model.fit( train_X_nn, train_y_nn, batch_size=32, epochs=100, callbacks=[early_stopping], validation_data=(val_X_nn, val_y_nn), shuffle=True, ) # 予測 val_pred_nn = model.predict(val_X_nn).flatten() test_pred_nn = model.predict(test_X_nn).flatten() # 評価 acc = accuracy_score(val_y_nn, np.where(val_pred_nn > 0.5, 1, 0)) print("Accuracy:", acc)
アンサンブル
GBDTとニューラルネットワークの予測値を平均します。
test_pred = test_pred_lgb * 0.5 + test_pred_nn * 0.5
提出ファイルの作成
submission = sample_submission.assign(Survived=np.where(test_pred > 0.5, 1, 0)) submission.to_csv("./submission.csv", index=False)
結果
スコアは0.77751でした。 今後もコンペに参加してデータサイエンティストを目指していきたいです!!