한글 조사를 지원하는 AIML 구현

채팅봇을 추가하고자 AIML을 도입했다. 물론 정식 결정된 바는 아니라 혼자 깔짝대는 수준이다. 어쨌거나 AIML 인터프리터를 직접 짤 시간과 자원이 없기 때문에 C#으로 개발한 Program#을 쓰기로 했다. 그런데 아마도 대부분의 AIML 봇이 그렇겠지만, 영어권에서 개발된 프로그램인만큼 조사 같은 한국어의 특성을 제대로 반영하지 못한다. 여태껏 발견한 문제 중 절반은 처리했는데, 가장 어려운 절반이 남아있어 걱정이다.

오늘은 그나마 문제 중에서 가장 쉽게 해결한 응답시의 조사처리에 대해 정리한다.

<category>
	<pattern>너 * 알아</pattern>
	<template>
	  <star/>을 내가 어떻게 알아.
	</template>
</category>

이와 같은 AIML이 있으면 다음과 같은 응답을 예상하게 된다.


인간: 너 사과 알아?
: 사과를 어떻게 알아?

이때 문제는 <star/>, 즉 사과 뒤에 붙는 조사를 동적으로 결정해야 한다는 점이다. ‘<star/>을’이라고 했지만 <star/> 값이 ‘사과’가 되는 바람에 문제가 발생했다.

이 문제는 비교적 처리하기 쉬웠는데 AIMLTagHandler 클래스를 상속 받아 기능확장할 수 있도록 Program#을 개발했기 때문이다. 우선 나는 다음과 같은 AIML을 생각했다.

<category>
	<pattern>너 * 알아</pattern>
	<template>
	  <josa suffix="을를"><star/></josa> 내가 어떻게 알아.
	</template>
</category>

그리고 <josa> 태그를 처리할 사용자 정의 핸들러는 다음과 같이 짰다.

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;

using AIMLbot.Utils;

namespace imedia.AimlTags
{
    [CustomTag]
    public class josa : AIMLTagHandler
    {
        private readonly string _tagname = "josa";

        public josa()
        {
            this.inputString = _tagname;
        }

        protected override string ProcessChange()
        {
            if( string.Compare(this.templateNode.Name, _tagname, true) != 0 )
                return string.Empty;

            if (this.templateNode.Attributes.Count != 1)
                return string.Empty;

            string subject = this.templateNode.InnerText;
            string suffix = string.Empty;

            if (subject.Length == 0)
                return string.Empty;

            foreach(XmlAttribute xmlAttr in this.templateNode.Attributes)
            {

                if(xmlAttr.Name == "suffix")
                    suffix = xmlAttr.Value;
            }

            return Combine(subject, suffix);
        }

        internal static string Combine(string subject, string suffix)
        {
            if (suffix != "은는" "amp;"amp; suffix != "이가" "amp;"amp; suffix != "과와" "amp;"amp; suffix != "을를")
            {
                if (subject.Length > 0)
                {
                    return subject;
                }
                return string.Empty;
            }

            char[] josaCandidates = suffix.ToCharArray();


            return Combine(subject, josaCandidates);
        }

        private static string Combine(string subject, char[] josaCadidates)
        {
            char lastChar = subject[subject.Length - 1];
            string lastCharString = lastChar.ToString();

            HangulUtil.LanguageType lang = HangulUtil.GetLangageType(lastCharString);
            if(
                lang != HangulUtil.LanguageType.Korean
                "amp;"amp; lang != HangulUtil.LanguageType.KoreanJaum
                "amp;"amp; lang != HangulUtil.LanguageType.KoreanMoum
                ) // 한국어가 아닐 때
            {
                RegexOptions options = RegexOptions.IgnorePatternWhitespace;
                options |= RegexOptions.IgnoreCase;

                Regex regex = new Regex("[aeiou]", options);
                if (regex.IsMatch(lastCharString) == true)
                    return subject + josaCadidates[1];

                return subject + josaCadidates[0];
            }

            // 한국어일 때
            char[] decomposed = HangulUtil.DivideJaso(lastChar);

            if (
                decomposed.Length < 3
                || (decomposed.Length == 3 "amp;"amp; decomposed[2] == ' ')
                )
            {
                return subject+josaCadidates[1];
            }

            return subject+josaCadidates[0];
        }
    }
}

여기서 HangulUtil.DivideJaso란 게 등장한다. 이 메서드의 역할은 하나의 한글 문자를 초성, 중성, 종성으로 나누는 것이다. 누군가 이 기능을 개발해놓아서 신경쓰지 않아도 됐다.

using System;
using System.Text;


namespace imedia.AimlTags
{
    internal static class HangulUtil
    {
        // 초성, 중성, 종성에 대한 코드 테이블.
        private static string m_ChoSungTbl = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ";
        private static string m_JungSungTbl = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ";
        private static string m_JongSungTbl = " ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ";

        private static ushort m_UniCodeHangulBase = 0xAC00;
        private static ushort m_UniCodeHangulLast = 0xD79F;

        //자소 결합
        public static string MergeJaso(string choSung, string jungSung, string jongSung)
        {
            int ChoSungPos, JungSungPos, JongSungPos;
            int nUniCode;

            ChoSungPos = m_ChoSungTbl.IndexOf(choSung);     // 초성 위치
            JungSungPos = m_JungSungTbl.IndexOf(jungSung);   // 중성 위치
            JongSungPos = m_JongSungTbl.IndexOf(jongSung);   // 종성 위치

            // 앞서 만들어 낸 계산식
            nUniCode = m_UniCodeHangulBase + (ChoSungPos * 21 + JungSungPos) * 28 + JongSungPos;

            // 코드값을 문자로 변환
            char temp = Convert.ToChar(nUniCode);
            return temp.ToString();
        }

        //자소 분리(초성+중성+종성)
        public static char[] DivideJaso(char hanChar)
        {
            int ChoSung, JungSung, JongSung;

            ushort temp = 0x0000;

            try
            {
                temp = Convert.ToUInt16(hanChar); //Char을 16비트 부호없는 정수형 형태로 변환
            }
            catch (Exception e)
            {
                //return e.ToString();
            }

            if ((temp < m_UniCodeHangulBase) || (temp > m_UniCodeHangulLast)) return null;

            //초성자, 중성자, 종성자 Index계산
            int nUniCode = temp - m_UniCodeHangulBase;
            ChoSung = nUniCode / (21 * 28);
            nUniCode = nUniCode % (21 * 28);
            JungSung = nUniCode / 28;
            nUniCode = nUniCode % 28;
            JongSung = nUniCode;

            return new char[] { m_ChoSungTbl[ChoSung], m_JungSungTbl[JungSung], m_JongSungTbl[JongSung] };
        }

        public enum LanguageType
        {
            Unknown
            , OverBound
            , OneByteChar
            , Korean // 완성형 문자
            , KoreanJaum // 자음
            , KoreanMoum // 모음
            , Chinese // 한글 한자
            , Japanese // 일본 가나
        }

        /// <summary>
        /// KS-완성형 코드 기반 문자 판별 함수
        /// </summary>
        public static LanguageType GetLangageType(string sourceString)
        {
            if (sourceString.Length > 2)
                return LanguageType.OverBound;

            byte[] _tmp = Encoding.Default.GetBytes(sourceString);

            if (_tmp.Length != 2)
                return LanguageType.OneByteChar;

            if ((_tmp[1] > 0xFE) || (_tmp[1] < 0xA1))
                return LanguageType.Unknown;

            if ((_tmp[0] >= 0xB0) && (_tmp[0] <= 0xC8)) // 완성형 문자
                return LanguageType.Korean;
            else if (_tmp[0] == 0xA4) // 한글 자모
            {
                if (_tmp[1] >= 161 && _tmp[1] <= 190) // 자음
                    return LanguageType.KoreanJaum;
                else // 모음
                    return LanguageType.KoreanMoum;
            }
            else if ((_tmp[0] >= 0xCA) && (_tmp[0] <= 0xFD)) // 한글 한자
                return LanguageType.Chinese;

            if ((_tmp[0] >= 0xAA) && (_tmp[0] <= 0XAB)) // 일본 가나
                return LanguageType.Japanese;

            return LanguageType.Unknown;
        }
    }
}

끝이다! 이외에 Program#에 한글지원을 추가하려면 해결해야 할 이슈가 많다. 완전히 해결했다 싶으면 소스 코드는 공개하겠다.

Advertisements

최 재훈

블로그, 페이스북, 트위터 고성능 서버 엔진, 데이터베이스, 지속적인 통합 등 다양한 주제에 관심이 많다.
Close Menu