LoginSignup
27
35

More than 5 years have passed since last update.

[ゲームAI] ファジー理論

Last updated at Posted at 2017-07-25

概要

ファジー理論自体はAIだけのものではありませんが、ゲームAIとも相性がよく、知っておくと色々と応用が効きそうなので、理解したことをまとめておこうと思います。

ファジー理論とは、Wikipediaから引用すると以下のように説明されています。

ファジィ論理(ファジィろんり、英: Fuzzy logic)は、1965年、カリフォルニア大学バークレー校のロトフィ・ザデーが生み出したファジィ集合から派生した多値論理の一種で、真理値が0から1までの範囲の値をとり、古典論理のように「真」と「偽」という2つの値に限定されないことが特徴である。さらにlinguistic variablesは、「ちょっと暑い」というような、言語学的(linguistic)な(と、ファジィの研究者は表現する)ものを表す変数(variables)である(その内容自体は、「気温が摂氏30度の時は 0.2(30度は「ちょっと」ではないから)」「気温が摂氏25度の時は 0.8」「気温が摂氏20度の時は 0.3」といったように、至って定量的なものであり、「言語学的な値」という何かよくわからないフワフワしたものを扱ってくれる魔法ではない)。ファジィ論理は制御理論(ファジィ制御)から人工知能まで様々な分野に応用されている。

ざっくりと言うと、自然言語で会話する際に出てくる「ちょっと」「少し」「とても」などのような曖昧な表現を、システマチックに「0か1」で表すのではなく、それぞれに応じた値を算出、使用する、というものです。

さらに具体例も引用しておきます。

ファジィ論理の真理値の具体例

ファジィ論理は洗濯機や冷蔵庫のような家電機器の制御に使われる。例えば洗濯機では、洗濯物の量や洗剤の濃度を調べて、洗濯槽の回転などを調整する。
基本的な応用の特徴として、連続値をいくつかの区分に分ける点が挙げられる。例えば、アンチロック・ブレーキ・システムでは温度を測定するが、温度をいくつかの区分に分け、それぞれにメンバシップ関数を定義し、ブレーキを適切に制御する。各関数は同じ温度に0から1までの真理値を割り当てる。これらの真理値を使って、ブレーキをどう制御すべきかを決定する。

[出典: Wikipedia]

なんとなくイメージ湧いたでしょうか。
上の図は、「cold」とする温度、「warm」とする温度、そして「hot」とする温度の3種類の範囲があり、「現在の温度」から、それぞれの度合いがいかほどか、を求めることによって「少しあたたかい」などを表現できるようにします。

ファジー集合とメンバーシップ関数

さて、図を見てなんとなくイメージが湧いてきたかと思いますが、これをどう定義し、どう使うのか。
今回の話は「ゲームAI」という話なので、ゲームAIとして「プログラム化できる」ことを想定しつつ書いていきたいと思います。

まず理解しないとならないこととして、「ファジー集合」と「メンバーシップ関数」のふたつがあります。
ファジー集合とは、上の図で言うと「cold」「warm」「hot」の3つ。複数あるので集合ですね。
そして、それぞれの要素に対して、与えられた内容がそれぞれにどれくらい帰属するのかを示す(計算する)のがメンバーシップ関数です。

メンバーシップ関数の主な役割は、入力された実数(クリスプ数)をメンバーシップ度(0~1)にマッピングすることです。

(正確ではないですが)イメージをプログラム化すると以下のような感じです。

float temperature = 30.5f;
float cold = Cold(temperature);
float warm = Warm(temperature);
float hot = Hot(temperature);

temperature(温度)を判定したい場合、それぞれのメンバーシップ(度合い)を計算する関数に温度を与えると、それぞれがどれくらいの度合いなのかを返してくれます。
最終的にはこれを合計したり、平均化したりして使用します。(使用に関する詳細については後述)

30.5度という値が「寒い」のか「あたたかい」のか「暑い」のか。それを3段階ではなく、「少し暑い」などにもマッピングできるのがこのファジー理論です。
そして面白いことに、このメンバーシップ関数の計算方法を少し変えるだけで、30.5度が「あたたかい」なのか「とても暑い」なのかを主観的に変えることができるのです。
(計算式を変え、返す値を変えるだけで結果が変わる、ということです)

ゲームで使うことを考えると、この計算式の内容を変えるだけで、ちょっとしたキャラクターごとの性格の違いを表せそうですよね。
これが「主観的に変えることができる」と上で書いた理由です。

メンバーシップ関数を計算式にする

さて、使い方については上記の通りです。
今度はメンバーシップ関数を実際のプログラムとして記述し、与えられた実数(クリスプ数)からメンバーシップを求めます。(これをファジー化と言います)

標準的なメンバーシップ関数は以下のようになります。

右上がりのグラフ

ファジーグラフ右肩上がり.png

こんな形のグラフです。
ある閾値以下は0、もう一方の閾値以上は1、その間を線形で表すグラフです。

float FuzzyGrade(float value, float x0, float x1)
{
    float x = value;

    if (x <= x0)
    {
        return 0;
    }
    else if (x >= x1)
    {
        return 1;
    }
    else
    {
        // 分母を計算
        float denom = x1 - x0;
        return (x / denom) - (x0 / denom);
    }
}

右下がりのグラフ

ファジーグラフ右肩下がり.png

こんな形のグラフです。
ある閾値以下は1、もう一方の閾値以上は0、その間を線形で表すグラフです。
上のグラフのちょうど逆の形ですね。

float FuzzyReverseGrade(float value, float x0, float x1)
{
    float x = value;

    if (x <= 0)
    {
        return 1;
    }
    else if (x >= x1)
    {
        return 0;
    }
    else
    {
        float denom = x1 - x0;
        return (x1 / denom) - (x / denom);
    }
}

三角形型のグラフ

ファジーグラフ三角形.png

中心を最大値として、左右に値が下がる形のものです。

float FuzzyTriangle(float value, float x0, float x1, float x2)
{
    float x = value;

    if (x <= x0)
    {
        return 0;
    }
    else if (x == x1)
    {
        return 1;
    }
    else if ((x > x0) && (x < x1))
    {
        float denom = x1 - x0;
        return (x / denom) - (x0 / denom);
    }
    else 
    {
        float denom = x2 - x1;
        return (x2 / denom) - (x / denom);
    }
}

台形のグラフ

ファジーグラフ台形.png

全部の形をくっつけたような形。

float FuzzyTrapezoid(float value, float x0, float x1, float x2, float x3)
{
    float x = value;

    if (x <= x0)
    {
        return 0;
    }
    else if ((x >= x1) && (x <= x2))
    {
        return 1;
    }
    else if ((x > x0) && (x < x1))
    {
        float denom = x1 - x0;
        return (x / denom) - (x0 / denom);
    }
    else
    {
        float denom = x3 - x2;
        return (x3 / denom) - (x / denom);
    }
}

ファジールール

ファジー原理

ファジー入力に対する論理的なルールを作成する場合、その手段が必要となります。
通常のブーリアンでの評価とは異なり、出力される値が 0 1 ではなく、 0 ~ 1 の間で評価されるためです。
そして、ファジー変数に対するそうした論理演算は一般的に以下のように定義されます。

ファジー理論演算子

理論和

通常のブーリアンの式では「||」を用いて表現されるやつですね。
if (a || b) return; みたいな。
これを、ファジー理論で表すと以下のようになります。

float FuzzyOr(float a, float b)
{
    return Mathf.Max(a, b);
}

計算としては、与えられた値のうち、大きい方を返すだけの処理ですね。
つまり「より真に近い(大きい)ほう」を優先する感じです。

理論積

次はAnd。ブーリアンの式では「&&」ですね。

float FuzzyAnd(float a, float b)
{
    return Mathf.Min(a, b);
}

今度は Max ではなく Min を使っています。
イメージ的には「大は小を含む」ため、「& なら小さい方を採用」という感じでしょうか。

理論否定

続いて否定。「!flag」みたいなやつですね。
処理は簡単、1 - a と、1 から引くだけです。
そもそもの値が 0 - 1 で表されているので、 1 から引けば反転(否定)していることになるわけですね。

float FuzzyNot(float a, float b)
{
    return 1.0f - a;
}

ルールの評価

ブーリアンでの論理演算は、最終的に true or false の値が出力されます。
しかし、ファジー理論では前述の演算子を見てもらっても分かるように、出力が 0 ~ 1 の値を取り、通常の評価では成り立たないことが分かります。

従来のブール判定では、 if - else if - else を利用して順番に評価することでプログラムを記述します。
しかし、ファジー理論ではすべてのルールを並行で処理し、その結果得られた値を元に「結論の強さ」を算出します。

※ ここでの「ルール」は、ファジー化するメンバーシップ関数ひとつを意味しています。つまり、メンバーシップ関数を3つ用意している場合は、評価されるルールは3つとなります。

例えば、敵との距離に応じてなにか判断を行うAIがあったとします。
ブーリアンでのルールであれば、何m以内なら攻撃、それ以上遠く、かつ何m以内なら状況を見る、それ以外は無視、というようなケースで判断を行うかもしれません。
しかしこれだとどうしてもルールの境目で判断がばつっと切り替わってしまいます。

そうではなく、人間が判断するように曖昧な(ファジーな)判断をさせたい。その場合に今回のファジーなルール評価を行うわけです。

具体的に図にすると以下のようなイメージです。

ファジーグラフルール.png

これは「距離」に対応したメンバーシップ関数の例です。
上記の例では3つのメンバーシップ関数があり、これでひとつのファジー集合を表しています。
つまり、「距離」という数値を入力すると、準備された3つのメンバーシップごとに計算され、それの結果として、距離に関するファジー集合の「強さ」が算出される、というわけです。

これ以外にも、自身のHPの状態に応じた「ルール(ファジー集合)」を設けたり、レベルに関するルールなど必要な数だけ複数定義し、「それらすべてのルール」を評価した結果の「強さ」を用いて、最終的なAIの行動の判断材料とする、というのが大まかな流れとなります。

非ファジー化

とある値をファジー化することで、0 ~ 1 の間で曖昧に判断することができるようになりました。
しかし、実際にゲームを作る場合はさらにその値を何がしかの値に戻す必要が出てきます。

例えば、強い敵が接近してきた場合はすばやく逃げる、弱い敵が接近してきた場合はゆっくり逃げる、のように「逃げるか否か」だけでなく「どれくらいのスピードで」という情報もほしい場合が往々にしてあります。
その際に利用するのが、ファジー化の逆の「非ファジー化」です。

出力メンバーシップ関数

ファジー化のときに定義した「メンバーシップ関数」ですが、非ファジー化して値を元に戻すのも同様にメンバーシップ関数と呼びます。
結局のところ、「とある値」をなにかしらの値にマッピングするので、方向が違うだけでやっていることは同じ、というわけですね。
(ここでの「同じメンバーシップ関数」は、まったく同じ関数を指すのではなく、あくまで概念的な意味合いでのメンバーシップ関数が同じ、ということです)

参考にした書籍では、以下の図のような出力メンバーシップ関数が掲載されていました。

ファジーグラフ非ファジー化.png

具体的に言うと、「敵のレベル」や「敵の数」など、数値的に換算できるもの(実数)から、ファジー化を行い、脅威を図ります。
基本的には 0 ~ 1 の間で値を算出します。
そして、その算出された値を、今度は「出力メンバーシップ関数」に渡し、そこからなにかしらの実数を導き出します。
上記の例で言えば、「逃避」と判断された場合に、「どれくらいの速度」で逃げればいいのかを算出するイメージです。

出力メンバーシップ関数は様々に定義でき、それこそ実装したいゲームによって異なるでしょう。(あるいはキャラクターの性格などからも変わるかもしれません)

今回参考にした書籍には、上記のグラフの「加重平均」を算出する場合もある、と書かれていました。
が、計算負荷がそこそこ高く、またそこまでの精度がいらない場合にはより簡略化した値を使うようです。

具体的には「シングルトン出力メンバーシップ関数」を事前に用意しておき、その関数で計算した結果を採用する、というものです。
例えば、「逃避」は -10 、「何もしない」は 1 、「攻撃」は 10 という固定値を決めておき、それぞれのルールから算出された値と掛け算して、その結果を採用する、という感じです。

そして、すべての出力の合計について考える場合は「加重平均」が必要となります。
書籍から引用させてもらうと、

一般に、μを出力集合のtrueの度合いとし、xを出力集合に関連付けられたクリスプ数値(シングルトン)であるとします。非ファジー化された集約集合は、次のようになります。

output = \frac{\sum_{i=1}^{n} μ_{i}x_{i}}{\sum_{i=1}^{n} μ_{i}}

上記を踏まえて、仮に「0.2 の度合いで攻撃、0.4 の度合いで何もしない、0.7 の度合いで逃避」という計算結果の場合、出力される結果は以下のようになります。

[(0.2 * 10) + (0.4 * 1) + (0.7 * -10)] / (0.7 + 0.4 + 0.3) = -2.5

この結果から、-2.5 の度合いで「逃避する」というのが妥当な判断、と考えることができます。
(マイナスなら反対に移動する=逃避する、です)

サンプルコード

今回の例を元に、staticな Fuzzy クラスを実装してみた例が以下です。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// ファジー理論のメンバーシップ関数を定義
/// </summary>
static public class Fuzzy
{
    /// <summary>
    /// 右上がりのグラフのメンバーシップ関数
    /// </summary>
    /// <param name="value">評価する値</param>
    /// <param name="x0">最低位置の値</param>
    /// <param name="x1">最大位置の値</param>
    /// <returns></returns>
    static public float Grade(float value, float x0, float x1)
    {
        float x = value;

        if (x <= x0)
        {
            return 0;
        }
        else if (x >= x1)
        {
            return 1;
        }
        else
        {
            // 分母を計算
            float denom = x1 - x0;
            return (x / denom) - (x0 / denom);
        }
    }

    /// <summary>
    /// 右下がりのグラフのメンバーシップ関数
    /// </summary>
    /// <param name="value">評価する値</param>
    /// <param name="x0">最低位置の値</param>
    /// <param name="x1">最大位置の値</param>
    /// <returns></returns>
    static public float ReverseGrade(float value, float x0, float x1)
    {
        float x = value;

        if (x <= x0)
        {
            return 1;
        }
        else if (x >= x1)
        {
            return 0;
        }
        else
        {
            float denom = x1 - x0;
            return (x1 / denom) - (x / denom);
        }
    }

    /// <summary>
    /// 三角形型のグラフのメンバーシップ関数
    /// </summary>
    /// <param name="value">評価する値</param>
    /// <param name="x0">三角形左端の値</param>
    /// <param name="x1">中心位置の値</param>
    /// <param name="x2">三角形右端の値</param>
    /// <returns></returns>
    static public float Triangle(float value, float x0, float x1, float x2)
    {
        float x = value;

        if (x <= x0)
        {
            return 0;
        }
        else if (x == x1)
        {
            return 1;
        }
        else if ((x > x0) && (x < x1))
        {
            float denom = x1 - x0;
            return (x / denom) - (x0 / denom);
        }
        else
        {
            float denom = x2 - x1;
            return (x2 / denom) - (x / denom);
        }
    }

    /// <summary>
    /// 台形のグラフのメンバーシップ関数
    /// </summary>
    /// <param name="value">評価する値</param>
    /// <param name="x0">台形の左端の値</param>
    /// <param name="x1">台形上部の左側の値</param>
    /// <param name="x2">台形上部の右側の値</param>
    /// <param name="x3">台形の右端の値</param>
    /// <returns></returns>
    static public float Trapezoid(float value, float x0, float x1, float x2, float x3)
    {
        float x = value;

        if (x <= x0)
        {
            return 0;
        }
        else if ((x >= x1) && (x <= x2))
        {
            return 1;
        }
        else if ((x > x0) && (x < x1))
        {
            float denom = x1 - x0;
            return (x / denom) - (x0 / denom);
        }
        else
        {
            float denom = x3 - x2;
            return (x3 / denom) - (x / denom);
        }
    }

    /// <summary>
    /// ファジー理論AND演算子
    /// </summary>
    /// <param name="a">評価するファジー値A</param>
    /// <param name="b">評価するファジー値B</param>
    /// <returns>理論値</returns>
    static public float And(float a, float b)
    {
        return Mathf.Min(a, b);
    }

    /// <summary>
    /// ファジー理論OR演算子
    /// </summary>
    /// <param name="a">評価するファジー値A</param>
    /// <param name="b">評価するファジー値B</param>
    /// <returns>理論値</returns>
    static public float Or(float a, float b)
    {
        return Mathf.Max(a, b);
    }

    /// <summary>
    /// ファジー理論NOT演算子
    /// </summary>
    /// <param name="a">評価するファジー値A</param>
    /// <returns>理論値</returns>
    static public float Not(float a, float b)
    {
        return 1.0f - a;
    }
}

そして、これを用いて実際のコンテンツに使ってみたのが以下のコードです。
処理としてはDaydreamコントローラ風に、回転角度から位置を推測するような感じのものです。

※ Daydream向けの、Googleから提供されているSDKでは、コントローラの位置をある程度推測して計算する処理が入っています。(Daydreamコントローラは位置トラッキングが行えないため)
開発中はPCとViveを使って実装を行っていますが、似た動きをするものがほしかったので自作したものになります。

/// <summary>
/// 現在の方向に応じて、コントローラの位置の補正距離を算出する
/// </summary>
/// <returns>算出された補正距離</returns>
private float CalcDistance()
{
    float upCrisp = Vector3.Dot(_projectController.forward, Vector3.up);
    float angleCrisp = Vector3.Dot(_camera.forward, _projectController.forward);

    float angle = Fuzzy.Grade(angleCrisp, 0f, 1f);

    float down = Fuzzy.Triangle(upCrisp, -1f, -0.55f, 0.1f);
    float up = Fuzzy.Grade(upCrisp, 0.1f, 1f);

    float t = Fuzzy.Or(Fuzzy.Not(angle), Fuzzy.Or(up, down));

    return t * _distanceCoeff;
}

実際はファジー理論じゃなくてSDKと同じように実装したほうがいいと思うけど、なんとなく使ってみたかったので実装しましたw

まとめ

ファジー理論だけではこれといって大きな使い方はできないかもしれませんが、その他のAIなどと組みわせることによって、if - else でばっつりと別れていた分岐処理を、いい感じに曖昧に判断してくれるようになります。
また、AIだけでなく、その他のところでもこの「曖昧な判断」は役に立つと思うので覚えておいて損はないと思います。

参考書籍


ゲーム開発者のためのAI入門

27
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
35