디즈니 캐릭터 분류 모델
fastai 라이브러리를 사용한 디즈니 캐릭터 분류 모델 만들어보는 튜토리얼 노트북입니다. fastbook의 라이브러리의 일부를 사용하는 방법과, MS Azure의 Cognitive Service를 활용하여 이미지 검색/다운로드 하는 방법, 구축된 이미지 데이터로 분류 하는 모델을 만든느 방법까지를 다룹니다. 그러면서, fastai 라이브러리의 약간은 내부적인 내용도 함께 다뤄집니다.
- fastai(v2) 라이브러리 설치
- fastai 버전확인
- fastbook 제공, 유틸리티 라이브러리 설치
- 필요한 모든 라이브러리를 import
- Bing 으로부터 이미지 다운로드 및 데이터셋 구축
!git clone https://github.com/fastai/fastai
!pip install -e "fastai[dev]"
import fastai
fastai.__version__
fastbook 제공, 유틸리티 라이브러리 설치
마이크로소프트 Bing 검색엔진을 통한 이미지 검색 등의 라이브러리는 fastai 차원에서 제공하는 일반화된 라이브러리가 아님. 다만, 책 내용의 실습을 위해서 fastbook 저장소에서 별도로 작성되어 제공되는것임.
만약, 책 실습 외의 상황에서도 search_images_bing
등의 API를 사용하고 싶다면, 반드시 아래 코드를 실행하여 fastbook
패키지를 import 해 줘야함.
fastbook의 유틸리티 함수로서 작성된 함수의 목록은 다음과 같음.
search_images_bing
plot_function
draw_tree
cluster_columns
딱히 구현상 어려운 수준의 함수는 아니므로, 참고하여 직접 구현해도 됨.
!pip install -Uqq fastbook
import fastbook
from fastbook import *
from fastai.vision.all import *
from fastai.vision.widgets import *
Azure Cognitive Service API 키 값
API 키를 얻어오기 위해서는
-
일단 MS Azure에 가입이 되어 있어야함
- 계정이 없다면 가입 링크에 접속한 후, "체험 계정 만들기" 버튼을 클릭해서 계정을 생성해야함
- 계정이 있다면, 단순히 로그인만 해 주면 됨
-
MS Azure 계정이 있다면, 로그인 후 Azure Portal에 접속해 줘야함
- 상단의 검색 바에서 "Cognitive Services"를 타이핑한 다음, 검색된 결과를 클릭함 (당연히 Cognitive Services 라는걸 클릭 해야함)
- +New 버튼을 클릭
- Marketplace에서, "Bing Search"를 검색하여 클릭
- "Create" 버튼을 클릭
- 각종 정보 입력. 단, "Pricing Tier"에는 반드시 "무료인 것을 선택"
- Resource Group이 없는 경우, 텍스트박스 하단의 "New Group"을 클릭하여 하나 생성
-
생성된 Bing Search 를 클릭
- 좌측 메뉴의 "Keys and Endpoint"를 선택
- "Show Keys" 버튼을 클릭하여, 숨김표시된 Key 값을 풀어줌
- Key1 을 복사하여, 아래의
key
값에 넣어줌
key = '당신만의 Azure Cognitive Service (Bing Search) API Key를 넣어주세요'
이미지 다운로드
몇 가지 알아두면 좋을만한 정보의 나열
-
Path
는 fastai에서 개발한fastcore
에 포함된 기초 라이브러리로, 기본적으로는 Python에서 표준적으로 제공하는pathlib.Path
를 확장한 것임.-
pathlib.Path
의 기능을 모두 그대로 사용 가능하지만, 여기에 다음의 몇 가지 편의 사항을 추가함..readlines()
-
.read()
-
.write()
-
.save()
.load()
.ls()
-
-
search_images_bing
함수를 이용하여, URL 목록을 가져옴.- Azure Cognitive Service API Key 및 검색하고자 하는 키워드를 파라미터로서 제공 해 줘야함.
- 이 함수가 반환하는 객체는 Python의 표준 객체인 list를 확장한 L 이라는 객체임 (fastcore)
-
download_images
함수를 이용하여, 준비된 URL 목록의 모든 이미지를 다운로드함.- 정확히는 results가 URL 목록은 아니며, Bing Search Service가 반환한 JSON 포맷의 내용임.
- L 객체는 attrgot 이라는 메서드를 제공하는데, 리스트에 포함된 모든 아이템으로부터 인자로 지정된 속성의 값들만을 추출하여, 별도의 리스트(L)을 반환함.
- 첫 번째 인자인 dest가 이미지 다운로드 후 저장될 위치임
# 아래 한줄의 코드는 일종의 리스트를 만들어줌 (정확히는 Tuple)
disney_characters = 'disney malificent', 'disney cinderella', 'disney jasmin', 'disney mulan', 'disney belle', 'disney pocahontas'
path = Path('disney')
# 최상위 디렉토리의 생성
if not path.exists():
path.mkdir()
# 각 이미지 클래스 별로 반복하여 접근
for character in disney_characters:
# 클래스 이름의 Path 지정 및 생성
dest = (path/character)
dest.mkdir(exist_ok=True)
# search_images_bing 함수를 이용하여, URL 목록을 가져옴
results = search_images_bing(key, character)
# download_images 함수를 이용하여, 준비된 URL 목록의 모든 이미지를 다운로드함
download_images(dest, urls=results.attrgot('content_url'))
모든 이미지파일의 Path 목록
fastai에서 제공하는 get_image_files
는 지정된 Path
를 기점으로, 하위에 포함된 모든 이미지 목록을 재귀적으로 검색하여 들고옴
구분없이 몽땅 들고오는 이유는 다음과 같음
- 다운로드된 이미지는 폴더이름 단위로 클래스가 구분됨
- 이후
DataBlock
또는ImageDataLoaders
객체 생성시 클래스(레이블)을 구분해내기 위한 로직 추가가 가능함. 구분하는 별도의 함수를 만들게 되며, 단순히 규칙을 지정해 주기만 하면됨.
- 이후
아래 코드의 실행결과는 fnames
객체 내용을 출력해줌
- 출력 결과의 앞 부분 (#...)은 리스트에 포함된 아이템의 개수를 의미함. 원래 표준 list 객체는 이러한 정보를 출력하지 않으나, L은 출력해 주는 특성이 있음.
fnames = get_image_files(path)
fnames
failed = verify_images(fnames)
failed
L
객체에는 함수형 언어적 기능인 map
메서드가 구현되어 있음. map
메서드의 파라미터는 어떤 함수가 지정될 수 있음.
map
메서드가 호출되는 순간, 각 아이템을 반복적으로 접근하면서, 제공된 함수를 각 아이템에 적용하여 반환된 결과를 싸그리 모아서 새로운 L
객체를 만들어줌.
Path.unlink
라는 함수가 하는일은 failed
에 포함된 모든 아이템 (Path
)에 대하여, 파일을 삭제하는일을 수행함. Path.unlink
는 Python의 표준 라이브러리임.
failed.map(Path.unlink)
-
DataBlock은 DataLoaders를 만들기 위한 저수준의 API. 각 인자가 가지는 의미는
- blocks: Block의 리스트. Block은 데이터를 표현하는 수단
- 두 개 이상의, 여러개의
Block
을 지정하는것도 가능함. 단, 이때는n_inp
라는 파라미터의 값을 조절하여, 입력으로 사용될 Block이 몇 개인지를 지정해 줘야만 함. 예를 들어서blocks=(ImageBlock, BBoxBlock, BBoxLblBlock)
처럼 설정했는데 그 중 첫번째만을 입력으로 삼고 싶다면,n_inp=1
이라고 지정해 줘야하만함. - 이미지처리 관련, 정의된 Block은 다음과 같은것들이 있음. 기본적으로 모두,
TransformBlock
인스턴스를 반환함.TransformBlock
은 단순히,type_tfms, item_tfms, batch_tfms, dl_type, dls_kwargs
내용들을 잡아두기 위한 Wrapper 클래스임.ImageBlock
MaskBlock
PointBlock
BBoxBlock
BBoxLblBlock
- 두 개 이상의, 여러개의
- get_items: 데이터를 가져오기 위한 함수를 지정
-
DataBlock
생성 후,DataLoaders
를 반환받기 위해서,dataloaders()
라는 메서드를 사용하게 됨.dataloaders()
메서드에는 경로(Path)를 지정해 주게 되어 있는데, 이 경로를 기반으로 get_items의 행동이 결정됨. - 가령 아래처럼
get_image_files
를 지정하면,dataloaders(path)
메서드 호출시,path
밑에 딸린 모든 이미지를 긁어오게됨
-
- splitter: 데이터를 학습/검증으로 분리해내기 위한 수단을 지정
- 다양한 Splitter 클래스가 존재함
RandomSplitter
TrainTestSplitter
IndexSplitter
GrandparentSplitter
FuncSplitter
MaskSplitter
FileSplitter
ColSplitter
RandomSubsetSplitter
- 경우에 따라서, 데이터셋이 미리 train/valid 와 같은 디렉토리로 나뉘어져 제공되는 경우가 있음.
- 이 때는 splitter에 할당되는 객체의
valid_pct
값을 지정하지 않으면 됨. 그러면 자동으로train
및valid
라는 이름의 디렉토리를 대상으로 삼음. 즉, 다른 이름의 디렉토리라면, 이 이름을train
및valid
라는 이름으로 맞춰줘야함.
- 이 때는 splitter에 할당되는 객체의
- 다양한 Splitter 클래스가 존재함
- get_y: 레이블을 지정하는 수단을 지정
- blocks의 출력 개수가 여러개 될 수 있듯이, get_y 또한 여러개가 지정될 수 있다. 이 경우는 리스트에 두 개의 함수를 포함시켜주면 됨.
- 기본 제공 parent_label은 단순히, 부모 디렉토리명을 레이블로 보겠다는 뜻이된다.
- RegexLabeller도 기본제공되는데 regex 기반으로 레이블을 지정할 수 있어서, 매우 강력함.
-
item_tfms: 각 아이템 별 데이터 변형
- 보통 이미지의 경우, Resize를 해줌
- 왜 Resize를 batch_tfms에서 해주지 않는가 하면, 이미지의 크기가 모두 제각각이기 때문임. GPU는 동일한 크기, 동일한 연산을 동시 다발적 (배치 단위)으로 단순히 계산하는데 최적화 되어 있음. 따라서, 모두 제각각인 이미지를 일단 최초에 동일한 크기로 맞추는 작업은 개별적으로 CPU에서 수행될 필요가 있음.
-
batch_tfms: 배치단위의 데이터 변형
- item_tfms에서 모두 동일한 크기로 맞춰지거나, 쨋든 GPU에서 배치단위로 계산되기에 최적화된 데이터 묶음에 대하여, 묶음 형태의 데이터 변형을 가한다.
- 여러가지 변형 방법이 기술될 수 있지만,
aug_transforms
를 사용하는것이 초반에는 선호됨. 다양한 데이터 증강 기법들이 모두 포함되어 있음.- 링크를 확인해 볼것
- blocks: Block의 리스트. Block은 데이터를 표현하는 수단
-
약간 상위레벨에서 보자면, 크게 세 종류의 변형(Transform)이 일어남
- Type Transform
- blocks 인자를 통해서, 입력/출력에 대한 Type 변형
- Item Transform
- item_tfms 인자를 통해서, 각 아이템별 데이터 변형
- Batch Transform
- batch_tfms 인자를 통해서, 배치단위의 데이터 변형
- Type Transform
disney_characters = DataBlock(
blocks=(ImageBlock, CategoryBlock),
get_items=get_image_files,
splitter=RandomSplitter(valid_pct=0.2, seed=42),
get_y=parent_label,
item_tfms=Resize(128),
batch_tfms=aug_transforms(mult=2.0, size=224))
DataBlock의 인자 중 blocks에 할당되는 값들의 정체가 궁금할 것임. 간단한 ImageBlock과 CategoryBlock을 통해서 이들이 무엇인지 살펴봄.
-
ImageBlock
def ImageBlock(cls=PILImage): "A `TransformBlock` for images of `cls`" return TransformBlock(type_tfms=cls.create, batch_tfms=IntToFloatTensor)
- TransformBlock을 반환하는데,
type_tfms
및batch_tfms
인자의 값을 지정해주었음 -
약간 혼란이 올 수 있는 부분은 cls인데, PILImage가 PIL의 Image와는 다른것임. fastai에서 Image 클래스를 확장한 것으로, create라는 별도의 메서드가 추가됨.
class PILBase(Image.Image, metaclass=BypassNewMeta): _bypass_type=Image.Image _show_args = {'cmap':'viridis'} _open_args = {'mode': 'RGB'} @classmethod def create(cls, fn:(Path,str,Tensor,ndarray,bytes), **kwargs)->None: "Open an `Image` from path `fn`" if isinstance(fn,TensorImage): fn = fn.permute(1,2,0).type(torch.uint8) if isinstance(fn, TensorMask): fn = fn.type(torch.uint8) if isinstance(fn,Tensor): fn = fn.numpy() if isinstance(fn,ndarray): return cls(Image.fromarray(fn)) if isinstance(fn,bytes): fn = io.BytesIO(fn) return cls(load_image(fn, **merge(cls._open_args, kwargs))) def show(self, ctx=None, **kwargs): "Show image using `merge(self._show_args, kwargs)`" return show_image(self, ctx=ctx, **merge(self._show_args, kwargs))
- 보다시피 create가 하는일은 여러종류의 입력 type이 들어올 수 있다는것을 가정한 후, 이들을 적절한 형태로서 통일화된 결과로 맞춰주기위한 변화작업임. 느낌적으로? numpy 형식으로 변환해 주는것 같음.
- TransformBlock의 batch_tfms는 필수요건이 아님. 단, 여기에 Transform하려는 오퍼레이션이 설정된 경우, DataBlock 정의시 전달된 batch_tfms과 merge되어서 결국은 한 뭉떵이가 됨.
... self.default_batch_tfms = _merge_tfms(*blocks.attrgot('batch_tfms', L())) ... self.new(item_tfms, batch_tfms) ... def new(self, item_tfms=None, batch_tfms=None): self.item_tfms = _merge_tfms(self.default_item_tfms, item_tfms) self.batch_tfms = _merge_tfms(self.default_batch_tfms, batch_tfms) return self
- TransformBlock을 반환하는데,
-
CategoryBlock
def CategoryBlock(vocab=None, sort=True, add_na=False): "`TransformBlock` for single-label categorical targets" return TransformBlock(type_tfms=Categorize(vocab=vocab, sort=sort, add_na=add_na))
-
이번에는 type_tfms로 지정된 것이 Categorize 라는 클래스 인스턴스임
class Categorize(DisplayedTransform): "Reversible transform of category string to `vocab` id" loss_func, order, store_attrs = CrossEntropyLossFlat() , 1 , 'vocab,add_na' def __init__(self, vocab=None, sort=True, add_na=False): store_attr(self, self.store_attrs+',sort') self.vocab = None if vocab is None else CategoryMap(vocab, sort=sort, add_na=add_na) def setups(self, dsets): if self.vocab is None and dsets is not None: self.vocab = CategoryMap(dsets, sort=self.sort, add_na=self.add_na) self.c = len(self.vocab) def encodes(self, o): return TensorCategory(self.vocab.o2i[o]) def decodes(self, o): return Category (self.vocab [o])
-
-
dls = disney_characters.dataloaders(path)
show_batch
메서드는 training 데이터셋에 포함된 데이터 중 일부를 랜덤하게 출력해줌. 다음과 같은 파라미터로 구성될 수 있음.
TfmdDL.show_batch(b=None, max_n=9, ctxs=None, show=True, unique=False, **kwargs)
max_n
값의 설정에 따라서, 출력되는 이미지의 개수를 정할 수 있음. 출력 대상이 될 데이터셋은 train_ds
, valid_ds
로서 접근이 가능함.
dls.valid_ds.show_batch()
와 같은일이 가능하다는것.
dls.show_batch()
learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(4)
learn.fine_tune(4)
cleaner = ImageClassifierCleaner(learn)
cleaner
count = 0
for idx in cleaner.delete():
cleaner.fns[idx].unlink()
count = count+1
print(count)
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()
learn.export()
btn_upload = widgets.FileUpload()
out_pl = widgets.Output()
lbl_pred = widgets.Label()
learn_inf = load_learner('export.pkl')
def on_click_classify(change):
img = PILImage.create(btn_upload.data[-1])
out_pl.clear_output()
with out_pl: display(img.to_thumb(256,256))
pred,pred_idx,probs = learn_inf.predict(img)
lbl_pred.value = f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'
btn_run = widgets.Button(description='Classify')
btn_run.on_click(on_click_classify)
VBox([widgets.Label('Select your disney character!'),
btn_upload, btn_run, out_pl, lbl_pred])