336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

목차

1. SKT-AI/KoGPT2와 자연처(NLP)

2. 데이터 전처리 (나무위키, 블로그)

3. KoGPT2와 기능구현 (인풋 아웃풋 조정)

4. 짧은 텍스트 / 문장유사도

5. 배포 (구글 애즈, GA, AWS)

 


드디어 파이널 프로젝트 글도 마무리 ㅠㅠㅠ 프로젝트 중 안 좋은 일이 있어서 서비스 배포 기간에 참석을 하지 못 했다. 원래는 GPT3를 활용하여 기업 설명을 완성시켜주는 서비스도 만드려고 했으나 불가능했고, 프로젝트 이후에나 GPT3를 다뤄봐야겠다고 생각했다.

 

 

 

서비스 이름도 조금 더 찰떡 같은 이름을 지으려고 했으나 막상 지어보니 별로라서 그냥 그대로 유지했다. copylight (카피의 한 줄기 빛이 되다.) copyfast (카피를 빠르게! - 근데 패스트 발음을 하면 전염병 의미가 있어서 패스), copydai (ai로 카피 만들기 좋은 날 카피데이) 등등 ..

 

예전에 카피라이팅에 도전을 많이 했었는데 아무리 생각해도 언어유희나 임팩트 있는 문구를 창작하는 건 재능이지 않을까 싶다. 노력으로는 되지 않아서 우리가 만든 것이 인공지능 카피라이팅이 아닐까? 

 

 

 

Django를 이용해 웹 구현을 했고, 나는 디자인 부분에서 조금 도움을 줄 수 있었다. 원래는 로딩창 부분에서 Tip이나 튜토리얼 부분을 gif로 만들어서 심심하지 않게 하려고 했다. 하지만 장고 개발하던 팀원이 좀 무리지 않냐고 해서 수용했다.

 

 

 

 

 

 

 

추가적으로 장고로 로컬을 이용한 연결이 끝이 아닌, AWS를 통해 인스턴스를 생성하여 우분투 서버를 사용하여 웹 서비스를 배포했다. 파파고로 한국어를 영어로 자동 번역하는 서비스도 있었는데 여기서 API 키를 그대로 노출시면 안 되다고 해서.. 중간에 과정이 많이 있던 걸로 기억한다. 

 

그리고 aws에서 얻은 탄력적 ip주소를 사용하여 접속하는 것 보다 도메인 주소를 지정하여, 서비스 내용을 알 수 있게끔 가비아 사이트를 이용해 도메인을 연결하였다. 나중에 서비스 이름을 바꿀 수도 있어서 간단하게 990원짜리를 이용하였다.

 

 

 

예전에 유튜브를 보면서 이 부분은 꼭 해봐야겠다 생각이 들어서, GA와 구글 애즈를 사용해야 된다고 강력하게 주장했다. 이유를 설명하자면 국비지원 교육의 단점에 대해서도 잠깐 얘기를 해야하는데.. (검색을 하고 우연히 들어오시는 분들을 위해서 적어두겠습니다.)

 

- SSAFY처럼 커리큘럼이 체계적이지 않고, 생각보다 몇몇 강사분들은 전문적이지 않다. 

- 포트폴리오용 토이프로젝트를 많이 진행하는데 이걸로는 메리트가 없어보인다. 본인이 해야한다. 공부든 뭐든 원래 그렇듯이

(학부생이나 관련 개발자들은 최소 몇 개월에 걸쳐서 프로젝트를 진행할텐데, 고작 일주일의 프로젝트로 뭐를 했다고 할 수 있을까?)

 

위 유튜버의 말대로, 남들 다 하는 토이프로젝트에서 남들과는 차별점을 둘 수 있는 것이 배포를 해보고, 유저의 피드백이나 오류들을 개선하는 과정이지 않을까 싶다. 그리고 유저의 피드백을 받기 위해서 구글 애즈로 현업자들 상대로 광고를 집행해 노출도 시키고, 직접적인 피드백 받기가 어려우면 GA로 어떤 페이지에서 오래 머물렀고, 이탈이 있었는지 등을 분석하면 좋을거라 생각했다.

 

 

 

 

광고 캠페인 집행 : 유튜브와 구글에서 검색을 통해서 금방 제작이 가능했다. 물론 효율적인 광고를 위해서는 세부적으로 다뤄야할 부분들이 많았다. 처음 광고를 해봐서 그런지 약간 돈만 날린 부분도 있었고, 다른 광고 캠페인들처럼 전환이 이뤄졌다할만한 결과가 없기 때문에 성과 측정은 어려웠다.

 

 

 

 

 

구글애널리틱스 자격증은 혹시나 필요할까봐 따긴 땄는데, 직접 이런 식으로 통계를 본 적은 처음이다. 물론 유튜브로 구독자 분석, 채널 분석을 많이 하긴 했지만 그거와는 조금 다른 분야였다. GA 코드는 <head> 태그 바로 뒤에 복붙하면 됩니다. 이것도 GA 자격증을 따기 위해서 나오는 문제중 하나입니다. ㅋㅋㅋ

 

원래는 사이트 주소를 오픈하면 좋은데, 현재 서버에 문제가 좀 있어서 수정중이라고 들었습니다. 

 

앞으로 할 부분은 자연어 공부를 조금 더 하면서 유튜브 채널, 구독자 분석을 진행하려고 합니다. 감사합니다.

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

목차

1. SKT-AI/KoGPT2와 자연처(NLP)

2. 데이터 전처리 (나무위키, 블로그)

3. KoGPT2와 기능구현 (인풋 아웃풋 조정)

4. 짧은 텍스트 / 문장유사도

5. 배포 (구글 애즈, GA, AWS)

 


KoGPT2로 슬로건, 광고문구를 생성하고 이를 어떻게 개선할 것인가에 대해서 이어서 작성하겠습니다. 처음에 포스팅 하나에 넣으려고 했는데 생각보다 길어져서 나눴습니다. 이번 포스팅은 아웃풋과 인풋을 어떻게 조정했는지에 대해서 쓰겠습니다.

 

 

 짧은 텍스트 / 문장 유사도 찾기 

사용 이유 와 목적 : 인풋 데이터에 '금융'과 관련된 설명을 넣었는데, 갑자기 '좋은 일자리 만들어주세요'라는 문구가 뜬금없이 튀어나오게 된다. 데이터가 충분하면 이런 일이 없겠지만 추가로 모을 수는 없어서.. 이러한 결과값들을 최대한 배제하는 방법에 대해서 생각했다. 

 

- TF-IDF, CNN을 활용을 활용한 슬로건 분류

- Word2Vec을 사용해서 자주 등장하는 단어와 유사한 값을 지닌 단어가 포함된 문장을 노출

- 문장 유사도 (키워드, 요약, sentence transformer)

 

 

 

 

4-3 파이썬 팀프로젝트 CNN 카테고리 분류 모델 학습 및 평가

네 번째 프로젝트 1. 간단한 intro 2. 웹 크롤링 및 전처리 3. 모델 학습 및 평가 프로젝트를 하면서 느낀 보완사항은 : -데이터의 길이가 너무 짧으면 단어를 추출하는데 한계가 있고, 과적합이 발

0goodmorning.tistory.com

TF-IDF, CNN 카테고리 분류

슬로건, 광고문구에 인풋 값과 관련 없는 결과가 나올 확률은 크지 않기 때문에(이후에 보여드립니다), 카테고리(y)와 슬로건(x)을 학습시켜서 모델링을 해봤다.(이전 코드 참고)

 

하지만 아무리 수정을 해도 CNN 모델의 성능은 좋아지지 않았다. 40% 정확도가 최선으로 나왔는데, 이를 생각해보면 카테고리와 슬로건의 상관관계가 거의 않아서 정확도 개선이 되지 않는 것으로 보인다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from konlpy.tag import Kkma
import collections 
 
slogan_list2 = []
for slogan in slogan_list :
    kkma = Kkma()
    slogan_ = kkma.nouns(slogan)
    slogan_list2.append(slogan_)
 
#중첩 리스트 제거
list_removed = sum(slogan_list2, []) 
 
#단어 카운팅
dict={} 
dict=collections.Counter(list_removed) 
dict = sorted(dict.items(), key=lambda x: x[1], reverse=True)
#print(dict)
 
#가장 많이 나오는 
keyword = next(iter(dict))[0#사전 첫번째 단어
print(keyword)
cs

꼬꼬마로 형태소 분류를 해서 가장 자주 나오는 명사들의 개수를 카운팅하고, 단어 : 개수를 딕셔너리로 만든다.

 

 

 

1
2
3
4
5
6
7
from gensim.models import Word2Vec
 
model = Word2Vec(slogan_list2, vector_size=100, window=4,
                           min_count=2, workers=4, epochs=50, sg=1
 
#가장 많이 나오는 단어와 유사한 단어
model.wv.most_similar(keyword,topn=10)
cs

결과값을 확인 했을 때, 나쁘지 않게 나왔지만.. 이 단어들로이 포함되지 않는 문구를 필터링한다면, 걸러져야할 광고문구보다 괜찮은 광고문구들도 대부분 걸러질 것으로 보여서 다른 방법을 또 고민했다.

 

텍스트 유사도를 구할 때, 추천 시스템에서 사용했던 TF-IDF, 코사인 유사도를 사용하려고 했다. 하지만 파이널 프로젝트에는 적용하기 힘들었던 이유가 비교할 문장이 길지 않고, 한 문장으로 이루어졌기 때문이다. 프로젝트가 끝나고 현재 Textrank나 문장 요약, 키워드 추출 등을 공부하고 있는데, 이 방법도 적합하지 않은 방법이었다.

 

 

 

그래서 찾게 된 것이 Sentence Transformer다. 그런데 생각보다 구글에는 예시가 많지 않았다. 영어 모델은 유사도 높게 나왔는데, 한글의 경우 위의 사진처럼 '한 남자가'라는 단어가 일치한다고 유사도 94퍼센트가 나오는 아이러니한 현상이 발견됐다. 구글링을 더 하다가 Ko-Sentence-BERT-SKTBERT 모델이 나왔는데 오류 때문에 잘 되지 않았다.

 

 

 

 

 

Pretrained Models — Sentence-Transformers documentation

We provide various pre-trained models. Using these models is easy: Multi-Lingual Models The following models generate aligned vector spaces, i.e., similar inputs in different languages are mapped close in vector space. You do not need to specify the input

www.sbert.net

더 검색하다가 발견한 모델! 유사도가 SKTBERT에서 테스트한 예제들과 결과가 비슷하게 나와서 사용하기로 했다.

 

 

 

 

결과가 좋다! 그리고 예상했던대로 인풋데이터와 관련이 없는 결과(슬로건, 광고문구)는 다른 문구들과 비교했을 때 전체적으로 유사도가 높지 않음을 확인할 수 있다. / RPG 게임에 웬 패션 스타일인가?

 

하지만 여기서도 또 문제 아닌 문제가 생겼다. 필터링 되는 슬로건 중에서도 키워드만 바꾸면 괜찮아보이는 슬로건들이 있다. 그래서 이걸 살리는 것도 좋지 않겠냐는 멘토님의 말씀이 있어서.. 사용자가 직접 민감도를 설정해 필터링의 할 수 있는 기능을 추가했다. 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#문장 유사도
#개별 추출
from sentence_transformers import SentenceTransformer, util
import numpy as np
import random
import pandas as pd
 
#모델 불러오기
model = SentenceTransformer('distiluse-base-multilingual-cased-v1')
 
#회사 리스트
company = pd.read_csv('datasets\company_list.csv')
company_list = company.company.values.tolist() #회사명을 리스트로
 
#비교할 슬로건 선택 
no_sim_list = [] #관련 없는 슬로건 추출
total_slogan = [] #슬로건 전체를 담는 리스트 / 중첩리스트용
= 0
try : #n이 증가하지 않을 경우 무한루프? 
    while n < 5 :
        #유사도 비교할 리스트
        corpus = kor_list
        corpus_embeddings = model.encode(corpus, convert_to_tensor=True)
        
        #유사도 비교할 문장
        query = random.sample(kor_list, 1)
        print("Query : ", query)
        
        #코사인 유사도 사용하여 5개 유사한 슬로건 찾기
        top_k = 6 #query 포함 top 5개
        query_embedding = model.encode(query, convert_to_tensor=True)
        cos_scores = util.pytorch_cos_sim(query_embedding, corpus_embeddings)[0]
        cos_scores = cos_scores.cpu()
        top_results = np.argpartition(-cos_scores, range(top_k))[0:top_k] # np 사용 이유 : 순위를 순서대로 맞추기 위함
        
        #민감도 비교하기 위한 유사도 더하기      
        sum = 0
        for idx in top_results[1:top_k]:
            sum += cos_scores[idx]
        f_sum = float(sum)/5 #tensor to float
        print(f_sum)
        
        #사용자 인풋 민감도 비교    
        sim_list = [] #유사 슬로건 담을 리스트
        sim_list2 = [] #수정된 슬로건 담을 리스트
        if f_sum >= input_sim / 100 :
            for idx in top_results[0:top_k-1]:
                copy_ = corpus[idx].strip()
                sim_list.append(copy_)
            
            print(sim_list)
            sim_list2 = sim_list    
            for i in range(len(sim_list2)) :
                for c in company_list :
                    if c in sim_list2[i] :
                        sim_list2[i] = sim_list2[i].replace(c,'*'*len(c))
       
            total_slogan.append(sim_list2)
            kor_list = differ_sets(kor_list, sim_list)
            n += 1
            #print(len(kor_list))
            
        else : 
            no_sim_list.append(query)
            kor_list = differ_sets(kor_list, query)  #kor_list에서 query를 제거 
            print('관련이 없는 슬로건 데이터 추가'
  
                
                
except :
    print('데이터가 부족합니다.')
    
print('완료')
#print(no_sim_list)
 
print(total_slogan)
cs

우선 슬로건 문구를 포함한 리스트에서 영어로만 이뤄진 슬로건을 제외해 kor_list를 만들었다. 영어가 포함된 문장의 경우 유사도가 얼추 비슷하게 나왔는데, 영어로만 이루어진 문장은 문장 유사도 성능이 떨어져서 아예 제외시켰다. 이후 한 개를 랜덤으로 뽑아서 유사도가 비슷한 값 5개를 뽑아서 평균을 냈을 때, input_sim과 크기를 비교해서 살릴지 버릴지 고민을 했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#차집합 함수
def differ_sets(a,b) : 
    lst = list(set(a)-set(b))
    return lst
    
 
#영어 슬로건 따로 추출
import re
 
eng_list = []
for slogan in slogan_list :
    slogan_ = re.sub('[^A-Za-z가-힣]''',slogan) #영어 한글만 남기기
    slogan_ = re.sub('[^가-힣]',' ', slogan_) #영어는 공백으로 남긴다
    if slogan_.isspace():    #isalpha()는 영어 또는 한글 유무를 찾아서 안 됨
        eng_list.append(slogan)
        
print(eng_list)
 
#차집합 
kor_list = differ_sets(slogan_list, eng_list) #한국 슬로건만 있는 리스트
cs

영어로만 이루어진 문장을 뽑는데 애를 먹었다. isalpha()를 사용하게 되면 영어로만 이루어진게 아니라, 한글이 있을 때도 True를 반환하기 때문에 다른 방법을 사용해야했다. 우선 공백을 없애고, 한글만을 남게한다. 만약 영어로만 이루어졌으면 isspace()함수에서 True를 반환하기 때문에 영어만 포함된 문장을 뽑을 수 있다. 반대로 한글로만 이뤄진 문장이 필요하면 '^가-힣' 대신 '^A-Za-z'을 활용하면 된다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
company_list = company.company.values.tolist()
len(company_list)
 
for c in company_list :
    for i in range(len(total_slogan) :
           if c in total_slogan[i] :
            # print(c)
            slogan_edit = total_slogan[i].replace(c,'*'*len(c))
            # print('수정')
 
slogan_edit
cs
기업, 제품, 서비스 광고문구를 크롤링했기 때문에, 슬로건 자체에 회사명, 제품, 서비스가 들어가는 경우가 있다. 그래서 이를 별표처리를 해줬다. 여기서 문제가 끝난 줄 알았는데.... 아웃풋뿐만 아니라 인풋 데이터도 조정해야 했다. 토크나이징을 할 때 문제가 있었다. 우리는 손크롤링을 할 때 회사 설명에 주로 명사 위주의 키워드를 넣었는데, 토크나이징을 할 때 띄어쓰기 유무에 따라서 결과값이 많이 달라졌다.
 
 
예를 들어 '패션의류'의 경우, 우리는 '패션 의류'를 설명에 입력했는데 토크나이저가 패션(명사) 의(조사) 류(명사) 이런 식으로 인식하기도 하고 제각각 달랐다. 그래서 명사 띄어쓰기 필요성이 느껴져서 형태소 분류를 진행했다. mecab, okt, kkma를 사용해봤는데 전체적인 성능은 mecab이 좋았으나, 명사 추출은 kkma가 조금 더 딱딱하게 잘 끊어냈다.

 

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"""
** 가이드 **
-input_sim
0~15 : 가장 자유로움 
15~30 : 자유로움
30~45 : 조금씩 걸러짐
45~60 : 많이 걸러짐   / 강제로 기업 설명을 인식시켜서 더 제한적인 슬로건
60~ : 거의 다 걸러짐  / 슬로건 100개를 했을 경우 유사율 평균 70 이상은 거의 없음
 
-input_text
최대한 명사 위주의 설명
영어를 쓸경우 뒤에 나오는 단어와 붙여쓰면 더 좋은 결과 ex) 'LED 마스크'보다는 'LED마스크' 
"""
 
input_sim = 40  # input data 유사성 민감도 지정 / 숫자가 작을수록 관련 없는게 나올 확률이 커짐 / 최소 50이상 설정
input_text = '커피전문기업'
input_text_list = input_text.split(' '# input data 띄어쓰기로 나누기
eng_text = re.sub('[^a-zA-z]',' ',input_text).strip()
 
kkma = Kkma() #꼬마를 작용시 분모가 중복 되는 경우가 생김, 이를 제거해야 함
copy=[]
for txt in input_text_list :
    txt_ = kkma.nouns(txt)
    # print(txt_)
 
    if len(txt_) > 1 : #(명사가 쪼개졌을 경우)
        max_string = max(txt_, key=len#가장 긴 값을 제거 (중복값)
        txt_.remove(max_string)    
    
    copy += txt_
# print(copy)
 
if len(copy) >3 : 
    del_list = []
    for i in range(math.ceil(len(copy)-2)) : 
        overlap_txt = ''.join((itemgetter(i,i+2)(copy))) # abc를 kkma로 쪼갤 경우 =>  a, ab, abc, b, c => abc 제거 => ab를 제거하는 과정 
        if overlap_txt in copy :
            del_list.append(overlap_txt) 
    #print(del_list)
    [i for i in del_list if not i in copy or copy.remove(i)] #차집합인데 순서가 안 바뀜 
text = ' '.join(copy)
 
if input_sim > 45 :
    text += ',' #,를 넣을 경우 강제로 기업설명으로 인식시켜서 조금 더 제한적인 슬로건 등장 
 
#영어 슬로건이 포함 된 경우 초기상태로
if eng_text :
    if eng_text in input_text :
        text = input_text
    
print(text)
cs
이번에도 산 넘어 산이었다. kkma가 딱딱하게 명사를 잘 끊어내는 것과 달리, 만약 합성어가 ab이면 우리는 a와 b 결과만 나오면 되는데 a, ab, b로 쪼개지면서 다시 ab값이 등장해서 문제가 됐다. 그래서 단어 길이가 최대인 값을 지우면 되겠거니 했는데.... abc의 경우 a, ab, abc, b, c로 쪼개져서 abc 뿐만 아니라 ab를 지워야했다. 이를 어떻게 해야할까 하다가 저 코드가 나오게 됐다. 리스트끼리 뺐을 때도 리스트 순서는 바뀌지 않으면서 리스트를 유지하는 법도 배웠다.
 
또 영어도 한글과 붙어 있으면 값이 다르게 나왔는데, 이게 gpt2가 현재 단어 다음에 나올 단어의 확률을 예측하는 방식으로 학습했기 때문에 ㅠㅠ 붙여쓰는 것과 띄어쓰기를 하는 것은 결과가 달랐다. 원래 이것도 하나하나 코드를 짜려다가 조금 더 사용자에게 자율성을 주자고 조교님이 그러셔서 영어가 포함이 되면 인풋 데이터 그대로 모델에 입력이 됐다.
 
아직도 공부할 부분은 많은 것 같다..
 

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

목차

1. SKT-AI/KoGPT2와 자연처(NLP)

2. 데이터 전처리 (나무위키, 블로그)

3. KoGPT2와 기능구현 (인풋 아웃풋 조정)

4. 짧은 텍스트 / 문장유사도

5. 배포 (구글 애즈, GA, AWS)

 


KoGPT2 학습방식 개요 - 저는 주로 ver.1을 다루고 다른 팀원이 ver.2를 다뤘다. '기업설명, 슬로건'으로 이루어진 문장이 여럿 들어가는 거와 기업설명, 슬로건을 나눠서 학습시키는 것과 차이가 커보이지는 않는데 ver.2의 경우 성능이 좋지 않았다. 아마 코드가 영어슬로건 생성에 맞춰진 거라서 그렇지 않을까 싶다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from transformers import TextDataset, DataCollatorForLanguageModeling
from transformers import GPT2LMHeadModel
from transformers import Trainer, TrainingArguments
from transformers import PreTrainedTokenizerFast
 
 
def load_dataset(file_path, tokenizer, block_size = 128):
    dataset = TextDataset(
        tokenizer = tokenizer,
        file_path = file_path,
        block_size = block_size,
    )
    return dataset
 
 
def load_data_collator(tokenizer, mlm = False):
    data_collator = DataCollatorForLanguageModeling(
        tokenizer=tokenizer, 
        mlm=mlm,
    )
    return data_collator
 
def train(train_file_path,model_name,
          output_dir,
          overwrite_output_dir,
          per_device_train_batch_size,
          num_train_epochs,
          save_steps):
  tokenizer = PreTrainedTokenizerFast.from_pretrained(model_name,
                bos_token='</s>', eos_token='</s>', unk_token='<unk>',
                pad_token='<pad>', mask_token='<mask>')
  train_dataset = load_dataset(train_file_path, tokenizer)
  data_collator = load_data_collator(tokenizer)
 
  tokenizer.save_pretrained(output_dir, legacy_format=False)
   
  model = GPT2LMHeadModel.from_pretrained(model_name)
 
  model.save_pretrained(output_dir)
 
  training_args = TrainingArguments(
          output_dir=output_dir,
          overwrite_output_dir=overwrite_output_dir,
          per_device_train_batch_size=per_device_train_batch_size,
          num_train_epochs=num_train_epochs,
      )
 
  trainer = Trainer(
          model=model,
          args=training_args,
          data_collator=data_collator,
          train_dataset=train_dataset,
  )
      
  trainer.train()
  trainer.save_model()
 
train_file_path = './datasets/slogans.txt'
model_name = 'skt/kogpt2-base-v2'
output_dir = './models2'
overwrite_output_dir = False
per_device_train_batch_size = 8
num_train_epochs = 5.0
save_steps = 500
 
train(
    train_file_path=train_file_path,
    model_name=model_name,
    output_dir=output_dir,
    overwrite_output_dir=overwrite_output_dir,
    per_device_train_batch_size=per_device_train_batch_size,
    num_train_epochs=num_train_epochs,
    save_steps=save_steps
)
cs

gpt2 모델 중에서 pytorch로 학습을 시킨 경우가 많았는데, 생각보다 복잡하고 배우지를 않아서 사용하지 못 했다. skt/kopgt2 토크나이저를 사용하였고, skt/kogpt2-base-v2 모델을 사용하여 학습을 진행했다. 생각보다 학습 코드 자체에는 우리가 수정해서 성능을 개선시킬 것이 많지 않았다. 그래서 결과를 어떻게 개선시킬 수 있는지에 더욱 초점을 두고 프로젝트를 진행했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from transformers import PreTrainedTokenizerFast, GPT2LMHeadModel
 
def load_model(model_path):
    model = GPT2LMHeadModel.from_pretrained(model_path)
    return model
 
 
def load_tokenizer(tokenizer_path):
    tokenizer = PreTrainedTokenizerFast.from_pretrained(tokenizer_path)
    return tokenizer
 
 
def generate_text(sequence, max_length):
    model_path = "./models2"
    model = load_model(model_path)
    tokenizer = load_tokenizer(model_path)
    ids = tokenizer.encode(f'{sequence},', return_tensors='pt')
    final_outputs = model.generate(
        ids,
        do_sample=True,
        max_length=max_length,
        pad_token_id=model.config.pad_token_id,
        top_k=50,
        top_p=0.95,
    )
    print(tokenizer.decode(final_outputs[0], skip_special_tokens=True))
 
# sequence = input()
# max_len = int(input())
 
input = '기업 설명'
 
sequence = input
max_len = 50
 
print('input :' + sequence)
 
for i in range(10):
    print(generate_text(sequence, max_len))
print('=' * 30)
cs

GPT 언어 모델 자체가 주어진 단어 다음에 등장할 단어의 확률을 예측하는 방식으로 학습된다. 그래서 transformer로 문장을 생성할 때 성능을 높이기 위해 top_k 와 top_p 샘플링 방식을 이용했다.

 

 

 

Top-K 샘플링 : 현재 단어 다음으로 나올 수 있는 단어를 개수로 제한한다. 아주 작은 확률의 단어도 선택 가능.

Top-P 샘플링 : 현재 단어 다음으로 나올 수 있는 단어를 확률로 제한한다. 누적활률로 계산을 하기 때문에 확률이 낮은 단어는 제외.

 

 

 

 

결과에서 문제점들이 많이 발견됐다. 

1. input 값과 관련 없는 슬로건들이 등장한다.

2. 결과값의 문장 모양이(?) 통일성이 없다. => 슬로건, 광고문구만 뽑기 위해서 제약이 생긴다.

3. 완벽한 문장들이 등장해서 슬로건으로서의 가치가 떨어진다.

 

 

 

 

1, 2번을 해결하기 위해서 결국 노가다를 시작했다. 데이터셋을 조금 더 통일감 있게 구성을 하고('기업설명, 슬로건'으로 통일), 슬로건은 카테고리로 세분화가 되어있었는데, 더 세분화를 시켰다(화장품 -> 세럼, 스킨케어 등등). 그리고 1번을 더욱 정교하게 처리하기 위해서 텍스트 문장 유사도를 사용했다.  

 

 

 

3번 문제도 약간은 복잡했다. 에포크 수를 변경하면서 loss 값이 가장 적은 최적의 에포크 수를 찾아서 저장을 했다. 하지만 실제로 에포크에 따른 모델들을 다 돌려봤을 때, loss값이 가장 적었던 모델이 너무 완벽한 문장이 나와서 오히려 광고문구 같은 느낌이 나지 않는 경우도 발생했다. 영어 모델의 경우는 에포크와 어떤 gpt2 모델을 돌리느냐에 따라서 결과값의 차이가 컸다.

 

그리고 혹시나 데이터 양이 많지 않아서 생긴 문제가 아닐까 싶어서(1만 개 정도), 애플의 슬로건만(200개?) 가지고 학습을 진행했더니 정말 충격적인 결과가 나왔다.

 

 

 

 

데이터의 양이 적으면 애초에 슬로건 자체가 형성이 되지 않고, gpt2나 kogpt2에서 이미 사전에 학습한 내용을 가지고 문장을 생성하기 때문에 데이터 자체에 문제는 아니었다. 물론 데이터가 많으면 많을수록 우리가 노가다 해야할 부분도 많겠지만... 더 좋은 결과가 나올 것이라고 장담한다.

 

 

 

잠깐 스트레칭을 하면서

고개도 도리도리 돌려주세요

 

이어서 쓰려다가 너무 길어질 것 같아서

2편으로 나눕니다.

 

 

GPT3 관련 내용은 아래 링크로 ▼

 

 

GPT3 사용법, 설치부터 쉽게! 슬로건, 동화 만들기까지? KoGPT3 등장?

6-1 파이널 프로젝트 : 자연어처리, kogpt2를 이용한 슬로건 생성 목차 1. SKT-AI/KoGPT2와 자연처(NLP) 2. 데이터 전처리 (나무위키, 블로그) 3. KoGPT2와 기능구현 (인풋 아웃풋 조정) 4. 짧은 텍스트 / 문장

0goodmorning.tistory.com

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

목차

1. SKT-AI/KoGPT2와 자연처(NLP)

2. 데이터 전처리 (나무위키, 블로그)

3. KoGPT2와 기능구현 (인풋 아웃풋 조정)

4. 짧은 텍스트 / 문장유사도

5. 배포 (구글 애즈, GA, AWS)

 


 

[네이버 어벤저스] 내가 보는 상품 홍보문구, 실은 AI가 썼다고?

국민 포털로 출발한 네이버가 다양한 플랫폼과 서비스들로 영역을 대폭 확장하고 있다. 이용자 경험을 위한 체질 개선뿐만 아니라, 중소상공인(SME) 및 창작자들과 이용자들을 연결해 디지털 비

n.news.naver.com

[네이버 어벤저스] 내가 보는 상품 홍보문구, 실은 AI가 썼다고?

뭔가 우리 팀에서 진행했던 파이널프로젝트 결과가 나오고 나서 부랴부랴 기사를 낸 것 같은 착각이 들어서 뿌듯했다. 기업에서도 이런 생각을 하고 있었고, 우리가 이걸 해냈구나? 하는 느낌.. 빨리 파이널 프로젝트 글을 쓰고 나서 다른 글도 올려야 하는데, 추가로 gpt3와 자연어 공부를 같이 하고 있어서 글 쓰는 시간도 부족한 것 같다. 그리고 정리를 해놨던 파일이 갑자기 증발이 되는 바람에 목차도 많이 줄였다.

 

우리가 필요한 데이터는 기업 설명(단순 기업명 가지고는 안 됐다)과 슬로건, 광고 문구였다. 하지만 온라인 상에는 그러한 데이터셋이 존재하지 않았다. 슬로건, 광고문구 데이터를 어떻게 모았는가 하면, 블로그에서 기업명과 함께 크롤링을 하고, 기업명을 네이버, 나무위키, 사람인 등에 다시 검색해서 기업의 정보를 모았다. 사람인에서 기업 설명이 깔끔하게 입력이 됐으면 이를 사용했을텐데 이상하게 분류된게 많아서 결국 손으로 데이터 전처리를 진행했다.

 

(* 잡담 : 도전하자. 프로젝트에 올라온 글들의 제목이 매력이 없어 보인다. 추후에 구글에서 검색이 되면서 사람들이 클릭하게끔 또 만들어야할 듯 싶다)

 

 

 

 

출처 : python awesome

어떻게 프로젝트를 진행했는지 다시 생각해보니, 다양한 시도를 하긴 했구나 하는 느낌? style gan을 사용해서 로고생성까지 시도하려고 했으나 생각보다 결과가 좋지 않고 비효율적이었다. 그래서 새로운 캐릭터를 생성하자는 느낌으로 해볼까 했는데, 캐릭터를 크롤링하고 찾는 것도 일일 것 같아서 우선 할 수 있는 부분에 더욱 중점을 뒀다.

 

 

 

 

출처 : 금강일보

네이버 블로그 크롤링의 문제점

1. 우선 java script로 구성이 돼서 동적 크롤링을 사용해야 한다. 2. iframe으로 둘러 쌓여서 크롤링이 쉽지 않았다그리고 스마트 에디터 one과 2.0 버전에 따라 크롤링 해야하는 방법이 다르다.

 

동적 크롤링인 셀레니움이 속도도 느리고 중간중간 빠지는 데이터들도 있어서 사용하는 것을 별로 안 좋아해서 어떻게 하면 beautiful soup으로 할까 하다가.. 네이버 블로그 검색창에 제목을 검색해서 하니까 잘 진행이 됐다. link가 포함된 부분을 크롤링하고 get('href')로 링크만 크롤링을 했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
# 네이버 블로그 iframe 제거 함수
def delete_iframe(url):
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"}
   req = requests.get(url, headers=headers)
   req.raise_for_status()  # 문제시 프로그램 종료
    soup = BeautifulSoup(res.text, "lxml")
 
    src_url = "https://blog.naver.com/" + soup.iframe["src"]  # iframe 안에 있는 src 부분을 가져옴
 
    return src_url
 
cs

출처 : https://github.com/tiger-beom/naverblog_scraping

 

iframe 제거 함수를 미리 정의해놓으면 나중에 네이버 블로그 크롤링하는데 편리할 것이다. 2.0 -> 3.0 -> one 다 써봤었는데 2.0보다는 one이 더 깔끔하긴 하지만.. 정말 초반에는 오류 투성이었다. 글을 써야 하는데 평상시보다 몇 배가 더 걸렸던 기억이 있다. 투데이 2만이 넘었던 블로그였는데 ..

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 네이버 블로그 글 크롤링 함수
def text_scraping(url):
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"}
    req = requests.get(url, headers=headers)
    req.raise_for_status()  # 문제시 프로그램 종료
    soup = BeautifulSoup(res.text, "lxml")
 
    if soup.select_one('div.se-main-container'):
        text = soup.select_one('div.se-main-container').text
        text = text.replace('\n''')
        # print("블로그")
        return text
    elif soup.select_one('div#postViewArea'):
        text = soup.select_one('div#postViewArea').text
        text = text.replace('\n''')
        # print('블로그 2.0')
        return text
    else:
        return "오류"
cs

div.se-main-container가 포함되면 스마트에디터-one, div#postViewArea가 포함이 되면 2.0 버전이라고 생각하면 된다. 네이버 포스트의 경우 또 다른 방식으로 글을 크롤링해야 한다. 궁금하면 위의 깃허브로 들어가시면 됩니다.

 

 

 

 

나무위키 크롤링 문제점

동적, 정적 크롤링이 모두 안 됐다! 왜 그런가 찾아보니 나무위키에서 크롤링 하는 것을 원천 차단한 모양이다. 트래픽 과부하를 막기 위해서 그런 것으로 보인다. 그래서 봇으로 인식을 하면 아예 창이 들어가지지 않았다. 

 

우선 코드를 짜봤는데 진행이 되지 않았고, 내 문제인가 싶어서 위키독스에 나와있던 코드를  따라했는데도 되지 않았다. 결국 구글링을 열심히 해서 봇이 아닌 사람으로 인식하는 코드를 추가로 넣어줘야 했다

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#봇이 아닌 사람으로 인식하는 코드 / 자신의 크롬 위치 입력
subprocess.Popen(r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\chrometemp"')
 
options = webdriver.ChromeOptions()
options.add_experimental_option("debuggerAddress""127.0.0.1:9222")
 
chrome_ver = chromedriver_autoinstaller.get_chrome_version().split('.')[0]
try:
    driver = webdriver.Chrome(f'./{chrome_ver}/chromedriver.exe', options=options)
except:
    chromedriver_autoinstaller.install(True)
    driver = webdriver.Chrome(f'./{chrome_ver}/chromedriver.exe', options=options)
 
driver.implicitly_wait(3)
start = time.time()
cs

첫번째 줄에 있는 코드는 컴퓨터마다 크롬 위치가 다를 수 있기 때문에 확인해야 한다. 그리고 나서 크롬드라이버를 아예 새로 설치하는 것 같았다. import chromedriver_autoinstaller 를 통해서 패키지를 추가하자.

 

이렇게 코드를 입력하면 이제 본격적으로 셀레니움을 통해서 검색을 하면 된다고 생각했지만... 또 문제가 있었다. 어떤 정보를 긁어와야 하는지? 동음이의어가 존재할 때 어떻게 처리를 할지? 그리고 기업명이 단순히 기업명만 있으면 좋겠지만, 슬로건이나 광고문구를 가져오다 보니 지역명이나, 캠페인도 포함이 돼서 이를 먼저 처리해야했다. 그리고 no_search 리스트를 만들어서 제외했다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
            same = driver.find_element_by_xpath('//*[@id="app"]/div/div[2]/article/div[3]/div[1]/ul/li/a').text
 
            #동음이의어가 존재할 경우 원하는 정보를 긁어오지 않음, 예외로 뺴기 (분류에 동음이의어/* 여부)
            if '동음이의어' in same:
                explane = 'same'
                explanes.append(explane)
 
            #아닐 경우 그대로 진행
            else :
               try:
                    explane = driver.find_element_by_css_selector('div.wiki-heading-content').text
                    explanes.append(explane)
                    #print(explane)
 
                except:
                    explane = 'NaN'
                    explanes.append(explane)
cs

간단하게 접근을 했다. 개요에서 회사에 대한 설명을 긁어오기 전에 우선 동음이의어가 검색이 되는지 파악을 하고, 검색이 되는 경우 크롤링을 진행하지 않았다. 그리고 사람인, 네이버 뉴스 검색에서 회사 설명을 찾았다. 예전에 기사를 써본적이 있는데, 처음에 회사나 제품, 서비스에 대한 설명을 간단하게 적어놓는다는 점을 착안했다.

 

이렇게 크롤링을 하면 되겠다 싶었는데... 결과를 비교하니 참담했다. 그래서 손크롤링을 진행했다고 한다. ㅠㅠㅠ

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

목차

1. SKT-AI/KoGPT2와 자연처(NLP)

2. 데이터 전처리 (나무위키, 블로그)

3. KoGPT2와 기능구현 (인풋 아웃풋 조정)

4. 짧은 텍스트 / 문장유사도

5. 배포 (구글 애즈, GA, AWS)

 

모바일로 보니 너무 불편해서 수정 좀 했습니다 ㅠㅠ


 

결과물부터 보자면, kogpt2를 사용해서 슬로건을 생성하였다. 웹으로 서비스를 배포(AWS)하고, 구글애즈로 노출을 시키면서, GA(구글애널리틱스)로 트래픽을 분석하여 더 개선시키는 방향으로 진행했다.

 

 

 

 

 

 

GPT3 사용법, 설치부터 쉽게! 슬로건, 동화 만들기까지? KoGPT3 등장?

6-1 파이널 프로젝트 : 자연어처리, kogpt2를 이용한 슬로건 생성 목차 1. SKT-AI/KoGPT2와 자연처(NLP) 2. 데이터 전처리 (나무위키, 블로그) 3. KoGPT2와 기능구현 (인풋 아웃풋 조정) 4. 짧은 텍스트 / 문장

0goodmorning.tistory.com

읽으면 유용한 글 : GPT3

 

 

 

 

 

패키지, 툴 등 사용 기술

기간 : 2021.07.12 ~ 2021.08.18 (1 달 조금 넘게)

사용 기술은 그림을 참고. 

 

파이널 프로젝트라서 거창한 것을 해보려고 다양한 아이디어를 냈으나 쉽게 주제를 결정할 수 없었다. 이것저것해보다가 시간은 4주도 남지 않아서 처음에 진행하려고 했던 슬로건 생성을 해보자고 했다. 과연 슬로건처럼 간단하면서 임팩트 있는 문구를 인공지능이 생성할 수 있을까? 하는 의문이 있었지만 인공지능은 해냈다. (우리가 해냈다)

 

이번 시간을 통해서 자연어처리에 대해서 많이 공부를 할 수 있었고, gpt2와 프로젝트가 끝난 이후 gpt3에 대해서도 추가적으로 공부를 하였다. 주로 인공지능을 통해서 분류를 하거나 예측을 하였는데, 새로운 문장을 생성한다는게 참 신기했다.

 

 

 

GPT(Generative Pre-trained Transformer)는 언어모델로 '자연어 디코더 모델'이라고 생각하면 쉽다. 자연어 처리 기반이 되는 조건부 확률 예측 도구이며, 단어가 주어졌을 때 다음에 등장한 단어의 확률을 예측하는 방식으로 학습이 진행 된다. 문장 시작부터 순차적으로 계산한다는 점에서 일방향을 보인다.

 

 

 

 

 

KoGPT2 skt-ai에서 만든 한글 디코더 모델. KoGPT2에서는 기존 GPT의 부족한 한국어 성능을 극복하기 위해 많은 데이터(40g)로 학습된 언어 모델로서 한글 문장 생성기에서 좋은 효과를 보인다고 한다. 시 생성, 가사 생성, 챗봇 등의 서비스 구현한 사례가 있다.

 

단순히 모델 사용보다 자연어를 어떻게 처리할지 많은 공부를 하게 됐다. 간단하게 정리한 부분을 추가적으로 공유하려고 한다.

 

자연어 처리로 할 수 있는 것들 

-텍스트 분류 (스팸 메일)

-감성 분석 (긍/부)

-내용 요약 (추출/ 생성)

-기계 번역 (번역)

-챗봇

 

 

 

 

출처 : dreamstime

자연어 처리 과정

-Preprocessing (전처리) : stopwords 불용어 제거, 형태소 분석, 표제어 추출 / 컴퓨터가 자연어를 처리할 수 있게

-Vectorization (벡터화)  : 원핫인코딩, count vectorization, tfdif, padding

-Embedding : word2vec, doc2vec, glove, fasttext

-Modeling : gru, lstm. attention

 

*Transfer learning (전이 학습) : pretrain한 임베딩을 다른 문제를 푸는데 재사용 

*Fine-tuning (파인 튜닝) : pretrain된 모델을 업데이트하는 개념. / 엔드투엔드에서 발전

 

 

 

임베딩

- 자연어를 숫자의 나열인 벡터로 바꾼 결과 혹은 과정 전체 (벡터 공간에 끼워넣는다)

- 말뭉치의 의미, 문법 정보가 응축되어 있음, 단어/문서 관련도 계산 가능

- 품질이 좋으면 성능이 높고 converge(수렴)도 빠르다

- NPLM(최초) / Word2Vec(단어수준) / ELMo(문장수준), BERT, GPT

 

단어 문장간 관련도 예상 / t-SNE 차원 축소 100차원->2차원으로 / Word2Vec 개선 모델 FastText / 행렬 분해 모델 / 에측 기반 방법 / 토픽 기간 방법 등으로 나뉨

 

잠재의미분석 : 말뭉치의 통계량을 직접적으로 활용

희소 행렬 - 행렬 대부분의 요소 값 0

단어-문서행렬, TF-IDF, 단어-문맥 행렬, 점별 상호정보량 행렬

 

 

단어수준 임베딩 단점 : 동음이의어 분간 어려움 

=> ELMo, BERT, GPT 시퀀스 전체의 문맥적 의미 함축해서 전이학습 효과가 좋음

 

*다운스트림 태스크 : 풀고 싶은 자연어 처리의 구체적 문제들 

(품사 판별, 개체명 인식, 의미역 분석, 형태소 분석, 문장 성분 분석, 의존 관계 분석, 의미역 분석, 상호참조 해결)

*업스트팀 태스크 : 다운스트렘 태스크 이전에 해결해야할 괴제. 단어/문장 임베딩을 프리트레인하는 작업

 

토큰 : 단어, 형태소, 서브워드

토크나이즈 : 문장을 토큰 시퀀스로 분석

형태소 분석 : 문장을 형태소 시퀀스로 나누는 과정

 

 

 

 

출처 : 벡터가 어떻게 의미를 가질까?

TF-IDF : 백오브워즈 가정(어떤 단어가 많이 쓰였는가) / term frequency inverse document

순서 정보는 무시하는 특징이 있다. 주제가 비슷하면 단어 빈도 또는 단어 비슷할 것이다. (정보 검색 분야에서 많이 쓰인다.)

사용자 질의에 가장 적절한 문서 보여줄 때 코사인 유사도를 구해서 보여준다.

 

-TF : 특정 문서에 얼마나 많이 쓰이는지

-DF : 특정 단어가 나타난 문서의 수

-IDF : 전체 문서를 해당 단어의 DF로 나눈 뒤 로그를 취함. 값이 클수록 특이 단어

(단어의 주제 예측 능력과 직결 됨)

 

 

 

출처 : 브런치

ELMo, GPT : 단어가 어떤 순서로 쓰였는가? 주어진 단어 시퀀스 다음에 단어가 나올 확률이 어떤게 큰지? 

n-gram (말뭉치 내 단어들을 n개씩 묶어서 빈도를 학습), 다음 단어 나타날 확률 조건부확률의 정의를 활용해 최대우도추정법으로 유도 => 한 번도 나오지 않으면 확률이 0이 되므로 보완을 해줘야 한다

 

마코프 가정 (한 상태의 확률은 그 직전 상태에만 의존한다) => 그래도 등장하지 않았던 단어 나오면 0이 되기 때문에 백오프, 스무딩 방식 (높은 빈도를 가진 문자열 등장확률을 일부 깎고, 등장하지 않은 케이스에 확률 부여

 

뉴럴 네트워키 기반 언어 모델 : 다음 단어를 맞추는 과정 학습 (엘모,지피티) 

마스크 언어 모델 : 문장 전체를 보고 중간에 있는 맞추기 (BERT)

 

 

 

 

출처 : Towards Data Scince

Word2Vec : 어떤 단어가 같이 쓰였는가 (분포 가정) 단어의 의미는 곧 그 언어에서의 활용이다?

타깃단어와 그 주위에 등장하는 문맥단어 계산 / 분포 정보가 곧 의미? 의문점

형태소 분류 - 계열 관계 : 해당 형태소 자리에 다른 형태소가 대치 될 수 있는지

품사 분류 – 기능(주어, 서술어), 의미(같은 뜻), 형식(이름, 성질, 상태)

형태는 같지만 기능과 의미가 달라질 수 있다.(기능과 분포는 다르지만 밀접한 관련)

PMI 두 단어의 등장이 독립일 때 대비해 얼마나 자주 같이 등장하는지 (단어 가중치) 단어-문맥 행렬

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

다섯 번째 프로젝트

 

1. 추천 시스템에 대한 생각

2. 추천시스템(TFIDF, Word2Vec)

3. GUI로 앱처럼 사용할 수 있게 (PyQt5)

 


드디어 마무리 단계에 왔다 ㅠㅠㅠ 티스토리 블로그 글을 작성하면서 파이널 프로젝트를 하는데 아직도 모르는게 태산이다. 파이널 프로젝트는 자연어(NLP)로 진행중인데, 왜 국문학과를 찾는지 알것 같다.

 

이번 프로젝트를 할 때, 후기 개수 자체가 부족하고, 과마다 특성 있는 키워드가 영화 리뷰처럼 다양하지 않아서(ex. OOO 원장님 너무 친절하고 좋아요. 아프지 않게 잘해주시고 병원이 깔끔해요. 등등) 힘들 것이라 예상을 했다. 결과는 예상대로 워드클라우드로 돌렸을 때 불용어처리할 것이 엄청 많았다. 그래서 추가적으로 팀원들이 후기를 one sentence로 만들 때 진료과목도 집어넣기는 했는데, 이게 얼마나 효과가 있을지는 잘 모르겠다.

 

이번에는 Django 대신에 PyQt5를 이용해서 GUI 앱을 만들었다. 저번에 해보자고 했는데 드디어.. 완성!

 

 

 

 

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

PyQt5 내용은 위키독스를 통해서 잘 배울 수 있다. 모르는 기능들을 하나하나 찾아서 구현이 됐다. 물론 심각한 노가다가 필요한 작업이라고 생각한다. Django, 안드로이드스튜디오 많은 시간이 필요하다.

 

 

 

 

우선 제일 첫번째 안 (다른 팀원)

- 검색기능 / 진료과목 카테고리 / 지역을 클릭하면 => 추천 병원과 병원 정보가 나오게 하는 것이다. 

 

 

 

 

두번째 안 (나)

- 검색기능 / 진료과목을 라디오버튼으로 클릭 => 추천 병원과 나에게 맞는 병원 리스트 담기 병원 정보 나오게 하기 

 

 

 

세번째 안 (절충안)

- 병원을 모를 수도 있기 때문에, 우선 진료과목과 지역을 먼저 선택하게 했다. 그후에 나오는 병원과 유사한 병원을 추가로 검색하고 싶으면 검색하게 만들었다. 그리고 홈페이지의 경우 url을 직접 복사붙여넣기 하는 것보다 바로가기로 만드는 것이 좋다고 생각하여 따로 뺐다.

 

 

 

 

우선 터미널에서 designer를 입력하여 Qt Designer를 실행시켜 ui를 만든다. 안드로이드스튜디오처럼 자신이 원하는 버튼이나 위젯을 끌어다 쓰고, 클래스 옆에 파이참에서 호출하기 쉬운 objectName을 지정해준다. 

 

우리가 구현했던 기능은 자동완성기능, 필터링 기능(지역 진료과목), url클릭시 바로 웹으로 넘어가기, 리스트내 요소를 클릭하여 정보 받아오는 기능, 추천기능, 리셋기능,  등이다. 간단해 보여도 간단하지 않고 시간이 꽤 걸렸다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QStringListModel
from PyQt5 import uic
import pandas as pd
from sklearn.metrics.pairwise import linear_kernel
from gensim.models import Word2Vec
from scipy.io import mmwrite, mmread
import pickle
import webbrowser
 
form_window = uic.loadUiType('plz_Yes_button_edit_3.ui')[0]
 
class Exam(QWidget, form_window):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
 
###########################################
 
if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Exam()
    w.show()
    sys.exit(app.exec_())
 
cs

가장 기본적인 구성 - 본인이 만든 ui를 불러오는 코드부터 GUI를 실행시키는 코드까지

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
        #데이터 불러오기
        self.df_review = pd.read_csv('./datasets/model_data_Hospital_and_info2.csv',index_col=0)
        self.df_review.info()
        self.Tfidf_matrix = mmread('./models/tfidf_hospital_review_l.mtx').tocsr()
        self.embedding_model = Word2Vec.load('./models/word2VecModel_hospital_l2.model')
        with open('./models/tfidf_l.pickle''rb'as f:
            self.Tfidf = pickle.load(f)
 
        # 카테고리 목록 리스트화
        self.cmb_title_2.addItem('과를 선택하세요')
        category = list(self.df_review.category.unique()) #카테고리 중복 없이
        category = sorted(category)
 
        for c in category :
            self.cmb_title_2.addItem(c)
 
        # 지역 목록 리스트화
        self.cmb_title.addItem('지역을 선택하세요')
        add_list = []
        for i in self.df_review.addresses:
            a = i.split(' ')[0#지역이름만
            add_list.append(a)
 
        add_set = set(add_list) #중복 제거 위해 set
        address = list(add_set) #다시 list
        address = sorted(address)
        address.pop(0#지역 아닌 다른 단어가 있어서 pop
 
 
        for add in address:
            self.cmb_title.addItem(add) #지역 목록
 
        # 병원 목록과 진료과목 리스트로 만들기
        total = ''
        for c in self.df_review.clinics:
            total += c
 
        totals = total.split(', ')
        total_set = set(totals)
        total = list(total_set)  #진료 과목
        total = sorted(total)
 
        titles = list(self.df_review.names) # 병원 이름
        titles = sorted(titles) # 따로 정렬하는 이유는 병원 이름이 먼저 나오게 하기 위해서
 
        key_title = titles + total      #병원 + 진료 과목
 
cs

데이터, Tfidf, Word2Vec 모델을 불러오고 카테고리 리스트, 지역 목록 리스트, 병원 이름과 진료과목을 리스트로 만들었다. 공간을 크게 차지하지 않고 옵션을 선택할 수 있는 QComboBox에 리스트를 추가했다. 카테고리와 병원 목록은 쉽게 만들 수 있었으나, 지역의 경우 통일이 되지 않아서 따로 전처리를 하였다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#주소 통일
add_dic = {'강원도' : '강원','경기도':'경기','경상남도':'경남','경상북도':'경북','광주광역시':'광주','대구광역시':'대구','대전광역시''대전',
           '부산광역시''부산''서울시' :'서울','서울특별시':'서울','서초구':'서울시 서초구','수원시':'경기','인천광역시':'인천','전라북도':'전북','제주특별자치도':'제주','충청북도':'충북'}
add_list = list(add_dic)
 
list = []
for i in df_review.addresses :
    a = i.split(' ')
    if a[0in add_list :
        a[0= add_dic[a[0]]
    #print(a)
    a = ' '.join(a)
    list.append(a)
df_review.addresses = list
print(df_review.addresses.head(100))
df_review.to_csv('./model_data_Hospital_and_info2.csv')
cs

지역의 경우 17개 시도별로 나누려고 split으로 주소를 나눠 가장 첫번째 글자를 가져왔다. 그렇기 때문에 서울시, 서울, 서울특별시처럼 같은 지역인데 이름이 다를 경우 통일하였다. 다행히 크게 수작업을 해도 되지 않아서 안도의 한숨이...

 

 

 

1
2
3
4
5
        #자동완성
        model = QStringListModel()
        model.setStringList(list(key_title))
        completer = QCompleter()
        completer.setModel(model)
cs

자동완성 기능 생각보다 어렵지 않았다. from PyQt5.QtCore import QStringListModel 불러오고 QCompleter로 자동완성을 시켜주고 이걸 QLineEdit으로 받아줬다.

 

 

 

1
2
3
4
5
6
7
8
9
        # 버튼 함수
        self.le_title.setCompleter(completer)
        self.le_title.returnPressed.connect(self.btn_recommend_slot)
        self.btn_recommend.clicked.connect(self.btn_recommend_slot) # 엔터 또는 버튼 클릭시
        self.cmb_title_2.currentIndexChanged.connect(self.cmb_title_slot_2)
        self.cmb_title.currentIndexChanged.connect(self.cmb_title_slot)
        self.listWidget.itemClicked.connect(self.hospital_info)
        self.btn_html.clicked.connect(self.open_web)
        self.btn_recommend_5.clicked.connect(self.btn_clicked)
cs

버튼을 클릭하거나, 엔터를 누르거나, index가 변할 때 어떤 식으로 작동할지를 구현하는 준비단계?

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    # 병원 지역별로 필터링
    def cmb_title_slot(self):
        print('지역 선택 클릭')
        self.le_title.clear() # 먼저 병원을 클릭 했을 때
        title = self.cmb_title_2.currentText()
        address = self.cmb_title.currentText()
 
        region = self.df_review[(self.df_review.category == title) &(self.df_review.region == address)].iloc[:101# 자체 추천 순위로 출력
        recommend = list(region)
        #print(recommend)
 
        self.listWidget.clear()
        self.listWidget.insertItems(0, recommend)
 
 
    # 카테고리 탑10 병원
    def cmb_title_slot_2(self):
        print('과 선택 클릭')
        title = self.cmb_title_2.currentText()
 
        top = self.df_review[self.df_review.category == title].iloc[:10,1]
        #recommend = '\n'.join(list(top)) # 이거는 lbl_result에
        recommend = list(top)
 
        self.listWidget.clear()
        self.listWidget.insertItems(0, recommend)
cs

지역 필터링, 병원 필터링 currentIndexChanged, index가 바뀌면 함수가 실행이 된다. 원래 cmb_title_slot1과 2를 바꿔야 하는데 처음에 설정을 그렇게 해서 지나갔다. 

 

현재 QComboBox에서 선택한 텍스트를 가져와야 필터링이 되기 때문에, currentText로 현재 텍스트를 가져왔다.  그리고 선택한  진료과목과 일치하는 탑10 병원을 listWidget에 출력하였다. QLineEdit으로 해도 되지만 나중에 병원 정보를 클릭해야 하기 때문에 안 된다. 그리고 list로 받아와서 0 번째 부터 다시 보여주는 코드를 실행했다. 마찬가지로 지역 설정을 바꿨을 경우, 우선 진료과목과 지역을 동시에 만족하는 병원을 추천하였다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    # 병원을 클릭 했을 때, 병원 정보 보여주기
    def hospital_info(self):
        print('병원 정보 클릭')
        title = self.listWidget.currentItem().text()
 
        try :
            a = self.df_review[self.df_review.names == title].iloc[03].split(',')[:10# 주요 진료 과목 10개만
            a = ','.join(a)
            b = self.df_review[self.df_review.names == title].iloc[04# 주소
            c = self.df_review[self.df_review.names == title].iloc[06# 전화번호
            #d = self.df_review[self.df_review.names == title].iloc[0, 5] # 홈페이지 url / 홈페이지 오픈 버튼으로 대체
            recommend = '[ 주요 진료 과목 ]\n{0}\n\n[ 주소 ]\n{1}\n\n[ 전화번호 ]\n{2}'.format(a, b, c)
            self.infotext.setText(recommend)
            recommend = '홈페이지 바로가기 클릭!'
            self.btn_html.setText(recommend)
        except :
            pass
 
cs

어떻게 listWidget에 있는 아이템을 가져오나 했더니, 텍스트를 가져오는 것과 비슷하게 currentItem()으로 텍스트를 불러올 수 있었다.  크롤링을 할 때 조금 멍청하게 가져와서 전처리 과정을 한 번 더 거쳤다. 진료과목을 리스트 안에 집어넣었는데 이것을 그대로 csv에 집어넣어서 '[' 리스트가 문자가 됐다. 

 

a는 진료과목, b는 주소,  c는 전화번호, d는 홈페이지 url을 보여주려고 했으나 사용자가 따로 긁어야하는 불편함이 있어서 바로 홈페이지 오픈을 시켜주기로 했다. 

 

 

 

 

1
2
3
4
5
6
    # 홈페이지 오픈
    def open_web(self):
        print('홈페이지 바로가기 클릭')
        title = self.listWidget.currentItem().text()
        html = self.df_review[self.df_review.names == title].iloc[05]
        webbrowser.open(html) # 홈페이지 연동
cs

검색을 해보니 엄청 복잡하게 url을 오픈 하는 경우가 있는데, import webbroser를 하고 html을 open 하면 바로 url로 넘어가진다코딩을 하다보면 어떻게 하면 더 코드를 간단하게 짤까 고민을 하면서, 다른 사람이 짜놓은 코드와 비교를 하다보면 가끔 감탄을 할 때가 온다. 나도 누가 코드를 보고 감탄했으면 좋겠다는 마음이.. 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    def btn_recommend_slot(self):
        print('추천 시스템 클릭')
        title = self.le_title.text()
 
        try:
            if title in list(self.df_review['names']):
                h_idx = self.df_review[
                    self.df_review['names']==title].index[0]
                cosine_sim = linear_kernel(
                    self.Tfidf_matrix[h_idx],
                    self.Tfidf_matrix)
                # recommend = '\n'.join(
                #     list(self.getRecommendation(cosine_sim))[1:])
                recommend = list(self.getRecommendation2(cosine_sim))[:-1]
 
            #elif title in total :
 
 
            else:
                print(title, '예외 키워드')
                sentence = [title] * 10
 
                sim_word = self.embedding_model.wv.most_similar(title, topn=10)
                labels = []
                for label, _ in sim_word:
                    labels.append(label)
                print(labels)
 
                for i, word in enumerate(labels):
                    sentence += [word] * (9 - i)
 
                sentence = ' '.join(sentence)
                sentence_vec = self.Tfidf.transform([sentence])
                cosine_sim = linear_kernel(sentence_vec,
                                           self.Tfidf_matrix)
                # recommend = '\n'.join(
                #     list(self.getRecommendation(cosine_sim))[:-1])
 
                recommend = list(self.getRecommendation2(cosine_sim))[:-1]
        except:
            if title :
                recommend =['검색어를 다시 확인해주세요']
                self.infotext.clear()
 
                default_text = '[ 주요 진료 과목 ]\n\n[ 주소 ]\n\n[ 전화번호 ]'
                self.infotext.setText(default_text)
 
            else:
                pass
        self.listWidget.clear()
        self.listWidget.insertItems(0, recommend)
cs

병원명을 정확히 입력했을 때와 진료과목 또는 다른 키워드를 입력했을 때, 그리고 검색어를 입력하지 않았을 때를 나눠서 진행했다. 

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    #키워드 기반 추천 시스템
    def getRecommendation2(self, cosine_sim):
        title = self.cmb_title_2.currentText()
        address = self.cmb_title.currentText()
        print(title, address)
        simScores = list(enumerate(cosine_sim[-1]))
        simScores = sorted(simScores, key=lambda x: x[1], reverse=True)
 
        if title == '과를 선택하세요' and address =='지역을 선택하세요':
            pass
        else :
            simlist = []
            for i in simScores :
                add = self.df_review.iloc[i[0],7# 지역
                tit = self.df_review.iloc[i[0],0# 카테고리
 
                if add == address and tit == title : # 지역, 카테고리 동시에 일치할 때만 추가
                    #print(add)
                    simlist.append(i)
 
            h_idx = [i[0for i in simlist[0:10]]
 
            if len(h_idx) == 0:
                RecHosptiallist = [f'{address} 지역에는 관련된 키워드가 없습니다.']
                return RecHosptiallist.names
            else :
                RecHosptiallist = self.df_review.iloc[h_idx]
                print(RecHosptiallist, '출력')
                return RecHosptiallist.names
 
        simScores = simScores[0:11]
        h_idx = [i[0for i in simScores]
        RecHosptiallist = self.df_review.iloc[h_idx]
        return RecHosptiallist.names
cs

검색을 했을 경우 유사한 병원을 추천하긴 하는데, 전혀 다른 진료과목이나 지역이 나오면 안 되기 때문에 여기서도 필터링 기능을 사용하였다. 모델의 아쉬운 점은 지방으로 갈 수록 후기도 많지 않고, 대부분 큰 병원들은 수도권에 몰려서 지방으로 갈수록 추천해줄만한 병원이 많지 않았다. 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    #리셋 기능
    def btn_clicked(self):
        print('리셋 버튼 클릭')
        self.cmb_title_2.clear()
        self.cmb_title.clear()
        self.le_title.clear()
        self.infotext.clear()
 
        #병원 정보 처음에 나오는 내용
        default_text = '[ 주요 진료 과목 ]\n\n[ 주소 ]\n\n[ 전화번호 ]'
        self.infotext.setText(default_text)
 
        category = list(self.df_review.category.unique())
        category = sorted(category)
        self.cmb_title_2.addItem('과를 선택하세요')
        self.cmb_title.addItem('지역을 선택하세요')
 
        add_list = []
        for i in self.df_review.addresses:
            a = i.split(' ')[0]
            add_list.append(a)
 
        add_set = set(add_list)
        address = list(add_set)
        address = sorted(address)
        address.pop(0)
 
        for add in address:
            self.cmb_title.addItem(add)  # 지역 목록
 
        for c in category:
            self.cmb_title_2.addItem(c)  # 카테고리 목록
cs

리셋버튼을 누르면 모든 것이 리셋이 되게 하려고 했다. 찾아보니 그런 코드가 있긴 있는데, 지금 GUI의 경우 지역 목록과 카테고리 목록은 남아야 해서 리셋을 누르는 동시에 다시 지역목록과 카테고리 목록이 뜨게 했다. 여기서 특히 오류가 많이 났는데, 어디서 호출을 해주느냐에 따라서 이게 중복 노출이 되는지, 부분 노출이 되는지 갈렸다.

 

아무튼 GUI 완성 ㅠㅠ

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

다섯 번째 프로젝트

 

1. 추천 시스템에 대한 생각

2. 추천시스템(TFIDF, Word2Vec)

3. GUI로 앱처럼 사용할 수 있게 (PyQt5)


항상 과정이 비슷해서 앞에 부분은 코드는 생략합니다.

 

우선 로그인이 필요한 서비스라서 동적 크롤링으로 후기를 긁어왔다. 그리고 추가로 병원 이름, 클리닉명, 주소, 링크, 전화번호는 로그인 없이도 크롤링이 가능해서 BS4로 긁어왔다. 과정은 크롤링한 데이터를 전처리하고, 워드클라우드를 생성해서 불용어를 처리하고(중요), 하나의 문장으로 합쳤다.  

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from scipy.io import mmwrite, mmread #매트릭스 저장할 때 mmwrite, 읽을 땐 mmread
import pickle #변수 타입 그대로 저장
 
df_review_one_sentence = pd.read_csv('./preprocess/total_hospital_review_one_sentence.csv', index_col=0)
print(df_review_one_sentence.info())
 
Tfidf = TfidfVectorizer(sublinear_tf=True)      # sublinear_tf는 값의 스무딩 여부를 결정하는 파라미터
Tfidf_matrix = Tfidf.fit_transform(df_review_one_sentence['reviews'])       # fit_transform 된 Tfidf를 갖고 있으면 추후 데이터 추가 가능하므로 따로 저장
 
with open('./models/tfidf.pickle''wb'as f:
    pickle.dump(Tfidf, f)       # Tfidf 저장
 
mmwrite('./models/tfidf_hospital_review.mtx', Tfidf_matrix)        # 유사도 점수 매트릭스 저장
cs

TfidfVectorizer로 문서 내에서 단어 토큰을 생성하고, 각 단어의 수와 가중치를 조정하여 문자를 순서 벡터로 변환했다. 이 과정은 따로 건드리는 부분이 없다. 다만 데이터 전처리가 중요하다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pandas as pd
from sklearn.metrics.pairwise import linear_kernel
from scipy.io import mmwrite, mmread
import pickle
from gensim.models import Word2Vec
 
df_hospital_review_one_sentence = pd.read_csv('./preprocess/total_hospital_review_one_sentence.csv', index_col=0)       # 리뷰 읽어 df 생성
 
Tfidf_matrix = mmread('./models/tfidf_hospital_review.mtx').tocsr()        # matrix 불러오기
with open('./models/tfidf.pickle''rb'as f:      # tfidf 불러오기
    Tfidf = pickle.load(f)
 
def getRecommendation(cosine_sim):      # 코사인 유사도롤 활용하여 유사한 병원 추천하는 함수
    simScore = list(enumerate(cosine_sim[-1]))      # 각 코사인 유사도 값에 인덱스 붙임
    simScore = sorted(simScore, key=lambda x:x[1], reverse=True)        # simScore(코사인 유사도, x[1])가 큰 것부터 정렬. reverse=True 내림차순 정렬.
    simScore = simScore[1:11]       # 유사한 병원 10개 리스트. 0번 값은 자기 자신이므로 배제.
    hospital_idx = [i[0for i in simScore]     # 인덱스(i[0]) 뽑아서 리스트 생성
   recHospitalList = df_hospital_review_one_sentence.iloc[hospital_idx]        # df에서 해당 병원 리스트 추출
    return recHospitalList
 
 
#병원명 검색
hospital_idx = df_hospital_review_one_sentence[df_hospital_review_one_sentence['names']=='김이비인후과의원'].index[0]      # 병원 이름으로 인덱스 값 찾기
# hospital_idx = 127
# print(df_review_one_sentence.iloc[hospital, 0])
 
cosine_sim = linear_kernel(Tfidf_matrix[hospital_idx], Tfidf_matrix)      # linear_kernel은 각 Tfidf 값을 다차원 공간에 벡터(방향과 거리를 가짐)로 배치한 뒤, 코사인 유사도를 구해줌. cosine = 삼각형의 밑변 / 윗변
                                                                       # 비슷한 영화는 유사한 위치에 배치됨. 유사할수록 각이 줄어드므로 코사인 값이 1에 가까워짐. -1에 가까울수록 반대, 0에 가까울수록 무관
recommendation = getRecommendation(cosine_sim)
# print(recommendation)
print(recommendation.iloc[:, 1])
cs

pickle에 저장한 Tfidf를 불러와서 코사인 유사도를 활용하여 유사한 병원을 추천해준다. 우선 병원의 idx 값을 구한다. 그리고 추천시스템에 이와 유사한 병원들의 리스트를 반환 받아서 출력한다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
from gensim.models import Word2Vec
 
review_word = pd.read_csv('./preprocess/total_cleaned_review.csv', index_col=0)
print(review_word.info())
cleaned_token_review = list(review_word['cleaned_sentences'])
print(len(cleaned_token_review))
cleaned_tokens = []
count = 0
for sentence in cleaned_token_review:
    token = sentence.split(' ')     # 띄어쓰기 기준으로 각 단어 토큰화
    cleaned_tokens.append(token)
# print(len(cleaned_tokens))
# print(cleaned_token_review[0])
# print(cleaned_tokens[0])
embedding_model = Word2Vec(cleaned_tokens, vector_size=100, window=4,
                           min_count=20, workers=4, epochs=100, sg=1)   # vector_size는 몇차원으로 줄일지 지정, window는 CNN의 kernel_size 개념, 앞뒤로 고려하는 단어의 개수를 나타냄
                                                                        # min_count는 출현 빈도가 20번 이상인 경우만 word track에 추가하라는 의미(즉, 자주 안 나오는 단어는 제외)
                                                                        # workers는 cpu 스레드 몇개 써서 작업할 건지 지정, sg는 어떤 알고리즘 쓸건지 지정
embedding_model.save('./models/word2VecModel_hospital.model')
print(embedding_model.wv.vocab.keys())
print(len(embedding_model.wv.vocab.keys()))
cs

Word2Vec 단어의 의미를 다차원에 분산시켜 표현하여, 단어간 위치를 파악해서 유사도를 분석해준다. Skip-Gram의 경우 중심 단어를 보고 주변에 어떤 단어가 있을지 예측. CBoW는 주변에 있는 단어들로 중간에 있는 단어를 예측한다.

 

 

 

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#리뷰 기반 키워드 검색
embedding_model = Word2Vec.load('./models/word2VecModel_hospital.model')
key_word = '친절'
 
sim_word = embedding_model.wv.most_similar(key_word, topn=10)
labels = []
sentence = []
for label, _ in sim_word:
    labels.append(label)
print(labels)
for i, word in enumerate(labels):
    sentence += [word] * (9-i)
sentence = ' '.join(sentence)
#sentence = [key_word] * 10      # tf에서 높은 값을 가지도록 리스트 복사
print(sentence)
 
sentence_vec = Tfidf.transform([sentence])
cosine_sim = linear_kernel(sentence_vec, Tfidf_matrix)
recommendation = getRecommendation(cosine_sim)
print(recommendation)
cs

키워드를 기반으로 하여 병원을 추천해주는 시스템이다.

 

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

다섯 번째 프로젝트

 

1. 추천 시스템에 대한 생각

2. 추천시스템(TFIDF, Word2Vec)

3. GUI로 앱처럼 사용할 수 있게 (PyQt5)

 


기간 : 2021.07.02 ~ 2021.07.09

 

언어 : Python

패키지 : Scikit-learn, GENSIM(Word2Vec, TF-IDF), Selenium, Pandas, Maplotlib, WordCloud

툴 : PyCharm, Jupyter Notebook

 

 

 

영화 리뷰 기반으로 추천시스템을 같이 진행을 했다. 새로운 프로젝트를 하려고 했으나 생각보다 자료 모으기가 쉽지 않았다. 원래는 보드게임 후기를 모으려고 했으나 데이터가 너무 정제되지 않았다. 인터파크나 티켓팅하는 사이트에서 뮤지컬 후기를 긁어오려고 했으나  도배도 많았고, 알바들이 너무 많아서 포기했다. 네이버 검색을 하려고 했으나 뮤지컬의 경우 책 제목이 중간중간 있어서 일일이 또 처리를 해야 해서 병원 후기를 기반으로 한 병원 추천시스템을 진행하기로 했다.

 

데이터 크롤링 -> 데이터 전처리 -> 추천 시스템 -> GUI 순으로 프로젝트가 진행 됐으나 앞에 부분은 계속 반복되는 것 같아서 이번에 추천시스템에 대한 생각을 정리해보고자 한다.

 

 

 

출처 : 공부합시다 티스토리

협업 필터링

- 취향이 비슷한 사람끼리 그룹화해서 추천하는 방식이다. (예를 들어서 20~30대 남성, 서울 중구 지역에 사는 사람들)

- 단점은 처음 사용하는 이용자거나 새로 나온 상품을 추천하기 어렵다.

 

콘텐츠 기반 필터링

- 이용자가 소비한 콘텐츠를 기준으로 유사한 특성을 가진 콘텐츠를 추천한다.

- 단점은 다양성이 떨어진다. (이전에 진중권 교수가 얘기했던 확증편향 문제가 생긴다.)

 

 

 

분명 콘텐츠는 많은 것 같은데 또 막상 뒤적거려보면 볼게 없는 넷플릭스. 예전에 넷플릭스 추천시스템을 검색해봤을 때 수천개의 데이터를 가지고 카테고리를 나누고 각각 가중치를 곱해서 복잡하게 만들어졌다고 하는데 잘 모르겠다...

 

우선 플랫폼은 유저가 더욱 오래 사용해야 돈을 버는 구조인데, 이런 식으로 갑자기 볼 콘텐츠가 사라지면 이탈률이 커져서 플랫폼 자체적으로 손해다. 그래서 이들을 붙잡으려고 콘텐츠를 추천한다. 하지만 너무 유사한 콘텐츠를 추천하게 되면 확증편향 문제는 물론, 사람들이 질리게 되는 단점이 있는 것 같다.

 

 

 

 

유튜브의 알 수 없는 알고리즘. 내가 평상시에 보고 있던 콘텐츠도 아니고, 관심을 가지고 있던 콘텐츠도 아닌데 왜 갑자기 뜨는 것일까? 심지어 오늘 어제 영상도 아닌 몇 개월, 몇 년 전 영상이 추천이 된다?

 

나도 마케팅을 하려고 하는 사람이다 보니, 유튜브 알고리즘을 이해하려고 많은 분석과 시도를 해봤다. 조회수 100만, 200만, 300만 넘는 영상을 여러 개 보유하고 있어서 탐색기능에 대해서는 어느정도 이해를 했다. 하지만 아직도 나를 한 번도 보지 않았던 영상으로 이끄는 알고리즘에 대해서는 분석이 조금 필요하다.

 

우선 구글과 유튜브는 어느정도 연동(?)이 되어있어서, 구글에서 트래픽이 증가하면 유튜브에서도 어느정도 추천되는 모양이다. 예전에 빅맥송 오디션이 있었는데, 구글에서 검색량이 많아지자 유튜브에서도 이전 영상들이 다시 노출되는 현상이 있었다.

 

 

 

 

 

유튜브 알고리즘을 따라가려면 이용자의 개인 데이터, 노출 클릭률, 영상시청지속시간, 다음 화면의 클릭 유무 등 엄청나게 다양한 데이터가 필요하겠지만 이는 불가능하기 때문에, 간이 추천시스템을 만드려고 생각을 했다.

 

우선 우리가 했던 프로젝트는, 후기를 기반으로 TF-IDF로 각 단어의 가중치를 줘서 얼마나 자주 나오는지를 비교하여 코사인 유사도를 활용하여 영화를 추천했다. 그렇다면 코사인 유사도를 구하는 식에서 reverse = False를 할 경우에 정 반대의 영화를 추천하겠다는 생각이 들었다.

 

정반대의 영화의 경우 사용자가 관심이 없던 영화일 수도 있고, 아니면 한 번도 보지 않았던 장르의 영화일 수도 있다. 총 9개의 영화를 보여줄 때 6개는 관련성 있고, 2개는 중간 정도, 1개는 전혀 관련 없는 영화를 보여주면서 노출클릭률을 살펴본다. log.txt 파일에 데이터를 담아서 관련성 있는 영화를 클릭했을 때는 영향을 주지 않고, 총 9개의 영화를 다시 새로고침했을 때에만 점차 관련 없는 영화가 나오는 값을 줄여나간다. (-1 ~1 사이면 , -0.9 -0.8 -0.7 이런 식으로...)

 

랜덤으로 계속 영화가 나오기 때문에 이것도 정확하지는 않지만, 어떻게 하면 편향성을 줄이면서 플랫폼에 이용자를 더 오래 잡게 할 수 있을지에 대해서 계속 고민해야 할 것 같다.

 

 

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

네 번째 프로젝트

 

1. 간단한 intro

2. 웹 크롤링 및 전처리

3. 모델 학습 및 평가


프로젝트를 하면서 느낀 보완사항은 :

-데이터의 길이가 너무 짧으면 단어를 추출하는데 한계가 있고, 과적합이 발생한다.

-유의미한 단어가 존재해야 정확도가 더 개선된다. 모델을 계속 만져봐도 정확도는 크게 개선되지 않는다.

-각 사이트마다 카테고리 분류가 다르게 되어있어서 소비자가 불편할 수도 있을 것이다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 모듈 임포트
import pandas as pd
import numpy as np
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from tensorflow.keras.preprocessing.text import *
from tensorflow.keras.preprocessing.sequence import pad_sequences
import pickle # pickle을 이용하면 int 값을 그대로 저장하고 그대로 불러옴
from konlpy.tag import Okt # 형태소 분리기 임포트
 
pd.set_option('display.unicode.east_asian_width',True)
 
df = pd.read_csv('/content/10x10(kor2).csv', index_col=0)
 
print(df.head())
print(df.info())
 
# 중복 행 확인
col_dup = df['title'].duplicated()
print(col_dup)
 
sum_dup = df.title.duplicated().sum()
print(sum_dup)
 
 
# title 컬럼 기준으로 중복 제거 (row 통째로 제거)
df = df.drop_duplicates(subset=['title'])
 
sum_dup = df.title.duplicated().sum()
print(sum_dup)
 
 
# 인덱스 새로 고침
df.reset_index(drop=True# False로 줄 시 기존 인덱스를 colum으로 올림
               inplace=True)
 
print(df.head())
print(df.tail())
cs

이 과정만 잘 따라와도 누구나 쉽게 카테고리 분류가 가능하다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 데이터 나누기
= df['title']
= df['category']
 
 
# 원핫인코딩 위해 라벨로 변경
encoder = LabelEncoder()
labeled_Y = encoder.fit_transform(Y)
label = encoder.classes_
 
# 지정한 라벨 저장
# 이미 학습한 라벨들을 이용해야 학습된 결과들과 똑같이 도출
with open('/content/10x10_category_encoder.pickle''wb'as f:
    pickle.dump(encoder, f)   
 
# 원핫인코딩 => 정답데이터 원핫인코딩(희소벡터처리)
onehot_Y = to_categorical(labeled_Y)
 
# 첫번째 데이터 형태소로 분리
okt = Okt()
okt_X = okt.morphs(X[0])
 
 
# 모든 row를 형태소로 분리
for i in range(len(X)):
    X[i] = okt.morphs(X[i])
cs

추후에 데이터들을 불러서 모델 성능이 얼마나 좋은지 평가를 하기 위해서 라벨들을 통일시켜야 한다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# stopwords 불러오기
stopwords = pd.read_csv('/content/stopwords2.csv')
 
# stopwords에 있는 단어를 제거하고 리스트에 담기
words = [] # 빈 리스트 생성
 
for word in okt_X:
    if word not in list(stopwords['stopword']):
        words.append(word)
 
#한 행씩 모든 데이터 처리
for i in range(len(X)): # X=df['title']
    result = []
    for j in range(len(X[i])):
        if len(X[i][j]) > 1:
            if X[i][j] not in list(stopwords['stopword']):
                result.append(X[i][j])
    X[i] = ' '.join(result)
cs

불용어 제거 단계

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#단어를 컴퓨터에서 인식시키기 위해 각 단어마다 숫자(라벨링) 부여 => 토큰화
# 수치적인 의미는 없음
token = Tokenizer()
token.fit_on_texts(X)
tokened_X = token.texts_to_sequences(X)
print(tokened_X[0])
 
#읽고 쓰는 과정에서 데이터의 자료형을 따로 인코딩할 필요 없이 그대로 저장
import pickle
 
with open('/content/10x10_token3.pickle''wb'as f:
    pickle.dump(token, f)
 
wordsize = len(token.word_index) + 1
 
max = 0
for i in range(len(tokened_X)):
    if max < len(tokened_X[i]):
        max = len(tokened_X[i])
 
#LSTM모델 사용을 위해 데이터 크기가 다른 것들을 0을 채워넣어 max사이즈로 통일시킴
X_pad = pad_sequences(tokened_X, max)
 
X_train, X_test, Y_train, Y_test = train_test_split(
    X_pad, onehot_Y, test_size=0.1)
print(X_train.shape)
print(X_test.shape)
print(Y_train.shape)
print(Y_test.shape)
 
xy = X_train, X_test, Y_train, Y_test
np.save('/content/10x10_data_max_{}_size_{}_2'.format(max, wordsize), xy)
cs

언어를 컴퓨터가 이해하기 위해서 토크나이징 과정을 거친다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import *
from keras.callbacks import EarlyStopping
 
 
X_train, X_test, Y_train, Y_test = np.load(
    '/content/datasets/10x10_data_max_13_size_20320_2.npy',
    allow_pickle=True)
#max: 한 행의(리스트 글자의) 최대 길이
#size: 차원 수
 
##모델 생성
model = Sequential()
#X_pad로 사이즈 통일시킨 데이터를 embedding layer가 원핫인코딩+벡터라이징 처리해줌
#벡터라이징: 각 인덱스별 의미부여를 벡터공간이라는 개념을 도입해 처리
 
model.add(Embedding(20320300, input_length=13))
#차원수 20320를 차원수 300으로 낮추고 input_length는 max최대값인 13으로 입력
 
model.add(Conv1D(512, kernel_size=5
            padding='same', activation='relu'))#1차원 컨볼루션=> conv1d, 2차원 컨볼루션=>conv2d
model.add(MaxPool1D(pool_size=1))
#conv다음에는 maxpool함께 감
 
model.add(LSTM(128, activation='tanh',
               return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(64, activation='tanh',
               return_sequences=True))
model.add(Dropout(0.1))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dense(10, activation='softmax'))#y값개수: 10(카테고리개수)
early_stopping = EarlyStopping(monitor='val_accuracy', patience=3)  
# 5 에포크 동안 해당 값이 좋아지지 않으면 학습을 중단.
 
print(model.summary())
cs

모델 구조

 

 

 

모델 정확도는 높게 나오지만 지속적으로 val_loss 값이 증가하는 것으로 보아, 약간의 과적합이 발생함을 확인했다.

 

제목 자체가 길지 않기 때문에 okt로 형태소 분석을 끝내고, stopwords로 불용어를 제거, 기타 전처리 과정을 끝냈을 때, 제목 최대 길이가 13밖에 나오지 않아서 그렇지 않을까 하는 추측을 해본다.

 

 

 

학습한 모델을 가지고 신상품순, 판매량순으로 새로 데이터를 크롤링해서 성능을 평가해보았다. 지속적으로 학습을 시키고, 더 많은 데이터를 학습시켰다면 신상품이 들어온다고 해도 정확도가 많이 떨어지지 않을 것으로 보인다.

 

 

 

 

 

10x10으로 학습시킨 모델을 쿠팡, 지마켓에 적용하려고 하니까 성능이 확실히 떨어지는 것을 확인할 수 있다. 제목도 각 사이트마다 짓는 방법도 다를 것이며, 같은 제품이라도 다른 카테고리에 있음을 확인했다.

 

예를 들어, 비타민C 고려은단은 당연히 건강식에 있을거라고 생각했는데... 쿠팡에서 아이 유아쪽 카테고리에 있었다.

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

네 번째 프로젝트 순서

 

1. 간단한 intro

2. 웹 크롤링 및 전처리

3. 모델 학습 및 평가


팀프로젝트로 진행했기 때문에 각각 팀원들이 직접 크롤링을 해볼 수 있게, 카테고리를 정해서 각자 코드를 만들어서 공유하자고 얘기를 했다. 이전에 했던 네이버 기사랑 비슷한 느낌이라서 우선 빠르게 진행을 하고, 팀원들이 질문을 하면 피드백을 해주는 형식으로 진행했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
import pandas as pd
from bs4 import BeautifulSoup
 
# 카테고리 설정 및 카테고리 별 url 숫자 지정
Category = ['Furniture','Life','Deco','Kitchen','Food','Baby,Kids','FasihonClothing','FasionGoods','Beauty','Jewelry']
Category_num = ['121','120','122','112','119','115','117','116','118','125']               
 
# 빈 데이터프레임 생성
df_section_title = pd.DataFrame()
 
for j in range(0,10):
    category = Category[j]
    print(category, ' 카테고리 크롤링 시작')
    category_num = Category_num[j]
    key_list = []
    
    #150페이지 크롤링
    for i in range(1,151):
        
        # 제목 추출  
        url = f'https://www.10x10.co.kr/shopping/category_main.asp?rect=&prvtxt=&rstxt=&extxt=&sflag=n&disp={category_num}&cpg={i}&chkr=False&chke=False&mkr=&sscp=Y&psz=40&srm=ne&iccd=0&styleCd=&attribCd=&icoSize=S&arrCate=&deliType=&minPrc=&maxPrc=&lstDiv=list&ab='
        headers = {'user-agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36'}
        html = requests.get(url, headers=headers)
        title = BeautifulSoup(html.text, 'html.parser')        
        keywords = title.select('p.pdtName.tPad07')        
        
        
        #6개는 행사 상품
        for k in keywords[6:78]:
            keyword = k.text
            key_list.append(keyword)        
        if i % 50 == 0:
            print(i,'페이지 완료')
cs

 

 

 

 

제목이 안 그래도 짧은데, 제목에 과적합을 야기할 수 있는 불필요한 단어들이 많이 포함되어 있어서 전처리가 필요했다. 특히 [무료배송], (1+1), ★특가할인★ 등과 같이 특정 문구[],{},()와 그 안에 들어있는 단어들을 제거해야했다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# [] / () 지우기
key_list2 = []
    
for key in key_list :
    if '[' and '(' in key :
        key = re.sub(r'\[[^)]*\]''', key)
        key_re = re.sub(r'\([^)]*\)''', key)
        key_list2.append(key_re)        
    elif '[' in key :
        key_re = re.sub(r'\[[^)]*\]''', key)
        key_list2.append(key_re)
    elif '(' in key :
        key_re = re.sub(r'\([^)]*\)''', key)
        key_list2.append(key_re)
    elif '★' in key :
        key_re = re.sub(r'\★[^)]*\★''', key)
        key_list2.append(key_re)
    else :
        key_list2.append(key)
print('(),[]삭제 완료')    
cs

이를 제거할 수 있는 코드 

 

 

 

re.sub(r'\([^)]*\)', '', key)

붉은 색 글씨 속에 있는 것들이 제거 됨을 알 수 있다.

개발자에게 쉬는 날은 없다. - 기억하자

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    #형태소 분류
    titles =[]
    for key in key_list2 :
        titles.append(re.compile('[^가-힣]').sub(' ',key))
        # '가'부터 '힣'빼고 나머지 제외(한글모두)
        # ^:뒤에 붙은 것 제외하고 모두
        #정규표현식 or 점프투파이썬 정규표현식 찾아보기
 
    df_title = pd.DataFrame(titles, columns=['title'])
    df_title['category'= category
    df = pd.concat([df, df_title], axis = 0, ignore_index=True)
    
    print(f'{category} 추가')
    print('-'*30)
 
if 'Unnamed: 0' in df.columns :
    df = df.drop(['Unnamed: 0'], axis=1)   
    df = df.dropna(axis=0)
    
# ' ' 스페이스 공백 지우기 
index_list = []
for i in range(len(df)):
    if df['title'][i].isspace() == True :
        index_list.append(i)
 
df = df.drop(index= index_list, axis = 0)
    
df.to_csv('test_data.csv', encoding='utf-8-sig')    
cs

다른 팀의 경우 조금 더 장문의 글을 형태소 분석을 해야 해서 mecab을 이용했다. 하지만 우리의 경우 제목이 길지 않아서 간단하게 okt로 형태소 분류를 진행했다. 영어의 경우, 대부분 브랜드 네임이라서 따로 카테고리 분류를 진행하지 않았다.

하지만 문제가 생긴 부분은 영어가 사라진 부분이 NaN 값이 아닌 공백으로 존재하고 있었다. 그래서 공백으로만 존재하는 행을 제거했다. isspace()일 경우 따로 리스트를 생성해서 drop하는 방식을 택했다. 

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys
from konlpy.tag import Okt
from collections import Counter
from wordcloud import WordCloud
 
def get_noun(category):
    
    okt=Okt()
    noun = okt.nouns(category)
    for i,v in enumerate(noun):
        if len(v) < 2:
            noun.pop(i)
            
    count = Counter(noun)
    noun_list = count.most_common(100)
    
    return noun_list
 
def visualize(noun_list):
    
    wc = WordCloud(font_path='Jalnan.ttf',\
              background_color = "white", \
              width = 1000, \
              height=1000, \
              max_words=100, \
              max_font_size=300)
    
    wc.generate_from_frequencies(dict(noun_list))
    wc.to_file('10x10_WordCloud.png')
    
if __name__=='__main__':
    filename = sys.argv[1]
    
    f= open('10x10(kor2).csv''r', encoding='utf-8')
    category = f.read()
    noun_list = get_noun(category)
    visualize(noun_list)
 
 
print('저장완료')
cs

추가로 모델 학습을 하는데 있어 불필요한 용어들이 너무 많아서 워드클라우딩을 사용하여, 불용어 리스트를 만들어서 제거하였다.  

 

 

+ Recent posts