머신러닝 & 딥러닝

C# 머신러닝 프로젝트: NOCHES 멤버의 말투를 잡아라!

카루-R 2022. 2. 20. 00:48
반응형

환영합니다, Rolling Ress의 카루입니다.

C# 으로 머신러닝 모델을 학습시킬 수 있다는 걸 알고.. 부리나케 준비했습니다. NOCHES 친구들과 함께 실험을 진행했는데, 확인해보시죠.


Rolling Ress 실험 개요

========================

진행자: 카루

진행기관: Rolling Ress

피실험자: ㄴ**, ㅂ**, ㅅ**, ㅅ**, ㅇ**

실험 개요:

- 머신러닝을 통한 기계의 언어 분류

- 피실험자의 NOCHES 카톡 데이터를 사용할 예정

- 개인의 카톡 데이터를 학습시킨 뒤 임의의 문장을 넣었을 때 누구의 말투와 가장 비슷한지 알려줌

기대 효과:

- 다른 멤버 말투 따라하기 전에 진짜 그 멤버 말투인지 검증 가능

피실험자 유의사항:

없음. 평소처럼 지내시면 됩니다.

=========================

참가를 원하지 않는 경우 댓글에 "불참"을 작성해주세요. 해당 피실험자의 데이터는 제외됩니다.


아주 대충 간단한 개요죠. 이제 시작해보겠습니다. 우선, 카카오톡에서 '대화 내용 내보내기'를 통해 지금까지의 대화를 텍스트 파일로 추출하겠습니다.

[카루] [5:27 PM] 카루가 보낸 메시지
또는
2022. 5. 27. 오전 9:19, 카루 : 카루가 보낸 메시지

카카오톡에서 대화 내용을 txt 파일로 내보내면 위와 같은 문자열로 제공됩니다. 첫 번째 대괄호 안에 이름이 들어가고, 마지막에 메시지가 들어가죠. 우리는 이걸 아래와 같이 바꾸고 싶습니다.

** 다른 친구에게서 파일을 받았는데, 다른 형식으로 뜨네요. 참고 바랍니다.

카루
카루가 보낸 메시지

어떻게 할 수 있을까요? 그냥 단순히 한 문자씩 순회하면서 대괄호를 찾고, 인덱스를 구해서 서브문자열을 빼내도 될 것 같지요. 그런데 어느 세월에 그러고 있습니까. 그래서, 여기선 정규표현식을 사용하겠습니다.

^\[(.*?)\] \[\d*:\d* [AP]M\] (.+)$
또는
^[\d. 오전후:]*?, (.*?) : (.*)$

^과 $는 문자열의 시작과 끝을 의미합니다.

\[(.*?)\] : 대괄호로 둘러싸인 문자는 모조리 캡처하여 그룹 1로 지정(? : 비탐욕적 탐색)

\[\d*:\d* [AP]M\] : 대괄호로 둘러싸이고 '숫자:숫자', AM 또는 PM이 나타나는 경우 캡처

(.+) : 뒤에 나오는 모든 문자열을 캡처하여 그룹 2로 지정

[\d. 오전후:]*?, : 쉼표 전까지, 숫자/마침표/공백/오전/오후/: 가 나오면 모조리 캡처

(.*?) : (.*) : ':'의 앞뒤를 캡처하여 각각 그룹1 / 그룹2로 지정 (? : 비탐욕적 탐색)

즉, 아래와 같이 분해되는 겁니다.

좋네요. 그럼 이걸 가지고 실전에 써보죠. 카카오톡의 경우 사진이나 동영상을 보내면 그냥 '사진', '동영상' 따위로 표시되기 때문에, 이건 나중에 C# 코드로 걸러주겠습니다.

using System.Text.RegularExpressions;
using static System.Linq.Enumerable;

void PrintBar() => Console.WriteLine(new string('=', 30));
string RefineName(string name) => /* 비공개 */

Console.OutputEncoding = System.Text.Encoding.UTF8;
PrintBar();
Console.WriteLine(@" 카카오톡 채팅 데이터 분석을 시작합니다.");
PrintBar();

Regex regex = new(@"^[\d. 오전후:]*?, (.*?) : (.*)$");
string[] lines = File.ReadAllLines(@"raw.txt");

List<(string name, string msg)> chatData = new();
foreach (string line in lines)
{
    // 정상적인 문자열이 아니면 다음 문장으로
    if (string.IsNullOrWhiteSpace(line) || !regex.IsMatch(line))
        continue;

    Match match = regex.Match(line);
    GroupCollection groups = match.Groups;

    string name = RefineName(groups[1].Value);
    string msg = groups[2].Value.Trim();

    // 의미 있는 메시지가 아니어도 다음 문장으로
    if (new[] { "사진", "동영상", "이모티콘", "삭제된 메시지입니다." }.Any(x => x == msg))
        continue;

    if (msg.Length <= 2)
        continue;

    chatData.Add((name, msg));
}

using StreamWriter file = new(@"D:\Programming\.NET\C#\NochesLanguage\Preprocessor\refined.txt");
foreach ((string name, string msg) in chatData)
{
    await file.WriteLineAsync($"{name}\t{msg}");
}

string RefineName(string) 함수는 저희 멤버의 이름이 직접적으로 노출되는 관계로, 여기에 작성하지 않았습니다. 그냥 별명을 실명으로 바꿔주는 함수입니다. 일단 출력 결과를 확인해보죠.

정상적으로 나오네요. 그럼 이걸 다시 파일로 출력하면 됩니다. 끝 부분의 foreach를 살짝 수정해줍시다.

using StreamWriter file = new("input.txt");
foreach ((string name, string msg) in chatData)
{
    await file.WriteLineAsync($"{name}\t{msg}");
}

이럼 콘솔에 출력 결과를 띄우는 게 아니라 파일로 값을 저장하게 됩니다.

좋아요, 그럼 다른 프로젝트에서 머신러닝 모델을 빌드합시다. 훈련 시간은 5분을 줬어요. 확실히 시간이 길수록 정확도가 올라갑니다. 여유가 있다면 시간은 넉넉하게 잡아주세요. 지금 로지스틱 회귀를 사용하고 있는데, 다중분류니까 아마 내부적으로는 소프트맥스 함수를 사용할 겁니다.

테스트 정확도를 높이기 위해 시간을 10분 주었습니다. 1시간 정도 학습시키면 더 정확해질 것 같긴 한데, 괜히 과적합 걸릴까봐 무섭네요. 정확도가 크게 향상된 모습이 보입니다.

ㅋㅋㅋㅋㅋ만 입력해도 사람마다 특색이 드러나기에, 다른 결과가 나오는 걸 볼 수 있어요. 그리고, 정확합니다.

실험에 참여해준 노체스 친구들을 위해 GUI 프로그램을 만들었어요.

public class GuessSender
{
    public string Message { get; set; }
    public ChatModel.ModelInput Input { get; set; }
    public ChatModel.ModelOutput Output { get; set; }
    public string SenderPrediction { get; set; }
    public List<(string sender, float score)> ScoreWithName { get; set; } = new();
    public float[] Scores { get; set; }
    public GuessSender(string msg)
    {
        Message = msg;
        Input = new() { Col1 = msg };
        Output = ChatModel.Predict(Input);

        SenderPrediction = Output.Prediction;
        Scores = Output.Score;
        var names = GetSlotNames(ChatModel.PredictEngine.Value.OutputSchema, "Score");
        foreach (int i in Range(0, Scores.Length))
            ScoreWithName.Add((names[i], Scores[i]));
    }
}

그리고 간단하게 값을 받을 수 있도록 Wrapper Class도 함께 준비했습니다. 이제 끌어다 쓰기만 하면 됩니다.

private void button1_Click(object o, EventArgs e)
{
    if (string.IsNullOrEmpty(textBox1.Text)) {
        MessageBox.Show("문장을 입력하세요.", "프로그램 터진다", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        return;
    }

    GuessSender predict = new(textBox1.Text);
    StringBuilder sb = new();

    sb.AppendLine($"입력된 문장: {textBox1.Text}");
    sb.AppendLine();

    float max = predict.Scores.Max();
    sb.AppendLine(max switch {
        > 0.90f => $"⇒ 이건 빼박 {predict.SenderPrediction}의 말투입니다.",
        > 0.80f => $"⇒ 이건 {predict.SenderPrediction}의 말투일 확률이 매우 높습니다.",
        > 0.60f => $"⇒ 이건 {predict.SenderPrediction}의 말투일 확률이 높습니다.",
        _ => $"⇒ 이건 {predict.SenderPrediction}의 말투로 추측됩니다.",
    });

    foreach (var (sender, score) in predict.ScoreWithName)
        sb.AppendLine($"{sender}: {score * 100:0.00}%");

    if (string.IsNullOrEmpty(textBox2.Text))
        textBox2.Text = sb.ToString();
    else
    {
        StringBuilder sb2 = new();
        sb2.AppendLine();
        sb2.AppendLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        sb2.AppendLine();
        textBox2.AppendText(sb2.ToString());
        textBox2.AppendText(sb.ToString());
    }
}

그렇게 해서 만든 새로운 WinForms 프로그램, NOCHES 말투 분석기를 소개합니다.

참고로 확률에 따라 뜨는 멘트가 조금씩 달라요. 이건 직접 써보면서 확인하시기 바랍니다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ


자, 험난했던 저의 첫 머신러닝 프로젝트가 끝났습니다. 머리아파요...ㅋㅋㅋㅋ 다음번엔 다시 딥러닝 기초를 다루고, 본격적으로 머신러닝 관련 개발을 이어나갈 계획입니다.

반응형