simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

演繹的思考をするAIについて考えてみる

タイトル、どんだけ考えるんだよと突っ込みたいですが

どうも我々はひとり残らずどんなときも推論を続けながら生きているみたいなのですよ。
その推論には大きく2つに分類する見方があって、今日はそれをさらっと紹介して
その発展について考えてみたいと思います。

どんな推論であれ、抽象的、具体的分類について言及しているとのことで
例えば、「人は死ぬ」「ソクラテスは死ぬ」だと「人」と「ソクラテス」どちらが抽象的でどちらが具体的な分類をしているかというと
人の方が抽象的で、ソクラテスの方が具体的な分類になります。(ごめん、流石にこれできない人とは会話できないわ)


抽象的なことを理由に具体的なことを推理するのが演繹的な思考で
具体的なことを理由に抽象的なことを推理するのが帰納的な思考だそうです。
(思考です。)

演繹的思考:人は死ぬ、ソクラテスは人、ゆえに、ソクラテスは死ぬ
帰納的思考:ソクラテスは死ぬ、ソクラテスは人、ゆえに、人は死ぬ

演繹的思考で証明できないことは、前提となっている「人は死ぬ」という事象
帰納的思考で証明できないことは、結論として導いた「人は死ぬ」という事象

証明できない事象は同じ
前提と結論とあべこべなのが演繹と帰納の違いということで
演繹は抽象度の高い事象を使って、具体的な事象の推理を行うことで
帰納は具体的な事象をつかって、抽象度の高い事象を推理することです。

さて、ここで演繹的思考をするAIを作るとしたら、何から始めるかを考えていきたいと思います。

ある事象を分類する方法が2つ以上あるとき、比較して抽象的分類なのか具体的分類なのか
白黒はっきりつけることがまず、求められることがわかっています。

例えば、カラス、と、鳥
今そこにいるカラス、とカラス
比較する分類により、時に具体的な分類として認識したものは、次の瞬間、抽象的な分類となったりする。
これを判断するにはどうしたら良いのでしょうか?

考えている途中でAIについて詳しそうな人にアドバイスを求めたところ
フレーゲの言語記号論とLogic Theoristを調べてみたらどうかとのことだった。

フレーゲの言語記号論について調べてみました。
世の中の事象をすべて記号で表そうとする試みだったかな、人の心の中の「なんとなく」を遠ざけるやり方。

しかし、世界はすべてつながっていて、境界を設定すると矛盾が生じるため、記号で表すことには失敗するらしい。
知能は世界を理解するために、もっともらしい境界を設定して記号を与えているにすぎず、正しく切り分けている記号など一つもなかったという話。
そんなはずはない!と思うのですが、例えば自分(我)という記号はいったいどこからどこまでなのかを、はっきりくっきり切り分けようとすると、簡単に矛盾した表現ができます。
例えばこのブログの文章が自分と言えたり、言えなかったりで議論に決着が着くことはありません。
いったいどこからどこまでが自分なのでしょうか?
ということで、思考や説明するときに都合が良いから世界を切り分けて記号を与えている、ということだそうです。

Logic Theorist の方はオペレータと呼ぶ操作を限定して、初期状態と目標状態を設定し
目標状態のひとつ前の状態を、目標に設定し
そのひとつ前の状態のひとつ前の状態を目標に設定し
と繰り返していき、初期状態から次の状態へどう動かせば良いのかを当てるというものです。
これは世界を人間が与えることで、オペレータの範囲内では矛盾の生じない記号で世界を切り分けることが可能となっているので
ある仕事が成功する条件は、それ、それを実行する条件はこれ、今すぐある仕事もそれも出来ないけど、これならできる
じゃあ、これを行うという論理思考で目標を達成するというものです。

と勝手に「フレーゲの言語記号論」と「Logic Theorist」という記号から、それらしい説明ができるような状態に私の知能は変化したわけです。

アドバイスの調査はこれくらいにして
哲学に話が振れたので、有名なアリストテレスの手法で、観測できる事象を何でもかんでもカテゴライズしていくことを考えてみました。

鳥とカラスとすぐそこにいる動物であるカラスを例に考えてみます。

あらかじめ、鳥を動物というカテゴリーに入れます。(知っている人が教える)
次に、カラスを鳥というカテゴリーに入れます。(知っている人が教える)
このとき、カラスは動物というカテゴリーに入ります。(教わった方が勝手に)
目の前の何かを動物とカラスというカテゴリーに入れます。(知っている人が教える)
このとき、目の前の何かは動物であり、カラスであり、鳥であり、動物であると認識されます。(教わった方が勝手に)

教わった方は気づきます。そして問います。
目の前の何かは動物であるとのことだが、カラスは動物であるし、この2つの「動物」は同じなのか?と
同じ動物を指していると、知っている人が教えます。
(このフェーズは、無くても良いかも)

さて、教わった側に質問をします。
鳥とカラス、どちらが具体的かと

教わった側は鳥とカラスのカテゴリを調べ、カラスが鳥に属していることに気づきます。
比較するに妥当な2つであると判断します。
そこで回答します。
カラスは鳥の一種だから、カラスの方が具体的であり、残った鳥の方が抽象的であると

続いて、目の前にいる動物とカラスはどちらが具体的かと問います。
どちらもカラスなので比較は妥当と判断します。
目の前にいる動物はカラスのカテゴリーに属しています。
そして回答します。
目の前の動物の方が具体的で、カラスの方が抽象的であると

比較対象が自分のカテゴリーにある場合、カテゴリー側が抽象的と判断するロジックです。

知識をカテゴリー分けしながら、各カテゴリーの属性を学んでいくことで
鳥は飛ぶと学んだときに
カラスは飛ぶと推理できれば
これは演繹的思考です。

こんな感じで知識を構築するプログラムを書けば
ある学習レベルに到達すれば、「右手にバラを持った人がいる」という文を与えた時に
「何その文章?バラの棘が手に刺さって、怪我しませんか?」
と返すAIが作れる気がします。

ふと、ここに書いたことがひらめいたのでこんな記事を書いてみました。
ということで、作ってみたくなったので作ってみましょう。

Unity:3層ニューラルネットワークプログラム

人工知能とか機械学習とかを勉強していきたいと思います。
3層ニューラルネットワークをご存知でしょうか?

まずは解説されている書籍を読んでみます。
「ディジタル画像処理」のニューラルネットワークの説明を参考にしました。
書籍リンク:CG-ARTS協会 | 書籍・教材

ネットでもニューラルネットワークについて色々と情報が入手できますが
わかりやすいという点で、この書籍の説明より良いものを見たことがありません。

さて、この本の説明内容にそってプログラムを書きます。

先に結果だけ書くと、うまく動作していることがわかりました。
f:id:simplestar_tech:20160131223731p:plain
f:id:simplestar_tech:20160131223745p:plain

3層ニューラルネットワークのプログラムがこちら

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

public class NeuralNetwork3
{
    public float Rate = 0.05f;

    public void Setup(int inputUnitCount, int intermediateUnitCount, int outputUnitCount)
    {
        _inputCount = inputUnitCount;
        _unitCount = intermediateUnitCount;
        _resultCount = outputUnitCount;
        _layerInputIntermediateValues = new float[_inputCount * _unitCount];
        _intermediateUnits = new float[_unitCount];
        _layerIntermediateOutputValues = new float[_unitCount * _resultCount];
        _ek = new float[_resultCount];

        for (int i = 0; i < _layerInputIntermediateValues.Length; i++)
        {
            _layerInputIntermediateValues[i] = Random.value;
        }
        for (int i = 0; i < _layerIntermediateOutputValues.Length; i++)
        {
            _layerIntermediateOutputValues[i] = Random.value;
        }
    }

    public void Learn(float[] inputVector, float[] outputVector, float[] resultVector)
    {
        _CalculateIntemediateUnits(inputVector);
        _CalculateResultVector(resultVector);
        _UpdateIntermediateOutputValues(resultVector, outputVector);
        _UpdateInputIntermediateValues(inputVector, resultVector);
    }

    int _inputCount;
    int _unitCount;
    int _resultCount;
    float[] _layerInputIntermediateValues;
    float[] _layerIntermediateOutputValues;
    float[] _intermediateUnits;
    float[] _ek;

    private void _CalculateIntemediateUnits(float[] inputVector)
    {
        for (int unitIdx = 0; unitIdx < _unitCount; unitIdx++)
        {
            _intermediateUnits[unitIdx] = 0;
            for (int inputIdx = 0; inputIdx < _inputCount; inputIdx++)
            {
                _intermediateUnits[unitIdx] += inputVector[inputIdx] * _layerInputIntermediateValues[_inputCount * unitIdx + inputIdx];
            }
        }
    }

    private void _CalculateResultVector(float[] resultVector)
    {
        for (int resIdx = 0; resIdx < _resultCount; resIdx++)
        {
            resultVector[resIdx] = 0;
            for (int unitIdx = 0; unitIdx < _unitCount; unitIdx++)
            {
                resultVector[resIdx] += _intermediateUnits[unitIdx] * _layerIntermediateOutputValues[_unitCount * resIdx + unitIdx];
            }
        }
    }

    private void _UpdateIntermediateOutputValues(float[] resultVector, float[] outputVector)
    {
        for (int resIdx = 0; resIdx < _resultCount; resIdx++)
        {
            float diff = (resultVector[resIdx] - outputVector[resIdx]);
            float s = 1 / (1 + Mathf.Exp(-resultVector[resIdx]));
            _ek[resIdx] = diff * s * (1 - s);
            for (int unitIdx = 0; unitIdx < _unitCount; unitIdx++)
            {
                _layerIntermediateOutputValues[_unitCount * resIdx + unitIdx] -= Rate * _ek[resIdx] * _intermediateUnits[unitIdx];
            }
        }
    }

    private void _UpdateInputIntermediateValues(float[] inputVector, float[] resultVector)
    {
        for (int unitIdx = 0; unitIdx < _unitCount; unitIdx++)
        {
            float diffs = 0;
            for (int resIdx = 0; resIdx < _resultCount; resIdx++)
            {
                diffs += _ek[resIdx] * _layerIntermediateOutputValues[_unitCount * resIdx + unitIdx];
            }
            float s = 1 / (1 + Mathf.Exp(-_intermediateUnits[unitIdx]));
            diffs *= diffs * s * (1 - s);
            for (int inputIdx = 0; inputIdx < _inputCount; inputIdx++)
            {
                _layerInputIntermediateValues[_inputCount * unitIdx + inputIdx] -= Rate * diffs * inputVector[inputIdx];
            }
        }
    }
}

使い方:
公開変数 Rate は学習率です。
対象データに応じて適切に設定してください。
入力ベクトルのサイズと中間層のユニット数と出力ベクトルのサイズを渡して Setup します。
その後、用意している対象データを Learn に渡すことで内部の重み係数が更新されます。

以上です。

正しいかテストするコードは次の通り

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

public class TestNN3Behaviour : MonoBehaviour
{

    public int DataCount = 50;
    public int IterationCount = 50;

    NeuralNetwork3 _neuralNetwork3 = new NeuralNetwork3();
    List<InputSet> _inputSets;

    enum Janken
    {
        GOO,
        CYO,
        PAR,
        MAX
    }

    enum Wairo
    {
        NO,
        LO,
        HI,
        MAX
    }

    struct InputSet
    {
        public Janken aite;
        public Wairo wairo;
        public Janken jibun;
    }

    // Use this for initialization
    void Start()
    {
        int inputUnitCount = 6;
        int intermediateUnitCount = 9;
        int outputUnitCount = 9;
        _neuralNetwork3.Setup(inputUnitCount, intermediateUnitCount, outputUnitCount);
        _CreateInputSet();
        float[] resultVector = new float[outputUnitCount];
        string nowString = System.DateTime.Now.ToString("HHmmss");
        for (int itrIdx = 0; itrIdx < IterationCount; itrIdx++)
        {
            int victoryCount = 0;
            float error = 0;
            for (int dataIdx = 0; dataIdx < _inputSets.Count; dataIdx++)
            {
                InputSet input = _inputSets[dataIdx];
                float[] inputVector = _GetInputVector(ref input);
                float[] outputVector = _GetOutputVector(ref input);

                _neuralNetwork3.Learn(inputVector, outputVector, resultVector);

                Janken result = _GetResultJanken(resultVector);
                if (result == input.jibun)
                    ++victoryCount;
                for (int resIdx = 0; resIdx < outputUnitCount; resIdx++)
                {
                    float diff = (resultVector[resIdx] - outputVector[resIdx]);
                    error += diff * diff;
                }
            }
            float vRate = victoryCount / (float)_inputSets.Count * 100;

            using (StreamWriter writer = new StreamWriter("Result" + nowString + ".csv", true))
            {
                writer.WriteLine(vRate.ToString("0.00000") + "," + error.ToString("0.00000"));
            }
        }
    }

    // Update is called once per frame
    void Update()
    {

    }

    private Janken _GetResultJanken(float[] resultVector)
    {
        Janken result = Janken.MAX;
        float maxRes = 0;
        int maxResIdx = 0;
        for (int resIdx = 0; resIdx < resultVector.Length; resIdx++)
        {
            if (maxRes < resultVector[resIdx])
            {
                maxRes = resultVector[resIdx];
                maxResIdx = resIdx;
            }
        }
        result = (Janken)(maxResIdx % (int)Janken.MAX);
        return result;
    }

    private float[] _GetOutputVector(ref InputSet input)
    {
        float[] outputVector = new float[9];
        outputVector[0] = (input.jibun == Janken.GOO && input.wairo == Wairo.NO) ? 1 : 0;
        outputVector[1] = (input.jibun == Janken.CYO && input.wairo == Wairo.NO) ? 1 : 0;
        outputVector[2] = (input.jibun == Janken.PAR && input.wairo == Wairo.NO) ? 1 : 0;
        outputVector[3] = (input.jibun == Janken.GOO && input.wairo == Wairo.LO) ? 1 : 0;
        outputVector[4] = (input.jibun == Janken.CYO && input.wairo == Wairo.LO) ? 1 : 0;
        outputVector[5] = (input.jibun == Janken.PAR && input.wairo == Wairo.LO) ? 1 : 0;
        outputVector[6] = (input.jibun == Janken.GOO && input.wairo == Wairo.HI) ? 1 : 0;
        outputVector[7] = (input.jibun == Janken.CYO && input.wairo == Wairo.HI) ? 1 : 0;
        outputVector[8] = (input.jibun == Janken.PAR && input.wairo == Wairo.HI) ? 1 : 0;
        return outputVector;
    }

    private float[] _GetInputVector(ref InputSet input)
    {
        float[] inputVector = new float[6];
        inputVector[0] = input.aite == Janken.GOO ? 1 : 0;
        inputVector[1] = input.aite == Janken.CYO ? 1 : 0;
        inputVector[2] = input.aite == Janken.PAR ? 1 : 0;
        inputVector[3] = input.wairo == Wairo.NO ? 1 : 0;
        inputVector[4] = input.wairo == Wairo.LO ? 1 : 0;
        inputVector[5] = input.wairo == Wairo.HI ? 1 : 0;
        return inputVector;
    }

    private void _CreateInputSet()
    {
        _inputSets = new List<InputSet>();
        for (int dataIdx = 0; dataIdx < DataCount; dataIdx++)
        {
            InputSet inputSet = new InputSet() { aite = (Janken)Random.Range(0, (int)Janken.MAX), wairo = (Wairo)Random.Range(0, (int)Wairo.MAX) };
            inputSet = _SetWinnerHand(inputSet);
            _inputSets.Add(inputSet);
        }
    }

    private static InputSet _SetWinnerHand(InputSet inputSet)
    {
        switch (inputSet.aite)
        {
            case Janken.GOO:
                switch (inputSet.wairo)
                {
                    case Wairo.NO:
                        inputSet.jibun = Janken.PAR;
                        break;
                    case Wairo.LO:
                        inputSet.jibun = Janken.GOO;
                        break;
                    case Wairo.HI:
                        inputSet.jibun = Janken.CYO;
                        break;
                    case Wairo.MAX:
                        break;
                    default:
                        break;
                }
                break;
            case Janken.CYO:
                switch (inputSet.wairo)
                {
                    case Wairo.NO:
                        inputSet.jibun = Janken.GOO;
                        break;
                    case Wairo.LO:
                        inputSet.jibun = Janken.CYO;
                        break;
                    case Wairo.HI:
                        inputSet.jibun = Janken.PAR;
                        break;
                    case Wairo.MAX:
                        break;
                    default:
                        break;
                }
                break;
            case Janken.PAR:
                switch (inputSet.wairo)
                {
                    case Wairo.NO:
                        inputSet.jibun = Janken.CYO;
                        break;
                    case Wairo.LO:
                        inputSet.jibun = Janken.PAR;
                        break;
                    case Wairo.HI:
                        inputSet.jibun = Janken.GOO;
                        break;
                    case Wairo.MAX:
                        break;
                    default:
                        break;
                }
                break;
            case Janken.MAX:
                break;
            default:
                break;
        }
        return inputSet;
    }
}

テスト内容は入力ベクトルが6要素
組み合わせとしては 9 通りの入力に対して、9通りの答えを返すというもの
出力ベクトルは9要素とし
中間層は9ユニットとしています。

どういうテストかというと
ジャンケンで勝つログを与えるというものです。
賄賂を受け取るとその量に応じてあいこにしたり、わざと負けたりするというのが正解としています。
これ結構意地悪なルールで勝率100%は難しいと思って設定しました。

データ数50で50回イテレーションを回すと(合計50 x 50 Learn を実行するということ)
記録するコードの通り csv には最初に示した通りの結果が出ました。
なんと!こんないじわるなルールでも正解率100%のAIができたのです。

是非あなたが持っている価値あるデータを入力に3層ニューラルネットワークを学習させてみてください。
誤差は順調に減少していくと思いますから、そうなったとき好ましい結果を返すAIになってくれると良いですね。

今回のプログラムはUnityパッケージとしてダウンロードできるようにしておきます。
http://file.blenderbluelog.anime-movie.net/ANN3Layer.zip

Unity:音声合成でリアルタイムにコンピュータにしゃべってもらう その3でオウム返し

音声合成でリアルタイムにしゃべってもらう話は以前しましたね。
simplestar-tech.hatenablog.com
ここで、予告通り今回はオウム返しするシステムを作ります。
マイクでしゃべったことをテキストに変換する話も以前しましたね。
simplestar-tech.hatenablog.com
ここで

これらをつなげるだけで行けました。
結果、マイクに向かってしゃべったことをCeVIOが返してくれました。
誰かと会話しているみたいで、もはや独り言ではなかった。(新感覚)

f:id:simplestar_tech:20160131160341p:plain

これを実現したコードを次に示します。

using UnityEngine;
using System.Collections;

public class RepeatBehaviour : MonoBehaviour {

    public CeVIOControlBehaviour CeVIOCtrl;
    public VoiceRecognitionBehaviour VoiceRecognition;

    private Queue _sentenceQueue = new Queue();

	// Use this for initialization
	void Start () {
        VoiceRecognition.OnRecData += VoiceRecognition_OnRecData;
	}
	
	// Update is called once per frame
	void Update () {

        if (0 < _sentenceQueue.Count)
        {
            string sentence = (string)_sentenceQueue.Dequeue();
            VoiceRecognition.StopRec();
            Debug.Log(sentence);
            CeVIOCtrl.StartTalk(sentence);
            VoiceRecognition.StartRec();
        }
	}

    void VoiceRecognition_OnRecData(string sentence)
    {
        _sentenceQueue.Enqueue(sentence);
    }
}

VoiceRecognitionBehaviour
のイベントにハンドラを設定し
認識結果の文字列を受け取ったら、それを
CeVIOControlBehaviour
Talk関数に渡すというものです。

無限ループに陥らないように、CeVIOがトーク中は音声認識を切っている点が工夫したところですね。

あとは既存記事をたどれば以下のコードの意味も読み解けると思います。
VoiceRecognitionBehaviour

using UnityEngine;
using System.Collections;

public class VoiceRecognitionBehaviour : MonoBehaviour {

    #region Inspector Config Vars
    public enum RecognitionType
    {
        Dictation,
        CommandControl
    }

    public RecognitionType mode = RecognitionType.Dictation;
    public string[] commands;
    private string cur_deviceName = "VF0800"; //Optional: F200 Device Name
    #endregion

    #region SenseInput Vars
    private PXCMSession _session;
    private PXCMAudioSource _source;
    private PXCMSpeechRecognition _sr;
    private PXCMSpeechRecognition.Handler _handler;
    private pxcmStatus _sts;
    #endregion

    #region Public Events To Subscribe
    public delegate void OnRecDataDelegate(string sentence);

    public event OnRecDataDelegate OnRecData;

    public delegate void OnAlertDelegate(string alertlabel);

    public event OnAlertDelegate OnAlertData;

    public delegate void OnShutDownDelegate();

    public event OnShutDownDelegate OnShutdown;

    #endregion

    internal void StopRec()
    {
        _sr.StopRec();
    }

    internal void StartRec()
    {
        _sr.StartRec(_source, _handler);
    }

    private void OnRecognition(PXCMSpeechRecognition.RecognitionData data)
    {
        if (mode == RecognitionType.CommandControl)
        {
            if (data.scores[0].confidence > 50)
                if (OnRecData != null)
                    OnRecData(data.scores[0].sentence);
        }
        else
        {
            if (OnRecData != null)
                OnRecData(data.scores[0].sentence);
        }
    }

    private void OnAlert(PXCMSpeechRecognition.AlertData data)
    {
        if (OnAlertData != null)
            OnAlertData(data.label.ToString());
    }

    // Use this for initialization
    void Start()
    {
        _SetupRecognition();

        _sts = _sr.StartRec(_source, _handler); // for default device: source = null
        if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
            Debug.LogError("Failed to Start Recognition: " + _sts);
    }

    private void _SetupRecognition()
    {
        _session = PXCMSession.CreateInstance();
        if (_session == null)
            Debug.LogError("Failed to create a session");
        _source = _session.CreateAudioSource();
        _SetInputDevice();

        _sts = _session.CreateImpl<PXCMSpeechRecognition>(out _sr);
        if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
            Debug.LogError("Failed to create a Speech Recognition Instance: " + _sts);
        _SetLanguage();

        /* Set handler: OnRecognition & OnAlert */
        _handler = new PXCMSpeechRecognition.Handler();
        _handler.onRecognition = OnRecognition;
        _handler.onAlert = OnAlert;

        _SetRecognitionMode();
    }

    private void _SetRecognitionMode()
    {
        switch (mode)
        {
            case RecognitionType.Dictation:
                _sts = _sr.SetDictation(); // Set Dictation Mode
                if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
                    Debug.LogError("Failed to set Dictation Mode: " + _sts);
                break;
            case RecognitionType.CommandControl:
                if (commands.Length == 0)
                {
                    Debug.LogError("Grammar list is Empty");
                    // return;
                }

                _sts = _sr.BuildGrammarFromStringList(1, commands, null); // Build the grammar
                if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
                    Debug.LogError("Failed to Build Grammar from list: " + _sts);

                _sts = _sr.SetGrammar(1); // Set active grammar
                if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
                    Debug.LogError("Failed to set Grammar: " + _sts);
                break;
        }
    }

    private void _SetLanguage()
    {
        PXCMSpeechRecognition.ProfileInfo pinfo;
        _sts = _sr.QueryProfile(out pinfo);
        if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
            Debug.LogError("Failed to retrieve a Speech Recognition Profile: " + _sts);
        pinfo.language = PXCMSpeechRecognition.LanguageType.LANGUAGE_JP_JAPANESE;
        _sr.SetProfile(pinfo);
        if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
            Debug.LogError("Failed to set language Profile: " + _sts);
    }

    private void _SetInputDevice()
    {
        _source.ScanDevices();
        bool isDeviceSet = false;
        PXCMAudioSource.DeviceInfo dinfo = null;
        int deviceNum = _source.QueryDeviceNum();
        Debug.Log("Found " + deviceNum + " input devices.");

        for (int d = 0; d < deviceNum; d++)
        {
            _sts = _source.QueryDeviceInfo(d, out dinfo);
            if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
                continue;

            if (dinfo.name.Contains(cur_deviceName))
            {
                _sts = _source.SetDevice(dinfo);
                if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR)
                {
                    Debug.LogError("Failed to Set Device: " + _sts);
                }
                else
                {
                    isDeviceSet = true;
                    Debug.Log("Selected F200 Input: " + dinfo.name);
                }
                break;
            }
        }

        /* If F200 not found select last found Input device */
        if (!isDeviceSet)
        {
            _sts = _source.SetDevice(dinfo);
            if (_sts < pxcmStatus.PXCM_STATUS_NO_ERROR) Debug.LogError("Failed to Set Device: " + _sts);
            else Debug.Log("Selected: " + dinfo.name);
        }
    }

    // Use this for Cleap Up
    void OnDisable()
    {
        /* Inform Subscribers */
        if (OnShutdown != null)
            OnShutdown();

        /* Stop the session */
        if (_sr != null)
        {
            // Stop recognition
            _sr.StopRec();

            // Destroy the Speech Recognition Instance
            _sr.Dispose();
        }

        /* Destroy the session */
        if (_session != null)
            _session.Dispose();
    }
}

CeVIOControlBehaviour

using UnityEngine;
using System.Collections;
using System.Diagnostics;

public class CeVIOControlBehaviour : MonoBehaviour
{
    
    private Process _process;

    public void StartTalk(string text)
    {
        if (null == _process)
        {
            _process = new Process();
            _process.StartInfo.FileName = Application.dataPath + "/External/CeVIOCtrl.exe";
            _process.StartInfo.Arguments = text;
            // for Redirect
            {
                _process.StartInfo.CreateNoWindow = true;
                _process.StartInfo.UseShellExecute = false;
                _process.StartInfo.RedirectStandardOutput = true;
            }
            // for ExitEvent
            {
                _process.EnableRaisingEvents = true;
                _process.Exited += Process_Exited;
            }
            _process.Start();
            _process.WaitForExit();
        }
    }

    public void ExitTalk()
    {
        if (null != _process)
        {
            if (!_process.HasExited)
            {
                _process.CloseMainWindow();
                _process.Dispose();
                _process = null;
            }
        }
    }
	
    // Use this for initialization
	void Start () {
        _process = null;
	}

    // Update is called once per frame
    void Update()
    {
    }

    void Process_Exited(object sender, System.EventArgs e)
    {
        _process.Dispose();
        _process = null;
    }
}

さて、動作を確認するには
CeVIO4.0のインストールと
RealsenseSDK 関連のアセットを取得してUnityにインポートする必要があります。

それ以外のものをアップしておきます。
上記で示したものと同一のサンプルです。
http://file.blenderbluelog.anime-movie.net/Echo.zip

CeVIO4.0がリリースされたので新機能を試してみた

新機能の抑揚をMAXにしたら、えらい生き生きとした音声になった。
今後もCeVIOで音声合成しようと思った次第です。

とりあえず、さすがCeVIO音声作ってみた。
http://file.blenderbluelog.anime-movie.net/sasucevi.mp3

前回の記事の CeVIOCtrl を 4.0 のアセンブリでビルドし直すことで
前回のプログラムも問題なく動作することを確認

引き続き音声認識との連携を書いていきます。

Unity:音声合成でリアルタイムにコンピュータにしゃべってもらう その2でCeVIO

前回
simplestar-tech.hatenablog.com
の続きです。

cevio.jp
を触ってみました。

起動するとトラックごとのテキスト入力欄があるので、今回は「CeVIOだよ!」と入力

f:id:simplestar_tech:20160117115152p:plain

再生すると次の音声ファイルが作られました。

http://file.blenderbluelog.anime-movie.net/CeVIO_dayo.mp3

CeVIO のうれしい機能は、発音記号ごとに音程を変えられるというところ

f:id:simplestar_tech:20160117122910p:plain

マウスの直感的な操作でぬるりとイントネーションをいじれます。
気になるイントネーションをいじると、より自然に聞こえたりします
(まだ自然に聞こえないのは自分のスキルが足りないためでもあるので、ここは訓練あるのみ)

http://file.blenderbluelog.anime-movie.net/CeVIO_dayo2.mp3

さて、公式に外部インタフェースも公開されているとのことで
.NETアセンブリとして利用 - CeVIO Creative Studio ユーザーズガイド
こちらを読んでみました。

CeVIO本体を起動していないとCeVIOは動作しないとのことです。
なるほど、音を鳴らすのも CeVIO 本体という訳ですね。

手始めに Unity から"こんにちは"を"さとうささら"にしゃべってもらうコードを書いてみます。
Unity 5 64bit 版で直接 dll を参照する場合、v3.5 以下の .NET バージョンの DLL が必要です。
CeVIO は v4.0 以上なので直接参照はできません。
そこで Unity から外部プロセスを実行する機能を利用して、その外部プロセスにテキストを渡す形で CeVIO を動かします。
リップシンクを目的として音素記号が欲しい場合はコンソール出力より文字列を受け取れば良いので、外部プロセス形式でも関数のような利用が可能です。

まずは Unity 側のコードを先に書いておきましょうか
次の記事を参考にしました。
Unityから外部プログラム(プロセス)を実行する - Neareal
実際のコードはこんな感じです。

using UnityEngine;
using System.Collections;
using System.Diagnostics;

public class CeVIOControl : MonoBehaviour {
    
    private Process _process;
	
    // Use this for initialization
	void Start () {
        _process = null;
	}

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (null == _process)
            {
                _process = new Process();
                _process.StartInfo.FileName = Application.dataPath + "/External/CeVIOCtrl.exe";
                _process.StartInfo.Arguments = "こんにちわ さとうささらです。 はじめまして!";
                // for Redirect
                {
                    _process.StartInfo.CreateNoWindow = true;
                    _process.StartInfo.UseShellExecute = false;
                    _process.StartInfo.RedirectStandardOutput = true;
                }
                // for ExitEvent
                {
                    _process.EnableRaisingEvents = true;
                    _process.Exited += Process_Exited;
                }
                _process.Start();
                // Redirect
                string output = _process.StandardOutput.ReadToEnd();
                UnityEngine.Debug.Log(output);
            }
        }
        if (Input.GetKeyDown(KeyCode.Return))
        {
            if (null != _process)
            {
                if (!_process.HasExited)
                {
                    _process.CloseMainWindow();
                    _process.Dispose();
                    _process = null;
                }
            }
        }
    }

    void Process_Exited(object sender, System.EventArgs e)
    {
        UnityEngine.Debug.Log("Process_Exited");
        _process.Dispose();
        _process = null;
    }
}

使い方
Space キーを押すと "こんにちわ" とPCがしゃべります。
途中で中断したい場合は Return キーを押すと中断します。
(注意:上記コードは StandardOutput の ReadToEnd を呼んでいるので中断しません。これをコメントアウトすれば中断できます。)

続いて CeVIOCtrl.exe の作り方を示します。

VisualStudio より C# WPF アプリをテンプレートに作成します。
名前は CeVIOCtrl としました。
参照追加より C:\Program Files (x86)\CeVIO\CeVIO Creative Studio\CeVIO.Talk.RemoteService.dll
を追加して、次のクラスを用意します。

using CeVIO.Talk.RemoteService;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CeVIOCtrl
{
    class CeVIOController
    {
        internal static void Talk(string text)
        {
            ServiceControl.StartHost(false);
            Talker talker = new Talker();
            foreach (string cast in Talker.AvailableCasts)
            {
                talker.Cast = cast;
                talker.Speak(text).Wait();
                break;
            }
            foreach (var item in talker.GetPhonemes(text))
            {
                Console.Write(item.Phoneme);
            }
            Console.WriteLine();
        }
    }
}

受け取ったテキストの読み上げと、音素解析を行ってそれをコンソール出力しています。
プログラムの細かい事はCeVIO公式ページに示されているので大丈夫です。
そちらを読みましょう。

黒いコンソール画面が出るのが嫌で、Windows アプリとしました。
MainWindow の Visibility を Hidden に設定します。
そしてコマンドライン引数を受け取って動かすように
App.xaml にて StartUp イベントハンドラを登録し、下記の通り実装します。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace CeVIOCtrl
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            foreach (string arg in e.Args)
            {
                CeVIOController.Talk(arg);
            }
            Shutdown(0);
        }
    }
}

これで CeVIOCtrl.exe "こんにちわ"
と、引数指定で実行すれば Window が出ないまま音声だけが流れます。
(注意:CeVIO本体のウィンドウは表示されます。)

しかしですね、Unity 側のコードで書きましたが CreateNoWindow = true; とすれば
コンソールアプリでもウィンドウは出ません。
どうも Windows アプリで起動するとカーソルが一瞬砂時計になってしまうようなので、コンソールアプリに切り替えました。
(アプリの切り替えは簡単、Properties の出力の種類にて Windows アプリケーションからコンソールアプリケーションに切り替えるだけです。)

さて、CeVIO の音声再生と音素記号の出力とUnity側で音素記号の受け取りまで、まとめて書いてしまいましたので、使用する目的に合わせて機能の切り分けを行ってください。

リップシンクさせてユニティちゃんなどのキャラクターにしゃべってもらうということがこれで出来そうです。
次の記事では Intel RealSense の音声認識と合わせてオウム返しをするプログラムを作ってみます。

今回の成果物は下記に配置しました。
上記で示した内容と同じものです。

CeVIOCtrlプロジェクト一式
http://file.blenderbluelog.anime-movie.net/CeVIOCtrl.zip

CeVIOCtrlを呼ぶUnityパッケージ
http://file.blenderbluelog.anime-movie.net/CeVIOTest.unitypackage

Hearthstone: ハースストーンのスプリットダメージの期待値プログラム 高速化

およそ想定される最大計算量の 5^10 の分岐確率計算に 0.4 秒要するというのが前回の省メモリプログラムの速度でした。
これに対して
「秒という単位が出た時点で遅いです。」
という意見をもらいまして、さらなる高速化が求められている訳です。

高速化できる所を探してみました。

一つにアクティブなカード数を数えるループの部分があります。
分岐の親の場のカード数から変動があるかどうかは、親の処理で攻撃したときにライフ0になったかどうかを確認するだけで良いので
アクティブなカード枚数はその時に更新して子に渡せば良いというものです。

もう一つ高速化できるとしたら、場にカードが1枚残った場合
分岐は発生しないので、現在の確率を足して、早々に計算を切り上げるというものです。

さらにもう一つ、条件は限られますが攻撃回数より低いライフが一つもない場合は
計算せずにすべて 0 を返すと、意地悪な条件のもとで無駄な計算をせず済みます。

高速化前と高速化後のコードについて、計算時間をみてみましょう。

まず、高速化前のプログラムで
場に 4, 5, 6, 7, 8 というカードがある状態で
10回スプリットダメージを与えた時に、正しい答えを出すのに要した時間は 356 ms でした。
確率は次の通り
0.120805, 0.032919, 0.006379, 0.000864, 0.000078

第一案の現在のカード数を子に引き渡すように書いてみました。
すると同じ解答を得るのに 255 ms 要しました。
1.4 倍まで高速化できました。

コード見ていてひらめいたことに、確率計算のための割り算を無駄にループ内に入れていました。
この計算をループの外に置いて高速化をはかりましたが、ウェイトが低すぎて高速化は確認できませんでした。

次に

二つ目の案の最後の一枚が残ったら再帰処理を終了させるように書いてみました。
重要なのは、すべてのカードが撃破される場合はすでに除外しているので、最後の一枚が残ったら必ず生き残るということがわかります。
なので、場に一枚だけになった瞬間に分岐処理は終えても計算結果に影響は出ません。

246 ms 要してます。
ほんの少し高速化できました。

ここでさらに高速化できそうな処理を思いつきます。
例えば残りのダメージ回数が場に残っているカードの最低ライフに届かない場合
その残っているカードについてのみ撃破率の計算は不要です。
そんな時は、それまでに倒されているカードの確率を集計すれば良いのです。
という再帰処理を終了させる条件を加えてみました。

すると同じ解答を得るのに 128 ms を要しました。
前回の高速化よりさらに 1.92 倍も高速化できました。

さてさて、高速化のすごいところはここから
条件を変えてみましょう。
場に 7, 8, 9, 10, 11 というカードがある状態で
10回スプリットダメージを与えた時に、正しい答えを出すのに要した時間は
高速化前は 354 ms でした。
高速化後は 3 ms でした。
どちらも正解の
0.0008643, 0.0000779, 0.0000042, 0.0000001, 0.0000000 を計算しています。
なんと 118 倍も高速化しています。
というか 10 回もスプリットダメージを与えて全分岐をしらべると単純に
9765625 通りの可能性を一つずつ見ていかなければならないのに、正しい答えを出すのに 3 ms かよ。
という驚きがあります。

通常の計算量についても見てみます。
場に 4, 5, 6, 7, 8 というライフのカードがあったときに
8回スプリットダメージを与えて、それぞれのカードの撃破率を 0~1 で計算すると
0.0562765, 0.0104067, 0.0012314, 0.0000845, 0.0000026
となり、これに要する時間は 3 ms でした。

さて、今回の高速化版のコードがこちら

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

class SplitDamageCalculator
{
    /// <summary>
    /// カードの撃破率を計算
    /// </summary>
    /// <param name="cardLives">カードのライフリスト</param>
    /// <param name="damageCount">スプリットダメージ回数</param>
    /// <param name="cardDefeatRates">カードごとの撃破率(0~1)</param>
    public static void CalculateDefeatRates(int[] cardLives, int damageCount, out float[] cardDefeatRates)
    {
        int numCards = cardLives.Length;
        cardDefeatRates = new float[numCards];
        float probability = 1;
        int totalLife = 0;
        for (int j = 0; j < numCards; j++)
            totalLife += cardLives[j];
        if (damageCount >= totalLife)
        {
            for (int i = 0; i < numCards; i++)
                cardDefeatRates[i] = 1;
        }
        else if (0 < damageCount)
        {
            int defeatTargetCount = 0;
            for (int i = 0; i < numCards; i++)
                if (damageCount >= cardLives[i])
                    ++defeatTargetCount;
            if (0 < defeatTargetCount)
            {
                _CalcuCombination(ref cardDefeatRates, ref cardLives, probability, numCards, numCards, damageCount);
            }
        }
    }

    /// <summary>
    /// 組み合わせの確率計算
    /// </summary>
    /// <param name="cardDefeatRates">確率計算結果格納先</param>
    /// <param name="rootCombination">分岐の根のライフの組み合わせ</param>
    /// <param name="rootProbability">分岐の根の分岐確率</param>
    /// <param name="numCards">初期の場のカード枚数</param>
    /// <param name="numActiveCards">分岐の根の場のカード枚数</param>
    /// <param name="damageCount">残っているダメージ回数</param>
    private static void _CalcuCombination(ref float[] cardDefeatRates, ref int[] rootCombination, float rootProbability, int numCards, int numActiveCards, int damageCount)
    {
        if (0 == damageCount)
        {
            for (int k = 0; k < numCards; k++)
                if (0 == rootCombination[k])
                    cardDefeatRates[k] += rootProbability;
            return;
        }
        int defeatTargetCount = 0;
        for (int i = 0; i < numCards; i++)
            if (damageCount >= rootCombination[i])
                ++defeatTargetCount;
        if (0 < defeatTargetCount)
        {
            float probability = rootProbability * 1 / (float)numActiveCards;
            for (int i = 0; i < numCards; i++)
            {
                if (0 != rootCombination[i])
                {
                    int localActive = numActiveCards;
                    if (0 == --rootCombination[i])
                        --localActive;
                    _CalcuCombination(ref cardDefeatRates, ref rootCombination, probability, numCards, localActive, damageCount - 1);
                    ++rootCombination[i];
                }
            }
        }
        else
        {
            for (int k = 0; k < numCards; k++)
                if (0 == rootCombination[k])
                    cardDefeatRates[k] += rootProbability;
        }
    }
}

検算プログラムのプロジェクト一式はこちらに配置しました。

http://file.blenderbluelog.anime-movie.net/SplitDamageCalculator.zip

Hearthstone: ハースストーンのスプリットダメージの期待値プログラム省メモリ化

ふと、前回書いたプログラム
simplestar-tech.hatenablog.com

について
デバッグ用に組み合わせ一覧を記録していましたが
ほしいのはカードごとの撃破率なので、計算結果が出たら
加算していけば答えを変えずに省メモリ化できることに気づきまして
下記の通り、省メモリ化を施しました。
若干の高速化もできています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

class SplitDamageCalculator
{
    public void CalculateP(int[] cardLives, int damageCount, out float[] cardDefeatRates)
    {
        int numCards = cardLives.Length;
        cardDefeatRates = new float[numCards];
        float probability = 1;
        int totalLife = 0;
        for (int j = 0; j < numCards; j++)
            totalLife += cardLives[j];
        if (damageCount >= totalLife)
        {
            for (int i = 0; i < numCards; i++)
                cardDefeatRates[i] = 1;
        }
        else if (0 < damageCount)
        {
            _ListCombinationProbability(ref cardDefeatRates, ref cardLives, probability, numCards, damageCount - 1);
        }
        else
        {
            for (int i = 0; i < numCards; i++)
                cardDefeatRates[i] = 0;
        }
    }

    private static void _ListCombinationProbability(ref float[] cardDefeatRates, ref int[] rootCombination, float rootProbability, int numCards, int damageCount)
    {
        if (0 > damageCount)
        {
            for (int k = 0; k < numCards; k++)
                if (0 == rootCombination[k])
                    cardDefeatRates[k] += rootProbability;
            return;
        }
        for (int i = 0; i < numCards; i++)
        {
            if (0 != rootCombination[i])
            {
                int numActiveCards = 0;
                for (int j = 0; j < numCards; j++)
                    if (0 != rootCombination[j])
                        ++numActiveCards;
                float probability = rootProbability * 1 / (float)numActiveCards;
                rootCombination[i]--;
                _ListCombinationProbability(ref cardDefeatRates, ref rootCombination, probability, numCards, damageCount - 1);
                rootCombination[i]++;
            }
        }
    }
}

また、とこかで大幅に高速化できる要素がひらめいたら追記します。

ひらめいたので書きました。
simplestar-tech.hatenablog.com