채팅봇을 추가하고자 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 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.