Edited at

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

More than 3 years have passed since last update.


はじめに

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



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


翻訳してみる

作業は 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で)

その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: リモートから)

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).