[社内コンペ] 細胞画像認識

これは何?

某社で行われている社内コンペのメモです。私の備忘録でもあります。

結果:2位
精度:68.3%
120枚をクラス1~3で各40枚としていました。
ただし、その数値に合わせに行こうとすることはしませんでした。

今回の目的変数

「画像が何に種別されるか」の3値分類問題です。

回答は、
・通常細胞
・変性細胞
・変性細胞(悪化型)
とよくある形です。

今回の評価指標・ベースレート

Accuracy(精度)です。

出題者によると、お試しの意味も込めてAccuracyだそうです。
普通なら複合指標による評価が妥当とされています。
Precision(適合率:真の陽性をいかに漏れなく見分けられたか)やAUC(認識性能の高低を表す)、MAP(Mean Average Precision、Precisionの応用)などなど。

ベースレートはaccuracy60%です。

僕は、検証の際はAUCを用いていました。
他のコンペでよくある、「指標はRMSE、ただMAEで評価を行ったほうが結果が良くなる」という事例を途中で採用しました。

今回の説明変数の特徴

画像

訓練に使用できるのは1,200画像

 極度に少ないです。
 通常10万~とある場合が多いのですが、今回はこの制約下で行います。
 ただし、カラー写真ですので染色等での色の変化(非常に重要)は確認できます。
 なぜ色の変化が必要かというと、見た限りでは核を染色しているし、その他にも染色処置をしていたためです。
 変性細胞には核の形態変化が確認でき、その形態変化をDNN(Deep Neural Network)が捉えるには色が必要ではないか?ということです。
 ※本来は詳細情報を当該領域の研究者(顧客の会社所属)に確認したかったのですが…不可能だったことが残念です。
 ※このようなドメイン知識は作成する上で必要になります。

見分けるのには訓練が必要

 画像を「変性細胞である」とは見てわかるのですが、「悪化している」かはわかりませんでした。
 * 一応生化学系の修士 → 製薬会社(感染症系)に居たのである程度はわかりましたが…他の人はわからないそうです。
 悪化判断ために、本を実家から取り寄せ&知り合いのおすすめを新規購入しました。
 結局、変性細胞の種類が多すぎて判断できなかった…。
 ※特に細胞診はどの部位であるかが重要です。そもそもの細胞の形状が違います。今回はそのラベリングは存在せず、3クラスラベルしかなかったので、「大丈夫かコレ?」とはなっていました。

分け方が雑

 画像を確認したところ、採取部位別にデータがまとまっているわけではないようでした。
 なぜなら通常細胞の形がそもそも違うとひと目でわかりました。

 一般的に組織ごとに細胞の形は異なりますし、それらが変化した変性細胞も見た目が異なります。細胞への症状としては、核の膨張や(免疫機構の攻撃による)細胞の生死が確認できました。

 問題としては画像の中心に変性細胞がなかったり、写っている細胞数がごく少なかったり、同じ細胞を回転させただけで撮っている可能性がある写真も確認できました。

今回の目標

コピペしない。
画像認識の良い解決法を見つける。

ある程度個人で古典的な画像認識から深層学習を学んでいたので、どんな手法があるかなどは少し分かる状態でした。
Kerasなどの説明書をメインに、解説書(Kaggle等のDiscussion含む)、論文を見てモデルを構築します。

今回行った作業の流れ

画像を見て、前処理を考える

当たり前ですが、最初に画像を確認しました。
難しいポイントは何かアタリを付けましょう。
今回はフィルタ等は使用しませんでした。
※ノイズを取り除くにはいろいろなフィルタ(ガウシアン、バイラテラル、プリューウィット、LoG等)があります。

listにopen_cvで読んだ画像データを入れて、以下のような処理を行いました。

def image_preprosessed(img):
    # ノイズ除去
    # https://qiita.com/icoxfog417/items/53e61496ad980c41a08e
    # https://www.slideshare.net/takahirokubo7792/ss-71453093
    #img = cv2.GaussianBlur(img, (1,1), 0)
    return img

※実際は処理を行っていない。

自作Sequentialモデルを作成

よくあるConvを重ねただけの単純なモデルを作成しました。(精度30%)

モデルの定義

def model_base(model):
    model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(image_scale, image_scale, 3)))
    model.add(layers.MaxPooling2D((2, 2)))
    model.add(layers.Conv2D(64, (3, 3), activation='relu'))
    model.add(layers.MaxPooling2D((2, 2)))
    model.add(layers.Conv2D(128, (3, 3), activation='relu'))
    model.add(layers.MaxPooling2D((2, 2)))
    model.add(layers.Conv2D(128, (3, 3), activation='relu'))
    model.add(layers.GlobalAveragePooling2D())
    model.add(layers.Dropout(0.5))
    return model

Dense層を置いていないのは、GlobalAveragePooling(GAP)のほうが、
・精度が良く
・パラメータが少なくなるので計算が早い

という性質があるからです。(詳細:https://arxiv.org/pdf/1312.4400.pdf)

GAPからGlobalMaxPoolingの置換も効果ありそう(精度+0.5%)ということは分かりました。

モデルの実装

model_no1 = models.Sequential()
model_no1 = model_base(model_no1)
model_no1.add(layers.Dense(n_categories, activation='softmax'))
model_no1.compile(
    loss='categorical_crossentropy',
    optimizer=optimizers.Adam(lr=1e-4),
    metrics=['categorical_accuracy']
)

追加でmerticsにkerasのAUCを導入していました。
optimizer(最適化関数)も結局はAdamではなく、SGDでした。

最適化関数のSGDは学習は遅くなるものの、Adamのように局所解に落ち込みにくいので、結果精度が高くなる傾向にあります。
DNNは(数学的に見ると)学習曲面上で局所解が多いらしい(詳細は理解できなかった…)ので、精度を上げるためには問題に合った最適な最適化関数を選択する必要があります。

学習済みモデルのファインチューニング(1)

よくあるVGG16をblock5_conv1層付近でファインチューニングしました。(精度50%)

モデルの定義

base_VGG16 = VGG16(weights='imagenet', include_top=False, input_shape=(image_scale, image_scale, 3))

学習層の定義

base_VGG16.trainable = True
set_trainable = False
for layer in base_VGG16.layers:
    if layer.name == 'block5_conv1': # この層以降は学習可能
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

書いてある通り、転移学習はある層以降の学習を可能にして行うことが多いです。
前半の層はさまざまな種類のフィルタ構造になっており(確認するには各層を可視化するとOK)、画像の各特徴量(角、線、円等)を自動的に抽出する働きがあります。
後半の層は、事前学習用のモデルに合わせた重みになっているので、適用したい問題に合わせた重みに変化させます。

そのため、どの層から学習させるかにも検討すること必要です。
調べている限り、Batch Normalizationのみ学習可能にさせるなど、色々方法がありそうです。
とはいえ、VGG16は全く検証ができなかったことが残念です。
元々の精度が低かったので放置しました。

学習済みモデルのファインチューニング(2)

Xceptionを108層以降ファインチューニングしました。

モデルの定義

base_XP = Xception(weights='imagenet', include_top=False, input_shape=(image_scale, image_scale, 3))

※include_topをTrueにすると、元々の画像サイズにTrain画像を合わせないと動かなくなります。なお、それぞれのモデルで画像サイズが違う問題があるので、input_shapeで設定しています。

学習層の定義

base_XP.trainable = True
for i, layer in enumerate(base_XP.layers):
    if i <= 108:
        # BNだけTrueにする
        if layer.name.startswith('batch_normalization') or layer.name.endswith('bn'):
            layer.trainable = True
            print("◆%s\r\n %s is Trainable:%s" % (i, layer.name, layer.trainable))
        else:
            layer.trainable = False
            print("◆%s\r\n %s is Trainable:%s" % (i, layer.name, layer.trainable))
    else:
        layer.trainable = True
        print("◆%s\r\n %s is Trainable:%s" % (i, layer.name, layer.trainable))

Trainすべき層が問題に合ってるのかどうか甚だ疑問です。
ただ、こちらで一番上手く行ったので、ここに記載させていただきました。

学習済みモデルのファインチューニング(3)

InceptionResNetV2をblock8_10_conv層以降ファインチューニングしました。
Kerasで導入可能なモデルは、https://keras.io/ja/applications/を参考にしてください。

モデルの定義

base_IRN2 = InceptionResNetV2(weights='imagenet', include_top=False, input_shape=(image_scale, image_scale, 3))

学習層の定義

base_IRN2.trainable = True
set_trainable = False
for layer in base_IRN2.layers:
    if layer.name == 'block8_10_conv': # 要検討
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

結果としては、Xceptionとほぼ同等でした。
ただし、このモデルだけPseudo Labelingを行うと精度が下がる問題を抱えていました。

評価指標の変化を確認

ここまでで4モデルを作成しました。
・自作簡易モデル
・VGG16ファインチューニング
・Xceptionファインチューニング
・InceptionResNetV2(IRNV2)ファインチューニング

それぞれ、TrainのLoss(損失関数)とAccuracy(精度)、AUCを、Testからどれくらい離れていたかを確認しました。
K-Foldクロスバリデーション(K=5~10)も試しましたが、学習・検証に時間かかりすぎたので途中でホールドアウト法に戻しました。(良くないことです)
ホールドアウト法でも問題ないデータ量(n>10,000)なら良いのですが…

基本的にファインチューニングを行うようなモデルは表現力が高い(意訳:色々と細かいところまで学習できる)がゆえに過学習しやすいので、Testと回答の精度が離れがちです。
そのため、どのモデルがTestで見える見た目の精度と回答の精度が近いか確認しておきます。

・結果
基本、XceptionとIRNV2は精度も高く適正な値を保ちやすかったです。DropOutを高い値(0.7付近)に持っていたことも要因と考えています。
VGG16は過学習しすぎで、自作簡易モデルはデータに対してモデルが小さすぎて学習できず…という結果でした。

検討事項

・予測に使うDense層の前にDropOut層の強さ(0.01~0.99)を検討すること
→ 特徴量がうまく生成できない場合、非常に高い(0.9)値で上手く行ったことがある。(らしい)

・画像数の水増し(Data Augmentation)
→ ImageGeneratorを使っていたので変更は簡単でしたが、意味を理解して使えてない気がしました。ここも要検討です。

# 機能追加
# https://towardsdatascience.com/image-augmentation-for-deep-learning-using-keras-and-histogram-equalization-9329f6ae5085
datagen_train = ImageDataGenerator(
    rescale=1./255,
    rotation_range=90, # 回転
    width_shift_range=0.9, # 横シフト
    height_shift_range=0.9, # 縦シフト
    zoom_range=0.4,
    horizontal_flip=True,
    vertical_flip=True,
    fill_mode='wrap', # abcd|abcd|abcd
)

→ ImageGeneratorを使う場合、メモリが足りなくなる時があります。その時はbatch_sizeを減らすと良いです。
※計算時間はかかるようになりますが、計算は止まらなくなります。

history = model_xp.fit_generator(
    datagen_train.flow(X_train, y_train, batch_size=10),
    epochs=100,
    validation_data=datagen_valid.flow(X_test, y_test, batch_size=10),
    steps_per_epoch=100,
    callbacks=[model_decay, model_check]
)

・LearningRateSchedulerの変更タイミング
→ 基本100epochsで学習を行っていたので、学習率を変更する(下げていく)タイミングをどうするか決めかねて適当に設定していました。

# 学習epochごとに学習率低減を実装(all100)
def step_decay(epoch):
    x = 1e-3
    if epoch >= 80: x = 1e-4
    if epoch >= 83: x = 1e-5
    if epoch >= 86: x = 1e-6
    if epoch >= 89: x = 1e-7
    if epoch >= 92: x = 1e-8
    if epoch >= 95: x = 1e-9
    return x

初期を1e-3にしている理由は、あまりにも学習率を上げると学習済みモデルの重みを参考にせず、壊してしまうことが起こるためです。
少しずつ計算を進め、最適解を探すようにしましょう。

・ModelCheckpointの設定
→ Test_AUC基準で取得していました。

・画像を大きくすると精度は上がるのか?(フルカラーの写真のため)
→ 少し大きくすると精度は上がりました。さらに大きくするとデータ量が爆発してメモリに乗らない…

出来なかったことは以下の通りです。
・モデル内での回答の重み付け
→ 変性細胞(悪化型)の識別精度が悪いこともあり、「変性細胞(悪化型)の回答の確率出力をさらに重み付けるような方法に変更してはどうか」ということを考えていました。
重み付けアンサンブルからの着想ですが、うまくいくのかどうかわかりません。

・アンサンブル
→ 結局XceptionとIRNV2の差が大きすぎたので、アンサンブルしても意味がありませんでした。
画像の前処理を変更したモデル同士でのアンサンブルや、Pseudo LabelingしたXception+しないIRNV2でのアンサンブル等も考えられます。

・ガウス過程でモデルの適正さ?を計算し、アンサンブルの重み付けを最適化できるらしい(未確認)

・輝度勾配ヒストグラムを局所特徴量として扱うことができる(普通画素のみだが)らしい(未確認)

注意すべきポイント

適切に評価できる評価体形を作らない限り、改善が実際に進んでいるか全然わからなくなります。

基本的に最新のモデルになればなるほど最終的な精度は高くなりやすい傾向にあります。
最新のモデルは下記で検索可能です。
State of The Art…Image Classification on CIFAR-100
レビュー論文と言って、論文をまとめた論文があるのでそちらで検索も非常に早いです。
下記は2017年時点のレビュー論文です。
Deep Convolutional Neural Networks for Image Classification: A Comprehensive Review

英語が苦手でも大丈夫。
ページごとGoogle翻訳するか、PDFはGoogleDriveに保存してDocumentで開くとテキスト化できます。

その他:画像水増し

下記のように画像に加工を加える方法があるので、それぞれ試します。
・ノイズを加える
・平滑化する(ガウシアンフィルタ)
・明度・彩度の調整
・グレースケール化
・ImageDataGeneratorの改良
→ とはいえ、CNNのフィルタ構造と同じような感じで調整すると意味がないので、その点は気を付けます。

1,200画像データから適切に学習するのは非常に難しいので、いろいろな角度から画像を学習できるようにします。

その他:画像水増し対応モデルの作成

上記に対応できるモデルを作成します。
グレースケール化であるとRGB3色から白黒1色になるため、一般的なモデルのファインチューニングは困難になる・・・のか?
(RGBからグレースケールへの計算式は存在するので変換は可能か?)

最後に

一位になりたかった
・・・いろいろと試せ、理解が進んだので良かったです。

やってみたではなく、何を考えたか、なぜそう考えたかを包括的に書いた記事が少ないので、少しずつですがアップしていこうと思います。
その他コードやデータサイエンス関連の質問はR:Twitterまでお願いします。

[Meisyo] ver 0.31 リリース情報

名将と呼ばれた者達をver 0.31にアップデートします。

更新概要

1・投手配球設定の追加
2・練習試合マッチング方法の修正
3・課金決済の導入
4・その他
5・アンケート

更新詳細

投手配球設定の追加

投手の配球パターンを1球ごとに設定できるように変更しました。

これまでは、変化球球種の比率しか選ぶことができず、投球コースについては完全自動(ランダム)でしかありませんでした。
そのため、基本的なコースの投げ分けや、左右打者での投げ分けを設定することができませんでした。

今回の追加で、コースと変化球球種の投げ分けおよび左右打者での投げ分けを設定することができるようになりました。

懸念点としては、投球パターンを数多く設定しないとワンパターンになるので、逆に打たれやすくなる可能性が高いことです。

開発中の図

ある程度直観的に使用することが可能です。
パターン数を多くする等の操作には慣れが必要…。

練習試合マッチング方法の修正

練習試合のマッチングが意図しない動作になっていたので修正を行いました。

詳細としては下記の通りです。
同レートの人が存在する順位にマッチングされた場合、特定の人に挑戦が固定される不具合がありました。
同順位を上からソートし、ソート順で相手が決まる(固定してしまう)問題がありました。
そのソートをランダム化して同順位では一人しか確認しないアルゴリズムにしました。
そうすることで固定した相手と当たらないようにしました。

ご報告ありがとうございました!

課金決済の導入

アイテムショップからPayPalを介して課金できるようになりました。
名将ポイント1,000P(有償)が1,100円です。
有償のみで特別に購入可能なものは、プレミアムアカウントチケット30日が500Pです。

詳細は購入場所に提示している利用規約をご覧ください。

その他

操作上分かり辛かったり、説明がない部分の追記を行いました。
UIの変更、機能の追加(公式戦優勝報酬の追加、公式戦の未実施試合自動消化等)についても行っております。

例
・連盟戦チーム振り分け時のメール一斉送信
・投手配球設定に関わる変更

アンケート

機能の追加についてのアンケートを行います。

最後に

追加練習用の新コンテンツ(熟練プレイヤー向け)を準備しています。

探索
→ 選手強化(選手体力使用)、アイテム獲得
 MineCraft的なもので、1マップ(自動生成有)でアイテムを制限時間内に探していくゲームになる予定です。

実装は2~3月になると思いますので、お楽しみに。

[Meisyo] ver 0.30(a) リリース情報

名将と呼ばれた者達をver 0.30(a)にアップデートします。
なお、年内はこれで更新終了とします。
* 年末~年始は連盟戦のデバッグを行う予定です。

更新概要

1・連盟戦の追加
2・表示の全面改修
3・ユーザー動線の明確化
4・チュートリアル再調整
5・各種アルゴリズムの最適化
6・報酬・獲得物の見直し(12/20追記)

「1・連盟戦の追加」と「2・表示の全面改修」のため、大きくプログラム構成も変更しました。

更新詳細

連盟戦の追加

連盟戦(3高校合同チーム)でのリーグ戦を実装しました。
これまではあくまで一人で試行錯誤するゲームでした。

連盟戦の追加によって、他のユーザーとの交流をしながら、試行錯誤する楽しみ方もできれば良いと考えています。
当月中に参加登録したアクティブなユーザー2名と共にチームを作り上げます!
* 登録を忘れたら参加できませんorz(とはいえ不思議な力で調整は可能です)


*開発中の画像(チームの総合力が偏らないように調整しています)

連盟の各ユーザーは自分の高校から6名を選抜し、合計18人でリーグ優勝を目指します。
スタメン登録変更や試合の開始タイミング等、すべての連盟所属ユーザーが操作可能です。
そのため、チャットでの連携や意見の交換が必要になってくるでしょう。
各チームのトッププレイヤーの能力、チームバランスを上手く取り持って優勝する連盟はどこでしょうか?
個人的にも楽しみです。

今回8チーム(24名)の参加が見込まれているため、(登録に問題がなければ)各チーム7戦実施予定です。
* 連盟戦では選手の体力減少は実装していないため、試合内のみ考慮でスタミナをやりくりすればOKです。

バランス調整やバグ取りがまだまだできていないところですので、不具合等があればれいまで連絡下さい。

表示の全面改修

上記連盟戦でも表示が変わっていて少し驚かれたかもしれませんが、大幅に変えました。
スマートフォンでのプレイしやすさを考えて調整しています。

Androidのプレイ感が非常に悪かった(ボタンが小さすぎて反応しない等)ことがわかったので、今後も継続的に修正していきます。
*1 自分のスマートフォンをiOSからAndroidに変えたので…。
Chromeが酷くてFirefoxがまだ使えるかな、でした。
*2 iOSは会社で借りることが可能なので、仕事が終わってからチェックしています。
iOSの方が表示や処理速度面で優位だと思われます。

黒基調にし、より見やすくスタイリッシュ(?)な表示を目指していきます。
(試合以外)文字を少しでも減らすようにしました。

ユーザー動線の明確化

色々混雑していたユーザー動線を引き直しました。
メイン画面を中心にし、一目で何ができるそうかをはっきりさせました。

文字数減の効果もあり、すっきりとしている…つもりです。
* 説明不足感があるかもしれません。

チュートリアル再調整

チュートリアルの表示や説明が現状と合わない点、表示がバグを起こしていた点を修正しました。

各種アルゴリズムの最適化

ゲーム体感の軽量化のため、自動練習や試合等の処理速度を向上させました。

報酬・獲得物の見直し

 下記、課金追加によりプレミアムアカウントチケットを報酬設定していたシナリオ・連盟戦の優勝報酬を変更しました。
 「優勝景品ボックス」という、選択式のアイテムボックスを獲得可能です。
 連盟戦は優勝チーム全員が獲得可能の予定です。

 また、獲得物や選手についても、バランスを崩さない程度に自由度を追加しました。
 例:選手獲得(肩)の追加

今後の予定

投手配球(詳細)追加(01/02追記)

投球パターンの追加を行います。

これまでは変化球の割合配分だけでしたが、打者交代までのパターンを設定できるようになります。
各パターンは1球ごとにコース・変化球指定を行えるようになります。
パターンは右左打者で設定可能で、それぞれの割合を決めることができます。
(詳細はまだ詰めていません)

バグ修正

今回表示を変更した点、連盟戦についてバグが残っている気がします。
その点を修正していきます。

広告出稿

改修が終了したため、Googleに広告出稿を行います。
* ゲーム内での広告表示はゲーム感が損なわれるため行いません。

新たな敵(公式戦)や味方(連盟戦)が増えることで皆さんがより楽しめるようにしていきます。

課金決済追加

課金決済及び課金ポイント(名将ポイント(有償))を追加します。
Meisyo実施計画書にもある通りです。

ユーザー数増 → 広告出稿増 → ユーザー数増 → 広告出稿増を狙います。
最終的には、ユーザー数最大化を目指します。
* 月50人以上に定着して遊んでもらえるとは思ってなかったので、できればもっと増やしたいのです…。
* 給料にも限界がアル…

なお、課金によってゲームバランスを崩すようなコンテンツは追加しません。
「時短にはなるけど・・・」程度で抑え、「それがないと無理」なゲームバランスにはしません。

個人的には・・・下記の点を注意しておきたい。
1・強いカードが出るガチャがない
2・選手を即LvUPできたりするけど、割高
3・プレアカ(経験値+50%とアイテムBOX+5コ(1か月))が500円 ← 一番買ってほしい、というよりこれだけで十分。
4・課金ゲーは嫌だ

1
 強いカードって誰にとって?…普遍的に強いカードってなかなか無いんですよね
 というより、そもそも存在しないのでは・・・(本当は存在してほしくない)
 各選手もある程度天井があると思いきや、たまにヒーロー爆発するのでなんだろう。

2
 選手促成栽培の「意志の巻物」はすごく良いんですが、特訓等でちゃんと選手を見極めないとゴミ選手(失礼)を強化したりするので…
 「お前100打席でええ成績残しとったのに」が結構出たり見る目がない…という状況が多くなりそう。

3
 Netflixですら1,200円、ディズニーDELUXEが700円なこの世の中で…。
 元々の予定通り500円にしました。ユーザー数を増やす資金力をください。(広告で色々試してみたいのはある)

4
 ソーシャルゲームでは課金ガチャがバランス崩壊の原因なことが多かったり…。
 仕事での知識で仕入れてるんですが、結構闇深い。ホンマにユーザーが楽しいのだろうか?
 サービスでもゲームでも、なぜかKPIだけ見て、システム自体が改悪されて死んでいくのはもうこりごりです。
 ということで課金ゲーは嫌です。
 ユーザー数がKGIです。いかに残ってもらえるようにするかです。

まずは法律関係等でつまづくと思うので、追加はいつになるかわかりませんが…。(‘A`)
どうやってやるんだろう

最後に

2019年はこんな感じで更新は終わりです。
いつも遊んでいただいてありがとうございます。

2020年は東京オリンピック!多彩なイベントがあると思います。

Meisyoもイベント等企画出来たらいいなあと思いつつ、まずは足元を固めようと思いながら2019年が終わりそうです。
それでは~(^^)/

[Meisyo] 育成方針の微調整

ちょっと問題視していた育成方針を12/1に調整を行います。

なぜ問題視していたか

1. 育成方針間であまりにも採用率(ある育成方針の使用ユーザー/全ユーザー)が違うという問題が前からありました。
以前[Meisyo]育成方針の不均衡を減らすための分析で触れた通り、少し調整したあと、データを確認していませんでした。

2. ミートと投球技術(人気の育成方針)でパラメータが2種類しか強化できない。監督Lvがある程度上がるとパラメータを3種類強化するようになるので、1種類が無駄になる。
上記の育成方針は環境で強すぎたため、2種類強化だけにしていました。

今回はデータを確認して、
1.不均衡がなくなっているかを確認します。
2.不均衡があれば調整をします。

現在のデータ

過去30日以内に更新された選手での育成方針採用データは以下の通り。
結論から言うと、ミートと投球技術の採用率が高いため調整を行います。

選手タイプ 育成タイプ 採用ユーザー数 全ユーザー数 採用率(%)
野手 野手平均 23 69 33.3000
野手 守備 28 69 40.5000
野手 ミート 47 69 68.1000
野手 平均 22 69 31.8000
野手 投球技術 1 69 1.4000
野手 パワー 39 69 56.5000
野手 投手平均 1 69 1.4000
野手 速・肩 10 69 14.4000
野手 走・体 19 69 27.5000
投手 ミート 1 69 1.4000
投手 平均 17 69 24.6000
投手 投球技術 43 69 62.3000
投手 投手平均 21 69 30.4000
投手 速・肩 32 69 46.3000
投手 走・体 10 69 14.4000

メンテナンス・調整内容

上昇係数を公開します。◎〇△の上にマウスを置くと数値が表示されるようになります。

速・肩:制球0.2減少
投技:制球0.2減少、体力0.1増加
巧打:ミート0.2減少、パワー0.1増加、反応0.2減少

さいごに

調整で実現が不可能な選手が発生する場合があります。
特に、今回のような下方修正の場合顕著です。
ただ申し訳ございませんが、リセット等する予定はありません。
もし行うとしても新サーバー作るくらいでしょうか…。

もし新サーバー作るなら下記が必須だと考えています。
・連盟?戦を実装
 複数高校での合同チームのリーグ戦
・デザインの刷新
 現行スマホゲーム寄りに変更
・プログラム言語をpythonに切り替え
 現在のPHPフレームワークのバージョンが古い、プログラムコード最適化、機械学習導入のため
・配球設定(投手)、配球学習アルゴリズム(打者)を追加

まだまだ時間がかかりそうですね。

「実践!Chainerとロボットで学ぶディープラーニング」を作ってみた

実践!Chainerとロボットで学ぶディープラーニングを購入しました。
単純にロボを動かすのは楽しいです。
周りに意外と買った人が居ないようなので、レビューを書きました。

所感

ちょっとお高いけど、土日を使えば深層学習およびRaspberry Piの大枠を掴むことが可能

「71,800円+税」なので、個人で買うには結構お高いですが、
資料も的確にまとまっていて380Pなので(ちょっと間違い有り)すごく理解が深まります。

とりあえず箱が大きい

結構スペース取るなあ・・・

中身はわりと小さい

組むのに時間がかかる

意外と複雑なんだよなあ・・・

動くと楽しい(暗くてごめんなさい!)

こんな人におすすめ

・プログラミング言語を1度でも触ったことがある人
・深層学習に興味がある人
・まとまった時間が取れる人

EV3(ロボット)の組み立てにわりと時間がかかるので、興味がないと続かないような気がします。コードもそこまで難しくない(ちょっと間違いが有るので修正必須)ので、コードを書いたことがある人なら最後まで学ぶことが可能だと思います。

流れ

説明書には下記のとおりに書かれていますが、

所感では、以下の通りです。

  1. EV3の組み立て(50%)
  2. LEGOの説明書より配置を理解することが難しい。パーツは余る。

  3. RaspberryPiの環境構築(20%)
  4. 説明不足(「WLANのアドレスで接続しないと動かない」が書いていないなど)がたまにある。

  5. 教材を理解する(10~20%)
  6. わかりやすい。

  7. 実際に動かす(10~20%)
  8. 楽しい。

組み立て図は常に構造を理解していないと難しいので、少しずつ組み上げていくには骨が折れると思います。
そのため、土日で一気にやる方が良いかと。

わかること

・Pythonの記法
・深層学習の基本的な数学
・センサーとのやりとりのプログラミング
・Chainerの使い方

プラスアルファ
・遊び心
 EV3のストップ&ゴーがあまりにも激しいので、向き変更の際に少しずつ減速・加速するプログラムを独自で作りました。バックしながら動かすのとかも。
・時間はコスト
 学習器を学習させるのには普通のPCだとかなりかかりそうです。GPUを使える環境かだったのですが、待ち時間が結構ありました。

わからないこと

・深層学習の原理
・深層学習をもっと深く
その他はだいたい網羅されていると思います。

まとめ

「ちょっとお高いけど、よくまとまっている学習には便利なキットである」のは間違いないと思います。
この記事で使用している画像は実践!Chainerとロボットで学ぶディープラーニングに関係する企業様の画像を使用しています。

次に何をやろうか?

簡単にいろいろやりたい方
→ How Do I Get Started?

実践的なコードの書き方
→ 3 ways to create a Keras model with TensorFlow 2.0 (Sequential, Functional, and Model Subclassing)
英語嫌ならGoogle翻訳かMouse Dictionaryがおすすめです。

深層学習についての知見がほしい
→ サーベイ論文(深層学習の論文を網羅的に調べた論文)
PDFはDropboxに落として、HTML化してから読むといいと思います!

[Meisyo] ver.0.20(a)更新情報

Meisyoでは10月20日にver.0.20(a)として下記アップデートを行いました。かなり大がかりな変更も含まれているため、詳細に報告いたします、

安定し次第、0.20(b)に移行します。
安定するまではバグが出る可能性があります。バグ等はご連絡ください。
追記 大きなバグ等は発生していないため、11/1にアップデートを行った際に0.20(b)に移行します。

  1. 自動練習の追加(手動練習の廃止)
  2. バランス再調整(速球投手の地位向上)
  3. 練習試合のランキング変更方式の変更
  4. 各種追加

10/15まで行ったアンケートの結果も併せて公表しました。
多くのご意見のうち、可能な範囲で今回アップデートに反映しています。
今後のアップデートにも反映させる予定です。

自動練習の追加(手動練習の廃止)

手動で練習を行う方式から、行いたい練習を指示して自動で練習を行う方式に変更しました。指示は24時間有効で、練習途中での追加指示が可能です。

練習が指示されていない(または指示時間が切れた)場合は、メイン画面にて自動でポップアップ表示されます。練習状況の確認は、練習状況のNewsから「練習実施中…」のリストをクリックしてください。

1日あたりの経験値獲得量は現行とほぼ変わりません。行いたい練習を行っている選手には+20%の経験値補正がかかります。その他の選手には補正はかかりません。

1日あたりのアイテム獲得期待値が0.3(気合の塊のみ)から2(気合の塊、プロキュア30、モード5換算)へ増加しました。

加えて、アイテムが自動獲得になったためアイテム予備倉庫機能を追加しました。獲得アイテムがある場合のみ、練習指示状況の下に表示されます。(下記参照)
練習で獲得したアイテムは予備保管庫(10個、プレミアムアカウントチケット使用中で15個)に入れられ、保管庫からあふれたアイテムはすべて削除されます。そのため、獲得状況を頻繁に確認したほうがより多くのアイテムを獲得できるようになります。なお、予備倉庫が満タンになっていても経験値獲得には影響しません。

頑張れば少し差がつく程度ですが、バランスを考えて調整していきたいと思います。

バランス再調整(速球投手の地位向上)

速球投手が他の投手に比べ相対的に打たれやすかったので調整しました。
具体的には速球(基礎値+育成値+強化値)によって打者のスイングスピードがランダムで低下するようにしました。
※この調整は投手の能力に依存し、打者の能力には依存しません。
※速球投手以外の投手にはほとんど影響しません。

大きな変更としては、打者スイングスピードが低下したため被HR率が下がり、被打率(WHIP)等も低下します。

被HR率

防御率

WHIP

計算方式としては、スイングスピードが減少処理(100%→N%へ変化)されます。
Speed_S(SP=106): Mean…75.95%
Control(SP=32): Mean…98.41%
Speed_A(SP=100): Mean…80.64%

計算式は以下の通りです。

$G['bt_swsp_mn'] = (1.0373 ** $G['pc']['S_p_sp']) * mt_rand(0, 100) / 100; # 減少量
$G['bt_swsp'] *= (100-$G['bt_swsp_mn'])/100;

※高ければ高いほど打たれにくいように調整するため、指数関数を使用しています。

投球を1,000回実施すると以下のような分布になります。
mt_rand(等分布)を使用しているため、等分布します。

変更理由

「高校野球の華、速球投手が打たれやすくてどうするんだ・・・?」という思想の元変更。
全ての能力値にそれぞれ意味があるようにしたいと考えています。(物理計算をチューニングするのは難しい)

これでゲームバランスの大きな調整は終わった、と思いたい・・・。
なおバランス調整が終わった後、イベントとしてたまーに現在は木製バットですが金属バット化して超打高な月を作ったり、常に雨でボールが転がりにくい月を作る予定です。

練習試合のランキング変更方式の変更

練習試合レートが勝利時に蓄積される形式に変更されました。引き分け・負けても減りません。シーズン開始時にリセットされます。
なお、報酬形態も変更しました。シーズン1勝または+50ptごとに報酬が増加します。

変更理由

「頑張った人のチームが上のリーグに行って報酬を多く獲得してほしい。何もしない人は落ちるべき。」という思想の元変更。
今までは強いチームが上に行って、相対的に弱いチームが下に行っていた。
弱いチームの人は何回練習試合をしても、報酬をもらえなかったため。
1回でも勝てば報酬が増えるので、練習試合に挑戦する意味を持たせたかった。

もちろん強いチームは勝ちやすいので戦えば上位に行きやすい。
シナリオも経験値、報酬がおいしい(優勝でプレミアムアカウントチケット)のでどっちにするかーって話ですね。

各種追加

アンケートでのご意見を取り入れ、いろいろと追加しました。

アップデートに追加済

  1. 全高校野手・投手成績比較
  2. 便利(だが打たれにくい投手の傾向は発見できなかった…)

  3. 公式戦歴代優勝校、トーナメント表
  4. トーナメント表は結果のみ表記

  5. 能力値の説明
  6. 育成情報で公開。
    必要があれば計算式も表示します。

  7. スタメン変更方式の変更
  8. これまでは1人1人切り替える必要があったが、直観的に使用できるようにクリックのみ変更可能に。
    割と便利になった気がする。

  9. 成績指標の追加
  10. UZR等は1球ごとのログを取らないと難しそうなのでやめておく。(以前の試合でのエラー頻発の原因…コードを見直したらできそう)

  11. 修行の表示改良
  12. 修行前に修行後のLvとExp状況を見れるように変更。

  13. 選手の手動LvUP
  14. EXPがLvUP必要以上にある場合(リセット使用時、修行時等)、LvUPをカード詳細のLv[+]から可能に。

今後追加予定

     

  1. 連合戦
  2. だいたいの仕様:1か月1回、エントリーした監督たちを(だいたい同じくらいになるように)ランダムに配分した連合を作り、それで5日のリーグ戦を戦う。参加者、優勝者には賞品あり。毎月5-10日エントリー、11-15日チーム発表・16-20日連合戦21日結果発表。
    連合は3名(以下、連合メンバーと呼称)を基本にし、チームメンバーには各高校から各6人の選手をエントリーします(2名の場合は各9人)。各高校からのメンバー選出は当該高校の監督しかできませんが、連合チームの組替え・采配は連合メンバーなら誰でも可能とします。そのため、チャットでのコミュニケーションを取りながらチームの構成を考えると良いでしょう。これまでとは一味違ったMeisyoをお楽しみください。

  3. バント
  4. 高校野球では当たり前に使われているが、一番難易度が高そうなので待機中。
    物理計算にバントを入れて、成功率をどうチューニングするかが問題。

  5. 練習方法のバランス再調整
  6. 投球技術がバランス悪い(2種UPのみ)ので。
     

  7. スキルLv・スキルLvUPの実装
  8. たぶん同レアリティのスキルであればUP可能になる。気合の塊と同じ感じ。UP幅は+1~2%/Lv程度になる予定。
     

  9. マイナススキルの追加
  10. 選手にはそれぞれ苦手なことがある

  11. 挑戦相手の追加
  12. 実装済み:守備、先発、打撃(ミート型)
    中継ぎ(体力とは何かLvで長く使えない)、走塁+打撃(守備は死んだ)、パワー、肩(キャッチャー想定)の追加を予定。

アンケート結果

実施フォーム(終了済)

投票順(有効票14)
・練習(挑戦キャラクターの追加)3
・UI・UXの追加2
・采配機能の追加2
・各種記録閲覧可能に1
・ゲームバランス調整(上限解放)1
・ゲームバランス調整(不得意スキルの追加)1
・その他新要素等1
・その他1
・公式戦(表示追加)1
・自治会及び対抗戦の追加1

色々なご意見ありがとうございました。
データだけではどうにもならない部分が多いにあるので、参考にしていきます。
また、直接言っていただいた場合は可能であれば対応します。

Jupyter Notebook「7ブロック」でかんたん複数動画のフレームの切り出し(Movie To Image)

はじめに

機械学習のために、動画から画像のデータセットを取り出す必要がありました。
動画 → 画像の変換には多くのフリーソフトはありますが、
今回使おうとした「MOV」ファイルが読み込めるまともなフリーソフトがありませんでした。
※更新停止していたり、ウイルスだと判定されたり散々でした…。

というわけで、Pythonで書いたほうが早いなとなりました。

下記にコードを載せています。
よくわからない人はダウンロードにあるzipファイル(テスト動画付き)を使ってください。

Windows、Macどちらでも使用可能です。

ダウンロード

MovieToImage.zip

コード

使用するモジュールを読み込みます。

import os
import cv2
import glob
from tqdm import tqdm

※インストールできていなければインストールしてください。

基礎設定を行います。
デフォルトでは、Jupyterファイルのあるフォルダの中にimageフォルダを作り、その中に保存します。

file_name = 'IMG_0001.MOV' # 読み込みファイル
fps = 30 # fps(1秒に1枚切り取る)
dir_name = 'image' # 保存dir
image_name = '_img%s.jpg' # 出力ファイル名

保存用関数を設定します。
1フレーム単位で保存するとほぼ動きがない画像になるので、1秒単位で切り分けます。

def video_to_images(video_file=file_name, root_dir='', save_dir=dir_name, image_file=image_name, fps=fps):
    # saveディレクトリを作成
    if not os.path.exists(save_dir):
        os.makedirs(save_dir)
    
    # 各ムービーのディレクトリを作成
    image_dir = save_dir + '/' + video_file + '/'
    if not os.path.exists(image_dir):
        os.makedirs(image_dir)
        
    # 未設定の場合fpsを設定
    fps = 60 if fps is None else fps
    
    # ビデオファイル名を設定
    read_file = root_dir + video_file
    
    # Videoをフレーム単位で切り取る
    i = 0
    cap = cv2.VideoCapture(read_file)
    while(cap.isOpened()):
        flag, frame = cap.read()
        # 読み込めなかったら終了
        if flag == False:
            break
        # 1枚進める
        i += 1
        if i % fps != 0: # 1秒1枚を出力
            continue
        fname = image_dir + video_file + image_file % str(int(i/fps)).zfill(6)
        cv2.imwrite(fname, frame)
        print('No.%s Save [%s]' % (i, fname))

    cap.release() # capリリース

テストとして1動画ファイルを切り取ってみます。
エラーでなければ、imageフォルダの中に動画ファイル名ごとにフォルダが作成されています。
1枚ずつ切り出すことができました。

# 1画像ファイルを加工
video_to_images()

複数動画の加工を行います。
複数動画を入れるフォルダと拡張子名を設定します。

# 基礎設定
dir_mov = 'mov' # ムービー用dir
file_ex = '.mov' # ムービー拡張子名

globでファイル検索をかけ、filesに格納します。

files = glob.glob(dir_mov + "/*" + file_ex)

実行します。
簡単に処理が終わりました。

# Cut開始
for path in tqdm(files):
    print("#-----------------------------------------------------------#")
    print("# Start MovieToImage... file:%s" % path)
    print("#-----------------------------------------------------------#")
    file_name_one = path.split("\\")[-1]
    video_to_images(video_file=file_name_one, root_dir=dir_mov)

おわりに

Yahoo知恵袋か何かで、「動画ソフトでプリントスクリーン→ペイントすると良い」と当たり前のように書かれていて驚きました。
それちょっと辛くない?時間かかりすぎでしょ・・・というわけで今回作成しました。
何かあればメッセージを残してくださいませ。

[Meisyo]育成情報

これは何?

名将と呼ばれた者達(略:Meisyo)の説明書です。

まず何をやればいいの?というところから、
どうすれば上手くいくの?という疑問にお答えします。

質問や気になるところがあれば、れいまでご連絡ください。

目次

初心者がやっておきたいこと
中級者になるために
公式戦に優勝するために
ステータス詳細
Meisyoでできないこと、今後できること
仕様詳細

初心者がやっておきたいこと

  1. 方針決定
  2. チーム全体の育成方針を決めます。
    そこから選手の発掘、各選手の育成方針、監督の指導Pの使用用途などが決まります。

    全体の方針で言うならば・・・打力中心、守備力中心という大枠から考えてみましょう。
    打力とは短打力?長打力?それとも機動力?
    ※機動力は守備にも使えます。

    守備力中心なら中心になるのは守備陣?投手?
    投手中心なら速球中心?制球中心?変化球中心?

    現行のチームでは打力中心(短打力中心)が多いです。
    上記のチームの弱みは守備力ですので、打線が少しでも打てないと負けます。
    公式戦では守備を重視したチームが安定して勝ち上がっている印象です。

  3. 毎日の練習
  4. 練習は設定してから24時間自動で進むので、毎日1分でもログインすればお得です。
    ちなみに練習で獲得できるアイテムは8時間くらいで予備倉庫が溢れるので、そこ辺りで回収するとなおよいです。
    アイテムは選手の経験値アップや強化値アップ、試合チケットなどいろいろと役立ちます。

  5. 選手の発掘
  6. 選手招待後、練習の特訓でその選手が打てるか・打たれないか調査できます。
    選手によって結果がバラバラなので、1選手ずつしておいた方がいいです。

    結果は能力値にある程度比例しますが、完全には読めません。
    それを行うことで能力低いけど意外とイケる!選手を見つけていきましょう~。

  7. 即戦力選手の入手
  8. 練習の挑戦で、トッププレイヤーにも通用する即戦力選手が入手可能です。
    まずは投手、続いて野手を手に入れておくと楽でしょう。

    挑戦自体の難易度は低め(成功率50%程度)です。入手できると試合が非常に楽になります。

  9. 毎日の試合
  10. 練習より試合の方が経験値的に圧倒的においしいです。
    経験値獲得量は試合>練習です。
    ただし、試合に出場していない選手は経験値がもらえません。

    選手の発掘も併せて行えるのでおすすめします!
    シナリオはある程度相手が強いので星1相手で監督Lv5~がおすすめです。

  11. ステータス詳細の理解
  12. ステータス詳細をちょっと理解しているとゲームを進めやすいです。

中級者になるために

  1. 控え選手の発掘
  2. メイン選手たちが揃ってきても、調子が悪いタイミングがあります。
    メイン選手の代役を揃えておきましょう。

    控え選手の発掘を行っていると、メイン選手より良いのがたまに混じっていたり。

  3. 継投設定
  4. 投手は一人で投げきれないので継投設定をしておきましょう。
    体力30までは全力で投げることができるので、そこまでで代えるとGood。

    ワンポイントリリーフ、抑え等プロが使っている戦術も使用可能です。

  5. 盗塁・バント設定、代打・代走・守備固め設定
  6. 投手は打力が比較的低いです。
    そのため、チャンスを広げるためにバントさせたり、投手交代前に代打を送っておくと点が入りやすいです。(代打が投手として登板することはありません)
    代打も走力が低い場合があるので、代走を設定しておくと良いでしょう。

    勝っているときの守備固めは強いチームはほとんど使っています。
    確実に勝ちをもぎ取りましょう!

公式戦に優勝するために

  1. 相手チーム方針の把握
  2. 対戦する敵チームの方針を試合前に確認し、相手の弱点を攻めましょう。
    ある投手はある打者に打たれにくいという相性もありますので、日々調査しておくのもいいかもしれません。

  3. 勝ちやすい・負けやすい相手を知る
  4. 自分たちが勝ちやすい、負けやすい相手を知っておきましょう。

    ちなみに作者は打力チーム相手が苦手です。
    自分と同じようなバランス・投手中心チームには、いろいろと研究しているのでかなり有利を取れます。

  5. 結果の数値理解
  6. 打率、OPS、ERA、WHIP等選手の結果を示す指標があります。セイバーメトリクスとも言われています。
    どの選手が本当にいい選手なのか?を知るには数値で理解する必要があります。

    作者は打率がいかに高くても出塁率が高くないと使いません。

    見落とされがちなのは守備機会とエラー率。
    どの守備位置にどれだけ球が飛んでいきやすいのか。
    飛んできやすいところに守備のいい選手を置くべきだろうか?という。いかに守備が高くてもエラーばっかりしていれば意味がありませんし。

  7. 選手強化をこまめにする
  8. 気合の塊がいつの間にか溜まっているので、選手を強化しておきましょう。
    +2までは安全圏なので、野手の場合STR+16、投手の場合STR+20まで強化することが多いです。

    +3以降は強化失敗すると強化値が落ちるので、他の選手をフル強化してからでしょう。

  9. 調子・体力を回復する
  10. どうしても使いたい選手にはアイテムを使って、調子・体力を回復しておきましょう。
    アイテム使用にマイナスはありません。

    控え選手の層が薄いとアイテム中毒にならざるを得ません・・・(作者の現状)

ステータス詳細

調子

選手の調子を示す。
平均50で、だいたい30~70間で変動する。
アイテムで上昇させることが可能。
調子には周期性があり、上がりっぱなし・下がりっぱなしは存在しない。
(アイテムで上げ続けても、落ちる慣性が大きくなっていく。)

肩力値は変動せず、他の能力値の変動値は以下の通り。

ミート・パワー・走力・守備:0.4/調子1
エラー回避・反応:0.2/調子1
球速:0.08km/調子1
制球:0.8/調子1
体力・変化量1.2.3:0.4/調子1

調子が良い(調子>60)選手は調子の悪い(調子<40)選手より全能力が10程度上昇するため、調子が良い選手を使った方が良い結果が得られる可能性が高い。

体力(HP)

少ないと能力が落ちる。
投手は試合中に減っていくが、野手は開始時の体力のまま試合終了までプレイ可能。
投手は30以下になると急激に能力が落ちる。
自然回復で最大100まで回復する。

ステータスの体力が低いとHPが減りやすく、高いと減りにくい。
連投したい場面が多い選手は体力を上げたい。
アイテムで回復できる。

Lv・Exp

1~100。
選手の成熟度を示す。
選手能力リセット券を使うことで、経験値を保有したまま育成値をリセットしてLv1に戻すことができる。
→ 育成方針変更や育成失敗した場合に使用する想定。

スキル

特殊能力。(スキル詳細)
1選手1つのスキルを付与できる。

現行はプラス能力のみ実装している。

ミート

球にバットを当てる能力。変化球対応力も兼ねている。
高ければ高いほど、真芯でとらえやすくなる。

パワー

スイングスピード。
高ければ高いほど強い打球が飛ぶため、長打率が上がりやすい。
低すぎるとうまくバットのポテンシャルを引き出せていないため、打ち存じが多い。

走力

走塁時の走力。守備時の移動速度にもある程度関わる。
2塁盗塁時は、走者走力+5>捕手肩力であればほぼ成功する。
3塁成功は、走者走力+15>捕手肩力でほぼ成功する。

肩力

送球時の速度。
低いとほぼ山なりボールになる。

守備力

守備時の移動速度。
低いととてももたついた守備になる。

エラー回避

守備時のエラーを回避する能力。
101以上あればエラーしない。0だと1割エラーするため、少しは上げておきたい。

反応速度

打撃時・守備時に球に反応する瞬発速度。
打撃時は直球、キレの高い変化球に対応できる能力を示す。

球速

80~160km。
高ければ高いほど振り遅れ率が高くなる。
加えて、相手のスイングスピードを低下させる効果があり、打たれても飛びにくくなる。

制球

コントロール。
低いと四球、失投を連発するため、少しは高めておきたい。

体力

ステータスの体力。
高ければ高いほどHPの減りが少ない。

変化球(方向)

変化球は3種類保有可能。
保有パターンとしては以下の通り。
・2方向変化球+チェンジアップ
・同方向2種類変化球+別方向1種類変化球

どちらが強いかは違いはないようだ。

変化量

変化量。
多ければ多いほど打者を翻弄しやすい。
ミートが低い打者が多いチームに有効。

速球キレ

速球のキレ。
キレが良いと反応の低い打者は反応できない。

変化キレ

変化球のキレ。
低ければ低いほど投手の近くで曲がり、高いほど打者の手前で急激に曲がる。
反応が高いとある程度対応可能。

属性

選手の心理属性。
打者は投手の属性によって能力が±10上下する。
心 → 技 → 体 → 心 → のように3すくみになっている。

Meisyoでできないこと、今後できること

できないこと

  1. 試合中の操作
  2. 監督として介入できる場面を制限しています。
    「試合前に指示を終え、選手に頑張ってもらう。」というのが基本コンセプトです。

  3. 有名選手のカードコレクション
  4. 多様性を重視するため、有名選手(≒強い選手)は導入しません。
    どのチームのどの選手も「誰かが考えた良い選手」という唯一無二性をもって頂きたいからです。
    ※初心者のうちに使えるオリジナル選手(練習の挑戦で獲得可能)は導入しています。

    NPCチームには存在しますが、それは「どう乗り越えるか?」を考えるゲームです。

  5. 強い選手をガチャで手に入れる
  6. ガチャ(選手招待)はあくまで選手の卵を手に入れるためのものです。
    そこから試合を経験させることでその選手の特徴を掴み、「どう使っていくか」です。

  7. 常勝軍団になる
  8. 100%勝てる相手はいません。
    「勝てる確率をどこでいかに上げていくか」がキーポイントです。

  9. 何回も試合したい
  10. ゲームバランス設定上、試合は1日1回(20時間に1回)です。
    監督レベルアップで回復することで複数回実行可能です。

    ・・・リアルでも、1日に複数回試合するのは体力的に無理では?と思いますが。

今後できること

  1. 各種繰り返し作業の自動化
  2. 練習、公式戦、模擬戦のように自動進行が可能な範囲を増やしていきます。
    あくまで監督は考えて準備することに時間を使ってほしいと思っています。
    ※こちらは多くの機能に追加しました

  3. キャラクターの覚醒など
  4. 育成を深めるため、覚醒ランクを導入します。
    ※こちらは機能に追加しました。

  5. 全体的な軽量化で遊べる
  6. アルゴリズムの改修を随時行っています
    ※こちらは大幅改修しました。
    (予定)プログラミング言語をPHP → Pythonに移行します。

仕様詳細


各機能の仕様詳細を記載しています。

練習

練習指示が設定されていれば実施される。
指示した練習を行っている選手の獲得経験値量が50%増える。

更新はゲーム内ページ読み込み時、最短4分に1度行われる。
LvUPは可能な限り行われ、手動レベルアップは選手詳細で可能です。

だいたい24時間で消費可能アイテム3つ分くらい拾える。
※倉庫が10個埋まるのに8時間程度かかる。

追加練習

特訓・挑戦が行える。
20時間に1回可能。
特訓は選手の体力が回復した状況で実施される。

練習試合

20時間に1回可能。(シナリオと時間共有)
勝てばレートが上がり(+16~)、引き分け・負けでは変化しない。
リーグにより報酬量は変動する。
1勝で40%増加、レートが+50ptごとに(最大+300p)獲得アイテム量が20%増(乗算後小数点切り捨て)になる。
選手の体力を使用する。

シナリオ

20時間に1回可能。(練習試合と時間共有)
負ければその大会の1回戦(地方大会、全国大会に分けられる)に戻される。
勝てば相手の強さによって獲得経験値量が増加する。
優勝すれば難易度に応じた優勝賞品がもらえる。
選手の体力は使用せず、試合も体力が回復した状況で実施される。

模擬戦

試合チケットを1枚使用することで実施可能。
勝てば相手の強さによって獲得経験値量が増加する。
負けでのデメリットはない。
選手の体力は使用せず、試合も体力が回復した状況で実施される。

自動進行が可能なため、放置での試合進行がが可能。

プレミアムアカウントチケット(略:プレアカ)

全ての獲得経験値量が+50%される。
練習の獲得アイテム用補助倉庫が+10個(10→20)される。(アップデートにより+5から10へ変更)
時短にどうぞ。

[Python]グリッドサーチを軽量化し、チューニングしたパラメータも反映する機構を作る

パラメータチューニング方法であるグリッドサーチ、
確かに自動で実行してくれて、すごく便利なのですが問題点があります。

めっちゃ時間がかかる
もし、下記のパラメータ設定のモノを全てグリッドサーチしようとすれば、ゲーミングPCでも余裕で24時間を超えます。
・・・特に時間制限のあるColaboratoryでは安易に使えない。

ただ、パラメータ1つ1つグリッドサーチすると
それはそれで「そのパラメータでしかモデルを捉えられない」という問題をはらみます。

そこで、今回は下記の方法の実装を行いました。

グリッドサーチしてパラメータ1を「A」に決める
↓
パラメータ1は「A」として、
グリッドサーチしてパラメータ2を「B」に決める
↓
パラメータ1は「A」、パラメータ2は「B」として、
グリッドサーチしてパラメータ3を「C」に決める
↓

なぜこんなことをしたいのかというと、
適当にパラメータを投入すれば勝手に上手くやってくれるし、時間もかからない機能が欲しかっただけです。

モジュールのインポート

モジュールを必要最低限呼び出します。
scalerはお好きなものをご使用ください。

from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler, Normalizer
scaler = MinMaxScaler()
#scaler = StandardScaler()
#scaler = RobustScaler()
#scaler = Normalizer(copy=True, norm='l2')

学習器のインポート

今回は回帰の問題だったのでSVRや回帰型XGBを使用しましたが、分類も対応可能です。

from sklearn.svm import SVR # サポートベクター回帰
import xgboost as xgb # XGB

データのインポート

pandasで読み込まれており、前処理は終わっているX、yを仮定します。
ただし、正規化(または標準化等)はまだ実施していないものとします。

X = ...
y = ...

パラメータ設定

設定できる項目はまだまだありますが、一つの例として記載します。

cv_list = {
    'XGB':
    {
        'discriminator': xgb.XGBRegressor(objective='reg:linear'),
        'random_state': [0],
        'booster': ['gbtree', 'gblinear'],
        'n_estimators': [1, 3, 5, 10, 20, 30, 50, 100, 200],
        'max_depth': [3, 4, 5, 6, 7, 8, 9, 10],
        'subsample': 1.1 - 10 ** np.linspace(-1, -0.8, 20), # ~1
        'learning_rate': 10 ** np.linspace(-1.2, -0.9, 20),
        'gamma': np.linspace(0, 1.0, 20), # 0~
        'reg_lambda': 1.1 - 10 ** np.linspace(-1, -0.3, 20), # ~1
        'reg_alpha': -0.1 + 10 ** np.linspace(-1, -0.3, 20) # 0~
    },
    'SVR':
    {
        'discriminator': SVR(),
        'kernel': ['poly', 'rbf', 'sigmoid', 'linear'],
        'degree': np.arange(1, 10, 1),
        'C': 10 ** np.linspace(-5, 3, 5),
        'gamma': 10 ** np.linspace(-5, 0, 5),
        'epsilon': 10 ** np.linspace(-5, 0, 5)
    }
}

あるパラメータが必要だと思えば追加すれば良いと思います。

グリッドサーチ実行

複数個のモデルのグリッドサーチに対応しています。
パイプラインを使ってデータ分割→scaler実施→model.fit→判定を自動で行います。
パイプラインを使用すると、param_gridをモデル名__パラメータ名にしなければならないことにも対応しています。
最後に一番良いパラメータを表示します。

for model in cv_list:
    # model作成
    print("#---------------------------------------------------------#")
    print("#", model, "loaded")
    print("#---------------------------------------------------------#")
    model_test = cv_list[model]['discriminator']
    model_name = "model"
    pipe_model = Pipeline([("scaler", scaler), (model_name, model_test)])
    model_param = cv_list[model].copy()
    del model_param['discriminator']
    
    # paramごとにCV
    best_param = {}
    for param in model_param:
        # make
        param_one = model_name + "__" + param
        param_grid = {}
        param_grid[param_one] = model_param[param]
        param_grid.update(best_param) # 以前のチューニング結果を反映
        print(">> Tuning '%s' is..." % (param))
        model_cv = GridSearchCV(pipe_model, param_grid=param_grid, cv=3,
                                return_train_score=False, n_jobs=-1, verbose=0)
        model_cv.fit(X, y)
        # Best
        best_param[param_one] = [getattr(model_cv.best_estimator_.steps[1][1], param)]
        print('Best Params:', best_param[param_one])
    
    # last
    print(">> All Best Params is...")
    print(best_param)

GridSearchCVに自作評価関数を入れることも可能です。

scoring=make_scorer(func_scoring, greater_is_better=True)

最後に

10分もかからずにグリッドサーチが終わると思います。
すべてのパラメータを総当りで行うのはコスト(時間)がかかりすぎるのでおすすめはしません。

お役に立てたのであれば嬉しい限りです。

【社内コンペ】回帰分析メモ 完全版

これは何?

某社で行われている社内コンペのメモです。私の備忘録でもあります。
結果:1問差で2位でした。残念。

今回の目的変数

建築物の坪単価
※「え、建物の単価じゃないのか」と思ったあなたは正常。

今回の評価指標・ベースレート

正解単価の±10%以内に入った数 / 全数
※35%を超えれば良い(2019/09/19 やっと超えました。)
※重回帰(LinearRegression)でのベースレートは22%

今回の説明変数の特徴

0. リアルデータで欠損値が多い。
打ち込みミスが多く、データの信頼性が低いかもしれない(担当者談)←マジカヨ・・・
確かに延床面積と建築面積が同じっておかしいよなあ!10階~の建物なのに。(なお、その補正はしてないもよう。)

1. どれが一番重要という変数がない。
自作合成変数(Nullカラム数が多いと坪単価が低い)が変数重要度No.1だったりする。

2. 変数の数が多い
138変数ある。
カテゴリ変数をone-hot化するだけで255変数!

3. 決定木の場合はone-hot化すると判定精度が低下しかねない。
線形の場合は必須。
線形と決定木系のよくやる前処理方法は違うようだ。アルゴリズムが全く別だししゃーないか。

4. 設計会社区分で各カラムのmean、medianを取ったがうまくいかず。
上記の方法は、Train内で情報リーク(TestにTrainの情報が行って判別精度を甘めに見積もってしまうこと)が起こってしまうので採用できないようだ。
上記区分が不均一なデータだったので、SMOTEを試したがうまく行かず。
そもそもSMOTEはあまりうまく行かないようだ。(まあ適当にN点間の平均値取ってるだけだし仕方ないよね。)

5. 専門知識が無い
カラム内容を見てもさっぱりでした。もちろん少しは本で理解したが。

今回の目標

基本に忠実に行う。
コードはできるだけ自動化させる。(変数選択、合成等)
目視で変数選択しようにも、重要度がどれもほぼ変わらないぞ~(==;)みたいな状態だったので。

基本(的な学習器)に忠実に(テストして、最高のモノで学習を)行う。
回帰の場合は下記の通りとなっているが、果たしてそうなのか?
SGD
↓
L1 ElasticNet
↓
L2 SVR(linear)
↓
SVR(rbf) EnsembleRegressors

Choosing the right estimator – scikit-learnより

作った処理の流れ

注意するところ

最適なCV方法をまず見つけることが一番大事。正答率という羅針盤がない航海になる。
変数名が専門用語だったので理解できず。業務内容理解しよね。

内容確認

Train+Testまとめて実施した。平均値埋めとかしない限りはリークは起こらないので問題なし。

目的変数を加工してはいかんのか?
→ いかんわけじゃない。目的変数を正規化したり、log化したり、合成するのもありだったりするようだ。

データ可視化

ひと目見て、あっ・・・(察し)となった。

目的変数に大きな外れ値が存在した。
それで何も考えずに行った正答率が非常に低かったのかも。
IQR(四分位値のアレ)を使って除外した。

説明変数にも大きな外れ値(カテゴリ変数はもちろん除く)があったので、
IQRを使ってNull化→欠損値と違う値で埋めた(決定木用)。これは精度向上に効果があった。

加えて、色々切り分けて見ても重要な変数がほぼない。
設計会社区分別での坪単価等が異なったので、それらしい変数を入れてみたがうまくいかず。

相関係数確認

-0.5~0.5だらけ

欠損値補正

Median、0等で埋めたり、TrainのみAveで埋めたりした中で一番良いやつを頼む。(ただし情報リークには気をつけることだ。)
学習器を使って埋める方法等もっとあると思う。

ただし、Mean埋めは線形タイプに、0(や-1など元々のカラム出てきてない値)は決定木タイプで使用するとGood。
異常値であることを機械に知らせてあげよう。
Nan値を予測して埋める手法を試してみたが正答率は結局下がった。もしかしたら線形タイプでは上がるかもしれない。

もう少し工夫はできる気がする。

変数合成

Null個数カラム=「レコードの中にNull値数が多いレコードは何故か値が低い」という知見を利用。

データ水増し

今回はやりましたがうまくいきませんでした。
分類(特に画像処理)だとよくやるもよう・・・画像を回転させるとそれだけで1枚にできるらしい。

正規化

MinMax、StandardScaler、RobustScaler、Normalizer
今回はMinMaxが一番良かった。StandardScalerが死んでた。

ただし、線形には効果はあるけど、決定木系には効果なし。
決定木系は順序(Rank)と集合で見ているもよう。

多モデルのテスト

テスト用関数も作った。
一番いいやつを頼む。

線形

from sklearn.linear_model import LinearRegression # 重回帰分析
LR = LinearRegression()
from sklearn.linear_model import SGDRegressor # データ量多い回帰
SGDR = SGDRegressor()
from sklearn.linear_model import Lasso # ラッソ(L1)
L1R = Lasso()
from sklearn.linear_model import Ridge # リッジ(L2)
L2R = Ridge()
from sklearn.svm import SVR # サポートベクター回帰
SVRR = SVR(gamma='auto')

決定木

from sklearn.tree import DecisionTreeRegressor # 決定木
DTR = DecisionTreeRegressor()
from sklearn.ensemble import BaggingRegressor # バギング回帰
BGR = BaggingRegressor(DTR, n_estimators=10, max_samples=0.3)
from sklearn.ensemble import RandomForestRegressor # ランダムフォレスト回帰
RFR = RandomForestRegressor(n_estimators=100)
import xgboost as xgb # XGB
XGBR = xgb.XGBRegressor(objective="reg:linear")

厳密に言うとバギング回帰は決定木ではない。
今回は中に決定木を入れているので決定木にした。

結果
XGBの独り勝ち

価格予想の論文でSVRが使われていたので、コレで頑張って22 → 40%くらい出してた。(つもりになっていた。ただの過学習だった)

しかし、XGB(回帰)をちょっと入れてみるか・・・結果33%で「は?(これまでの頑張りとは)」ってなった。

基本に忠実にやってみたけどやっぱりXGBが強い。
SVRも結構できる子やな~って感じ。(ただし過学習の恐怖からは逃れられない。)

CV値(自作関数)の確認も行った。
決定木の前処理を行えば、線形は死んで(マイナス突破)いた。
決定木系はプラスだった、が正答率25~30%程度と低い。

パラメーターチューニング

グリッドサーチ
・SVR、XGBで頑張った。
SVRは結局kernelはpolyが一番良く、sigmoidが一番悪かった。変数減らしてやったほうがとても早い。

ランダムサーチ
・SVR、XGBで頑張った。
今回評価指標がすごくレアなものなので、グリッドサーチに勝てなかった。過学習してるだけかも知らん。

その他の学習器の試行

keras(MLPとNNとDeepLearning)
Train+TestのdropNA後標準化のみ(8変数)で32%が出たように見えたが、変数が増えると死んだ(20~25%)。
たぶん変数の多さに比べてネットワークの大きさが足りない(層を多くしたりノードを横に増やすとプラスだったが限定的だった)。
あとは前処理が全然試行錯誤できていない。Log処理(疑似標準化)が強いもよう。

変数自動合成

元々あった255変数を、下記の方法で正答率が上がるものをピックアップして追加する。
・組み合わせ(+、-、×、÷)
・変数単独で計算(x^2、exp、log2、log10)する

ただし、めっちゃ手間がかかるので遺伝的プログラミング(遺伝的プログラミングによる特徴量生成でLightGBMの精度向上【kaggle Advent Calendar 11日目】)を導入した。コードはコンペが終わったら全部載せました。↓下の方にあります。

決定木は変数間の相互作用的なのは見えやすいが、比率(割り算)や差(引き算)を認識するのが困難なのでこういった自動化の手法は必須だと考えている。
3つ以上の変数の合成変数もアリなもよう。(遺伝的プログラミングなら理論上試行する)

変数選択

今回は後退的選択法を選択。
チューニングしたXGBを使い、重要度の低い変数から1つずつ削り取っていく方法にした。(詳細)
変数の重要度が低い順から「切ったら精度下がるか?」で判定する方法を実装している。(自作)

回答提出用加工

下記の方法を採用
・なにもしない単一モデル
・平均アンサンブル
・重み付けアンサンブル

Aveや価格帯での重み付け(「ここら辺の価格帯はこの学習器が調子いい」というのがあるので)で具合のいいように組み合わせてみる。
分類は多数決が多いらしい。
回帰でアンサンブルって聞いたことがない(たぶん不勉強→Aveアンサンブルが多いもよう)。

平均が残念だったので重み付けして調整した。
正答率が1~3%くらい上がる感じだが、正しく施行しないと過学習と同じ様になる。
重み付けは1/RMSE^2で実行。1/RMSEでも余り変わらず。

あまりここだけに拘泥するのはよくない。
良い特徴量を作れたほうが強い。

回答提出用加工

TrainとTest+回答提出用(1)を使った学習
分類で良く使われる手法。

精度UPにはつながるが、ええのかこれとは思う。

回答提出

一番いい奴を頼む。

なんでこれ書いたの?

機械学習の一つ一つの手法ばっかりが着目されていて、やることをまとめて書いてある回帰分析の方法がなかったので。
数学や統計の以前に、プロジェクトとしてどうすんのコレ?っていう。

回帰分析の資料がねえ!
分類のタスクがほとんどで、網羅的に書いている文章も少ない。
あとはとっつきやすいように数式はゼロにしてある文章が欲しかったからです。
(たぶん真似して誰かが書いてくれるでしょう)

結果

1問差で2位でした。

コード

まずは基本的な設定と確認。
不要なコードもあるので適宜追加・削除してください。

基本的なモジュール(機能)の読み込み。

# data analysis
import pandas as pd
import numpy as np
from scipy import stats
import math, copy
# visualization
import seaborn as sns #
import matplotlib.pyplot as plt
from datetime import datetime
import statistics
%matplotlib inline

最後にmatplotlib(グラフ描写ソフト)の出力をファイル内に行うよう指定。

matplotlibの図に漢字を使うと豆腐になる対策済み。

plt.rcParams['font.family'] = 'IPAexGothic'

顧客から指定のあったカテゴリ変数を設定しておく。他タスクにも応用可能。

master_category = np.array(['管轄区分', '設計区分', '構造1', '構造2', '構造3'])

※一般的なもののみ表示。

エクセルデータだったのでそれで処理。

# read data(学習データ、適用データ)
df_train = pd.read_excel('train.xlsx', encoding='SHIFT-JIS')
df_test = pd.read_excel('test.xlsx', encoding='SHIFT-JIS')

エクセルファイルはSHIFT-JISがほとんどです。忘れると漢字を上手く読み込めないよ。

めんどうなのでtrainとtestをまとめて確認します。

# まとめて確認
df_comb = pd.concat([df_train, df_test])
# trainの先頭から5件(行)表示
df_comb.head(5)
# 基礎情報
print("train shape: %s" % str(df_train.shape))
print("test shape: %s" % str(df_test.shape))
print("comb shape: %s" % str(df_comb.shape))
# 各種統計量の表示
df_comb.describe()

カテゴリ変数設定が合ってるかどうか確認しておく。

for mcl in master_category:
    print(mcl)
    print(df_comb.iloc[:, df_comb.columns.str.contains(mcl)].columns)

※master_categoryの1要素(文字列)ずつを、df_combの列が含んでいるか確認しています。

欠損値数とその割合を見ておく。

# 欠損値調査
df_nn = df_comb.notnull().sum()
df_nu = df_comb.isnull().sum()
df_all = df_nn + df_nu
df_np = df_nu / df_all * 100
pd.DataFrame({'ALL':df_all, 'Null数':df_nu, 'Null%':df_np}, index=df_comb.columns)

以降加工
決定木用の加工を行う。

加工用にデータフレームをコピーする。

df_ts = df_comb.copy()

コピーしておくと加工に失敗しても手戻りが少ない。

確認時に“1行あたりのNullカラムが多いと価格が下がる”知見を獲得していたので追加。

# Nullカラムが多いと値段が低い傾向にある
if 'Nullカラム数' not in df_ts.columns:
    df_ts['Nullカラム数'] = df_ts.isnull().sum(axis=1)

infとかいう困ったちゃんをnanに置き換える
↓
fillnaする(決定木用なのでもともと持っていない値にする。)

# infをNanへ変換
df_ts['合成変数21'] = df_ts['合成変数21'].replace([np.inf, -np.inf], np.nan)
df_ts['合成変数21'] = df_ts['合成変数21'].fillna(-1)

なぜか1、0ではなく有、無、Nanの列があったので加工。
無とNanは別モノだよと学習器(木)に教えてあげる。

# 'すごい変数'は「有」「無」Nanで表記されているのでそれぞれ値を振る
df_ts['すごい変数'] = df_ts['すごい変数'].replace({'有': 1, '無': 0, np.nan: -1}).astype('float64')

カテゴリ変数とその他の変数の穴埋めをする。
今回は決定木なので、欠損値を0として設定しておく。

# 残ったカラムの穴埋めを行う
# 決定木の場合外れ値(0等)、線形の場合mean・medianが良いらしい
for i, col in enumerate(df_ts.columns):
    # 0の場合スルー
    if df_ts[col].isnull().sum() == 0:
        continue
    # カテゴリ変数の場合
    if col in master_category:
        num = 0 # 1つの値(欠損値=0)として規定
    else: # カテゴリ変数ではない(=連続値)の場合
        #num = df_ts[col].dropna().median()
        #num = df_ts[col].dropna().mean()
        num = 0
    print("[%s] fill:%s" % (col, num))
    print("→ %.2f~%.2f" % (df_ts[col].dropna().min(), df_ts[col].dropna().max()))
    df_ts[col] = df_ts[col].fillna(num)

※計算を早くするためにNull数が0の列はスルーしておく。
※Median・Meanを採用するときは先にdropnaをしてあげるとエラーが出ない。

加工確認

# 値の確認
df_ts.head(5)
# 最終確認
df_ts.describe()

ここで列ごとに外れ値を検知して、決定木用に「-1」とする。
ここでは四分位値を指標に外れ値を検出している。

df_dm = df_ts.copy()
for i, col in enumerate(df_dm.columns):
    # カテゴリ変数の場合無視
    if col in master_category:
        continue
    # メインカラムの場合除外
    if col in ['No', '総合計']:
        continue
    # 実行
    num = df_dm[df_dm[col] != 0][col]
    # 四分位値を出力
    Q1 = num.quantile(.25)
    Q3 = num.quantile(.75)
    # IQRでの外れ値検出
    IQR = Q3 - Q1
    th_ov = Q3 + 1.5 * IQR
    th_lw = Q1 - 1.5 * IQR
    # 変換
    df_target = df_dm[col].values
    count = 0
    for j in range(df_target.shape[0]):
        if df_target[j] > th_ov or df_target[j] < th_lw:
            df_target[j] = np.nan
            count += 1
    df_dm[col] = df_dm[col].replace(np.nan, -1)
    # 出力
    print("No.%s [%s]" % (i, col))
    print("...Target Num is %s" % (num.shape[0]))
    print("→ ONLY %.2e~%.2e ... IS NOT %s" % (th_lw, th_ov, count))

その他の方法もあるので一概にこれがいいとは言えない。
※今回の場合これを行うと精度が安定して上がったので採用した。

最後にNull値を持つ列がないか確認する。

# Null値を検出
df_dm[df_dm.isnull().any(axis=1)]

列だと思う。行じゃなかったはず(曖昧)←なんでや

正規化・標準化

今回は正規化を採用(しても決定木では意味はないが)
・・・線形の学習器も並行して使ってみてたから仕方ない!
とりあえず関数化しとこね。

# 正規化を行う
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler, Normalizer
scaler = MinMaxScaler()
#scaler = StandardScaler()
#scaler = RobustScaler()
#scaler = Normalizer(copy=True, norm='l2')

def data_normalization_fit(df):
    scaler.fit(df.astype('float64'))

def data_normalization_transform(df):
    df = pd.DataFrame(data=scaler.transform(df), columns=df.columns, index=df.index)
    return df

※ふつーにscaler.fitとscaler.transformでいいと思う。
※ある経緯があってこの形にしているが、現状ではあまり意味はない。

最終確認(shape)

df_dm.shape

学習

combineしていたので分割。
つまり今まででmean埋めをしていると、情報リークになってしまうやり方になっていた。
mean埋めする場合はtrainとtestは分けておくといいと思う。

df_train = df_dm[:df_train.shape[0]]
df_test = df_dm[df_train.shape[0]:]

※0埋めと-1埋めしかやってないのでセーフ。

ちゃんと分けられているか確認。

print(df_train.shape, df_test.shape)

相関係数の表示

# 相関係数の表示
fig, ax = plt.subplots(figsize=(20, 20)) 
sns.heatmap(df_train.corr(), square=True, vmax=1, vmin=-1, center=0)

※実データでは相関が見えない悲しみを背負っていた。

学習前に色々取得。
もちろん要らんやつもある。

# トレーニング用sklearnの読み込み
from sklearn.metrics import accuracy_score, roc_curve, auc, confusion_matrix, mean_squared_error
from sklearn.model_selection import train_test_split, GridSearchCV, RandomizedSearchCV, cross_val_score, KFold

学習データの作成。

# トレーニングデータ(全体)の作成
y = df_train['総合計']
X = df_train.drop(['総合計', 'No'], axis=1)

ちなみにdf_testの方もデータフレームに入れておく。
変数自動合成・選択の時、手動で入れるのがめっちゃめんどいので。

Z = df_test.drop(['総合計', 'No'], axis=1)

これも上記と理由は同じ。
先に列順をソートしておく。
列の順番変わると学習器がアヘる(というより、列順は変わらないものとして学習している)ので、注意するように。

X = X.astype('float64')
Z = Z.astype('float64')
X = X.sort_index(axis=1, ascending=False)
Z = Z.sort_index(axis=1, ascending=False)

またも確認。

print(X.shape, Z.shape)
X.describe()

データ分割。
shuffle入れてもいいけどどっちでもいい。今回は使用せず。
stratifyも今回は層化サンプリング(クラス分類で使用)は必要ないので設定しない。

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) # , shuffle=True
print('train size: %s, test size: %s' % (X_train.shape,X_test.shape))

正規化(標準化)

# 正規化(情報リーク対策)
data_normalization_fit(X_train)
X_train = data_normalization_transform(X_train)
X_test = data_normalization_transform(X_test)

英語は気にしてはいけない

今回ホールドアウト法とクロスバリデーションを使う。
そのため、クロスバリデーション用のK_Foldを設定しておく。
cv=K_Fold(数字の代わり)のように使う。

K_Fold = KFold(n_splits=3, shuffle=False, random_state=0)

評価関数を作っておく。
上がホールドアウト法用で、下がクロスバリデーション用。
動けば良い。(平均絶対誤差率の10%以内で1point)

# 評価
def accuracy_test(y_pred, y_test):
    sum_acc = 0
    histgram = []
    for i, num in enumerate(y_test):
        pred = y_pred[i]
        per = (num - pred) / num
        histgram.append(per)
        if abs(per) <= 0.1:
            sum_acc += 1
    return [sum_acc, histgram]
# 評価関数を自前で作成する(損失率)
from sklearn.metrics import make_scorer
def func_scoring(y_true, y_pred):
    acc = 0
    for t, p in zip(y_true, y_pred):
        score = (t - p) / t
        if abs(score) <= 0.1:
            acc += 1
    return acc / y_true.shape[0]

色々な書き方を試してみるのも面白い。

学習器を色々インポートして設定しておく。
sklearnのチュートリアルに書いてあるものを一つずつ書いているだけ。
※SGDはデータ量的に全然足りないが試しに使っている。

from sklearn.linear_model import LinearRegression # 重回帰分析
LR = LinearRegression()
from sklearn.linear_model import SGDRegressor # 勾配降下法
SGDR = SGDRegressor(max_iter=1000, tol=1e-3)
from sklearn.linear_model import Lasso # ラッソ(L1)変数の個数を減らすマン
L1R = Lasso()
from sklearn.linear_model import Ridge # リッジ(L2)変数の重みを減らすマン
L2R = Ridge()
from sklearn.linear_model import ElasticNet # L1+L2
L1L2R = ElasticNet()
from sklearn.tree import DecisionTreeRegressor # 決定木
DTR = DecisionTreeRegressor(random_state=0)
from sklearn.ensemble import RandomForestRegressor # ランダムフォレスト回帰
RFR = RandomForestRegressor(n_estimators=100)
from sklearn.svm import SVR # サポートベクター回帰
SVRR = SVR(gamma='auto')
import xgboost as xgb # XGB
XGBR = xgb.XGBRegressor(objective='reg:linear')
# V
XGBR_V = xgb.XGBRegressor(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bytree=1, eval_metric='rmse',
                          random_state=0, n_estimators=350, max_depth=4, subsample=0.9415106807538888,
                          learning_rate=0.11288378916846889, gamma=0.0,
                          reg_lambda=1.0, reg_alpha=0.0,
                          importance_type='gain', max_delta_step=0, min_child_weight=1,
                          missing=None, n_jobs=-1, nthread=None, objective='reg:linear', predictor='gpu_predictor',
                          scale_pos_weight=1, seed=None, silent=True)
from sklearn.ensemble import BaggingRegressor # バギング回帰
BGR_DTR = BaggingRegressor(DTR, n_estimators=100, max_samples=0.3)

チューニング済みのも入れた。
適宜チューニングして最適なものを追加していく。

パイプライン用に読み込んでおく。
クロスバリデーション、グリッドサーチ等時に正規化等を自動的に読み込んでくれるので便利。

# pipe_line
from sklearn.pipeline import Pipeline

テストするモデルを設定しておく。
とりあえず適当にするのでなんでも入れてOK。

list_model = {'重回帰':LR, 'SGD':SGDR, 'L1':L1R, 'L2':L2R, 'L1_L2':L1L2R, 'SVR':SVRR
              '決定木':DTR, 'ランダムフォレスト回帰':RFR, 'XGB':XGBR, 'XGB_V':XGBR_V, 'バギング(DTR)':BGR_DTR}

ホールドアウト法を実施。

# 各モデルをテスト(ホールドアウト法)
for name, model_test in list_model.items():
    # Fit
    model_test.fit(X_train, y_train)
    # Predict
    y_pred_test = model_test.predict(X_test)
    # 回答チェック&ヒストグラム化
    [sum_acc, histgram] = accuracy_test(y_pred_test, y_test)
    # 表示
    print("#---------------------------------------------------------#")
    print("#", name)
    print("#---------------------------------------------------------#")
    #plt.hist(histgram, bins=100, range=(-0.6, 0.6))
    #plt.show()
    print("MAPE Average: %.2f%, Median: %.2f%" % (np.mean(histgram), statistics.median(histgram)))
    print("The Accuracy: %.2f%(=%s / %s)" % ((sum_acc / y_test.shape[0]) * 100, sum_acc, y_test.shape[0]))

クロスバリデーションを実施。

# 各モデルをテスト2(CV:K-Fold法)
for name, model_test in list_model.items():
    # 表示
    print("#---------------------------------------------------------#")
    print("#", name)
    print("#---------------------------------------------------------#")
    pipe_model = Pipeline([("scaler", scaler), ("model", model_test)])
    CV = cross_val_score(pipe_model, X, y, cv=3, scoring=make_scorer(func_scoring, greater_is_better=True))
    print("Cross-validation scores: %.3f(%s)" % (CV.mean(), CV))

cv=3でなくても別に構わない。

アンサンブル用に適当に選択しておく。

list_model_sel = {'XGB':XGBR, 'XGB_V':XGBR_V}

XGB簡易パラメーターチューニング用に設定。
全て行うと計算回数が20^5*10^2(いくらですかこれ:3億2千万回)を、
10*2+20*5(120回)に削減している。
もちろんn_estimatorsの設定値によって大分変わるが減ったのは間違いない。

cv_list = {
    'XGB':
    {
        'discriminator': xgb.XGBRegressor(objective='reg:linear'),
        'random_state': [0],
        'booster': ['gbtree', 'gblinear'],
        'n_estimators': [100, 150, 200, 250, 300, 350, 400, 450, 500],
        'max_depth': [3, 4, 5, 6, 7, 8, 9, 10],
        'subsample': 1.1 - 10 ** np.linspace(-1, -0.8, 20), # ~1
        'learning_rate': 10 ** np.linspace(-1.2, -0.9, 20),
        'gamma': np.linspace(0, 1.0, 20), # 0~
        'reg_lambda': 1.1 - 10 ** np.linspace(-1, -0.3, 20), # ~1
        'reg_alpha': -0.1 + 10 ** np.linspace(-1, -0.3, 20) # 0~
    },
    'SVR':
    {
        'discriminator': SVR(),
        'kernel': ['poly', 'rbf', 'sigmoid', 'linear'],
        'degree': np.arange(1, 10, 1),
        'C': 10 ** np.linspace(-5, 3, 20),
        'gamma': np.linspace(0, 1.0, 20),
        'epsilon': 10 ** np.linspace(-5, 0, 20)
    }
}

ついでにSVR用も設定している。ほんとついでです。(超平面を作り出せるrbfさんがんばれ)

パラメーターチューニングを実行。
記述されている上から順にパラメーターをチューニング→設定、チューニング→設定・・・としている。

for model in cv_list:
    # model作成
    print("#---------------------------------------------------------#")
    print("#", model, "loaded")
    print("#---------------------------------------------------------#")
    model_test = cv_list[model]['discriminator']
    model_name = "model"
    pipe_model = Pipeline([("scaler", scaler), (model_name, model_test)])
    model_param = cv_list[model].copy()
    del model_param['discriminator']
    
    # paramごとにCV
    best_param = {}
    for param in model_param:
        # make
        param_one = model_name + "__" + param
        param_grid = {}
        param_grid[param_one] = model_param[param]
        param_grid.update(best_param) # 以前のチューニング結果を反映
        print(">> Tuning '%s' is..." % (param))
        model_cv = GridSearchCV(pipe_model, param_grid=param_grid, cv=3, scoring=make_scorer(func_scoring, greater_is_better=True),
                                return_train_score=False, n_jobs=-1, verbose=0)
        model_cv.fit(X, y)
        # Best
        best_param[param_one] = [getattr(model_cv.best_estimator_.steps[1][1], param)]
        print('Best Params:', best_param[param_one])
    
    # last
    print(">> All Best Params is...")
    print(best_param)

終わったらbest_paramを表示してくれる。うれしい。
これで学習は終わり。

変数自動合成(遺伝的アルゴリズム)
遺伝的プログラミングによる特徴量生成でLightGBMの精度向上【kaggle Advent Calendar 11日目】とかいう神が居たので応用した。
※上手く使うととても強い。
※時間はXGBでするととてもかかるので注意。決定木でやるとすごく早い。
※結局神は降臨しなかった。(抜きん出れなかったという意味で)

コピーしてnumpy行列にしておく。
numpy行列なので、列名とかの情報がなくなっているのでsaveしておく。

# numpy配列に変換
X_syn = X.copy().values
y_syn = y.copy().values
Z_syn = Z.copy().values
# 内容保存(加工しなおしに使える)
X_save = X.copy()
y_save = y.copy()
Z_save = Z.copy()

データ分割

XS_train, XS_test, yS_train, yS_test = train_test_split(X_syn, y_syn, test_size=0.2, random_state=0) # , shuffle=True
print('train size: %s, test size: %s' % (XS_train.shape, XS_test.shape))

必要最低限を採用

import random, operator, time
from deap import creator, base, gp, tools, algorithms
from tqdm import tqdm

※tqdmはfor文の進捗を示すモジュール。要らないと言えば要らない。

seed設定。

random_seed = 0

今回は0にした。(意図はない)

ベースラインスコアを設定しておく。

# ベースラインスコア(train)
clf = XGBR_V
pipe_model = Pipeline([("scaler", scaler), ("model", clf)])
base_train_score = np.mean(cross_val_score(pipe_model, XS_train, yS_train, cv=3,
                                           scoring=make_scorer(func_scoring, greater_is_better=True)))
# test
scaler.fit(XS_train)
clf.fit(scaler.transform(XS_train), yS_train)
[sum_acc, histgram] = accuracy_test(clf.predict(scaler.transform(XS_test)), yS_test)
base_test_score = sum_acc / yS_test.shape[0]

※これ以上のスコアを出す系統を採用するため。
※なかったら変なのでも追加されてしまう。

自作関数も入れておいた。

# 除算関数の定義(左項 / 右項で右項が0の場合1を代入する)
def protectedDiv(left, right):
    eps = 1.0e-7
    tmp = np.zeros(len(left))
    tmp[np.abs(right) >= eps] = left[np.abs(right) >= eps] / right[np.abs(right) >= eps]
    tmp[np.abs(right) < eps] = 1.0
    return tmp
# log関数の指定
def math_log(a):
    eps = 1.0e-7
    tmp = np.zeros(len(a))
    tmp[a >= eps] = np.log(a[a >= eps])
    tmp[a < eps] = 1.0
    return tmp

logやexpは特徴量を作り出す時によく使う。
面倒なので(変数組み合わせてもいい変数できないし)、すべて自動でやってもらうおうという怠慢。
詳しくはググってどうぞ。(なお体系的な資料は出てこないもよう)

初期設定。

# 乱数シード設定
random.seed(random_seed)
# 適合度を最大化するような木構造を個体として定義
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMax)

スコア保持用

# scores
results = [] # 特徴量数、学習データのscore(K-fold CV)、テストデータのscore
exprs = [] # 生成された特徴量の表記

スコア保持用2

# 初期値の生成
n_features = XS_train.shape[1] # 特徴量の数
base_score = base_train_score # ベースラインレートを指定
# メモリ保持
best_expr = 0
best_score = base_score
best_train_score = 0
best_test_score = 0

スコア保持用3

nf_plus = 30 # 最大追加特徴量数

合成前最終確認

print("Features: %s" % (n_features))
print("BaseRate: %.2f%" % (base_score * 100))

合成。
変数合成に成功した場合、その変数をXにも追加するがZにも追加しておく。
(手動で入れるととても時間かかって・・・)

for i in tqdm(range(100)): # プログレスバーの表示
    # 基礎表示
    num_prog = XS_train.shape[1] - X_train.shape[1]
    print("# Rank: %s, Last: %s, StartTime: %s" % ((num_prog + 1), (nf_plus - num_prog), datetime.today().strftime("%Y%m%d_%H%M")))
    print("Features: %s, Base Score: %.2f%" % (n_features, base_score*100))
    
    # 特徴量が指定数まで増えたら終了
    if n_features >= XS_train.shape[1] + nf_plus:
        print("Features are Over.")
        break
        
    # 構文木として利用可能な演算の定義
    pset = gp.PrimitiveSet("MAIN", n_features)
    pset.addPrimitive(operator.add, 2) # A+B
    pset.addPrimitive(operator.sub, 2) # A-B
    pset.addPrimitive(operator.mul, 2) # A*B
    pset.addPrimitive(protectedDiv, 2) # A/B
    pset.addPrimitive(operator.neg, 1) # -A
    pset.addPrimitive(np.cos, 1) # cosA
    pset.addPrimitive(np.sin, 1) # sinA
    pset.addPrimitive(np.tan, 1) # tanA
    pset.addPrimitive(math_log, 1) # log1pA

    # 関数のデフォルト値の設定
    toolbox = base.Toolbox()
    toolbox.register("expr", gp.genHalfAndHalf, pset=pset, min_=1, max_=3)
    toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr)
    toolbox.register("population", tools.initRepeat, list, toolbox.individual)
    toolbox.register("compile", gp.compile, pset=pset)

    # 評価関数の設定
    # 新しく生成した変数を元の変数に加えてK-fold CVを求める
    def eval_genfeat(individual):
        func = toolbox.compile(expr=individual)
        features_train = [XS_train[:,i] for i in range(n_features)]
        new_feat_train = func(*features_train)
        XS_train_tmp = np.c_[XS_train, new_feat_train]
        return np.mean(cross_val_score(pipe_model, XS_train_tmp, y_train, cv=3, n_jobs=-1,
                                        scoring=make_scorer(func_scoring, greater_is_better=True))), # このタプル化コンマは消さない

    # 評価、選択、交叉、突然変異の設定
    # 選択はサイズ10のトーナメント方式、交叉は1点交叉、突然変異は深さ2のランダム構文木生成と定義
    toolbox.register("evaluate", eval_genfeat) # 評価関数
    toolbox.register("select", tools.selTournament, tournsize=10) # 選択方法
    toolbox.register("mate", gp.cxOnePoint) # 交叉
    toolbox.register("expr_mut", gp.genFull, min_=0, max_=2) # 突然変異
    toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr_mut, pset=pset) # ?

    # 構文木の制約の設定
    # 交叉や突然変異で深さ5以上の木ができないようにする
    toolbox.decorate("mate", gp.staticLimit(key=operator.attrgetter("height"), max_value=5)) # 交叉
    toolbox.decorate("mutate", gp.staticLimit(key=operator.attrgetter("height"), max_value=5)) # 突然変異

    # 世代ごとの個体とベスト解を保持するクラスの生成
    pop = toolbox.population(n=300) # 個体...300
    hof = tools.HallOfFame(1) # ベスト解

    # 統計量の表示設定
    stats_fit = tools.Statistics(lambda ind: ind.fitness.values)
    stats_size = tools.Statistics(len)
    mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size) # mstats基礎設定
    mstats.register("avg", np.mean)
    mstats.register("std", np.std)
    mstats.register("min", np.min)
    mstats.register("max", np.max)

    # 進化の実行
    # 交叉確率50%、突然変異確率10%、10世代まで進化
    start_time = time.time()
    pop, log = algorithms.eaSimple(pop, toolbox, 0.5, 0.1, 10, stats=mstats, halloffame=hof, verbose=True)
    end_time = time.time()

    # ベスト解の保持
    best_expr = hof[0]
    best_score = mstats.compile(pop)["fitness"]["max"]

    # K-fold CVのscoreが前ステップのscoreを超えていた場合
    # 生成変数を学習、テストデータに追加し、base_scoreを更新する
    if best_score > base_score:
        # 生成変数の追加
        func = toolbox.compile(expr=best_expr)
        features_train = [XS_train[:,i] for i in range(n_features)]
        features_test = [XS_test[:,i] for i in range(n_features)]
        features_Z = [Z_syn[:,i] for i in range(n_features)]
        new_feat_train = func(*features_train)
        new_feat_test = func(*features_test)
        new_feat_Z = func(*features_Z)
        XS_train = np.c_[XS_train, new_feat_train]
        XS_test = np.c_[XS_test, new_feat_test]
        Z_syn = np.c_[Z_syn, new_feat_Z]

        # テストscoreの計算(プロット用)
        # train
        best_train_score = np.mean(cross_val_score(pipe_model, XS_train, yS_train, cv=3, scoring=make_scorer(func_scoring, greater_is_better=True)))
        # test
        scaler.fit(XS_train)
        clf.fit(scaler.transform(XS_train), yS_train)
        [sum_acc, histgram] = accuracy_test(clf.predict(scaler.transform(XS_test)), yS_test)
        best_test_score = sum_acc / yS_test.shape[0]

        # base_scoreの更新と特徴量数の再計算
        base_score = best_score
        n_features = XS_train.shape[1]

        # 表示と出力用データの保持
        print("Go to Next... 計算所要時間 %.1f分" % ((end_time - start_time) / 60))
        print("Features: %s, Best Score: %.2f%" % (n_features, best_score*100))
        print("Train: %.2f%, Test: %.2f%" % (best_train_score*100, best_test_score*100))
        results.append([n_features, best_score, best_train_score, best_test_score])
        exprs.append(best_expr)

XGBの場合2000行のデータを1周り計算するのに、8コア使用PCで1時間くらいかかる。
計算コストがバカにならないのでチューニング済決定木の方がいい気がする。

# 結果の出力
print("# Results")
print("Baseline train: %.2f%" % (base_train_score*100))
print("Baseline test: %.2f%" % (base_test_score*100))
print("Best train: %.2f%" % (results[-1][1]*100))
print("Best test: %.2f%" % (results[-1][3]*100))
# 結果のプロット
res = np.array(results)
plt.plot(res[:,0], res[:,1], "o-", color="b", label="train(3-fold CV)")
plt.plot(res[:,0], res[:,3], "o-", color="r", label="test")
plt.plot(X_train.shape[1], base_train_score, "d", color="b", label = "train baseline(3-fold CV)")
plt.plot(X_train.shape[1], base_test_score, "d", color="r", label = "test baseline")
plt.xlim(X_train.shape[1]-1, XS_train.shape[1]+1)
plt.grid(which="both")
plt.xlabel('n_features')
plt.ylabel('score')
plt.legend(loc="lower right")
#plt.savefig("gp_featgen.png")

とても大事。
再現性を取るためにこれを出力しておかないと死ぬ。

# 生成した構文木の出力
gp_col = []
print("# Generated feature expression")
for i in range(XS_train.shape[1] - X_train.shape[1]):
#for i in range(XS_train.shape[1] - 146):
    col_name = "GP_COL" + str(i)
    gp_col.append(col_name)
    print("+%s = %s" % (col_name, exprs[i]))

下記でもよい。

# 過去保存用
for i in range(len(exprs)):
    print(i, exprs[i])

X、y、Zを戻す。

# 元に戻す
X = pd.DataFrame(np.concatenate([XS_train, XS_test], 0), index=X_save.index, columns=np.concatenate([X_save.columns, gp_col]))
y = pd.Series(np.concatenate([yS_train, yS_test], 0), index=y_save.index)
Z = pd.DataFrame(Z_syn, index=Z_save.index, columns=np.concatenate([Z_save.columns, gp_col]))
print(X.shape, y.shape, Z.shape)

変数選択(簡易)
後退的選択法の簡易版を実装している。
XGBで重要度の低い変数から落として、精度が上がれば正式採用。

X_auto = X.copy()
y_auto = y.copy()
Z_auto = Z.copy()

XGBを学習させ、重要度を測定。

# モデル選択(チューニング済み)
auto_model = XGBR_V
pipe_model = Pipeline([("scaler", scaler), ("model", auto_model)])
auto_CV = cross_val_score(pipe_model, X_auto, y_auto, cv=K_Fold, scoring=make_scorer(func_scoring, greater_is_better=True),
                          n_jobs=-1, verbose=2)
# ベースレートの指定
auto_base_rate = auto_CV.mean()
print("base_rate:", auto_base_rate)
# 特徴量の重要度を測定
features_imp = pd.Series(dict(zip(X_auto.columns, auto_model.feature_importances_)))
features_imp.sort_values(ascending=True, inplace=True)
# 表示
plt.figure(figsize=(10, 30))
plt.barh(features_imp.index, features_imp)

初期設定

# 自動用Series(途中保存可能)
auto_rank = pd.Series()
auto_var = pd.Series(X_auto.columns)

# 保存用list
auto_graph = []
auto_columns = []
auto_data = []

滅茶苦茶単純なのでスルー

# AUTO_VAR
for i, col_var in enumerate(features_imp.index):
    auto_ranks = {}
    print('# Rank', i, 'Start.', 'Len:', str(auto_var.shape[0] - auto_rank.shape[0]),'Time:',datetime.today().strftime("%Y%m%d_%H%M"))
    # 変数を1残す
    if auto_var.shape[0] <= 1:
        break
    # 特徴選択(Backward Selection)
    col_drop = pd.concat([auto_rank, pd.Series([col_var])])
    # X選択
    X_sel = X_auto.drop(col_drop.values, axis=1)
    # Fit
    auto_CV = cross_val_score(pipe_model, X_sel, y_auto, cv=3, scoring=make_scorer(func_scoring, greater_is_better=True),
                              n_jobs=-1, verbose=2)
    auto_acc = auto_CV.mean()
    # 解答&削除判定
    print("→ Col:%s, Point:%.4f(%.4f)" % (col_var, auto_acc, (auto_acc - auto_base_rate)))
    if auto_acc > auto_base_rate:
        auto_rank = pd.concat([auto_rank, pd.Series(col_var)])
        auto_var = auto_var[auto_var != col_var]
        print("→→ Dropped!")
        auto_base_rate = auto_acc
    # グラフに追加
    auto_columns.append(col_var)
    auto_graph.append(auto_acc)
    auto_data.append(X_sel.columns)

列drop → クロスバリデーションして精度検証 → それより上なら正式採用

# プロット
plt.plot(range(1, len(auto_graph) + 1), auto_graph, label="Correct Num")
plt.xlabel('Epochs')
plt.ylabel('Correct Num')
plt.legend()
plt.show()

いろいろ保存

arg_max = int(np.argmax(auto_graph))
print('No.', arg_max, 'Correct:', auto_graph[arg_max])
print('Columns:', auto_data[arg_max])
# save_cols
save_cols = auto_data[arg_max]

再現性を取るために残っている変数と落とした変数を表示する。

print("Save:", save_cols.values)
print("Drop:", auto_rank)

元に戻す。

# 再変換
y = y_auto.copy()
X = X_auto[save_cols]
Z = Z_auto[save_cols]

アンサンブル(適当)
実際これでいいのかとは思っている。精度UPの実績はあるが。

# 線形回帰の実装
LR_model = LinearRegression()
# 辞書の実装
EMB_DICT = {}

学習の最後で選んだ適当な学習器を使ってアンサンブルする。

# 辞書の追加
EMB_DICT.update(list_model_sel)

とりあえずまずはどれくらい当たっているかグラフで確認する。

data_emb = []
rmse_emb = []
for name, model in EMB_DICT.items():
    print("#", name)
    model.fit(X_train, y_train)
    data_x = model.predict(X_test).reshape(-1, 1)
    data_y = y_test.values.reshape(-1, 1)
    LR_model.fit(data_x, data_y)
    [sum_acc, histgram] = accuracy_test(data_x, data_y)
    print("回帰係数:%.2f, 切片:%.2f" % (LR_model.coef_, LR_model.intercept_))
    print("R^2:%.2f, SumAcc:%.2f" % (LR_model.score(data_x, data_y), sum_acc / data_y.shape[0] * 100))
    plt.scatter(data_x, data_y, s=100, c="yellow", alpha=0.5, linewidths="2", edgecolors="orange")
    plt.plot(data_x, LR_model.predict(data_x))
    plt.show()
    data_emb.append(data_x)
    rmse_emb.append(np.sqrt(mean_squared_error(data_y, data_x)))

ついでにアンサンブルデータも保存しておく。

今回は平均アンサンブルと重み付け(1/RMSE^2)アンサンブルを実装している。
平均アンサンブルは精度上昇に貢献しなかった。

model_emb = np.average(data_emb, axis=0)
print("# emb")
data_x = model_emb
data_y = y_test.values.reshape(-1, 1)
LR_model.fit(data_x, data_y)
[sum_acc, histgram] = accuracy_test(data_x, data_y)
print("回帰係数:%.2f, 切片:%.2f" % (LR_model.coef_, LR_model.intercept_))
print("R^2:%.2f, SumAcc:%.2f" % (LR_model.score(data_x, data_y), sum_acc / data_y.shape[0] * 100))
plt.scatter(data_x, data_y, s=100, c="yellow", alpha=0.5, linewidths="2", edgecolors="orange")
plt.plot(data_x, LR_model.predict(data_x))
plt.show()

重み付けアンサンブル

emb_weights = []
for i, val in enumerate(rmse_emb):
    print("%s: %s" % (i+1, val))
    emb_weights.append(1/pow(val, 2))
model_emb_weights = np.average(data_emb, axis=0, weights=emb_weights)
print("# emb weights")
data_x = model_emb_weights
data_y = y_test.values.reshape(-1, 1)
LR_model.fit(data_x, data_y)
[sum_acc, histgram] = accuracy_test(data_x, data_y)
print("回帰係数:%.2f, 切片:%.2f" % (LR_model.coef_, LR_model.intercept_))
print("R^2:%.2f, SumAcc:%.2f" % (LR_model.score(data_x, data_y), sum_acc / data_y.shape[0] * 100))
plt.scatter(data_x, data_y, s=100, c="yellow", alpha=0.5, linewidths="2", edgecolors="orange")
plt.plot(data_x, LR_model.predict(data_x))
plt.show()

とはいえコード自体は簡単なものである。

加工と実装。

AP_X = data_normalization_transform(X)
AP_Z = data_normalization_transform(Z)
data_emb_apply = []
for name, model in EMB_DICT.items():
    print("#", name, "Apply")
    model.fit(AP_X, y)
    data_x = model.predict(AP_Z).reshape(-1, 1)
    data_emb_apply.append(data_x)
model_emb_apply = np.average(data_emb_apply, axis=0, weights=emb_weights)
answer = model_emb_apply.flatten()

提出前加工
ふつう。工夫はない。

df_answer = pd.DataFrame({'No' : df_test['No'].values,
                          '総合計' : answer })
# 提出用データ作成
df_answer.to_csv('Sub_'+datetime.today().strftime("%Y%m%d_%H%M")+'.csv', index=False, encoding="SHIFT_JIS")

終わりません。

答えの合成
黒魔術的なモノですが、効果はあります。
予測した値を使ってさらに予想する方法です。

NZc = pd.concat([df_answer, Z], axis=1).drop('No', axis=1)
NXc = X.copy()
Nyc = pd.DataFrame(y.copy(), columns=['総合計'], index=NXc.index)
NData =pd.concat([pd.concat([Nyc, NXc], axis=1), NZc], axis=0)
print(NData.shape)
NData.head()
# トレーニングデータ(全体)の作成
Ny = NData['総合計']
NX = NData.drop(['総合計'], axis=1)
data_emb = []
rmse_emb = []
for name, model in EMB_DICT.items():
    print("#", name)
    model.fit(NX_train, Ny_train)
    data_x = model.predict(NX_test).reshape(-1, 1)
    data_y = Ny_test.values.reshape(-1, 1)
    LR_model.fit(data_x, data_y)
    [sum_acc, histgram] = accuracy_test(data_x, data_y)
    print("回帰係数:%.2f, 切片:%.2f" % (LR_model.coef_, LR_model.intercept_))
    print("R^2:%.2f, SumAcc:%.2f" % (LR_model.score(data_x, data_y), sum_acc / data_y.shape[0] * 100))
    plt.scatter(data_x, data_y, s=100, c="yellow", alpha=0.5, linewidths="2", edgecolors="orange")
    plt.plot(data_x, LR_model.predict(data_x))
    plt.show()
    data_emb.append(data_x)
    rmse_emb.append(np.sqrt(mean_squared_error(data_y, data_x)))
emb_weights = []
for i, val in enumerate(rmse_emb):
    print("%s: %s" % (i+1, val))
    emb_weights.append(1/pow(val, 2))

手順をやり直しているだけなのでコメントのしようがない。

model_emb_weights = np.average(data_emb, axis=0, weights=emb_weights)
print("# emb weights")
data_x = model_emb_weights
data_y = Ny_test.values.reshape(-1, 1)
LR_model.fit(data_x, data_y)
[sum_acc, histgram] = accuracy_test(data_x, data_y)
print("回帰係数:%.2f, 切片:%.2f" % (LR_model.coef_, LR_model.intercept_))
print("R^2:%.2f, SumAcc:%.2f" % (LR_model.score(data_x, data_y), sum_acc / data_y.shape[0] * 100))
plt.scatter(data_x, data_y, s=100, c="yellow", alpha=0.5, linewidths="2", edgecolors="orange")
plt.plot(data_x, LR_model.predict(data_x))
plt.show()
AP_NX = data_normalization_transform(NX)
AP_Z = data_normalization_transform(Z)
data_emb_apply = []
for name, model in EMB_DICT.items():
    print("#", name, "Apply")
    model.fit(AP_NX, Ny)
    data_x = model.predict(AP_Z).reshape(-1, 1)
    data_emb_apply.append(data_x)
model_emb_apply = np.average(data_emb_apply, axis=0, weights=emb_weights)
answer = model_emb_apply.flatten()
df_answer = pd.DataFrame({'No' : df_test['No'].values,
                          '総合計' : answer })
# 提出用データ作成
df_answer.to_csv('Sub_'+datetime.today().strftime("%Y%m%d_%H%M")+'_2.csv', index=False, encoding="SHIFT_JIS")

おわり

最後に

意外と超大作になってしまった。(文字数3万文字越えって・・・)
jupyterコードについては、めちゃめちゃデータ(守秘義務)が表示されているので社内のみで共有します。申し訳ございません。

最後まで見ていただき、ありがとうございました。