セマンティックウェブ・ダイアリー

小出誠二の個人的な業務日誌

今までもこのブログでチョビチョビと日本語の問題を取り上げてきたけど(サロゲート問題とかね),いろいろと悩ましいと書いてきた気がする.この辺で一応の決着を(個人的には)つけておこうと思ったのがこのブログ執筆の動機です.というのも.セマンティックウェブ(というか正確にはIRI)や気になっている Common Logic でもその文字コードはUNICODEとなっているし,現在の Common Lisp 処理系も大部分は内部文字コード実装はUNICODEとなっていて,外部環境はほとんど整合的にととのってきているので,一部に表示のバグの問題や,UNICODE正規化の問題が残っているにせよ,そういうマイナーな問題はいずれ時代が解決してくれると信じて,日本語で語彙やオントロジーを開発して Common Lisp で Common Logic やセマンティックウェブ処理系をこれから開発するにあたってその基本的方針を決めておきたいおきたいと思った次第です.

以下のような原則を設けます.

  1. 文字コードはすべてUNICODEとする
  2. UNICODEはすべて正規化形式C(NFC)とする
  3. サロゲートペアは扱わない
  4. IVDも扱わない

原則1によって,IPADICはオリジナルのEUCではなくUTF-8に変換しておくということになります.実は読み込んだり.書き出したりするときにはリーダーの外部formatを自在に指定できるのだけれど,一応UNIODEに限定しておけば,ここで処理系の違いで悩むことはないだろうと思うわけです.もし同一のUTF-8あるいはUTF16ファイルを読み込んで結果が違えば,それは明らかに処理系で正すべきバグでしょうね.

原則2によって,文字比較は単純に文字コード比較になります.つまり異なる文字コードが同じ文字を表すということがない.そもそもRDFでは明確にUNICODEにはNFCを使うと書いてある.特に日本語ファイル名の扱いには注意が必要です.Windowsではファイルの一行目の文字列からファイル名をつくりだしても問題ありませんが,Macではファイル名はNFCではなく,NFDになるので文字列マッチができません.これは何としてもAppleには早く治してほしいところです.

原則3によって,サロゲートペアに所属する文字を扱うことはできません.たとえば𠮷は使えないのでどうしても使いたいときには吉を使うことになります.同様に𠀋を使いたいときにはそのかわりに丈を使うことになります.ちなみに,セマンティックウェブとしては本当はサロゲートペアは容認できるのだけれど,サロゲートペアを受け付けない,加えて私が主に利用しているAllegro Common Lispでは,サロゲートペアは使えません.ただし,UNICODE中にあればサロゲートペアでなければ異体字であっても使えます.たとえば森鴎外でも森鷗外でも使えます.逆にこの両者を区別しないでマッチさせようとすると,そのためのルーチンが必要になります.同様に高田と眦弔Lispで区別して使えますが,両者を区別しないで使おうとすると,そのためのルーチンが必要です.

逆に字形としては異なっていても同一のUNICODEとなるものもあります.代表的なものでは辻(2点つじ)と(1点つじ).これを区別しようとするとUNICODEコードに加えて,字形(グリフ)コードを追加しなければならなくなる.グリフを必要とする漢字は人名に多いのですが,Ideographic Variation Database (IVD)を導入することで識別可能にはなります.私が主戦場としたい機械工学技術関係の分野では,サロゲートペアの必要性は皆無とはいえないが無視できるていどであろう.というわけで原則4にあるようにこれは無視します.いいかえれば,(2点つじ)と(1点つじ)は区別しない.これは葛飾北斎の葛も同様です.住基ネットや戸籍管理アプリではIVDは必須ですので,日本語人名関係のLODでは悩みは深いですね.

 世の中に意外とApache Jena Fusekiをオントロジーサーバーとして,サービスする記事がないことに気が付いて,それならこんな記事でも公開する意味があるかと思い,書いてみます.
 まずは基本的なことですが,Apache JenaとApach Jena Fusekiは別物です.ですから,学会などで「Jenaをオントロジーサーバーにして」などとは,決して言わないように.FusikiはJavaの走るところならどこでも走ります.極端なことを言うと,Rapsberry Pi B+でも組み込みのJavaでOKです.一般のLinuxではまずはJavaをインストールしてください.
 ubuntuなら

$ sudo apt install default-jre
で大丈夫です.

 次にApache Jena Fuseki(バイナリー)をhttps://jena.apache.org/download/#apache-jena-fusekiか,http://ftp.riken.jp/net/apache/jena/binaries/からダウンロードします.

この記事を書いている時点で,最新版はapache-jena-fuseki-3.17.0.tar.gzですね.これ1年前と変わっていない気がする.

$ curl -O https://downloads.apache.org/jena/binaries/apache-jena-fuseki-3.17.0.tar.gz

無事ダウンロードできたらそれを解凍し,/home/<ユーザ>の下にフォルダごと移動し,長い名前を短くfusekiに変えておく.

mv apache-jena-fuseki-3.17.0 fuseki

 fusekiフォルダにあるfuseki-serverを起動すれば,fusekiサーバーが立ち上がる.

$ fuseki/fuseki-server
[2021-02-16 09:54:02] Server     INFO  Apache Jena Fuseki 3.17.0
[2021-02-16 09:54:02] Config     INFO  FUSEKI_HOME=/home/seiji/fuseki
[2021-02-16 09:54:02] Config     INFO  FUSEKI_BASE=/home/seiji/run
[2021-02-16 09:54:02] Config     INFO  Shiro file: file:///home/seiji/run/shiro.ini
[2021-02-16 09:54:03] Server     INFO  System
[2021-02-16 09:54:03] Server     INFO    Memory: 4.0 GiB
[2021-02-16 09:54:03] Server     INFO    Java:   11.0.10
[2021-02-16 09:54:03] Server     INFO    OS:     Linux 5.4.0-65-generic amd64
[2021-02-16 09:54:03] Server     INFO    PID:    6334
[2021-02-16 09:54:03] Server     INFO  Started 2021/02/16 09:54:03 JST on port 3030

サーバーのポート3030にウェブブラウザでアクセスすれば見えるはずだ.

これがいわゆるRDFストアと呼ばれるもので,これに自分の好みのRDFデータをアップロードしておけばそれを全世界に公開することもできる.

 かつて第二次AIブームで一世を風靡したエキスパートシステムですが,これはIF〜THEN〜という形式の知識表現システムでした.それで,今更ながらですが,これが改めて今求められているのですが,そういうAIシステムがなかなか見つからない.ところが最近やっとLisaという名前のシステムを見つけたら,これがなかなか本格的なものなので,このブログで紹介しようと思ったのですが,例題として用いられていたのが,古典的なMonkey and Bananaなのです.Monkey and Bananaなら「Common Lispと人工知能プログラミング」において「第23章 一般問題解決器」において紹介している.そこで,Lisaの紹介の前に「Common Lispと人工知能プログラミング」のその部分を紹介しておこうというわけです.

 以下は「Common Lispと人工知能プログラミング」からの抜粋です.Lisaについてはこのあとのブログ記事で紹介します.


23.2 状態,目標,作用子

 GPS における主要なデータは,今現在の状態を示す *state*,スタートからエンドまで様々な計画段階での目標である goal,それに現在の状態 *state* に働いてそれを変更する作用子 op である.

(defvar *state* nil "The current state: a list of conditions.")

(defstruct op "An operation"
  (action nil) (preconds nil) (add-list nil) (del-list nil))

 *state* は個々の状態を指示するシンボルのリストである.たとえば,古典的な AI 問題である,Monkey & Banana では,最初は,おなかをすかせた猿がおもちゃのボールを持って,ドアの入口に立っており,ドアの近くには椅子があって,部屋の真ん中にはバナナが天井から吊るされている状態である.それをこんな感じのリストで表現する.

初期状態:(at-door on-floor has-ball hungry chair-at-door)

 ゴールも同様にシンボルのリストで表現される.たとえば,Monkey & Banana 問題のゴールは猿がおなかがすいてない状態である.

ゴール:(not-hungry)

 構造体 op のインスタンスとして,個々の作用子を定義する.たとえば,食べるという行為で,おなかがすいていた状態からすいていない状態に変化する作用子は次のようなものである.

eat-banana:(make-op :precond '(has-bananas)
                     :add-list '(empty-handed not-hungry)
                     :del-list '(has-bananas hungry))

 この作用子を適応すると GPS は *state* から del-list に記述してあるシンボルを削除し,add-list にあるシンボルを *state* に加える.precond にはその作用子を適用するのに満たさなければならない条件を書いておく.この場合それは has-bananas である.もちろん手にもっていなければバナナは食べられない.

 初期状態と最終ゴールの記述に加えて,必要十分な作用子をすべて記述して,対象領域の記述が完成する.Monkey & Banana のすべての作用子は以下のとおり.

(defparameter *banana-ops*
  (list
   (make-op :action 'climb-on-chair
            :preconds '(chair-at-middle-room at-middle-room on-floor)
            :add-list '(at-bananas on-chair)
            :del-list '(at-middle-room on-floor))
   (make-op :action 'push-chair-from-door-to-middle-room
            :preconds '(chair-at-door at-door)
            :add-list '(chair-at-middle-room at-middle-room)
            :del-list '(chair-at-door at-door))
   (make-op :action 'walk-from-door-to-middle-room
            :preconds '(at-door on-floor)
            :add-list '(at-middle-room)
            :del-list '(at-door))
   (make-op :action 'grasp-bananas
            :preconds '(at-bananas empty-handed)
            :add-list '(has-bananas)
            :del-list '(empty-handed))
   (make-op :action 'drop-ball
            :preconds '(has-ball)
            :add-list '(empty-handed)
            :del-list '(has-ball))
   (make-op :action 'eat-bananas
            :preconds '(has-bananas)
            :add-list '(empty-handed not-hungry)
            :del-list '(has-bananas hungry))))

 action には eat-bananas とか climb-on-chair といった猿の行為を表すシンボルが与えられるが,これは全く人へのメッセージのために使われるだけであり,GPS から見て重要なものは,事前条件 preconds と,追加リスト add-list および削除リスト del-list に書かれる内容である.

23.3 GPS はどのように働くのか?(第1版)

 前章と同様に,GitHubリポジトリから GPS システムをダウンロードし,まずは GPS1 を実行してみる.GPS1の第1引数が初期状態,第2引数がゴール,第3引数が作用子のリストである.

最初,猿はドアの入口に立っていて(at-door),手にボールを持って(has-ball),床の上にたっている(on-floor).ドアのそばには椅子がある(chair-at-door).ためしに GPS(第1版) を動かしてみると,以下のように実行される.以下すべての実行において,カレントパッケージを:gpsにすることに注意されたい.

cl-user(9): (in-package :gps)
gps(10): (GPS1 '(at-door on-floor has-ball hungry chair-at-door)
               '(not-hungry)
               *banana-ops*)

(executing push-chair-from-door-to-middle-room) 
(executing climb-on-chair) 
(executing drop-ball) 
(executing grasp-bananas) 
(executing eat-bananas) 
solved

 猿は椅子をドアの場所から部屋の真ん中まで持っていき,椅子に登って,ボールを手から落とし,バナナをつかみ,それを食べる.これで猿の状態はnot-hungryになった.

 GPS の動きをしっかりとつかむには,STRIPS 流の探索木(search tree)を書くとよい.探索木のトップのノードには [S0, G0] を置く.ここで S0 は初期状態,G0 はトップのゴールだけを含むリストである.もしゴールの要素がすべて状態に含まれていればよいが普通はそんなことはない.作用子の precond がすべて状態に含まれているものがあれば,即時にその作用子を実行できるが,そのようなものが無ければ実行したらゴールを満足するような作用子(関連する作用子)を見つけ出し,その precond を新たなゴールとして G0 のリストの先頭にプッシュしてそれを G1 とする.S0 は変わらない.図23.1 に Monkey & Banana の探索木を示すが,ゴールとして与えられた not-hungry について即時に実行できる作用子はないのでとりあえず関連する作用子 eat-bananasprecond を追加したノードが図に示されている.


省略

図23.1 Monkey & Banana 探索木

 今度はその新たなゴール(サブゴール)を達成しようとする.今はそれは has-bananas である.同様にして,has-bananas が状態にあれば即作用子を実行できるが,なければまた関連する作用子を見つけ出し,その precond を再び新たなゴールとして G1 にプッシュする.それが G2 になる.同様なことを繰り返して push-chair-from-door-to-middle-room が関連する作用子となったときには,それを即時実行できる状態ということが判明し,それを実行する.ここで実行するとは,状態から del-list の要素を取り除き,add-list の要素を追加することである.それを状態 S1 とする.状態が変わったことで,以前は実行できなくてゴールとしたものが実行できるようになるかもしれない.そういう場合はそれを即時実行する.以後同様である.

 最初に Norvig による GPS の簡単なバージョン(第1版)を示す.

(defvar *ops* nil "A list of available operators.")

(defun GPS1 (*state* goals &optional (*ops* *ops*))
  "General Problem Solver: achieve all goals using *ops*."
  (if (every #'achieve1 goals) 'solved)))

 関数 GPS1 では複数のゴールをもらったら,それをすべて達成しようとする.すべてのゴールが達成されたときは,solved を返す.

(defun achieve1 (goal)
  "A goal is achieved if it already holds,
  or if there is an appropriate op for it that is applicable."
  (or (member goal *state*)
      (some #'apply-op1 
            (find-all goal *ops* :test #'appropriate-p1))))

(defun appropriate-p1 (goal op)
  "An op is appropriate to a goal if it is in its add list."
  (member goal (op-add-list op)))

 achieve1 はゴール一つを受け取って,それが *state* にあれば達成済みとしてすぐに帰るが,なければそのゴール達成に直接に関連する作用子をすべて取り出して,そのどれか一つでも使ってゴールが達成できればよしとする.ゴール達成に直接関連する作用子とはゴールをすぐ達成するのに適切なもので,それはそのゴールが作用子の add-list に含まれるもののことである.

(defun apply-op1 (op)
  "Print a message and update *state* if op is applicable."
  (when (every #'achieve1 (op-preconds op))
    (print (list 'executing (op-action op)))
    (setf *state* (set-difference *state* (op-del-list op)))
    (setf *state* (union *state* (op-add-list op)))
    t))

 apply-op1 は受け取った作用子の preconds をサブゴールとして,そのすべてを達成しようとする.関数 GPS1では (every #'achieve1 goals) となっていたが,ここでは (every #'achieve1 (op-preconds op)) となっていることに注意されたい.達成できたときは executing ... とメッセージを印刷し,*state* から del-list の内容を削除し,add-list の内容を追加する.

このページのトップヘ