女子高生になりたい

はるみちゃんのブログだよ。えへへ。

stable baselines3+imitationで模倣学習(BC&AIRL)

stable baselines3では、模倣学習のfeatureがimitationというライブラリに移譲されることになりました。

stable-baselines3.readthedocs.io

github.com

これにより、(過渡期である事も要因であるとは思いますが)以前は非常に簡単にできていた模倣学習に一手間必要になりました。

そこで今回は、stable baselines3とimitationを使った模倣学習の実行について備忘録を残しておきたいと思います(これを書いている時点では、ドキュメントがほぼなかったので・・)


エキスパートデータの収集

言わずもかな逆強化学習ではまずエキスパートデータを用意する必要があります。 今回はatariのインベーダー環境に対して、人手で操作してエキスパートデータを生成してみたいと思います

stable baselinesの2系ではstable_baselines.gail.generate_expert_trajを使うことで簡単に軌跡の記録ができましたが、3系ではimitationのTrajectoryデータ型として記録していきます

imitation/types.py at bb363a6c1039f29d4670647734fd21de9aa2e85d · HumanCompatibleAI/imitation · GitHub

1つのエピソードに対して1つのTrajectoryオブジェクトとなり、複数エピソードではTrajectoryのArrayとして保存することになります。

以下は、実装例です

from imitation.data.types import Trajectory
from stable_baselines3.common.atari_wrappers import *
import gym
import pyglet
from pyglet.window import key
import time
import pickle

def get_key_state(win, key_handler):
    key_state = set()
    win.dispatch_events()
    for key_code, pressed in key_handler.items():
        if pressed:
            key_state.add(key_code)
    return key_state

def human_expert(_state, win, key_handler):
    key_state = get_key_state(win, key_handler)
    action = 0
    if key.SPACE in key_state:
        action = 1
    elif key.LEFT in key_state:
        action = 3
    elif key.RIGHT in key_state:
        action = 4
    time.sleep(1.0 / 30.0)
    return action

def main():
    record_episodes = 1
    ENV_ID = 'SpaceInvaders-v0'
    env = gym.make(ENV_ID)
    env.render()

    win = pyglet.window.Window(width=300, height=100, vsync=False)
    key_handler = pyglet.window.key.KeyStateHandler()
    win.push_handlers(key_handler)
    pyglet.app.platform_event_loop.start()
    while len(get_key_state(win, key_handler)) == 0:
        time.sleep(1.0 / 30.0)
    
    trajectorys = []
    for i in range(0, record_episodes):
        state = env.reset()
        actions = []
        infos = []
        observations = [state]
        while True:
            env.render()
            action = human_expert(state, win, key_handler)
            state, reward, done, info = env.step(action)
            actions.append(action)
            observations.append(state)
            infos.append(info)
            if done:
                ts = Trajectory(obs=np.array(observations), acts=np.array(actions), infos=np.array(infos))
                trajectorys.append(ts)
                break
    with open("invader_expert.pickle", mode="wb") as f:
        pickle.dump(trajectorys, f)
if __name__ == '__main__':
    main()

キー入力を受け取る部分の実装はnpakaさんの以下の記事を参考にさせて頂きました

note.com

インベーダーが下手すぎてエキスパートとは程遠いデータを収集することができました。

BCによる学習

こちらはimitationのquickstartと同様です。

https://github.com/HumanCompatibleAI/imitation/blob/master/examples/quickstart.py

with open("invader_expert.pickle", "rb") as f:
    trajectories = pickle.load(f)
transitions = rollout.flatten_trajectories(trajectories)

ENV_ID = 'SpaceInvaders-v0'
venv = util.make_vec_env(ENV_ID, n_envs=2)
logger.configure(".BC/")
bc_trainer = bc.BC(venv.observation_space, venv.action_space, expert_data=transitions)
bc_trainer.train(n_epochs=100)
bc_trainer.save_policy('space_invader_policy_v0')

保存したpolicyは以下のようにロードができます

bc_trainer = bc.reconstruct_policy("space_invader_policy_v0")

BC→順強化学習とモデルの実行

まず、BCの学習により得たポリシーを使ってインベーダーをプレイさせてみます。 こちらはポリシーのpredictを使って簡単に実行が可能です。

def main():
    ENV_ID = 'SpaceInvaders-v0'
    env = gym.make(ENV_ID)
    bc_trainer = bc.reconstruct_policy("space_invader_policy_v0")
    state = env.reset()
    while True:
        env.render()
        action = bc_trainer.predict(state)
        state, reward, done, info = env.step(action)
        if done:
            break
if __name__ == '__main__':
    main()

次に、このポリシーをベースに順強化学習をさせることも可能です。

エキスパートのデータを渡して、初期の探索を手助けしてあげた後、順強化学習による最適化を進めたい・・というのが直感的なニーズですが、良いのか悪いのかは正直よくわかりません。

こちらは、単純に強化学習モデルクラスの第一引数であるPolicyに先ほどのものを入れて初期化→再学習すればよさそうなんですが、素直に入れると動きません。

そこで、ハック的なテクニックですが以下のようにする必要があります。

class CopyPolicy(ActorCriticPolicy):
    def __new__(cls, *args, **kwargs):
        return bc_trainer.policy

model = sb3.PPO(CopyPolicy, venv, verbose=0)
model.learn(total_timesteps=128000, callback=callback)

このアプローチは以下のissueで言及されています

github.com

AIRLによる学習

こちらも、imitationのquickstartに記載されている通りで大丈夫です

https://github.com/HumanCompatibleAI/imitation/blob/master/examples/quickstart.py

最後に.gen_algo.saveでPPOモデルを保存します。 ゲームの実行にはこのモデルが必要になります。

venv = util.make_vec_env(ENV_ID, n_envs=2)
logger.configure(".AIRL/")
airl_trainer = adversarial.AIRL(
    venv,
    expert_data=transitions,
    expert_batch_size=32,
    gen_algo=sb3.PPO("MlpPolicy", venv, verbose=1, n_steps=1024),
)
airl_trainer.train(total_timesteps=2048)
airl_trainer.gen_algo.save("airl_trainer_gen_algo")

モデルの実行

PPO.loadで先ほど保存したモデルをロードしてきます。 後は特に違いはありません。

def main():
    ENV_ID = 'SpaceInvaders-v0'
    env = gym.make(ENV_ID)
    model = PPO.load("airl_trainer_gen_algo")

    state = env.reset()
    while True:
        env.render()
        action = model.predict(state)
        state, reward, done, info = env.step(action)
        if done:
            break
if __name__ == '__main__':
    main()

【TextMeshPro】入力文字列をTMP_InputField経由でなくTextMeshProUGUIを直接参照して取得してしまったばかりにゼロ幅スペースで苦しんだ記録

些細な話ですけど、ハマる人いると思うんで、備忘録として置いておきます

結論

TextMeshPro用のInputFieldであるTMP_InputFieldを使うときは、子GameObjectのTextMeshProUGUIを参照して入力された文字を取得してきてはいけない!ゼロ幅スペースが入ったりする。

正しくは、TMP_InputFieldのtextプロパティからとってくる。これだとゼロ幅スペースは入っていない。


以下、詳しい話

いきさつ

ZZ3Dというアプリ( https://apps.apple.com/jp/app/zz3d/id1475406292)にVRoidHub連携機能を導入しようとSDKを組み込んでいました。

連携には、認可コードをアプリ側から入力するフローがあるんですが、正しいはずの値を入力してもリジェクトされてしまう状況でした。

ひとしきり問題がありそうなところはデバッグしましたが、問題が見当たらず困っていました。

そこで、SDKとセットでついてきたExampleシーンを動かしたところ、こちらではうまく動いてしまいました。 つまり、向こうの認可サーバの問題やSDKのバグではないということがわかります。

となると、やっぱりこちらのソースコードが間違っているのかな・・?と思うわけなんですが、SDKの利用部分ではサンプル実装と全然違いがないんですね。困りました( ;∀;)

と、いうことがありまして、最後に、「ログに流れる文字列を見る限りでは認可コードは正しいように見えるけど、バイナリレベルで違っているんじゃないか?」ということを疑いました。

(結果、それが正解だったみたいです)

実際に確認

InputFieldに入力した文字列を取得してきたものと、正しい認可コードの文字列をバイナリエディタで比較してきました。

(InputField経由)
f:id:sakata_harumi:20201205233251p:plain (ただしいもの)
f:id:sakata_harumi:20201205233249p:plain

はい。「E2 80 8B」これが余計にくっついています( ;∀;) (0D 0Aは改行コードですが、これは2つの文字列を上下に並べたため挿入されたものですので関係ありません)

このE2 80 8B、どうやら「ゼロ幅スペース」というやつらしいです。

ja.wikipedia.org

くううううううううう!どこから生えてきたんだ!( ;∀;)

起きてしまった理由

結論にも書いたのですが「TMP_InputField経由でなくTextMeshProUGUIを直接参照して入力文字列を取得」したせいでした。

TMP_InputFieldのPrefabは以下ような構成になっています。

f:id:sakata_harumi:20201205235618p:plain

ここの「TextArea/Text」に入力された文字が入っている・・ように見えたので、ここから文字列を参照していました。

しかし、実際はそうではなく、ここには視覚化のためのテキストが入ってる、、とのことで実際に入力されたものとは差があるらしいです。

例えば、パスワード形式のInputFieldにした場合、ここには「***」という文字列が入ってくるらしいです。

forum.unity.com

そこで、入力された値を正しく取得するためにはTMP_InputFieldのtextプロパティから取得しないといけない、ということでした。

みなさんも気を付けてください( ;∀;)

ゲームを作った。一人は大変だ。だからディスコードサーバーをつくった。

ゲームを作ったよ('ω')

つくったゲームはこちら。

www.freem.ne.jp

ここ三か月くらい、平日の夜の時間などのスキマ時間でこつこつ作ってました。

クトゥルフっぽい世界観を目指した弾幕シューティングです。

30分くらいで終わるのでカジュアルに遊べます。

よかったら遊んでね!

一人で作りきるのは厳しい( ;∀;)

色々理由はありますが、「スキル不足で素材が作れない」「モチベーション維持が難しい」「かけられる時間の総量に限界がある」の三点が主でしょうか。

特にスキル不足が厳しい。 自分はUnity素人ですが、実装ならある程度何とでもなる・・気がしています。

問題が、素材まわり。

ほんとにセンスがないので、どうしよう・・という感じ。次はもっとがんばるぞ( ;∀;)

たまに一人で実装、デザイン、音楽すべてやってる人がいますがその人は超人です。超人になりたい・・。

なので、ディスコードコミュニティを作った(*ノωノ)

と、いうわけで、孤独なすべてのクリエイターのためのディスコードサーバーを作りました!

サーバーの目的(例)

  • 孤独なクリエイター同士で慣れ合う。進捗報告等してモチベをあげる。
  • 一緒にものづくりをする仲間を募集する
  • わからないところを質問、回答しあう
  • 通話をつないでもくもく作業をする
  • みんなで桃鉄する

同人サークルじゃないので、活動の強制はありません。まったりと自分のペースで創作に励むためのコミュニティです。

気が合う仲間がいれば、ぜひいっしょに何か作りましょう・・!

参加はディスボードから!

入ってください・・・。お願いします・・・。

disboard.org

それでも孤独に開発を続ける( ^)o(^ )

そもそも創作とは孤独なものなんだ!!!!!!!

引き続き、がんばっていきます( ;∀;)

レーダースキャンしてるっぽいシェーダーを作りたかった(失敗)

やりたいことができてないんだけど、未来への糧のために記録として残しておきます

今後は失敗の記録もしていく所存・・!

やりたかったこと

以前の記事で、遮蔽にキャラクターが隠れていても見えるようなレンダラを作成しました

sakataharumi.hatenablog.jp

ただ、上の記事の最後をみてもらうとわかるんですがOverrideしたマテリアルが微妙で、スキャンしてる感がありません・・。

そこで、スキャンしてる感があるシェーダーを作りたいな、と。

具体的には、「アウトラインが強調されて、アウトラインの中は適当な色で、ぐわんぐわんスキャンされてる感じ」をイメージしていました(語彙力がない)

できたもの(失敗)

youtu.be

スキャンしてるっぽい表現には成功していますが「アウトラインの表示」がうまくいってません。

ちなみに、シンプルなテクスチャであればよい感じにアウトラインだけが表示されて、スキャンされているっぽくなります。

youtu.be

試したアプローチ方法

今回作成したShaderGraphの全容を掲載します。

f:id:sakata_harumi:20201123203028p:plain

スキャンエフェクト部(赤の部分)とアウトライン抽出部(青の部分)から成ります。



スキャンエフェクト部の実装はシンプルで、Photoshopでグラデーションの画像を用意してUVアニメーションさせて、それをアルファにセットしてるだけです。

アウトライン抽出部ではCustomFunctionを利用しています。こちらを利用するとHLSLで書かれた関数を実行することができます。

docs.unity3d.com

今回は、微分フィルタによるエッジ抽出で、アウトラインを表示させるというアプローチを試しました。 微分フィルタには代表的なソーベルフィルタを採用しました。

ソーベルフィルタでは、縦用と横用で別のカーネルを用います。カーネルの具体的な値は以下のwikipediaを参照してみてください。

Sobel operator - Wikipedia

ソーベルフィルタの実装はこちらのブログを参考にさせて頂きました。

pond-comfat.hatenablog.com

今回実装したCustomFunction(sobel.hlsl)

#ifndef MYHLSLINCLUDE_INCLUDED
#define MYHLSLINCLUDE_INCLUDED

void sobel_float(float2 uv, Texture2D tex, SamplerState s, out float3 Out)
{
    float2 delta = float2(0.01, 0.01);

    float2 uv0 = uv + delta;
    float2 uv1 = uv + float2(0, -delta.y);
    float2 uv2 = uv + float2(delta.x, -delta.y);
    float2 uv3 = uv + float2(-delta.x, 0);
    float2 uv4 = uv + float2(0, 0);
    float2 uv5 = uv + float2(delta.x, 0);
    float2 uv6 = uv + float2(-delta.x, delta.y);
    float2 uv7 = uv + float2(0, delta.y);
    float2 uv8 = uv + float2(delta.x, delta.y);

    float3 col0 = tex.Sample(s, uv0);
    float3 col1 = tex.Sample(s, uv1);
    float3 col2 = tex.Sample(s, uv2);
    float3 col3 = tex.Sample(s, uv3);
    float3 col4 = tex.Sample(s, uv4);
    float3 col5 = tex.Sample(s, uv5);
    float3 col6 = tex.Sample(s, uv6);
    float3 col7 = tex.Sample(s, uv7);
    float3 col8 = tex.Sample(s, uv8);

    float cgx = col0 * -1 + col2 * 1 + col3 * -2 + col5 * 2 + col6 * -1 + col8 * 1;
    float cgy = col0 * -1 + col1 * -2 + col2 * -1 + col6 * 1 + col7 * 2 + col8 * 1;
    float cg = sqrt(cgx * cgx + cgy * cgy);
    Out = float3(1, 0, 0) * (cg - 0.2);
}
#endif

やっていることは、wikipediaに書かれていたソーベルフィルタのカーネルを着目するUV座標を軸として畳み込んでいるだけです。

縦のエッジと横のエッジでそれぞれ別のカーネルになっているので、それぞれについて計算した上で合成する必要があることにだけ気を付けないといけません。


最後に、スキャン部とアウトライン部をMultiply Nodeで合成してAlphaに設定してあげれば完成です。(完成じゃないんだけど)

おわりに

複雑なテクスチャに対して微分フィルタでアウトライン表示させてみるというアプローチは多分うまくいかないことがわかりました。

別の方法を探りたい。。。誰か何か良い方法を知ってたら教えてください( ;∀;)

Rendererを修正して遮蔽に隠れていてもオブジェクトが見える表現をする (by UniversalRenderPipeline)

遮蔽に隠れてもオブジェクトが見えるあの表現がしたい!と思ったのでやり方をまとめます。 (オクルージョン処理させない、みたいな表現が適切なんでしょうか)


具体的にどんな表現かといいますと、Apexのブラッドハウンドの戦術アビリティのような所謂"ウォールハック"の表現です。

youtu.be

このような表現が、ゲームでは多々見られます。

これまでこのような表現をするためにはShaderを自分で書いていたかと思います。

しかし、SRPの登場により、レンダーパイプラインを修正することで実現可能になりました。 (もしかしたら昔からShaderなしで実現できたのかもしれませんが・・Unity素人なのでその辺りはわかりません・・)

実装方法

※URP環境下での例になります

シチュエーション

適当にそれっぽいマップを用意します

f:id:sakata_harumi:20201122163958p:plain

コンテナの後ろには兵士が隠れています。

この兵士コンテナ越しでも見えるようにしていきましょう。

f:id:sakata_harumi:20201122164538p:plain

手順

(1) Layerを追加し、モデルのレイヤーを変更する

透過対象のオブジェクト(今回だと兵士のモデル)に設定するレイヤーを追加しておきます。

今回は「WallHackTarget」レイヤーを追加してみました。

f:id:sakata_harumi:20201122171048p:plain

兵士用のモデルのレイヤーをこのレイヤーに変更しておきます

(2)RendererのOpaque Layer Maskの設定を修正

URPに設定されているRendererの設定を修正していきます。 (デフォルトだと"UniversalRenderPipelineAsset_Renderer")

まずは"Filtering"→"Opaque Layer Mask"を選択し、WallHackTargerレイヤーのチェックを外します。

Opaque Layer Maskの名前の通り、ここでチェックがついているレイヤーのみ不透明になりますので、チェックを外したことによってこのレイヤーが透明になります。

よって、兵士がカメラに映らなくなりました。

f:id:sakata_harumi:20201122171841p:plain

(3)RendererのFeatureを追加していく

Renderの最下部にある"Add Renderer Feature"から、設定を追加していきます。

1つ目は、遮蔽越しに見えるようにするための設定です。以下のように設定します。

f:id:sakata_harumi:20201122172626p:plain

"After Rendering Opaques" つまり、不透明オブジェクトのレンダリングが終了した後に適用される処理になります。

Filtersも特に言及することないのですが、見た通りWallHackTargetレイヤーを不透明にしています。

このままでは、普通にモデルが表示されるだけですし、遮蔽にも隠れてしまいます。

ここで、Overridesの設定をしていきます。 ポイントは"Depth"の"Depth Test = Greater"これだけになります。

DepthTestとは、深度バッファの情報を比較してオクルージョン処理をするための設定になります。

この値によってDepthTestの実行方法を変えることができます。

デフォルトでは、ある物体の手前にあるピクセルは描画しないようにするため"LessEqual"(既に描画されてるオブジェクトと距離が等しいか、それより近い場合に描画)に設定されています。

しかし、WallHackレイヤーに限っては"Greater"(既に描画されているオブジェクトより距離が遠い場合に描画)に変更します。

こうすることで、手前に何かオブジェクトがあったとしても描画されるようになりました。ウォールハックができています。

※overrideするmaterialはよしなに設定してください

f:id:sakata_harumi:20201122175008p:plain

しかし、この設定だけだと当然DepthTestがGreaterの条件にマッチするときしか描画されないので、逆に手前に何もオブジェクトがない状態では表示されないままです。

f:id:sakata_harumi:20201122175153p:plain

そこで、最後にもう1つだけ以下のようなRendererの設定を追加します。

f:id:sakata_harumi:20201122175317p:plain

上記の設定で、WallHackTargetレイヤーを不透明にするようにしておきます。

Overrideの設定は何もしなくて良いです。DepthTestはデフォルトで"LessEqual"になっていますので。

これで、遮蔽がないときはふつうに表示され、遮蔽があるときは壁越しに表示される表現が完成しました!
f:id:sakata_harumi:20201122175543p:plain f:id:sakata_harumi:20201122180638p:plain

おわりに

使用するRendererはRendererListに複数登録できますので、通常時に使うRendererとウォールハックRendererを特殊スキルを使用時などの条件で切り替えることによって、無事ゲームに組み込むことができそうです! めでたい!

変位指定パラメータ「非変・共変・反変」と反変のつかいどころ

おはぽえ〜〜

先日、私の通っている女子高の同級生との会話で、Scalaの変位指定パラメータの話題になりまして。(イマドキのJKはScalaが流行中!)

変位指定パラメータって、どういうものかの解説はたくさんあるんだけど、どういう時に使うか?みたいな話が全然ないよね、という話をしてました(特に反変)
なので、そのあたりの自分の解釈をまとめられたらな〜と思います

非変

型パラメータに変位指定アノテーションを指定しないと、非変になります。


例として、燃えるゴミと燃えないゴミがある世界を考えます。

trait Garbage {
  def name: String
}
trait Burnable extends Garbage
trait Incombustible extends Garbage

で、ゴミ箱の定義があります。

trait Trash[A] {
  def put(a: A): Unit
}

ゴミ箱を作りましょう。 燃えるゴミは燃やして、燃えないゴミは潰します。

implicit val trashForBurnable = new Trash[Burnable] {
  override def put(a: Burnable): Unit = println(s"burn! ${a.name}")
}

implicit val trashForIncombustible = new Trash[Incombustible] {
  override def put(a: Incombustible): Unit = println(s"crush! ${a.name}")
}

ゴミ箱を作ったので、ゴミも作っておきます。 燃えるゴミは紙で、燃えないゴミは缶にしときます。

val paper = new Burnable {
  override def name: String = "paper"
}
val can = new Incombustible {
  override def name: String = "can"
}

最後に、ゴミを渡すとゴミ箱に入れる関数を作ってみます

def throwAway(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
    trash.put(a)

def throwAway(a: Incombustible)(implicit trash: Trash[Incombustible]): Unit =
    trash.put(a)

これで、ゴミを捨てることができるようになりました!やったーー!

throwAway(paper) //burn! paper
throwAway(can) //crush! can

ここまでくると、throwAwayが2つあるので、抽象化して1つにまとめたい気がしてきます。

BurnableIncombustibleGarbageのサブタイプなので、以下のように書ける気もします。

def throwAway(a: Garbage)(implicit trash: Trash[Garbage]): Unit = trash.put(a)

しかし、これはTrash[Garbage]インスタンスが存在しないということで、使えません。

throwAway(paper) //Error! could not find implicit value for parameter trash: Trash[Garbage]

これは、BurnableGarbageのサブタイプですが、Trash[Garbage]Trash[Burnable]のサブタイプではないことを意味しています。

この関係が「非変」の関係です。Scalaでは変位指定パラメータを指定しないと、非変となります。

共変

ScalaのListやOptionの定義には共変の変位指定パラメータが指定されています

trait List[+A]
trait Option[+A]

共変パラメータを指定すると、BがAのサブタイプだったとき、F[B]F[A]のサブタイプとなります。

var garbage = List.empty[Garbage]
val burnables = List(paper, paper, paper)
garbage = burnables

上記のように、Listが共変なのでList[Burnable]List[Garbage]を代入することが可能になります。

ちなみにjavaArrayListは共変じゃないので、このようなことはできません。

var garbage = new java.util.ArrayList[Garbage]()
val burnables =
  new java.util.ArrayList[Burnable](java.util.Arrays.asList(paper, paper, paper))
garbage = burnables //Error! type miss match.


と、いうことで、先程の例に戻ります。

def throwAway(a: Garbage)(implicit trash: Trash[Garbage]): Unit = trash.put(a)

この関数を使うためには、Trash[Garbage]インスタンスが要求されました。
そこで、Trashに共変パラメータを付与し、Trash[Burnable]がサブタイプになるようにしてやればTrash[Garbage]を作らなくても良いのでは?という話になります。
理屈上はそのとおりですが、今度は別の問題が発生します。

trait Trash[+A] {
  def put(a: A): Unit //Error! Covariant type A occurs in contravariant position in type A of value a
}

Covariant type A occurs in contravariant position in type A of value aと怒られてしまいました。
そう、共変の型パラメータをメソッドの引数に使うことはできないんです・・・

なぜ共変のパラメータを引数に使えないの?

まず、Tashに共変パラメータを指定すると以下のような定義が可能になります。

implicit val trashForBurnable: Trash[Garbage] = new Trash[Burnable] {...}

で、以下のような関数も、当然定義ができます。

この定義自体は問題ないようにみえます。

def throwAway(a: Garbage)(implicit trash: Trash[Garbage]): Unit = trash.put(a)


が・・しかし、上記の例だと、この関数にTrash[Garbage]の実態としてサブタイプのTrash[Burnable]が渡ってくるわけです。

にも関わらず、第一引数のa: Garbageには当然、GarbageやサブタイプであるIncombustibleを渡せることになってしまいます。

この時、Trashの実態はTrash[Burnable]なので、Incombustibleのゴミを入れられても、燃えないゴミの処理方法はわからないので困ってしまいます!

・・ということで、引数に共変の型パラメータを指定することはできません。

一方、戻り値の型であることは問題ないので、こちらは共変パラメータを利用可能です。

反変

反変は、BがAのサブタイプだったときにF[A]F[B]のサブタイプになります

関係が逆転するんですね。

つまり、Trashに反変パラメータを指定してやると、Trash[Garbage]Trash[Burnable]のサブタイプになります。

trait Trash[-A] {
  def put(a: A): Unit
}

こうしておくと、以下のような関数があったときに、Trash[Garbage]インスタンスもこの関数に渡すことができるようになります

def throwAway(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
  trash.put(a)

以下はこれまで通り、もちろん問題なく動作します。

implicit val trashForBurnable: Trash[Burnable] = new Trash[Burnable] {
  override def put(a: Burnable): Unit = println(s"burn! ${a.name}")
}
throwAway(paper)

反変指定により、Trash[Garbage]Trash[Burnable]のサブタイプとみなせるようになりましたので、以下が動くようになります。

//  implicit val trashForBurnable: Trash[Burnable] = new Trash[Burnable] {
//    override def put(a: Burnable): Unit = println(s"burn! ${a.name}")
//  }
implicit val trashForAnything: Trash[Garbage] = new Trash[Garbage] {
  override def put(a: Garbage): Unit = println(s"eat! ${a.name}")
}
throwAway(paper) // eat! paper

で、これ何に使うの?

一言でいうと、上記の例ような「燃えるゴミ箱か、何でも入れていいゴミ箱どっちかに入れてね」という気持ちを表現するのに使えます。
もし、これが反変じゃなく共変の形、つまりTrash[Burnable]Trash[Garbage]のサブタイプだった時の世界と比較すると、良さがわかります。
この前提の場合、「燃えるゴミ箱か、何でもいいゴミ箱どっちかに入れてね」という気持ちを表現することができなくなります。

def これは燃えるゴミを捨てる関数(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
  trash.put(a)


上記の関数には、燃えるゴミ用のゴミ箱が渡ってくるかもしれませんし、何でも入れていいゴミ箱が渡ってくるかもしれませんし、燃えないゴミ用のゴミ箱が渡ってくるかもしれません・・つまり、型で気持ちを伝えることができません。
反変を使うと、この気持ちが表現可能になります。

def throwAway(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
  trash.put(a)


この他、よく例に挙げられるのがオブジェクトの文字列表現を得るTraitなどでしょうか。

trait Printable[A] {
  def format(value: A): String
}
def print[A](input: A)(implicit p: Printable[A]): Unit =
  println(p.format(input))


上記のtraitは以下のように使うことができます。

implicit val intPrintable = new Printable[Int] {
  def format(input: Int) = s"number: ${input.toString}"
}
print(1) //number: 1


しかし、AのPrintableが提供されていない場合は、当然、printを使うことができなくなります

print(List(1,2,3)) //Error! no implicit parameter


これでは不便です。特別なフォーマットで表示は不可能でも、汎用的な表現でprintできるようにしたいですね。
そこで、Printableを反変にします。

trait Printable[-A] {
  def format(value: A): String
}


そして、AnyのPrintableを作っておきます。

implicit val anythingPrintable = new Printable[Any] {
  def format(input: Any) = input.toString
}


Scalaでは全てのオブジェクトはAnyのサブタイプなので、これで全ての型でPrintableが使用できるようになります。

print(List(1,2,3)) //List(1, 2, 3)



と、いうわけでScalaの変位指定についてまとめてみました。
反変についての理解はもうちょっと深めたいですね〜また何かわかったら加筆していきたいと思います

cats.effectのIOモナドとcontextshiftを丁寧に理解する

並行処理と並列処理

並行処理(concurrent processing)

1つのCPU(コア)スレッドを切り替えながら複数のタスクを実行すること。

人間の目には複数のタスクが同時に進んでいるように見える

実行するスレッドを切り替える必要があるのでコンテキストスイッチのコストがかかることに留意(=スレッドをたくさん作って、それらを並行処理するとコンテキストスイッチによる無駄な時間が多くなりパフォーマンスが悪化する)

並行処理はCPUのアイドル時間をなくすために用いる。
以下のようにCPUバウンドな処理とIOバウンドな処理を繰り返す例を思い浮かべると効果がわかりやすい

TASK_A: CPU処理ー>IO処理ー>CPU処理ー>IO処

TASK_B: IO処理ー>CPU処理ー>IO処理ー>CPU処理

並列処理(multiple processing)

複数のコアを使って同時に複数のタスクを実行すること。

ネイティブスレッドとグリーンスレッド

ネイティブスレッド

wiki ( https://ja.wikipedia.org/wiki/スレッド_(コンピュータ)) を引用

ライトウェイトプロセス(light-weight process、LWP)または軽量プロセスとは、
スレッドを複数並行して実行するためのカーネル内の機構。
マルチプロセッシングにおいて、ひとつのプロセス内のスレッドを複数個同時に実行する仕組みである。
カーネルスレッドとLWPを総称してネイティブスレッドと呼ぶこともある。

カーネルレベルで実現されるスレッド。

コンテキストスイッチのコストはプロセス並みに大きい(マイクロ秒くらいらしい)

ネイティブスレッドはOSのスケジューラーによって資源割当を行うため、並列処理が可能

グリーンスレッド

ユーザ空間(ユーザープロセスの動作するアドレス空間)で実装されたスレッド機構のうち、特にVM上で動くものがグリーンスレッド。

OSのスケジューラーが関与しないため、マルチコア対応が不可能。 コンテキストスイッチのコストが低い(ナノ秒くらいらしい)ため1コアに対して大量のスレッド必要な場合などに有用(ref: C10K問題)

多くの言語でネイティブスレッドが標準で採用されているが、RubyPythonはマルチコアの恩恵を受けにくいのと、Goはネイティブスレッドと軽量スレッドのハイブリッドパターンになっている。

Ruby・・GVLの機構により同時に実行されるネイティブスレッドは基本は1つのみ(ただし、ネットワークIO、ディスクIO等のAPIはその限りではないらしい) ※GVL(Giant VM lock)=GIL(グローバルインタプリタロック)

Python・・GILあり

Go・・ネイティブスレッド&軽量スレッド(goroutine)

ゴルーチンのスケジューラーが「GOMAXPROCS」の数だけ生成したネイティブスレッドに対して軽量スレッドをいい感じにスケジュールしてくれるらしい

Go1.5以前はスケジューラが未熟でGOMAXPROCS=1以外だとパフォーマンス劣化したりしたらしいが、今は改善されているっぽい

(補足)

  • PythonRubyはスレッドセーフでないC言語のライブラリを使って作られているのでGILの機構が必要
  • RubyPython x 並列とかでググると、並列処理にスレッドを使えという記事が散見されるが、これらは誤り。マルチプロセスでやらないといけない。

(Scala)Future x ExecutionContextによる並列処理

cats.effect.IOの話の前に。

Scalaでは scala.concurrent.Future というAPIが提供されており、これにより並列処理を実現できる。

Futureを生成すると、内部的にはJavaのRunnableに変換される。 このRunnableをExecutionContextが良い感じにスレッドに割り当ててくれる。 スレッドは先述した通り、OSのスケジューラーにより均等に割り当てられ、実行されていく。

※つまり、Futureはスレッドを新しく作るというわけではないことに注意

よく「Futureでブロッキングが発生するをするな」という話があるが、これはどういうことか。

まず、Futureを実行するときにExecutionContextを求められるので脳死import scala.concurrent.ExecutionContext.Implicits.global をすることがあると思う。

globalで提供されるExecutionContextは標準で「利用可能なコア数の同数のスレッド」を持つ。

このExecutionContext上でブロッキングする処理を入れてしまうと、単純に1スレッドが無駄に専有されてしまう。
この状況下ではCPUが処理をしていないのにも関わらず、スレッドに別の仕事をさせることができなくなり、パフォーマンスが劣化する。

では、ブロッキング含む処理をしたいときはどうしたら良いか。 ブロッキング処理専用のExecutionContext(新しいスレッド)を作り、そこにFutureを割り当ててる。そうすると、globalのECを汚染することはない

ただ、OSのスケジューラーはスレッドに対して均等に処理時間を割り当てるのでスレッドを増やしすぎてもコンテキストスイッチのコストが上がってしまう。

(スレッドの優先度は一応設定できるものの、実際どの程度スケジューリングに影響されるかはわからない。システムコールによりネイティブスレッドの優先度を変更するため、OSによって挙動は異なる) https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Thread.html#setPriority-int-

IOモナドとコンテキストシフト

これから、cats.effect.IOをみていく。

IOとFutureの違い

そもそもFutureと何が違うのか。これは、catsのドキュメントがわかりやすい。

f:id:sakata_harumi:20200830225414p:plain

(引用: https://typelevel.org/cats-effect/datatypes/io.html

IOモナドはFutureと違い純粋。評価時に正確に1つの結果が得られる。 コードで挙動の違いをみていく。

val f = Future {
  println(s"future_println")
}
val i = IO {
  println("io_println")
}
println("====START=====")
Await.result(f, Duration.Inf)
Await.result(f, Duration.Inf)
i.unsafeRunSync
i.unsafeRunSync
future_println
====START=====
io_println
io_println

Futureは値を生成した時に評価され、その後は改めて評価されることはない。つまり、値の作成に副作用が存在してしまう。

一方IOは unsafeRunSync のタイミングで評価されていることがわかる。つまり、値の生成時点でIOは副作用を産まず、参照透過に扱うことができる。

IOモナドを使ったスレッドのスケジューリング

IO.shift を使うと計算を別のスレッドプールにシフトしたり、同じスレッドプールに再度スケジュールすることができる

import java.util.concurrent.Executors
import cats.effect.{ContextShift, Fiber, IO}
import scala.concurrent.ExecutionContext

val ecOne = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())
val ecTwo = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())

val csOne: ContextShift[IO] = IO.contextShift(ecOne)
val csTwo: ContextShift[IO] = IO.contextShift(ecTwo)

def infiniteIO(id: Int)(cs: ContextShift[IO]): IO[Fiber[IO, Unit]] = {
  def repeat: IO[Unit] = IO(println(id)).flatMap(_ => repeat)

  repeat.start(cs)
}
val prog =
  for {
    _ <- infiniteIO(1)(csOne)
    _ <- infiniteIO(11)(csOne)
  } yield ()

prog.unsafeRunSync()

(引用: https://typelevel.org/cats-effect/concurrency/basics.html#thread-scheduling )

上記の例では、スレッドが1つのExecutionContextを生成し、これを用いてIOの実行を割当ている。 infiniteIO(1)infiniteIO(11) が同じECを使ってどちらも同じスレッドにタスクを割り当てようとする。しかし、前者の infiniteIO(1)のタスクが完了することはないため、スレッドが専有された状態になるため、後者は実行されることはない。

val prog =
  for {
    _ <- infiniteIO(1)(csOne)
        _ <- infiniteIO(11)(csOne)
    _ <- infiniteIO(2)(csTwo)
        _ <- infiniteIO(22)(csTwo)
  } yield ()

prog.unsafeRunSync()

(引用: https://typelevel.org/cats-effect/concurrency/basics.html#thread-scheduling )

これだと12が動くようになる。それぞれのスレッドは専有されているが、OSのスケジューラーは平等に実行時間を割当てる。

ただし、各々のスレッドは開放されることがないので 1122 が動くことはない

IO.shiftを利用して、スレッドに再スケジュールしてやることで、11,22も動かすことができる

def infiniteIO(id: Int)(implicit cs: ContextShift[IO]): IO[Fiber[IO, Unit]] = {
  def repeat: IO[Unit] = IO(println(id)).flatMap(_ => IO.shift *> repeat)
  repeat.start
}

(引用: https://typelevel.org/cats-effect/concurrency/basics.html#thread-scheduling )

repeatが実行されると IO(println(id))が実行された後に「 IO.shift を挟んでから」再度repeatをコールするような形に変更されている。

これにより IO.shiftのタイミングでタスクの再割当てが起こり、別のタスクがスケジューリングされる余地が産まれる。

(今回は同じECに再割当てを行っているが、別のECに割当てることも勿論可能)