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)

 

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


 

결과물부터 보자면, 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)


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

 

우선 로그인이 필요한 서비스라서 동적 크롤링으로 후기를 긁어왔다. 그리고 추가로 병원 이름, 클리닉명, 주소, 링크, 전화번호는 로그인 없이도 크롤링이 가능해서 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

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

 

 

+ Recent posts