대봉감일까, 단감일까?

Author

Heeyoung KIm

Published

December 9, 2022

대봉감과 단감을 구분하는 AI를 구축하겠습니다.

탄닌은 떫은 맛을 나게 하는 성분으로, 와인에도 함유되어 있습니다.

대봉감은 홍시를 만들어 먹는 감으로, 탄닌 수용성이 커 딱딱한 상태에서 먹을 때 굉장히 떫은 맛이 납니다. 그에 비해 단감은 탄닌 수용성이 낮아 딱딱한 상태에서도 단맛이 주를 이룹니다.

이를 구분할 수 있는 AI를 만든다면 떫은 감은 홍시로, 단감은 단단한 상태에서 맛있게 먹을 수 있지 않을까요?

우선 한글 사용을 위해 나눔폰트를 다운로드하고 기본 폰트로 설정합니다. (참고 링크)

from sys import platform

if platform == 'linux':

    !sudo apt-get update
    !sudo apt-get install -y fonts-nanum
    !sudo fc-cache -fv
    !rm ~/.cache/matplotlib -rf

    import matplotlib.pyplot as plt

    plt.rc('font', family='NanumBarunGothic') 

::: {#cell-4 .cell _kg_hide-input=‘true’ _kg_hide-output=‘true’ tags=‘[]’ execution_count=2}

!pip install -Uqq fastai duckduckgo_search

:::

Step 1: 대봉감과 단감의 사진 다운로드하기

::: {#cell-6 .cell _kg_hide-input=‘true’ tags=‘[]’ execution_count=3}

from duckduckgo_search import ddg_images
from fastcore.all import *

def search_images(term, max_images=30):
    print(f"Searching for '{term}'")
    return L(ddg_images(term, max_results=max_images)).itemgot('image')

:::

search_images는 duckduckgo에서 이미지를 찾는 함수입니다.

#NB: `search_images` depends on duckduckgo.com, which doesn't always return correct responses.
#    If you get a JSON error, just try running it again (it may take a couple of tries).
urls = search_images('대봉감', max_images=1)
urls[0]
Searching for '대봉감'
'http://www.food123.kr/gamimg/aa6.jpg'
  • download_url로 url의 이미지를 dest에 다운로드합니다.
  • Image.open으로 경로에 위치한 데이터를 열 수 있습니다.
from fastdownload import download_url
dest = '대봉감.jpg'
download_url(urls[0], dest, show_progress=False)

from fastai.vision.all import *
im = Image.open(dest)
im.to_thumb(256,256)

단감에도 동일하게 진행하겠습니다.

download_url(search_images('단감', max_images=1)[0], '단감.jpg', show_progress=False)
Image.open('단감.jpg').to_thumb(256,256)
Searching for '단감'

정확한 사진을 불러온 것 같습니다. 이제는 여러 대봉감, 단감 이미지를 다운로드 받고 각각의 폴더에 저장하겠습니다.

searches = '대봉감', '단감'
from pathlib import Path
path = Path('daebong_or_dangam')
from fastai.vision.all import *
from time import sleep

아래 for문은 다음과 같이 수행됩니다. - dest에 키워드 Path를 지정합니다. - dest.mkdir로 폴더를 만듭니다. - download_images로 이미지를 다운로드 합니다. urls의 개수는 위에서 만든 search_images의 기본 max_images인 30입니다. - resize_images는 해당 폴더에 존재하는 사진에 대해 반복적으로 수행됩니다.

for o in searches:
    dest = (path/o)
    dest.mkdir(exist_ok=True, parents=True)
    download_images(dest, urls=search_images(f'{o}'))
    sleep(10)
    resize_images(path/o, max_size=400, dest=path/o)
Searching for '대봉감'
Searching for '단감'

Step 2: 모델 훈련시키기

verify_images는 열리지 않는 이미지를 확인합니다.

failed = verify_images(get_image_files(path))

failed에 존재하는 경로, 즉 열리지 않는 이미지들에 대해 map을 통해 Path.unlink를 각각 적용합니다.

Path.unlink는 삭제와 동일합니다.

failed.map(Path.unlink)
len(failed)
1

모델을 훈련시키기 위해서는 Dataloaders가 필요합니다. 이를 위해서는 Fast.ai의 중수준 API인 DataBlock을 사용하거나 데이터 타입별로 미리 구축된 고수준 API를 사용할 수 있습니다.

아래 DataBlock에 사용하는 인자는 blocks, get_items, splitter, get_y, item_tfms 입니다.

dls = DataBlock(
    blocks=(ImageBlock, CategoryBlock),
    get_items=get_image_files,
    splitter=RandomSplitter(valid_pct=0.2, seed=42),
    get_y=parent_label,
    item_tfms=[Resize(192, method='squish')]
).dataloaders(path, bs=32)

blocks는 어떤 input과 output을 가지고 있는지 명시해주는 것입니다. 예제에서는 input을 Image, output을 Category로 가지고 있기 때문에 ImageBlockCategoryBlock을 사용합니다.

get_items는 아이템을 불러오는 함수를 지정합니다.

splitter는 훈련용, 검증용 데이터셋을 나눌 기준을 설정합니다. RandomSplitter 외에도 다양한 Splitter들이 존재합니다.

get_y는 목적변수의 값을 어디에서 가져올지 지정하는 것입니다. 위에서 사용된 parent_label은 상위 폴더의 이름으로 가져오는 것을 의미합니다.

item_tfms는 아이템 변환의 내용을 지정합니다. 각각 이미지를 동일하게 192 사이즈로 리사이징하고, 방법은 ’squish’를 사용했습니다. 아래는 사용 가능한 method입니다.

ResizeMethod()
fastcore.basics.ResizeMethod(Squish='squish', Crop='crop', Pad='pad')
dls.show_batch(max_n=6)

데이터의 준비는 모두 끝났습니다. 이제 learner를 정의하고, 훈련시키면 됩니다.

learner에는 dataloadersarchitecture 지정이 필수적입니다.

데이터 특성별 learner가 존재하는데, 이미지를 위한 것은 vision_learner입니다.

learner = vision_learner(dls, resnet18, metrics=error_rate)
/usr/local/lib/python3.9/dist-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and will be removed in 0.15, please use 'weights' instead.
  warnings.warn(
/usr/local/lib/python3.9/dist-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and will be removed in 0.15. The current behavior is equivalent to passing `weights=ResNet18_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet18_Weights.DEFAULT` to get the most up-to-date weights.
  warnings.warn(msg)

정확도를 측정하기 위한 메트릭으로 error_rate를 사용하겠습니다. error_rate는 라벨을 맞추는 정확도로 생각해주시면 되겠습니다.

learner.fine_tune(epochs=10)
epoch train_loss valid_loss error_rate time
0 1.326514 1.511976 0.589744 00:03
epoch train_loss valid_loss error_rate time
0 0.375253 0.329849 0.153846 00:01
1 0.335464 0.031661 0.000000 00:01
2 0.240976 0.003346 0.000000 00:01
3 0.185915 0.000432 0.000000 00:01
4 0.143643 0.000189 0.000000 00:01
5 0.115001 0.000153 0.000000 00:01
6 0.095365 0.000119 0.000000 00:01
7 0.079920 0.000103 0.000000 00:01
8 0.068021 0.000119 0.000000 00:01
9 0.058917 0.000138 0.000000 00:01

fine_tune은 사전 학습된 architecture를 학습시키기 위한 함수입니다. 이 함수는 사전 학습된 부분은 많이 학습하지 않고, 새로 추가된 출력 계층은 많이 학습합니다. 자세한 내용은 추후에 배웁니다.

Step 3: 모델을 사용해봅니다

img = PILImage.create('단감.jpg')
img.to_thumb(400)

predict는 세 가지 값을 tuple로 산출합니다. 각각 예측되는 레이블, Loss function의 값(정확히 이해하지 못했습니다), 예측 확신도입니다.

is_dangam, _, probs = learner.predict(img)
print(f'이 사진은 {is_dangam} 입니다.')
print(f'{probs[0]:.4f}의 확률로 그렇습니다.')
이 사진은 단감 입니다.
1.0000의 확률로 그렇습니다.