0

ไปฟังเรื่อง Statistical machine translation ของ CAS-ICT

ตามที่ได้เมล์มาจาก http://groups.google.com/group/thlta/browse_thread/thread/633acafdd560b738 มีบรรยายเรื่อง SMT ที่ CAS-ICT โดย Prof. Dr. LIU Qun วันนี้ก็ได้ไปฟังมาแล้ว ได้ความรู้มากกว่าที่คาดไว้อีก เรื่องแปลโดยใช้ Forest ผมโหลดไฟล์มาแล้ว แต่ยังไม่ได้อ่าน พอไปฟังแล้วก็จับประเด็นได้เลย ^_^

จัด 10 โมงเช้าเวลากำลังดีครับ แต่ก็อย่าที่ทราบกันว่า อุทยานวิทยาศาสตร์ ออกจะไกลสักหน่อย ผมก็เลยตื่นมาตั้งแต่ 6 โมงครึ่ง แล้วก็ไม่รู้ทำอะไรอยู่กว่าจะได้ออกจากบ้างก็ 8 โมง แต่ก็ไปทัน

PA227457.JPG

ให้ดูว่าผมไปทันด้วย :-P

ไม่นานนัก ดร.เทพชัย ก็เปิดงาน

PA220003.JPG

เริ่มบรรยายแล้ว

Prof. Dr. Lui Qun

ภาพของบนใช้โทรศัพท์มือถือถ่าย แต่ว่าสิ่งที่สนใจคือ มี Vauquois triangle บน slide ด้วย แต่ว่าเขียนมาเพื่อใช้อธิบาย SMT เวลานี้

PA227461.JPG

Prof. Dr. LIU Qun แบ่งระดับการวิเคราะห์ที่ใช้ในการแปลเป็น 2 ระดับ คร่าวๆ คือ Phrase-based SMT และ Syntax-based SMT

Syntax-based SMT ยังแบ่งได้เป็น 2 ระดับย่อยๆ อีกคือ แบบที่ใช้ linguistic syntax กับ formal syntax แบบ linguistic syntax คือใช้ต้นไม้ที่อ้างอิงความรู้ทางภาษาศาสตร์ ส่วน formal syntax ก็ออกมาเป็น tree เหมือนกัน แต่ tree ที่สร้างไม่จำเป็นต้องเป็น tree ที่ถูกต้องตามหลักภาษาศาสตร์

SMT ที่แปลง tree แบบ linguistic syntax ไปเป็น อีกข้อความอีกภาษาเรียกว่า Tree-to-String SMT

ถ้าผมเข้าใจผิดก็ช่วยทักท้วงด้วยนะครับ

PA227463.JPG

ตามธรรมเนียมก็มอบของที่ระลึกกันไป ขอโทษที่ไม่ชัดนะครับ

Sunlong bus

เสร็จงานแล้วผมก็นั่งรถเมล์ ปอ. 29 กลับมาจากธรรมศาสตร์ ศูนย์รังสิต ครับ 20 บาท ถ้วน ถูกกว่า 510 ขาไป 4 บาท ขากลับรถเมล์ก็มาจากจีนครับยี่ห้อ Sunlong

นอกจากเรื่องวิชาการแล้วแนวการทำงานวิจัย CAS-ICT ก็ดูเน้นทางวิทยาการคอมพิวเตอร์มากกว่าภาษาศาสตร์ ถ้าผมจำไม่ผิดที่นั่นมีนักศึกษาปริญญาเอก ประมาณ 10 คน ปริญญาโทอีกประมาณ 10 คน software engineer อีก 3 คน ไม่มีนักภาษาศาสตร์เลย แม้แต่คนเดียว ผมคิดว่าที่ทำแบบนี้ได้น่าจะเป็นเพราะภาษาจีนมีทรัพยากรณ์ทางภาษาเช่น คลังต้นไม้ (Treebank) และ คลังข้อความขนาน (Parallel corpus) มากพอ งานที่นักภาษาศาสตร์ก็อยู่ในคลังต้นไม้อยู่แล้ว

งานที่น่าสนใจอีกอย่าง เขาทำระบบแปลสิทธิบัตรด้วย ที่พิเศษคือผู้ใช้ สามารถสร้าง template ได้เอง template ก็คล้ายๆ กับเป็นกฎแบบหนึ่ง ลักษณะประมาณนี้

c1 c2 c3 $X c4 $Y c5 c6
e1 $Y e2 $X e3 e4

โดยที่ e1, e2, e3 … เป็นคำภาษาอังกฤษ และ c1, c2, c3 … เป็นคำภาษาจีน ส่วน $X และ $Y ก็เป็นตัวแปรที่จะเปลี่ยนไปได้เรื่อยๆ (ผมเขียนแบบนี้เพราะจำตัวอย่างชัดๆ ไม่ได้)

ทุกภาพในบันทึกนี้สามารถคลิกดูภาพใหญ่ได้ครับ ทุกภาพใช้สัญญาอนุญาตแบบ Attribution-Noncommercial-No Derivative Works 2.0 Generic (คร่าวแล้วก็คือเอาไปใช้ได้นั่นเองครับ ส่วนรายละเอียดสามารถตามลิงค์ไปอ่านได้)

0

Lexical score ใน Moses

ใน moses นี้ train-factored-phrase-model.perl เป็นโปรแกรมหลักในการคิดคะแนน ต่างๆ ของ phrase

lexical score เริ่มจากนับๆ คำก่อน อันนี้ก็ทำใน train-factored-phrase-model.perl เลยใน sub routine ชื่อ get_lexical

แต่ว่าตอนคิดคะแนนของแต่ละ phrase ไปทำใน score.cpp แทน แต่ว่าก็อ่าน lexical table จาก ที่สร้างไว้ใน get_lexical แล้ว ไฟล์ชื่อประมาณ lex….f2n และ lex…..n2f พออ่านมาได้แล้วก็คิดคะแนนกันในตอนท้ายๆ ของ processPhrasePairs ใน score.cpp

0

Moses hypothesis (search graph)

We can get Moses search graph by using the option -output-search-graph. Its format is explained at http://www.statmt.org/moses/?n=Moses.AdvancedFeatures. However after I have read it, I still did not understand many thing especially “covered” and “stack”. (“recombined” is not in scope of this post.) Both “covered” and “stack” are related covered foreign (source) language words. Stack is number of covered words and “covered” is referred to start and end position of covered words. These are trivial. However probably because of my English is bad, from the explanation, I did not understand that “covered” referred to “current” or “latest” translated words but “stack” is referred translated words including previous (ancestor) hypotheses too.

Example:
0 hyp=859 stack=2 back=80 score=-7.85572 transition=-1.97807 forward=11192 fscore=-16.5095 covered=4-4 out=ที่

0 hyp=11 stack=1 back=0 score=-5.63929 transition=-5.63929 forward=160 fscore=-15.6831 covered=0-0 out=ฉัน ไม่ รู้

0 hyp=80 stack=1 back=0 score=-5.87765 transition=-5.87765 forward=880 fscore=-15.1593 covered=3-3 out=กิน อาหาร

0 hyp=859 stack=2 back=80 score=-7.85572 transition=-1.97807 forward=11192 fscore=-16.5095 covered=4-4 out=ที่


hypothesis_m

For example, hypothesis 859′s stack=2 since it is include the word “eat” (3-3) from hypothesis 80 too. Previous (ancestor) hypotheses can be traced by looking at “back” (back pointer). “out” is translated string (in target language). It is directly corresponded to “covered” since “out” does not contain translated string from previous hypotheses but only current one.

In brief, “out” and “covered” is about current translation made by a hypothesis only but “stack” is about current + what are inherited from previous hypotheses (ancestors). And previous hypotheses can be traced by “back”.

0

Reading phrase table (for Moses) using Python

I’m going to analyze phrase table that is generated by Moses. So I have studied phrase table format from http://www.statmt.org/moses/?n=FactoredTraining.ScorePhrases and written a Python script for reading a phrase table into Python dict. The code is as follow.

import re

def _decode_tokens(field):
    return filter(lambda t: t != '', re.split(" ", field))

def _decode_link(link):
    m = re.match("\((.*)\)", link)
    if m:
        toks = filter(lambda l: l != '', re.split(",", m.group(1)))
        return map(lambda l: int(l), toks)
    else:
        raise RuntimeError

def _decode_links(field):
    links = filter(lambda t: t != '', re.split(" ", field))
    return map(_decode_link, links)

def _decode_num(field):
    toks = filter(lambda t: t != '', re.split(" ", field))
    return map(lambda tok: float(tok), toks)

def read_phrase_table(filename):
    NUM_FIELD = 5
    for i, line in enumerate(open(filename)):
        fields = re.split("\|\|\|", line.strip())
        if len(fields) != NUM_FIELD:
            raise RuntimeError
        phrase = {}
        phrase['source'] = _decode_tokens(fields[0])
        phrase['target'] = _decode_tokens(fields[1])
        phrase['links'] = _decode_links(fields[2])
        phrase['rev_links'] = _decode_links(fields[3])
        nums = _decode_num(fields[4])
        phrase['phrase_trans_prob'] = nums[0]
        phrase['lex_weight'] = nums[1]
        phrase['rev_phrase_trans_prob'] = nums[2]
        phrase['rev_lex_weight'] = nums[3]
        phrase['phrase_penalty'] = nums[4]
        yield phrase

def main():
    for phrase in read_phrase_table("phrase-table.0-0"):
        print phrase

if __name__ == '__main__':
    main()
1

Simple + dirty Python binding for Moses (SMT decoder)

I want to call Moses from python like below:

from moses import Moses
m = Moses("enth/model/moses.ini")
print m.decode("i eat rice")
# result: ฉัน กิน ข้าว

so i create this extension.

moses.cpp:
#include
#include “structmember.h”
#include “Parameter.h”
#include “StaticData.h”
#include “Manager.h”
#include “Hypothesis.h”
#include
#include
#include

typedef struct {
PyObject_HEAD
Parameter *param;
const StaticData *staticData;
vector *weights;
vector *inputFactorOrder, *outputFactorOrder;
FactorMask *inputFactorUsed;
long translation_id;
} Moses;

static int
Moses_traverse(Moses *self, visitproc visit, void *arg)
{
return 0;
}

static int
Moses_clear(Moses *self)
{
// FIXME
return 0;
}

static void
Moses_dealloc(Moses* self)
{
Moses_clear(self);
self->ob_type->tp_free((PyObject*)self);
}

static PyObject *
Moses_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
Moses *self;

self = (Moses *)type->tp_alloc(type, 0);

if (self != NULL) {
self->param = new Parameter();
if(self->param == 0) {
Py_DECREF(self);
return NULL;
}
self->staticData = &(StaticData::Instance());
if(self->staticData== 0) {
Py_DECREF(self);
return NULL;
}

self->weights = new vector;
if(self->weights == 0) {
Py_DECREF(self);
return NULL;
}

self->inputFactorOrder = new vector();
if(self->inputFactorOrder == NULL) {
Py_DECREF(self);
return NULL;
}

self->outputFactorOrder = new vector();
if(self->outputFactorOrder == NULL) {
Py_DECREF(self);
return NULL;
}

self->inputFactorUsed = new FactorMask();
if(self->inputFactorUsed == NULL) {
Py_DECREF(self);
return NULL;
}

self->translation_id = 0;

return (PyObject *)self;
}

static int
Moses_init(Moses *self, PyObject *args, PyObject *kwds)
{
const char *ini_path;

if(!PyArg_ParseTuple(args, “s”, &ini_path))
return -1;
if(!(self->param->LoadParam(std::string(ini_path))))
return -1;

if (!StaticData::LoadDataStatic(self->param))
return -1;

if(self->weights) {
delete self->weights;
self->weights = new vector(self->staticData->GetAllWeights());
}

if(self->weights->size() != self->staticData->GetScoreIndexManager().GetTotalNumberOfScores())
return -1;

if(self->inputFactorOrder && self->outputFactorOrder && self->inputFactorUsed) {
delete self->inputFactorOrder;
delete self->outputFactorOrder;
delete self->inputFactorUsed;
self->inputFactorOrder = new vector(self->staticData->GetInputFactorOrder());
self->outputFactorOrder = new vector(self->staticData->GetOutputFactorOrder());
self->inputFactorUsed = new FactorMask(*self->inputFactorOrder);
}
return 0;
}

static PyMemberDef Moses_members[] = {
/*
{“first”, T_OBJECT_EX, offsetof(Moses, first), 0,
“first name”},
{“last”, T_OBJECT_EX, offsetof(Moses, last), 0,
“last name”},
{“number”, T_INT, offsetof(Moses, number), 0,
“moses number”}, */
{NULL} /* Sentinel */
};

static PyObject *
Moses_decode(Moses* self, PyObject *args)
{
const char *source_sentence;
if(!PyArg_ParseTuple(args, “s”, &source_sentence))
return NULL;
std::stringstream s;
s < < source_sentence <inputFactorOrder)) {
if (long x = source.GetTranslationId()) {
if (x >= self->translation_id) {
self->translation_id = x + 1;
}
} else {
source.SetTranslationId(self->translation_id++);
}
Manager manager(source, self->staticData->GetSearchAlgorithm());
manager.ProcessSentence();
const Hypothesis *hypo = manager.GetBestHypothesis();
PyObject* result = PyList_New(0);
while(hypo != NULL) {
stringstream phrase_stream;
phrase_stream < GetCurrTargetPhrase();
PyList_Append(result,
PyString_FromString(phrase_stream.str().c_str()));
hypo = hypo->GetPrevHypo();
}
PyList_Reverse(result);
return result;
} else {
PyErr_SetString(PyExc_RuntimeError, “Input cannot be read properly”);
return NULL;
}
}

static PyMethodDef Moses_methods[] = {
{“decode”, (PyCFunction)Moses_decode, METH_VARARGS,
“Return decoded target language”},
{NULL} /* Sentinel */
};

static PyTypeObject MosesType = {
PyObject_HEAD_INIT(NULL)
0, /*ob_size*/
“moses.Moses”, /*tp_name*/
sizeof(Moses), /*tp_basicsize*/
0, /*tp_itemsize*/
(destructor)Moses_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number*/
0, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash */
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /*tp_flags*/
“Moses objects”, /* tp_doc */
(traverseproc)Moses_traverse, /* tp_traverse */
(inquiry)Moses_clear, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
Moses_methods, /* tp_methods */
Moses_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)Moses_init, /* tp_init */
0, /* tp_alloc */
Moses_new, /* tp_new */
};

static PyMethodDef module_methods[] = {
{NULL} /* Sentinel */
};

#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
initmoses(void)
{
PyObject* m;

if (PyType_Ready(&MosesType) < 0)
return;

m = Py_InitModule3("moses", module_methods,
"Example module that creates an extension type.");

if (m == NULL)
return;

Py_INCREF(&MosesType);
PyModule_AddObject(m, "Moses", (PyObject *)&MosesType);
}

setup.py:
from distutils.core import setup, Extension

module1 = Extension('moses',
define_macros = [('MAJOR_VERSION', '0'),
('MINOR_VERSION', '1')],
include_dirs = ['../moses/src'],
libraries = ['moses', 'z', 'oolm', 'dstruct', 'misc'],
library_dirs = ['../moses/src'],
sources = ['moses.cpp'])

setup (name = 'moses',
version = '0.1',
description = 'This is a demo package',
author = 'Vee Satayamas',
author_email = 'vsatayamas@gmail.com',
url = 'http://www.python.org/doc/current/ext/building.html&#039;,
long_description = '''
This is really just a demo package.
''',
ext_modules = [module1])

1

เตรียม parallel corpus สำหรับ word alignment จาก .po

วิธีที่ก็ใช้ polib อ่าน .po ของ GNOME มาแล้วก็พยายาม ตัดเครื่องหมายทิ้ง. แม้แต่ string ที่มีหลายบรรทัดก็ตัดทิ้ง.

ขั้นแรกเลยคือเข้าไปที่ web L10N ของ GNOME http://l10n.gnome.org/languages/th/gnome-2-22 หลังจาก copy & paste และแก้ด้วยความถึกนิดหน่อย ผมก็ได้รายการของ .po มา.

Download po file pessulus       100% (30/0/0)
Download po file Sabayon        100% (230/0/0)
GNOME developer platform (66% translated)
Download po file atk    95% (117/0/6)
Download po file gail   100% (103/0/0)

พอได้แบบนี้มาแล้วก็เขียนโปรแกรมมาตัดๆ หน่อย

get_pkg.rb

while gets
    if $_ =~ /Download po file ([^s]+)/
        print "wget  http://l10n.gnome.org/POT/#{$1}.HEAD/#{$1}.HEAD.th.pon"
    end
end

ก็ได้ script ที่ load .po ออกมา

$ ruby get_pkg.rb > download.sh && sh download.sh

พอได้แบบนี้มาแล้วผมก็เขียนโปรแกรมมา extract ของใน .po มาชื่อ ext_po.py


#-*- coding: UTF-8 -*-
import sys
import polib
import getopt
import re

class Params:
    def __init__(self, o_eng_path, o_tha_path, i_po_paths):
        self.o_eng_path = o_eng_path
        self.o_tha_path = o_tha_path
        self.i_po_paths = i_po_paths

    def __str__(self):
        return str(self.__dict__)

def usage():
    print "Usage: " + sys.argv[0] + " -e  -t   ..."

def usage_and_exit():
    usage()
    sys.exit(2)

def get_params():
    try:
        opts, args = getopt.getopt(sys.argv[1:], "e:t:")
        keys = [o[0] for o in opts]
        if not "-e" in keys:
            print "Require English output filename"
            usage_and_exit()
        if not "-e" in keys:
            print "Require Thai output filename"
            usage_and_exit()
        if len(args) &lt; 1:
            print &quot;Require po filename&quot;
            usage_and_exit()
        return Params(o_eng_path = filter(lambda o: o[0] == &quot;-e&quot;, opts)[0][1],
                      o_tha_path = filter(lambda o: o[0] == &quot;-t&quot;, opts)[0][1],
                      i_po_paths = args)
    except getopt.GetoptError, err:
        print str(err)
        usage_and_exit()

def entry_constraints(entry):
    return not entry.translated() and entry.msgstr != &quot;&quot;
        and entry.msgid != &quot;&quot;

def remove(txt, sym):
    return re.sub(sym, &quot; &quot;, txt)

def convert_text(txt):
    syms = [&quot;_&quot;, &quot;...&quot;, &quot;:&quot;, &quot;|&quot;, &quot;-+&quot;, &quot;/+&quot;, &quot;%w&quot;, &quot;&quot;,
            &quot;&quot;&quot;, &quot;©&quot;, &quot;~&quot;, &quot;(&quot;, &quot;)&quot;]
    return reduce(remove, syms, txt)

def split_line(txt):
    return filter(lambda tok: not re.match(&quot;^ *$&quot;, tok),
                  re.split(&quot;n&quot;, txt))

def split_line_in_entry(ans, entry):
    e_lines = split_line(entry[0])
    t_lines = split_line(entry[1])
    if len(e_lines) == 1 and len(t_lines) == 1:
        return ans + [(e_lines[i], t_lines[i]) for i in range(len(e_lines))]
    else:
        return ans

def ext_po_file(o_eng_file, o_tha_file, po):
    entries = filter(entry_constraints, list(po))
    entries = [(entry.msgid, entry.msgstr) for entry in entries]
    entries = [(convert_text(e[0]), convert_text(e[1])) for e in entries]
    entries = reduce(split_line_in_entry, entries, [])
    for entry in entries:
        o_eng_file.write(entry[0] + &quot;n&quot;)
        o_tha_file.write(entry[1] + &quot;n&quot;)

def ext_with_ofiles(o_eng_file, o_tha_file, i_po_paths):
    for i_po_path in i_po_paths:
        po = polib.pofile(i_po_path)
        ext_po_file(o_eng_file, o_tha_file, po)

def ext(o_eng_path, o_tha_path, i_po_paths):
    o_tha_file = open(o_tha_path, &#039;w&#039;)
    o_eng_file = open(o_eng_path, &#039;w&#039;)
    ext_with_ofiles(o_eng_file, o_tha_file, i_po_paths)
    o_eng_file.close()
    o_tha_file.close()

def init_charset():
    reload(sys)
    sys.setdefaultencoding(&#039;utf-8&#039;)

def main():
    init_charset()
    params = get_params()
    ext(params.o_eng_path, params.o_tha_path, params.i_po_paths)

if __name__ == &#039;__main__&#039;:
	main()

จากนั้นก็สั่ง (โดยสมมุติว่า .po ทั้งหมดอยู่ใน folder เดียวกัน)

$ python ext_po.py -e e -t t  *.po

cut.sh เอาไว้ตัดคำโดยใช้  kucut อีกที

#!/bin/sh
iconv -f UTF-8 -t TIS-620 < $1 > $1.tis
kucut --line=" " $1.tis
iconv -f TIS-620 -t UTF-8 < $1.tis.cut > $1.cut

แล้วก็ใช้ cut.sh โดยเรียก

$ ./cut.sh t && mv t.cut t

จากนั้นก็ไปเรียก GIZA++ แบบที่แก้ๆไปแล้ว ได้เลย

$ plain2snt e t

$ train-giza++ e.vcb t.vcb e_t.snt

เป็นอันเรียบร้อย

ได้ผลออกมาแบบนี้

# Sentence pair (1) source length 6 target length 6 alignment score : 0.000163118
รายการ เมนู ใหม่ ต้อง มี ชื่อ
NULL ({ }) New ({ 3 }) menu ({ 2 }) items ({ 1 }) need ({ 4 5 }) a ({ }) name ({ 6 })
# Sentence pair (2) source length 5 target length 5 alignment score : 0.00022357
เมนู ใหม่ ต้อง มี ชื่อ 

ผลอ่ายากนิดนึงแบบ New ({3}) หมายถึงว่า new ตรงกับคำภาษาไทยตัวที่ 3 ใน "รายการ เมนู ใหม่ ต้อง มี ชื่อ" ก็คือใหม่นั่นเอง.