[출처 : http://soyoja.com/160]


어떤 자연수 n 이 소수인지 구할때, n 이 작을 경우에는 다음과 같은 방법을 사용한다. 

1. Trial Division 
소수의 성질을 이용, 어떤 수 n 이 소수인지 판별하기 위해 n 을 2 부터 n-1 까지 하나씩 나누어보아 나누어 떨어지는 수가 존재하지 않으면 n 은 소수가 된다.

Notes 
- 2를 제외한 모든 소수는 홀수이므로, 어떤 수 n 이 짝수이면 소수가 아니다. 
- 만약 N 이 합성수라면 N = a*b 형태가 되므로 ( a > 1 && b > 1 ), a 와 b 둘 중 하나는 sqrt(N) 보다 작거나 같음을 알 수 있다. 그러므로 N 을 2부터 sqrt(N) 까지 나눠보아 나누어지지 않으면 N 은 소수가 된다. 

void trial(int end)
{
 for( int a = 2; a <= end; a++ )
 {
  if( a%2 == 0 && a != 2 ) 
  {
   prime[a] = 0;
   continue; 
  }
  bool isprime = true;
  for( int b = 2; b*b <= a; b++ )
  {
   if( a%b == 0 ) 
   {
    isprime = false; 
    break; 
   }
  }
  if( isprime ) prime[a] = 1;
 }
}

2. 에라토스테네스의 체(Sieve of Eratosthenes)
n 이 작을 때 ( 일반적으로 n <= 10,000,000 ) 2 부터 n 까지의 소수를 찾는 가장 효율적인 방법으로 알려져 있다. 어떤 수 P 가 소수이면 P 로 나눌 수 있는 P 의 배수들은 소수가 아닌 것으로 체크한다. 이러한 과정을 반복하면서 주어진 [2,n] 구간 내에서 소수를 찾아낸다.

void sieve(int end)
{
 for( int a = 2; a*a <= end; a++ )
 {
  if( prime[a] == 0 )   // 'a' is a prime 
  {
   for( int b = a*2; b <= end; b += a )
    prime[b] = 1;
  }
 }
}

소수일 것 같은 수 (Probable Prime) 판별법 
어떤 자연수 n 이 소수인지 아닌지 판별할 때, n 이 매우 커지면 유한시간내에 n 이 소수인지 판별하기 곤란해지게 된다. 이 때문에 n 의 소수 여부를 판별하는 근사해를 구하는 검사법이 여럿 개발되었는데, 이 검사법에 의해 n 이 합성수가 아님이 판별되는 경우에 n 을 소수일 것 같다 (Probable Prime) 고 한다. 
Probable Prime (PRP) 의 정의는, 소수가 갖는 특정 상태를 만족하는 자연수이다. 

1. Wheel Factorization 
소수의 특징 중 하나인 [2,n] 구간내에 존재하는 소수의 갯수는 n 이 커질 수록 분포도가 희박해진다는 점에서 착안한 방법. 에라토스테네스의 체의 특수한 응용방법이다. 기본 원리는 에라토스테네스의 체와 동일하나, 구간내의 모든 소수에 대해 배수를 찾아 검사하는 것이 아니라 몇개의 소수를 가지고 이 수들로 나누어지는 수들을 걸러내는 방식으로 구하는 것이 차이점이다. 소수 2, 3을 가지고 Wheel Factorization 을 한다면, 우선 2와 3 의 배수를 모두 소수가 아닌 것으로 판별한다. 그리고 그 다음 소수인 5, 7 를 이용해 또다시 이 작업을 반복한다. 
일반적으로 많이 쓰는 factorization 은 6N+1, 6N+5 를 사용하여 주어진 구간 [1,n] 내에서 나누어 지지 않는 수를 소수일 것 같은 수로 판별한다. ( Wheel Factorization 은 n 이 커질수록 정확도가 떨어지므로 이를 위해 n 이 커질수록 더 많은 소수를 이용해 나누어보아야 한다. )

2. 페르마의 작은 정리 (Fermat's Little Theorem)
페르마의 마지막 정리로 유명한 수학자 페르마가 제안한 정리. 
p 가 a로 나눠지지 않는 소수라면 ap-1 = 1 (mod p) 를 만족해야 한다. 
이때 상기 정리를 만족하지 않는 수는 합성수이며, 상기 정리를 만족하는 수는 소수일 가능성이 매우 높은 수(Probable Prime) 가 된다. 
증명은 상기 링크 참조. 

3. 밀러-라빈 소수판별법 (Miller-Rabin Primality Test)
n 이 소수인지를 검사하려 할 때, 다음 두 식이 성립한다면 a 는 n 이 합성수라는 강한 증거가 된다. ( 이 식이 성립한다면 n 은 소수일 가능성이 매우 높은 수가 된다.)

 a^{d} \not\equiv 1\pmod{n}  
 a^{2^rd} \not\equiv -1\pmod{n} for all 0 \le r \le s-1  

Probable Prime 판별법은 이외에도 여러가지가 개발되어 있다. 


참고1
임의의 큰 자연수를 소인수분해하는 문제는 NP 문제로 알려져 있다. 
거대한 합성수의 소인수분해는 암호문 해독에 응용되어, 기존의 암호문 전달 체계와 다른 암호학에 공공열쇠 개념을 도입하여 리베스트(Ron Rivest)와, 샤미르(Adi shamir), 아들만(Len Adleman)의 이름을 딴 RSA방식이라는 암호이론이 개발되기도 했다.
http://mathworld.wolfram.com/PrimalityTest.html


참고2 - 관련 사이트 
TopCoder Tutorial : Primality Test - Non Deterministic Algorithms
Wikipedia

- CString을 char* 로 변환하기

1. memcpy 사용하기

CString str = "test";
unsigned char st[30];
memcpy(st, (unsigned char*)(LPCTSTR)str,i);

2. strcpy 사용하기

CString strData = "test";
int length = strData.GetLength();
char* st = new char[length];
strcpy(st, strData.GetBuffer(0));

3. 형변환 사용하기

CString str;
str = "test";
char* st = LPSTR(LPCTSTR(str));

- char* 를 CString으로 변환하기

CString클래스의 Format함수를 사용

char st[] = "test";
CString str;
str.Format("%s", st);


[출처 : http://frog3147.tistory.com/entry/CString%EA%B3%BC-char%EA%B0%84%EC%9D%98-%EB%B3%80%ED%99%98]

파일 상태를 읽으려면

  • CFile 클래스를 사용하여 파일 정보를 읽고 설정합니다. CFile 정적 멤버 함수인 GetStatus를 사용하여 파일의 존재 여부를 확인할 수 있습니다. 지정된 파일이 존재하지 않으면 GetStatus가 0을 반환합니다.

따라서 GetStatus 결과를 사용하여 다음 예제와 같이 파일을 열 때 CFile::modeCreate 플래그의 사용 여부를 결정할 수 있습니다.

CFile theFile;
char* szFileName = "c:\\test\\myfile.dat";
BOOL bOpenOK;

CFileStatus status;
if( CFile::GetStatus( szFileName, status ) )
{
    // Open the file without the Create flag
    bOpenOK = theFile.Open( szFileName, 
                    CFile::modeWrite );
}
else
{
    // Open the file with the Create flag
    bOpenOK = theFile.Open( szFileName, 
                    CFile::modeCreate | CFile::modeWrite );
}

[출처 : http://msdn.microsoft.com/ko-kr/library/cc451414(VS.71).aspx]

Mel 주파수 캡스트럼 (MFCC)

Mel-frequency cepstral coefficient

이 놀은 음성신호처리 분야에서 음성의 특성을 표현하기 위해 주로 사용되는 Mel-frequency cepstral coefficient (MFCC)에 대해 설명하고 있습니다.



시작하기 전에: 이 놀의 내용은 영문 wikipedia의 MFCC관련 내용을 기반으로 작성되었습니다.

개요

음성신호처리 분야에서 Mel-frequency cepstrum (MFC)은 단구간 신호의 파워스펙트럼을 표현하는 방법 중 하나로, 비선형적인 Mel스케일의 주파수 도메인에서 로그파워스펙트럼에 코사인변환 (cosine transform)을 취함으로써 얻을 수 있다. Mel-frequency cepstral coefficients (MFCCs)는 여러 MFC들을 모아 놓은 계수들을 의미한다.

MFCC와 일반적인 캡스트럼의 차이는 일반적인 캡스트럼의 경우 주파수 밴드가 균등하게 나누어져 있는 반면 MFCC의 경우 주파수 밴드가 Mel-scale에서 균등하게 나누어진다는 것이다. Mel-scale로의 주파수 워핑은 소리를 더욱 잘 표현할 수 있는 장점이 있다(참고문헌필요).  따라서 오디오압축 등에서 사용된다.

MFCCs는 일반적으로 다음의 과정을 통해 구할 수 있다[1]:
  1. 단구간 음성에 Fourier Transform을 취한다.
  2. 위 값들에서 Mel-scale의 필터뱅크를 이용해 파워스펙트럼을 구한다.
  3. 각 Mel-scale의 파워에 로그를 취한다.
  4. 위 값에 discrete cosine transform 을 취한다.
  5. MFCCs 값이 나온다.
위 과정은 여러 가지 계산 방법 중에 하나로, 다른 방법으로도 계산할 수 있다[2].


응용분야

MFCCs는 일반적으로 음성인식 시스템이나 화자인식 시스템 등에서 인식을 위한 특징파라메터로 사용된다[3]. 또한 음악장르 인식과 같이 Music information retrieval 분야에서도 사용되고 있다.


잡음에 대한 민감도

MFCC는 더해지는 잡음(additive noise)에 특별히 강인하지는 않다. 그래서 몇몇 연구자들은 이를 극복할 수 있는 방법들을 제안하기도 했다.

참조

  1. Min Xu et al. (2004). "HMM-based audio keyword generation". in Kiyoharu Aizawa, Yuichi Nakamura, Shin'ichi Satoh. Advances in Multimedia Information Processing - PCM 2004: 5th Pacific Rim Conference on Multimedia. Springer.
    http://books.google.com/books?id=Ijdp9UWb5ZYC&pg=PA569&dq=Mel-frequency-cepstrum&as_brr=3&sig=M4PKJKxiclebl7_BDBWkrPjYWOQ.
  2. Fang Zheng, Guoliang Zhang and Zhanjiang Song (2001), "Comparison of Different Implementations of MFCC," J. Computer Science & Technology, 16(6): 582–589.
  3. T. Ganchev, N. Fakotakis, and G. Kokkinakis (2005), "Comparative evaluation of various MFCC implementations on the speaker verification task," in 10th International Conference on Speech and Computer (SPECOM 2005), Vol. 1, pp. 191–194.


[출처 : http://knol.google.com/k/bong-jin-lee/mel-주파수-캡스트럼-mfcc/2p9i0m613vquw/10]

음성 인식 실험을 위한 HTK 사용방법


Steve Young, Dan Kershaw, Julian Odell, Dave Ollason, Valtcho Valtchev, Phil Woodland의 /The HTK Book Version 2.2/의 3장에 나와 있는 튜토리얼을 기반으로 작성하였습니다.
 

1. 음성 녹음

    우선 인식 실험을 수행하기 위해서는 학습에 필요한 음성이 필요하다. 일반적으로 구축하고자하는 목적에 맞게 텍스트를 준비하고, 발성 음성을 녹음하는 작업이 수반된다. HTK에서는 녹음 툴로 HSLab이라는 명령어가 제공된다.

    HSLab  -C  HAUDIO  noname  

    (예)  HAUDIO

    TRACE =0
    LINEOUT = TRUE
    PHONESOUT = FALSE
    SPEAKEROUT = TRUE
    LINEIN = FALSE
    MICIN = TRUE
    SOURCELABEL= ESPS
    TARGETLABEL=HTK
    TARGETFORMAT=HTK
    SOURCERATE=625

     

2. 레이블 파일 작성

    일반적으로 학습을 하기 위해서는 레이블링된 파일이 필요하다. 물론, embeded training(HTK의 HERest)을 하는 경우, 시간 정보열이 요구되지 않으므로 올바른 학습용 발음열을 입력으로 넣어주면 된다. 
    대부분 레이블링된 파일은 모델의 초기값을 생성하는 경우, 많이 사용된다. (HTK의 HRest)

    레이블링 툴로는 보통 Xwaves를 사용하며 레이블링한 파일은 HTK 형식으로 변환되어져야 한다.
    (But, 학습시 레이블 파일을 사용하는 경우에는 레이블 파일의 형식에 따라 변환을 하여야 하나 명령어 옵션을 찾아보면, 다른 형식들을 지원하는 것이 있으므로 확인)

    HTK 레이블 형식

    Xwaves 형식

     

    HTK 형식

    signal s0001
    type 0
    comment created using xlabel Thu Jan 22 
    19:27:57 1998
    font - misc-*-bold-*-*-*-15-*-*-*-*-*-*-*
    separator ;
    nfields 1
    #
        0.450000   -1 sil
        0.500000   -1 N
        …
        1.300000   -1 A
        1.350000   -1 sil

     

    0 4500000 sil사
    4500000 5000000 N
    5000000 5500000 AA

    12500000 13000000 A
    13000000 13500000 sil
     
    (100ns단위)

 

3. 트랜스크립션(Transcription) 파일 작성


    wlist - 인식 단위 리스트 작성
    인식할 문장들을 디코딩 단위로 나누어 리스트를 작성한다.

    words

     

    Wlist



    학교

    간다




    있다

    % sort words | uniq > wlist

    간다



    온다
    있다

    학교

    words.mlf

    #!MLF!#
    "*/s0001.lab”


    학교


    ㄴ다
    .
    "*/s0002.lab"

    ....

    예) words.mlf

    인식하고자 하는 단어 리스트들을 나열한 파일로서  라인의단위는 인식하고자 하는 디코딩 단위와 발음사전과 일치하여야 한다.

    phones0.mlf

    #!MLF!#
    "*/s0001.lab”
    sil
    N
    AA
    N
    WW
    N
    H
    AA
    KQ
    KK
    OW
    EY
    K
    AA
    N
    D
    A
    sil
    .
    "*/s0002.lab"
    N
    ....

    인식하고자 하는 단어 리스트들에 대응하는 폰열(Phone list)을 나열한파일로서  라인에는 폰 하나씩 적는다.

    문장의 시작에는 반드시 sil로 시작하고, 문장의 끝에도 sil로 끝난다.

    파일의 끝에는 마침표(.)로 문장의 끝을 구분한다.

    phones1.mlf

    #!MLF!#
    "*/s0001.lab”
    sil
    N
    AA
    N
    WW
    N
    Q
    H
    AA
    KQ
    KK
    OW
    EY
    Q
    K
    AA
    N
    D
    A
    sil
    .
    "*/s0002.lab"
    N
    ....

    phones0.mlf에 적힌 폰열(phone list)은 같다.

    일반적으로 사전 단위의 구분에 따라  Q를 삽입하거나, 어절단위에 Q  삽입, 끊어읽기에 따라 Q를 삽입하여 스크립트를 작성한다.

    어떻게 구분짓는 것이 인식률에 가장 좋은지는 아직 실험결과를 얻지 못한 상태이다.
    일반적으로 영어권에서는 단어단위가 끊어읽기 단위이기 때문에 한국어에서 고려되어야하는 사항과는 다르다.

 

4. 발음사전(Pronunciation Dictionary) 만들기

    HTK에서 제공하는 HDMan 명령어는 영어 음성사전 작성에 사용되는 것으로 사전 생성에는 해당 단어가 어떻게 발음되어지는지 폰 열들이 기록되어진다. 
    이런한 사전 생성 방식은 한국어 발음사전을 만들기에는 부적합하다. 한국어의 경우 디코딩 단위(사전의 엔트리 단위 = lexical 단위)가 영어권과 다르며, 어절간이나 단어사이에 일어나는 발음변이 현상을 반영하는 방법이 고려되어져야 한다. 
    이렇게 고려되어진 한국어 음운변화 현상을 반영하여 한국어 음소 Set에 맞는 사전 생성 프로그램을 작성하여 사용하여야 한다.

    인식단위가 적은 경우, 직접 손으로 사전을 작성해도 되지만, 인식 어휘가 크게 증가하는 경우 자동 생성 프로그램이 필요하게 된다. 
    공개된 발음생성 프로그램이 있느냐는 질문을 많이 받는데, 사실 국내에서는 찾아보기 힘들다. 다만 관련 논문들을 참조하여 직접 작성하는 것을 권장한다. 일반적으로 대학 연구실에서는 각기 연구실 내에서 사용하는 프로그램이 있으나 공개용은 구하기 힘들고, 직접 문의를 하거나 대상 어휘를 주고 사전생성 프로그램 결과를 받는 방법은 가능할 듯하다.

     

    발음사전 예제

    표제어

    PLU List

    간다



    온다
    있다

    학교

    K AA N T AA
    N AA
    N WW N
    EY
    OW N T AA
    IY TQ TT AA
    Z IY PQ
    HH AA KQ K JO

     

    한국어 자동 발음열 및 발음사전 생성

    [관련논문]
    1. 이경님, 전재훈, 정민화,
     한국어 연속음성 인식을 위한 발음열 자동 생성한국 음향학회지, 제 20권, 제 2호, pp. 35-43, 2001
    2. 전재훈, 차선화, 정민화, 이정철, 
    형태음운론적 분석에 기반한 한국어 발음생성, HCI 학술대회, 1998.
    5. 차선화, 정민화, 
    TTS 시스템을 위한 한국어 발음열 자동생성, 제 15회 음성통신 및 신호처리 워크샵, 1998.
    6. 위선희,정민화, 
    음운변화 규칙을 이용한 음성사전 생성기, HCI 학술대회, 1996.
    7. 
    Jehun Jeon, Sunhwa Cha, Minhwa Chung, Jun Park, and Kyuwoong Hwang, AUTOMATIC GENERATION OF KOREAN PRONUNCIATION VARIANTS BY MULTISTAGE APPLICATIONS OF PHONOLOGICAL RULES, ICSLP 98.

     

5.  HLED 명령어와 발음사전을 이용하여 트랜스크립션 작성하기

    발음 사전의 표제어와 words.mlf에 라인별로 기재된 단어를 매치하여 phones0.mlf와 phones1.mlf를 생성하는 명령어이다. 
    사전에는 해당 표제어에 대한 발음열 리스트가 기재되어 있고, 영어권의 경우, 단어 단위가 디코딩 단위이므로 일대일 매칭으로 트랜스크립션을 얻을 수 있다.
    한국어의 경우 디코딩 단위에 따라서 이러한 사전과 단어열의 일대일 매칭이 올바른 학습용 발음열 트랜스크립션을 얻기 어렵기 때문에, 한국어에 맞는 학습용 발음열 생성기가 필요하다.

    mkphones0.led

     

    mkphones1.led

    EX
    IS
      sil  sil
    DE  Q

     

    EX
    IS
      sil  sil

HLED-0

 HLEd -l '*' -d dict -i phones0.mlf mkphones0.led words.mlf

HLED-1

 HLEd -l '*' -d dict -i phones1.mlf mkphones1.led words.mlf

 

6. 음성 특징 추출 (Feature Extraction) : step5 - Coding the Data

    사람의 음성을 인식하기 위해서는 음성의 특징들을 뽑아 사용합니다. 
    입력된 음성 
    파일로부터 특징 추출 과정을 수행하는 단계로 일반적으로 LPC나 MFCC를 사용합니다. HTK에서 제공하는 feature 타입은 책을 참고하십시오.

    예) codetr.scp

     /user/speech/DB/s0001.wav          /user/speech/DB/s0001.mfc
     /user/speech/DB/s0002.wav          /user/speech/DB/s0002.mfc
     ...                                                ...

    config파일

     

    # Coding parameters
    TARGETKIND=MFCC_0
    TARGETRATE=100000.0
    SAVECOMPRESSED=T
    SAVEWITHCRC=T
    WINDOWSIZE=250000.0
    USEHAMMING=T
    PREEMCOEF=0.97
    NUMCHANS=26
    CEPLIFTER=22
    NUMCEPS=12
    SOURCERATE=625
    TARGETLABEL=HTK
    TARGETFORMAT=HTK
    NONUMESCAPES=T 

    * PC에서 wave 파일 형식으로 녹음한 경우
    SOURCEFORMAT=NOHEAD (헤더가 없는 경우)
    - SOURCEFORMAT=WAVEFORM (웨이브 형식인 경우)

    NONUMESCAPES = T는 
    화일에 프린트할때(WriteString, ReWriteString) 한글이 8bit 숫자로 바뀌지 않도록 하는 옵션
    .

HCOPY

 HCopy -T 1 -C config -S codetr.scp


Configuration File
   (16bit, 16KHz 음성 파일 기준)

    config

     

    config1

    # Coding parameters
    TARGETLABEL=HTK
    TARGETKIND=MFCC_0
    TARGETRATE=100000.0
    SAVECOMPRESSED=T
    SAVEWITHCRC=T
    WINDOWSIZE=250000.0
    USEHAMMING=T
    PREEMCOEF=0.97
    NUMCHANS=26
    CEPLIFTER=22
    NUMCEPS=12
    SOURCEFORMAT=NOHEAD
    SOURCERATE=625

     

    # Coding parameters
    TARGETLABEL=HTK
    TARGETKIND=MFCC_0_D_A
    TARGETRATE=100000.0
    SAVECOMPRESSED=T
    SAVEWITHCRC=T
    WINDOWSIZE=250000.0
    USEHAMMING=T
    PREEMCOEF=0.97
    NUMCHANS=26
    CEPLIFTER=22
    NUMCEPS=12
    #SOURCEFORMAT=NOHEAD
    SOURCERATE=625

     

7. Monophone 생성 단계

    각 명령어를 실행하기 전에 다음 파일  필요한 디렉토리 hmm0 ~ hmm15, lib 미리 생성해야 한다. 'mkdir ~'...  HTK 자체에서 자동으로 디렉토리를 생성해 주지 않는다. 디렉토리가 없어도 실행은 되지만, 지정한 디렉토리에 계산된 값을 저장할 위치를 찾지 못한 상태로 프로그램이 종료되면 다시 실험을 수행해야하므로 반드시 디렉토리가 있는지 없는지 확인한 다음, 실험을 수행한다. 
    또한 실험에 사용되는 파라메터 파일 및 스크립트가 필요하다.

    Master Macro File(MMF)

proto

 

hmm0/hmmdefs

~o <VecSize> 39 <MFCC_0_D_A>
~h "proto"
<BeginHMM>
   <NumStates> 5
   <State> 2
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  
   <State> 3
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  
   <State> 4
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  
   <TransP> 5
        0.0  1.0  0.0  0.0  0.0
        0.0  0.6  0.4  0.0  0.0
        0.0  0.0  0.6  0.4  0.0
        0.0  0.0  0.0  0.7  0.3
        0.0  0.0  0.0  0.0  0.0
<EndHMM>

 

~h “sil”
<BeginHMM>
   <NumStates> 5
   <State> 2
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  
   <State> 3
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  
   <State> 4
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  
   <TransP> 5
        0.0  1.0  0.0  0.0  0.0
        0.0  0.6  0.4  0.0  0.0
        0.0  0.0  0.6  0.4  0.0
        0.0  0.0  0.0  0.7  0.3
        0.0  0.0  0.0  0.0  0.0
<EndHMM>

~h “IY”
<BeginHMM>
<NumStates> 5
   <State> 2
      <Mean> 39
        0.0  0.0  0.0  0.0  
      <Variance> 39
        1.0  1.0  1.0  1.0  

 

train.scp

/user/speech/DB/s0001.mfc
/user/speech/DB/s0002.mfc

 

monophones0

 

monophones1

sil
IY
EY

R
LI

 

sil
Q
IY
EY

R
LI

 

    초기 mean과 variance 값 계산

HCOMPV0

 HCompV -C config1 -f 0.01 -m -S train.scp -M hmm0 proto

    -  scan a set of data file 
    -  global mean
     variance 계산 
    -  모
     gaussian 같은 mean variance 할당

       HCOMPV 수행 , hmm0/vFloors 복사하고 수정하여 macros 작성한다.
 

()

hmm0/vFloors

 

macros

 

<Variance> 39

 1.361697e-01 5.078497e-02 … 3.495922e-04

~o
<VecSize> 39
<MFCC_0_D_A>

~v varFloor1
<Variance> 39
 1.361697e-01 5.078497e-02 … 3.495922e-04

 

    Monophone 모델 학습 (반복)

HEREST1

 HERest -T 1 -C config1 -I phones0.mlf -t 250.0 150.0 1000.0 -S train.scp
 -H hmm0/macros -H hmm0/hmmdefs -M hmm1 monophones0 

HEREST2

 HERest -T 1 -C config1 -I phones0.mlf -t 250.0 150.0 1000.0 -S train.scp
 -H hmm1/macros -H hmm1/hmmdefs -M hmm2 monophones0

HEREST3

 HERest -T 1 -C config1 -I phones0.mlf -t 250.0 150.0 1000.0 -S train.scp
 -H hmm2/macros -H hmm2/hmmdefs -M hmm3 monophones0

     

     * 학습하고자하는 문장수가 많은 경우, 파라메터의 값이 일정 임계치를 넘지 못해서 생기는 에러가 발생한다.(HTK Error solution 참고) 이런 경우 병렬로 학습 문장들을 나누어서 학습한다. 아래 예는 train-part1.scp와 train-part2.scp로 나누어 병렬 학습 후, HMM 모델을 합치는 방법이다.

    HEREST1

     HERest -T 1 -C config1 -I phones0.mlf -t 250.0 150.0 1000.0 -S train.scp
     -H hmm0/macros -H hmm0/hmmdefs -M hmm1 monophones0 

    => 위의 명령 스크립트 대신 아래와 같이 3단계로 나누어 학습한다.

    HEREST1-a

     HERest -T 1 -C config1 -I phones0.mlf -t 250.0 150.0 1000.0 -S train-part1.scp -H hmm0/macros -H hmm0/hmmdefs -M hmm1 -p 1 monophones0 

    HEREST1-b

     HERest -T 1 -C config1 -I phones0.mlf -t 250.0 150.0 1000.0 -S train-part2.scp -H hmm0/macros -H hmm0/hmmdefs -M hmm1 -p 2 monophones0

    HERESTP1

     HERest -T 1 -C config1 -t 250.0 150.0 1000.0 -H hmm0/macros -H hmm0/hmmdefs -M hmm1 -p 0 monophones0 hmm1/*.acc

     

 

8. Silence Short-pause 결합

    - 우선 hmm3/hmmdef hmm3/macros hmm4/ 디렉토리로 복사한다.
    - hmm4/hmmdefs
    에서 ‘sil’ state3 부분을 ‘Q’ state2 복사하고, 다음과 같이 수정한다
       (
    그림 3.9 참조)

    (hmm3/hmmdefs


    ~h "sil"
    <BEGINHMM>
    <NUMSTATES> 5
    <STATE> 2
    <MEAN> 39
     -1.780108e+01 -3.706721e+00 … 1.562776e-02
    <VARIANCE> 39
     9.590965e+00 5.748668e+00 … 7.088412e-02
    <GCONST> 8.631488e+01
    <STATE> 3
    <MEAN> 39
    -1.919943e+01 -4.943765e+00 … 4.967156e-02
    <VARIANCE> 39
    1.922424e+00 4.332380e+00 … 3.408970e-02
    <GCONST> 7.500365e+01
    <STATE> 4
    <MEAN> 39
     -1.970972e+01 -4.890028e+00 … 4.017399e-03
    <VARIANCE> 39
     1.040388e+00 3.472893e+00 … 9.952580e-03
    <GCONST> 6.251125e+01
    <TRANSP> 5
     0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
     0.000000e+00 5.861149e-01 4.138851e-01 0.000000e+00 0.000000e+00
     0.000000e+00 0.000000e+00 7.178701e-01 2.821299e-01 0.000000e+00
     0.000000e+00 0.000000e+00 0.000000e+00 9.630499e-01 3.695011e-02
     0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
    <ENDHMM>
    ...


    (hmm4/hmmdefs


    ~h “sil”

    ~h “Q”
    <BEGINHMM>
    <NUMSTATES> 3
    <STATE> 2
    <MEAN> 39
    -1.919943e+01 -4.943765e+00 … 4.967156e-02
    <VARIANCE> 39
    1.922424e+00 4.332380e+00 … 3.408970e-02
    <GCONST> 7.500365e+01
    <TRANSP> 3
    0.000000e+00 1.000000e+00 0.000000e+00
    0.000000e+00 7.178701e-01 2.821299e-01
    0.000000e+00 0.000000e+00 9.630499e-01
    <ENDHMM>


    sil.hed

    AT 2 4 0.2 {sil.transP}
    AT 4 2 0.2 {sil.transP}
    AT 1 3 0.3 {Q.transP}
    TI silst {sil.state[3],Q.state[2]}

 

HHED4

 HHEd -H hmm4/macros -H hmm4/hmmdefs -M hmm5 sil.hed monophones1 

 

HEREST5

 HERest -T 1 -C config1 -I phones1.mlf -t 250.0 150.0 1000.0 -S train.scp -H hmm5/macros -H hmm5/hmmdefs -M hmm6 monophones1

HEREST6

 HERest -T 1 -C config1 -I phones1.mlf -t 250.0 150.0 1000.0 -S train.scp -H hmm6/macros -H hmm6/hmmdefs -M hmm7 monophones1

 

    Training data realign하여 aligned.mlf 생성한다. (이때사전에 ‘SILENCE sil’ 등록)

HVITE7

 HVite -T 1 -l '*' -o SWT -b SILENCE -C config1 -a -H hmm7/macros -H hmm7/hmmdefs -i aligned.mlf -m -t 250.0 -I words.mlf -S train.scp dict monophones1

    § 기타 추가 옵션 :  -b lab (확장자를 rec 대신에 lab 저장ß  레이블링 파일을 찾지않고 학습

 

HEREST8

 HERest -T 1 -C config1 -I aligned.mlf -t 250.0 150.0 1000.0 -S train.scp -H hmm7/macros -H hmm7/hmmdefs -M hmm8 monophones1

HEREST9

 HERest -T 1 -C config1 -I aligned.mlf -t 250.0 150.0 1000.0 -S train.scp -H hmm8/macros -H hmm8/hmmdefs -M hmm9 monophones1

    § 기타 추가 옵션 :  -X rec (레이블 파일의 확장자를 선택)

 

9. Tied-State Triphone 생성 단계

mktri.led

WB  Q
WB  sil
TC

 

HLED10

 HLEd -n triphones0 -l '*' -i lib/wintri.mlf mktri.led aligned.mlf 

    wintri.mlf 학습에서 사용된 triphone list aligned.mlf를 이용하여 재구성된 트랜스크립션 파일이다. HLEd를 사용하면 wintri.mlf와 triphones0가 자동으로 생긴다.

 

     % mktrihed < monophones0 > mktri.hed

    예) mktri.hed

    CL triphones0
    TI T_IY {(*-IY+*,IY+*,*-IY).transP}
    TI T_EY {(*-EY+*,EY+*,*-EY).transP}
    TI T_EH {(*-EH+*,EH+*,*-EH).transP}

    TI T_L {(*-L+*,L+*,*-L).transP}
    TI T_R {(*-R+*,R+*,*-R).transP}
    TI T_LI {(*-LI+*,LI+*,*-LI).transP}

 

 

HHED11

 HHEd -B -H hmm9/macros -H hmm9/hmmdefs -M hmm10 mktri.hed monophones1

     

    트라이폰 모델 학습

HEREST12

 HERest -T 1 -C config1 -I lib/wintri.mlf -s stats -t 250.0 150.0 1000.0 -S train.scp -H hmm10/macros -H hmm10/hmmdefs -M hmm11 triphones0

HEREST13

 HERest -T 1 -C config1 -I lib/wintri.mlf -s stats -t 250.0 150.0 1000.0 -S train.scp -H hmm11/macros -H hmm11/hmmdefs -M hmm12 triphones0

 

    Tiedlist를 만드는데 사용하는 question

(예) tree.hed - 자음의 경우음의 분류기준에 따라 state-tying.

RO 100.0 stats
TR 0
QS "L_Stop-Beg"  {P-*,B-*,PQ-*,PP-*,PH-*} 
QS "L_Stop-Mid"  
{D-*,TQ-*,T-*,TT-*,TH-*} 
QS "L_Stop-End"  {K-*,G-*,KQ-*,KK-*,KH-*} 
QS "R_Stop-Beg"  {*+P,*+B,*+PQ,*+PP,*+PH} 
QS "R_Stop-Mid"  {*+D,*+TQ,*+T,*+TT,*+TH} 
QS "R_Stop-End"  {*+K,*+G,*+KQ,*+KK,*+KH}
QS "L_Affr"      {TS-*,JH-*,TST-*,CH-*} 
QS "L_Fric"      {S-*,SH-*,SS-*,SHS-*} 
QS "L_Nasal"     {M-*,N-*,NI-*,NX-*} 
QS "L_Liquid"    {R-*,L-*,LI-*} 
QS "R_Affr"      {*+TS,*+JH,*+TST,*+CH} 
QS "R_Fric"      {*+S,*+SH,*+SS,*+SHS} 
QS "R_Nasal"     {*+M,*+N,*+NI,*+NX} 
QS "R_Liquid"    {*+R,*+L,*+LI} 
QS "L_IY"       {IY-*} 
… 
QS "L_R"        {R-*} 
QS "L_LI"       {LI-*} 
QS "R_IY"       {*+IY}


QS "R_R"        {*+R}
QS "R_LI"       {*+LI} 
 
TR 2 
TB 350.0 "IY_s2" {(IY, *-IY, *-IY+*, IY+*).state[2]} 
… 
TB 350.0 "R_s2"  {(R, *-R, *-R+*, R+*).state[2]} 
TB 350.0 "IY_s3" {(IY, *-IY, *-IY+*, IY+*).state[3]} 
… 
TB 350.0 "R_s3"  {(R, *-R, *-R+*, R+*).state[3]} 
TB 450.0 "IY_s4" {(IY, *-IY, *-IY+*, IY+*).state[4]} 
… 
TB 450.0 "R_s4"  {(R, *-R, *-R+*, R+*).state[4]} 
TB 450.0 "LI_s4" {(LI, *-LI, *-LI+*, LI+*).state[4]}

TR 1 
AU "fulllist" 
CO "tiedlist" 
ST "trees"

 

    Question을 가지고 tiedlist를 만드는 과정

HHED14

 HHEd -H hmm12/macros -H hmm12/hmmdefs -M hmm13 tree.hed triphones0 > log

    Tiedlist 트라이폰 모델을 학습하는 과정

HEREST15

 HERest -T 1 -C config1 -I lib/wintri.mlf -s stats -t 250.0 150.0 1000.0 -S train.scp -H hmm13/macros -H hmm13/hmmdefs -M hmm14 tiedlist

HEREST16

 HERest -T 1 -C config1 -I lib/wintri.mlf -s stats -t 250.0 150.0 1000.0 -S train.scp -H hmm14/macros -H hmm14/hmmdefs -M hmm15 tiedlist

 

10. Word network 생성 방법

    FSN wdnet 작성 : 의미 문법 gram 작성 (HTK  참조)

HPARSE

  HParse  -C  config1  gram  wdnet

     back-off bigram : 통계적 언어모델

HLSTATS

 HLStats -b bigfn -o wlist words.mlf

HBUILD

 HBuild -n bigfn wlist wdnet

     

11. 인식 단계

     일반적으로 학습 데이터와 테스트 데이터를 분리하여 인식 실험을 한다. 간단하게 동작이 잘 되는지를 확인하기 위해 학습 데이터를 이용해 확인하는 경우도 있다. 테스트 데이터를 선정하는 방법 또한 다양하다. 선별된 테스트 데이터는 학습시 발생한 트라이폰 리스트에 없는 경우가 생겨 네트웍 loading시 에러가 나는 경우가 발생한다. 이때에는 tiedlist를 만드는 부분부터 다시 확인하여 테스트 데이터 내에 생길 수 있는 트라이폰 리스트가 모두 포함될 수 있도록 한다.

HVITE-tr

 HVite -C config1 -T 33 -H hmm15/macros -H hmm15/hmmdefs -S train.scp -l '*' -o ST  -i recout.mlf(출력화일) -w wdnet(언어모델 네트웍) -t 200(beam threshold) -p 0.0 -s 5.0 dict(인식사전) tiedlist(HMM리스트)

HVITE-test

 HVite -C config1 -T 33 -H hmm15/macros -H hmm15/hmmdefs -S test.scp -l '*' -o ST  -i recout-test.mlf -w wdnet -t 250(beam threshold) -p 0.0 -s 5.0 dict tiedlist


    Option 설명 : 
    -t : Beam threshold 설정, default는 무한 대여서 인식 시간이 무지 오래걸립니다. 경험치로 200 정도면 큰 차이없이 결과를 얻을 수 있을겁니다. 숫자가 적을수록 빔 폭이 작은 대신 인식속도는 빨라집니다.
    -s : Langage Scale Factor 
    -p : Insertion Penalty

 

12. 인식 결과 평가

    testref.mlf 인식할 문장 순서대로 정답을 기술한 내용이 들어 있어야 한다. (words.mlf 비슷)
    recout.mlf
     인식결과 파일로서, SILENCE 등은 제외하고 인식결과를 평가한다. (-z SILENCE)
    (
     프로그램을 돌리기 위해서는  HVITE에서 -o ST 옵션이 주어져야만 . )

     

HRESULTS

 HResults  -I testref.mlf  tiedlist  recout1.mlf

 

 

. 질문사항이 있으면 Research Bulletin을 활용해주세요.

     

     

 



[출처 : http://media.sungshin.ac.kr/~knlee/htk/htktutorial.html]
XML file: sample.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<root>
   <ApplicationSettings
           option_a = "10"
           option_b = "24"
           >
   </ApplicationSettings>
   <OtherStuff
           option_x = "500"
           >
   </OtherStuff>
</root>


Include file: parser.hpp

#ifndef XML_PARSER_HPP
#define XML_PARSER_HPP
/**
 *  @file
 *  Class "GetConfig" provides the functions to read the XML data.
 *  @version 1.0
 */
#include <xercesc/dom/DOM.hpp>
#include <xercesc/dom/DOMDocument.hpp>
#include <xercesc/dom/DOMDocumentType.hpp>
#include <xercesc/dom/DOMElement.hpp>
#include <xercesc/dom/DOMImplementation.hpp>
#include <xercesc/dom/DOMImplementationLS.hpp>
#include <xercesc/dom/DOMNodeIterator.hpp>
#include <xercesc/dom/DOMNodeList.hpp>
#include <xercesc/dom/DOMText.hpp>

#include <xercesc/parsers/XercesDOMParser.hpp>
#include <xercesc/util/XMLUni.hpp>

#include <string>
#include <stdexcept>

// Error codes

enum {
   ERROR_ARGS = 1, 
   ERROR_XERCES_INIT,
   ERROR_PARSE,
   ERROR_EMPTY_DOCUMENT
};

class GetConfig
{
public:
   GetConfig();
  ~GetConfig();
   void readConfigFile(std::string&) throw(std::runtime_error);
 
   char *getOptionA() { return m_OptionA; };
   char *getOptionB() { return m_OptionB; };

private:
   xercesc::XercesDOMParser *m_ConfigFileParser;
   char* m_OptionA;
   char* m_OptionB;

   // Internal class use only. Hold Xerces data in UTF-16 SMLCh type.

   XMLCh* TAG_root;

   XMLCh* TAG_ApplicationSettings;
   XMLCh* ATTR_OptionA;
   XMLCh* ATTR_OptionB;
};
#endif


C++ Program file: parser.cpp

#include <string>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <list>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

#include "parser.hpp"

using namespace xercesc;
using namespace std;

/**
 *  Constructor initializes xerces-C libraries.
 *  The XML tags and attributes which we seek are defined.
 *  The xerces-C DOM parser infrastructure is initialized.
 */

GetConfig::GetConfig()
{
   try
   {
      XMLPlatformUtils::Initialize();  // Initialize Xerces infrastructure
   }
   catch( XMLException& e )
   {
      char* message = XMLString::transcode( e.getMessage() );
      cerr << "XML toolkit initialization error: " << message << endl;
      XMLString::release( &message );
      // throw exception here to return ERROR_XERCES_INIT
   }

   // Tags and attributes used in XML file.
   // Can't call transcode till after Xerces Initialize()
   TAG_root        = XMLString::transcode("root");
   TAG_ApplicationSettings = XMLString::transcode("ApplicationSettings");
   ATTR_OptionA = XMLString::transcode("option_a");
   ATTR_OptionB = XMLString::transcode("option_b");

   m_ConfigFileParser = new XercesDOMParser;
}

/**
 *  Class destructor frees memory used to hold the XML tag and 
 *  attribute definitions. It als terminates use of the xerces-C
 *  framework.
 */

GetConfig::~GetConfig()
{
   // Free memory

   delete m_ConfigFileParser;
   if(m_OptionA)   XMLString::release( &m_OptionA );
   if(m_OptionB)   XMLString::release( &m_OptionB );

   try
   {
      XMLString::release( &TAG_root );

      XMLString::release( &TAG_ApplicationSettings );
      XMLString::release( &ATTR_OptionA );
      XMLString::release( &ATTR_OptionB );
   }
   catch( ... )
   {
      cerr << "Unknown exception encountered in TagNamesdtor" << endl;
   }

   // Terminate Xerces

   try
   {
      XMLPlatformUtils::Terminate();  // Terminate after release of memory
   }
   catch( xercesc::XMLException& e )
   {
      char* message = xercesc::XMLString::transcode( e.getMessage() );

      cerr << "XML ttolkit teardown error: " << message << endl;
      XMLString::release( &message );
   }
}

/**
 *  This function:
 *  - Tests the access and availability of the XML configuration file.
 *  - Configures the xerces-c DOM parser.
 *  - Reads and extracts the pertinent information from the XML config file.
 *
 *  @param in configFile The text string name of the HLA configuration file.
 */

void GetConfig::readConfigFile(string& configFile)
        throw( std::runtime_error )
{
   // Test to see if the file is ok.

   struct stat fileStatus;

   int iretStat = stat(configFile.c_str(), &fileStatus);
   if( iretStat == ENOENT )
      throw ( std::runtime_error("Path file_name does not exist, or path is an empty string.") );
   else if( iretStat == ENOTDIR )
      throw ( std::runtime_error("A component of the path is not a directory."));
   else if( iretStat == ELOOP )
      throw ( std::runtime_error("Too many symbolic links encountered while traversing the path."));
   else if( iretStat == EACCES )
      throw ( std::runtime_error("Permission denied."));
   else if( iretStat == ENAMETOOLONG )
      throw ( std::runtime_error("File can not be read\n"));

   // Configure DOM parser.

   m_ConfigFileParser->setValidationScheme( XercesDOMParser::Val_Never );
   m_ConfigFileParser->setDoNamespaces( false );
   m_ConfigFileParser->setDoSchema( false );
   m_ConfigFileParser->setLoadExternalDTD( false );

   try
   {
      m_ConfigFileParser->parse( configFile.c_str() );

      // no need to free this pointer - owned by the parent parser object
      DOMDocument* xmlDoc = m_ConfigFileParser->getDocument();

      // Get the top-level element: NAme is "root". No attributes for "root"
      
      DOMElement* elementRoot = xmlDoc->getDocumentElement();
      if( !elementRoot ) throw(std::runtime_error( "empty XML document" ));

      // Parse XML file for tags of interest: "ApplicationSettings"
      // Look one level nested within "root". (child of root)

      DOMNodeList*      children = elementRoot->getChildNodes();
      const  XMLSize_t nodeCount = children->getLength();

      // For all nodes, children of "root" in the XML tree.

      for( XMLSize_t xx = 0; xx < nodeCount; ++xx )
      {
         DOMNode* currentNode = children->item(xx);
         if( currentNode->getNodeType() &&  // true is not NULL
             currentNode->getNodeType() == DOMNode::ELEMENT_NODE ) // is element 
         {
            // Found node which is an Element. Re-cast node as element
            DOMElement* currentElement
                        = dynamic_cast< xercesc::DOMElement* >( currentNode );
            if( XMLString::equals(currentElement->getTagName(), TAG_ApplicationSettings))
            {
               // Already tested node as type element and of name "ApplicationSettings".
               // Read attributes of element "ApplicationSettings".
               const XMLCh* xmlch_OptionA
                     = currentElement->getAttribute(ATTR_OptionA);
               m_OptionA = XMLString::transcode(xmlch_OptionA);

               const XMLCh* xmlch_OptionB
                     = currentElement->getAttribute(ATTR_OptionB);
               m_OptionB = XMLString::transcode(xmlch_OptionB);

               break;  // Data found. No need to look at other elements in tree.
            }
         }
      }
   }
   catch( xercesc::XMLException& e )
   {
      char* message = xercesc::XMLString::transcode( e.getMessage() );
      ostringstream errBuf;
      errBuf << "Error parsing file: " << message << flush;
      XMLString::release( &message );
   }
}

#ifdef MAIN_TEST
/* This main is provided for unit test of the class. */

int main()
{
   string configFile="sample.xml"; // stat file. Get ambigious segfault otherwise.

   GetConfig appConfig;

   appConfig.readConfigFile(configFile);

   cout << "Application option A="  << appConfig.getOptionA()  << endl;
   cout << "Application option B="  << appConfig.getOptionB()  << endl;

   return 0;
}
#endif


 // Initialize the XML4C2 system.
   try
   {
       XMLPlatformUtils::Initialize();
   }

   catch(const XMLException& toCatch)
   {
       char *pMsg = XMLString::transcode(toCatch.getMessage());
       XERCES_STD_QUALIFIER cerr << "Error during Xerces-c Initialization.\n"
            << "  Exception message:"
            << pMsg;
       XMLString::release(&pMsg);
       return 1;
   }

1.  XML 생성하기  

DOMImplementation* impl =
  DOMImplementationRegistry::getDOMImplementation(X("Core"));

//(Root )노드 생성 
DOMDocument* doc = impl->createDocument(
         0,                    // root element namespace URI.
         X(”company”),   // root element name
         0);                   // document type object (DTD).

DOMElement* rootElem = doc->getDocumentElement();  

// node 생성와 값에 대한 값을 넣는다.
// 노드 생성
DOMElement*  prodElem = doc->createElement(X(”product”));
rootElem->appendChild(prodElem);

// 그 노드에 맞는 값
DOMText*    prodDataVal = doc->createTextNode(X(”Xerces-C”));
          prodElem->appendChild(prodDataVal);

2. 생성한 XML 파일로 저장하기.  

static XMLCh*  gOutputEncoding = 0;

DOMWriter  *theSerializer = ((DOMImplementationLS*)impl)->createDOMWriter();

// set user specified output encoding
theSerializer->setEncoding(gOutputEncoding);
XMLFormatTarget *myFormTarget;

myFormTarget = new LocalFileFormatTarget(”c://output.xml”);

theSerializer->writeNode(myFormTarget, *doc);  
3. 이미 생성된 XML 파일 검색하기  

// 이미 기본 설정한거라 보고…다운받으면 그 안에 소스가 존재합니다. 참고 바람.
// 외부 파일을 가져온다.
parser->resetDocumentPool();
doc = parser->parseURI(xmlFile);

// 파일을 검색할때는 재귀가 기본이다.

// DOMNode *n

if (n->getNodeType() == DOMNode::ELEMENT_NODE)

{

    // 노드의 이름 가져온다.
    char *name =  XMLString::transcode(n->getNodeName());

    // 노드에 대한 값을 가져온다.
    char* nodeValue = XMLString::transcode (n->getFirstChild()->getNodeValue ());

    // 속성
   if(n->hasAttributes()) {

        DOMNamedNodeMap *pAttributes = n->getAttributes();
        int nSize = pAttributes->getLength();

        // 한 노드에 붙어있는 모든 속성과 값을 가져온다.
        for(int i=0; nSize>i; i++){  
                   DOMAttr *pAttributeNode = (DOMAttr*) pAttributes->item(i);
                   // get attribute name
                   char *name = XMLString::transcode(pAttributeNode->getName());
                  
                   XERCES_STD_QUALIFIER cout << "Attribute Name : " << name << " -> “;
                   XMLString::release(&name);
                  
                   // get attribute type
                   name = XMLString::transcode(pAttributeNode->getValue());
                   XERCES_STD_QUALIFIER cout << “Attribute Value : “<< name << XERCES_STD_QUALIFIER endl;
                   XMLString::release(&name);
               }
   }
}



[출처 : http://www.soulfree.net/126]

+ Recent posts