첫 머신러닝 라이브러리 사용기

첫 머신러닝 라이브러리 사용기

개발일기

 

원체 실용주의자라 내가 필요하다고 느끼면 미친 듯이 몰입해서 학습하고 사용하는 편인데,

스스로 쓸모가 없다고 느끼면 아예 관심을 안 가져버리는 성격이다.

 

30살 다되도록 영어로 대화해 본 일이 버거킹에서 어떤 외국인이

키오스크를 어떻게 쓰냐고 물어본 적 한 번밖에 없었다.

그 정도로 평소에 영어를 접할일이 없었기 때문에 큰 관심을 갖지 않고 있었다.

 

그런데 최근에 목표가 생겨서 영어를 공부하기 시작했다.

우선 실력향상을 위해서 원서를 읽어야 할 일이 매우 많아졌기 때문이고,

나중에 오픈소스 활동을 해보고 싶어졌기 때문이다.

그러자면 외국인들이 무슨 소리를 하는지 잘 알아먹고 키배(?)도 뜨고 그래야 할 것 같다.

그러니 읽기와 쓰기만큼이라도 열심히 갈고닦아 보려는 중이다.

 

영어 문법을 중학교 1학년 수준부터 기초공부를 차근차근하고 있는데,

생각보다 아는 단어가 많이 부족하다 여겼다.

그래서 기왕에 외울 단어인데, 내가 접하고 사용할 단어 위주로 외워보자는 생각에

내가 자주 보는 스프링 부트, 스프링 시큐리티 문서 같은 것들을 파싱해서

그 문서에서 많이 나온 단어들을 보기 좋게 정제해 단어 사전을 만들면 큰 도움이 되지 않을까?라는 생각을 했다.

 

처음엔 단순히 공식문서의 URI를 호출하여 바로 파싱을 하려고 했다.

헌데 URI를 호출하고 보니 문서가 제대로 렌더링이 되어있질 않았다.

그래서 문서를 직접 HTML 파일로 다운로드하여 이를 스트림으로 읽어 들여 파싱하기로 계획을 바꿨다.

 

public class DocumentParser {
    public String read(String path) {
        StringBuilder sb = new StringBuilder();
        File file = new File(path);
        try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
            String read;
            while((read = br.readLine()) != null) {
                sb.append(read).append("\n");
            }
        }
        catch(IOException e) {
            log.error(e.getMessage());
        }
        return sb.toString();
    }
	...
}

 

HTML 문서를 읽고 보니 온갖 HTML 태그가 덕지덕지 붙어있었다.

이를 제거하기 위한 로직을 또 작성했다.

 

public Set<String> parsing(String html) throws IOException {
        StringBuilder sb = new StringBuilder();
        Pattern pattern = Pattern.compile("<p>.*</p>");
        Matcher matcher = pattern.matcher(html);
        
        while(matcher.find()) {
            String s = " " + html.substring(matcher.start(), matcher.end())
                                 .replaceAll("<b>", "").replaceAll("</b>", "")
                                 .replaceAll("<a [^<>]*>", "").replaceAll("</a>", "")
                                 .replaceAll("<p>", "").replaceAll("</p>", "")
                                 .replaceAll("<sup [^<>]*>", "").replaceAll("</sup>", "")
                                 .replaceAll("<span [^<>]*>", "").replaceAll("</span>", "")
                                 .replaceAll("<i [^<>]*>>", "").replaceAll("</i>", "")
                                 .replaceAll("<table [^<>]*>>", "").replaceAll("</table>", "")
                                 .replaceAll("<block [^<>]*>>", "").replaceAll("</block>", "")
                                 .replaceAll("<ul [^<>]*>>", "").replaceAll("</ul>", "")
                                 .replaceAll("<li [^<>]*>>", "").replaceAll("</li>", "")
                                 .replaceAll("<div [^<>]*>>", "").replaceAll("</div>", "")
                                 .replaceAll("<h [^<>]*>>", "").replaceAll("</h>", "")
                                 .replaceAll("www\\.", "").replaceAll("http", "")
                                 .replaceAll("\\.com", "").replace(".", " ")
                                 .replaceAll("\\[[^\\[\\]]*\\]", "");
            
            String s1 = splitCamelCase(s);
            if(s1.contains(" ")) {
                String[] strings1 = s1.split(" ");
                for(String s2 : strings1) {
                    sb.append(s2).append(" ");
                }
            }
            else {
                sb.append(s);
            }
        }
        ...
}

private String splitCamelCase(String s) {
 	return s.replaceAll(String.format("%s|%s|%s",
                                  "(?<=[A-Z])(?=[A-Z][a-z])",
                                  "(?<=[^A-Z])(?=[A-Z])",
                                  "(?<=[A-Za-z])(?=[^A-Za-z])")," ");
}

 

보통 HTML 문서에서 서술하는 부분은 p 태그 안에 들어있으므로 p 태그 위주로 읽고

읽어 들인 p 태그에서 잡다한 HTML 태그를 제거해준다.

또한 그 외에 필요 없는 문자들도 정리를 해준 후

프로그래밍 언어에서 자주 사용되는 용어들을 또 분리해줬다

이건 무슨 소리냐면

 

InputStreamReader
Input
Stream
Reader

 

이처럼 카멜 케이스파스칼 케이스로 작성된 프로그래밍 용어들은 쪼개면 여러 단어로 바뀐다.

여기까지 하고 보니 그럼에도 불구하고 너무 무의미한 단어가 많았다.

 

이를 깔끔하게 처리하려고 많은 생각을 해보고 정보를 뒤져보다가 머신러닝 라이브러리를 알게 됐다.

여러 종류가 있었는데 그중 Apache OpenNLP라는 자연어 처리 라이브러리를 사용해보게 됐다.

 

항상 그렇듯이 우선 공식문서를 보면서 사용법을 익히는 게 먼저다.

나 같은 경우 단순히 단어장을 만드는 게 목표기 때문에 문장을 처리할 필요가 없다.

따라서 Tokenizer를 이용하면 될 것 같았다.

 

public Set<String> parsing(String html) throws IOException {
        StringBuilder sb = new StringBuilder();
        Pattern pattern = Pattern.compile("<p>.*</p>");
        Matcher matcher = pattern.matcher(html);
        
        while(matcher.find()) {
            String s = " " + html.substring(matcher.start(), matcher.end())
                                 .replaceAll("<b>", "").replaceAll("</b>", "")
                                 .replaceAll("<a [^<>]*>", "").replaceAll("</a>", "")
                                 .replaceAll("<p>", "").replaceAll("</p>", "")
                                 .replaceAll("<sup [^<>]*>", "").replaceAll("</sup>", "")
                                 .replaceAll("<span [^<>]*>", "").replaceAll("</span>", "")
                                 .replaceAll("<i [^<>]*>>", "").replaceAll("</i>", "")
                                 .replaceAll("<table [^<>]*>>", "").replaceAll("</table>", "")
                                 .replaceAll("<block [^<>]*>>", "").replaceAll("</block>", "")
                                 .replaceAll("<ul [^<>]*>>", "").replaceAll("</ul>", "")
                                 .replaceAll("<li [^<>]*>>", "").replaceAll("</li>", "")
                                 .replaceAll("<div [^<>]*>>", "").replaceAll("</div>", "")
                                 .replaceAll("<h [^<>]*>>", "").replaceAll("</h>", "")
                                 .replaceAll("www\\.", "").replaceAll("http", "")
                                 .replaceAll("\\.com", "").replace(".", " ")
                                 .replaceAll("\\[[^\\[\\]]*\\]", "");
            
            String s1 = splitCamelCase(s);
            if(s1.contains(" ")) {
                String[] strings1 = s1.split(" ");
                for(String s2 : strings1) {
                    sb.append(s2).append(" ");
                }
            }
            else {
                sb.append(s);
            }
        }
        
        String s = sb.toString().trim().toLowerCase()
                     .replaceAll("[^a-zA-Z\\s\\.]", " ")
                     .replaceAll(" +", " ");
        
        InputStream inputStream = getClass()
                .getResourceAsStream("/models/en-token.bin");
        TokenizerModel model = new TokenizerModel(inputStream);
        TokenizerME tokenizer = new TokenizerME(model);
        String[] tokens = tokenizer.tokenize(s);
        
        Set<String> set = new HashSet<>();
        for(String s1 : tokens) {
            if(s1.length() > 2) {
                set.add(s1);
            }
        }
        return set;
    }

 

우선 HashSet을 이용해 중복제거를 할 것이기 때문에 정제된 단어를 모두 소문자로 변경한다.

이후 영어 소문자, 영어 대문자, 공백, 점(.)을 제외한 모든 문자를 제거해준다.

그리고 두 칸 이상 떨어진 공백을 모두 한 칸으로 바꿔주면

단어인지 아닌지 모를 것들이 공백한칸 단위로 구분이 될 것이다.

이를 Apache-OpenNLP 라이브러리를 활용해 영단어가 학습된 모델과 비교하여 구분해준 후

길이가 2보다 큰 단어들을 모두 HashSet에 집어넣어줬다.

이후 테스트 코드를 돌려보는데, 제대로 정제가 됐는지 눈으로 확인하고 싶으므로

평소엔 잘 쓰지 않는 표준 출력 로직을 추가해줬다.

 

public class DocumentParserTest {
    
    @ParameterizedTest
    @DisplayName("Document_파싱")
    @MethodSource("whereDocuments")
    public void parsingDocumentationFromHTMLFile(String path) throws Exception {
        // when
        DocumentParser parser = new DocumentParser();
        String html = parser.read(path);
        Set<String> set = parser.parsing(html);
    
        StringBuilder sb = new StringBuilder();
        for(String s : set) {
            sb.append(set + "\n");
        }
        System.out.println(sb);
    
        // then
        assertThat(set).isNotNull();
    }
    
    // given
    private static Stream<Arguments> whereDocuments() {
        return Stream.of(
                Arguments.of("src/test/resources/springboot_document.html"),
                Arguments.of("src/test/resources/springsecurity_document.html")
                        );
    }
    
}

 

 

차근차근 읽어보니 나름 만족할만한 퀄리티로 단어들이 뽑혀 나온다.

머신러닝, 딥러닝 같은 단어는 자주 들어봤는데 관심은 잘 안갖고 있다가

갑자기 머신러닝이 필요해져서 사용해보니 정말 좋은 것 같다는 생각이 들었다.

아무튼 이제 이를 잘 활용해서 단어 사전을 만들어봐야겠다.

 


© 2022. All rights reserved.