티스토리 뷰

Apache openNLP를 이용하여 간단하게 고객센터 챗봇을 만드는 방법에 대해 알아보겠습니다. SpringBoot + Thymeleaf + Redis를 사용하여 작성하였고 전체소스는 Github를 참고해 주세요.

 

1. 라이브러리 추가

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.2'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.cus'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// basic
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

	// lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

	// NLP
	implementation 'org.apache.opennlp:opennlp-tools:2.0.0'
	implementation 'org.apache.lucene:lucene-core:8.11.1'
	implementation 'org.apache.lucene:lucene-analyzers-nori:8.11.1'

	// Redis
	implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.1.5'
}

tasks.named('test') {
	useJUnitPlatform()
}
  • NLP와 Redis에 관련된 라이브러리를 추가합니다.

 

2. 학습데이터 생성

package com.cus.center;

import com.cus.center.service.RedisService;
import opennlp.tools.doccat.DoccatFactory;
import opennlp.tools.doccat.DoccatModel;
import opennlp.tools.doccat.DocumentCategorizerME;
import opennlp.tools.doccat.DocumentSample;
import opennlp.tools.util.ObjectStream;
import opennlp.tools.util.TrainingParameters;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@SpringBootTest(classes = CustomerCenterApplication.class)
@AutoConfigureMockMvc
@ActiveProfiles("local")
class CustomerCenterApplicationTests {

  @Autowired
  RedisService redisService;

  @Test
  void 레디스데이터입력() throws Exception {
    redisService.setQuestionList("Q001", new String[]{"안녕하세요?", "안녕", "하이", "반가워", "어이", "안뇽", "잘 지내?", "뭐해?"});
    redisService.setQuestionList("Q002", new String[]{"주문을 취소하고 싶어요.", "주문취소", "주문취소방법", "취소 원해요", "취소할래요", "오더 취소"});
    redisService.setQuestionList("Q003", new String[]{"배송 상태를 확인하고 싶어요.", "배송상태", "배송", "배송 어디야?", "배송추적", "배송 됐나요?"});
    redisService.setQuestionList("Q004", new String[]{"환불이 가능한가요?", "환불", "환불가능", "환불여부", "돈 돌려줘", "환불할 수 있나요?", "환불해줘"});
    redisService.setQuestionList("Q005", new String[]{"회원 가입은 어떻게 하나요?", "회원가입", "가입방법", "회원가입하기", "회원갚", "회원가입", "가입법", "회원하러"});
    redisService.setQuestionList("Q006", new String[]{"비밀번호를 잊어버렸어요.", "비밀번호분실", "비밀번호변경"});
    redisService.setQuestionList("Q007", new String[]{"주문 확인 이메일을 못 받았어요.", "확인메일", "주문확인메일"});
    redisService.setQuestionList("Q008", new String[]{"제품의 보증 기간은 얼마나 되나요?", "보증", "보증기간"});
    redisService.setQuestionList("Q009", new String[]{"교환 정책은 어떻게 되나요?", "교환", "교환정책"});
    redisService.setQuestionList("Q010", new String[]{"문제가 해결되지 않았어요.", "해결되지않음", "여전히", "문제해결"});
    redisService.setQuestionList("Q011", new String[]{"고마워", "감사", "고맙"});
    redisService.setQuestionList("Q012", new String[]{"결제가 안돼요.", "결제문제", "결제오류", "결제 불가", "결제 안됨", "결제가 안됩니다"});
    redisService.setQuestionList("Q013", new String[]{"계정이 잠겼어요.", "계정잠김", "로그인 불가", "계정 잠김", "계정 로그인 불가"});
    redisService.setQuestionList("Q014", new String[]{"앱이 작동하지 않아요.", "앱오류", "앱 문제", "앱 안됨", "어플리케이션 오류", "어플 문제"});
    redisService.setQuestionList("Q015", new String[]{"연락처를 변경하고 싶어요.", "연락처변경", "전화번호변경", "핸드폰 번호 변경", "휴대폰번호 변경"});
    redisService.setQuestionList("Q016", new String[]{"할인 코드가 적용되지 않아요.", "할인코드오류", "할인코드문제", "쿠폰오류", "쿠폰안됨", "할인코드 안됨"});


    redisService.setAnswerList("Q001", "안녕하세요! 무엇을 도와드릴까요?");
    redisService.setAnswerList("Q002", "주문 취소를 원하시면, 주문 번호와 함께 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q003", "배송 상태를 확인하려면 주문 번호를 입력해 주세요. 고객 서비스 센터에서 상태를 확인해 드리겠습니다.");
    redisService.setAnswerList("Q004", "제품에 따라 다릅니다. 일반적으로 제품 수령 후 30일 이내에 환불이 가능합니다. 자세한 사항은 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q005", "회원 가입은 웹사이트의 '회원 가입' 버튼을 클릭하고 필요한 정보를 입력하면 됩니다.");
    redisService.setAnswerList("Q006", "비밀번호를 잊으셨다면, 로그인 페이지에서 '비밀번호 찾기'를 클릭하여 이메일을 통해 비밀번호를 재설정할 수 있습니다.");
    redisService.setAnswerList("Q007", "주문 확인 이메일이 발송되지 않았다면, 스팸 폴더를 확인해 보시고, 이메일 주소가 올바른지 확인해 주세요. 여전히 문제를 해결하지 못했다면 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q008", "제품의 보증 기간은 제품에 따라 다릅니다. 보증 기간에 대한 정보는 제품 설명서나 웹사이트에서 확인할 수 있습니다.");
    redisService.setAnswerList("Q009", "제품 교환은 제품 수령 후 30일 이내에 가능합니다. 교환을 원하시면 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q010", "문제가 해결되지 않았으면, 구체적인 문제를 자세히 설명해 주시면 추가로 도와드리겠습니다. 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q011", "별말씀을요");
    redisService.setAnswerList("Q012", "결제가 진행되지 않을 경우, 사용 중인 결제 수단의 상태를 확인하시고, 문제가 지속되면 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q013", "계정이 잠긴 경우, 보안상의 이유로 잠긴 것일 수 있습니다. 계정 복구를 위해 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q014", "앱이 정상적으로 작동하지 않으면, 최신 버전으로 업데이트 후 다시 시도해 주세요. 문제가 지속되면 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q015", "연락처를 변경하시려면, 계정 설정에서 연락처를 업데이트 하시거나, 고객 서비스 센터에 문의해 주세요.");
    redisService.setAnswerList("Q016", "할인 코드가 적용되지 않을 경우, 코드가 유효한지 확인하고 다시 시도해 주세요. 문제가 해결되지 않으면 고객 서비스 센터에 문의해 주세요.");

  }

  @Test
  void 학습데이터생성() throws Exception {
    Map<Object, Object> qnaMap = redisService.getQuestionList();

    List<DocumentSample> sampleList = new ArrayList<>();
    for (Map.Entry<Object, Object> map : qnaMap.entrySet()) {
      String key = String.valueOf(map.getKey());
      DocumentSample sample = new DocumentSample(key, redisService.getQuestionList(key));
      sampleList.add(sample);
    }

    ObjectStream<DocumentSample> sampleStream = new ListObjectStream<>(sampleList);

    TrainingParameters params = new TrainingParameters();
    params.put(TrainingParameters.ITERATIONS_PARAM, 150); // 반복 학습 횟수
    params.put(TrainingParameters.CUTOFF_PARAM, 1);       // 최소 카운트 기준

    // 모델 학습
    DoccatModel model = DocumentCategorizerME.train("ko", sampleStream, params, new DoccatFactory());

    // 모델 저장 경로
    File modelFile = new File("src/main/resources/models/doccat.bin");
    modelFile.getParentFile().mkdirs();  // 디렉토리 생성

    // 모델 파일 저장
    try (FileOutputStream modelOut = new FileOutputStream(modelFile)) {
      model.serialize(modelOut);
    }

    System.out.println("모델 생성 완료 : " + modelFile.getAbsolutePath());

  }

  public static class ListObjectStream<T> implements ObjectStream<T> {
    private final List<T> list;
    private int index = 0;

    public ListObjectStream(List<T> list) {
      this.list = list;
    }

    @Override
    public T read() throws IOException {
      if (index < list.size()) {
        return list.get(index++);
      }
      return null;
    }

    @Override
    public void reset() throws IOException, UnsupportedOperationException {
      index = 0;
    }

    @Override
    public void close() throws IOException {
      // No resources to close
    }
  }

}
  • TestClass를 이용하여 레디스데이터입력()을 호출한 뒤, 학습데이터생성()을 통해 doccat.bin 파일을 생성합니다.

 

3. 챗봇서비스코드 작성

package com.cus.center.service;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import opennlp.tools.doccat.DoccatModel;
import opennlp.tools.doccat.DocumentCategorizerME;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.ko.KoreanAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ChatService {

  private DocumentCategorizerME categorizer;
  private final RedisService redisService;

  @PostConstruct
  public void init() throws Exception {
    // 모델 초기화
    try (InputStream modelIn = getClass().getResourceAsStream("/models/doccat.bin")) {
      DoccatModel model = new DoccatModel(modelIn);
      categorizer = new DocumentCategorizerME(model);
    }
  }

  public String generateResponse(String userInput) {
    // 한국어 문장 분석 및 토큰화
    String[] tokens = tokenizeKorean(userInput);

    // 카테고리 분류
    double[] outcomes = categorizer.categorize(tokens);
    String category = categorizer.getBestCategory(outcomes);

    // 카테고리에 따른 응답 생성
    String result = redisService.getAnswerData(category);

    if (result == null || result.isEmpty()) {
      return "알아듣지 못했어요.";
    } else {
      return result;
    }
  }

  private String[] tokenizeKorean(String text) {
    List<String> tokens = new ArrayList<>();

    // KoreanAnalyzer는 매번 새로 생성하여 사용
    try (Analyzer analyzer = new KoreanAnalyzer()) {
      try (var tokenStream = analyzer.tokenStream(null, new StringReader(text))) {
        CharTermAttribute termAttr = tokenStream.addAttribute(CharTermAttribute.class);
        OffsetAttribute offsetAttr = tokenStream.addAttribute(OffsetAttribute.class);
        tokenStream.reset();
        while (tokenStream.incrementToken()) {
          tokens.add(termAttr.toString());
        }
        tokenStream.end();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return tokens.toArray(new String[0]);
  }
}
  • KoreanAnalyzer를 이용해 한국어 문장 분석 및 토큰화를 진행합니다.
  • 분류된 카테고리로 redis hash에서 해당 value를 찾아 답변을 return 합니다.

 

4. 실행화면


모든 내용을 포스팅하기엔 소스가 길어 전체소스는 Github를 참고해 주세요.

감사합니다.

최근에 올라온 글
Total
Today
Yesterday