simplestarの技術ブログ

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

Unity:NMeCabのソースコードを使って動的に漢字の読み仮名を推定する方法

■前書き
Unityで形態素解析

私たち人間の脳内では「何か」が実行され

ユニティ デ ケイタイソ カイセ

という「音」が脳内で響き渡ります。

これを、自分の手で作り上げ、コンピュータに行わせる手段を示します。

■横道
形態素解析って言葉は、難しい
ので、動的に漢字の読み仮名を推定する方法、といった少しわかりやすい表現に変えてみた。(機能は限定されちゃうけど)

■本題
MeCab京都大学のとある共同研究ユニットにて開発されたオープンソース 形態素解析エンジン
高速に動作するのが持ち味で、和布蕪(めかぶ)は, 作者の好物だからだとのこと

オリジナルの MeCabC++ で書かれているけど、Unity から呼びやすいように C# で動くようにコードを移植してくれた例が見つかりました。
ja.osdn.net

Unity への導入の参考記事はこちら
qiita.com

MacOS, AndroidでNMecabが使えずに困る→解決方法
dll を配置してサンプルを書く場合は Windows, Editor 上でしかうまくいかず
MacOS ビルド、Android Build などで機能しなくなります。

解決するには、込み入った手順が必要なので、本記事にその作業内容だけ記録します。
Android で動いたら完了である

Android 環境で動かないことを確認
dll を配置して、問題が再現することを確かめます。→確かに機能しない
例外が発生しており
PlatformNotSupportedException: Operation is not supported on this platform.

ソースコードで動くように調整
LibNMeCab.dll の代わりに
src/LibNMeCab 以下にある
Properties, app.config, LibNMeCab.csproj 以外のファイルを全コピー
unsafe コード許可を求められるので、Project Settings > Player > Allow 'unsafe' Code にチェックを入れます。

SettingChangingEventArgs が見つからないエラーが出たら、それを利用している空の関数を削除し

MeCabParam の初期値の未実装エラーについては、以下のコードを、次のコードブロックに書き換えます。

        public MeCabParam()
        {
            this.Theta = MeCabParam.DefaultTheta;
            this.RcFile = MeCabParam.DefaultRcFile;

            Properties.Settings settings = Properties.Settings.Default;
            this.DicDir = settings.DicDir;
            this.UserDic = this.SplitStringArray(settings.UserDic, ',');
            this.OutputFormatType = settings.OutputFormatType;
        }
        public MeCabParam()
        {
            this.Theta = MeCabParam.DefaultTheta;
            this.RcFile = MeCabParam.DefaultRcFile;

            this.DicDir = "";
            this.UserDic = new string[] { };
            this.OutputFormatType = "lattice";
        }

一応これでソースコードを用いて、dll と同じ処理結果が得られるようになります。

Android ビルドでまずいコードの書き換え

先ほどと同じように Android 端末上でデバッグ実行すると、次の例外により処理が止まっていることを確認できるようになります。
DirectoryNotFoundException: Could not find a part of the path "/jar:file:/data/app/com.XXXXXXX.NMeCabTest-yWcMcBBKreSyIzMxor-Saw==/base.apk!/assets/NMeCab/dic/ipadic/char.bin".

ビルドするプラットフォームごとにファイルアクセス方法が変わるので、ここは UnityWebRequest の出番です。

デバッグ中に
MeCabInvalidFileException: dictionary file is broken が発生

問題を明らかにしていくとこういうことらしい
reader.BaseStream.Length 65536 != (magic ^ DictionaryMagicID) 49205956

一回のファイルダウンロードの最大サイズに見合わない巨大なファイルを開くからこうなる
解決策は?

char.bin, unk.dic, sys.dic, matrix.bin という順番でファイルを Open していく流れでした。
これについて、それぞれの Open を呼ぶコードを以下の WebRequest コードを挟み込む UniOpen に置き換えていきます。

        public void Open(string fileName)
        {
            using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            using (BinaryReader reader = new BinaryReader(stream))
            {
                this.Open(reader, fileName);
            }
        }

        public void UniOpen(string filePath)
        {
            var r = new System.Text.RegularExpressions.Regex(".*file://.*", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            if (r.IsMatch(filePath))
            {
                UnityEngine.Networking.UnityWebRequest www = UnityEngine.Networking.UnityWebRequest.Get(filePath);
                var asyncOp = www.SendWebRequest();
                while (!asyncOp.isDone)
                {
                    System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(0.01f));
                }
                if (www.isNetworkError || www.isHttpError)
                {
                    UnityEngine.Debug.Log(www.error);
                }
                else
                {
                    UnityEngine.Debug.Log($"isDone {www.isDone} path = {filePath}");
                    byte[] byteArray = www.downloadHandler.data;
                    using (Stream stream = new MemoryStream(byteArray))
                    using (BinaryReader reader = new BinaryReader(stream))
                    {
                        this.Open(reader);
                    }
                }
            }
            else
            {
                Open(filePath);
            }
        }

具体的には CharProperty.cs, MeCabDictionary.cs, Connector.cs の三つの Open 関数の実装に、上のような UniOpen 関数を追加して
これを各種呼び出しの箇所で呼ぶようにしてあげます。

以上

次のテストコードで Android 上でも形態素解析が機能することを確認できました!

using NMeCab;
using UnityEngine;
using UnityEngine.UI;

public class NMeCabTest : MonoBehaviour
{
    public Text text;

    void Start()
    {
        string result = "";
        string sentence = "Unityで形態素解析";
        this.text.text = sentence;
        Debug.Log($"sentence = {sentence}");

        MeCabParam param = new MeCabParam();
        param.DicDir = $"{Application.streamingAssetsPath}/NMeCab/dic/ipadic";

        var t = MeCabTagger.Create(param);
        Debug.Log($"OutPutFormatType = {t.OutPutFormatType}");
        MeCabNode node = t.ParseToNode(sentence);
        while (node != null)
        {
            if (node.CharType > 0)
            {
                Debug.Log(node.Surface + "\t" + node.Feature);
                result += node.Surface + "\t" + node.Feature + "\r\n";
            }
            node = node.Next;
        }
        this.text.text = result;
        Debug.Log("");
    }
}

これにて、記事は完成です。超うれしい。