LoginSignup
13
14

More than 5 years have passed since last update.

Ruby: Prologで翻訳「時の蝿はひとつの矢を好む」

Last updated at Posted at 2015-01-21

はじめに

Prolog サーバに Ruby から問い合わせをして簡単な翻訳(英訳/和訳)をしてみました。
現在翻訳できる文章は『time flies like an arrow』です。

動作概要は、以下です。

  1. Prolog サーバを起動する
  2. DCG ファイル(文法・語彙)をサーバにロードして、サーバに翻訳(構文解析)をサービスさせる
  3. サーバに問い合わせを行い翻訳させる

Prolog でサーバを立てる、は @GRGSIBERIA さんの以下の記事のアイデアです。

Prolog 側では DCG(限定節文法)を使って翻訳をしています。
DCG に関しては以下のリンクの内容を参考にしています。

ミニ Prolog パーサについては、以下の拙稿のものを流用しています。


動作環境は以下です。
Prolog 処理系は SWI-Prolog、形態素解析には MeCab、Natto を使用しています。

  • Ubuntu Linux 14.04
    • Ruby 2.1.5 p273
    • SWI-Prolog version 6.6.4 (「sudo apt-get install swi-prolog」でインストール)
    • MeCab 0.996 (「sudo apt-get install mecab libmecab-dev」でインストール)
    • (gem) mecab 0.966 (「gem install mecab」 でインストール)
    • (gem) natto 0.9.7 (「gem install natto」 でインストール)
    • (gem) pry 0.10.1 (「gem install pry」 でインストール)
    • 作成したスクリプト類
  • Mac OS X 10.10.1 Yosemite
    • Ruby 2.0.0 p481
    • (gem) pry 0.10.1

スクリプト類については本稿末尾に掲載します。

翻訳してみる

translator-demo-1.png

作業は rake で行います。(rake のタスクは以下)

rake daemon[drb_uri,port,host,encoding]  # drb デーモンとして起動
rake en[sentence]                        # 翻訳: 日本語 -> 英語
rake jp[sentence]                        # 翻訳: 英語 -> 日本語
rake load[files]                         # ファイルのロード
rake server[port,server_program]         # Prolog サーバの起動

rake の server、load タスクで、Prolog サーバの起動、DCG ファイルのロードをします。

:
: server は 'server.pro' があるディレクトリで、
: load は 'dcg-*.pro' があるディレクトリで実行します。
:

$ rake server           # サーバが起動されます(ポート 53330/tcp を LISTEN)
   :
$ rake load
   :                    # DCG がロードされて応答メッセージがたくさん出ます
: ポート 53330/tcp が LISTEN されているか確認
$ ss -tl | grep -w 53330
tcp    LISTEN     0      128                  *:53330                 *:*

これで、サーバが翻訳サービスをするようになりました。
このターミナルはサーバのメッセージが出るので、以後の作業は別のターミナルで行います。

rake の jp、en タスクで翻訳してみます。
翻訳できる文章は『time flies like an arrow』です。

$ rake "jp[time flies like an arrow]"         # 和訳。 結果は以下の3つ
時間はひとつの矢のように飛ぶ
時の蝿はひとつの矢を好む
蝿をひとつの矢のように拍子を合わせる

$ rake "en[時間はひとつの矢のように飛ぶ]"          # 英訳(1)
time flies like an arrow

$ rake "en[時の蝿はひとつの矢を好む]"             # 英訳(2)
time flies like an arrow

$ rake "en[蝿をひとつの矢のように拍子を合わせる]"   # 英語(3)
time flies like an arrow

サーバを止めるには swipl (SWI-Prolog のプロセス)を kill するなどします。
プロセスを見つけるには、ps、pidof コマンドなどが役に立ちます。

$ ps aul | grep -w [s]wipl  # swipl のプロセス情報を抽出表示

$ pidof swipl               # swipl のプロセスの PID 表示 (PID は複数表示がありえる)

$ kill 12345                # PID 12345(例) のプロセスを kill

$ killall swipl             # (注) この場合、起動している全ての swipl プロセスが kill されます

訳をもっとマシにしたい、語彙を増やしたい、別の文法を訳せるようにしたいなどを実現するには、DCG ファイル(本稿文末に掲載)をもっと拡充していく必要があります。

現状では『time flies like an arrow』に関連する文法・語彙ならば適当に翻訳できます。

$ rake "jp[arrow flies]"
矢は飛ぶ

$ rake "en[時の矢]"
time arrow

翻訳してみる (その2: pryで)

translator-demo-2.png

その1 では、rake の jp/en タスクで翻訳しましたが、タスクの実体は、Prolog::Translator のメソッドを呼び出しです。
pry 等で翻訳させることもできます。(この方法が一番簡単でした)

: rake server
: rake load
: ... の後に、

$ pry -r prolog_translator             # prolog_translator を require する
            :

[1] pry(main)> cd Prolog::Translator

[2] pry(Prolog::Translator):1> jp "time flies like an arrow"
=> ["時間はひとつの矢のように飛ぶ", "時の蝿はひとつの矢を好む", "蝿をひとつの矢のように拍子を合わせる"]

[3] pry(Prolog::Translator):1> en "時間はひとつの矢のように飛ぶ"
=> ["time flies like an arrow"]
            :

ちなみに、Prolog::Proxy も、節の登録(ins/insert)、問い合わせ(q/inquire)などのモジュールメソッドを持っているので、同じ要領でサーバへの操作ができます。

[16] pry(Prolog::Translator):1> cd -                      # main に戻って

[17] pry(main)> cd Prolog::Proxy                          # Proxy に cd して

[18] pry(Prolog::Proxy):1> ins "person(plato)"            # 事実の登録
=> "[assert(person(plato))]"

[19] pry(Prolog::Proxy):1> ins "mortal(X) :- person(X)"   # 規則の登録
=> "[assert((mortal(_G61):-person(_G61)))]"

[20] pry(Prolog::Proxy):1> q "mortal(Who)"                # 問い合わせ
=> "[mortal(plato)]"

Prolog::Translator オブジェクトは通信用に Prolog::Proxy オブジェクトを持っているのでモジュールメソッドでなく、インスタンスをつくってインスタンスメソッドで操作することもできます。
Prolog::Proxy オブジェクトは Prolog::Translator#proxy で参照できます。

[1] pry(main)> cd Prolog::Translator

[2] pry(Prolog::Translator):1> tr = new
=> ...

[3] pry(Prolog::Translator):1> tr.en "時の蝿はひとつの矢を好む"
=> ["time flies like an arrow"]

[4] pry(Prolog::Translator):1> tr.proxy.ins "person(socrates)"
=> "[assert(person(socrates))]"

[5] pry(Prolog::Translator):1> tr.proxy.q "person(Who)"
=> "[person(socrates)]"

翻訳してみる (その3: リモートから)

translator-demo.png

Prolog::Translator.#daemon で Prolog::Translator オブジェクトをサービスする drb サーバをデーモンとして起動できます。
これは rake の daemon タスクで行えます。

: rake server
: rake load
: ... の後に、

$ rake daemon        # drb デーモンが起動する

Prolog::Translator のデフォルトのサービス URI は「druby://:53333」です。
例えば、別のターミナルの pry で以下のようにできます。

$ pry -r drb                          # drb を require する
   :
[1] pry(main)> DRb.start_service      # DRbサービスを起動する
=> ...
        :
[2] pry(main)> tr = DRbObject.new_with_uri 'druby://:53333' # リモートオブジェクト取得
=> ...

[3] pry(main)> tr.jp "time flies like an arrow"
=> ["時間はひとつの矢のように飛ぶ", "時の蝿はひとつの矢を好む", "蝿をひとつの矢のように拍子を合わせる"]
                 :

リモートホストの pry からアクセスする場合は druby の URI に drb サーバ側の IPアドレス(あるいは名前解決できるホスト名)を入れてリモートオブジェクトを取得します。(以下は例です)

tr = DRbObject.new_with_uri 'druby://192.168.1.101:53333'
                                     ^^^^^^^^^^^^^

サーバを Linux、pry を Mac OS X でテスト実行してみました。
リモート側には prolog_translator.rb 等のインストールは不要です。drb は Ruby 標準添付なので、ネットワークの(Ruby がインストールされた)別ホストから翻訳を利用することができます。

テスト実行の時、ファイアウォールのブロックが効いていると通信できませんでした。
私の Linux 環境のファイアウォールを一旦無効にしました。

: テスト実行直前
$ sudo ufw disable          # 私は ufw を使っています
ファイアウォールを無効にし、システム起動時にも無効にします

: テスト実行終了後
$ sudo ufw enable
ファイアウォールはアクティブかつシステムの起動時に有効化されます。

drb 経由だとモジュールメソッドが使えません。
Prolog サーバの操作をする場合は、Prolog::Translator オブジェクトが持っている Prolog::Proxy オブジェクトを利用します。
Prolog::Proxy オブジェクトは Prolog::Translator#proxy で参照できます。

[4] pry(main)> tr.proxy.q "mortal(Who)"   # 問い合わせ
=> "[mortal(plato)]"                      # この結果は、既に事実・規則が登録済の場合

デーモンを止めるには rake プロセスを kill するなどします。
プロセスを見つけるには、ps、pidof コマンドなどが役に立ちます。

$ ps aul | grep -w [r]ake   # rake のプロセス情報を抽出表示

$ pidof rake                # rake のプロセスの PID 表示 (PID は複数表示がありえる)

$ kill 12345                # PID 12345(例) のプロセスを kill

$ killall rake              # (注) この場合、起動している全ての rake プロセスが kill されます

スクリプト

  • Rakefile
  • Ruby スクリプト (*.rb)
    • prolog_tranlator.rb
    • prolog_proxy.rb
    • prolog_parser.rb
  • Prolog スクリプト (*.pro)
    • server.pro
    • dcg-en-grammar.pro
    • dcg-en-vocabulary.pro
    • dcg-jp-grammar.pro
    • dcg-jp-vocabulary.pro

Ruby スクリプトは require できるディレクトリ($RUBYLIBで指定されるディレクトリなど)に置きます。
Rakefile と Prolog スクリプトは作業するカレントディレクトリに置きます。


Rakefile
require 'prolog_translator'

SERVER = 'server.pro'
PORT   = Prolog::Proxy::PORT

#
# Prolog サーバへの起動と設定
#
desc 'Prolog サーバの起動'
task:server, [:port, :server_program] do |t, args|
  port   = args[:port]           || PORT
  server = args[:server_program] || SERVER
  Process.spawn "swipl -l #{server} -g 'create_server(#{port}).'"
end

desc 'ファイルのロード'
task:load, [:files] do |t, args|
  files = (args[:files]||'').split(',')
  Prolog::Translator.load files
end

desc 'drb デーモンとして起動'
task:daemon, [:drb_uri, :port, :host, :encoding] do |t, args|
  Prolog::Translator.daemon(args[:drb_uri]) do |my|
    my.port     = args[:port]     if args[:port]
    my.host     = args[:host]     if args[:host]
    my.encoding = args[:encoding] if args[:encoding]
  end
end

desc '翻訳: 英語 -> 日本語'
task:jp, [:sentence] do |t, args|
  Prolog::Translator.jp(args[:sentence] || '').each {|j| puts j }
end

desc '翻訳: 日本語 -> 英語'
task:en, [:sentence] do |t, args|
  Prolog::Translator.en(args[:sentence] || '').each {|e| puts e }
end

prolog_translator.rb
require 'drb'
require 'natto'
require 'prolog_proxy'

module Prolog
  class Translator
    include DRb::DRbUndumped

    DRB_URI = 'druby://:53333'

    DCG_FILES = %w(dcg-en-grammar.pro dcg-en-vocabulary.pro
                   dcg-jp-grammar.pro dcg-jp-vocabulary.pro)

    class << self
      def load(files=nil, dir=nil, *args)
        new(*args).load(files, dir)
      end

      def jp(sentence, *args)
        new(*args).jp(sentence)
      end

      def en(sentence, *args)
        new(*args).en(sentence)
      end

      def drb(uri=nil, *args, &block)
        DRb.start_service (uri || DRB_URI), new(*args, &block)
        DRb.thread.join
      end

      def daemon(*args, &block)
        Process.daemon
        drb *args, &block
      end
    end

    attr_writer :proxy, :port, :host, :encoding

    def port     ; @port     ||= Prolog::Proxy::PORT     ; end
    def host     ; @host     ||= Prolog::Proxy::HOST     ; end
    def encoding ; @encoding ||= Prolog::Proxy::ENCODING ; end

    def proxy
      @proxy ||= Prolog::Proxy.new(port:port, host:host, encoding:encoding)
    end

    def initialize(proxy:nil, port:nil, host:nil, encoding:nil, &block)
      tap {|my| my.proxy, my.host, my.port, my.encoding =
                   proxy,    host,    port,    encoding }

      instance_eval(&block) if block
    end

    # DCG ファイルのロード
    def load(files=nil, dir=nil)
      files = DCG_FILES if !files || files.empty?
      Prolog::Proxy.load(files, dir)
    end

    # 翻訳: 英語 -> 日本語
    def jp(sentence)
      ss    = sentence.split.map(&:to_sym)  
      query = {translate:[var(:J), ss, var()]}  # 述語 translate/3
     Prolog.to_ruby(proxy.inquire(ruby:query)).map{|pred| arg(pred,0).join }
    end

    # 翻訳: 日本語 -> 英語
    def en(sentence)
      ss    = jsplit(sentence).map(&:to_sym)
      query = {translate:[ss, var(:E), var()]}  # 述語 translate/3
      Prolog.to_ruby(proxy.inquire(ruby:query)).map{|pred| arg(pred,1) * ' '}
    end

    private
    def var(sym=:_)
      { nil => sym.to_sym }
    end

    def arg(pred, n)
      pred.values.first[n]
    end

    def jsplit(s)
      ss = []
      Natto::MeCab.new.parse(s) {|n| ss << n.surface }
      ss.compact
    end
  end
end

if __FILE__ == $0
  #
  # $ ruby prolog_translator.rb --drb [options] [URI]          # drb server
  #
  # $ ruby prolog_translator.rb --drb --daemon [options] [URI] # drb daemon
  #
  require 'ostruct'  
  require 'optparse'  

  opt = OpenStruct.new ARGV.getopts '', 'drb', 'daemon',
                                        'port:', 'host:', 'encoding:'
  uri = ARGV.shift

  if opt.drb
    Prolog::Translator.send(opt.daemon ? :daemon : :drb, uri) do |my|
      my.port     = opt.port     if opt.port
      my.host     = opt.host     if opt.host
      my.encoding = opt.encoding if opt.encoding
    end
  end
end

※ prlog_proxy.rb は「Ruby: ミニ Prolog パーサ (改)」のものよりアップデートされています。

prolog_proxy.rb
require 'drb'
require 'socket'
require 'timeout'
require 'resolv-replace'
require 'prolog_parser'

module Prolog
  def self.connect(*args, &block)
    Proxy.new(*args, &block)
  end

  class Proxy
    include DRb::DRbUndumped

    DRB_URI = 'druby://:53331'

    PORT     = 53330
    HOST     = 'localhost'
    ENCODING = 'EUC-JP'     # SWI-Prolog向け設定

    class << self
      def inquire(query, *args, &enc)
        new(*args).inquire(query, &enc)
      end
      alias q inquire

      def insert(clause, *args, &enc)
        new(*args).insert(clause, &enc)
      end
      alias ins insert

      def load(files, dir=nil, *args, &enc)
        new(*args).load(files, dir)
      end

      def remove(clause, *args, &enc)
        new(*args).remove(clause, &enc)
      end

      def drb(uri=nil, *args, &block)
        DRb.start_service (uri || DRB_URI), new(*args, &block)
        DRb.thread.join
      end

      def daemon(*args, &block)
        Process.daemon
        drb *args, &block
      end
    end

    attr_writer :port, :host, :encoding
    attr_accessor :tmout

    def port     ; @port     ||= PORT     ; end 
    def host     ; @host     ||= HOST     ; end 
    def encoding ; @encoding ||= ENCODING ; end 

    def initialize(port:nil, host:nil, encoding:nil, &block)
      tap {|my| my.host, my.port, my.encoding =
                   host,    port,    encoding }

      instance_eval(&block) if block
    end

    # Prolog サーバのデータベースに節を問い合わせをする
    def inquire(query=nil, ruby:nil, &enc)
      query = Prolog.to_prolog(ruby) if ruby

      timeout(tmout) do
        TCPSocket.open(host, port) do |s|
          s.puts "#{enc_to(query, &enc)}."
          enc_from(s.gets, &enc)
        end
      end
    end
    alias q inquire

    # Prolog サーバのデータベースに節を追加する
    def insert(clause=nil, ruby:nil, &enc)
      clause = Prolog.to_prolog(ruby) if ruby
      query = dcg?(clause) ? "dcg_translate_rule((#{clause}),X),assert(X)"
                           : "assert((#{clause}))"
      inquire(query, &enc)
    end
    alias ins insert

    # Prolog サーバのデータベースにファイルの内容(節)を追加する
    def load(files, dir=nil)
      files.map{|fname| File.join(dir || '.', fname)}.each do |file|
        Prolog.parse(file:file).map {|term| insert(ruby:term) }
      end
    end

    # Prolog サーバのデータベースから節を削除する
    def remove(clause=nil, ruby:nil, &enc)
      clause = Prolog.to_prolog(ruby) if ruby
      query = dcg?(clause) ? "dcg_translate_rule((#{clause}),X),retract(X)"
                           : "retract((#{clause}))"
      inquire(query, &enc)
    end

    private
    def enc_to(s, &block)
      enc = block ? block.() : encoding
      (s && enc) ? s.encode(enc) : s
    end

    def enc_from(s, &block)
      enc = block ? block.() : encoding
      (s && enc) ? s.encode('UTF-8', enc) : s
    end

    def dcg?(clause)
      # TODO: もう少し厳密な判定に
      clause =~ /-->/
    end
  end
end

if __FILE__ == $0
  #
  # $ ruby prolog_proxy.rb --drb [options] [URI]          # drb server
  #
  # $ ruby prolog_proxy.rb --drb --daemon [options] [URI] # drb server (daemon)
  #
  require 'ostruct'
  require 'optparse'

  opt = OpenStruct.new ARGV.getopts '', 'drb', 'daemon',
                                        'port:', 'host:', 'encoding:'
  uri = ARGV.shift

  if opt.drb
    Prolog::Proxy.send(opt.daemon ? :daemon : :drb, uri) do |my|
      my.port     = opt.port     if opt.port
      my.host     = opt.host     if opt.host
      my.encoding = opt.encoding if opt.encoding
    end
  end
end

  • prolog_parser.rb

このスクリプトに関しては GitHub: Mini Prolog Parser (for Ruby with Racc) にある prolog_parser.ry を racc でコンパイルして作成します。(スクリプトは随時アップデートされています)

:
: racc で prolog_parser.ry から prolog_parser.rb をコンパイル
:
$ racc -g -o prolog_parser.rb prolog_parser.ry

racc は Ruby 標準添付のパージェネレータ Racc のコマンドです。


各 DCG プログラムに関しては、「AZ-Prolog マニュアル」「Prologプログラミング: 言語処理」の内容を元にして作成させていただいています。

dcg-en-grammar.pro
s(s(NP,VP))     --> np(NP), vp(VP).         % 文       -> 名詞句 動詞句
s(s(NP))        --> np(NP).                 % 文       -> 名詞句
s(s(VP))        --> vp(VP).                 % 文       -> 動詞句
np(np(N))       --> n(N).                   % 名詞句   -> 名詞
np(np(A,N))     --> adj(A), n(N).           % 名詞句   -> 形容詞 名詞
np(np(D,N))     --> det(D), n(N).           % 名詞句   -> 冠詞 名詞
vp(vp(V))       --> v(V).                   % 動詞句   -> 動詞
vp(vp(V,NP))    --> v(V), np(NP).           % 動詞句   -> 動詞 名詞句
vp(vp(V,PP))    --> v(V), pp(PP).           % 動詞句   -> 動詞 前置詞句
vp(vp(V,NP,PP)) --> v(V), np(NP), pp(PP).   % 動詞句   -> 動詞 名詞句 前置詞句
pp(pp(P,NP))    --> prep(P), np(NP).        % 前置詞句 -> 前置詞 名詞句
dcg-en-vocabulary.pro
n(n(time))       --> [time].
n(n(flies))      --> [flies].
n(n(arrow))      --> [arrow].
v(v(time))       --> [time].
v(v(flies))      --> [flies].
v(v(like))       --> [like].
adj(adj(time))   --> [time].
prep(prep(like)) --> [like].
det(det(an))     --> [an].
dcg-jp-grammar.pro
js(s(NP,VP))     --> jnp(NP),['は'],jvp(VP).        % 文       -> 名詞句 'は' 動詞句
js(s(NP))        --> jnp(NP).                       % 文       -> 名詞句
js(s(VP))        --> jvp(VP).                       % 文       -> 動詞句
jnp(np(N))       --> jn(N).                         % 名詞句   -> 名詞
jnp(np(A,N))     --> jadj(A), jn(N).                % 名詞句   -> 形容詞 名詞
jnp(np(D,N))     --> jdet(D), jn(N).                % 名詞句   -> 冠詞 名詞
jvp(vp(V))       --> jv(V).                         % 動詞句   -> 動詞
jvp(vp(V,NP))    --> jnp(NP),['を'],jv(V).          % 動詞句   -> 名詞句 'を' 動詞
jvp(vp(V,PP))    --> jpp(PP),jv(V).                 % 動詞句   -> 前置詞句 動詞
jvp(vp(V,NP,PP)) --> jnp(NP),['を'],jpp(PP),jv(V).  % 動詞句   -> 名詞句 'を' 前置詞句 動詞
jpp(pp(P,NP))    --> jnp(NP),jprep(P).              % 前置詞句 -> 名詞句 前置詞句

translate(Japanese, English, Semantics) :-
    s(Semantics, English, []),
    js(Semantics, Japanese, []).
dcg-jp-vocabulary.pro
jn(n(time))       --> ['時間'].
jn(n(flies))      --> ['蝿'].
jn(n(arrow))      --> ['矢'].
jv(v(time))       --> ['拍子'],['を'],['合わせる'].
jv(v(flies))      --> ['飛ぶ'].
jv(v(like))       --> ['好む'].
jadj(adj(time))   --> ['時'],['の'].
jprep(prep(like)) --> ['の'],['よう'],['に'].
jdet(det(an))     --> ['ひとつ'],['の'].

server.proは「PrologなNoSQLサーバを立てる」のものを流用させていただいています。

server.pro
create_server(Port) :-
    tcp_socket(Socket),
    tcp_setopt(Socket, reuseaddr),
    tcp_bind(Socket, Port),
    tcp_listen(Socket, 5),
    tcp_open_socket(Socket, AcceptFd, _),
    dispatch(AcceptFd).

dispatch(AcceptFd) :-
    tcp_accept(AcceptFd, Socket, Peer),
    thread_create(process_client(Socket, Peer), _, [detached(true)]),
    dispatch(AcceptFd).

process_client(Socket, _) :-
    setup_call_cleanup( tcp_open_socket(Socket, In, Out),
                        handle_service(In, Out),
                        close_connection(In, Out)).

close_connection(In, Out) :-
    close(In, [force(true)]),
    close(Out, [force(true)]).

handle_service(In, Out) :-
    read(In, Chars),
    findall(Chars, Chars, L),
    write(Out, L),
    writeln(L).
13
14
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
13
14