채팅봇을 추가하고자 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#에 한글지원을 추가하려면 해결해야 할 이슈가 많다. 완전히 해결했다 싶으면 소스 코드는 공개하겠다.
Author Details
Kubernetes, DevSecOps, AWS, 클라우드 보안, 클라우드 비용관리, SaaS 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.