<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>re-play</title>
        <link>https://velog.io/</link>
        <description>Graduate School of DataScience, NLP researcher. AI engineer at NAVER PlaceAI</description>
        <lastBuildDate>Wed, 01 Mar 2023 08:37:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>re-play</title>
            <url>https://images.velog.io/images/jonas-jun/profile/f229a07d-2e4e-49b3-af2b-6c83827f2376/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. re-play. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jonas-jun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[torchmetrics: pytorch-lightning 친화적인 metrics & logging]]></title>
            <link>https://velog.io/@jonas-jun/torchmetrics</link>
            <guid>https://velog.io/@jonas-jun/torchmetrics</guid>
            <pubDate>Wed, 01 Mar 2023 08:37:24 GMT</pubDate>
            <description><![CDATA[<h2 id="ai-모델의-평가-metric">AI 모델의 평가 metric</h2>
<p>ai 모델로 분류 문제를 풀 때 보통 accuracy, top-k accuracy, precision, recall, f1 score 등을 사용한다. 각각의 의미는 여기에 다루지 않겠다. 자연스럽게 모델을 학습하는 도중 validation을 진행할 때 위와 같은 평가지표(metric)을 계산하고, 최고의 weight(체크포인트)를 선정하게 된다. 최종적으로 선정된 모델을 test set에 대해 평가할 때도 같은 metric을 사용한다.</p>
<h2 id="as-is-scikit-learn의-metric">AS-IS: scikit-learn의 metric</h2>
<p>공부를 시작한 이후로 가장 많이 사용한 방식은 sklearn의 metric 라이브러리이다. 오래되었고 당시 많은 사람들이 사용하고 있었기 때문에 아무 의심 없이 싸이킷런 라이브러리가 최선이겠거니 생각하고 사용해왔다. 그러나... 아래와 같이 iteration의 결과값들을 모아주는 작업을 진행해야 했다. (<em>이것이 코드를 지저분하게 만드는지 생각하지도 못했다.</em>)</p>
<pre><code class="language-python"># pytorch lightning module
def validation_step(self, batch, batch_idx):
    images, labels, file_paths = batch
    loss, logits = self.shared_step(batch)
    self.log(&#39;val_loss&#39;, loss, on_epoch=True)
    return logits.cpu(), labels.cpu()

def validation_epoch_end(self, outputs):
    logits, targets = torch.tensor([]), torch.tensor([])
    for output in outputs:
        logits = torch.cat((logits, output[0]))
        targets = torch.cat((targets, output[1]))
    preds = torch.max(logits, dim=1).indices
    preds, targets = preds.numpy(), targets.numpy()
    top1 = accuracy_score(targets, preds)
    top5 = top_k_accuracy_score(targets, logits.numpy(), k=5, labels=range(41))
    precision = precision_score(targets, preds, average=&#39;macro&#39;, zero_division=1)
    recall = recall_score(targets, preds, average=&#39;macro&#39;, zero_division=1)
    f1 = f1_score(targets, preds, average=&#39;macro&#39;, zero_division=1)
    auc = roc_auc_score(targets, logits.numpy(), multi_class=&#39;ovo&#39;, labels=range(41))

    # log
    self.log(&#39;val_acc_top1&#39;, top1, sync_dist=True)
    self.log(&#39;val_acc_top5&#39;, top5, sync_dist=True)
    self.log(&#39;val_precision&#39;, precision, sync_dist=True)
    self.log(&#39;val_recall&#39;, recall, sync_dist=True)
    self.log(&#39;val_f1_macro&#39;, f1, sync_dist=True)
    self.log(&#39;val_auc&#39;, auc, sync_dist=True)</code></pre>
<h2 id="to-be-torchmetrics로-간결하게">TO-BE: torchmetrics로 간결하게</h2>
<p><img src="https://velog.velcdn.com/images/jonas-jun/post/f30e44f3-d8fb-4c66-95f0-087b68746413/image.png" alt=""></p>
<p>pytorch-lightning에서 torchmerics라는 라이브러리를 제공하고 있다. 이들에 따르면 위와 같은 기능들을 제공한다고 한다. &quot;Boilerplate를 줄인다&quot;는 말은 비슷한 코드들이 반복되는 것을 방지한다는 뜻으로 모듈화를 잘 해두었다는 의미이다. 가장 관심이 갔던 부분은 역시 &quot;Automatic&quot;이다.</p>
<ul>
<li>배치에서 나오는 데이터들을 accumulation 해주고</li>
<li>multiple devices 간의 싱크를 제공해준다고 한다.</li>
</ul>
<p>첫 번째는 AS-IS에서 outputs에서 output들을 뽑아서 하나로 모아주는 작업을 직접 코드상에서 하지 않도록 만들어준다. 이에 따라 validation_step에서 특별한 이유가 없다면 output을 return 하지 않아도 된다.
두 번째는 DDP 등의 방식으로 학습을 진행할 때, 여러 디바이스에서 나오는 결과값들을 모아주는 작업을 직접하지 않도록 도와준다. 사실 pytorch-lightning 자체에서 이러한 작업들을 도와주기 때문에 요즘에는 큰 문제가 되지는 않지만, multi-gpu 학습시에 기기간의 데이터 분산과 병합 문제는 깊은 단계에서의 작업이기 때문에 혹여 문제가 발생할 시에 핸들링하기가 어렵다.
다시 말해, <strong>outputs에 loop을 돌지 않아도 되기 때문에 코드가 간결해지고, 잘 짜여진 pytorch-lightning과 그에 잘 호환되는 metric 라이브러리를 활용하여 분산학습을 용이하게 도와줄 수 있다.</strong></p>
<pre><code class="language-python">def validation_step(self, batch, batch_idx):
    idx, x_val, y_val = batch
    logits = self(x_val)
    loss = self.val_criterion(logits, y_val)
    self.log(&#39;val_loss&#39;, loss, on_epoch=True, sync_dist=True)
    self.metric.update(logits, y_val)

def validation_epoch_end(self, outputs):
    metric_out = self.metric.compute()
    self.log(&#39;val_acc_top1&#39;, metric_out[&#39;acc_top1&#39;].cpu().item(), sync_dist=True)
    self.log(&#39;val_acc_top5&#39;, metric_out[&#39;acc_top5&#39;].cpu().item(), sync_dist=True)
    self.log(&#39;val_precision&#39;, metric_out[&#39;precision&#39;].cpu().item(), sync_dist=True)
    self.log(&#39;val_recall&#39;, metric_out[&#39;recall&#39;].cpu().item(), sync_dist=True)
    self.log(&#39;val_f1_macro&#39;, metric_out[&#39;f1_macro&#39;].cpu().item(), sync_dist=True)
    print(&#39;Result after Epoch: {:02d}&gt;&gt;&#39;.format(self.current_epoch))
    pprint(metric_out)
    self.metric.reset()</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[앙상블 모델로 이미지 분류 성능을 높여보자]]></title>
            <link>https://velog.io/@jonas-jun/imgclsensemble</link>
            <guid>https://velog.io/@jonas-jun/imgclsensemble</guid>
            <pubDate>Wed, 02 Nov 2022 11:46:28 GMT</pubDate>
            <description><![CDATA[<h2 id="문제--데이터셋">문제 &amp; 데이터셋</h2>
<p><img src="https://velog.velcdn.com/images/jonas-jun/post/21e50d4e-2697-45b9-8a78-9ad2373077d5/image.png" alt="">
출처: <a href="https://www.kaggle.com/code/abdelmoneimmostafa/intel-image-classification-90-29-accuracy">kaggle notebook</a></p>
<p>캐글에 게시된 intel image classification 데이터셋을 활용하여 간단하게 <strong>이미지 분류기 앙상블 모델</strong>을 실험해보았다. 실제 현업에서 마지막 classifier에 ML 모델을 도입하여 성능 개선을 이뤄본 경험이 있어 다른 데이터셋에도 적용해보고 싶었기 때문이다.
데이터셋은 아래와 같은 6개의 클래스, 총 24.3k장으로 구성되어 있다. EDA를 직접 자세히 진행해보지는 않았으나 산과 바다, 거리, 빌딩 등 비교적 명확한 경계를 가지고 있는 이미지들로 보인다. 다시 말해 성능이 꽤 높게 나올 수 있음을 뜻한다.</p>
<blockquote>
<p>buildings
forest
glacier
mountain
sea
street</p>
</blockquote>
<h2 id="아이디어">아이디어</h2>
<p>이미지든 자연어든 ai 모델로 분류를 진행할 때, 데이터를 embedding(또는 feature) 벡터로 나타낸 후 해당 벡터들을 활용하여 최종 분류(classification)를 진행하게 된다.
<img src="https://velog.velcdn.com/images/jonas-jun/post/4c8bf2d7-16b3-45de-89ea-387d5db728cb/image.jpg" alt=""></p>
<p>모델에는 Transformer 기반의 모델들이 들어가기도 하고 이미지에서는 CNN 기반의 Resnet, EfficientNet, Convnext 등이 간단히 사용되기도 한다.</p>
<p>모델에서 뽑아준 vector의 각각 숫자들을 하나의 feature로 보고, 머신러닝 모델의 input으로 넣어준다는 아이디어로 모델링을 진행하였다.</p>
<h2 id="모델">모델</h2>
<p>여기에서는 모델은 이미지+텍스트 멀티 모달(Multi-model) 학습이 진행된 CLIP(Resnet 50x16)을 활용하면서 Classifier 부분에 전통적인 Machine Learning 모델은 SVM(Support Vector Machine)을 사용하였다. SVM에 대해서는 별도로 docs를 참고해보시길.</p>
<h2 id="코드">코드</h2>
<h4 id="모델-1">모델</h4>
<pre><code class="language-python"># CLIP feature extractor
class CLIPextractor(nn.Module):
    def __init__(self, pretrain_name=&#39;RN50x16&#39;):
        super(CLIPextractor, self).__init__()
        model, preprocess = clip.load(pretrain_name)
        self.clip = model.visual
        self.input_resolution = model.visual.input_resolution
        print(&quot;model.visual.output_dim      :&quot;, model.visual.output_dim) # 768

    def forward(self, input):
        return self.clip(input)

# SVM classifier
regr = SVC(kernel=&#39;rbf&#39;, random_state=seed)</code></pre>
<h4 id="학습">학습</h4>
<p>모델에서 768 dimension의 feature vector들을 추출한 후 간단하게 regression 모델에 fit을 진행해주면 된다. 자세한 코드는 아래 github repo를 참고.</p>
<pre><code class="language-python"># feature extract
def feature_extract(model, loader, device):
    model.to(device)
    _ = model.eval()

    features = torch.tensor([])
    labels = list()
    with torch.no_grad():
        for batch in tqdm(loader):
            image, label, _ = batch
            image = image.to(device)
            feature = model(image).detach().cpu()
            features = torch.cat([features, feature])
            labels += label

    return features, labels

# SVM fit
regr.fit(np.array(train_features), np.array(train_labels))</code></pre>
<h4 id="test">TEST</h4>
<p><img src="https://velog.velcdn.com/images/jonas-jun/post/7e73fef0-871e-40e4-9c82-a602dc2e6f30/image.png" alt="">
테스트셋에서 대해 accuract 95%, confusion matrix를 보면 틀린 케이스가 거의 없음을 알 수 있다. 공개된 캐글 notebook을 볼 때 테스트셋에 대해 95% 이상 성능을 보이는 경우가 눈에 띄지 않는다. 코드상으로 간단하지만 강력한 방식이라 할 수 있겠다.</p>
<h2 id="links">links</h2>
<p><a href="https://github.com/jonas-jun/img_cls_ensemble">github/jonas-jun/img_cls_ensemble</a>
<a href="https://www.kaggle.com/datasets/puneet6060/intel-image-classification">kaggle/intel-image-classification</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kubernetes in action] WSL에 minikube 설치하기]]></title>
            <link>https://velog.io/@jonas-jun/minikubewsl</link>
            <guid>https://velog.io/@jonas-jun/minikubewsl</guid>
            <pubDate>Fri, 20 May 2022 07:25:28 GMT</pubDate>
            <description><![CDATA[<p>k8s를 실험해보기 위해서는 node cluster가 필요하다. 네이버의 경우 n2c, 구글이나 아마존의 클라우드에서도 배포가 가능한 환경을 받을 수 있다.
미니쿠브는 무료로 로컬에서 쿠버네티스 클러스터를 사용할 수 있게 해주는 환경 프로그램이다. <a href="https://minikube.sigs.k8s.io/docs">minikube docs</a></p>
<blockquote>
<p>minikube quickly sets up a local Kubernetes cluster on macOS, Linux, and Windows. We proudly focus on helping application developers and new Kubernetes users.</p>
</blockquote>
<h4 id="minikube-설치-페이지">minikube 설치 페이지</h4>
<p><img src="https://velog.velcdn.com/images/jonas-jun/post/81a946f6-5eb5-4e5c-a871-e7a9b9734bac/image.png" alt="install_page">
원래는 위와 같은 공식 설치 페이지(<a href="https://minikube.sigs.k8s.io/docs/start">link</a>)에서 본인의 운영체제에 맞는 버전을 다운로드 받아 설명대로 설치하면 된다. mac의 경우 brew를 통해서도 설치 가능하다.
다만 윈도우에서 wsl을 사용하는 유저는 설치를 조금 다르게 해줘야 한다. <em>wsl은 리눅스 시스템인데 윈도우용을 받아서 윈도우에 설치한다면 wsl 상에서 실행이 되지 않는다.</em> Path 설정을 해주는 될 것 같기는 하지만 좀 더 간단한 방법으로 설치하도록 하자.
<img src="https://velog.velcdn.com/images/jonas-jun/post/b406f476-ac01-480f-8ae1-df6e5e9f44d2/image.png" alt="install_linux">
버전을 윈도우가 아니라 리눅스로 선택하면 아래와 같이 binary 파일 다운로드 주소를 얻을 수 있다. wsl의 아무곳에서나 위의 명령어를 통해 minikube를 설치한다.</p>
<pre><code class="language-bash">curl -LO minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64</code></pre>
<p>curl을 통해 다운로드 받은 후 아래와 같이 install을 해준다. 리눅스의 usr/local/bin에 설치해줌으로서 어느 경로에서든 kubectl 명령어를 사용할 수 있게 된다.</p>
<pre><code class="language-bash">sudo install minikube-linux-amd64 /usr/local/bin/minikube</code></pre>
<h4 id="설치-확인">설치 확인</h4>
<pre><code class="language-bash">&gt; minikube version
minikube version: v1.25.2
commit: 362d5fdc0a3dbee389b3d3f1034e8023e72bd3a7</code></pre>
<h4 id="minikube-시작">minikube 시작</h4>
<pre><code class="language-bash">minikube start --driver=docker</code></pre>
<p>docker desktop이 설치된 상태에서 도커 기반으로 minikube를 실행한다. 드라이버는 따로 지정해주지 않아도 자동으로 도커로 설정이 될 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WSL2 ubuntu 환경에 anaconda 설치하기]]></title>
            <link>https://velog.io/@jonas-jun/wslanaconda</link>
            <guid>https://velog.io/@jonas-jun/wslanaconda</guid>
            <pubDate>Sun, 06 Mar 2022 09:37:13 GMT</pubDate>
            <description><![CDATA[<p>윈도우 데스크탑 구매 후 wsl(window subsystem for linux) 환경을 구축하였다. wsl-리눅스(ubuntu 20.04)에 anaconda를 설치하여 머신러닝/데이터분석 환경을 만들었는데, 처음 써 보는 wsl과 맥os의 경로가 다른 부분이 있어 약간의 혼선이 있었다.</p>
<p><em>설명은 linux z shell(zsh)가 잘 설치되어 있다는 가정 하에 합니다.</em></p>
<h2 id="1-아나콘다-for-linux-다운로드">1. 아나콘다 for linux 다운로드</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/2f813e76-5e54-4c60-a0e3-ab3289893fcc/image.png" alt="">
<a href="https://www.anaconda.com/products/individual">anaconda 홈페이지</a>에서 linux용 콘다를 다운로드 받는다. 해당 페이지의 링크를 복사한 다음 리눅스 터미널에서 wget으로 다운로드 받으면 된다. wsl2의 home/user 아래에 다운로드 받았다.</p>
<pre><code class="language-bash">wget https://repo.anaconda.com/archive/Anaconda3-2021.11-Linux-x86_64.sh</code></pre>
<h2 id="2-설치하기">2. 설치하기</h2>
<p>쉘 파일(.sh)이 받아지면 sh *.sh로 아나콘다를 설치한다. 나 같은 경우 home/jonas/로 가서 sh 파일을 설치하였다. 설치 과정에서 경로를 묻게 되는데 아마 sh 파일이 있는 그 디렉토리 내에 그대로 설치가 될 것이다. yes 해주면 됨.
<img src="https://images.velog.io/images/jonas-jun/post/31a263fb-1ff8-4747-a6aa-139817e1d96e/image.png" alt=""></p>
<pre><code class="language-bash">sh Anaconda3-2021.11-Linux-x86_64.sh</code></pre>
<h2 id="3-zsh에-환경변수-추가">3. zsh에 환경변수 추가</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/9e41f7b2-a1eb-4928-aec6-e6e1c7d3dddb/image.png" alt="">
bash는 z shell이든 환경변수에 아나콘다의 경로를 지정해주어야 어느 경로에서든 기본적으로 콘다를 실행할 수 있다. 그리고 conda initialize 내용도 설정 파일에 추가해주어야 한다.</p>
<p>1) vi나 vscode를 활용하여 zshrc를 연다. 최상위 디렉토리에 숨김파일로 들어있기 때문에 경로는 ~/.zshrc 이다. vscode로 편집하는 것이 편하기 때문에 code로 오픈.</p>
<pre><code class="language-bash">code ~/.zshrc # vi ~/.zshrc
source ~/.zshrc</code></pre>
<p>2) conda의 경로를 추가한다. 위에서 설치한 anaconda3 폴더를 보면 bin에 binary 파일들이 있는데, 그 폴더를 환경변수에 추가해주어야 한다. home으로부터 시작되는 본인의 anaconda3/bin 경로를 넣어주면 된다.</p>
<pre><code class="language-bash">export PATH=~/home/jonas/anaconda3/bin:$PATH</code></pre>
<p>3) conda initialize 내용을 zshrc에 추가한다.</p>
<pre><code># &gt;&gt;&gt; conda initialize &gt;&gt;&gt;
 # !! Contents within this block are managed by &#39;conda init&#39; !!
 __conda_setup=&quot;$(&#39;/home/jonas/anaconda3/bin/conda&#39; &#39;shell.zsh&#39; &#39;hook&#39;     2&gt; /dev/null)&quot;
 if [ $? -eq 0 ]; then
     eval &quot;$__conda_setup&quot;
 else
     if [ -f &quot;/home/jonas/anaconda3/etc/profile.d/conda.sh&quot; ]; then
         . &quot;/home/jonas/anaconda3/etc/profile.d/conda.sh&quot;
     else
         export PATH=&quot;home/jonas/anaconda3/bin:$PATH&quot;
     fi
 fi
 unset __conda_setup
 # &lt;&lt;&lt; conda initialize &lt;&lt;&lt;</code></pre><p>4) 편집이 완료되면 저장 후 쉘(터미널)에서 source로 업데이트한다.</p>
<pre><code class="language-bash">source ~/.zshrc</code></pre>
<h2 id="4-잘-실행되는지-확인">4. 잘 실행되는지 확인</h2>
<pre><code class="language-bash">conda --version</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/d7fbe296-ee72-4459-b2f2-62e6ba0efe5a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HuggingFace Tokenizer [unused01] 토큰 사용하기]]></title>
            <link>https://velog.io/@jonas-jun/tokenizerspecialtokens</link>
            <guid>https://velog.io/@jonas-jun/tokenizerspecialtokens</guid>
            <pubDate>Sat, 20 Nov 2021 09:13:14 GMT</pubDate>
            <description><![CDATA[<h2 id="problem">Problem</h2>
<p>언어 모델 학습 이전에 적절한 토크나이저의 학습이 필요하고, 이 때 [unused00] 토큰들을 추가해서 이후에 스페셜 토큰으로 활용하곤 한다.
나는 aspect word 토큰 앞 뒤에 스페셜 토큰을 붙여서 그 안의 토큰들을 태깅할 계획이었는데 아래와 같이 [ASP]와 [unused00] 토큰들을 추가해서 학습시켰음에도 불구하고 제대로 토크나이즈가 되지 않는 문제가 발생했다.
<img src="https://images.velog.io/images/jonas-jun/post/ce3a7acf-52fa-4e6a-a21a-af08243aa324/unused01.jpg" alt="">
<img src="https://images.velog.io/images/jonas-jun/post/be5d23ca-5c0d-4b07-80a9-9dcdd96e4a40/unused03.jpg" alt=""></p>
<h2 id="solution">Solution</h2>
<p>허깅페이스 깃허브에도 이와 같은 이슈를 문의한 내용이 있었고, 참고하여 해결하였다. 해결방법은 &quot;do_basic_tokenize=False&quot;. Document의 해당 부분을 읽어보지 못해서 왜 베이직 토크나이즈를 False로 주는지 그 의미는 아직 잘 이해하지 못했으나, 이후에도 사용할 일이 있을 것 같아서 메모해둔다.
<img src="https://images.velog.io/images/jonas-jun/post/e5306cf2-9131-4b53-9fa5-2c859c243ca4/unused02.jpg" alt=""></p>
<h2 id="example">Example</h2>
<p>tokenizer를 읽어올 때 do_basic_tokenize=False를 똑같이 주었다. from_pretrained가 아니어도 같은 로직으로 작동한다.</p>
<pre><code class="language-python">from transformers import BertTokenizer
tkzer2 = BertTokenizer(PATH_TOKENIZER, do_lower_case=False,
                     do_basic_tokenize=False)</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/732c51c5-47ec-4a40-ad34-d8b3ec061cb3/unused04.jpg" alt=""></p>
<p>references
<a href="https://github.com/huggingface/transformers/issues/4683">github/huggingface</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PyTorch Dataset 구축 방식 시간 비교 (List형 vs Dict형)]]></title>
            <link>https://velog.io/@jonas-jun/Dataset%EC%8B%9C%EA%B0%84%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@jonas-jun/Dataset%EC%8B%9C%EA%B0%84%EB%B9%84%EA%B5%90</guid>
            <pubDate>Sat, 18 Sep 2021 23:59:36 GMT</pubDate>
            <description><![CDATA[<h2 id="두-가지-dataset-유형">두 가지 Dataset 유형</h2>
<p><strong>Dict형</strong>: dataset[index]를 찍어보았을 때 아래와 같이 데이터셋이 담겨 있는 형태이다. dataset list 안에 dict 형태로 데이터가 존재한다.</p>
<blockquote>
<p>{&#39;input_ids&#39;: [1944, 126, 34, 122, 2693, ...],
&#39;label&#39;: 2,
&#39;tokens&#39;: [&#39;_헤드셋&#39;, &#39;보다&#39;, &#39;_더&#39;, &#39;_빨리&#39;, ...],
&#39;text&#39;: 헤드셋보다 더 빨리왔. 시냅스연결 후 크로마 색도 좋고 ...}</p>
</blockquote>
<p><strong>List형</strong>: dataset[index] 안에 list 형태로 데이터셋이 담겨 있다.</p>
<blockquote>
<p>[[1944, 126, 34, 122, 2693, ..], 2, [&#39;_헤드셋&#39;, &#39;보다&#39;, &#39;_더&#39;, &#39;_빨리&#39;, ...], 헤드셋보다 더 빨리왔. 시냅스연결 후 크로마 색도 좋고 ...]</p>
</blockquote>
<p>Torch 모델에 넣기 위한 Dataset을 구축할 때 위의 두 가지 방법을 사용하곤 한다. 수십만 건의 데이터를 처리할 때는 큰 차이를 느끼지 못했는데 수천만 건의 데이터로 데이터셋을 만들다 보니 어떤 방식이 얼마나 빠를지 궁금해서 테스트를 해보았다.</p>
<h2 id="실험-데이터셋">실험 데이터셋</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/ded05a24-7264-4d2e-a133-acbff500957e/image.png" alt="">
간단한 리뷰-감성(별점은 customized하여 encoding) 데이터셋이다. 370만 건 정도만 가지고 테스트를 해보았다.</p>
<h2 id="tokenizer">Tokenizer</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/6b37c0cb-0d76-4143-8777-46d680822148/image.png" alt="">
자연어 데이터를 Dataset으로 구축할 때에는 Tokenize 작업이 필요하다. 사실 이 부분이 가장 많은 시간을 차지할 것이다. 실험에선 약 500만 건의 동일 카테고리 리뷰로 학습한 SentencePiece Tokenizer를 사용했다. Google의 spm 라이브러리로 사전학습 후 저장해둔 model을 사용한다.</p>
<h2 id="실험-코드">실험 코드</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/13b55759-16e4-41d5-aa02-ce3f4eff5c40/image.png" alt="">
<img src="https://images.velog.io/images/jonas-jun/post/886bca6c-0430-4617-8cff-ac7dca7ca814/image.png" alt=""></p>
<p>Dict 형태는 위 처럼 dict를 만들어서 추가해주는 형태이고 List형은 아래처럼 추가한다. 너무 많지 않은 데이터셋이기 때문에 list형태로 전체를 받아서 저장해둔다. 그리고 시간 측정.</p>
<h2 id="결과-비교-list형이-20-가량-빠름">결과 비교: List형이 20% 가량 빠름</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/51e72026-ae7b-4337-8576-4137c094dec2/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/jonas-jun/post/84a3280f-4074-47ba-b337-789791813771/image.png" alt="">
<img src="https://images.velog.io/images/jonas-jun/post/b90faab9-5fb8-4d12-965e-2efa58018459/image.png" alt=""></p>
<p>위가 dict형태, 아래가 list형태로 데이터셋을 처리한 결과이다. 각 샘플을 dict로 조합하는 과정에서 시간이 더 소모되는 건 맞지만 약 20% 정도의 차이는 예상보다 컸다.</p>
<p><strong>모델에서 Data를 load할 때 dict형은 정확하게 key 값으로 접근 가능하다는 장점이 있다. 하지만 데이터 크기가 더 증가하거나 동료들과의 코드 공유가 적은 개인 연구를 진행할 때는 list 형태를 활용하는 편이 좋겠다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[처음 써보는 면접 후기]]></title>
            <link>https://velog.io/@jonas-jun/%EC%B2%98%EC%9D%8C-%EC%8D%A8%EB%B3%B4%EB%8A%94-%EB%A9%B4%EC%A0%91-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@jonas-jun/%EC%B2%98%EC%9D%8C-%EC%8D%A8%EB%B3%B4%EB%8A%94-%EB%A9%B4%EC%A0%91-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 15 Sep 2021 11:59:27 GMT</pubDate>
            <description><![CDATA[<p>부끄럽고 자괴감이 많이 들었지만 부족했던 부분과 면접 자리에서 대답이 아쉬웠던 질문들을 기억해두기 위한 기록.</p>
<ol>
<li><p>내가 AI/ML 엔지니어로서 가질 수 있는 강점?
AI/ML 모델 기본기. 단지 도메인 specific한 공부만 한 게 아니라 모델의 학습 원리와 과정에 대해서도 공부를 했다.
1) 주가데이터 예측, 식사메뉴 예측 등의 과제를 통해 seq. 데이터에 어울리는 RNN 기반의 모델들을 데이터셋에 맞게 구현해보았고,
2) 얼굴 이미지를 embedding 하여 유사한 celeb의 얼굴을 유사도로 찾아내거나, 리뷰 텍스트를 1dim CNN으로 인코딩하는 등 CNN 계열의 모델들로 직접 구현해보았다.
3) Transformers의 내부 구조에 대해 공부를 하고 attention의 의미를 이해하고 scoring 방법을 달리하여 구현할 수 있다.
[Alpha]로 페이퍼를 이해하고 정리하여 공유했던 경험이 많아서, ML 기본 지식을 더해서 유사하게 구현해볼 수 있다.</p>
</li>
<li><p>SW엔지니어로서 경험이 부족하다.
나는 개발자로서 Ops 전반에 대한 경험이 부족하다.
매일 코딩테스트 문제를 풀면서 자료구조나 알고리즘에 대해서는 자신이 있고, task가 주어졌을 때에 잘 구현이 안되는 부분들은 검색을 해가면서 풀어낼 수 있다. 이 부분은 독하게 마음 먹고 트레이닝을 해 와서 기본적인 코딩테스트는 무리가 없이 항상 통과를 할 수 있다.
미약하지만 모델링과 성능 테스트에서 그치지 않고 사용을 위해 프로그램화한 경험이 있다. (미세먼지 예측 모델)
다만, 추가로 프로그램을 완성하여 배포해보는 경험이나 하드코딩을 보여줄 수 있을 만한 경험이 있으면 좋을 것 같다..</p>
</li>
</ol>
<ol start="3">
<li>데이터의 분포를 파악해서 성능 개선을 했던 경험은?
1) 미세먼지 등급 예측 모델을 만들 때 custom metric으로 &#39;bad recall&#39;이라는 지표를 설정했고, 그렇다면 나쁨/매우나쁨 단계를 정확하게 예측하는 것이 중요해진다. 하지만 실제 한국에서의 미세먼지 농도 데이터를 보면 나쁨/매우나쁨 단계인 적이 많지 않다. data의 augmentation을 어떻게 해야하지? 언어나 이미지라면 self-supervised learning이 이뤄질 수 있도록 task를 제시하고 학습을 시킬 수 있는데.. 나 같은 경우는 RF를 구현할 때 데이터를 bootstrap하는 과정에서 class weight를 나쁨 단계에 높게 줘서 학습이 좀 더 이뤄질 수 있도록 시도해보았고 동일 weight를 줬을 때보다 accuracy가 약간 떨어졌지만 bad recall 스코어를 개선시킬 수 있었다.</li>
</ol>
<ol start="3">
<li>한번에 메모리에 올라가지 않는 데이터를 특정 column(ex. id) 기준으로 정렬해야할 때 어떻게 할까?</li>
</ol>
<ul>
<li>Iterator 상태에서 ID를 기준으로 데이터를 자르고 각 segment를 메모리에 올려서 inplace 방식의 정렬을 사용한다. 그리고 마지막에 ID의 순서에 따라 데이터들을 merge할 수 있다.</li>
<li>실제로는 어떻게 이뤄지고 있을지?</li>
</ul>
<ol start="4">
<li><p>B+Tree의 특징은? (B-Tree와 비교해야지)</p>
</li>
<li><p>legacy한 코드를 고쳐야할 때? 어떻게 할까?</p>
</li>
<li><p>나는 몇년차의 대우를 받아야 하는가?</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[해커랭크] Roads and Libraries, 각 싸이클마다 도서관이 하나씩 들어가게 하는 비용]]></title>
            <link>https://velog.io/@jonas-jun/Roads-and-Libraries</link>
            <guid>https://velog.io/@jonas-jun/Roads-and-Libraries</guid>
            <pubDate>Tue, 07 Sep 2021 03:31:30 GMT</pubDate>
            <description><![CDATA[<h2 id="question">Question</h2>
<p>1) 도시의 개수, 2) 도서관을 짓는 비용, 3) 도로를 짓는 비용, 4) 연결 가능한 도로 (edge)가 주어지면, 모든 도시에서 도서관에 접근 가능하도록 도서관과 도로를 공사하는 최소 비용을 구하는 문제
<img src="https://images.velog.io/images/jonas-jun/post/832ba94f-9895-4704-9e17-a2434cfca5b1/image.png" alt=""></p>
<ul>
<li>연결 가능은 하더라도 굳이 연결하지 않아도 다른 도시를 통해 도서관에 갈 수 있다면 그 도로는 짓지 않아도 됨. (1-&gt;2-&gt;3)이 먼저 연결된다면 (1-&gt;3)의 도로를 짓지 않아도 된다는 뜻</li>
<li>도로 연결보다 모든 도시에 도서관을 지어버리는 게 비용이 저렴한 경우도 있음</li>
</ul>
<h2 id="solution">Solution</h2>
<p>우선 비용 측면에서 계산이 필요하다.</p>
<p>1) 도로 비용 &gt;= 도서관 비용: 전체 도시에 도서관을 짓는 게 저렴
2) 도로 비용 &lt; 도서관 비용: 가능한 범위에서 도시들을 다 잇고 각 싸이클마다 도서관을 하나씩 짓는 게 유리</p>
<p>아래와 같이 하나의 싸이클 내에서 도서관을 배치하는 경우에 따른 비용을 생각해보자.</p>
<blockquote>
<p>도시마다 도서관을 짓는 경우: cost_lib * num_cities
모두 도로로 연결하고 도서관은 1개만 짓는 경우: cost_lib * 1 + cost_road * (num_cities-1)
도서관을 k개 짓는 경우: cost_lib * k + cost_road * (num_cities - k)</p>
</blockquote>
<p>각 케이스마다 비교하는 부등식을 써 보면 결국엔 cost_lib과 cost_road를 비교해서 도로가 저렴하다면 가능한 도로로 모두 연결하고 그렇지 않다면 가능한 도서관을 많이 짓는 편이 좋다는 결론을 낼 수 있다.</p>
<p>PSEUDO</p>
<ul>
<li>도로와 도서관 비용 비교해서 도로가 더 비싸거나 같다면 도시 개수 * 도서관 비용 return (모든 도시에 도서관)</li>
<li>연결 가능한 길 --&gt; 그래프로 만들기(DFS를 위해)</li>
<li>DFS 돌면서 실제 연결해야 하는 길의 개수 세기 (연결 가능하지만 이미 visited된 도시라면 안 가도 되기 때문)</li>
<li>한 DFS 싸이클마다 도서관을 하나씩 지어야 함</li>
<li>도로 비용 * 실제 DFS가 이동한 도로의 개수 + 도서관 비용 * 싸이클의 개수</li>
</ul>
<p>코드 일부
DFS 돌면서 num_roads를 늘려주고, 각 싸이클마다 도서관의 수(num_cycles)를 하나씩 늘려줌. 모든 코드는 아래의 깃허브에서.</p>
<pre><code class="language-python">    num_roads = 0
    def dfs_helper(root):
        nonlocal graph, visited, num_roads
        visited[root] = True
        for loc in graph[root]:
            if not visited[loc]:
                num_roads += 1
                dfs_helper(loc)

    num_cycles = 0
    for city in range(1, n+1):
        if not visited[city]:
            dfs_helper(city)
            num_cycles += 1</code></pre>
<p>[github] (<a href="https://github.com/jonas-jun/cote/blob/main/hr_Roads_Libraries.py">https://github.com/jonas-jun/cote/blob/main/hr_Roads_Libraries.py</a>)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[boj1679] 숫자놀이. DFS, DP, itertools 모두 학습이 가능한 문제]]></title>
            <link>https://velog.io/@jonas-jun/boj1679</link>
            <guid>https://velog.io/@jonas-jun/boj1679</guid>
            <pubDate>Thu, 26 Aug 2021 09:01:57 GMT</pubDate>
            <description><![CDATA[<p>boj1679 숫자놀이 백준 1679
DFS와 DP, itertools로 직접 구현을 모두 해볼 수 있는 문제이면서, 의외로 itertools로 풀었을 때가 DFS보다 더 효율적이었던 문제</p>
<h2 id="question">Question</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/23157ab0-6e68-47bc-94a0-0fbbf5655187/image.png" alt="">
nums array와 최대 개수를 주면, nums의 숫자들을 최대 개수 한도에서 아무거나 뽑아서 그 합을 만들 수 있다. 그리고 1부터 숫자를 올려가면서 못 만드는 숫자가 처음 나오면 return.
[1,2]와 2개가 주어진다면 1, 2, 3, 4(2+2)까지 만들 수 있고 답은 5가 되는 것이다. 숫자가 좀 길어지면 중간중간 빈 숫자들이 발생하게 된다.</p>
<h2 id="solution">Solution</h2>
<p>우선 공통적으로 memo list를 만들어서 (길이는 max값 * 최대 개수: 만들 수 있는 가장 큰 수) 가능한 숫자들을 True, 불가능하면 False로 할당해준다. 마지막에 1부터 1씩 올라가면서 False를 발견하면 return.</p>
<pre><code class="language-python"># idx 1이 그대로 숫자 1을 나타낼 수 있도록 앞에 False를 하나 더 붙여줬음
memo = [False for _ in range((nums[-1]*max_num) + 1)]</code></pre>
<h3 id="solution-1-itertools-product">Solution 1: itertools product</h3>
<p>itertools.product는 주어진 iterable 자료 안에서 중복/순서 관계 없이 n번 원소를 추출하는 함수이다.
숫자가 3개 주어지고 최대 5개를 활용할 수 있다면 (0,0,1), (1,2,2), (2,1,1) 등의 각 숫자별 개수 pair를 만들어야 한다. (1,2,2)는 숫자A가 1번, 숫자B가 2번, 숫자C가 2번 포함된 수.
그렇다면 [1,2,3,4,5]에서 중복/순서 관계 없이 3개의 숫자를 뽑으면 되는 것이다. 그리고 (3,4,5)와 같은 경우들은 합이 5가 넘어가므로 불가능하기 때문에 filter로 제거해준다.</p>
<pre><code class="language-python">pool = list(filter(lambda x: 0&lt;sum(x)&lt;=max_num, list(product(pool, repeat=length))))</code></pre>
<p>pair들을 loop 돌면서 가능한 경우들을 memo에서 제거해주면 된다.
코드는 아래 깃헙에.</p>
<h3 id="solution-2-dfs">Solution 2: DFS</h3>
<p>한 step에서 +3, +5로 갈 수 있는 DFS를 돌리면 된다. 주의할 점은 꼭 5개가 되어야만 하는 게 아니기 때문에 매 step 마다 memo를 업데이트해주는 것.</p>
<pre><code class="language-python">def dfs_helper(cum, length):
    nonlocal nums, max_num, memo
        memo[cum] = True
        if length == max_num: return

        for num in nums:
            dfs_helper(cum + num, length+1)

dfs_helper(0, 0)</code></pre>
<p>시간 초과가 나왔다. itertools는 되는데 이게 시간초과가 나오는 이유는 최대 개수가 커질 때이다. (dfs를 돌아야할 깊이가 깊어질수록) 경우의 수가 급수로 늘어나기 때문이다. (결과는 아래에)</p>
<h3 id="solution-3-dynamic-programing">Solution 3: Dynamic Programing</h3>
<p>DFS와 유사한 개념이면서 중복 탐색을 줄일 수 있는 방법이다.
dp[i]는 숫자를 i번 골랐을 때 가능한 합들
dp[i+1]은 dp[i]에 각각의 nums를 더해줬을 때 나오는 수들이 되어야 한다.
DFS와 달리 DP에서는 중간 단계들에서 중복되는 수들을 없애줄 수 있다. 그래서 다음 단계로 넘어갈 때 같은 숫자를 탐색하게 되는 경우가 사라지게 된다.
예를 들어, i번째에서 3을 만들 수 있는 경우가 3가지가 있다면, DFS는 그 세 가지 경우에서 모두 i+1번째로 가면서 nums의 숫자들을 더해주게 되지만, DP는 합이 3인 경우엔 하나로 취급하여 셋중 한 가지 경우만 추가로 탐색을 이어간다고 생각할 수 있다.</p>
<pre><code class="language-python">    for i in range(2, len(dp)):
        before = dp[i-1]
        for b in before:
            for num in nums:
                memo[b+num] = True
                dp[i].add(b+num)</code></pre>
<p>각 i를 set으로 만들어줘서 중복을 피해주고 숫자가 나오는 대로 memo를 업데이트 해준다.</p>
<p>세 가지 솔루션 모든 코드는 <a href="https://github.com/jonas-jun/cote/blob/main/bj_1679_NumberGame.py">github</a></p>
<h2 id="result">Result</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/48245a62-3d79-4119-bdbb-f8aea808e75c/image.png" alt="">
백준 시스템 안에서 DFS는 시간초과, dp가 itertools에 비해 1/2 정도 시간 단축되지만 itertools를 사용해도 통과가 된다.</p>
<p><img src="https://images.velog.io/images/jonas-jun/post/744e9abb-8edc-4c62-a9f2-d8502e7e2b8e/image.png" alt="">
짧은 케이스와 중간 케이스 둘을 로컬에서 돌려보았다. 짧을 때는 DFS가 itertools보다 빨랐지만, 길어질수록 효율이 뒤집어진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[boj1148] word A의 알파벳으로 word B를 만들 수 있니? Counter로 비교하기]]></title>
            <link>https://velog.io/@jonas-jun/boj1148</link>
            <guid>https://velog.io/@jonas-jun/boj1148</guid>
            <pubDate>Wed, 25 Aug 2021 15:03:57 GMT</pubDate>
            <description><![CDATA[<p>boj 1148 백준 1148 단어 만들기</p>
<h2 id="두-단어-간의-포함-관계">두 단어 간의 포함 관계</h2>
<p>word1의 알파벳들로 word2를 만들 수 있을까? 알파벳이 각각 몇개씩 포함되어 있는지 counter를 만든 후에, word2의 알파벳 각각이 word1에 모두 포함되어 있으면 가능하다.
<img src="https://images.velog.io/images/jonas-jun/post/f4e96c4e-3916-4539-ae65-a53776a02ee7/image.png" alt="">
word1에 a가 1개 이상 있어야 하고, b는 word1에 1개 이상, c도 word1에 1개 이상 들어 있으면 된다. word2의 char를 loop돌면서 word1의 counter와 비교해주면 되는 것이다. 코드는 아래 문제 솔루션 1번 부분에 담겨 있다.</p>
<h2 id="question">Question</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/08282d08-94e9-4f26-961a-20dd4b730a6c/image.png" alt=""></p>
<p>결국엔 퍼즐 9개 알파벳과 사전이 주어질 때, 알파벳을 조합해서 사전 속 단어를 몇 개나 만들 수 있는지?
가운데 문자는 꼭 활용해야 하는데, 가운데 어떤 알파벳이 들어가야 최대치가 되고 최소치가 되는지? 뽑아내는 것
<img src="https://images.velog.io/images/jonas-jun/post/09fb888f-6428-4190-bb28-3449cbb232da/image.png" alt="">
사전에는 단어 최대 20만 개.. 사전 속 단어들을 정렬 같은 거 하면 안된다.</p>
<h2 id="solution">Solution</h2>
<ol>
<li>사전에서 puzzle로 만들 수 있는 단어들을 추출 (counter 활용)</li>
<li>puzzle 문자를 하나씩 가운데에 놨을 때 가능한 단어 개수 파악</li>
<li>최대/최소를 뽑아서 형식에 맞게 return</li>
</ol>
<p>1번 부분: 퍼즐의 알파벳들로 단어A를 만들 수 있는지? 단어A의 문자 루프 돌면서 그게 퍼즐에 없거나 퍼즐보다 더 많이 있다면 못 만듦.</p>
<pre><code class="language-python">    for word in words:
        p = True
        word = build_counter(word)
        for char in word:
            if char not in tgt or word[char]&gt;tgt[char]:
                p = False
                break
        if not p: continue
        possible.append(word)</code></pre>
<p>2번 부분: 알파벳 하나씩 퍼즐 중앙에 놨을 때 그 알파벳 꼭 포함해야 하므로 가능한 단어들 중 그 알파벳이 포함되지 않은 단어들을 빼줘야 함. 가능한 단어만 count를 올려주었음.</p>
<pre><code class="language-python">    for char in tgt:
        temp = 0
        for word in possible:
            if char not in word: # 중간 문자는 꼭 포함해야 하므로.
                continue
            temp += 1
        ans[char] = temp</code></pre>
<p>나머지 풀 코드는 <a href="https://github.com/jonas-jun/cote/blob/main/bj_1148_makeWord.py">github</a> 에서 확인 가능.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Research] Word2Vec으로 쇼핑 리뷰 속 유사한 의미를 갖는 단어들을 찾아보자-2]]></title>
            <link>https://velog.io/@jonas-jun/Research-W2V2</link>
            <guid>https://velog.io/@jonas-jun/Research-W2V2</guid>
            <pubDate>Mon, 23 Aug 2021 09:11:42 GMT</pubDate>
            <description><![CDATA[<p>W2V의 Skipgram 방식으로 상품군별 쇼핑리뷰 corpus를 학습시키고, aspect seed와 유사한 단어들을 추출해내는 과정.</p>
<h2 id="1-상품군별-word2vec-학습">1. 상품군별 Word2Vec 학습</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/f7a640d6-520e-4d6f-90f6-bd0439c684c1/image.png" alt="">
morphs로 쪼개진 리뷰들이 List[List[str]] 형태로 담겨 있어야 한다. 그리고 gensim 라이브러리를 사용해주면 굉장히 간단하게 학습된다. 스마트워치 리뷰 데이터를 300차원의 dense vector로 바꿔줄 거고, 한번 학습시 좌우 5개의 단어를 예측해달라. 그리고 코퍼스 내에서 최소 5번 이상은 등장하는 단어들만을 대상으로 하며, sg=1: skipgram 방식을 사용하겠다는 뜻이다.</p>
<pre><code class="language-python">from gensim.models import Word2Vec

model_w = Word2Vec(w_morphs, vector_size=300, window=5, min_count=5, sg=1)</code></pre>
<p>학습 후 model_w라는 개체에 lookup matrix 관련 정보가 담기게 된다.</p>
<h2 id="2-most-similar-words를-후보군에-포함하기">2. Most similar words를 후보군에 포함하기</h2>
<p>앞서 선정한 seed words와 관계가 많은 단어들을 뽑아낼 것이다. 일단 얼마나 학습이 잘 되었는지 most_similar를 뽑아봤는데 어? 생각보다 괜찮다 라는 생각이 먼저 들었던 것 같다. 스피커의 저음 관련된 내용을 뽑을 때, 중저음, 베이스음, 고음 등은 함께 aspect words에 포함되어도 좋겠다. 마찬가지로 립스틱 리뷰에서 색상과 관련해서 색깔, 색, 컬러, 칼라, 컬러감 등을 모두 aspect로 추출해도 문제가 없다. (물론 걸러주는 것은 내가 직접 해야할 거 같다.. 데이터셋을 만들 때에는..)
<img src="https://images.velog.io/images/jonas-jun/post/20a4635b-c02d-40eb-bc1e-c9be211b8bb3/image.png" alt="">
<img src="https://images.velog.io/images/jonas-jun/post/e460d179-7f1e-4aa4-9597-46c426424d49/image.png" alt=""></p>
<p>Seed word들을 loop 돌면서 각각 most similar word를 뽑아내고, 거기서 &#39;NNG&#39;(일반명사) 형태소들만을 pool에 담았다. 그리고 중복된 단어들을 제거해서 최종적인 aspect의 candidates를 선정할 수 있었다.
<img src="https://images.velog.io/images/jonas-jun/post/e405a592-2dc8-4422-85f0-21db81a507aa/image.png" alt=""></p>
<h2 id="3-출력-후-직접-걸러내기">3. 출력 후 직접 걸러내기</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/4d6d3bd2-aaf5-47a1-8cac-e2bf2bf34f4c/image.png" alt="">
위 그림 그대로 수동 작업을 했다. 한줄에 하나씩 단어를 txt로 출력해놓고 적합한지 직접 판단해서 부적절한 단어들을 지워주었다.. 그래도 한번 걸러놓은 단어들이라 양이 그렇게 많진 않았다. 해놓고 나서 든 생각인데 pre-defined 카테고리를 만들어두고 그 카테고리에 속할 수 있는 단어들만 남겨놓자는 마인드로 진행했으면 더 좋았을 것 같다.</p>
<blockquote>
<p>디자인: 색깔, 크기, ...
기능: 운동, 수면, 알람, ...
배송: 택배, 배달, ...</p>
</blockquote>
<p>이렇게 스마트워치에 대한 카테고리를 미리 4~5개 뽑아놓고, 그 안에 속할지를 판단해보면 기준이 좀 더 명확했을 것 같다. 아마 이 부분은 다시 작업을 해야할지도 모르겠다.</p>
<h2 id="결과가-생각보다-나쁘지는-않네">결과가 생각보다 나쁘지는 않네?</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/99466ca7-1992-44f4-9c12-f8427e56ec53/image.png" alt="">
무드가습기 리뷰의 aspect이다. &#39;사이즈라&#39;, &#39;가격도&#39;, &#39;디자인도&#39;, &#39;이가격&#39; 처럼 &#39;사이즈&#39;, &#39;가격&#39; 등에서 변형되어서 토크나이징된 단어들도 aspect로 잡아낼 수 있게 됐다. khaiii를 사용해서 token classification을 할 때는 이런 단어들이 aspect로 labeling되어 있어야 하기 때문에 만족스러운 결과라 볼 수 있었다.
오타나 문법에서 조금 벗어난 표현들도 aspect에 포함할 수 있었다. &#39;다자인&#39;, &#39;싸이즈&#39;, &#39;무등등&#39;, &#39;새척&#39;은 &#39;디자인&#39;, &#39;사이즈&#39;, &#39;무드등&#39;, &#39;세척&#39;을 나타낸다는 걸 알 수 있는데, 이것들도 그대로 aspect에 포함시킬 수 있기 때문에 올바르게 labeling을 할 수 있을 것 같다.</p>
<p>이젠 이 단어들을 포함하는 리뷰들에 직접 B I O 태그와 또는 aspect word의 위치(토큰 번호)를 라벨링 해주는 작업을 하고 출력해보면 되겠다.</p>
<p>아직 데이터 전체를 공개하기가 어려워서 작업한 노트북 파일을 깃허브에 올리진 못했다. 만약 이 라벨링 데이터가 Aspect base로 리뷰를 이해하는데 도움이 되는 것 같으면 추후에 공개해볼 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Research] Word2Vec으로 corpus 속 유사한 의미를 갖는 단어들을 찾아보자-1]]></title>
            <link>https://velog.io/@jonas-jun/Research-W2V1</link>
            <guid>https://velog.io/@jonas-jun/Research-W2V1</guid>
            <pubDate>Mon, 23 Aug 2021 08:34:47 GMT</pubDate>
            <description><![CDATA[<p>앞서 Aspect seed가 될 수 있는 단어들을 추출했다. 단순 빈도 + 특정 상품군에서만 많이 등장하는 단어(Word level TF-IDF) + Common words를 Seed로 선정했다. 여기서는 이 seed word와 유사한 의미를 갖고 있는 단어들을 추출해서 aspect가 될 수 있을지 검토하는 작업을 다뤄보겠다. &quot;음질&quot;, &quot;발색&quot;, &quot;가습&quot;이 seed word라면 그와 유사한 단어들도 aspect word에 포함될 수 있도록 하는 과정이다.</p>
<blockquote>
<p>음질: 음향, 음감, 사운드, ...
발색: 발색력, 착색, 색상, ...
가습: 가습량, 분무량, 분무, 분사량, ...</p>
</blockquote>
<h2 id="word2vec">Word2Vec</h2>
<p>Word2Vec은 말 그대로 Word를 Vector로 바꿔주는 방식이다.
음질이 [0,0,0,1,0,0,0,0,0,0...] 이라는 onehot 벡터를 가진다고 할 때 이는 <strong>1)너무 sparse하여 모델의 크기가 커지는 문제가 발생할 수 있고,</strong> 단순히 onehot으로 나타냈기 때문에 <strong>2)음량 [0,0,0,0,0,0,..어딘가에 1,0,0,0] 벡터와의 관계를 나타낼 수도 없다.</strong>
그래서 v차원(vocab의 크기)이 아닌 m차원의 벡터로 dense하게 바꿔주면서도 다른 단어들과의 관계를 나타낼 수 있도록 학습시킨다.</p>
<h2 id="어떻게">어떻게?</h2>
<p>비슷한 의미를 갖는 단어들은 비슷한 환경에서 등장한다를 전제로 한다. 발색과 발색력은 다른 단어이지만 앞뒤 단어가 유사할 것이다. 그래서 아래 페이퍼의 그림과 같이 주위 단어들을 통해 가운데 단어를 예측(CBOW)한다거나 중간 단어를 활용해 주위 단어들을 예측(SkipGram)하는 방식으로 Learning이 이뤄지게 만든다.
<img src="https://images.velog.io/images/jonas-jun/post/cdc0b1d8-70a9-49db-8414-c5bacd93d2cb/image.png" alt=""></p>
<h3 id="cbow-continuous-bag-of-words">CBOW: Continuous Bag of Words</h3>
<p>CBOW는 주위 단어들로 중심 단어를 예측한다. Onehot 형태로 주위 단어들이 들어가기 때문에 어떤 weight matrix를 거쳐서 dense한 벡터로 바뀌게 된다. 그리고 주위 단어 벡터들의 합을 하나의 representation 벡터로 삼고, 두 번째 weight matrix를 통과시켜서 다시 Vocab 만큼의 크기를 갖는 벡터로 결과를 출력해준다. 여기서 주위 단어를 몇개 반영해서 중간 단어를 예측할지 나타내는 걸 window size라 한다.
첫번째 weight matrix는 v * m 차원이고 두번째 matrix는 m * v 차원이 되어야 한다. 그래야 dense한 벡터로 바꿔주었다가 마지막에 다시 v차원의 벡터를 출력함으로서 최종적으로 단어를 예측할 수 있게 되기 때문이다. 최종 v차원 벡터와 truth onehot 벡터 (중간 단어를 나타내는 one-hot 벡터)로 Cross Entropy Loss를 계산해서 중간 matrix의 weight를 바꿔준다.
<strong>중요한 점은 첫번째 weight matrix가 lookup table이 된다는 점이다.</strong> 아래처럼 onehot 벡터에서 dense vector를 뽑다보면 k번째 단어는 k번째 row로 출력되게 된다. 저 matrix가 사전이 되는 것이고, 우리는 저 사전에서 원하는 단어의 벡터를 뽑을 수 있다.
<img src="https://images.velog.io/images/jonas-jun/post/85144938-6f12-4be7-ad21-c4692c7b7506/image.png" alt=""></p>
<h3 id="skipgram">Skipgram</h3>
<p>Skipgram도 유사한 방식이다. Vocab size의 one-hot vector가 첫 번째 weight matrix를 통과하여 m차원의 dense vector로 바뀌게 된다. 정해진 window size 개수 만큼의 각각의 weight matrix를 통과시켜서 주위 단어들의 각 자리마다 v차원의 vector들을 뽑아내게 된다. 그리고 그 자리들에 실제로 등장하는 단어(truth)를 가지고 학습을 시킬 수 있다.
<strong>Skipgram이 CBOW보다 성능이 좋다고 알려져 있다.</strong> 학습이 더 많이 일어나기 때문이다. 단어 1개를 가져와서 주위 단어 개수만큼 weight 학습이 이뤄지기 때문에 여러 단어가 한번만 학습되는 CBOW보다 학습 횟수가 많다. Semantic Accuracy에서 다른 모델들에 비해 월등히 높은 Accuracy를 보인다.
<img src="https://images.velog.io/images/jonas-jun/post/ebb6e940-9fa1-459e-85b3-ae479dcba538/image.png" alt=""></p>
<h2 id="case-analysis">Case Analysis</h2>
<p>단순 정확도 수치보다는 어떤 예시들이 존재하는지 보면 이 임베딩 모델의 성능을 쉽게 이해할 수 있다. 페이퍼에서는 아래와 같은 방식으로 관계를 분석해보았다.  </p>
<blockquote>
<p>Paris - France + Italy = Rome</p>
</blockquote>
<p>Paris를 나타내는 벡터에서 France를 나타내는 벡터를 빼고 Italy 벡터를 더했더니... Rome이 나온다고? 이외에도 기업과 SW, 사람과 직업 등의 관계를 많이 이해하고 있었다. Corpus가 클수록 optimization 횟수도 늘어나고 주위 단어의 variance도 줄어들어 성능이 좋아질 것이다.</p>
<p><img src="https://images.velog.io/images/jonas-jun/post/1b22a4e0-34ab-42a8-8a65-482f9449dcf0/image.png" alt=""></p>
<p>실제 데이터에 적용했던 후기는 뒷 편에 계속...</p>
<p>Reference
<a href="https://arxiv.org/abs/1301.3781">Efficient Estimation of Word Representations in Vector Space</a> (2013)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Codility] Recursion과 nonlocal을 활용한 magicSquare 풀이]]></title>
            <link>https://velog.io/@jonas-jun/Codility-almostmagic</link>
            <guid>https://velog.io/@jonas-jun/Codility-almostmagic</guid>
            <pubDate>Sat, 21 Aug 2021 13:54:47 GMT</pubDate>
            <description><![CDATA[<p>Codility Almost Magic Square (coder of Rivia challenge)</p>
<h2 id="변수의-범위scope">변수의 범위(scope)</h2>
<p>코딩테스트 문제를 많이 풀어보면서 global이 아닌 nonlocal 변수들을 자주 사용하게 됐다. 변수의 영향 범위(variable scope)에 대해서 익숙해졌다.</p>
<ul>
<li>global: 전역</li>
<li>local: 함수 내부 (지역)</li>
<li>nonlocal: 함수 외부의 값을 가져오면서 그 값이 글로벌은 아닐 경우 (= 바깥 함수 내에서 정의된 변수인 경우)</li>
</ul>
<p>DFS 등을 recurse 방식으로 돌려줄 때 함수 안으로 가져와서 수정은 하지만 바깥 쪽에 계속 유지되어야 하는 경우(ex. visited map)나 바깥 함수에서 정의된 target 값들을 내부 함수에서도 사용해줘야 한다면 nonlocal을 사용하기 좋다.</p>
<p>이 문제는 코딜리티 콘테스트 문제라서 자세히 포스팅 하지는 않지만, (그만큼 좋은 풀이도 아닌 것 같긴 하다) 어디에 답이 포스팅되어 있지 않기 때문에 다른 사람들에게도 조금 도움이 되지 않을까 하여 짧게 남겨둔다.</p>
<h2 id="question">Question</h2>
<p>3*3 사각형과 그 안의 숫자들 [0,2,3,4,1,1,1,3,1]이 주어졌을 때 액션을 취해서 6개 라인(가로 세 줄, 세로 세 줄) 각각의 합을 같게 만들어주고, 그 결과를 출력. 주어진 숫자 list의 인덱스와 그림에서 각 칸 하단에 쓰인 숫자가 같다.
여기서의 액션은 한번에 한 칸의 숫자를 1씩 증가시킨다는 것
<img src="https://images.velog.io/images/jonas-jun/post/97e75ba3-1990-4c2e-80ab-b7363806e1e4/image.png" alt=""></p>
<h2 id="solution">Solution</h2>
<ul>
<li>같아져야 할 라인의 index  [0,1,2], [3,4,5], ..., [2,5,8] -&gt; list1</li>
<li>각 라인들의 합 [5,6,5,...,5]</li>
<li>Target: 각 라인들의 합이 target이 되어야 함. 빼줄 순 없으니 라인 합 중 가장 큰 값으로</li>
<li>Target까지 부족한 값: [1,0,1,...,1] 각 라인을 얼마나 더 채워야 하는지? -&gt; list2</li>
<li>1번 리스트의 인덱스와 2번 리스트의 인덱스가 같기 때문에 매칭이 가능해진다. 2번 리스트에서 루프를 돌면서 두 개를 선정하고 각각 채워야 할 값이 있으면서 동시에 갖고 있는 인덱스가 있다면 해당 공통 인덱스 값을 올려주면서 두 라인을 동시에 채워준다.</li>
<li>위 과정을 recursion으로 반복하다가 list2의 sum이 0가 된다면(더 채워야 할 라인이 없다면) 끝</li>
</ul>
<p>1번 리스트와 2번 리스트를 만들어주고 채워야 할 라인들만 각 리스트에 남겨놓은 후에 아래 함수를 recursion 실행하면 된다.</p>
<pre><code class="language-python">    def helper():
        nonlocal list_idx, list_target # index들의 리스트와 채워야 할 값들의 리스트
        for i in range(len(list_idx)-1):
            for j in range(i+1, len(list_idx)):
                # i, j의 채워야할 값들이 남아있고, 둘의 공통 부분이 있다면
                if list_target[i] and list_target[j] and (set(list_idx[i]) &amp; set(list_idx[j])):
                    add_idx = list(set(list_idx[i]) &amp; set(list_idx[j]))[0] # 추가해야 할 index
                    val = min(list_target[i], list_target[j]) # 최대 몇 까지 추가할 수 있는지
                    A[add_idx] += val
                    list_target[i]-=val
                    list_target[j]-=val
                    if sum(list_target)==0: return # 한번 작업하고 다 됐는지 확인
        helper() # 함수가 위에서 return 되지 않는다면 모두 채워진 게 아니기 때문에 다시 실행</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Codility] Stack을 활용해서 주어진 벽 모양 속의 직사각형 개수 세기]]></title>
            <link>https://velog.io/@jonas-jun/Codility-Wall</link>
            <guid>https://velog.io/@jonas-jun/Codility-Wall</guid>
            <pubDate>Fri, 20 Aug 2021 07:13:23 GMT</pubDate>
            <description><![CDATA[<p>Codiility Stone Wall 코딜리티</p>
<p>O(n)으로 loop를 돌면서 개수를 세는 문제는 흔하지만, 코딜리티의 이 문제가 도형 높이, 채워지는 물의 부피 등을 계산할 때 기본이 될 수 있는 문제라 생각하여 포스팅해둔다. 기본적이면서 매우 좋은 문제라 생각.</p>
<h2 id="question">Question</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/de3238da-a371-4fd3-bb52-a2484dc361e8/image.png" alt="">
H:List가 주어졌을 때 각 element: int 들은 H[i] -&gt; i~i+1 까지 벽의 높이를 뜻한다. 주어진 벽 모양을 직사각형으로 채울 때 가능한 최소의 직사각형 수는?</p>
<h2 id="solution">Solution</h2>
<ul>
<li><p>높이가 같다면? 새롭게 사각형을 추가하지 않아도 되므로 그냥 continue</p>
</li>
<li><p>높이가 증가한다면? stack에 숫자를 추가해둠 (그렇다면 서로 다른 높이들이 들어있게 되겠지)</p>
</li>
<li><p>높이가 감소한다면? 감소한 높이(H[i])로 stack에 들어있는 앞쪽 벽 높이 중 더 높은 벽들을 다 잘라낸다. 생각해보면 H[i]보다 높은 3가지 높이가 들어 있다고 한다면 3가지는 3개의 사각형으로 채울 수밖에 없다. 즉, stack에서 꺼내는 숫자만큼 ans += 1.
그리고 위쪽을 잘라냈다는 것은 잘라낸 부분 만큼은 새로운 높이(H[i])가 가장 높은 높이가 되는 것이므로.. 새로운 높이를 stack에 추가. (이 때 stack[-1]과 같다면 굳이 추가할 필요가 없다)</p>
</li>
<li><p>마지막에 stack에 숫자들이 남아있다면 그 만큼의 각각 다른 높이들이 남아있는 것이기 때문에 그 개수만큼의 사각형으로 채워야 하므로, ans에 stack의 길이만큼 더해준다.</p>
</li>
<li><p>코드 상으로는 stack 처음에 0을 추가해서 예외 없이 일관된 loop가 돌아갈 수 있도록 했다.</p>
</li>
</ul>
<h3 id="그림을-그려서-이해해보면-좋겠다">그림을 그려서 이해해보면 좋겠다.</h3>
<p>재미있게 풀었던 문제라서 그림을 그려보았다. 예시인 [8,8,5,7,9,8,7,4,8]의 경우이다.</p>
<p>높이가 감소할 경우, 낮은 높이로 자르면 윗 부분은 직사각형이 하나 생긴다.
<img src="https://images.velog.io/images/jonas-jun/post/e1210f5a-c7dc-470b-a50d-45b6f0e80605/image.png" alt=""></p>
<p>높이가 증가할 때는 stack에 높이를 추가해둔다. [A,B,C]까지 stack에 들어간다. 그리고 D가 등장하면서 높이는 감소. D높이로 자른다고 생각해보자. 그럼 D보다 높은 곳들은 사각형으로 잘리게 되는데, 여기서는 우선 C만 D보다 높기 때문에 하나의 사각형만 추가되게 된다.(3번 사각형)
그렇다면 이제 안 잘리고 남은 높이는 [A,B,D]로 바뀌게 된다.
<img src="https://images.velog.io/images/jonas-jun/post/eb761d69-26ca-4a40-96ab-9d309ecba799/image.png" alt=""></p>
<p>아래 그림에서 새로운 낮은 높이 C가 등장했다. C보다 높은 높이는 A와 B가 stack에 남아있을 텐데, C보다 높은 높이들을 하나씩 pop() 해주면서 새로운 사각형이 하나씩 생긴다.
그러고 나면 남은 높이(stack)는 C 하나가 되며 D는 증가 부분이기 때문에 규칙대로 stack에 그대로 추가한다. 모든 loop이 돌고 나면 pop() 했던 횟수인 5와 stack에는 [C, D]가 남게 되는 것이다. 둘의 높이가 다르니 추가로 사각형 두 개가 더 필요할 것이다.
다른 방식으로 자를 수도 있겠지만 나는 이렇게 이해하는 편이 편했다.
<img src="https://images.velog.io/images/jonas-jun/post/1d077af6-e264-4304-8968-6e4ec5228ef2/image.png" alt=""></p>
<pre><code class="language-python">def solution(H):
    ans = 0
    stack = [0]
    for val in H:
        if val == stack[-1]:
            continue
        if val &gt; stack[-1]:
            stack.append(val)
        else:
            while stack[-1] &gt; val:
                stack.pop()
                ans += 1
            if stack[-1] != val:
                stack.append(val)
    ans += (len(stack)-1)
    return ans</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Research] 리뷰에서 Aspect word가 될 만한 후보군을 통계적으로 추출해보기]]></title>
            <link>https://velog.io/@jonas-jun/Research-candidates</link>
            <guid>https://velog.io/@jonas-jun/Research-candidates</guid>
            <pubDate>Wed, 18 Aug 2021 13:25:07 GMT</pubDate>
            <description><![CDATA[<p>앞선 단계에서 리뷰에 맞는 전처리 작업들을 했으니, 이제 이 네 가지 상품군(스마트워치, 립스틱, 무드가습기, 블루투스스피커)에서 각각 주요 단어(aspect word)가 될 수 있는 단어들을 통계적으로 선별해본다.</p>
<h2 id="어려운-이유-답이-없다">어려운 이유, 답이 없다..</h2>
<p>이 작업이 어려운 이유는 <strong>정답이 없기 때문</strong>이다.
Aspect word라는 건 단지 많이 등장하는 명사인가? Pre-defined된 카테고리(기능, 내구성, 디자인, 가격, 배송 등)에 속하는 단어들이어야만 하는가? 그렇다면 카테고리는 어떻게 결정하는가? 사실 규칙이 명확하게 있다면 labeling을 하는 건 쉽지 말이다...
<img src="https://images.velog.io/images/jonas-jun/post/c193931a-8872-4029-a3f4-4f706d9d90ea/image.png" alt="">
디자인 이라는 카테고리를 눌러보면 꼭 &#39;디자인&#39;이라는 단어가 들어 있지 않고, 또는 어떤 명사형 aspect word가 포함되어 있지 않더라도 주요 표현으로 주목하고 있다. ex)<em>&#39;깔끔하고 이쁘다&#39;</em>
ABSA의 대표 데이터셋인 SemEval에서는 aspect는 거의 명사형이며 주요 카테고리를 설명할 수 있는 단어가 된다. 그리고 그에 붙은 서술어나 관형어 (&quot;깔끔하다&quot;)를 opinion라 부른다.</p>
<p>여기서는 트레이닝을 위한 세트를 만들어야 하는데 하나하나 검수하면서 라벨링을 할 수가 없기 때문에 &#39;명사형&#39; 이면서 00번 이상 등장한 단어라는 제약 조건을 걸어본다. 조건에 따라 일단 통계적으로 뽑아서 모델을 학습시킬 수 있을 정도의 데이터셋을 구축해보는 게 목표이다. </p>
<p><img src="https://images.velog.io/images/jonas-jun/post/20aa08cd-e953-4b35-9859-ab982194bf6e/image.png" alt=""></p>
<h2 id="선정-과정">선정 과정</h2>
<ol>
<li>Tokenize: 사전 결정한 Khaiii를 활용해서 형태소 tokenize</li>
<li>Counter: 등장횟수를 파악해서 단순하게 많이 등장하는 단어들을 고려</li>
<li>Customized TF-IDF: TF-IDF를 모방한 방식으로 특정 상품군에서 특히 많이 등장하는 단어들을 고려</li>
<li>노동.. 검수: 선별한 단어들의 적합성을 직접 검증해보기</li>
</ol>
<h3 id="1-tokenize">1. Tokenize</h3>
<p>앞서 리뷰 데이터셋 구축에 적합한 토크나이저를 비교해본 적이 있다. <a href="https://velog.io/@jonas-jun/ae-tokenizer">관련 포스팅</a> Khaiii를 활용해서 형태소 단위로 리뷰를 쪼갰다. Mecab()에 비해 3~4배 정도 시간이 많이 소요되었던 것 같다. Khaiii는 .analyze 메소드를 써서 자체 정의한 클래스로 값이 return되기 때문에 아래와 같이 .morphs와 str()을 붙여줘서 하나하나 반환 객체를 만들어줘야 한다.</p>
<pre><code class="language-python">def get_tokenized(reviews, tokenizer=&#39;khaiii&#39;):
    rst = list()

    if tokenizer==&#39;khaiii&#39;:

        def use_khaiii(text):
            api = KhaiiiApi()
            result = list()
            for sent in api.analyze(text):
                result += sent.morphs
            return [str(word) for word in result]

        for review in reviews:
            rst.append(use_khaiii(review))

    return rst</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/6ff7196b-a3f1-41ac-aab7-371c2040830e/image.png" alt="">
이와 같은 형태로 morphs 단위로 자를 수 있다. 시간이 꽤 필요하니 한번 자른 것은 꼭 txt파일로 저장해두자.</p>
<h3 id="2-counter">2. Counter</h3>
<p>웬만한 크기의 데이터에서 크게 차이는 건 아니지만, collections.Counter 보다 직접 loop를 돌면서 count dict를 만드는 게 속도가 빠르다. (수 차례 실험에서 얻어진 결과) 아래와 같은 함수로 <strong>&#39;NNG&#39;(일반명사)의 태그를 갖고 있으면서 길이가 2 이상인 단어들을 Counter에 추가</strong>해주었다. 우리는 명사형의 aspect를 추출할 생각이기 때문이다.</p>
<pre><code class="language-python">def get_word_count(tokenized: List[List]) -&gt; dict:
    vocab = dict()
    for sentence in tokenized:
        for word in sentence:
            if word.split(&#39;/&#39;)[1] in [&#39;NNG&#39;] and len(word.split(&#39;/&#39;)[0]) &gt;= 2: # 길이 2 이상, 일반명사, 고유명사(NNP)는 제거
                if word in vocab: vocab[word] += 1
                else: vocab[word] = 1
    return vocab</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/3ceaabd7-ceb8-4ff4-afcb-4e4d68af03b8/image.png" alt="">
상위 30개씩 뽑아본 결과, &#39;배송&#39;, &#39;선물&#39;, &#39;가격&#39;, &#39;디자인&#39; 등이 공통적으로 등장했다. 나중에 <strong>aspect seed</strong>라 부를만한 단어들을 선정할 때 여기서 적절히 상위권에서 끊어서 반영하게 된다.</p>
<h3 id="3-cumstomized-tf-idf">3. Cumstomized TF-IDF</h3>
<p>TF-IDF (Term Frequency - Inverse Documnet Frequency)는 어떤 쿼리문에 가장 관련도가 높은 문서를 찾아낼 때 유용한 계산 방식이다.
TF(d,t)는 문서 d 안에서 쿼리 단어 t가 등장하는 횟수이고, IDF(d,t)는 단어 t가 등장하는 문서의 수에 반비례하는 수치이다. TF-IDF는 이 둘을 곱해준다. 아래 IDF식에서 df(t)는 단어 t가 등장하는 문서들의 숫자이다. 즉, <strong>해당 문서에서 많이 등장하면서 다른 문서들에서는 적게 등장할 수록 이 스코어가 높다.</strong> 마침 검색엔진을 Haystack으로 구축해봤던 경험을 소개했던 글(<a href="https://blog.naver.com/ys10mjh/222303777938">링크</a>)에서 설명을 해두었으니 참고.
<img src="https://images.velog.io/images/jonas-jun/post/2f15a714-3442-42b3-bae8-bd6220177b9a/image.png" alt=""></p>
<p>아래는 TF-IDF와 비슷한 개념으로 Score를 뽑아내는 BM25(Best matching) 알고리즘. 복잡해보이지만 단어q가 등장하는 문서의 수가 분모에 오고(반비례), 해당 문서 내에서 단어 q가 등장하는 수가 분자에 온다는 점(비례)에서 TF-IDF와 유사한 의미를 갖는다.
<img src="https://images.velog.io/images/jonas-jun/post/1a45a3d3-e088-4aec-8e95-60be2934d68e/_2021-04-02__1.16.49.png" alt=""></p>
<p>본론으로 들어가서, 여기서는 <strong>다른 상품군 리뷰에는 많이 들어있지 않으면서 해당 상품군 리뷰에만 유의미하게 많이 들어있는 단어들을 찾아낼 수 있도록</strong> 위 방식들과 유사한 score metric을 만들어보았다.</p>
<ul>
<li><p>counters: 각 상품군의 counter dict를 포함한 list</p>
</li>
<li><p>target_idx: 우리가 스코어를 뽑고 싶은 상품군이 counters list에서 몇 번째에 들어있는지</p>
</li>
<li><p>threshold: 높은 스코어를 갖는다 해도 리뷰에서 워낙 조금만 등장하면 의미가 없을 수 있기 때문에 이 threshold 이상 등장하는 단어들만 걸러내기 위해 사용</p>
</li>
<li><p>TF는 말 그대로 그 문서 속에서 word A가 등장하는 횟수</p>
</li>
<li><p>IDF는 Document Frequency라는 본래 의미와 달리 여기서는 다른 상품군에서 word A가 평균적으로 등장하는 횟수로 했다.</p>
</li>
</ul>
<p>다시 말해, <strong>해당 상품군 리뷰에는 많이 등장하면서 다른 상품군 리뷰에서는 잘 등장하지 않을 수록 높은 스코어를 갖는 방식</strong>이다. 상품군에 특화된 aspect들을 찾기 위함이다.</p>
<pre><code class="language-python">def get_TFIDF(counters, target_idx, threshold):
    rst = dict()
    c_else = counters[:]
    c_target = c_else.pop(target_idx)

    for word in c_target:
        tf = c_target[word]
        if tf &lt; threshold: continue # 리뷰에서 threshold 이하로 등장한다면 그냥 pass
        # IDF는 해당 단어가 다른 항목 리뷰들에 평균적으로 포함된 수
        cum = 0
        for c in c_else:
            if word in c:
                cum += c[word]
        idf = cum / len(c_else)

        rst[word] = round(tf/(idf+1), 4) # TF를 IDF+1로 나눈 숫자
    return rst</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/727e696b-f5a9-4c39-881c-7929a4dccc82/image.png" alt="">
위에서부터 스마트워치, 립스틱, 무드가습기, 블루투스스피커 순이다. 모든 단어들이 유의미한 것은 아니지만 각각 그 상품군의 리뷰에서 유의미한 aspect를 대표할 수 있는 단어들이 꽤나 많이 포함되었다고 판단했다.</p>
<ul>
<li>워치: 착용감, 심박수, 스트랩 등</li>
<li>립스틱: 발색, 발림, 발색력, 지속력 등</li>
<li>가습기: 가습량, 분무량, 세척, 분사력 등</li>
<li>스피커: 음질, 사운드, 음향, 음량 등</li>
</ul>
<p>다음에는 단순히 많이 등장한 단어들 + 해당 상품군에서만 많이 등장한 단어들 + 공통적으로 포함시키고 싶은 소수의 단어들을 <strong>SEED aspect words</strong>라 이름 짓고 이들을 기반으로 <strong>W2V 형태의 임베딩 벡터 학습</strong>을 통해 aspect 단어가 될만한 후보들을 찾아보는 작업을 진행했다. (다음 포스팅에서)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Research] Khaiii 토크나이저를 활용한 리뷰 데이터셋 구축 - 2. 전처리]]></title>
            <link>https://velog.io/@jonas-jun/Research-%EC%A0%84%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@jonas-jun/Research-%EC%A0%84%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 18 Aug 2021 10:19:14 GMT</pubDate>
            <description><![CDATA[<p>Aspect를 추출하기 위한 작업 전에 리뷰 데이터 특성을 반영하여 일부 전처리를 진행했다.</p>
<h2 id="고려사항">고려사항</h2>
<ol>
<li>&#39;BEST&#39;로 시작하는 경우(&#39;BEST리뷰&#39;는 앞에 &#39;BEST&#39;가 붙어서 크롤링되었다) -&gt; review = review[4:] (&#39;BEST&#39;를 잘라주었다)</li>
<li>한글이 아예 없는 경우: drop. 한글이 아예 없이 영어나 이모티콘, 특수문자 등으로만 이뤄진 리뷰는 제거하였다.</li>
<li>White space가 하나도 없는 경우: drop.
ex) &#39;10자10자10자&#39;</li>
<li><strong>눈에 띄는 뻔한 오타는 그대로 두기로 했다.</strong> 리뷰 특성상 그런 오타들이 많기 때문에, 종합적인 PLM을 구축하는 것이 아닌 리뷰에 맞는 모델을 만들 때는 그대로 학습시키는 편이 좋다고 생각했다.
ex) 다자인-&gt;디자인</li>
</ol>
<p>3번의 예시. 띄어쓰기가 하나도 없는 경우 아래의 일부와 같이 너무 성의 없이 작성된 리뷰가 자주 보였다. <strong>띄어쓰기를 한번이라도 한 리뷰와 그렇지 않은 리뷰의 차이가 큰 편이라 생각했다.</strong> 아쉽지만 리뷰는 계속 스크래핑이 가능하니 기계적으로 모두 제거했다.
<img src="https://images.velog.io/images/jonas-jun/post/fc82c026-e41e-4868-b1ad-f18f0a97887a/image.png" alt=""></p>
<pre><code class="language-python">def preprocess(dic):
    new_dict = defaultdict(lambda: list())
    not_KOR = 0
    not_space = 0

    for i in range(len(dic[&#39;ratings&#39;])):
        review = dic[&#39;reviews&#39;][i]
        if not re.search(&#39;[가-힣]&#39;, review):
            not_KOR += 1
            continue # 한글이 없을 경우 제거
        if len(review.split())==1:
            not_space += 1
            continue # 띄어쓰기가 하나도 없을 경우 제거
        if review.startswith(&#39;BEST&#39;): review=review[4:] # BEST로 시작할 경우 BEST를 제외
        new_dict[&#39;reviews&#39;].append(review)
        new_dict[&#39;ratings&#39;].append(dic[&#39;ratings&#39;][i])
        new_dict[&#39;rebuys&#39;].append(dic[&#39;rebuys&#39;][i])
        new_dict[&#39;months&#39;].append(dic[&#39;months&#39;][i])

    print(&#39;한글이 없는 경우 {:,}개, 공백이 없는 경우 {:,}개 제거했습니다&#39;.format(not_KOR, not_space))
    return new_dict</code></pre>
<p>별점, 재구매, 한달사용기 여부를 당장은 필요하지 않을 수 있지만, 혹시 감성분석 용도로 데이터를 사용하거나 원래 리뷰와 재구매 리뷰의 차이 등에 대한 연구도 나중에 진행될 수 있으니 남겨두기로 했다.</p>
<p>전처리를 하며 염두에 두어야 할 점은, 단순히 리뷰만 남기거나 드롭하는 게 아니라 idx에 맞춰서 별점, 재구매, 한달사용 데이터셋에서도 같이 제거해줘야 한다는 것.</p>
<p><img src="https://images.velog.io/images/jonas-jun/post/95f16849-74d4-47ea-9c58-f98f36f8e5ea/image.png" alt=""></p>
<p>결과적으로 총 20만 개 중 약 1만 개 정도의 리뷰가 제거되었다. 다음에는 aspect가 될 만한 후보 단어들을 통계적으로 추출했던 과정을 포스팅할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[boj3944] int(digits, n). n진법 수 변환하기]]></title>
            <link>https://velog.io/@jonas-jun/boj3944-n%EC%A7%84%EB%B2%95%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@jonas-jun/boj3944-n%EC%A7%84%EB%B2%95%EB%B3%80%ED%99%98</guid>
            <pubDate>Wed, 18 Aug 2021 08:20:46 GMT</pubDate>
            <description><![CDATA[<p>boj3944 나머지 계산 백준 3944</p>
<h2 id="question">Question</h2>
<p>예시) 7진법 123456이 주어졌을 때 이 수를 6으로 나눴을 때의 나머지를 출력한다.
<img src="https://images.velog.io/images/jonas-jun/post/0fa4b3b7-059d-459d-bc01-6324d99f0e82/image.png" alt=""></p>
<h2 id="solution">Solution</h2>
<h3 id="solution-1-n진법의-원리">Solution 1: n진법의 원리</h3>
<p>10진법 782는 100*7 + 10*8 + 1*2 로 쓸 수 있다. 이는 다시 (99*7 + 1*7) + (9*8 + 1*8) + (1*2)로 나눠서 표현할 수 있다. n-1인 9로 나눈다고 생각해보면 각 자리수에서 1*k만 남게 되는 것을 알 수 있다. 즉, 단순하게 모든 자리의 숫자를 더해준 값을 다시 n-1로 나눠서 나오는 나머지가 전체 수의 나머지와 같다는 것이다.
각 자리수의 값들을 더해줄 때 loop을 돌거나 sum() 함수를 이용할 수 있는데, loop을 돌았더니 시간 초과가 나오더라.. sum() 또한 O(n)에 가깝게 구현이 될 텐데 <strong>내장함수가 직접 구현하는 것보다 빠른 경우가 많은 듯하다.</strong> 둘의 구동 시간 비교는 글 아래 결과에 있다.</p>
<pre><code class="language-python">def sol_1(n:int, digits: str) -&gt; int:
    cum = 0
    for i in digits:
        cum += int(i)
    return cum % (n-1)

def sol_2(n:int, digits: str) -&gt; int:
    return sum(map(int, list(digits))) % (n-1)</code></pre>
<h3 id="solution-2-int-활용">Solution 2: int() 활용</h3>
<p>int() 함수는 주로 아래처럼 str type의 digit를 int type으로 변경할 때 사용한다. 단지 타입 변환 느낌으로만 알고 있었다. 그런데 아래 이미지처럼 n진법의 수를 인식할 때에도 int()가 유용하다. &#39;1101011&#39;이라는 2진법 형태의 str을 10진법 int 형태로 변환했을 때 107이 나온다는 뜻이다.</p>
<pre><code class="language-python">int(&#39;35&#39;)</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/91795228-7bdd-4ef1-9455-db89bc85fd0f/image.png" alt="">
그렇다면 아래와 같이 간단하게 n진법의 수를 int() (10진법)으로 읽은 후 (n-1)로 나눠준 나머지를 출력하면 끝.</p>
<pre><code class="language-python">def sol_3(n:int, digits: str) -&gt; int:
    return int(digits, n) % (n-1)</code></pre>
<h3 id="결과">결과</h3>
<p>아래의 시간초과는 Solution1 방식 중 직접 loop을 돌면서 sum 값을 얻어내는 방식으로 풀었을 때, 위의 둘은 Solution2의 방식으로 풀었을 때이다. IDE 상에서 실행했을 때에는 <strong>Solution2가 가장 빠른데, 백준에서는 계속 시간초과가 나왔던 점이 의아했다.</strong> 다른 문제가 있는 건가?
<img src="https://images.velog.io/images/jonas-jun/post/b787e548-2649-487c-ae26-b5e12e6e0178/bj_3944_result.jpg" alt="">
이미지처럼 세 번째(solution 2)가 항상 가장 빨랐다.
<img src="https://images.velog.io/images/jonas-jun/post/f61a0a1d-c8ac-48b7-b1e2-58b5f7218a16/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Research] Khaiii 토크나이저를 활용한 리뷰 데이터셋 구축 - 1. Pipeline 계획]]></title>
            <link>https://velog.io/@jonas-jun/Research-dataset</link>
            <guid>https://velog.io/@jonas-jun/Research-dataset</guid>
            <pubDate>Tue, 17 Aug 2021 15:47:32 GMT</pubDate>
            <description><![CDATA[<p>앞서 <a href="https://velog.io/@jonas-jun/ae-tokenizer">토크나이저 비교 포스팅</a>에서 카카오의 Khaiii를 활용하여 Aspect Extraction task를 위한 데이터셋 라벨링 작업을 해보기로 결정했다. 여기엔 어떤 단계를 밟아가면서 이 작업을 수행할지 계획을 메모해두려고 한다.
여기서는 대략적인 틀에 대해서 소개하고, 진행하고 있는 내용을 짧게짧게 다음 시리즈로 공유할 생각이다.</p>
<h2 id="리뷰-전처리">리뷰 전처리</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/afe2a0cc-c8ab-48c6-9de3-00eec63591de/image.png" alt="">
리뷰는 정말 자유분방하다. 띄어쓰기가 일관되게 지켜지거나 안 지켜지는 것도 아니고, 오타도 많다. 심지어 아무 내용이 없이 이모티콘만 남겨져 있는 경우도 있다.
스크래퍼로 리뷰를 긁어오는 과정에선 &#39;한달사용기&#39;, &#39;재구매&#39;를 안내하는 문구만 지웠다. 리뷰 데이터를 탐색한 결과 추가로 처리해줘야할 내용들이 있어서 적용해본다.
<img src="https://images.velog.io/images/jonas-jun/post/6a66a7ba-43a0-4b79-a039-2052ceebef7f/image.png" alt=""></p>
<h2 id="aspect-후보군-선정">Aspect 후보군 선정</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/a4a9466f-6705-40a5-9b8a-8e6663a8021a/image.png" alt="">
카카오의 Khaiii 토크나이저를 활용해 아래처럼 형태소 단위로 자른다. 그리고 하나의 상품군 내에서 형태소의 빈도를 표현하는 counter를 만들고, word-level에서의 유사 TF-IDF score를 커스터마이징하여 스코어를 뽑아낸다. 그리고 이 둘을 함께 활용해서 aspect 단어가 될 만한 후보군들을 추출해본다.
<img src="https://images.velog.io/images/jonas-jun/post/28f0d14e-272c-4250-8c25-c38e090fe0e2/image.png" alt=""></p>
<h2 id="aspect의-결정">Aspect의 결정</h2>
<p>이 부분은 AI 모델이 해야 하지만, 학습을 위한 데이터셋을 구축할 때에는 수작업이 들어가야한다. 한국어로 라벨링된 데이터셋이 없고, aspect라는 게 어떤 객관적인 기준이 있는 게 아니라 단지 상품을 잘 표현해줄 수 있는 단어들이기 때문에 어떤 룰(Rule) 만으로 완전하게 판단할 수 없다.
<img src="https://images.velog.io/images/jonas-jun/post/0bcb6e0e-11fd-4639-8b49-b55e4debf085/image.png" alt="">
여기서는 앞서 선정한 aspect 후보군을 seed로 삼고, 상품군A에 대한 모든 리뷰들을 대상으로 Word2Vec(skipgram) 방식으로 embedding 벡터들을 학습하여, 유사한 단어들을 추려낸다.
<strong>Pre-defined된 카테고리와 그에 해당하는 문장 내 주요 단어(aspect word) 관계처럼 seed와 관계된 단어들을 일단 다 뽑아보는 것이다.</strong> 그리고 파일로 뽑아내어 직접 부적절한 단어들은 걸러낸다..(걸러냈다)</p>
<h2 id="labeling">Labeling</h2>
<p>AE나 AOPE(Aspect-Opinion Pair Extraction)을 위한 데이터셋은 흔치 않다.</p>
<ul>
<li>SemEval에서의 ABSA 데이터셋은 리뷰, aspect 카테고리, aspect word, aspect에 대한 감성, 그리고 aspect word의 위치(index)가 포함되어 있다. 레스토랑 데이터셋 2,500여 건에 대해서만 이렇게 잘 갖춰져 있고 랩탑 데이터셋는 aspect word에 대한 레이블이 따로 존재하지 않는다.<blockquote>
<p>I’ve asked a cart attendant for a lotus leaf wrapped rice and she replied back rice and just walked away. → {SERVICE#GENERAL, “cart attendant”, negative, from=&quot;12&quot; to=&quot;26&quot;}</p>
</blockquote>
</li>
<li>AOPE 태스크는 중국 연구자들(알리바바나 칭화대 등)에 의해 많이 이뤄지고 있다. TOWE(Target-wise Opinion Words Extraction) <a href="https://aclanthology.org/N19-1259/">paper link</a> 연구자는 이 태스크를 위해 아래와 같은 형태로 태깅된 데이터셋을 직접 구축하고 공개했다. Aspect 뿐 아니라 Opinion까지 구축된 리뷰 데이터셋은 이게 거의 유일하다고 봐도 무방하다.<blockquote>
<p>sentence: Easy to start up and does not overheat as much as other laptops.
aspect: Easy\O to\O start\B up\I and\O does\O not\O overheat\O as\O much\O as\O other\O laptops\O .\O
opinion: Easy\B to\O start\O up\O and\O does\O not\O overheat\O as\O much\O as\O other\O laptops\O .\O</p>
</blockquote>
</li>
</ul>
<p>여기서는 한글 리뷰로 아래 TOWE 형태의 데이터셋을 구축해보려고 한다. 이후 Token level classification으로 접근해야 하기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Research] Review에서 주요 단어를 추출하기 가장 좋은 tokenizer는 무엇일까]]></title>
            <link>https://velog.io/@jonas-jun/ae-tokenizer</link>
            <guid>https://velog.io/@jonas-jun/ae-tokenizer</guid>
            <pubDate>Mon, 16 Aug 2021 14:54:30 GMT</pubDate>
            <description><![CDATA[<h2 id="ae에서-tokenizer의-중요성">AE에서 Tokenizer의 중요성</h2>
<p>Aspect Extraction은 보통 token classification으로 진행된다. 문장 내에서 핵심이 되는 단어(aspect)가 시작되는 지점과 끝나는 지점을 B,I,O tagging 형태(begin, in, out)로 찾아낸다. Bert와 같은 PLM 모델에서 token들의 마지막 states들을 공통된 weight를 갖는 레이어에 통과시켜서 각각 classification을 하게 만든다. 아래는 BERT를 활용해 POS tagging과 같은 token classification task를 진행하는 방식.
<img src="https://images.velog.io/images/jonas-jun/post/528871c1-4999-4fa2-b261-3a95ab556c85/image.png" alt="">
<a href="https://d2l.ai/chapter_natural-language-processing-applications/finetuning-bert.html">이미지 출처</a>
따라서 transformers를 활용하여 AE task를 수행할 때도 aspect 단어들을 얼마나 잘 포함할 수 있도록 tokenize 할 수 있는지가 중요하다. 게다가 PLM의 성능을 활용하기 위해서는 input text가 동일한 tokenizer로 잘려야 하기 때문에 이왕이면 사전 학습 때부터 활용된 tokenizer를 그대로 사용할 수 있다면 최상이다.</p>
<h2 id="리뷰-데이터에-잘-어울리는-토크나이저는">리뷰 데이터에 잘 어울리는 토크나이저는?</h2>
<p>결론부터 말하자면 카카오의 khaiii 토크나이저가 가장 활용해 볼만했다. 앞서 직접 데이터를 수집했다는 포스팅을 올린 적이 있는데 (<a href="https://velog.io/@jonas-jun/Research-%EC%97%B0%EA%B5%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0">데이터 수집에 관한 포스팅</a>) 그 데이터들을 가지고 aspect words를 추출해내면서 여러 토크나이저들을 테스트해보았다.</p>
<ul>
<li>mecab</li>
<li>KoBERT (wordpiece) - SKT</li>
<li>KorBERT (morphs) - ETRI</li>
<li>khaiii - KAKAO</li>
</ul>
<p>이게 먼저 정해져야 다시 aspect를 추출하는 작업 (이 부분은 정리해서 포스팅해볼 예정)을 진행해보고, 새로 모델을 학습시키든지 기존 한국어 BERT를 활용하면서도 라벨링을 잘 해줘서 fine-tune이나 post-training 정도로 진행할지 결정할 수 있다.</p>
<h3 id="테스트-방식">테스트 방식</h3>
<p>사실 &#39;실험&#39; 이라고 이름 붙이기가 어려웠다. 어떤 통계적인 방법으로 비교해보기가 어려웠기 때문이다. (아마 aspect를 나름대로 추려낸 다음, 여러 토크나이저로 문장들을 잘랐을 때 그 aspect들을 온전히 포함하고 있는 비율로 비교하면 괜찮을 것 같긴 하다) 일단은 눈으로 비교해본 후, best tokenizer를 활용해 다시 한번 통계적으로 aspect를 추출해보는 작업을 할 생각이다. (기존에 단순 빈도와 TF-IDF 유사 방식으로 aspect를 뽑았을 때에는 속도를 위해 mecab을 사용했다)</p>
<ul>
<li>mecab은 Konlpy.tag으로 사용할 수 있다.</li>
<li>KoBERT는 &#39;monologg/kobert&#39;에 공개된 버전을 사용했다.</li>
<li>KorBERT는 ETRI 페이지에서 access key를 받아 API를 호출하여 사용했다.</li>
<li>Khaiii는 <a href="https://github.com/kakao/khaiii?fbclid=IwAR1nQziunRmg1NM9UrKMHOTuAjDxhgpNdQlNY-w6VFzyzx_jv_AUgwCiQKU">카카오의 github</a>을 clone 하여 설치 가능하다.</li>
</ul>
<h3 id="korbertbest-kobertbad-khaiiiusable">KorBERT(best), KoBERT(bad), Khaiii(usable)</h3>
<p>결론은 제목과 같다. KorBERT의 형태소 분석기가 BEST로 보였으며, 성능 좋은 wordpiece 방식을 사용한 KoBERT는 리뷰 AE task에서는 사용하기가 어려울 것 같다. 다만, KorBERT는 모델은 서약서를 쓰고 사용할 수 있지만, 모델에 넣기 위한 형태소 토크나이저는 API 형태로 제공되며 일 5,000건 밖에 되지 않아서 연구에 사용하기가 쉽지 않을 것 같다.</p>
<p>아래 몇가지 사례를 담아본다.  </p>
<p><strong>1. 배, 송...</strong></p>
<blockquote>
<p>남치니 생일로 준비했는데 너무너무너무 만족하네요 딱 배송도 잘 맞춰서 왔구요 소독기도 와서 너무 좋아요^^ 가격도 다른곳 보다 저렴하게 잘산거같아요 매장은 혜택이 아무것도 없드라고요 ㅠㅠ 판매자님 감사합니다^^</p>
</blockquote>
<p>위 리뷰에는 &#39;배송&#39;, &#39;가격&#39;, &#39;소독기&#39; 라는 aspect word가 들어있다. mecab으로 자른 결과를 활용해서 aspect를 추출했기 때문에 mecab은 모든 단어를 포함한다.</p>
<p><strong>Wordpiece 방식을 사용하는 KoBERT는 배송을 하나의 단어로 인식하지 못하고 있다.</strong> 이는 정답 labeling을 매우 어렵기 만들며, 모델에 들어갈 때 각각 다른 토큰으로 들어가야 하기 때문에 설령 BIO tagging으로 B와 I가 잘 나온다고 하더라도 모델이 그 단어를 온전히 aspect 단어라고 받아들일지도 의문이다. 리뷰 전체의 감성분석(sequence classification)에서는 매우 좋은 성능을 보이는 KoBERT이지만 토큰 단위 classification으로 활용하기가 쉽지 않아 보인다. KorBERT와 Khaiii는 무난하게 활용 가능해 보인다.
<img src="https://images.velog.io/images/jonas-jun/post/e3d6a5ca-d00b-4108-b668-3bcc97ed85d8/image.png" alt=""></p>
<p>*<em>2. 심박수 *</em></p>
<blockquote>
<p>애플스토어에서 나이키 44mm 사려고 약 한달넘게 계속 방문한 끝에 구매 했습니다. 이마트, 매장 방문해서 구매하려고 노력많이했으나, 드디어 애플 스토어에서 구해서 구매하게되었습니다. 역시 애플워치 너무 쓰기 편하고, 운동하기에 딱 좋습니다. 나이키 버전을 기다린 보람이 있습니다. 운동하기에 적합하고 심박수 체크 도움이 많이 됩니다. 핸드폰 무음일때 워치가 진동으로 알려주고 가격대비 너무 좋다고 생각합니다. 가성비에도 좋고 핸드폰 se 쓰고 있는데 워치도 se 셋트산 느낌입니다. 너무 잘 사용하고 있고, 판매자 님께 감사합니다. 안전하게 잘 받았습니다. 감사합니다.</p>
</blockquote>
<p>위 리뷰는 가격, 운동, 심박수라는 aspect를 갖는다. khaiii가 mecab과 유사하게 심박수를 하나의 단어로 인식하고 있다. 또한 &#39;가성비&#39;와 같은 단어도 좋은 aspect 후보가 될 수 있는데 (이 부분은 직접 하나씩 추가해줘야 할 듯하다) 잘 묶어서 토크나이징하고 있음을 알 수 있다.</p>
<p><img src="https://images.velog.io/images/jonas-jun/post/160e93d3-1498-423d-98b1-e96c51d89da5/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/jonas-jun/post/6eee71fb-9d38-49ff-b343-3b8a78b87892/image.png" alt=""></p>
<p><strong>3. KorBERT가 가장 적합한데...</strong></p>
<blockquote>
<p>배송 빠르고 좋아요. 2개 구입했어요.</p>
</blockquote>
<p>위와 같은 리뷰는 가장 흔한 리뷰이다. 배송이라는 단어를 mecab은 정확하게 인식하고 있기 때문에 aspect로 선정이 되었는데 khaiii는 제대로 자르지 못하고 있다. KorBERT는 항상 평균 이상의 적절한 토크나이징을 보인다. 역시 이걸 활용한 사전 학습 모델도 있고 가장 적합한 토크나이저로 보이는데... </p>
<p><img src="https://images.velog.io/images/jonas-jun/post/844f50ee-fb5c-422b-b82f-b370cafb8b64/image.png" alt=""></p>
<p>약 100여 개의 리뷰들을 찍어본 결과, 우선 리뷰는 참 분석이 어려워 보였다. 온라인 구어체이고 모바일로 많이 남기는 만큼 띄어쓰기나 오타가 많은 편이기 때문이다.</p>
<p>KorBERT의 토크나이저가 그나마 리뷰에서 핵심 aspect를 추출하기에 가장 좋아보였다. 하지만 API 사용 문제가 있어서 Khaiii를 사용해볼 생각이다. <strong>Khaiii로 토크나이징과 customized TF-IDF를 수행해서 통계적으로 다시 aspect를 추출해볼 계획이다.</strong> Mecab에 비해 약 3배 정도의 시간이 소요된다고 하는데, 최종적으로 khaiii의 성능을 확인해보고 어떤 토크나이저를 사용할지 확정을 해야겠다. ETRI가 공개한 결과에 따르면 <strong>한국어의 경우 형태소 기반의 모델의 성능이 좋다고 하니 khaiii를 사용해서 형태소+품사 기반(단어/품사)으로 학습을 진행해보는 건 어떨지도 고려해봐야겠다.</strong>
<img src="https://images.velog.io/images/jonas-jun/post/16c5b42f-9a8c-4b24-9807-758d36d653a2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AiR_Predictor] 서울시 초미세먼지 예측 모델을 사용 가능한 프로그램으로 배포]]></title>
            <link>https://velog.io/@jonas-jun/AiRPredictorforUse</link>
            <guid>https://velog.io/@jonas-jun/AiRPredictorforUse</guid>
            <pubDate>Sun, 15 Aug 2021 13:14:49 GMT</pubDate>
            <description><![CDATA[<p>이 글은 지난 6월 진행했던 초미세먼지 예측 모델(<a href="https://velog.io/@jonas-jun/SeoulAQI-3">서울시 초미세먼지 예측 모델링 포스팅</a>)을 실제 사용 가능한 프로그램으로 다듬어서 배포한 과정을 담고 있다. 모델은 단순하지만 머신러닝 모델의 저장과 로드(joblib), Standardization 값들로 학습된 모델에 넣기 위해 기존 학습데이터들의 분포도를 활용하여 새 데이터를 정규분포 상에 위치시키는 것 등을 학습해볼 수 있었다.</p>
<h2 id="모델링-결과와-사용할-모델-선택">모델링 결과와 사용할 모델 선택</h2>
<p><img src="https://images.velog.io/images/jonas-jun/post/1c225b50-1980-4631-ac76-93b99713d154/image.png" alt="">
LSTM encoder(+attention)와 Random Forest로 실험한 결과 프로젝트에서 목표했던 Bad Recall 결과가 가장 좋았던 때는 Random Forest를 사용하면서 데이터 샘플링을 할 때 나쁨과 매우 나쁨 단계의 데이터 추출 비중을 높여줬을 때였다. 프로그램으로 만들어서 배포할 모델을 Random Forest (class weight [1,1,3,5]로 결정했다.
Bad Recall과 모델의 전체적인 목표에 대한 포스팅 -&gt; <a href="https://velog.io/@jonas-jun/seoulAQI-1">Link</a></p>
<h2 id="새로-들어온-데이터를-input-data로-만들어보기">새로 들어온 데이터를 input data로 만들어보기</h2>
<p>이제 새로운 데이터를 모델에 잘 들어갈 수 있도록 실험 때와 같은 모습으로 만들어줘야 한다. 아래의 과정들을 진행했다.</p>
<h3 id="데이터-입력">데이터 입력</h3>
<p>우선 데이터를 어떻게 입력 받을지 고민해야 한다. 주피터 환경에서 데이터를 다룰 때는 pandas로 읽어서 dataframe 형태에서 처리했지만, 실제 서비스가 돌아갈 때는 txt 파일로 입력 받거나 실행 시에 하나의 샘플을 입력 받는 편이 빠르다.
<img src="https://images.velog.io/images/jonas-jun/post/3115a208-4e49-4978-98c3-0dc13d53ed64/image.png" alt="">
<img src="https://images.velog.io/images/jonas-jun/post/4f1d250d-f462-40c1-9060-51250eb776d0/image.png" alt="">
위 벡터와 같은 형태로 분석에 필요한 feature들을 입력 받아야 한다. txt 파일에 comma seperated 형태로 입력해 달라고 하면 사용자에게 과한 부탁일 듯하다. <strong>파일로 입력하는 건 그렇다 쳐도 강수(precipitation)이나 구름의 정도(cloud) 등을 직접 찾아보지 못하는 상황이 많은데, 그런 결측값들을 &#39;X&#39;나 False로 정확히 입력해달라고 하는 건 사용자를 어렵게 만드는 방식인 듯했다.</strong>
그래서 argument parse 형태로 파일 실행시에 각 값들을 입력 받기로 했다. default를 자동으로 설정할 수 있기 때문에 입력하지 않은 값들이 생길 때에도 처리하기가 편하다.
여기서는 default를 False로, 그리고 이후에 데이터 전처리 과정에서 과거 10년(2008-2018) 간의 평균치로 그 False를 대체하도록 했다.
<img src="https://images.velog.io/images/jonas-jun/post/68009339-62dc-4051-b227-1e465d86df78/image.png" alt=""></p>
<h3 id="데이터-전처리">데이터 전처리</h3>
<p>입력 받은 데이터를 모델에 들어갈 수 있는 형태로 바꿔준다.</p>
<h4 id="학습-데이터의-분포-정보-로드">학습 데이터의 분포 정보 로드</h4>
<p>결측값을 처리하기 위해 학습 데이터의 평균 값이 필요하다. 또한 각 feature가 10년 간의 학습데이터에서 갖는 분포(mean, std)를 알아야 한다. Standardization을 거친 값들로 모델을 학습시켰기 때문에, 새 데이터가 그 분포 안에서 어느 위치에 있는지 파악해야하기 때문이다.
<img src="https://images.velog.io/images/jonas-jun/post/1a6aaa7f-6ff9-4aa5-b59c-d9563cd3a693/image.png" alt="">
여기서는 non-categorical 피쳐들에 대해서 학습 데이터에서 갖는 평균과 표준편차를 위와 같은 형태로 가져왔다. 각 value에서 0번이 평균, 1번이 표준편차이다.</p>
<h4 id="결측값을-평균치로-채우기">결측값을 평균치로 채우기</h4>
<p>False를 갖는 피쳐는 value의 [0]번 데이터를 가져와서 그 값을 대체할 수 있도록 했다.</p>
<pre><code class="language-python">def fill_false(sample, dist):

    keys = list(dist.keys())
    for i in range(len(keys)):
        if not sample[i]:
            sample[i] = dist[keys[i]][0] # 평균치로 채우기
    return sample</code></pre>
<h4 id="standardization">Standardization</h4>
<p><img src="https://images.velog.io/images/jonas-jun/post/6a2fa156-8cbd-4ff1-826c-6704c29f7082/image.png" alt="">
앞서 학습 때에도 Standarization 방식으로 feature들을 re-scaling 했기 때문에 새로운 샘플에 대해서도 똑같이 진행한다. 지난 10년 간의 데이터로 그려진 분포에서 새 값들이 어느 수준에 위치하는지를 파악해서 input vector에 넣는 것이다. 처음부터 Min-Max scaler를 쓰지 않은 이유가 여기에 있다. 새 값들이 기존 min-max 범위 안에 들어있지 않을 경우도 많이 발생할테니.</p>
<pre><code class="language-python">standard = [0,1,2,7] # standardization idx: NO2, CO, SO2, pressure
    for i in standard:
        sample[i] = (sample[i] - dist[keys[i]][0]) / dist[keys[i]][1]</code></pre>
<p>학습 때 standardization을 진행했던 NO2, CO, SO2, pressure에 대해서 위와 같은 작업을 진행했다. new value에서 평균을 빼고 표준편차로 나눠주면 standization된 분포에서 갖는 위치값을 얻을 수 있다.</p>
<h4 id="encoding-categorical-features">Encoding (Categorical) features</h4>
<p>학습 때 진행했던 피쳐 전처리 과정을 똑같이 거친다. Target인 PM25 카테고리를 제외하고 총 12개의 피쳐들 중 풍향과 전체적인 날씨는 카테고리 데이터이기 때문에 그룹화 후 숫자로 인코딩이 필요했고, 풍속은 한국에서 주로 사용하는 mps(meters per second) 값을 받아서 mph(miles per hour)로 변환해준다. 또한, 학습 때와 마찬가지로 wind_speed와 gust 값들은 평균에서 너무 많이 벗어나는 데이터들을 줄여주기 위해 극한의 아웃라이어들을 사전 정의한 최대치로 내려주는 작업을 거치도록 했다. 풍속은 넉넉 잡아 20, 돌풍은 30으로 최대치를 잡았다. 10년 간의 학습 데이터 중 약 3% 정도가 그에 해당했다.
<img src="https://images.velog.io/images/jonas-jun/post/bd3aa14b-607a-493c-a1a0-fa5b8a06f6ac/image.png" alt="">
<img src="https://images.velog.io/images/jonas-jun/post/654aba31-90d4-4d74-99dc-0555a017375e/image.png" alt="">
이 같은 <strong>전처리 작업을 해줄 수 있는 함수들은 Process_Features.py에 들어 있으며 main.py에서 import해서 가져다 쓰도록 했다.</strong>
<img src="https://images.velog.io/images/jonas-jun/post/f65cf97b-f985-4419-b2f7-0628b2c965d3/image.png" alt=""></p>
<h2 id="load-model">Load Model</h2>
<p>torch를 통해 딥러닝 모델의 weight를 저장하고 다시 가져다 쓰는 건 기본이지만, Scikit Learn으로 구현한 ML 모델을 저장해서 활용해 본적은 이번이 처음인 듯하다. <strong>sklearn의 모델들은 joblib이라는 라이브러리로 쉽게 모델을 파일로 출력해서 저장해뒀다가 쓸 수 있다.</strong> 이 프레딕터에서 사용할 모델 파일은 AiR_Predictor_RF.pkl에 저장되어있다. <a href="https://joblib.readthedocs.io/en/latest/index.html">Document</a> </p>
<pre><code class="language-python">model = joblib.load(args.model)</code></pre>
<p>args.model에는 모델파일(.pkl) 경로를 넣어준다. <strong>모델을 불러온 후 추가로 이어서 더 학습시킬 수도 있다고 하니 머신러닝 모델링을 할 때에는 joblib을 유용하게 사용할 때가 있을 듯하다.</strong>
<em>joblib은 원래 sklearn.externals에서 import 했지만 최신 버전으로 바뀌면서 곧바로 import joblib 으로 사용하게 됐다.</em></p>
<h2 id="classification">Classification</h2>
<p>딥러닝 모델에서 단 한 개의 데이터라도 batch에 넣어 모델에 입력하는 것처럼 여기서도 X=[f1, f2, f3, ...]가 아닌 [X]= [[f1, f2, f3, ...]] 형태로 input vector를 만들어준다. 그리고 간단하게 model로 .predict 해주면 결과가 도출된다.
<img src="https://images.velog.io/images/jonas-jun/post/56e317f1-ef41-4f95-9e22-2a41c75cd9e4/image.png" alt=""></p>
<h2 id="결과-출력">결과 출력</h2>
<pre><code class="language-python">class_map = {0: &#39;좋음&#39;, 1: &#39;보통&#39;, 2: &#39;나쁨&#39;, 3: &#39;매우 나쁨&#39;}</code></pre>
<p>사용자에게 필요한 것은 0, 1, 2, 3이라는 class(미세먼지 class)가 아니라 그 classification 값들이 갖는 의미이다. 따라서 class_map을 거쳐서 자연어로 정보를 출력하도록 했다.</p>
<pre><code class="language-python">    print(&#39;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&#39;)
    print(&#39;3시간 뒤 초미세먼지(PM2.5) 농도는 {} 단계로 예상됩니다&#39;.format(class_map[y]))
    if y &gt;= 2:
        print(&#39;초미세먼지용 마스크를 준비해서 외출하세요!&#39;)
    else:
        print(&#39;초미세먼지용 마스크까지는 필요하지 않습니다&#39;)

    return</code></pre>
<p><img src="https://images.velog.io/images/jonas-jun/post/0effe9cd-2e9d-4e56-8f9e-d9e1af3a2b05/image.png" alt=""></p>
<h2 id="개선-예정-사항-to-do">개선 예정 사항 (To Do)</h2>
<ul>
<li>3시간 전이 아닌 12시간, 24시간 이후의 PM2.5를 예측하는 모델: Target의 특성상 단 3시간 만에 PM2.5 농도가 급하게 높아지거나 낮아질 가능성이 적다. 심지어 그게 input에 포함되기 때문에 이미 성능에 큰 영향을 주고 있을지도 모르겠다.</li>
<li>Feature를 줄여서 학습시킨 모델: 돌풍(gust)나 구름(cloud) 등 직접 구하기 힘든 지표들이 있다. 이미 그런 방대한 데이터로 모델을 만들어놨기 때문에 새 데이터 입력 때도 같은 종류의 데이터를 찾아야 한다는 문제가 있다. <strong>좀 더 유저가 찾아보기 쉬운 feature들로만 이뤄져 있으면서도 성능이 빠지지 않는 모델을 테스트해봐야한다.</strong></li>
<li>대기질 정보(CO, SO2, NO, PM2.5 농도)는 단순한 포털 검색으로 찾기가 어려울 수 있다. 대기질 안내 페이지에서 크롤링을 해서 가져올 수 있도록 하면 좋을 것 같다. (크롤러를 붙여보기)</li>
</ul>
<p>전체 프로그램은 -&gt; [github] (<a href="https://github.com/jonas-jun/AiR_Predictor_foruse">https://github.com/jonas-jun/AiR_Predictor_foruse</a>)</p>
]]></description>
        </item>
    </channel>
</rss>