Native Unicode sub-string support

ผมมองว่าสำหรับ Natural language processing ภาคปฏิบัติ การหา sub-string จาก Unicode string นี่มันสำคัญมากเลย

เอาเข้าจริงแทบทุกภาษา (programming language) ก็น่าจะใช้ Unicode ได้หมด ขึ้นอยู่กับว่าออกแรงมากออกแรงน้อยเท่านั้นเอง ถ้าทำง่าย ๆ ก็ดี

ในบทความไม่วิชาการนี้อยากจะลองใช้ substring กับ native string ของแต่ละภาษาเลยว่าจะทำงานได้อย่างที่ต้องการไหม อย่างเช่นว่า “abc”[0:1] ก็ควรจะได้ตัว a ออกมา แต่พอเป็น latin alphabet พวก a b c มันง่ายไป ภาษาส่วนมากก็ใช้ได้หมดถ้าเป็น “กขค”[0:1] ยากขึ้นมาหน่อย แต่เดี๋ยวนี้ก็เรียกว่าง่ายแล้ว

ผมก็เลยมาลองกับตัวอักษรนี่แทน 𨭎 เป็นกลุ่ม non-bmp หรือรหัส Unicode มีค่ามากกว่า 0xFFFF นั่นเอง

  1. ภาษาแรกเลย Python 3 นอกจากจะเขียนง่ายแล้วก็จัดการเรื่องนี้ได้เลยทำงานถูกต้องทุกประการ
    print("𨭎"[0:1])
    
  2. Ruby ก็ใช้ได้เนียน ๆ
    puts "𨭎"[0..1]
    
  3. JavaScript เจ๊ง … จริง JS นี่ใช้ภาษาไทยก็ได้ แต่เจอ non-bmp เจ๊ง
    console.log('𨭎'.substring(0,1));
    
  4. Lua เจ๊งชนิดที่ว่าภาษาไทยก็ไม่ได้

    print(string.sub("𨭎", 1, 1));

  5. PHP เจ๊งชนิดที่ว่าภาษาไทยก็ไม่ได้
    echo substr("𨭎",0,1);
    

ลองกลุ่ม duck type มาเยอะแล้ว ลองพวก static type บ้าง

  1. Rust น้องใหม่มาแรง เจ๊ง ภาษาไทยก็ไม่รอดเหมือนกัน แต่ดีหน่อยที่ว่าโปรแกรมหยุดทำงาน แล้วมี error message บอกว่า ไม่หั่นที่ byte แรกไม่ได้

    println!("{}", &("𨭎"[0..1]));

  2. Java เจ้าตลาด เจ๊ง
    System.out.println("𨭎".substring(0,1));
    
  3. Go ขี้เกียจเขียนครับ เคยลองไปหลายครั้งแล้วเจ๊งกระทั่งกับภาษาไทย แต่มีท่าแก้คือแปลงเป็น rune ก่อน
  4. C# บน Mono บน Fedora 21 เจ๊งเหมือน Java

    Console.WriteLine("𨭎".Substring(0,1));

  5. Free Pascal เจ๊ง
    Writeln(Copy('𨭎',1,1));
    
  6. Ada ใช้ Gnat บน Fedora 21 เจ๊ง

    procedure Main is
    Str : constant String := "𨭎";
    begin
    Put_Line (Str (1..1));
    end Main;

    แทบจะเรียกได้ว่า static type นี่แทบไม่มีอะไรใช้ได้เลย

  7. OCaml ขี้เกียจลองดูจาก spec แล้วควรจะเจ๊งแบบ Go
  8. Haskell ใช้ได้ เป็นภาษาแบบ static type ภาษาเดียวที่ไม่ต้องออกแรงมา

    putStrLn (take 1 "𨭎")

พิจารณามา 13 ภาษา ภาษาหา Unicode sub-string ได้สะดวก ๆ ก็คือ Python 3 ส่วน Python 2.7.x ไม่ได้นะ, Ruby 2 หรือ 1.9 ก็ได้มั้ง และถ้าต้องการ static type ก็มี Haskell ตัวเดียว ที่เป็น pure functional ด้วยอาจจะปรับตัวยากหน่อย

ภาษาอื่น ๆ ก็มีท่าแก้ เช่น PHP ใช้ mb_strsub และกำหนด internal encoding เป็น UTF-8; Rust ก็มีหลายท่าแต่ API มันยังไม่ stable หลัก ๆ คือแปลงเป็น Chars ก่อน; Go ก็แปลงเป็น []rune

ความคิดเห็นของผมก็คือถ้าไม่เน้นว่าโปรแกรมต้องทำงานเร็วมาก ก็ใช้ Python 3 ครับ แต่ถ้าต้องการเร็วจัด ๆ อันนี้คิดหนักแล้ว :-P

ขอบคุณ @plutonix (aka น้าธนา) ที่เอาเรื่อง codepoint กับ ES6 ให้อ่าน ผมถีงสำเนียกได้ว่า ภาษาที่ internal encoding เป็น UTF-16 มันมีกรณีที่เจ๊งแบบนี้

โปรแกรมตัดคำ แบบใช้ PHP ล้วน ๆ

ที่ผ่านมามีหลายท่านสนใจถามเข้าหลังจากที่แสดงตัวอย่างเรียก Swath จาก PHP ให้ดู แต่ส่วนมากพอใช้บน Windows หลายคน ก็งง ๆ ผมก็งง ก็เลยจัดอันนี้ไป โปรแกรมตัดคำมันบน PHP ล้วน ๆ เสียเวลาเขียนไปหลายชั่วโมงอยู่ครับ น่าจะมี bug อะไรเต็มไปหมดถ้าท่านใดพบกรุณาแจ้งไปที่ https://github.com/veer66/PhlongTaIam/issue เดี๋ยวนี้ใช้ github แล้วครับ เพื่อท่านใดจะช่วยแก้จะได้ fork แล้ว pull request กลับมาได้เลย ไม่ต้องเสียเวลาย้ายไปย้ายมาให้ลำบากเหมือนโครงการก่อน

เข้าไป download ที่ https://github.com/veer66/PhlongTaIam ได้เลยครับ ถ้าเอาง่าย ๆ ก็ click ที่ปุ่มที่เขียนว่า zip และมีรูปเมฆมีลูกศรชี้ลงครับ ก็ได้ code ไปทั้งหมด เอาไปวางใน htdocs ก็น่าจะใช้ได้เลย

แต่ก็อาจจะเจ๊งบน Windows หรือสิ่งแวดล้อมที่ต่างจากที่ผมใช้อยู่อยู่ดี ถ้าปัญหาอะไรก็ถามไว้ที่นี่ได้ครับ แต่ว่าส่วนมากผมมักจะตอบไม่ได้  แต่ก็เผื่อมีท่านอื่นตอบได้ครับ

Part-of-speech tagger สำหรับภาษาไทย

โพสนี้จะใช้ model ที่ได้มาจาก corpus ของ NAiST Lab นะครับ ส่วนโปรกรมก็จะใช้ Jitar เนื่องจากว่า Jitar เขาใช้ Java นะครับ ก่อนอื่นก็ต้องติดตั้ง JDK ก่อน จากนั้นก็โหลด Jitar และ Model มาได้เลย  ตาม URL นี้ http://naist.cpe.ku.ac.th/pkg/jitar_model_large.zip และ http://naist.cpe.ku.ac.th/pkg/jitar-20100224.zip

พอแตก Zip ออกมาได้หาไฟล์ jitar-0.0.4.jar, ngrams และ lexicon ลากไปอยู่ใน folder เดียวกัน จากนั้นก็สร้างไฟล์สำหรับลองใช้โปรแกรมผมใช้ชื่อ test.txt เขียนข้างในว่า “ฉัน กิน ข้าว” อย่าลืมเว้นวรรคด้วยนะครับ ต้องใช้ charset เป็น UTF-8 ด้วย

เสร็จแล้วก็ลองรันดูแบบนี้ครับ java -cp jitar-0.0.4.jar org.langkit.tagger.cli.Tag lexicon ngrams < test.txt ก็จะได้ผลลัพธ์ออกเป็น pper vt ncn

pper คือ สรรพนามสำหรับแทนคน

vt คือ สกรรมกริยม

ncn คือ สามัญนาม

ใช้ Swath จาก PHP

Update: โปรแกรมตัดคำแบบใช้ PHP เลยก็มีนะครับ: https://github.com/veer66/PhlongTaIam

ทางบ้านถามเข้ามาหลายท่าน ผมจึงเขียนออกมาได้แบบนี้ครับ

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>Swath Web</title>
	<meta name="generator" content="TextMate http://macromates.com/">
	<meta name="author" content="Vee Satayamas">
	
</head>
<body>
<form method="post">
<textarea name="input_text" cols="80" rows="10"></textarea>
<input type="submit" value="submit">
</form>
<?php

    function swath($input_text) 
    {
        $input_filename= tempnam("/tmp", "swath_");
        $output_filename= tempnam("/tmp", "swath_");
        $input_text_tis620 = iconv("UTF-8", "TIS-620", $input_text);
        file_put_contents($input_filename, $input_text_tis620);
        system("/usr/bin/swath < $input_filename > $output_filename");
        $raw = file_get_contents($output_filename);
        $raw_utf8 = iconv("TIS-620", "UTF-8", $raw);
        unlink($input_filename);
        unlink($output_filename);
        return preg_split('/\|/', $raw_utf8);
    }

    if($_REQUEST['input_text']) {
        $output = swath($_REQUEST['input_text']);
        print implode(" ", $output);
    }
?>
</body>
</html>

แต่ว่าแบบนี้เราต้องลง swath ไว้ใน /usr/bin นะครับ ฯ​ ลงไว้ที่อื่นก็ไม่น่าเป็นไรนะครับ เปลี่ยนใน code เอาเลยก็ได้ ฯ อีกอย่างคือต้องการใช้ iconv ด้วย ฯ แต่ถ้าไม่ใช้ iconv ก็น่าจะได้อีกเหมือนกัน ฯ​ เปลี่ยนหน้าเว็บเป็น TIS-620 ให้หมดก็น่าจะใช้ได้เลย ฯ

GIZA++ 1.0.3 บน Ubuntu 9.10

ผมพยายามจะลง Giza++ 1.0.3 บน Ubuntu 9.10 จริงๆ มี .deb ให้ใช้แต่ว่า ผมไม่ได้ใช้เพราะอยากจะ code ของ Giza++ ด้วย

แต่ว่าก็มีปัญหานิดหน่อย เพราะว่า string ที่จองมาเก็บปี จองมาน้อยไปหน่อย คล้ายๆ ปัญหา y2k ก็เลย patch ไปแบบนี้

diff -Nur giza-pp/GIZA++-v2/file_spec.h giza-pp.orig/GIZA++-v2/file_spec.h
--- giza-pp/GIZA++-v2/file_spec.h	2010-02-06 19:05:55.000000000 +0000
+++ giza-pp.orig/GIZA++-v2/file_spec.h	2009-03-20 11:41:12.000000000 +0000
@@ -37,13 +37,13 @@
   struct tm *local;
   time_t t;
   char *user;
-  char time_stmp[18];
+  char time_stmp[17];
   char *file_spec = 0;
   
   t = time(NULL);
   local = localtime(&t);
   
-  sprintf(time_stmp, "%03d-%02d-%02d.%02d%02d%02d.", local->tm_year, 
+  sprintf(time_stmp, "%02d-%02d-%02d.%02d%02d%02d.", local->tm_year, 
 	  (local->tm_mon + 1), local->tm_mday, local->tm_hour, 
 	  local->tm_min, local->tm_sec);
   user = getenv("USER");

รายงานบักไปแล้วเหมือนจะซ้ำด้วย http://code.google.com/p/giza-pp/issues/detail?id=20 บรรยากาศก็เงียบๆ

ลองใช้ XMLChid กับ UnigramTagger ของ NLTK

ทีแรกผมก็เขียน xmlchid_loader.py เพื่ออ่าน xmlchid.xml ให้อยู่ในรูปแบบ corpus ของ NLTK

from xml.sax import make_parser
from xml.sax.handler import ContentHandler

class Handler(ContentHandler):
    def __init__(self):
        self.corpus = []

    def startElement(self, name, attrs):
        if name == 'word':
            surface = attrs['surface']
            tag = attrs['pos']
            self.corpus[-1].append((surface, tag))
        elif name == 'sentence':
            self.corpus.append([])

def load_xmlchid(filename):
    parser = make_parser()
    handler = Handler()
    parser.setContentHandler(handler)
    parser.parse(filename)
    return handler.corpus

และก็ทีนี้ก็ไปเรียกใช้ UnigramTagger ตามฟอร์ม

#-*- coding: UTF-8 -*-
from xmlchid_loader import load_xmlchid
from nltk import UnigramTagger
corpus = load_xmlchid('xmlchid.xml')
corpus = filter(lambda sent: len(sent) > 0, corpus)
tagger = UnigramTagger(corpus)
result = tagger.tag([u'การ', u'ทำ', u'การบ้าน'])
result = map(lambda w: (w[0], "UNK") if w[1] is None else w, result)
print u" ".join([u"/".join(w) for w in result])

ผลลัพธ์ก็จะได้แบบนี้

การ/FIXN ทำ/VACT การบ้าน/UNK

การติดตั้งและใช้งาน KUCut บน Windows (XP)

KUCut มีคนถามบ่อยเหมือนกันว่าติดตั้งใช้งานอย่างไร ผมก็เลยเขียนเป็น blog ไว้เลยดีกว่า

  1. ติดตั้ง Python 2.6.2
  2. ขั้นตอนแรกก็ดาวโหลด KUCut มาก่อนจาก kucut-1.2.7.tar.gz
  3. จากนั้นก็แตกไฟล์ zip ที่ดาวโหลดมาแล้วออกมา ในกรณีนี้ผมเก็บไว้ใน Desktop
  4. เรียกใช้งานโปรแกรม command prompt
  5. ใช้คำสั่ง cd เปลี่ยน directory ไปเป็น directory ของ KUCut
    kucut_screen1
  6. ติดตั้ง KUCut ด้วยคำสั่ง
    C:\Python26\python.exe setup.py install
    
  7. ลองสร้างไฟล์ test1.txt เขียนภาษาไทยไว้เพื่อทดสอบ (ใช้ character set TIS-620)
    ผมเขียนสิ่งนี้ลงไป:

    ทดลอง โปรแกรมตัดคำ
    
  8. สั่งตัดคำโดยใช้คำสั่ง
    C:\Python26\python.exe C:\Python26\Scripts\kucut --line="_" test1.txt
    
  9. ก็จะได้ไฟล์ test1.txt.cut ออกมา เปิดดูข้างในก็ควรจะเป็นแบบนี้
    ทดลอง _ โปรแกรม ตัด คำ 
    

เป็นอันเรียบร้อย วิธีใช้จาก Python เดี๋ยวผมจะเขียนอีกทีครับ (ถ้าผมทำได้)