본문 바로가기
Framework & Library/Spring Framework

[Spring Framework] : 오피니언 마이닝, Opinion Mining (긍정 및 부정 분석)

by 오주현 2021. 10. 29.
반응형
오미니언 마이닝

- 형태소 분석은 품사를 뽑아내어 형태소의 어근, 어미를 파악하는 것으로 한국 밖에 없습니다.

 

- 데이터 사전

# 다양한 단어들이 모인 집단입니다.

# 형태소 분석을 통해 분석된 형태소와 데이터 사전과 매칭하여 데이터를 분석합니다.(데이터 사전은 정보이고, 정보는 곧 돈이기 때문에 오픈이 잘 안 됩니다.

# 일반적으로 JSON 형태로 데이터를 처리합니다.

 


데이터 베이스에 사용할 데이터를 유형에 맞게 생성해 줬습니다.

 

- 데이터를 넣어주었습니다.

 

- 데이터가 얼마나 들어갔는지 확인해 보면 14811개가 들어가있는 것을 확인할 수 있습니다.

select count(*) from NLP_DICTIONARY;

- SQL count (카운트) 쿼리니다.

- 만약 제대로 데이터가 안 들어가면 테이블에 있는 데이터를 지우고 다시 시도해 봅니다.

delete from NLP_DICTIONARY

- 긍정이면 + , 부정이면 - 로 설정되어 있습니다.

 

- "폴킴의 최신 노래는 모든 날, 모든 순간은 감명적이다"라는 문장을 입력했을 때 분석을 하면 3어절씩 묶어서 분석을 합니다. "폴킴의 최신 노래는" + "최신 노래는 모든" + 노래는 모든 날," - - - -이렇게 쭉 계산되어 이 문장이 긍정적인 문장인지 부정적인 문장인지 구분할 수 있게 됩니다.

 

- 문장을 분석하기 위해 계속 DB를 조회하는 방법은 DB서버에 부하를 주어 부적합한 방법입니다. 때문에 데이터 사전을 메모리에 올려두고 메모리에서 가져다 사용하는 것이 바람직합니다.( 이렇게 사용할 수 있는 이유는 데이터 사전은 변경이 빈번하게 발생하지 않기때문에 가능합니다.)

 

 


- @PostConstruct 어노테이션을 사용합니다. 이 어노테이션은 스프링 켜질 때 최초로 딱 한 번 실행되고 그 뒤로는 실행되지 않습니다. ( 메모리에 딱 한 번 올립니다. 이때, 함수는 한 개만 사용할 수 있습니다.)

 

- 문장의 단어 모음 (3어절)마다 매번 데이터 사전의 데이터만큼 반복하는 것은 불필요합니다. 이런 불필요한 연산을 최소화 하기 위해 단어 모음의 첫 초성을 기반으로 데이터 사전을 객체화 해야 합니다.

 

- 데이터 사전 구성을 ㄱ, ㄴ, ㄷ, ㄹ, ㅁ, ㅂ, ㅅ --- 순으로 초성대로 데이터 사전을 구성합니다.

 


package poly.dto;

public class NlpDTO {

	// DB 컬럼값
	private String word; //단어
	private String word_root; //단어 어근
	private String polarity; //단어
	
	public String getWord() {
		return word;
	}
	public void setWord(String word) {
		this.word = word;
	}
	public String getWord_root() {
		return word_root;
	}
	public void setWord_root(String word_root) {
		this.word_root = word_root;
	}
	public String getPolarity() {
		return polarity;
	}
	public void setPolarity(String polarity) {
		this.polarity = polarity;
	}
	
	
}

DTO

 


package poly.persistance.mapper;

import java.util.List;

import config.Mapper;
import poly.dto.NlpDTO;

@Mapper("NlpMapper")
public interface INlpMapper {

	//단어 정보 가져오기
	List<NlpDTO> getWord(NlpDTO pDTO) throws Exception;
}

IMapper

 


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- JAVA와 연결할 Mapper 파일 설정 -->
<mapper namespace="poly.persistance.mapper.INlpMapper">

	<!-- 단어 정보 가져오기 -->
	<select id="getWord" parameterType="NlpDTO" resultType="NlpDTO">
		SELECT
		WORD, WORD_ROOT, POLARITY
		FROM
		NLP_DICTIONARY
		WHERE 1=1

		<if test='word.equals("ㄱ")'>
			<![CDATA[
			AND WORD >= '가' AND WORD <= '깋'
			]]>
		</if>

		<if test='word.equals("ㄲ")'>
			<![CDATA[
			AND WORD >= '까' AND WORD <= '낗'
			]]>
		</if>

		<if test='word.equals("ㄴ")'>
			<![CDATA[
			AND WORD >= '나' AND WORD <= '닣'
			]]>
		</if>

		<if test='word.equals("ㄷ")'>
			<![CDATA[
			AND WORD >= '다' AND WORD <= '딯'
			]]>
		</if>

		<if test='word.equals("ㄸ")'>
			<![CDATA[
			AND WORD >= '따' AND WORD <= '띻'
			]]>
		</if>

		<if test='word.equals("ㄹ")'>
			<![CDATA[
			AND WORD >= '라' AND WORD <= '맇'
			]]>
		</if>

		<if test='word.equals("ㅁ")'>
			<![CDATA[
			AND WORD >= '마' AND WORD <= '밓'
			]]>
		</if>

		<if test='word.equals("ㅂ")'>
			<![CDATA[
			AND WORD >= '바' AND WORD <= '빟'
			]]>
		</if>

		<if test='word.equals("ㅃ")'>
			<![CDATA[
			AND WORD >= '빠' AND WORD <= '삫'
			]]>
		</if>

		<if test='word.equals("ㅅ")'>
			<![CDATA[
			AND WORD >= '사' AND WORD <= '싷'
			]]>
		</if>

		<if test='word.equals("ㅆ")'>
			<![CDATA[
			AND WORD >= '싸' AND WORD <= '앃'
			]]>
		</if>

		<if test='word.equals("ㅇ")'>
			<![CDATA[
			AND WORD >= '아' AND WORD <= '잏'
			]]>
		</if>

		<if test='word.equals("ㅈ")'>
			<![CDATA[
			AND WORD >= '자' AND WORD <= '칳'
			]]>
		</if>

		<if test='word.equals("ㅉ")'>
			<![CDATA[
			AND WORD >= '짜' AND WORD <= '찧'
			]]>
		</if>

		<if test='word.equals("ㅊ")'>
			<![CDATA[
			AND WORD >= '차' AND WORD <= '칳'
			]]>
		</if>

		<if test='word.equals("ㅌ")'>
			<![CDATA[
			AND WORD >= '타' AND WORD <= '팋'
			]]>
		</if>

		<if test='word.equals("ㅍ")'>
			<![CDATA[
			AND WORD >= '파' AND WORD <= '핗'
			]]>
		</if>

		<if test='word.equals("ㅎ")'>
			<![CDATA[
			AND WORD >= '하' AND WORD <= '힣'
			]]>
		</if>

	</select>
</mapper>

Mapper

 


package poly.util;

public class StringUtil {

	// 데이터 사전에 정의된 키 값을 가져오기 위한 함
	
	public static String getFirstWord(String word) {
		
		String res = "";
		
		if (word.matches("[가-깋]")) {
			res = "ㄱ";
			
		}else if (word.matches("[까-낗]")) {
			res = "ㄲ";
			
		}else if (word.matches("[나-닣]")) {
			res = "ㄴ";
			
		}else if (word.matches("[다-딯]")) {
			res = "ㄷ";
			
		}else if (word.matches("[따-띻]")) {
			res = "ㄸ";
			
		}else if (word.matches("[라-맇]")) {
			res = "ㄹ";
			
		}else if (word.matches("[마-밓]")) {
			res = "ㅁ";
			
		}else if (word.matches("[바-빟]")) {
			res = "ㅂ";
			
		}else if (word.matches("[빠-삫]")) {
			res = "ㅃ";
			
		}else if (word.matches("[사-싷]")) {
			res = "ㅅ";
			
		}else if (word.matches("[싸-앃]")) {
			res = "ㅆ";
			
		}else if (word.matches("[아-잏]")) {
			res = "ㅇ";
			
		}else if (word.matches("[자-짛]")) {
			res = "ㅈ";
			
		}else if (word.matches("[짜-찧]")) {
			res = "ㅉ";
			
		}else if (word.matches("[차-칳]")) {
			res = "ㅊ";
			
		}else if (word.matches("[카-킿]")) {
			res = "ㅋ";
			
		}else if (word.matches("[타-팋]")) {
			res = "ㅌ";
			
		}else if (word.matches("[파-핗]")) {
			res = "ㅍ";
			
		}else if (word.matches("[하-힣]")) {
			res = "ㅎ";
			
		}
		
		return res;
	}
}

StringUtil

 


package poly.service;

import poly.dto.NlpDTO;

public interface INlpService {

	//단어 정보 가져오기
	void getWord() throws Exception;
	
	int preProcessWordAnalysisForMind(NlpDTO pDTO) throws Exception;
	
	//감정 분석
	int WordAnalysisForMind(String firstWord, String text) throws Exception;
}

IService

 


package poly.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import poly.dto.NlpDTO;
import poly.persistance.mapper.INlpMapper;
import poly.service.INlpService;
import poly.util.CmmUtil;
import poly.util.StringUtil;

@Service("NlpService")
public class NlpService implements INlpService {

	private Logger log = Logger.getLogger(this.getClass());
	
	@Resource(name = "NlpMapper")
	private INlpMapper nlpMapper;
	
	//NLP_DIC를 ㄱㄴㄷㄹ 순으로 나눠서 저장한 이유는 전체 약 몇 만건의 데이터를 조회하는 것 보다.
	// 일정한 범위를 지정하여 데이터 조회 횟수를 감소하기 위해서 나눔
	// 가나다 순으로 저장될 데이터 사전들(가~하까지)
	private Map<String, List<NlpDTO>> NLP_DIC = new HashMap<String, List<NlpDTO>>();
	
	@Override
	@PostConstruct
	public void getWord() throws Exception {

		log.info(this.getClass().getName() + ".getWord start !!");
		
		// 데이터 사전 조회하기 위한 변수를 저장할 DTO(ㄱ,ㄴ,ㄷ,ㄹ..저장함)
		NlpDTO pDTO = new NlpDTO();
		
		//가나다 별 데이터가 저장될 변수
		List<NlpDTO> rList = null;
		
	// ㄱ실행
		pDTO.setWord("ㄱ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㄱ", rList);

		// ㄴ실행
		pDTO.setWord("ㄴ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㄴ", rList);
		
		// ㄷ실행
		pDTO.setWord("ㄷ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㄷ", rList);
		
		// ㄹ실행
		pDTO.setWord("ㄹ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㄹ", rList);
		
		// ㅁ실행
		pDTO.setWord("ㅁ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅁ", rList);
		
		// ㅂ실행
		pDTO.setWord("ㅂ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅂ", rList);
		
		// ㅅ실행
		pDTO.setWord("ㅅ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅅ", rList);
		
		// ㅇ실행
		pDTO.setWord("ㅇ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅇ", rList);
		
		// ㅈ실행
		pDTO.setWord("ㅈ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅈ", rList);
		
		// ㅊ실행
		pDTO.setWord("ㅊ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅊ", rList);
		
		// ㅋ실행
		pDTO.setWord("ㅋ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅋ", rList);
		
		// ㅌ실행
		pDTO.setWord("ㅌ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅌ", rList);
		
		// ㅍ실행
		pDTO.setWord("ㅍ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅍ", rList);
		
		// ㅎ실행
		pDTO.setWord("ㅎ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅎ", rList);
		
		// ㄲ실행
		pDTO.setWord("ㄲ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㄲ", rList);
		
		// ㄸ실행
		pDTO.setWord("ㄸ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㄸ", rList);
		
		// ㅃ실행
		pDTO.setWord("ㅃ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅃ", rList);
		
		// ㅆ실행
		pDTO.setWord("ㅆ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅆ", rList);
		
		// ㅉ실행
		pDTO.setWord("ㅉ");
		rList = nlpMapper.getWord(pDTO); //DB 조회
		
		if(rList ==null) {
			rList = new ArrayList<NlpDTO>();
		}
		
		NLP_DIC.put("ㅉ", rList);
		
		log.info(this.getClass().getName() + ".geWord End ! !");
		
	}

	
	// 감정 분석을 위한 문장 나누는 전처리 단계
	@Override
	public int preProcessWordAnalysisForMind(NlpDTO pDTO) throws Exception {

		log.info(this.getClass().getName() + ".wordAnalysisForMind start ! !");
		
		int res = 0;
		
		//분석할 문장 ( 특수 문자 모두 제거 )
		String text = CmmUtil.nvl(pDTO.getWord()).replaceAll("[^\\uAC00-\\uD7A3xfe0-9a-zA-Z\\\\s]", " ");
		
		// 연속된 스페이스(공백) 제거
		text = text.replaceAll("\\s{2,}", " ");
		
		log.info("text : " + text);
		
		//공백으로 단어를 나누기
		String[] textArr = text.split(" ");
		
		log.info("textArr.length : " + textArr.length);
		
		//데이터 사전의 단어의 수가 최대 3로 정의
		if(textArr.length < 4) {
			
			//문장의 첫 글자
			String firstWord = textArr[0].substring(0, 1);
			
			//데이터 분석 진행
			res = WordAnalysisForMind(firstWord, text);
		}else {
			//최대 반복 횟수
			int maxCnt = textArr.length - 2;
			
			log.info("###textArr.length : " + textArr.length);
			log.info("###maxCnt : " + maxCnt);
			
			for(int i = 0; i < maxCnt; i++) {
				
				// 문장의 첫 글자
				String firstWord2 = textArr[i].substring(0, 1);
				
				log.info("###반복 횟수 : " + i);
				String text2 = textArr[i] + " " + textArr[i + 1] + " " + textArr[i +2];
				
				res += WordAnalysisForMind(firstWord2, text2);
			}
		}
		
		log.info("Res : " + res);
		
		//로그찍기
		log.info(this.getClass().getName() + ".WordAnalysisForMind End !");
		
		return res;
	}

	//감정 분석 처리 함수
	@Override
	public int WordAnalysisForMind(String firstWord, String text) throws Exception {

		int res = 0;
		
		log.info("firstWord : " + firstWord);
		log.info("text : " + text);
		
		//데이터 사전 종류
		String dicType = StringUtil.getFirstWord(firstWord);
		
		log.info("DIC type : " + dicType);
		
		//데이터 사전에 존재하는 것만 분석 수행
		if (dicType.length() > 0) {
			
			//문장의 첫글자를 통해 해당되는 데이터 사전가져오기
			List<NlpDTO> rList = NLP_DIC.get(StringUtil.getFirstWord(firstWord));
			
			if (rList ==null) {
				rList = new ArrayList<NlpDTO>();
			}
			
			Iterator<NlpDTO> it = rList.iterator();
			
			while(it.hasNext()) {
				NlpDTO rDTO = it.next();
				
				if(rDTO == null) {
					rDTO = new NlpDTO();
				}
				
				//일치하는 값이 존재한다면,
				if(text.indexOf(CmmUtil.nvl(rDTO.getWord())) > -1) {
					log.info("DIC-word : " + CmmUtil.nvl(rDTO.getWord()));
					log.info("DIC-word getPolarity : " + CmmUtil.nvl(rDTO.getPolarity()));
					log.info("text : " + text);
					res += Integer.parseInt(CmmUtil.nvl(rDTO.getPolarity(), "0"));
					
					//데이터 사전에 검색이 되어있기 때문에 더 이상 while문을 실행하지 않는다.
					break;
					}
				}
			}
		
		return res;
	}

}

Service

 


package poly.controller;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;

import poly.dto.NlpDTO;
import poly.service.INlpService;
import poly.util.CmmUtil;

@Controller("NlpController")
public class NlpController {

	private Logger log = Logger.getLogger(this.getClass());
	
	@Resource(name = "NlpService")
	private INlpService nlpService;
	
	//문자 입력창
	@RequestMapping(value = "nlp/inputForm")
	public String inputForm() {
		
		log.info(this.getClass().getName() + ".inputForm !");
		
		return "/nlp/inputForm";
	}
	
	//긍정, 부정 분석하기
	@RequestMapping(value = "nlp/wordAnalysis")
	public String wordAnalysis(HttpServletRequest request, HttpServletResponse response,
			ModelMap model) throws Exception {
		
		log.info(this.getClass().getName() + ".wordAnalysis start !");
		
		String res = "";
		
		//분석할 문장
		String text_message = CmmUtil.nvl(request.getParameter("text_message"));
		log.info("######################"+text_message);
		
		NlpDTO pDTO = new NlpDTO();
		
		pDTO.setWord(text_message);
		
		int point = nlpService.preProcessWordAnalysisForMind(pDTO);
		
		if(point < 0) {
			res = "\"" + text_message + "\" 문장의 분석결과는 " + point + "로 부정적인 결과가 나왔습니다.";
			
		}else if (point == 0) {
			res = "\"" + text_message + "\" 문장의 분석결과는 데이터 사전에 존재하지 않아 분석이 불가능합니다.";
			
		}else {
			res = "\"" + text_message + "\" 문장의 분석결과는" + point + "로 긍정적인 결과가 나왔습니다.";
			
		}
		
		model.addAttribute("res", res);
		
		log.info(this.getClass().getName() + ".wordAnalysis end !");
		
		return "/nlp/wordAnalysis";
	}
}

Controller

 


<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>감정 분석을 위한 문장 입력폼</title>
</head>
<body>
	<h2>오피니언 마이닝 - 감정 분석</h2>
	<hr/>
	<form name = "form1" method = "post" action = "/nlp/wordAnalysis.do">
	<br/>
	분석 메시지<br/>
	<input type = "text" name = "text_message" style = "width:400px"/>
	<br/>
	<br/>
	<input type = "submit" value = "전송"/>
	</form>
</body>
</html>

inputForm.jsp

 


<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import = "poly.util.CmmUtil" %>
<%
	//Controller로부터 전달 받은 데이터
	String res = CmmUtil.nvl((String) request.getAttribute("res"));
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>감정 분석 결과</title>
</head>
<body>
	<h2>감정 분석 결과</h2>
	<hr/>
	<br/>
	<%=res %>
</body>
</html>

wordAnalysis.jsp


- 주의점 : 코드는 완성 했는데 값을 계속 ???로 받아오는 오류가 있었습니다.

인코딩을 UTF-8로 전부 다 바꿔주니 해결되었습니다.

반응형

댓글