ガチャとバトロワから抜け出せない我々

ガチャとバトロワといったら近年のゲームを代表する要素であると個人的には考えている. これら2つがなぜ流行ったのかについてなんとなく考えたことがあったのでまとめたい.

もちろんここで挙げたものは他の趣味に当てはまることも多い. そのため一概にガチャとバトロワを悪というわけではなく,健全に楽しむためにこれらは次のような性質を帯びていると心に留めておくためのメモである.

雑にまとめるとこれらは承認欲求と確率報酬に相性が良いと考えられる.

SNS時代と承認欲求と同質化

これは挙げている人も多いだろう.SNS時代ではガチャの結果やバトロワのランクやプレイ動画を共有することが容易になった, この共有は

・ガチャ→引きが良かった場合に承認,悪かった場合はより悪い人を見ることで自己正当化 .バトロワ→高ランクであれば承認,低ランクである場合は高ランクな人に憧れや同一視

と言った結果を与える.もちろんこれは他の趣味にも言えることであるが,特にゲーム関連は性質上共有が容易であるために相性が良いともわれる.

これらに加えて,SNSは同質化も生む.ここで言う同質化とはフォロー関係によって同じような趣味嗜好を持った人物でTLが構成されることを意味する.このような環境では意見が否定されることは少なく,課金者は◯万円爆死といった事を共有しあって,より課金の沼に嵌っていくというような現象が起こりうる.同質化は同時に分断も生む.例えば課金者と無課金者の間の分断は生じやすくなるだろう. 課金者>無課金者,高ランク>低ランクといった思想を生みやすく,課金者や高ランク者が自らを正当化し,承認を得ることができる機会が増加することとなる.

飽食の時代において承認欲求は最も強力な欲求の一つである.

確率報酬による報酬系ハック

人間の脳は確率報酬に対して大きな快感を得るというのは広く知られているところだろう. エビデンスも様々あるだろうが,ちょっと面倒なので割愛させてください… 私は確率報酬を狙って提示することを個人的に報酬系ハックと呼んでいる.

この報酬系とガチャの噛み合いは凄まじいのは言うまでもない.また,ガチャは排出率が確率固定なため手に入れたものの希少価値がある程度保証されており,それも報酬系ハックに寄与してるんじゃないかと思う.

バトロワは関係無いんじゃ?と思うが個人的にはそうでもないと思っている. バトロワ系ゲームは様々なところにランダム性(アイテムの湧き場所など)が散りばめられている上,参加人数が多いために確率報酬の性質を帯びていると考えている.(少なくとも主観的にはある程度運ゲーに思える) これに加えて,レーティングシステムというのが凄まじく,レーティングが正常に機能する限りはすべての試合が50%で報酬を手に入れられるゲームと化す.それはバトロワよりレーティングシステムの問題じゃね?というのはそれはそうだが,触れないわけにはいけないのでここで触れた.

確率的に明確な報酬提示をするシステムに人間の報酬系は抗えない

責任の不在

ガチャは完全な確率ゲーなので単純だろう.引く側に責任はまったくない. バトロワも単純だと思う.人数が多いしときにチーム戦であるバトロワはうまくいかなかった時の責任を他人に押し付けることができる. そしてそれをSNSに共有すればそれに賛同する人も集まってくる.

自分に責任がかからない,もしくは回避できる環境は自己が否定されることが無いということであり,自己承認的に非常に有利である.

サンクコスト効果

サンクコスト効果とは例えばガチャで1万円課金して目当てのキャラを引くことができなかったときに,そこで撤退することができない心理を表す.ガチャに関しては明確であろう. バトロワについては,ランクがサンクコストとなる.時間をかけて手に入れたランクやSNSでの地位を手放すことは難しくなっている.

実物を扱うような趣味では中古で売るという選択肢のおかげでこのサンクコスト効果は多少薄くなる, ゲームに置いてはそのゲームを離れたら何も残らないためサンクコストは非常に重くのしかかる.

最後に

ガチャやバトロワに次のような性質が有るからと言って,それだけで売れるわけでは当然なく,これらはゲーム自体の面白さやキャラクターと揃うことで初めて本領を発揮する. ゲームのヒットについて語られるときはゲームシステムやキャラに焦点が当てられることが多いし,実際そこがゲーム各社の力の入れどころであり,凄いところである.

一方で語られることの少ないベースシステムの持つ性質について気をとめることは必要だろうと思い今回まとめた. 特定の会社やゲームを貶める意図は無いです.

AHC001 参加記

AHC001に出ました.あんまり順位良くないんですが,(自分含め)ヒューリスティックコンテスト初心者の方の参考になればと思うので, 取り組みとか諸々をまとめようと思います.

問題概要

https://atcoder.jp/contests/ahc001/tasks/ahc001_a

ざっくり,

  • 104 * 104マスのフィールドがあります.ここに長方形の広告を配置します.
  • 広告はN(50〜200)個あり,それぞれ希望場所と希望面積があります.
  • 広告が希望場所を含まないとスコア0です.含む場合希望面積に近いほどスコアがもらえます.
  • 広告が重なっちゃだめ.

という問題でした.

自分の解法ベース.

最終提出のソースコードはこちら(後で追加予定)です.

(多分)ビームサーチでときました. ある広告配置があったとき,広告配置の近傍を

  1. ある広告を選んで,その広告をx方向もしくはy方向にkマスだけ大きく/小さくする
  2. ある広告を選んで,その広告をx方向もしくはy方向にkマスだけずらす.

と捉えて,状態遷移をしていきます.ここで2の操作は2回の1の操作として代替することも出来ます. しかし,そうすると,ある辺を短くする→長くするの順番によってシフトが実現されるときは,かならずどちらかの操作でスコアが減少する(スコアが面積に対しておおよそ単調な変化をする)ため,シフトが発生する可能性は非常に低くなります.

希望点を含みさえすればよいという今回の問題設定では広告のシフトは有効なため,シフトを陽に近傍に含むべきだと考えたためこのように2種類の近傍を取りました. この近傍を用いて,

  1. 再初期状態は面積1の広告をそれぞれの希望場所を含むように配置する
  2. 最初期状態を要素とする,候補状態配列A=[a_0]を作る
  3. Aごとにランダムに広告を1つ選び近傍操作をすることを適当な回繰り返し,R個の次に可能な状態(広告の重なりがない状態)を得て,S=(len(A)×R)個の次の状態を得る. 4 S個の状態から,スコアの改善度が高いT個を選んで,T個を次の初期状態A=[a_0,a_1...a_T]とする.
  4. 3と4を繰り返してスコアの改善をする.

アルゴリズムでスコアを伸ばします.

解法改善1

近傍操作の拡大縮小もしくはシフト量を1とかにしてると,時間内のループでスペースが埋まりきりません. そのため,拡大縮小もしくはシフト量kを1〜100からのランダムとかにします.(スコア45〜くらい)

解法改善2

スコアの改善度が高い組だけを選んでいるので,大きく盤面を戻すことがありません. そのため,盤面が非常に非効率的だった場合そこから抜け出すことができず,スコアを大きく落とします. (つまり乱数の機嫌にスコアがめちゃくちゃ影響される)

これはあまり良くないので,改善する必要があります.真面目にやるとすれば色々有るんでしょうが,そこまで時間が取れなかったので簡単に初期状態ガチャを回すことにしました. 実験の結果,1000msもあれば盤面あたり90〜付近のスコアが出てほぼ収束していることがわかりました.そのため,

  1. 最初期状態に対して,1000msでベース手法を適用して4回初期盤面ガチャを回す.
  2. 4盤面中最もスコアが良い盤面を初期状態として,ベース手法による改善をする(ただし,盤面が埋まっていて,可能な状態は限られるため移動量kは30程度で小さくとる)

として,スコアを安定させます.(スコア464〜くらい)

解法改善3

乱択でやっていると,どうしても抜け漏れが出るので,全広告に対して,

  1. 頂点を一つずつ見る
  2. 頂点をずらして(つまり広告を大きく/小さくして)スコアが改善する場合ずらす
  3. 1,2をスコア改善がなくなるまで繰り返す

という非常に単純な貪欲を最後(実行時間4500ms〜)に行うことで微妙なスコア改善を狙います. 正直これはわかるほどスコア改善しませんでしたが(乱数によるスコア分散に埋もれてしまう程度の貢献しか無かった), 原理上必ずほんの少しはスコアが改善するのでいれました.

やらなかったこと

  • 例えば広告が隣接している場合は,その隣接しているところを動かす(つまり2つ以上の広告を同時に大きく/小さくする)とスコアが改善する可能性がありますが,そのためには広告の接地情報や希望場所同士の距離を持って探索しなくてはならないので実装が重くて止めました.
  • ベース手法の3では次状態を生成してみてからそれが可能かどうか判定しています.これは広告数Nに対して最悪時O(N)の判定をしているので非効率です.ここを頑張って枝刈りすれば,だいぶ高速化されます.試さなかったことの1つ目と似たようなデータを持てばできる気がします.
  • 改善手法2で述べましたが,大きな手戻りをしないとスコアが改善しない場合もあります.そのため近傍遷移1回ではなく,複数回の近傍遷移をした後のスコアの最大値を元に次状態を決める(強化学習的な感じ)遷移をした方がスコアが伸びる気がしています.
  • これも改善手法2に関わりますが,同程度のスコアの場合,次にとれる状態数が多いほどその盤面の価値は高いです.そのあたりを組み込めばスコアが伸びた可能性はあります.
  • なんか正方形に近いほうが有利な気がします.相加相乗平均の等号成立条件を考えると,正方形が面積辺りの辺の長さが最も小さいことになります.広告が重なってはいけないという条件では,辺が長くなると他の広告を邪魔する可能性が上がるはずなので,長細くなるほど不利な気がします.とはいえこれはどう組み込めば良いのかとんと検討がつかず…

その他感想

  • ヒューリスティックは比較的業務プログラミングに近い(実装量とか拡張性,最初は速度無視で書いて後からチューニングでも良い等)と思ったので,スコアはともかく継続的に出るのはかなりプラスになるなと思いました.
  • ヒューリスティックはこんな感じのブログ記事が書けるのもポイントだと思います.(https://zenn.dev/fkubota/articles/3d8afb0e919b555ef068)[Kaggle日記]のような感じでコンペ中からまとめてくのが良い気がしています.
  • 後は非減少のレーティングも嬉しいです.(精神的/時間的な負荷がきつく)もう競プロは引退かなとか思ってましたが,ヒューリスティックはあと10年くらいは遊べそうで嬉しいです.

Chem Infomatics系の基礎事項まとめ(富士フィルム Brain(s)コンテスト参加記)

先日ChemInformatics系のコンペに出て、0から調べて色々知識を得たので、それについてまとめようと思います。 一応参加記なのですが、あんまり面白いことをやってないので基礎まとめの要素が強くなり、それを反映したタイトルにしました。

コンペで使ったモデル

実際にコンペで使ったモデルはmordred+LightGBMの面白みに欠けるモデルと、Transformeを事前学習してembedding生成モデル化+LIghtGBMのモデルでした。
(Privateの結果は出てませんが、後者はPublic LBでもCVでも性能が良かったのと、あんまり見ないモデルだと思うのでPrivateの結果によらずちょっとお気に入りです。)

後者のモデルはKatja Hansen氏のAmes試験データセットでの5Fold CVにおいて、AUC平均0.81を、"予測時にはSMILESのみから"得ることができました。AUC 0.81は精度単体としては大したことないんですが、SMILESだけから出たので結構嬉しいかなと思いました。

このpretrained Transoformerによる molecular embeddingはさらにチューニングとかを検証すればChemInfo系のコンペの飛び道具として使える可能性があると思います。FineTuningだけでいいので、使うのも割と楽です。こちらの手法は結構下の方のsmiles2vecのところでまとめています。

問題設定

一応問題設定に触れますが、あんまりここは以降のところには関係ないです。

mutagenicityという発がん性につながるような化合物の性質をあるかないかの2値で分類する問題でした。(mutangecityの本家の方のテストでは強/弱/無しの3段階くらいになることもあるみたいですが、今回は2値でした。)

学習用データセットは公開データセットを用いることになっており、メインとなったのがこちらの論文https://pubs.acs.org/doi/abs/10.1021/ci900161gのデータセットでした。件数としては6000件ほどで、正例負例が均衡なデータセットとなっています。

記述子とFingerprint

化学物質を機械学習で扱えるような形に変換する方法として、一般に記述子とFigerPrintの2つがあるようです。
詳細に中身を確認しているわけではないのですが、記述子は例えば分子量や、芳香族炭素の数のようなどちらかと言うと化学的性質を表すものを含み、fingerprintは純粋に構造的な物だけといった印象です。例えばFIngerPrintの一つ、Morgan Fingerprintは全原子を中心として、結合グラフにおいて距離r以下の部分構造をすべて数え上げるような操作をしています。

どちらの方が向いているかはタスクによるとおもいますが、機械学習の文脈では多くの場合記述子のほうが扱いやすいかなぁと思いました。(後述しますが、一般にfingerprintは高次元でスパースなベクトルなのでちょっと扱いに悩みます。) この記述子やFingerPrintを計算するライブラリとして、後述するRdkitというものが有名なようです。

Rdkit

非化学系の人間には全く馴染みが無かったんですが、ChemInfo的には定番of定番みたいなツールです。
これを用いて、Smile記法という化合物を文字列で表す記法から様々な化学的、物理的性質を計算したりすることができます。
その他にも便利なツールがめちゃくちゃ入っているので、Cheminfoをするならとりあえずこれというふうになりそうです。

Rdkitのインストール

RdkitのインストールはAnaconda一択です
環境を汚したくない派閥の方々はAnaconda系を入れるのを嫌うかもしれませんが、RDkitはAnaconda以外でのインストールは非常に煩雑で、やるべきではないと思います。
なんでpipで入らないかは、

RDKit: Why the RDKit isn't available on PyPi

にかかれています。抜粋すると、

The core problem is that the RDKit is not a pure python package; it's a mix of python and some compiled extension modules (shared libraries).

とのことで、python以外にも諸々必要でそのリンクの問題などを解決するのがPipだと難しいようです。
環境を汚したくない派閥のわたしはDockerでAnaconda環境を作ってその中で本コンペは作業していました。 GUIを使わず、jupyter-labで作業が済む範囲はDockerで環境作るのもそんなに大変じゃないので、結構いいと思いました。

Rdkitの使い方

については、公式の RDKitドキュメンテーション非公式日本語版サイト — RDKit_unofficial_translation_JP 2019.03.1 ドキュメント を参照して頂いたほうがいいので、ここでは深く触れません。 また、 化学の新しいカタチ | 有機合成化学者のための計算化学・ケモインフォマティクス入門 こちらのサイトも非常に参考になります。合わせて見るのをおすすめいたします。

ざっくりいえば、SMILES記法やその他によって表された分子をrdkit.Molというオブジェクトに読み込んで、そのMolオブジェクトのもとで様々な値等を計算するという流れになります。

記述子計算に役立つmordred

上記の記述子を計算するにあたって、Rdkitだけでは200個前後しか計算できません。しかし、記述子は日々様々な論文で提案されもっとたくさんあります。 そこで更に多くの種類の記述子を計算したくなることがありますが、その時の選択肢の一つに入るのがmordredです。

github.com

https://www.jstage.jst.go.jp/article/ciqs/2016/0/2016_Y4/_pdf/-char/ja

mordredは約2000もの記述子を計算することが出来る非常に便利なツールです。
2000もあるので、計算にはそこそこ時間がかかりますが、記述子をたくさん出せるので、脳死で使っておくだけで性能が向上したりします。
ただ、条件は不明ですが途中で計算が進まなくなるバグ?が発生したりするので、ちょっと注意が必要かもしれません

FingerPrintの種類

化学の新しいカタチ | 有機合成化学者のための計算化学・ケモインフォマティクス入門 おもにこちらのサイトを参考にしました。

morgan fingerprint

まず、分子をグラフ構造としてみます(原子がノード、結合がエッジ)。
その後、分子内に含まれるそれぞれの原子について、その原子からグラフとして距離がr以下の原子達によって作られる部分構造を見ていきます。
この部分構造にidを振って、OneHot Vector的にするのがMorgan FingerPrintです。
rは通常2か3を使うようです。個人的には2がいい感じでした(3になると種類が増大することに寄るデメリットの方が大きい気がしました)
当然存在しうる部分構造はものすごい数になるので、通常はスパースなベクトルか、ハッシュ化して固定長にしたベクトルを用います。
ハッシュ化はハッシュ衝突を避けるために1024bitとか2048bitを使うのが通常なようですが、512bitでもなんとかならないことはないイメージです。
(次元数の増大に弱い手法を使うときは512bitもやむなしかなと思う次第です。)

また、morgan fingerprintには、より抽象化した(ちゃんと理解してないですが、OとSとかCとSiは大体にた働きだよね?ということで同じ役割の原子として2つを区別せずまとめ上げる)バージョンも存在し、Rdkitではuse_features=Trueとすることでそちらのベクトルを使用可能です。

(use_features FalseはECFP,TrueはFCFPとか言われるFigerPrintの一種らしいです。)

アトムペアフィンガープリント(Atom pair fingerprint)

すべての原子の組について、それぞれの原子の原子タイプと、結合距離から求められるidをCount vector的にするのがAtom Pair Fingerprintです。
原子タイプはその原子の種類に加え、隣接する重原子の数とπ結合の数を加味して割り振られます。
morgan fingeprintと異なり結合距離が遠いものについても加味されるのが違いです。

morgan fingerprint同様ものすごくスパースなベクトルになります。
個人的にはlightgbm等の入力ベクトルとしてはあんまりうまく働きませんでした。(SVDとかで次元削減をしなくてはいけないせいもあるかもしれません)

トポロジカル二面角フィンガープリント

4原子によって定義されるすべての二面角について、その4原子の原子タイプによってidを振るようです。
あまり使っていないのでわかりません…
atom pairと同じような原子タイプを使っていますが、2面角の関係上遠く離れた原子については考慮していないフィンガープリントです。
これも個人的にはlightgbm等の入力ベクトルとしてはあんまりうまく働きませんでした。

ドナーアクセプターフィンガープリント(Donor Acceptor fingeprint)

基本思想はアトムペアフィンガープリントと同じですべての原子の組について、その原子タイプと結合距離からidをふって、それをcount vector的に扱います。
相違点は、原子タイプの粒度でこちらはその原子の役割(カチオン、アニオン、ドナー、アクセプター、極性原子、ハイドロフォビック、その他)の7種類で扱うようです。こちらも個人的実験の範囲ではあまりうまく働かなかったのですが、アトムペアと比較すると次元数が大分少なく取り回しが良かったです。

その他手法やツール

ここには今回はそんなに触らなかったんですが、選択肢には入ると思った手法やデータセット類をまとめます。
DeepChemのドキュメントの"Model Classes","Featurizers","MoleculeNet"を眺めると他にも色々見つかると思います。 The DeepChem Project — deepchem 2.4.0rc documentation

DeepChem

深層学習系の手法を使うときに便利なユーティリティやモデルがまとまったものです。主な機能として、

  • 有名データセットのロード
  • 各種Featurizer。Rdkitにないfingerprintなども存在します。
  • 各種モデル(smile2vec,smiles2image,GraphConvolution等)

があります。Rdkitよりも一部は多機能ですし、次cheminfo系のタスクをするときにはちゃんと使ってみたいです。
ちょっと日本語でのネット記事などが少ないので学習コストが高めかもしれないというのと、わたしがまだ理解できてないだけかもしれませんが、各種モデルの細かい設定をしようとすると難しいような気がしたのがあえて言えばマイナスかなと思います。

ただ、使いこなせればかなり強力なツールだと思います。

Graph Convolution

CNNにおける畳込みを二次元のグリッドではなくグラフ構造に拡張した手法です。
簡単なタスクでは記述子ベースのものとそんなに変わらないくらいのスコアなので、(溶解度予測とかだと本当に誤差レベルの差しか出ませんでした。) 今はまだ理解の難しさの割には積極的に採用するほどではないかなと思いました。
割と最近の手法になります。ライブラリとしてはDeepChemなどがありますが、個人的に中のグラフをいじったりが難しくちょっと使いにくい手法かなと感じました。

smile2vec

smiles記法をNLPタスクのように扱って(BERTとか,もしくはseq2seq的に)学習した中間表現をfeaturizerとして使う思想です。
LSTM系のものもあれば、Transformer系のものもあります。
嬉しさとしては、事前学習を教師なし学習にすることができれば、学習データセットが少ない場合などにほかの手法よりもスコアが出る可能性があると思っています。
ただ、普通にNLPタスクっぽく扱うのは化学的な性質を捉えた中間表現にはならない気がするので、なんらかの工夫が必要だと考えました。
今回わたしはそれまでのsmiles2vecと変えて(観測範囲では新規)、smiles→smilesを予測するのではなく、もう少し立体的な実情を表すと考えるsmiles→morgan fingerprintを事前学習することにしました。(これが最初に触れた変なモデルです。)具体的には、

transoformer encoderの最終出力をMaxpoolingしてそれを全結合層+Sigmoid-XentropyでMorganFingerPrintを予測する。
という事前学習をChemblからサンプリングしたデータセットに対して行い、そのpretrainedモデルをLightGBMに接続することで、CV0.81を出すことに成功しました。pretrainの入力は後述するrandomized smilesでAugmentationしています。
さらなる検証は必要ですが、化合物のpretrainedモデルとして意外とやれる子が誕生したかもしれません。

細かい話としては、

  • tokenizeはchar-base。元素記号を扱うので当然だけど大文字小文字は区別する。
  • Transfomerのアーキテクチャ: maxlen120,hidden 192,head 4, hopping 4(コンペのレギュでこれ以上大きくならなかった)
  • Maxpooling : (1,4)で(120,192,1)→shape(120,192/4,1)に。その後Flatten
  • ターゲットのmorgan fingerprintは512 bit,FCFP相当。あまり疎だと学習が進みにくいのとbit衝突の回避のバランスを取りたかった。
  • lossは各bitごとのXentropyの和。これ使うために最終出力はSoftMaxじゃなくてsigmoidを活性化関数に使う。
  • chemblからのサンプルはSMILESの文字数で、[0-20],[20-40]...[80-100]で均等に(これをしないと、最も多い60付近の化合物に多い構造ばっかり予測してしまうせいかイマイチうまく行かなかった。)
  • DataAugmentationでRandomize SMILESするので、Augmentation前のMaxlenは100でも、transformer入力のMaxLenは120にした。
  • LightGBMは初期パラメータ。

で行いました。

smile2image

smile2記法からRDkit等で生成した化合物の画像にCNNなどの画像系手法を噛ませて扱おうとする手法です。
個人的には化合物が大きくなった時の画像の縮尺などの問題が出るからイマイチかなぁと思っています。
実際にためしてはいないのでわかりません。

randomized smiles

smiles2vecとかだとsmilesの順序を変更することによるDataAugementationをすることができます。 Randomized SMILES strings improve the quality of molecular generative models | Journal of Cheminformatics | Full Text

このときにSMILESを並び替えるのを手動で実装するのはめちゃくちゃ苦行なので、簡易的にですがRDkitの原子のRenumberによるrandomizeを利用するとよかったです。
方法としては、

  1. SmilesをRdkitで読み込んでmolにする。
  2. 分子に含まれる原子数がN個だったとして、1〜Nをシャッフルした配列を作る。
  3. 2で作った配列を元に、Chem.RenumberAtomsで原子の通し番号を振り直す。
  4. Chem.MolToSmiles(mol,canonical=False)で新しいSMILESを出力する。

これで少し違うSMILESが出力されます。Canonical=Trueだと同じのになっちゃうのでそこミスらないように注意です…

記述子+決定木

記述子はスケールが一定じゃない(原子の個数を数えたり、電荷っぽい値だったりが混ざっている)ため、ニューラルネット系の手法やサポートベクターマシン系の手法よりは決定木系の手法の方がファーストトライとしては楽かなと思います。
ただ、人間が関わらないタスクについては(感情判定のような主観評価が混ざらないという意味です)サポートベクターマシンが強いとかいう噂もあるので、試して見る価値は大いにあると思います。

fingerprint系の手法

fingerprintの多くはかなり疎で直接扱うのがあんまり筋が良くない感じでした。
morgan fingeprint(Circular fingeprint)や、その他Deepchemでサポートされる中でもbit数を固定できるfingeprintをまずは使うのが丸いんじゃないかなぁと思いました。

Chembl

データセットです。薬系の化合物がすごいいっぱい集まっています。事前学習をするようなタスクのデータセットを集める元として優秀だと思います。
ただ、ダウンロードにすんごい時間かかります。

QM

化合物の量子力学的な値のデータセットです。 QMでtrainされたモデルをStackingすると記述子とかでは扱いきれてない量子力学的な作用とかはいったりするのかなぁとか考えてました。

Tox21

化合物の毒性に関するデータセットです。毒性の方向にも色々あってそれらがすべてまとまってるので、各種タスクに絞ると意外とデータ量が少ないです。

感想と疑問

  • conda-forgeが使えないのきつかったです。(local importに気づくまで絶望していました。)
  • 実行マシン512MB/提出10MBも結構きつかったです。(最初の方気づかずにBERT-base相当のtransformer動かしてて無駄になりました。)
  • anacondaがあるとpipしにくいので、Rdkit様にはなんとかしてpip対応してほしい…
  • 発がん性とか実際は代謝生成物とかも追わなくちゃいけない気がするので、難しそうだなと思いました。
  • 化学の専門家がSMILESだけから発がん性当てるタスクしたらどのくらい精度出るのかが素朴に気になります。とりあえず人間は越えたのだろうか。
  • 実際のところ量子力学的な性質を考えるのは意味があるんだろうか。むしろタンパク質とかとの立体的な相互作用の方が効くのかも?(そうであれば原子とその立体的位置を特徴量にして学習したらうまく行ったのかも。)
  • DescriptorってChirarlity反映できるのどのくらいあるんだろう。(生体作用の場合はChirarlityは結構Criticalな気がする(素人))

SIGNATE Student Cup 2020 [予測部門] 参加記

はじめに

去年に引き続きSIGNATE学生コンペに出てました。
今年はNLPコンペです。
NLPか〜考察ゲーってよりはSOTAな手法をガチャガチャするゲームなりそうで辛いな〜と思いつつ参加しました。
案の定SOTAな手法をガチャガチャして、闇の中を彷徨う苦行をしてたら終わったんですが、NLP初心者だったのでそれでも学びが多くて良かったです。

結果

21位でした!public40位くらいからのShake Upだったので満足です。
ちなみにBERT-base-uncasedに分類ヘッド付けてBERTごとFinetuningしたものと,CountVector+LightGBMとSWEM-max+LightGBMの6:2:2のblendでした.
後述しますが不均衡なデータだったので、
BERTモデルはUnder-sampleしてrandom seed average(n=6),
その他のLightGBM系モデルはimbalanced-learnのBalancedBaggingClassifier(n=30)
を使ってます。LightGBMは初期パラメータです。

例のLB Hackは使ってないです。(ただのargmaxです)

f:id:MosaasoM:20200827000533p:plain

タスク概要。

英語で職募集文が書いてあるので、それが

  1. データサイエンティスト
  2. 機械学習エンジニア
  3. ソフトウェアエンジニア
  4. コンサルタント

のどの職種か当てる問題。明確に判別が難しい領域が多めの問題設定。またターゲットに結構偏りがある(最多数ラベルと最少数ラベルで4〜5倍くらい離れてた。

ざっくり学び

  • まずはCountVectorやTf-Idf + LightGBMを使う。trainとtestで使われている単語が同じようなときは多分それだけでかなりスコアが出る。
  • 不均衡データはまずはimbalanced-learnのBalancedBaggingClassifierを使う。class weightによるチューニングは調整が難しく、過度なチューニングはShakeの可能性も上がりうるなどあんまり得しないと思う。
  • imblanced-learnを使うのが面倒なニューラルネット系の手法とかはUnderSamplingをrandom seed averageでひとまずいい気がする。学習も早くなるし。
  • under-samplingによる確率のバイアス修正は、testデータの分布がtrainと同一か不明なときにはやらなくていいと思った。(結局偽陽性とかとのトレードオフなので。)分布がわかってるときはやったほうがいいかもだけど。
  • 未知語が多いなら、次に試すのはSWEM-max(word2vecのMaxpooling)。
  • それでも駄目だったら仕方ないのでBERTなどのPretrained Modelを使う。未知語には間違いなく強い。
  • Transformer使うならとりあえずHuggingFaceのtransformersでよし。
  • でもtransformersのモデルたちは中の層の一部をいじったりできないので、それがしたいときは公式Githubとかから気合でやるしかない(今回はやってない)。Tensorflow1.xの実装とかも結構残ってるので、そこら辺をうまいことできるライブラリなりなんなり整備したほうがいいかも。
  • BERTは全層FineTuningしたほうが精度が出ることはある。コストは重いがfreezeは必ずしも最適ではない。(それはそう)
  • 今回はとくに元のBERTのタスクでは分類が難しい物同士の分類だったので、全層学習が良かったんだと思う。(例えば、He design (Server/Model) Architecture in google. みたいな文では()内のどちらも元もBERTのタスクだと大差なく扱われると思う。ここを補正してあげないとうまく行かない?)
  • BERT族のファインチューニングは学習率がAdamなら1e-5とかそのあたりが良かった。大きいと全然駄目。
  • BERTの他に文書分類ならとりあえずXLNet,ALBERT,RoBERTa,DistliBERT,DistliRoBERTaあたりがよし。XLNetはBERTと割と違うのでブレンドにはいいかも?やってないけど。
  • BERT以降のマイナーチェンジっぽいモデル(ALBERTとか)はGLUEに過学習してる雰囲気がなくはないので、心配だったら無難にBERTがいい気がする。
  • 再翻訳によるDataAugmentationならTransformersのMarianMTにいっぱいモデルがある。欧州諸語は結構精度出るが、逆に言うと再翻訳しても変わらないのも多いので、それを取り除く処理は必要かも。ちなみに日本語翻訳の精度は使えないくらいに低い…
  • 有償API使えるならDeepLでいいんですけどね。
  • MarianMT,結構処理時間かかるので(確か最後にビームサーチとかするのでGPUでもそんなに高速化しない)、ジェネレーターの中で呼び出すよりは予め水増しデータ作っちゃうほうがいいと思った。
  • 再翻訳によるDataAugmentationをするときにはCVの切り方に注意。水増し元のデータがtrainで、水増しデータがtestとかに行っちゃうと当然だけどLeakしてる。
  • 再翻訳によるDataAugmentationは効かないこともある。(今回は実際効いてなかった)
  • CountVector系手法とSWEM手法とBERT系手法のblendingは結構いい感じだった。多分全部結構違う方向性をもってるからだと思う。

細かいあれこれ

CountVector,Tf-Idf

  • まずはskleranのCountVectorizerとかTfIdfVectorizerでよし。
  • データ数が少ない時、CountVectorizerはbinary=Trueでいい気はする。(オーバーフィットの原因になりそう。未検証)
  • trainとtestの語彙に差があるときは、testの方でvectorizerをfitするのも手(testデータがすべてわかってればだけど)(今回は大差なかった)
  • stemmingとか品詞判定はとりあえずspacyが準備も利用も精度的にも楽だと思う。
  • 品詞でしぼるのもいい(名詞と形容詞だけとか)。実際副詞とかpunctuation系はノイズになることが多いと思う。

SWEM

  • とりあえずword2vecでやるのが楽。GloveとかFastTextはベクトルのファイルサイズが大きすぎて辛かった。
  • Mean,Max,Mean+Max,Hierarchical Maxとあるが、Maxが割と安定していた。
  • CountVectorとかに比べるとまだ未知語には強いがとはいえそこまでといった感じ。語彙に差が大きいときは微妙。
  • こっちもCountVectorとかと同じく、testにある単語だけとか品詞絞りとかもあると思う。(こっちも同じく大差なくて使ってない)
  • CountVectorと比較して次元数がめちゃくちゃ低いのはいいところ。(CountVectorは103〜104とかになりうるが、SWEMは300次元)

BERT系

  • とりあえずBERT-base-uncasedのfreezeと全層学習両方試すところから入るのがいいと思う。BERTはcased-uncasedとかバリエーションが多いのも良い。
  • ALBERTは自分の試した範囲では今回微妙だった(どうやっても精度があがらず、コンペ最初の方の結構な時間を溶かした)。GLUEにオーバーフィットしてるだけのモデルなのかもしれない?わからんけど。GLUEに近いタスクにはいいのかも?
  • RoBERTaは無難に強い(CVでは強かった)。PublicLBでは微妙だったが果たして…→実際微妙でした。
  • XLNetはBERTとちょっと学習方法とかが違うので多様性出すなら多分良い(今回は最終subに混ぜてないけど、予測)。ただ、BERTより学習が割と重いので実験とかがしんどい。
  • 今回は使わなかったけど、 BERTの精度を向上させる手法10選 - Qiitaこちらかなり勉強になった。
  • BERT-largeくらいのパラメータ数(330M)ならギリギリGoogle Colabで行けなくもないけど、batchサイズがかなり小さくなる(16とか8とか)。それ以上は無理。
  • transformersの機構を時系列予測っぽく使う(要はpretrainedは使わず、しかもBERT likeなタスクじゃなくて、Transformerの最終層から直接ターゲット値へのDenseにつないじゃうような使い方)は、未知語に弱くなるのでなんとも…とはいえ語彙がtrainとtestで同じようなときはCountVector系手法の上位互換になるかもしれない。
  • BERTで言うpooled outputは[CLS]トークン部分の最終層の出力のことらしい。用語が紛らわしすぎ。lambda layerとかで抜き出すと割と柔軟にいろいろできる気がする。
  • BERT側をfreezeする場合は、pretrained BERTの出力をニューラルネットじゃなくて、lightGBMとかSVM系の手法につなぐとニューラルネットよりいい精度が出ることが稀にある。今回は出なかったけど。
  • soft labeling([0,1,0]じゃなくて、[0.1,0.8,0.1]みたいな感じでラベルを与える)のも場合によってはいいかも。特にBERT本体ごと再学習するときは過学習を抑えやすくなる気がする(多分)。今回はあんまり関係なかったけど…今回みたいな明確な分離が難しいタスクだといい感じになるかなぁと思ったのに…

試せなかった手法とか。

  • dependency-parseの結果を利用するとか。
  • dependencyベースのword2vecを使ってみるとか。
  • bertの公式実装をちゃんと借りてきて一部だけfreezeしての学習とか。
  • SCDV。計算の重さの割に微妙と聞きかじったので試さなかった。
  • Doc2Vec。同上。
  • T5みたいなsentence-sentence系のモデルは分類問題でどう使うかわからなくて使えなかった。

その他

  • tfa(tensorflow add-on)はなんかmetricとか色々あって便利。
  • 辞書型っぽい書き方でDataFrame作れるの知らんかった…pd.DataFrame({"id":hoge,"predict":fuga})みたいな。
  • sklearnのOneVersusRestClassifierがSVMとかロジスティック回帰を多クラス拡張するときは便利。
  • SVM系手法はpredict_probaがあんまり当てにならないので、blend方式によっては使いにくい。(Votingならいいけど)
  • データとラベルの対応関係を保ったままシャッフルするときは zip()を使って複数配列(array, list)をshuffleする-python - ろぐれこーどこちらの方法が楽で良い。

感想。

  • 生コンペでGPU前提みたいな問題設計は割と辛いと思いました。
  • でも日本語じゃなくて英語なところに優しさを感じました。
  • 期間が短い…
  • フォーラムが盛り上がっててすごいなぁと思いました。(貢献できず申し訳無さを感じている)
  • フォーラムでも議論になってましたが、publicとprivateはrandom splitなのか、時系列splitなのか、何%のデータがpublicに当てられてるのかくらいの情報は欲しいですね…

良いラブホの条件をデータサイエンスする。

TL:DR;

ラブホレビューサイト(couples様)からスクレイピングしたラブホのデータと評価を元に、良いラブホの条件を機械学習で探った。
条件を元にユーザーが何を求めているかをリバースエンジニアリング的に考えてみた(かった)

免責

解析等は至って真面目に行いましたが、これは内容の正しさを保証するものではありません。
当記事の情報により生じたいかなる不都合や損害に対しても責任を負うことは出来ませんのでご了承ください。

なんでラブホなの?

ジョーク記事だからです。
別に旅館とかでもいいんですけど、題材にジョーク感がほしかったので。

スクレイピングにあたって。

  1. 情報解析目的である。
  2. 私的利用の範囲に留め、データの再配布はしない。
  3. クロールスクリプトも公開しない。
  4. robots.txtでAllowになっていることを確認した。
  5. robots metaがないか確認したが(自分が見た限りでは)存在しなかった。
  6. ログインせずに取得できる情報をとっているので利用規約上の問題は発生しない。
  7. リクエスト感覚は2秒開けた。
  8. ページ番号を乱択で取得するようなことはしていない。

ということで今回のスクレイピングは問題がないと判断しております。

データ

今回なんやかんやで出来たデータはこんな感じです。 別にここ真面目に読む必要は無いんですが、解析編で特徴量の話する時に必要なので書いてます。

データ数:3476件

カラム

  1. rating : 0〜5の評価。今回のTarget
  2. num reviews : 付いていたレビューの数。
  3. price hoge : 宿泊価格のhoge。料金体系が複雑すぎるので、統計量にした。hogeは最大、最小、平均、中央値。
  4. can go out : 外出可能かどうか
  5. can reserve : 予約可能かどうか
  6. how check in : checkin 方法。フロントとかパネルとか
  7. pay style : 支払い方法。フロントとかエアシューターとか(エアシューターって何?)
  8. available credit : 使えるカードの種類数
  9. benefits :特典。自然言語処理めんどくて文字数にしちゃった(許して)
  10. parking : 駐車場の台数
  11. num rooms : 部屋数
  12. servicr time :サービスタイムのタイポ。サービスタイムがあるかないか。
  13. belongs to- : ホテルグループのホテルかどうか(個人経営じゃないかに近いと思っている)
  14. hotel_in_1km : 周囲1kmのホテルの件数
  15. hotel_in_10km : 周囲10kmのホテルの件数
  16. num_Nan :欠損してるカラムの数。欠損が珍しくないデータなので入れてみた。
  17. 〜いっぱい : アメニティとか施設。0は無し、1はあり。0.5は一部有りってことにした。

モデル

脳死のLightGBMです。パラメータチューニングは一応Optunaしてみたんですが、結果があまりにも変わらなかったので、最終的には初期パラメータのモデルを使っています。
objective:regression,metric:rmseです。

まず特徴量の相関係数を眺める。

お約束なのでとりあえずやっておきます。

f:id:MosaasoM:20200615122615p:plain
相関係数

まさかのサービスタイムとチェックイン方法の意味なし! バグは一応確認したので、あったとしても結構深いバグでとるのが大変なので今回はこの2つの特徴量落としてやっていこうと思います。

解析編

解析方法

ランダム分割、Fold数4のCVを行っています。
metricとしてrmseは一応計算していますが、これに振り回されてもアレなのでpredict-targetの散布図を書くことでおおよその傾向を見ることとしたいと思います。
解析のためのfeature importanceは全体のデータに対して学習したもので見ます。(CVのときのfeature importanceってどうすればいいんだろう)

解析1

f:id:MosaasoM:20200615123454p:plain
予測と実際の差

おおよそ右肩上がりの傾向はつかめていますが、ちょっとブレが大きすぎますね… この原因として

・レビュー数が少ない場合rating(Target)自体の信頼性が低い

という仮説がたてられます。実際にTargetとPredictの誤差上位のレビュー数を確認してみると…

1 1 1 0 0 1 1 0 0 2 0 0 1 1 0 1 1 0 3 3 0 0 0 0 0 1 1 1 0 0 0 0 1 1 1 1 1 2 0 0 0 1 0 1 1 1 1 0 0 0

っていう感じでした。このレビュー数はratingをつけた数ではなく、文でのレビュー数なので、0も存在します。 おおよそレビュー数が低いと評価が安定しなそうです。レビュー数のヒストグラムをとりあえず見てみましょう。

f:id:MosaasoM:20200615143552p:plain
レビュー数のヒストグラム

うーん0〜1くらいのレビュー数が圧倒的に多いですね… これを切り捨てるのは汎化性能的にどうなんだという話もありますが、Target自体が信頼出来ないとなるとそうも言ってられないので、レビュー数<2のデータを切り捨ててやってみます。

レビュー数>=2のものだけでの解析

レビュー数>=2 のデータだけでの解析結果はこんな感じ。

f:id:MosaasoM:20200615143931p:plain
レビュー数>=2での予測

悪くないですね!さっきよりはだいぶちゃんと予測できてますし、このモデルベースならある程度考察に意味がありそうです。
ということでfeature_importanceを見てみます。縦1列だと見にくいのでExcelに貼って適当にいい感じにします。

f:id:MosaasoM:20200615145258p:plain
feature importance

次に、このfeature importance上位の特徴量から、良いラブホの条件(ユーザーが求めている条件)を考察していきたいと思います。

良いラブホの条件とは?

ラブホ街のラブホはいいラブホのことが多い。

まず最上位の条件であったhotel_in_10km及び、hotel_in_1kmについて、その一次での影響を見てみたいと思います。

f:id:MosaasoM:20200615145806p:plain f:id:MosaasoM:20200615145952p:plain

横軸が、10km圏内のラブホ件数、縦軸がratingです。このグラフからまず分かることは。

  1. 所謂ラブホ街が存在することがこのグラフからもわかる。
  2. (特に1kmの方のグラフに注目すると)ラブホ街のラブホは、大外れが少ない。

ということがわかります。おそらくラブホ街では競合が多いため、淘汰や競争の結果質の良いラブホが増えるようです。

ラブホに迷ったら、もし近くにラブホ街がある場合は、そっちに行くのが良い

金額の高いラブホはハズレが少ない。

次に、重要度の高い金額とratingの相関を見てみましょう。

f:id:MosaasoM:20200615150620p:plain
値段とrating

このグラフを見ると分かる通り、金額が上がるに連れてratingの最低値が上がっていることがわかります。 つまりラブホは高いほどいい傾向が有るということです。

何を当たり前のことを…と思われるかもしれませんが、ボッタクリ居酒屋みたいな問題が度々取りざたされる夜の商売において、 ラブホは値段とクオリティの釣り合いが結構取れている。というのは重要な情報だと思います。

とはいえ、格安な1万円台でも評価の良いところはちゃんと有るので、必ずしも高いお金を払う必要があるとは言えませんが、お金に余裕があるかたは払ったほうが安全ということは言えると思います。

宿泊1万円でも良いラブホはある。 けど、お金をかければかけるほど、ハズレが無い。

ユーザーが求めている条件とは?

良いラブホとして、ラブホ街、高いラブホというのはわかったとして、ユーザーはじゃあ何にがあるから高いラブホを良いと評価しているのか、ということも知りたいですね。ということで考えていきます。

feature imoprtance上位のうち、設備やアメニティに関するものを上位から見てみると。

  1. 駐車場
  2. 部屋数
  3. クレカ対応数
  4. 水中ライト
  5. 浴室テレビ
  6. コスプレ
  7. 予約可
  8. サウナ
  9. ホームシアター
  10. インターネット
  11. カラオケ
  12. ジェットバス
  13. 電マ
  14. 加湿器
  15. DVD/BDプレーヤー
  16. WiFi
  17. TVゲーム

と続いていきます。これら設備に関しては全部丁寧に見ているとキリが無いので、ざっくりとした所感だけにしようと思います。

駐車場と部屋数はあんまり関係ない?

これらに関しては直接の影響度は少なめでした。が、なんとなくの傾向として、どちらも中程度(parking 30台、部屋数40〜50程度)の規模のホテルの満足度が高めでした。 都市部の大きめのホテルとなるとグループ化されたホテルであったりして評価が高くなるのかもしれません。

カップル利用であってもエンタメを求めている?

今回スクレイピングした元はカップルを対象としたサイトであったため、女子会利用などでのレビューは比率として少なめになると思われます。そのような条件下でも

サウナ、ホームシアター、カラオケ、TVゲーム

といったエンタメ要素が特徴量上位となるあたり、現代のカップルはまさにレジャーホテルとしての役割を求めているのかもしれません。

ちなみにSMルームの重要度は地の底でした。

みんな日にちを決めてラブホに行っている?

予約可能なホテルのほうが平均的に評価が高い傾向が少し有りました。
予約を受け付けるホテルの値段帯が高いということもありそうですが、これが影響するということは、行きずりとかその場の流れというより、パートナーと日にちを決めて利用する人がおおいのかもしれませんね。

最近のカップルはエンタメも求めてそう。女子会需要などと合わせてレジャーホテル化は益々進むかも?

終わりに

ジョークで記事書こうとしたらなんか無駄にやること多くて時間を無駄にした気分です。
とはいえラブホのデータ解析って、表立って会社とかではやりにくいと思うので、意外と新規性のある試みだったんじゃないかなーと思っています。

余裕の有る方はぜひ別視点とか別データでの解析をしてくださると面白いことが見えてくるかもしれません。

蛇足

水中ライトって何?

ABC 148 F - Playing Tag on Tree

木の問題でした。

木の問題で意外と(個人的に)見落としがちなこと…

・ どの頂点を根にとってもいい場合がある。 ・ 木の最短経路はO(|E|) = O(|V|)なので脳死で通る

あたり?他にもある気がする…

木の特殊系で考慮するのとして、

・うに(真ん中のノードから他の全てのノードに手が伸びてる)

が昔出てきた(ほかは覚えてない)

今回は、とにかく逃げるためには遠くに行くというのと木の最短経路探索を組み合わせれば解ける。

ただ、境界条件というか終わる時の条件がちょっとややこしかったのでそこらへんはちゃんと図を書かないときついかも。

とりあえず、

class tree:
    def __init__(self,n):
        self.edge = [set() for i in range(n)]
    def add_edge_dual(self,f,e):
        self.edge[f].add(e)
        self.edge[e].add(f)
    def add_edge_single(self,f,e):
        self.edge[f].add(e)
    def connected(self,node):
        return self.edge(node)


def dfs(graph,s):
    visited = [False for i in range(len(graph.edge))]
    distance = [-1 for i in range(len(visited))]
    visited[s] = True
    distance[s] = 0
    buf = [[s]]
    now = s
    while len(buf) != 0:
        nexts = buf[0]
        buf = buf[1:]
        temp = []
        for nex in nexts:
            for child in graph.edge[nex]:
                if visited[child] == False:
                    distance[child] = distance[nex]+1
                    temp.append(child)
                    visited[child] = True
                else:
                    continue
        if len(temp) == 0:
            continue
        else:
            buf.append(temp)
    return  distance

こんな感じに木とdfsを定義(後でライブラリ行き) して、

n,u,v = map(int,input().split())
t = tree(n)

for i in range(n-1):
    a,b = map(int,input().split())
    t.add_edge_dual(a-1,b-1)

taka = dfs(t,u-1)
aoki = dfs(t,v-1)

aofar = -1
for i in range(n):
    if taka[i] < aoki[i]:
        if aoki[i] > aofar:
            aofar = aoki[i]


print(aofar-1)

これ、手数に関わるのが、青木くんの到達手数なのに、高橋くんの到達手数で最も遠いところを求めてて何回かWAを出しました(ちゃんと条件を確認しよう!)

SIGNATE マイナビコンペ2019 面積-築年数について

なんか初心者特有の謎特徴量を作ったら、一体これは何を表してるんだ的な話をTwitterでちょくちょく見たので、その有効性とかを軽く検証しようと思った次第です。
"くろすばりでーしょん"とか"ゆうこうすうじ"とか気にしてないのでその程度です。

結論は何もわかりませんでしたが、とりあえず有効そうってのはなんとなく確認できました。

分布の確認

f:id:MosaasoM:20191113095535p:plain
面積と賃料の散布図
f:id:MosaasoM:20191113095557p:plain
面積-築年数と賃料の散布図

上の画像が、面積と賃料の関係、下が面積-築年数後の賃料との関係です。
雰囲気ながめる限りだと、平べったく、非線形になった気がします。

相関係数

次に相関係数を確認してみます。

f:id:MosaasoM:20191113100121p:plain

相関係数は微妙に悪化してますが、まぁそんなに気にするほどの変化じゃないと思います。

線形回帰とXGBoostで確認

そしたらそれぞれに線形回帰及び適当なXGBoost Regressor(コンペで一番メインに使用)で回帰して決定係数とかを見てみようと思います。

f:id:MosaasoM:20191113101653p:plain

この通り、XGBoostのときのみスコア、決定係数に結構改善します。非常に単純化した状況下とはいえ有効性はありそうです。

より減価償却っぽいモデル

ところで、減価償却を考えるなら、単純にひくというより、年数によって割合で減っていってほしいので、それをなんとなく実装してみます。
まぁ面積に減価償却を反映するとかいう意味わからないことをするのにはかわりませんが…
ただ、ここで減価償却と家賃が減るタイミングはおそらく同一ではない(契約の更新のタイミングで変わるはず)ということを考慮し、floor(築年数/2)をもとに減価償却を考えることとします。

モデルは上の結果からXGBoost,減価償却は2年ごとに0.1%〜2%までとって2乗誤差的に良さそうなのを拾ってくることとします。 結果としては2年毎に1.8%の償却をかけるときがいい感じでした。 つまりは

面積×(1-0.18*(築年数//2))

ということです。

その時のR2と二乗誤差がこちら

f:id:MosaasoM:20191113111302p:plain

分布はこちら f:id:MosaasoM:20191113111709p:plain

うーーん何もわからないですけど、築年数による償却みたいな影響を考慮できてスコアがちょっと伸びるってのは良い気がします。

f:id:MosaasoM:20191113112454p:plain

最後に適当に分布重ねて見ましたが、うーーん引き算したモデルは結構分布変わってる雰囲気があるので改善するのも理解できますが…

一体機械学習くんはどこを見ているんだ…