2019/05/13 - [Study/인공지능학습] - [인공지능] coral usb accelerator + Raspberry pi zero w

2019/05/16 - [Study/인공지능학습] - [인공지능] Coral USB Accelerator와 Edge TPU




지난 포스팅에서 구글의 Coral USB Accelerator를 구입하고 간단한 모델을 돌려본 것을 정리해보았다.

이번 포스팅에서는 Coral USB Accelerator(이하 coral)의 공식 홈페이지를 참조하여 요구 사항 및

엣지 컴퓨팅에 사용되는 Tesorflow Lite에 대해 간단하게 살펴보고자 한다.


각각의 공식 홈페이지는 다음과 같다.


Coral
Tensorflow Lite


Coral USB Accelerator


Coral 시리즈는 지금 소개하고자 하는 USB Accelerator 외에 완전한 보드 형태인 Coral Dev Board가

있으나 이번 포스팅에서는 USB Accelerator에 대해서만 다루도록 하겠다. 다른 부분에 대해서는 공식홈페이지를

참고하기 바란다.


우선 데이터시트를 보면 coral은 구글에서 자체적으로 만든 프로세서를 사용하며 USB 3.0 Type C 소켓을

지원하고 데비안 기반의 리눅스에서 작동을 한다. 하지만 데비안에서 파생된 우분투 리눅스도 사용 가능하며

x86-64 또는 ARMv8 명령어 집합을 포함하는 ARM32/64 기반의 프로세서에서 사용이 가능하다.


이 기준에 따르면 라즈베리파이 zero 시리즈는 ARMv6 명령어 셋 기반이라 지원이 안되지만 홈페이지에도

나와 있듯이 비공식적으로는 사용 가능하다. 또한 coral은 USB 3.0을 지원하지만 라즈베리파이는 아직 2.0만을

지원하기 때문에 속도면에서 손실이 있다.


coral을 사용하기 위해서는 Edge TPU runtime과 Python 라이브러리를 설치해야 하는데 기본적으로

Python 3.5 버전을 지원하며 만일 3.6 버전 이상인 경우 install.sh 스크립트 파일의 맨 마지막 줄에서

python3.5 대신 python3으로 수정한 후 설치하면 된다. 라이브러리가 설치되는 동안 coral이 연결된

상태였다면 설치 후 다시 연결을 해주어야 coral이 작동한다.


또한 이 과정에서 클록 주파수를 최대로 사용할 것인지를 묻는데 만일 최대로 사용하게 되면 추론 속도가

2배가 되지만 대신에 전력 소모량이 많아지며 발열이 증가한다. 심지어는 최대로 할 경우에는 손닿지 않는

곳에 장치를 두거나 부상을 막기 위해서는 기본 속도를 사용하라고 경고하고 있을 정도니 발열이 꽤나 심한
것 같다. 클록 주파수 설정은 단순히 라이브러리 설치를 다시 진행하여 변경할 수 있지만 2019년 4월 이전에

설치한 경우별도의 업데이트 과정을 거쳐야 한다.


coral에는 흰색 LED가 하나 장착이 되어있는데 신호는 매우 단순하다. 그냥 불이 들어와 있으면 초기 상태인

것이고 연산을 수행하면 LED가 점멸한다. 이게 끝이다…


coral을 작동시키기 위해서는 USB를 통해 적어도 5V 전압에 500mA의 전류를 공급해주어야 한다. 내가 

라즈베리파이 zero w를 사용하면서 usb 허브에 coral을 물릴 수밖에 없었는데 이 때 전원 공급이 가장 

우려되었지만 별도의 외부 전원 없이 coral이 정상 동작 하였다. 혹시 전원 공급이 약해서 속도가 느린가 하고 

usb 허브에 외부 전원을 넣어 봤으나 느린 건 매한가지였다…-.-


이번 포스팅에서는 여기까지만 다루기로 한다. 기타 상세한 내용에 대해해서는 공식 홈페이지를 참고하기바란다.


TensorFlow models on the Edge TPU - 개요

엣지 컴퓨팅에 사용되는 TensorFlow Lite는 TensorFlow 공식 홈페이지에 소개가 되어있지만 Coral

홈페이지에도 TensorFlow 관련 내용이 있어 먼저 Coral 홈페이지의 내용부터 정리를 해보겠다.

Edge TPU의 경우 낮은 전력을 소모하면서 빠른 신경망 연산을 처리해야 한다는 제약이 있기 때문에

조금 특별한 신경망 연산 기능과 구조를 가져야 한다. 때문에 Edge TPU에서도 CNN과 같은 심층 신경망을

실행할 수 있지만 TensorFlow Lite라는 특별한 버전만 지원되며 Edge TPU에 맞게 컴파일 되어야 한다.


TensorFlow Lite는 TensorFlow의 경량 버전으로 TensorFlow Lite의 모델과 인터프리터 커널의
바이너리 사이즈를 줄임으로써 모바일 기기나 임베디드 시스템에서 추론의 속도를 높였다. 다만 모델을 바로
훈련 시키지는 못하며 TensorFlow에서 훈련시킨 모델을 TensorFlow Lite converter라는 툴을 이용하여

변환해야 한다.


Edge TPU에 대한 최적화 과정에서 Quantizing이라는 용어가 나오는데 이는 사이즈를 줄이기 위해

가중치나 활성함수의 출력에 사용되는 32비트 부동 소수점 타입을 8비트 고정 소수점 타입으로 변환하는

과정이라고 한다. 이 과정을 통해 모델이 더 작고 빨라지게 되며, 비록 정밀도가 떨어지긴 하지만 추론의

정확도에는 큰 영향을 미치지 않는다고 한다.


이러한 이유로 새로 모델을 만들고 훈련시키는 과정이 조금 복잡한데 구글에서 새로운 데이터 셋으로 재학습을

하면 Edge TPU에서 사용 가능한 TensorFlow 모델들을 다수 제공하고 있으므로 필요한 경우 이를 활용하면

시간을 절약할 수 있을 것이다.


Edge TPU work process출처 : https://coral.withgoogle.com/docs/edgetpu/models-intro/



하지만 직접 모델을 만들어 Edge TPU에서 사용하려고 한다면 다음의 요구 조건을 충족시켜야 한다.


  • 텐서 파라미터는 8비트 고정 소수점 타입이어야 한다.
  • 텐서의 크기는 컴파일 시에 그 크기가 고정되어있어야 한다.
  • bias 텐서 같은 모델 파라미터 역시 컴파일 시에 크기가 고정되어있어야 한다.
  • 텐서들은 3차원 이하여야 한다. 만일 3차원 이상의 크기를 가지는 텐서를 사용할 경우 가장 안쪽의
    3개 차원만이 1보다 큰 크기를 갖게 된다.
  • 모델은 Edge TPU를 지원하는 연산만 사용 가능하다.


이 조건이 충족되지 않아도 컴파일은 되지만 Edge TPU 상에서는 일부만 실행될 것이다. 이렇게 조건을 충족하지
않는 연산이 있는 경우에는 모델의 그래프가 Edge TPU에서 실행 가능한 부분과 그렇지 않은 부분으로 나뉘게

되는데 이 중 Edge TPU에서 지원하지 않는 부분은 CPU에서 실행된다.


여기서 주의할 점은 현재의 Edge TPU 컴파일러는 이러한 분할을 한 번만 할 수 있기 때문에 만일 첫 번째

연산이 Edge TPU에서 실행 가능한 연산이고 두 번째 연산이 실행 불가능한 연산이라면 두 번째 이후의

모든 연산은 비록 Edge TPU에서 실행 가능하다 하더라도 모두 CPU에서 실행되어 버린다.


출처 : https://coral.withgoogle.com/docs/edgetpu/models-intro/



이렇게 CPU에서 실행되는 연산이 포함되면 당연히 100% Edge TPU에서 실행될 때보다 추론이 늦어지므로

Edge TPU에서 실행 가능한 연산만 포함되도록 노력해야 한다(참고로 Edge TPU 컴파일러는 컴파일이 종료되면 

Edge TPU에서 실행된 연산과 CPU에서 실행된 연산의 수를 알려준다고 한다).


앞서도 언급했지만 밑바닥부터 모델을 만드는 시간과 노력을 절약하기 위해서는 구글에서 이미 Edge TPU와

호환되도록 만들어놓은 모델을 재학습하여 사용하면 되는데 이 때 사용되는 기술이 Transfer Learning(이전 학습
또는 전이 학습) 혹은 fine tuning(세부 조정)이라는 기술이다. 또 다른 방법으로 Edge TPU 장비에서 직접
재학습을 수행하는 Python API를 이용한 weight imprinting이라는 방법이 있다.

Transfer Learning은 소개의 범위를 벗어나므로 더 상세한 내용은 Coral 홈페이지를 참조하기 바란다.

이렇게 만들어진 모델을 Edge TPU에서 실행하기 위해서는 Edge TPU runtime과 API 라이브러리가 호스트
시스템(라즈베리파이 등)에 설치가 되어있어야 한다. 하지만 Coral 제품 중 Coral Dev Board나 SoM 등은 
이미 이러한 것들이 설치되어 있어 설치 과정을 생략하고 바로 사용 가능하다.

현재 Coral 제품에서는 C++과 Python 기반의 API가 사용 가능하다.


정리

이렇게 간단하게나마 Coral Accelerator의 하드웨어적인 측면과 소프트웨어적인 측면의 개요를 정리해 보았다.
중요하게 알아야 할 것은 모바일 기기 또는 임베디드 시스템을 위해 만들어진 것이라 원하는 모든 것을 할 수는
없다는 점이다. 물론 이러한 기기들의 컴퓨팅 파워가 더 올라갈 수는 있겠지만 저전력이라는 매리트를 포기하지
않는 한은 한계가 있을 것이다.

추후 추가로 정리하겠지만 TensorFlow Lite에서 제공하고 있는 모델은 총 5가지가 있는데 다음의 모델들이다.

  • Image classification
  • Object detection
  • Smart reply
  • Pose estimation
  • Segmentation

  • 아직은 처음 두가지만 재학습 없이 테스트해보았지만 나머지 모델들도 확인을 해보고 싶다. 특히 Pose estimation은
    활용도가 꽤 있을 것 같다.

    처음에는 그저 호기심으로 인한 지름으로 시작했는데 하나하나 정리하다보니 나같이 머신러닝이나 딥러닝을 
    앝은 수준에서 시작해보려는 사람들에게는 좋은 학습 대상이 될 수 있을 것 같다. 특히나 ‘인공지능? 어따 써먹지?
    하는 사람들은 몸소 느끼면서 배울 수 있을 것 같다.

    일단 무작정 머리에 들어오지도 않는 책만 읽는 것 보다는 라즈베리파이와 Coral 그리고 TF Lite에서 주어진
    모델들을 이용하여 뭔가 하나 만들어보는 쪽으로 올해의 목표를 수정해야겠다.


    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^



    2019/05/13 - [Study/인공지능학습] - [인공지능] coral usb accelerator + Raspberry pi zero w

    2019/05/16 - [Study/인공지능학습] - [인공지능] Coral USB Accelerator와 Edge TPU




    뭔가 하나를 진득하니 끝내야 하는데…이놈에 호들갑스러운 호기심은 눈밭에 강아지 뛰놀듯한다…
    얼른 미니 드론 만들고 다음에 싱글콥터 그 다음에 4족 보행 로봇 최종판을 만들어야 하는데…
    도대체 어느세월에...ㅠ.ㅠ


    이번 호기심의 대상은 바로 구글에서 엣지 컴퓨팅 보드로 출시한 coral 시리즈 중에 usb 가속기인

    coral usb accelerator이다. 페이스북에서 어떤 분이 올려놓으신 엣지 컴퓨팅을 위한 보드들의
    성능을 비교한 해외 블로그를 보고 나니 궁금해서 견딜 수가 없었다.


    Benchmarking Edge Computing


    아직은 책만 줄창 읽어대고 있는 정도의 수준이라 내가 이걸 사서 제대로 써먹을 수나 있을지 의문이었지만
    지름의 기본 자세인 '그래도 언젠가는 써먹겠지’라는 마음가짐으로 하나 질렀다.

    그리고 다행히도 간단한 테스트 정도는 해볼 수 있었다…^^;



    Edge Computing이란?


    우선 본론에 들어가기 전에 엣지 컴퓨팅이란 것이 무엇인지 부터 알고 넘어가자.


    예전에 메인프레임을 쓸 때는 당연한 일이었고 웹 기반의 시스템이 자리를 잡은 후에도 대부분의 중요한

    연산은 서버에서 처리를 하고 클라이언트 기기(PC)에서는 자료를 입력하는 정도의 기능만 수행을 했다.

    그만큼 서버는 강력한 기능을 갖고 있었고 클라이언트로부터 전송되는 데이터들은 서버가 감당하기에

    충분한양이었다.


    하지만 스마트폰이 지배하는 세상이 되면서 클라이언트의 종류와 그로부터 전송되는 데이터는 점점 증가를

    하게 되었고 급기야 IoT라는, 모든 것을 인터넷에 연결시키겠다는 야심찬 시도가 진행되면서 이제 클라이언트는

    그 종류를 한정할 수 없게 되었고 그 수많은 클라이언트들로부터 올라오는 데이터의 양은 서버가 감당하기 어려운

    상황이 되어버렸다.


    한편, 스마트폰을 시작으로 한 클라이언트 기기들은 소형화와 고성능화로 인해 웬만한 작업은 그 기기 내에서 처리를

    할 수 있게 되었다. 예전같으면 워크스테이션에서나 가능했을 동영상 편집을 스마트폰에서도 할 수 있게 된 것이다.

    상황이 이렇다보니 클라이언트 기기를 단순히 서버로 데이터를 전송하는 용도로만 사용하는 것이 엄청난 자원의 낭비가

    된 것이다.


    결국 가능한 한 클라이언트에서 처리할 수 있는 부분은 처리를 하고 서버에서는 연산에 높은 성능이 필요한 부분을

    처리하려는 시도를 하게 되었고 이 과정에서 클라이언트 측에서 처리되는 부분을 Edge Computing이라고 부르게

    되었다.


    하지만 아무리 클라이언트 기기들의 성능이 좋아졌다고 해도 머신러닝이나 딥러닝을 처리하기에는 한계가 있다보니

    이러한 고성능의 연산을 보조해주기 위한 많은 장치들이 개발되었다. 그 자체로 보드형태를 가지고 나온 제품과

    기존의 소형 컴퓨터 보드인 라즈베리파이 등에 연결해서 사용 가능한 USB 기기 형태로 나온 제품들이 있는데

    처음 소개한 블로그에서는 바로 이러한 기기들의 성능을 벤치마킹하고 있는 것이다.



    무엇을 선택할 것인가?


    이 블로그를 보면 몇가지 대상에 대해 상당히 많은 항목을 벤치마킹 하고 있다. 하지만 영어 울렁증의 도움으로

    아주 가볍게 한 가지를 선택할 수 있었다. 바로 제일 처음 나오는 추론 속도에 대한 벤치마킹을 기준으로 가장

    가성비가 높은 제품을 고르기로 한 것이다.


    일단 추론 속도에 대한 벤치마킹 그래프를 보면 구글의 coral 시리즈가 매우 성능이 좋다. 그래서 1차로 coral 
    시리즈를 구입하기로 결정을 했다(사실 인텔의 NCS2를 더 먼저 알게 되었고 디자인도 NCS2의 사이버틱한

    디자인이 더 마음에 들었지만 벤치마킹 결과와 가성비에서 탈락했다).


    라즈베리파이가 없었다면 완전체인 coral dev board를 구입했겠지만 이미 예전에 빅데이터 클러스터 공부한다고

    깝죽대면서 사다놓은 라즈베리파이가 수두룩 빽빽하게 있어 비용면에서 더 저렴한 coral usb accelerator로

    선택을 했다.





    구매는 MOUSER에서 했으며 가격은 $74이다. 배송은 DHL로 약 2일만에 도착을 했고 배송비는 무료다!

    생각보다 빨리, 안전하게 도착해서 참 다행이었다(가장 최근의 해외 직구는 banggood에서 주문한 모터를

    2달 만에 받은 것이었다…-.-).



    장비 연결


    현재 집에 있는 라즈베리파이는 3B 모델이 5대, zero w 모델이 2대 있다. 그런데 생각해보니 3B 5대는

    클러스터 만들면서 스택 케이스에 다 연결하고 배선까지 꽁꽁 묶어놓아서 도무지 풀어서 사용할 엄두가

    나질 않았다.


    결국 zero w를 사용하기로 결정을 했는데 이게 또 만만치 않다.라즈베리파이 zero w의 경우 USB포트가

    마이크로 USB타입으로 꼴랑 2개 있다. 그나마 하나는 전원공급용이므로 실제 사용 가능한 것은 1개 뿐이다.

    게다가 w가 무색하게 빌트인 된 Wi-Fi 모듈은 잘 붙는공유기 찾기가 하늘의 별따기라 어쩔 수 없이 USB 형태의

    무선 랜카드를 별도로 사용해야 한다.


    이렇게 해서 일단 필요한 USB장비는 벌써 2개가 되었다. USB 랜카드와 coral…여기서 또 한가지 생각지 못한

    문제가 생겼다. 라즈베리파이 사 모을 때 파이 카메라 V2 버전을 같이 산 것이 있어 그 걸 사용하려고 했는데…

    아뿔싸! 라즈베리파이 3B와 zero w의 카메라용 필름 케이블 소켓 크기가 다르다…OTL 변환 케이블이 있긴 있는데…
    돈도 돈이고, 배송도 배송이고…ㅠ.ㅠ


    다행히 아이들이 학습용으로 사용하던 USB CAM이 있어서 그걸 사용하기로 했다.
    이렇게해서 총 3개의 USB장치를 붙여야 하는 상황이 되었다. 결국 USB 허브까지 하나 구매해서 어찌어찌

    연결은 성공하였다~^^ (사진은 카메라 연결 전이다.)


    사실 최대한 부피를 줄인 장치를 만들어보고 싶어서 보드로 zero w를 선택한 것인데 결과적으로는 배보다 배꼽이 

    더 큰 상황이 되어버렸다. 나중에 기회가 된다면 하우징들은 다 벗겨버리고 연결을 해서 작은 통합 기기를 만들어
    보고 싶다(하지만 성능 때문에 zero w는 못쓸 듯…-.-).






    라이브러리 설치 및 테스트


    공식적으로 coral usb accelerator가 지원하는 기기는 라즈베리파이 2와 3의 모델 B와 B+ 뿐이다.

    즉, zero w는 공식적으로는 지원하지 않는 보드이다. 하지만 비공식적으로 설치 가능한 라이브러리가 존재하고

    공식 사이트에서도 소개하고 있다. zero w에 대한 라이브러리 설치 방법은 아래 링크에서 확인 가능하다.


    support for Raspberry Pi Zero


    설치 방법은 매우 간단하며, 압축된 라이브러리 패키지를 다운로드 받고 압축을 해제한 후 install.sh를 실행하면

    끝이다. 그리고나서 공식 사이트에 있는 설명대로 데모 프로그램을 돌려보면 된다.우선 설치 후 2개의 데모 프로그램을 

    실행해보았다. 이미지 분류와 얼굴 인식을 하는 데모였는데 정상적으로결과가 나오는 것을 확인하였다. 


    특히 얼굴 인식의 경우 구글에서 받은 이미지로도 테스트를 해보았는데 대체로잘 인식이 되었으나 특정 사진 한장은 전혀 

    인식을 못하였다. 다양한 요인이 있을 것 같은데 정확한 이유는 모르겠다. 맨 처음 사진은 Mobilenet SSD v2에서 제공

    되는 사진이고 다음 3장은 구글에서 가져온 사진인데 맨 마지막 사진은 인식에 실패했다.









    성능의 문제

    사실 지금까지 라즈베리파이를 사용해오면서 딱히 성능을 확인해야 할만큼 부하가 걸리는 작업을 해본 적이
    없기에 굳이 따져보지 않았는데 막상 coral usb accelerator를 연결하여 이미지 분류나 인식 작업을 해보니
    라즈베리파이 3 모델 B와 zero w 사이에는 엄청난 성능의 차이가 있었다.

    라즈베리파이 3 모델 B에서 데모 프로그램 실행 시 얼마나 시간이 걸리는지 측정한 자료가 거의 없어 확인이
    쉽지 않았는데 유튜브에서 발견한 동영상 하나에서 대략 5초 정도 걸리는 것을 확인했다.

    그런데 zero w 모델에서는 15초 정도의 시간이 걸렸다. 처음에는 coral usb accelerator가 연결되지 않았는지
    의심도 해보았으나 연결되지 않은 경우에는 실행 시 오류가 발생을 하였고 또 연결되었을 때 연산 시 LED가 
    점멸하는 것도 확인을 하였으니 분명 coral usb accelerator가 작동을 하고 있는 것이었다.






    이러한 성능의 문제는 다음에 테스트한 동영상에서의 사물 인식의 경우에 더 심각하게 나타났다.


    카메라 테스트


    일단 앞서 말한대로 파이 카메라를 사용할 수가 없어 USB Cam을 사용하게 되었다. 
    그리고…이미 사물 인식을 하기도 전에 영상은 너무 끊겨서 적당한 수양의 과정을 거치지 않고서는 참고 
    보아줄 수 없는수준이었다.

    그래도 나름 참는데는 일각연이 있는 터라 테스트를 진행해보았다. 2개의 다른 소스로 진행을 해보았다.
    처음 테스트는 아래의 블로그를 참고하였다.

    Hands-on with the Google Coral USB Accelerator


    사용한 모델 및 실행 코드 정보가 모두 위의 블로그에 정리되어 있다. 그저 다운로드 받고 실행하기만 하면
    되는 생각보다 간단한 과정으로 진행할 수 있다. 아래 동영상을 보면 알겠지만 일단 상당히 느리다…ㅠ.ㅠ
    인식률 또한 상상했던 것보다 좋지 않은데 두 번째 테스트와 종합적으로 봤을 때 사람과 차에 대한 인식룰은
    기가 막히다. 특히 사람은 팔뚝만 슬쩍 보여도 사람으로 인식한다.

    하지만 드라이버와 커터는 인식을 못했고 귤은 사과로 인식하거나 햄스터를 새, 개 등으로 인식하고 있었다.
    두 번째 동영상은 유튜브에서 영상을 하나 틀어놓고 그 영상을 찍으면서 인식을 시킨 것인데 버스나 차를 대체로
    잘 인식하는 것 같았다.






    두 번째 테스트는 다음의 2개 사이트를 참고했다.

    프미케의 낙서장
    https://qiita.com/PINTO/items/dd6ba67643bdd3a0e595


    사용한 모델은 첫 번째 테스트와 동일하지만 실행코드가 조금 다르다. 두 번째 테스트용 실행코드에는 초당 프레임

    정보가 나오는데 위 링크한 블로그들의 프레임 수와 비교하면 형편없는 수준이다. 그리고 특이한 것이 처음 테스트에서는 
    커터를 인식하지 못했는데 두 번째 테스트에서는 커터를 인식하였다.





    정리


    대체로 뭔가 하나에 관심을 갖게 되어 실행을 해볼 때까지 구입, 준비, 실행의 과정이 꽤나 길었는데 이번에는
    관심을 갖게된 후 이 블로그 포스팅을 작성하기까지 정확히 5일 걸렸다. 거의 일사천리로 진행이 된 것이다.
    그거 하나로도 위안이 된다…^^;;

    하지만 앞에서도 강조했듯이 라즈베리파이 zero w는 작다는 것 외에는 그닥 쓸모가 없다는 것이 판명났다.
    적어도 엣지 컴퓨팅에서는…최소한 라즈베리파이 3 모델 B 이상은 되어야 암에 걸릴 확률을 낮출 수 있을것이다.
    아직은 내 수준이 남들 발바닥 언저리에서 노는 수준이다보니 근거는 없지만 뭔가 모델들이 사람이나 차량에
    특화된 것 같은 느낌이다. 자율주행 RC카 같은 것 만들어보면 재밌을 것 같다.

    물론 조금 더 실력이 된다면 수화 번역기 같은 것을 한 번 만들어보고 싶다.

    어쨌든 이렇게 또 하나의 가능성을 맛본 것으로 만족하면서 포스팅을 마친다.


    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

     

    2018/11/25 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 1 - 특성의 scale

    2018/12/10 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 2 - step 공식 이해하기

    2019/01/28 - [Study/인공지능학습] - [머신러닝 Reboot] 개념잡기 : 경사 하강법 3 - 경사 하강법의 종류

    2019/04/07 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사 하강법 4 - 규제가 있는 선형 모델

    2019/04/14 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사하강법5 - 로지스틱 회귀

    TISTORY 에디터의 문제로 이미지 크기가 화면 폭에 꽉 차게 나오는
    문제 양해 부탁드립니다...ㅠ.ㅠ

     

    머신러닝 Reboot - 개념 잡기 : 경사하강법5 - 로지스틱 회귀

    이제 거의 회귀의 끝자락이다. 사실 로지스틱 회귀의 경우 인공지능에 대한 공부를 처음 시작할 무렵 꽤나 공을
    들여 열심히 정리한 바가 있다(물론 맞는 말을 써 놓은 것인지는 여전히 의문이다…ㅠ.ㅠ). 따라서 이번 포스팅
    에서는 간략하게 개념적인 요소만 정리하고 보다 세세한 부분은 이전 포스팅의 링크로 대체하고자 한다.

     

    확률 그리고 분류

    앞서 언급했던 회귀 모델들은 모두 새로운 샘플이 나타났을 때 그에 대한 결과를 예측하는 것이 주 목적이었다.
    하지만 로지스틱 회귀의 경우 예측을 하기는 하지만 그 예측이 확률값을 갖도록 하는 것이다. 

     

    예를들어 동물의 특성(다리 갯수, 몸의 크기, 날개의 유무 등)을 데이터로 하여 로지스틱 회귀는 이 동물이 개일 
    확률이 92%, 닭일 확률이 8%이라는 예측 결과를 리턴한다. 하지만 결국 이 결과는 어떤 동물의 데이터를 
    가지고 이 동물을 닭이나 개로 분류하는 기능을 하게 되는 것이다.

     

     

    이렇듯 확률 값을 리턴해야 하기 때문에 선형 회귀의 가설 함수와는 달리 로지스틱 회귀의 가설 함수는 0~1
    사이의 값을 리턴해야 하며 이를 위해 선형 회귀 가설 함수에 대한 새로운 함수를 정의하게 되는데 이 함수를 
    시그모이드 함수라고 한다.

     

    한편 위 예에서와 같이 분류할 대상(클래스)가 2개 밖에 없는 경우를 이진 분류라고 하며 기본적인 로지스틱
    회귀는 이진 분류기라고 할 수 있다.

     

    시그모이드 함수

    시그모이드 함수는 앞서 살펴본 바와 같이 0과 1사이의 값을 출력하는 S자형 그래프를 그리는 함수로 다음과 
    같은 형태의 함수이다.

     

     

    이 식에서 t는 선형 회귀의 가설 함수인데 t가 0보다 작은 경우에는 시그모이드 함수의 결과 값이 0에 가까워
    지고 t가 0보다 크거나 같은 경우에는 시그모이드 함수의 결과 값은 1에 가까워진다.

     

    보다 상세한 내용은 링크로 대신한다.

     

    로지스틱(Logistic) 회귀 함수 살펴보기 

     

    로지스틱 회귀의 비용함수

    로지스틱 회귀에서 기존의 선형 회귀 비용 함수를 그대로 사용하게 되면 비용 함수의 그래프가 다수의 지역
    최솟값이 발생하는 형태가 되어 전역 최솟값을 찾는데 실패하는 경우가 많이 발생한다. 이러한 문제를 해결
    하고자 로지스틱 회귀의 비용함수에서는 log를 사용하게 된다. 

     

     

    이진 분류의 경우 y 값이 어떤 클래스에 포함되느냐(1), 포함되지 않느냐(0)의 2가지 값만을 갖기 때문에
    y가 어떤 값이냐에 따라 앞의 로그식 또는 뒤의 로그식 하나만 사용되는 형태이다. 그리고 이 함수는 지역
    최솟값이 존재하지 않는 convex 함수로 최솟값을 찾아낼 수 있다.

     

    역시 상세한 내용은 링크로 대신한다.

     

    로지스틱회귀의 비용함수 이해

     

    결정경계

    이진 분류기에서 두 개의 클래스를 구분짓는 선을 결정경계라고 하며 이는 보통 모델이 50% 확률을 추정하는
    지점이 된다.

     

    소프트맥스 회귀

    3개 이상의 클래스를 분류하는 것은 2진 분류기를 여러개 훈련시켜 할 수 있다. 소프트맥스 회귀는 여러개의
    이진분류기를 사용하는 방법을 일반화 시켜 한 번에 3개 이상의 클래스를 분류할 수 있도록 만든 모델이다.
    소프트맥스 함수도 다양한 표현이 가능한데 ⎡핸즈온 머신러닝⎦에 사용된 식을 기준으로 보면 다음과 같다.

     

     

    이 때 각각의 기호는 다음을 의미한다.

     

    1. K는 클래스 수
    2. s(x)는 샘플 x에 대한 각 클래스의 점수를 담고 있는 벡터
    3. σ 함수는 샘플 x에 대한 각 클래스의 점수가 주어졌을 때 이 샘플이 클래스 𝑘에 속할 추정 확률

     

    소프트맥스 회귀는 서로 배타적인 클래스에 대한 분류에만 사용 가능하다는 특징이 있다.

    소프트맥스 회귀는 비용함수로 크로스 엔트로피 함수를 사용하는데 역시 핸즈온 머신러닝에 사용한 식을
    기준으로 보면 다음과 같다.

     

     

    앞서 말했듯이 소프트맥스 회귀는 로지스틱 회귀의 일반화 된 형식으로 크로스 엔트로피 함수를 클래스가 딱
    2개인(즉, K=2 인) 경우에 적용하면 로지스틱 회귀의 비용함수와 동일해진다.

    역시 상세한 설명은 이전 포스팅을 링크한다.

     

    다항로지스틱회귀 살펴보기

    로지스틱 회귀 비용함수로부터 Cross-entropy 도출하기

     

    정리

    예전에 처음 공부를 시작했을 때도 여기서부터가 어려워졌던 것 같다. 기존 선형 회귀도 잘 이해가 안가는
    상황에서 확률까지 등장을 하니…그래도 두 번째 정리라 조금 나아지긴 했지만 조금 더 깔끔하게 다듬어야
    할 필요는 있을 것 같다. 다듬는 것은 숙제로 남겨두고 서포트 벡터 머신으로 넘어가자…-.-

     

    숙제

    1. 핸즈온 머신러닝 로지스틱 회귀 챕터에 나오는 예제 소스들을 분석하고 실행시켜보자
    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

     

    2018/11/25 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 1 - 특성의 scale

    2018/12/10 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 2 - step 공식 이해하기

    2019/01/28 - [Study/인공지능학습] - [머신러닝 Reboot] 개념잡기 : 경사 하강법 3 - 경사 하강법의 종류

    2019/04/07 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사 하강법 4 - 규제가 있는 선형 모델

    2019/04/14 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사하강법5 - 로지스틱 회귀

     

     

    머신러닝 Reboot - 개념 잡기 : 경사 하강법 4 - 규제가 있는 선형 모델

     

    지난 시간까지 일반적인 선형 회귀에 대해 알아보았다. 하지만 당연하게도 세상의 문제는 너무도 다양하고 따라서
    그 문제를 해결할 수 있는 방법도 다양할 수 밖에 없다. 오늘은 그 중에서도 훈련 데이터에만 너무 최적화된 결과를
    만들어내는 과대 적합의 문제를 해결할 수 있는 선형 회귀에 대해 정리해보자.

     

    과대적합

     

    지난 포스팅의 후반부에 다항 회귀에 대해서 알아보았다. 다항 회귀는 데이터의 분포가 2차 이상의 함수
    그래프 형태를 띠고 있는 경우 사용하는 기법으로 보통 적절한 차수의 특성을 훈련 데이터에 추가하여 학습을
    진행한다. 이 때 지나치게 차수를 높이면 아래 그래프의 초록색 실선과 같이 상당히 난해한 결과가 나온다.
    이렇듯 모든 훈련 데이터 하나하나에 맞는 결과를 만들어내는 것을 과대적합이라고 한다. 

     

     

    비유를 하자면 기성복과 맞춤복의 차이라고 할 수 있겠다. 
    A라는 회사에서 직원들의 유니폼을 맞춤복으로 하기로 결정했다면 새로운 직원이 들어올 때마다 유니폼을 그 
    직원에 맞춰서 새로 만들어야 할 것이다. 하지만 유니폼을 기성복으로 결정한 경우에는 직원에 따라 조금 크기도
    하고 조금 작기도 하겠지만 하나의 사이즈로 보다 많은 직원들에게 지급을 할 수 있게 된다.

     

    다시 말해 기존 직원들(훈련 데이터) 각자에게 꼭 맞는 유니폼을 만들게 되면 새로운 직원이 들어왔을 때 다시 치수를
    조정한 유니폼을 만들어야 한다. 하지만 기성복으로 몇개의 사이즈를 준비해 놓는다면 새로운 직원이 들어와도
    미리 준비한 유니폼을 지급만 하면 될 것이다.

     

    이렇듯 보다 일반적인 상황에 적용을 하기 위해서는 과대적합을 피하는 것이 좋다. 다만 과대적합을 피하기 위한
    노력이 과하게 되면 오히려 과소적합에 빠질 수 있으니 주의해야 한다.

    일반적으로 모델이 너무 복잡한 경우 과대적합에 빠지기 쉽고 모델이 너무 단순한 경우 과소적합에 빠지기 쉬운데
    이를 편향/분산 트레이드 오프라고 한다 (핸즈온 머신러닝 179쪽 참조). 

     

    규제란?

     

    규제란 말 그대로 어떤 한도를 정하고 그 한도를 넘지 못하도록 제한하는 것을 말한다. 우리가 이제까지 보아 온 선형
    회귀 모델은 기울기(가중치)와 절편(bias)을 가지고 있었다. 이 모델은 기울기와 절편을 적절히 조절하고 조합해서
    최적의 결과를 찾게 되는데 이 때 기울기를 0으로 만들게 되면 조절할 수 있는 대상이 절편 하나로 줄어 제약이 발생한다.
    이렇게 조절할 수 있는 대상을 줄여가는 것을 규제라고 한다. 다만 대상을 완전히 없애는 것은 너무 극단적이므로 규제의
    대상이 적절한 범위 내의 값만을 갖도록 하는 것이 일반적이다.

     

    여기서 규제의 대상이 되는 것은 일반적인 선형 회귀에서는 기울기(가중치)이며 다항회귀의 경우에는 특성의 차수이다
    (하지만 특성의 차수를 제한 하는 것도 결국은 높은 차수의 특성에 붙어 있는 가중치를 0으로 만듦으로써 가능하니 
    결국은 규제라 함은 가중치를 제약 하는 것으로 보면 되겠다). 

     

    이러한 규제를 가하는 경우는 대체로 2가지 정도의 경우가 있는데 한가지는 확보한 데이터가 충분하지 않을 경우이고
    다른 한가지는 일부 특성들 간에 밀접한 상관 관계가 있을 경우이다. 이를 다중공선성이라 한다. 

     

    이렇듯 규제를 가하는 것은 특성을 조절하고자 할 때 사용하는 방법이다 따라서 편향에 대해서는 규제를 가하지 않고
    특성의 가중치에 대해서만 규제를 가하게 된다. 

     

    다음과 같이 생각해보자.
    선형 회귀의 비용 함수를 떠올려보면 훈련 데이터에서 특성 𝑥와 실제 결과 𝒚는 이미 주어진 값이며 가중치 𝜭를 변경해
    가면서 최솟값을 찾는데 비용함수가 최솟값이 되었을 때 가중치 𝜭는 최적값이 된다. 이렇게 모든 특성에 대해 가중치가
    최적값일 경우를 바로 과대적합이라고 볼 수 있는 것이다. 결국 가중치에 규제를 가하여 과대적합을 바로잡을 수 있는 
    것이다.

     

    일반적으로 가중치에 규제를 가하기 위해서는 비용 함수 뒤에 가중치 벡터의 크기(norm)을 더하게 되는데 바로 다음과 
    같은 형태가 된다.

     

     

    최솟값 = 비용함수 + 가중치 벡터의 크기

     

     

    비용함수에 대해서는 이미 알고 있고…뒤에 따라붙는 가중치 벡터의 크기는 무엇을 의미하는가?
    그냥 단순히 식을 분석해도 알 수 있지만 위의 식은 비용함수가 최솟값이 되어야 한다는 조건에 추가적으로 가중치의
    크기도 최소가 되어야 한다는 제약이 붙게 되는 것이다. 당연한 이야기지만 덧셈식이니 두 항이 모두 최소가 될 때
    식의 결과도 최소가 되는 것이 아니겠는가?

     

    그럼 구체적인 방법들을 살펴보면서 조금 더 자세하게 알아보도록 하자.

     

    릿지 회귀

     

    릿지 회귀는 비용함수 뒤에 규제항을 더하여 과대적합을 막는 방법으로 이 때 사용되는 규제항은 다음과 같다.

     

    릿지 회귀 규제항

     

    릿지 회귀 비용 함수

     

    이 규제항은 벡터의 길이나 크기를 나타내는 norm 중 L2 norm의 제곱을 2로 나눈 것에 해당한다(여기서 2로 나눈 것은
    비용함수에서와 마찬가지로 미분 결과를 간단히 만들기 위해서이다). 또한 앞에 붙은 𝜶는 그 값에 따라 규제의 정도가 
    정해지는 하이퍼파라미터로 예를들어 이 값이 0이라면 규제항 전체가 0이 되어 기존 선형 회귀의 비용함수만 남게되어 
    규제를 적용하지 않는다는 의미가 되는 것이다.

     

    직관적으로 확인하기 위해 직접 식을 만들고 값을 대입하여 정리를 해보자(아래 정리한 내용은 말 그대로 감을 익히자는
    의도일 뿐 실제로 이런 식으로 계산이 진행되지는 않는다. 또한 여기서는 미분까지 진행하지 않으므로 1/2는 뺐다). 

    아래와 같은 가설 함수가 있다.

     

    가설 함수

     

    이 가설함수에 대한 릿지 회귀 비용함수는 다음과 같다(w = 𝜭 이다).

     

    릿지 회귀 비용 함수

     

    여기서 임의로 𝑥1 = 3, 𝑥2 = 2, 𝑦 = 14라는 값을 정하고 𝜭 1과 𝜭 2를 구해보자. 다양한 𝜭 1과 𝜭 2의 조합을 만들어
    낼 수 있다. 아래는 그 중 몇가지 𝜭 1과 𝜭 2의 조합을 표로 만들어 본 것이다. 만일 일반적인 선형 회귀였다면 선형 회귀의
    비용함수가 0이되는 조합이 선택이 되었을 것이다(주황색 셀). 하지만 규제항이 더해짐으로 해서 최종적인 최솟값을
    만들어내는 조합은 이전 조합과 달라진다(초록색).

     

     

    또한 우연인지 필연인지…이 표에서 보면 가중치 𝜭 1과 𝜭 2가 전체적으로 균등한 크기로 작어지는 효과를 보이고 있다.
    이렇게 릿지 회귀의 경우 규제항이 제곱 함수로 가중치의 크기가 커질수록 비용함수의 결과값에 영향을 많이 미치게 된다.
    따라서 값이 큰 가중치를 제한하는 효과가 있다.

     

    라쏘 회귀

     

    라쏘 회귀 역시 비용함수 뒤에 규제항을 더하는 것은 릿지 회귀와 동일하다 다만 규제항의 식은 다음과 같다.

     

    라쏘 회귀 규제항

     

    라쏘 회귀 비용 함수

     

    이 규제항은 벡터의 길이나 크기를 나타내는 norm 중 L1 norm에 해당한다. L1 norm은 벡터를 구성하는 요소의
    절대값의 합으로 여기서도 가중치 𝜭1와 𝜭2의 절대값의 합으로 계산하면 된다. 또한 라쏘 회귀의 규제항 같은 경우
    1차 함수로 미분 불가능한 형태이기 때문에 2로 나누는 부분이 없다.

     

    위의 릿지 회귀에서와 동일한 조건으로 계산식을 구성해보면 다음과 같다.

     

     

    릿지 회귀에서와 마찬가지로 라쏘 회귀의 경우에도 규제항이 더해짐으로 해서 단순 선형 회귀와는 다른 조합의 가중치
    벡터가 선택이 된다. 릿지 회귀와 다른 점이라면 앞서 언급했듯이 규제항이 가중치 벡터 요소들의 절대값의 합이라는
    점이다. 이러한 이유로 규제항을 최소화 시키는 방향이 크기가 작은 가중치는 0으로 만들어버리는 경향이 있어 특성의
    일부만을 선별하는 효과를 갖게 된다. 

     

    엘라스틱넷

     

    앞서 보았듯이 릿지 회귀와 라쏘 회귀는 가중치를 다루는 방법이 조금 다르다. 다시 말해 각각의 용도가 다르다고 할 수
    있는 것이다. 이렇게 서로 다른 상황에 사용될 수 있는 두 제약을 하나의 식으로 묶어 어떤 상황에서든 유연하게 사용할 수
    있도록 만든 것이 바로 엘라스틱넷이다. 엘라스틱넷의 비용함수는 다음과 같다.

    엘라스틱넷 비용함수

     

    여기서 중요한 역할을 하는 것이 바로 r이라는 하이퍼파라미터로 릿지 회귀와 라쏘 회귀의 비중을 결정하는 파라미터이다.
    극단적으로 r이 1이라면 라쏘 회귀로 작동하며 r이 0이라면 릿지 회귀로 작동하게 되는 것이다.

     

    이렇게 3가지 형태의 규제가 있는 회귀 모델을 살펴보았는데 ‘핸즈온 머신러닝’을 인용하여 정리하면 다음과 같다.

     

    1. 일반 선형 회귀는 가급적 사용하지 않는 것이 좋다.
    2. 일반적으로는 릿지 회귀가 좋다.
    3. 실제로 영향을 미치는 특성이 몇 개 정도라면 라쏘 회귀나 엘라스틱넷이 좋다.
    4. 특성 수가 훈련 샘플 수보다 많거나 특성 몇 개가 강하게 연관되어 있다면 엘라스틱넷이 좋다. 

     

     

    조기 종료

     

    위에 언급한 규제 모델 외에 반복적인 학습 알고리즘에서 사용할 수 있는 방법으로 조기 종료가 있다. 이 방법은 검증
    에러가 최소에 도달하는 즉시 훈련을 멈추는 것을 말한다. 조기 종료는 매우 효과적이지만 간단하게 구현할 수 있다.
    (핸즈온 머신러닝 P186 참조)

     

    정리

     

    개념적으로는 이해가 가는 내용이지만 이 것을 어떻게 증명해야 하는지가 막연했던 내용들이다. 마치 공부 잘하는 녀석
    들이 훌륭하게 공식을 이용하여 문제를 풀어낼 때 나같은 수포자들은 숫자를 하나 하나 대입해가면서 답을 찾아가듯이
    말이다…그러다보니 몇쪽 안되는 이 부분을 정리하는데 거의 한 달이 걸렸다(그러고서도 충분하지 않다…ㅠ.ㅠ).

    일단 진도는 뽑아야겠기에 이정도 선에서 마무리를 하고 다음 내용으로 넘어가야겠다.

     

    숙제

     

    1. 릿지 회귀와 라쏘 회귀의 특징을 시각적으로 볼 수 있도록 python으로 구현해보자! 

    2. 책(핸즈온 머신러닝)에 나온 관련 그래프들을 정확하게 이해해보자!

    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^



    2018/11/25 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 1 - 특성의 scale

    2018/12/10 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 2 - step 공식 이해하기

    2019/01/28 - [Study/인공지능학습] - [머신러닝 Reboot] 개념잡기 : 경사 하강법 3 - 경사 하강법의 종류

    2019/04/07 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사 하강법 4 - 규제가 있는 선형 모델

    2019/04/14 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사하강법5 - 로지스틱 회귀


    머신러닝 Reboot - 개념 잡기 : 경사 하강법 3 - 경사 하강법의 종류


    지난 포스팅에서 경사 하강법의 특성 스케일과 스텝에 대해서 알아보았다.
    여전히 이해도가 낮은데다가 너무 세세한 부분까지 이해하려고 하다보니 진도가 잘 안나간다.
    그렇다고 뭔가를 확실히 알게된 것도 아니고…ㅠ.ㅠ 그저 ‘아~ 그래서 이럴 것 같다…’라는 느낌적인 느낌을
    갖게 된 것 정도가 소득이라면 소득일까…


    아직은 계란으로 바위치는 기분이지만 그래도 학습의 끈을 놓을 수는 없다.
    그래서 오늘도 정리는 계속된다~


    오늘은 경사 하강법의 종류에 대해 정리해보도록 하자.


    이 포스팅에 사용된 코드는 모두 핸즈온 머신러닝(오렐리앙 제롱 저, 박해선 역, 한빛미디어 출판)에서
    가져왔거나 일부 수정한 코드입니다.



    경사 하강법 복습


    경사 하강법이란 비용 함수를 최소화 할 수 있는 가중치(𝜭)를 찾아가는 과정으로 그 방법은 비용 함수를 가중치(𝜭)에 
    대해 미분하고 이 결과가 작아지는 방향으로 반복해서 진행을 하는 것이다. 이 때 매 반복이 각 단계 사이에 얼마만큼의 
    거리를 둘 것인지 하는 학습률이 매우 중요한데 학습률이 너무 높으면 중구난방 발산을 하게 되고 반대로 너무
    낮으면 시간이 오래 걸려 최솟값에 도달하지 못할 수 있다.


    학습률 적절학습률 적절학습률 낮음학습률 낮음학습률 높음학습률 높음


    경사 하강법은 매우 일반적이고 단순한 알고리즘이지만 함수의 그래프가 단 한개의 오목한 점을 가질 때(convex)만
    잘 작동을 한다. 함수의 그래프에 오복한 부분이 많은 경우(non-convex) 초기화가 어떻게 되느냐에 따라 지역 
    최솟값에서 멈출 수 있다. 



    또한 지난 포스팅에서 본 것처럼 특성의 스케일에 민감하여 2개 이상의 특성이 있을 때 각 
    특성간의 스케일이 크게 차이가 나면 최솟값에 도달하는데 오랜 시간이 걸리게 된다.


    참고 : 특성의 스케일 (https://mazdah.tistory.com/833)


    선형 회귀의 경우 비용함수의 그래프는 전역 최솟값 하나만 존재하는 그래프이기 때문에 경사 하강법은 좋은 방법이
    될 수 있으며 특성의 스케일만 적절하게 조정을 해주면 될 것이다. 


    이러한 경사 하강법은 훈련세트의 범위를 어떻게 지정하여 계산하느냐에 따라 배치 경사 하강법, 확률적 경사 하강법,
    미니 배치 경사 하강법으로 나누어 볼 수 있다. 


    배치 경사 하강법


    배치 경사 하강법은 매 스텝에서 전체 훈련 데이터를 사용하여 계산을 한다. 따라서 매우 큰 훈련 세트에서는 성능이 
    떨어지게 된다. 


    배치 경사 하강법에 대해서는 이전 포스팅에서 상세하게 다루었으므로 아래 링크를 참조하기 바란다.


    참고 : 배치 경사 하강법의 STEP (http://mazdah.tistory.com/834)


    아래는 핸즈온 머신러닝(이하 책)에 실린 예제 코드이다. 그래프 그리는 부분을 약간 수정했다. 라이브러리 import와
    데이터 생성 부분은 책을 참조하시기 바란다.


    # 다른 경사 하강법과의 비교를 위해 theta값을 저장할 배열 theta_path_bgd = [] eta = 0.1 # 학습률 n_iterations = 1000 # 반복 횟수 m = 100 # 샘플 수 theta = np.random.randn(2, 1) # theta 초기화 for iteration in range(n_iterations): gradient = 2/m * X_b.T.dot(X_b.dot(theta) - y) theta = theta - eta * gradient theta_path_bgd.append(theta) # 처음 10번의 step에 대한 그래프와 마지막 step의 그래프를 그리기 위한 코드 if iteration <= 10 or iteration == 999: X_new = np.array([[0], [2]]) X_new_b = np.c_[np.ones((2, 1)), X_new] y_predict = X_new_b.dot(theta) y_predict style = "g--" if iteration == 999: style = "r-" plt.plot(X_new, y_predict, style) plt.plot(X, y, "b.") plt.axis([0,2,0,15]) plt.show() theta ########################################################## # API 설명 # ########################################################## # np.ones((2,1))는 파라미터로 전달된 shape의 배열을 1로 채운다. (기본 데이터 타입은 실수로 1.으로 채워진다.) # np.c_는 파라미터로 전달된 2개의 배열에 대해 동일행의 요소를 배열로 합친 (첫 번째 파라미터의 요소는 0열, 두 번째 파라미터의 요소는 1열) 새로운 배열을 만든다. # 예) # >>> np.c_[[np.array([1,2,3]), np.array([4,5,6])]] # array([[1, 4], # [2, 5], # [3, 6]]) # >>> np.c_[np.array([[1,2,3]]), 0, 0, np.array([[4,5,6]])] # array([[1, 2, 3, 0, 0, 4, 5, 6]])


    책에는 처음 10번의 예측 그래프만을 그리고 있으나 배치 경사 하강법 마지막에 설명한 반복 횟수에 대한 설명을 확인
    해보고자 마지박 반복 그래프를 하나 더 추가하였다. 처음 10번의 그래프는 초록색 대시로, 마지막 그래프는 빨간 실선
    으로 표시했다.


    배치 경사 하강법배치 경사 하강법


    그래프를 보면 알 수 있듯이 10번째 step의 그래프와 마지막 step의 그래프 사이에는 큰 차이가 없다. 결국 990번은
    큰 의미가 없는 step이었다는 뜻인데, 이는 책에서 언급한 바와 같이 반복 횟수의 지정 또한 중요하다는 것을 의미한다.


    확률적 경사 하강법


    확률적 경사 하강법은 배치 경사 하강법이 훈련 세트가 커짐에 따라 느려지는 단점을 보완한다.


    확률적 경사 하강법은 매 스텝에서 무작위로 선택한 한 개의 샘플에 대해서만 그래디언트를 계산하므로 속도가 빠를 뿐만
    아니라 메모리 사용에 있어서도 효율이 좋아 매우 큰 훈련 세트도 적용 가능하다. 또한 불규칙성이 지역 최솟값을 건너
    뛰게 하여 전역 최솟값을 찾을 가능성을 높여주는 것도 장점으로 볼 수 있다.


    다만 최적값에 도달하는 과정이 배치 경사 하강법에 비해 불안정한 것이 단점이다. 또한 최적값에 근접은 하겠지만
    말 그대로의 최적값은 얻기 힘들다. 이러한 단점을 해결하는 방법으로 학습률을 조정하는 방법이 있다. 처음에는 학습률을
    크게 했다가 점차 학습률을 줄여나가는 방법으로 이 때 학습률을 조정하는 함수를 학습 스케쥴(learning schedule)이라
    부른다.


    이렇게 확률적 경사 하강법에서는 학습률자체의 크고 작음 뿐만 아니라 학습률을 줄여나가는 속도 역시 중요한 하이퍼 
    파라미터가 된다. 너무 빨리 줄여나가면 최솟값에 도달하지 못하고 너무 천천히 줄여나가면 시간이 오래 걸리거나 지역
    최솟값에 머물 수 있다.


    또다른 특징(단점)으로는 훈련세트를 한 번 도는 동안(1 epoch) 여러번 선택되는 샘플과 한 번도 선택되지 않는 샘플이 
    존재할 수 있는데 이를 방지하기 위해 매 epoch에서 훈련 세트를 섞는 작업을 추가할 수 있다. 다만 이렇게 하면 속도가
    느려지게 된다.


    # 다른 경사 하강법과의 비교를 위해 theta값을 저장할 배열 theta_path_sgd = [] # 확률적 경사 하강법 n_epochs = 50 t0, t1 = 5, 50 # 학습 스케쥴 하이퍼파라미터 # 학습률을 감소시키는 학습 스케쥴 함수 def learning_schedule(t): return t0 / (t + t1) theta = np.random.randn(2,1) for epoch in range(n_epochs): # epoch : 훈련세트 전체가 한 차례 반복 되는 단위. 50번 반복될 동안 전체 샘플 m만큼 # 반복되므로 50 epoch를 실행한다. for i in range(m): # 첫 번째 epoch에서 처음 20개의 샘플에 대한 그래프와 최종 그래프를 그림. if (epoch == 0 and i < 20) or (epoch == 49 and i == 99): y_predict = X_new_b.dot(theta) # 처음 그래프는 초록색 대시로, 나머지 그래프는 파란색 실선으로, 마지막 그래프는 # 빨간색 실선으로 표시한다. style = "" if epoch == 0 and i > 0: style = "b-" elif epoch == 0 and i == 0: style = "g--" elif epoch == 49 and i == 99: style = "r-" plt.plot(X_new, y_predict, style) # 0부터 99(m은 100개) 중에 하나를 랜덤하게 가져옴 random_index = np.random.randint(m) # 무작위로 선택된 인덱스를 확인해보자. 특정 인덱스는 중복해서 사용되고 또 다른 인덱스는 # 사용되지 않는다. if epoch == 0: print("random_index = ", random_index) # 훈련세트와 실제값의 배열에서 랜덤하게 가져온 인덱스 위치의 값 1개씩만 가져온 후 # gradient를 계산한다. xi = X_b[random_index:random_index+1] yi = y[random_index:random_index+1] gradients = 2 * xi.T.dot(xi.dot(theta) - yi) # 배치 경사 하강법과 달리 학습률을 계속해서 감소시킨다. eta = learning_schedule(epoch * m + i) # print("eta : ", eta) theta = theta - eta * gradients theta_path_sgd.append(theta) plt.plot(X, y, "b.") plt.xlabel("$x_1$", fontsize=18) plt.ylabel("$y$", rotation=0, fontsize=18) plt.axis([0,2,0,15]) plt.show() ########################################################## # API 설명 # ########################################################## # # Numpy random # # - randn(d0, d1, ..., dn) : 표준 정규분포를 따르는 무작위 실수를 파라미터로 전달받 차수의 배열에 # 채워 리턴한다. 파라미터가 없는 경우 한 개의 실수만 리턴한다. # 예) # >>> theta = np.random.randn(2,1) # >>> theta # array([[ 0.44730268], # [-0.04431121]]) # # - randint(low, high=None, size=None, dtype='l') : 무작위 정수를 리턴한다. low 파라미터는 # 필수로 low 파라미터만 있는 경우에는 0부터 low-1까지의 범위에서 무작위 정수가 리턴되며 # low와 high가 전달되는 경우 low부터 high-1까지의 범위에서 무작위 정수가 리턴된다. # size가 주어질 경우 size에 해당하는 배열 형태로 리턴된다. # 예) # >>> rival = np.random.randint(2) # >>> rival # 1 (0또는 1이 무작위로 출력됨) # >>> rival = np.random.randint(2, 5) # >>> rival # 1 (2 ~ 4 사이의 정수가 무작위로 출력됨) # >>> rival = np.random.randint(2,5, (2,3)) # >>> rival # array([[3, 3, 4], (2 ~ 4 사이의 정수가 2행 3열의 배열에 무작위로 할당되어 출력됨) # [3, 2, 3]])

    확률적 경사 하강법


    동일한 내용을 Scikt Learn으로는 다음과 같이 구현 가능하다.


    from sklearn.linear_model import SGDRegressor # epoch는 50, 초기 학습률은 0.1로 학습한다. sgd_reg = SGDRegressor(max_iter=50, penalty=None, eta0=0.1) sgd_reg.fit(X, y.ravel()) # 결과값 보기 : intercept_ = 편향, coef_ = 가중치 sgd_reg.intercept_, sgd_reg.coef_ ########################################################## # API 설명 # ########################################################## # # Scikit-learn # # - SGDRegressor (loss=’squared_loss’, penalty=’l2’, alpha=0.0001, l1_ratio=0.15, # fit_intercept=True, max_iter=None, tol=None, shuffle=True, # verbose=0, epsilon=0.1, random_state=None, # learning_rate=’invscaling’, eta0=0.01, power_t=0.25, # warm_start=False, average=False, n_iter=None) # . 확률적 경사 하강법을 수행하는 클래스로 linear_model 모듈에 포함되어있다. # . 파라미터 (상당히 많은 파라미터가 있는데 예제에 명시한 파라미터만 간단히 알아보자) # : max_iter - 전체 훈련 데이터의 반복 횟수(epoch 수)를 지정한다. # : penalty - 정규화 식. 여기서는 사용하지 않음. l2, l1, elasticnet 등을 사용할 수 # 있다. # : eta0 - 학습률의 초깃값. API상에 learning_rate라는 파라미터는 학습 스케쥴을 뜻한다. # 기본값인 ‘invscaling’는 eta = eta0 / pow(t, power_t) 공식에 따라 # 학습률을 조정한다. (자세한 내용은 핸즈온 머신러닝 171쪽 하단의 역주 참조)



    미니배치 경사 하강법


    미니배치 경사 하강법은 한 epoch에서 미니배치라는 임의의 샘플 세트만으로 계산을 진행하는 것으로 확률적 경사 
    하강법이 1개의 샘플만 사용하여 계산하는 것과 비교된다. 한편으로는 확률적 경사 하강법은 미니배치의 크기가 1인
    미니배치 경사 하강법으로 볼 수도 있다.


    이러한 미니배치 경사 하강법은 확률적 경사 하강법에 비해 안정적으로 최솟값에 접근하지만 지역 최솟값을 벗어나기는
    더 힘들다.


    장점으로는 행렬 연산에 최적화 되어있으며 GPU를 통해 큰 성능 향상을 얻을 수 있다.


    # 다른 경사 하강법과의 비교를 위해 theta값을 저장할 배열 theta_path_mgd = [] n_iterations = 50 # 미니배치 크기를 20으로 주었다. # 이 크기를 1로 주면 확률적 경사 하강법과 동일한 형태의 그래프를 볼 수 있다. minibatch_size =20 np.random.seed(42) theta = np.random.randn(2,1) # 무작위 초기화 t0, t1 = 200, 1000 def learning_schedule(t): return t0 / (t + t1) t = 0 for epoch in range(n_iterations): # 인자로 전달된 벡터 또는 행렬 요소의 순서를 무작위로 바꿈. # 행렬의 경우 첫 번째 인덱스의 순서만 바꾼다. # 매 epoch마다 순서를 뒤섞어 훈련 세트를 고르게 사용하도록 한다. shuffled_indices = np.random.permutation(m) X_b_shuffled = X_b[shuffled_indices] y_shuffled = y[shuffled_indices] # minibatch_size만큼 샘플을 뽑아 계산하므로 for문에서도 minibatch_size만큼씩 # 증가시킨다. for i in range(0, m, minibatch_size): #print(epoch , " : ", i) # 그래프 그리기. minibatch_size가 20이므로 한 epoch에서 5번만에 연산이 끝난다. # 따라서 2 epoch가 진행될 동안 총 10개의 그래프가 그려진다. if epoch < 2 and i < 100: y_predict = X_new_b.dot(theta) style = "b-" if i > 0 else "r--" plt.plot(X_new, y_predict, style) t += 1 # 매 epoch마다 뒤섞은 훈련 세트에서 minibatch_size만큼의 샘플만을 가져와서 # gradient를 계산한다. xi = X_b_shuffled[i:i+minibatch_size] yi = y_shuffled[i:i+minibatch_size] gradients = 2/minibatch_size * xi.T.dot(xi.dot(theta) - yi) eta = learning_schedule(t) theta = theta - eta * gradients theta_path_mgd.append(theta) plt.plot(X, y, "b.") plt.xlabel("$x_1$", fontsize=18) plt.ylabel("$y$", rotation=0, fontsize=18) plt.axis([0,2,0,15]) plt.show() ########################################################## # API 설명 # ########################################################## # # Numpy random # # - permutation(x) : 파라미터로 주어진 배열의 순서를 무작위로 바꾸어 출력한다. # 예) # >>> x = np.random.permutation([1,2,3,4,5,6,7,8,9,10]) # >>> x # array([ 7, 2, 1, 4, 3, 8, 9, 5, 10, 6])



    미니배치 크기 20미니배치 크기 20미니배치 크기 1미니배치 크기 1


    위의 그래프를 보면 확률적 경사 하강법의 그래프보다 미니배치 경사 하강법에서 미니배치를 1로 준 그래프가 좀 더
    발산이 심한데 이는 아마도 하이퍼파라미터가 달라서 발생하는 현상일 것이다.


    비교


    마지막으로 3개의 경사하강법을 실행하면서 저장한 theta값의 배열을 기반으로 그래프를 그려보면 아래와 같다.



    배치 경사 하강법이 곧장 최솟값으로 향한 반면 확률적 경사 하강법과 미니배치 경사 하강법은 최솟값 부근에서 매우
    불안정하게 움직인다. 하지만 앞서 살펴본대로 배치 경사 하강법은 샘플이 많아질수록 느려지고 확률적 경사 하강법과
    미니배치 경사 하강법도 적절한 하이퍼파라미터를 사용하면 결국은 최솟값에 도달한다.


    마지막으로 각 경사 하강법의 특징을 정리하면 다음과 같다.


    배치 경사 하강법 : 샘플 수가 클 때 느림, 특성 수에 상관없이 빠름, 특성 스케일 조정 필요

    확률적 경사 하강법 : 샘플 수에 상관없이 빠름, 특성 수에 상관없이 빠름, 특성 스케일 조정 필요

    미니배치 경사 하강법 : 샘플 수에 상관없이 빠름, 특성 수에 상관없이 빠름, 특성 스케일 조정 필요


    더 상세한 비교 내용은 핸즈온 머신러닝 172쪽을 참고하라


    다항 회귀


    다항회귀는 데이터가 비선형인 경우(2차 이상의 함수 형태를 띠는 경우) 사용할 수 있는 회귀 분석이다.
    다항회귀의 경우 직관적으로 접근하는 것이 좀 더 이해하기가 쉬운데 데이터의 그래프를 보고 적절한 차수를
    제곱한 특성을 추가하여 분석하는 것이다.


    비선형의 데이터를 선형 회귀 분석으로 계산할 수 있는 이유는 선형의 의미가 가설함수의 독립변수에 대한 것이 아니라
    가중치 𝜭에 대한 것이기 때문이다. 즉 𝜭에 대한 1차 식(선형 식)으로 풀어낼 수 있다는 의미인 것이다.


    이렇게 봤을 때 결과론적이긴 하지만 아래 코드를 보면 결국 이 예제의 가설함수는 위에 언급한 것과 같이 𝜭를 기준으로
    본다면 특성이 2개인 함수일 뿐이다. 다만 기존에는 서로 독립적인 특성들, 즉 x1, x2, x3…xn에 대해 다뤘다면 다항
    회귀에서는 x, x^2, x^3…x^n과 같이 하나의 특성 x에 대해 그 거듭제곱들이 특성이 되는 점이 다르다고 할 수있다.
    결국 다항회귀는 다중회귀의 특수한 형태라고 볼 수 있는 것이다.


    다만 우리가 훈련세트를 받았을 때 특성은 오직 x 하나만 주어지기 때문에 추가적인 특성(x의 거듭제곱)을 어떻게
    처리할 것인지에 대해서는 선택이 쉽지 않은데 처음 이야기한 바와 같이 직관에 의존해서 결정을 할 수도 있겠지만
    너무 차수가 높은 특성이 추가되는 경우 과대적합에 빠지기 쉬운 문제가 있다. 때문에 보통은 교차 검증을 사용하거나
    학습 곡선을 살펴 결정하게 된다.


    import numpy as np import numpy.random as rnd # 맷플롯립 설정 %matplotlib inline import matplotlib import matplotlib.pyplot as plt from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline # 각각 차수를 300, 2, 1로 하여 그래프를 그려본다. 튜플의 앞 두 개의 요소는 그래프 스타일이다. for style, width, degree in (("g-", 1, 300), ("b--", 2, 2), ("r-+", 2, 1)): # 특성을 추가한다. 300차, 2차, 1차를 적용해본다. polybig_features = PolynomialFeatures(degree=degree, include_bias=False) # 표준화 인스턴스를 만든다. std_scaler = StandardScaler() # 추정기로 선형 회귀를 사용한다. lin_reg = LinearRegression() # 2개의 변환기와 1개의 추정기로 구성된 Pipeline을 만든다. polynomial_regression = Pipeline([ ("poly_features", polybig_features), ("std_scaler", std_scaler), ("lin_reg", lin_reg), ]) # Pipeline 실행 polynomial_regression.fit(X, y) y_newbig = polynomial_regression.predict(X_new) plt.plot(X_new, y_newbig, style, label=str(degree), linewidth=width) plt.plot(X, y, "b.", linewidth=3) plt.legend(loc="upper left") plt.xlabel("$x_1$", fontsize=18) plt.ylabel("$y$", rotation=0, fontsize=18) plt.axis([-3, 3, 0, 10]) #save_fig("high_degree_polynomials_plot") plt.show() ########################################################## # API 설명 # ########################################################## # # Scikit-learn # # - LinearRegression(fit_intercept=True, normalize=False, copy_X=True, n_jobs=1) # . linear_model 모듈에 있는 클래스로 최소제곱법을 통한 선형회귀를 수행한다. # . 파라미터 (모든 파라미터는 기본 값이 있는 optional이다.) # : fit_intercept - 연산에 편향(bias)를 포함 시킬 것인지를 결정한다. # : normalize - 특성을 정규화 할 지 여부를 설정한다. fit_intercept가 False이면 # 무시된다. # : copy_X - True면 특성을 복사하고 False면 특성을 덮어씌운다. # : n_jobs - 수행할 job의 수로 사용할 CPU의 수라고 생각하면 된다. -1이면 모든 CPU를 # 사용한다. # # - PolynomialFeatures(degree=2, interaction_only=False, include_bias=True) # . 파라미터로 설정된 차수(degree)와 같거나 작은 차수의 모든 다항식 조합을 특성에 추가한다. # 차수가 2이고 전달된 특성이 [2, 3]라면 [2, 3, 2^2, 2 * 3, 3^2]이 생성된다. # 이 때 사용되는 특성은 2D 이상의 배열이어야 한다. 즉 [[2, 3]] 형태여야 한다. # . 파라미터(모든 파라미터는 기본 값이 있는 optional이다.) # : degree - 다항식의 차수 # : interaction_only - True로 설정되면 제곱수가 포함된 수는 모두 빠지고 원래의 특성 # 값들과 특성들의 곱만 포함된다. 즉 [[2,3]]에 대해 [2, 3, 6]이 # 출력된다. # : include_bias - 편향을 포함할 것인지를 설정한다. # # - Pipeline(steps, memory=None) # . list 타입으로 전달된 첫 번째 파라미터의 요소들을 순차적으로 실행한다. 이 때 앞 단계의 결과가 # 다음 다계의 입력으로 들어간다. 이 파라미터에는 반드시 변환기가 있어야 하며 변환기는 fit() # 함수와 transform() 함수가 구현되어 있어야 한다. Pipeline의 마지막에는 추정기가 실행되며 # 추정기는 fit() 함수만 구현되어 있으면 된다. # * 추정기와 변환기에 대해서는 핸즈온 머신러닝 101쪽의 "사이킷런의 설계 철학"을 참고하라. # ** Pipeline에 대해서는 핸즈온 머신러닝 108쪽 "2.5.5 변환 파이프라인"을 참조하라 # . 파라미터 # : steps - list 타입이어야 하며 변환기와 추정기가 포함되어있어야 한다. 각 요소는 # 이름/추정기(변환기) 쌍으로 되어있으며 마지막 단계는 추정기나 변환기를 모두 # 사용할 수 있지만 이전 단계는 모두 변환기여야 한다. # : memory - 변환기의 캐시 사용 여부를 결정한다. # # - StandardScaler(copy=True, with_mean=True, with_std=True) # . 특성의 평균을 뺀 후 표준편차로 나누어 표준화 해주는 클래스. 평균이 0 표준편차가 1인 # 정규분포로 표준화 한다. # * 자세한 내용은 핸즈온 머신러닝 107쪽 "2.5.4 특성 스케일링" 하단의 표준화를 참조하라. # . 파라미터 # : copy - 복사본 사용 여부를 결정한다. # : with_mean - 스케일링 하기 전에 데이터의 중간을 맞춰준다(의미를 잘 모르겠음...ㅠ.ㅠ). # : with_std - 분산 혹은 표준편차에 맞게 스케일링한다(역시 명확한 의미를 모르겠음...ㅠ.ㅠ).


    다항회귀다항회귀


    위 그래프를 보면 데이터의 형태가 아래로 오목한 2차 함수의 형태와 유사하다(물론 책의 전개상 데이터를 생성한 
    함수가 2차 함수에 노이즈를 추가한 것임을 알고 있으나 실제로는 이 함수를 찾아내는 것이 머신러닝의 역할이다).
    따라서 제공된 특성을 제곱한 값으로 특성을 추가한 예측 그래프가 가장 적절해 보인다. 차수를 300으로 올리면
    보다 많은 데이터와 일치하게 되지만 너무 훈련 데이터에 과대적합된 형태라 볼 수 있다.


    일반적으로 과소적합인 경우에는 더 복잡한 모델을 사용해야 하며, 과대적합인 경우에는 더 많은 훈련 샘플을 추가해야
    한다.


    정리


    가장 기초단계라고 할 수 있는 선형 회귀 이지만 꾸역꾸역 깊이 파고들다보니 역시 수학적인 요소들로 가득 차있다.
    현 상황에서야 굳이 각 API들의 세부적인 부분들까지 알 필요 없이 기본적인 사용법만으로 충분하겠으나 실무에
    적용하기 위해서는 분명 파라미터들의 의미를 이해하고 자유자재로 사용할 수 있어야 할 것이다.


    다만 Scikit Learn의 경우 국내에서는 텐서플로우나 케라스에 밀려 인기가 없는 탓인지 한글로 된 자료 찾기가 쉽지
    않았다. 우선은 공식 문서를 이용해 공부를 해야겠으나 우리말로도 못알아듣는 수학적 내용들을 영어로 어찌 이해할 수 
    있을지…ㅠ.ㅠ 일단 Scikit Learn의 공식 문서를 링크한다.


    https://scikit-learn.org/0.19/_downloads/scikit-learn-docs.pdf


    선형회귀의 종류에 대해서는 이정도로 정리하고 다음 포스팅에서는 규제가 있는 선형 모델에 대해 간단하게 알아보도록

    하겠다.

    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^


    2018/11/25 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 1 - 특성의 scale

    2018/12/10 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 2 - step 공식 이해하기

    2019/01/28 - [Study/인공지능학습] - [머신러닝 Reboot] 개념잡기 : 경사 하강법 3 - 경사 하강법의 종류

    2019/04/07 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사 하강법 4 - 규제가 있는 선형 모델

    2019/04/14 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사하강법5 - 로지스틱 회귀


    머신러닝 Reboot - 개념 잡기 : 경사 하강법 2 - step 공식 이해하기


    지난 시간에는 어설프게나마 경사 하강법이 왜 특성에 민감한지 그래프를 통해 알아보았다. 여전히 논리적으로 설명하기 
    힘든 부분이 있어 아쉬움이 남지만 직관적으로 봤을 때도 경사 하강법을 수행하기 위해서는 일단 특성들의 스케일을 
    맞추는 것이 좋다는 것은 알게 되었다.


    오늘은 이어서 어떤식으로 다음 기울기를 찾아 움직이는지 그 과정을 공식을 통해 알아보자.
    이번 정리는 오로지 “핸즈온 머신러닝”의 166쪽에 있는 ‘식 4-7 경사 하강법의 스텝’을 이해하기 위한 것이다.


    선형 회귀 관련 공식 복습 - 가설 함수와 비용 함수


    우선 복습 차원에서 선형 회귀의 가설함수와 비용 함수를 다시 한 번 보자. 여러 표현 방법이 있지만 여기서는 “핸즈온
    머신러닝”에서 발췌한 내용으로 정리를 해보겠다.


    먼저 가설함수를 보자. 간단한 식임에도 불구하고 다양한 표현이 존재하여 혼란을 주기 일쑤이다. 아래 표현들을 보면서
    정리해보자.


    선형 회귀 가설 함수그림 1


    1번 같은 경우 단순 선형 회귀라고 생각하면 되겠다. 𝜭와 𝑥가 모두 스칼라인 경우인 것이다. 다시 말해 특성이 1개인
    경우…


    2번과 3번은 모두 다중 선형 회귀를 표현한 식이며 𝜭와 𝑿는 모두 벡터이다. 그런데 묘하게 표현이 다르다.
    왜 다른지 차근차근 살펴보자.



    기본적으로 벡터는 종벡터(𝑛 X 1) 형태를 취한다. 이 때 𝑛은 특성의 수이다. 우리가 이미 잘 알고 있듯이 이 식들은
    가설 함수의 원래 형태인 아래의 형식을 벡터의 곱으로 표현한 것이다.


    선형 회귀그림 2


    이 식의 𝜭와 𝑥를 각각 벡터로 표시해보자. 벡터는 기본적으로 종벡터 형태를 취한다고 했으니 다음과 같이 표현할 수
    있다(여기서 𝜭 의 0번째 요소는 편향을 의미하며 따라서 X의 0번째 요소는 항상 1이다).


    그림 3


    그림 3-1


    그런데 𝜭와 𝑥 가 모두 𝑛 X 1벡터라고 한다면 (𝑛 X 1) ∙ (𝑛 X 1)이 되어 벡터(행렬)의 연산 법칙으로 인해 계산을 할 수 
    없게 된다. 따라서 앞에 있는 𝜭를 전치행렬로 만들어 (1 X 𝑛) ∙ (𝑛 X 1)이 되게 함으로써 연산이 가능하게 만드는 것이다.
    이 것이 바로 두 번째 식이다. 물론 전치행렬의 성질에 따라 다음과 같이 표현할 수도 있다.


    선형 회귀 가설 함수그림 4


    3번째 식은 2번째 식을 조금 더 확장한 것이라고 볼 수 있다. 2번이 식에서 𝑋는 𝑛개의 요소를 갖는 벡터였다.
    이러한 식이 𝑚개, 즉 𝑛개의 특성을 갖는 샘플이 𝑚개가 있다고 보는 것이다. 따라서 이 때는 식의 결과 역시 
    벡터가 되는 것이다. 즉, 3번의 식을 구성하는 각 요소는 다음의 의미가 있다(물론 이 때 편향을 생각하여
    𝑋 행렬의 1열은 모두 1로 채워져야 한다).


    그림 5


    그림 6


    그림 3


    여기에서 식은 2가지로 표현이 가능하다 𝑋를 𝑛 X 𝑚 행렬로 만든다면 식은 2번의 식과 동일한 형태가 만들어질
    것이다. 이렇게 본다면 2번의 식이 가장 일반적인 선형 회귀의 가설함수라고 볼 수 있을 것이다. 그리고 이 식을
    선형 회귀의 비용 함수에 대입하게 되면 아래와 같은 비용 함수의 식이 만들어진다.


    선형 회귀 비용 함수그림 7


    하지만 𝑋를 𝑚 X 𝑛 행렬로 만든다면 3번의 식이 된다. 이 3번의 식은 곧이어 설명할 경사 하강법의 step을
    계산하는 공식에 등장하게 된다.


    배치 경사 하강법


    경사 하강법은 가중치 𝜭의 변화에 따라 비용 함수의 결과가 얼마나 바뀌는지를 확인하는 연속되는 과정이고
    이를 알기 위해서는 비용 함수를 𝜭에 대해 미분해야 한다. 위에 언급한 그림7의 비용 함수를 𝜭에 대해 미분하면
    다음과 같은 식을 얻을 수 있다(이 과정에서도 변형이 있는데 식 맨 앞의 2/m에서 2를 없애기 위해 미리 비용 함수에
    1/2를 곱하는 경우도 있다. 이런 경우 2/m이 아닌 1/m이 된다).


    선형 회귀 비용 함수의 편도함수그림 8


    우리는 수알못이니 이 과정을 잠깐 설명하면 우선 미분의 성질 중 다음 성질을 알아야 한다. 바로 미분의 연쇄법칙이다.


     (f(g(x)))'=f'(g(x))g'(x)


    미분의 연쇄법칙을 적용해보자면 선형 회귀 비용 함수는 다음과 같이 구성되어있다.


    미분의 연쇄법칙그림 9


    따라서 차례차례 미분을 해보면 다음과 같이 풀이될 수 있다.


    미분의 연쇄법칙그림 10


    이와 같이 선형 회귀의 비용 함수에서 𝜭에 대해 미분한 도함수는 그림8의 식이 되는 것이다. 이 도함수는 곧 비용 함수의
    기울기를 의미하므로 경사 하강법은 이 도함수의 변화를 이용여 최솟값을 찾는 과정이고, 이는 초깃값으로 주어진 𝜭0에서 
    학습률과 비용 함수의 도함수를 곱한 값을 빼서 다음 𝜭1를 구하고 다시 이 𝜭1에서 학습률과 비용 함수의 도함수를 곱한 
    값을 빼서 𝜭2를 구하는 식으로 이 과정을 반복해 나가는 것이다.


    이 과정에서 비용 함수의 도함수를 그대로 사용하는 경우도 있지만 식의 단순화를 위해 이 비용 함수의 도함수의 변화량을
    행렬식으로 만들어 한방에 처리하는 방법도 있다. 이 것은 얼마전 포스팅한 정규방정식 관련 글에서 언급했듯이 𝚺는 
    행렬로 변환 가능하다는 것으로 설명할 수 있다.


    비용 함수의 도함수를 풀어보면 다음과 같다.


    선형 회귀 비용 함수의 도함수그림 11


    여기서 괄호 안에 있는 각 요소의 점곱(∙)을 기준으로 앞뒤로 분리를 하면 각각 다음과 같은 종벡터를 만들 수 있다.


    그림 12


    그림 13



    각각의 종벡터는 m X 1 형태의 종벡터로 그대로는 곱셈이 성립하지 않으므로 𝑋가 요소인 종벡터를 전치시켜서
    1 X m 형태의 횡벡터를 만들어 곱하면 동일한 식이 된다.


    그림 14


    그림 15


    이제 마지막으로 𝜭가 포함된 종벡터를 풀이해보자. 이 종벡터는 다시 아래와 같이 나눠볼 수 있다.


    그림 16


    여기서 다시 뺄셈 식의 앞부분을 생각해보면 𝑋(i)는 특성 수만큼의 요소를 갖는 벡터들이다. 즉 m행 n열의 행렬이
    되는 것이다.


    그림 17


    하지만 이렇게 되면 𝜭T는 1 X n의 벡터이고 𝑋는 m X n의 행렬이 되어 곱셈식이 성립되지 않는다. 따라서 𝜭T를
    다시 전치시켜 n X 1의 종벡터를 만든 후 𝑋 뒤에 곱하면 m X n 행렬과 n X 1 벡터의 곱이 성립된다. 이렇게하여
    최종적으로 정리된 선형 회귀 비용 함수를 𝜭에 대해 미분한 도함수의 변화량은 다음과 같이 표현할 수 있다.


    그림 18


    그리고 경사 하강법의 STEP을 구하는 공식은 아래와 같다.


    경사 하강법의 step 계산 공식그림 19


    정리


    여전히 수학은 어렵다. 나름 치환과 간략화에 주의하면서 각종 공식을 이리 저리 변형시켜가면서 이해하려고 하지만
    깔끔하게 정리되지 않는 것은 어쩔 수가 없다. 일단 오늘의 소득이라면 행렬을 횡벡터를 요소로 갖는 종벡터로 생각
    하면 조금 더 쉽게 이해되는 경우가 있다는 것 정도…


    오늘의 주된 내용은 “핸즈온 머신러닝”의 166쪽에 있는 ‘식 4-7 경사 하강법의 스텝’에 대한 풀이였는데 사실 책을
    보면 여전히 이해되지 않는 부분이 있다. 165쪽에 있는 식 4-5 비용 함수의 편도함수 식에서 j의 의미를 잘 모르겠다.
    얼핏 봤을 때 특성의 수를 의미할 것 같은데…그리고 괄호 안의 x와 괄호 밖의 x가 다르게 표기된 부분도 잘 이해가
    안간다. 이렇게 기호 하나가 추가되는 것만으로도 풀이가 안드로메다로 향하는 것을 보면 아직도 한참 멀었다…ㅠ.ㅠ


    일단 내가 정리한 식도 얼추 앞뒤가 맞아 들어가는 것 같으니 우선 오늘의 정리는 마무리 하고 다음 포스팅에서는
    여기서 정리한 식을 바탕으로 코드를 통해 배치 경사 하강법, 확률적 경사 하강법, 미니 배치 경사 하강법에 대해
    알아보도록 하겠다.


    피곤하다…ㅠ.ㅠ

    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^



    2018/11/25 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 1 - 특성의 scale

    2018/12/10 - [Study/인공지능학습] - [머신러닝 reboot] 개념 잡기 : 경사 하강법 2 - step 공식 이해하기

    2019/01/28 - [Study/인공지능학습] - [머신러닝 Reboot] 개념잡기 : 경사 하강법 3 - 경사 하강법의 종류

    2019/04/07 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사 하강법 4 - 규제가 있는 선형 모델

    2019/04/14 - [Study/인공지능학습] - [머신러닝 Reboot] 개념 잡기 : 경사하강법5 - 로지스틱 회귀


    머신러닝 Reboot - 개념 잡기 : 경사 하강법 1 - 특성의 scale


    새롭게 시작하는 머신러닝 학습은 기존에 진행하던 학습에서 장애가 되었던 용어와 공식에 대한 몰이해를 극복하고자
    진행하려고 한다. 다시 말해 직관적으로는 이해가 가지만 논리적으로 설명할 수없는 개념을 논리적으로 설명 가능하도록
    정리해보고자 하는 것이다.


    따라서 전체적으로 연관성을 가지고 이어지는 내용이라기 보다는 단편적인 용어의 정의나 공식의 풀이를 중심으로 
    하면서 관련 내용을 정리하는 방식으로 진행이 될 것이다. 


    이렇게 정리할 대상은 주로 ’핸즈온 머신러닝’이라는 책을 읽으면서 이해가 안가는 부분들을 대상으로 풀이할 것이며
    전체적인 순서 역시 ‘핸즈온 머신러닝’의 목차를 따를 것이다. 


    들어가는 말


    지난 시간에는 선형 회귀 분석의 비용함수로부터 가중치(𝜽 또는 W)의 최솟값을 한방에 알아낼 수 있는 정규방정식
    대해 알아보았다. 미분 등 복잡한 계산이 필요 없고 학습률같은 하이퍼파라미터를 관리할 필요가 없으며 또 빠른 예측이
    가능하다는 장점이 있지만 특성 수가 늘어남에 따라 속도가 많이 느려지는 단점이 있었다.


    오늘은 정규방정식의 단점을 해결할 수 있는, 다시 말해 특성 수에 관계 없이 일정 수준의 성능을 보장해주는 
    경사하강법애 대한 내용 중 특성의 스케일에 대해 알아보려고 한다. 


    경사하강법은 대체로 특성의 스케일에 민감한 것으로 알려져 있으며 일반적으로 아래 그래프로 그 사실을 설명한다.


    핸즈온 머신러닝 발췌핸즈온 머신러닝 발췌

    오늘은 경사하강법이 특성에 민감하다는 것을 예제 코드를 통해 조금 더 직관적으로 설명을 하고자 한다. 
    사실 논리적으로 증명을 하고싶었으나 역시 나의 실력으로는 역부족이었다. 이 내용과 위의 그래프를 이해하지
    못하여 이 포스팅을 준비하는데 무려 3주가 걸겼다…ㅠ.ㅠ


    게다가 내가 그간 얼마나 공부를 설렁설렁 했는 지 이번 기회에 알게 되었다. 그동안 나는 선형회귀의 비용함수와 
    경사하강법을 동일시 하여 생각했던 것이다. 서로 다른 함수를 동일하다고 생각하고 분석하고 있었으니 답이
    나올리가 있나…-.- 겨우 최근에야 경사하강법은 특정 함수(특히 convex 함수)의 최적값을 찾아낼 수 있는
    일반적인 알고리즘이라는 말을 이해하게 되었다.


    다시 말해 경사하강법은 선형회귀의 비용함수 뿐만 아니라 볼록(또는 오목)한 그래프가 그려지는 함수라면 어떤
    함수이든 그 최저점을 찾아낼 수 있는 방법이라는 것이다.


    지금부터 코드를 통해 이 내용을 간단히 살펴보자. 너무 간단해서 들어가는 말보다 본문이 짧을지도…-.-


    Python 코드로 보는 경사 하강법


    이 내용의 원본 소스 출처는 다음과 같다.


    https://github.com/shuyangsun/Cost-Function-Graph


    이 원본 소스 중 non-convex 함수들에 대한 내용은 제거 하고 convex 함수에 대한 내용만을 남겨 확인해보았다.


    일반적으로 특성이 2개인 함수까지는 시각화(그래프로 표현)할 수 있다. 이 부분은 내가 처음 머신러닝을 공부한다고
    정리를 하면서 다항로지스틱으로 넘어갈 때 꽤나 답답해 했던 부분이기도 하다. 특성이 2개인 경우까지는 시각화가
    가능해서 직관적으로 이해를 할 수 있었는데 특성이 3개 이상 되니 복잡한 수식만으로는 도무지 이해가 가지 않는
    것이었다…ㅠ.ㅠ


    여전히 특성이 3개 이상인 경우는 이해가 힘들기 때문에 오늘은 특성이 2개인 케이스를 대상으로 설명을 해보겠다.


    이 코드에서 사용할 함수는 비용함수는 아니고 f(a,b) = a^2 + b^2 이라는 함수이다. 이 함수가 표현하는 범위를
    3차원 그래프로 그려보고 그 범위 안에서 경사하강법이 어떤 경로로 최저점을 찾아가는지 보여주는 것이 아래의
    코드이다.


    import numpy as np
    import matplotlib.pyplot as plt
    import math
    from matplotlib import cm
    from mpl_toolkits.mplot3d import Axes3D
    
    def f(a,b):
    	return a**2 + b**2
    

    먼저 필요한 라이브러리들을 import하고 경사하강법을 통해 최저점을 찾아낼 대상 함수 f를 정의했다.


    def gradient_descent(theta0, iters, alpha):
    	history = [theta0] # to store all thetas
    	theta = theta0     # initial values for thetas
    	# main loop by iterations:
    	for i in range(iters):
    		# gradient is [2x, 2y]:
    		gradient = [2.0*x for x in theta] #함수 f(x,y)의 미분
    		# update parameters:
    		theta = [a - alpha*b for a,b in zip(theta, gradient)]
    		history.append(theta)
    	return history
    
    history = gradient_descent(theta0 = [-1.8, 1.6], iters =30, alpha = 0.03)


    다음으로 경사하강법을 함수로 정의하고 호출하여 그 결과를 history에 저장을 한다. 경사하강법의 다음 스텝을 결정하는
    일반식은 다음과 같으며 이를 python 코드로 구현한 것이다.




    경사하강법을 구현한 함수는 파라미터로 특성의 초깃값과 반복 횟수 그리고 학습률을 전달받는다. 이 코드에서 초깃값은 각 특성의 max에 가까운 값으로 정했다.


    # f(x,y) = x^2 + y^2 함수의 그래프 그리기 fig = plt.figure(figsize=(20, 15)) ax = fig.gca(projection='3d') #plt.hold(True) a = np.arange(-2, 2, 0.25) b = np.arange(-2, 2, 0.25) a, b = np.meshgrid(a, b) c = f(a,b) surf = ax.plot_surface(a, b, c, rstride=1, cstride=1, alpha=0.3, linewidth=0, antialiased=False, cmap='rainbow')



    주석된 내용처럼 f 함수가 표현하는 함수의 범위를 3차원으로 그려주는 코드이다. 특성 a와 b 모두 -2부터 2 사이의 값을
    가지며 0.25씩 증가 하도록 값을 주었다. 함수가 a^2 + b^2이기 때문에 최솟값 0부터 최댓값 8까지의 그릇 모양으로
    그래프가 표시된다.


    a = np.array([x[0] for x in history])
    b = np.array([x[1] for x in history])
    c = f(a,b)
    ax.scatter(a, b, c, color="r"); 
    
    print(c)
    
    plt.xlabel('Feature A')
    plt.ylabel('Feature B')
    
    plt.axis('equal')
    
    plt.show()
    


    이제 마지막으로 이전에 그려진 그래프 내에서 경사하강법을 통해 산출한 위치를 표시해준다. 특성이 2개이기 때문에
    각각의 특성에 경사하강법을 적용한 결과를 그래프에 그려보면 최종적으로 아래와 같은 그래프를 볼 수 있다.



    이 때 a = np.arange(-2, 2, 0.25)의 범위를 a = np.arange(-10, 10, 0.25)로 늘리게 되면 그래프의 형태가
    오목한 그릇 형태가 아닌 u자 모양으로 휘어진 판자의 형태가 된다.



    이런 상황에서는 가중치의 초깃값이 커질 수 있고 초깃값이 커지면 최솟값을 찾는데 그만큼 더 시간이 오래 걸리게 되며 이는 곧 특성값의 스케일 차이가 크게 되면 경사하강법의 성능이 나빠지게 된다고 볼 수 있는 것이다. 또한 내가 제대로 
    이해하고 있는지 모르겠으나 이 그래프 표현만 놓고 보면 단지 두 개의 특성간에 스케일의 차이가 있을 때 뿐만 아니라 
    두 특성의 스케일이 동일하더라도 그 규모가 커지면(예를들어 a와 b의 범위가 모두 10인 경우와 모두 100인 경우)
    이 때 역시 경사하강법의 성능이 나빠져 더 많은 횟수를 진행해야 최솟값에 가까워지게 된다.


    정리


    앞서도 말했지만 이 부분을 이해하기 위해 장장 3주 이상이 걸렸다. 그러다가 위의 python 코드를 발견했고 처음 코드를
    실행해봤을 때는 ‘유레카’를 외쳤지만 지금 다시 찬찬히 살펴보는 과정에서는 또 수많은 의문들이 일어나면서 내가 
    제대로 이해한 것이 맞는지 알 수 없게 되었다…ㅠ.ㅠ 일단 직관적으로 생각했을 때 작은 수를 계산할 때보다 큰 수를 
    계산할 때 더 많은 자원이 필요한 것은 당연한 것이니 특성의 스케일이 크면 그만큼 연산이 오래 걸린다고 보면 될 것이나
    역시 완전한 이해는 되지 못한다.


    더 나은 해법을 찾다가 contour라는 등고선 형태의 그래프를 그리는 방법이 있다는 것을 알아냈고 이 그래프가 위에
    언급한 핸즈온 머신러닝의 그래프와 유사해서 더 설명하기가 좋지 않을까 생각했으나 실제 코드에 적용하는 방법을
    몰라 이번 포스팅에서는 다루지 못했다. 시간 날 때 다시 정리를 해봐야겠다.


    다음 시간에는 경사하강법의 3가지 종류(배치 경사하강법, 확률적 경사하강법, 미니 배치 경사하강법)에 대해 간단하게
    정리해보겠다.

    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^


    인공지능 공부해보겠다고 설레발치기 시작한 것이 어언 2년여가 다되간다.

    그간 나름 책도 좀 보고 동영상 강좌도 좀 보고...페이퍼나 논문은 하나도 안보고...ㅠ.ㅠ


    그간 읽은 책들과 읽는 중인 책 그리고 읽기 위해 사놓은 책을 좀 정리해보면


    읽은 책

    텐서플로우 첫걸음
    골빈해커의3분 딥러닝
    머신러닝 워크북
    밑바닥부터 시작하는 딥러닝

    읽는 중인 책

    핸즈 온 머신러닝
    머신러닝 탐구생활


    읽다가 보류한 책

    강화학습 첫걸음
    기초 수학으로 이해하는 머신러닝 알고리즘
    처음 배우는 딥러닝 수학
    프로그래머를 위한 선형대수


    사놓기만 한 책

    머신러닝 실무 프로젝트
    딥러닝의 정석
    러닝 텐서플로우
    머신러닝 딥러닝 실전 개발 입문




    하지만 공부를 해 가면 해 갈수록 궁금한 것은 더 많아지고 이제는 과연 내가 뭔가를 이해 하고는 있는 건가 하는 생각에

    자괴감이 들기 시작했다. 똑같은 내용을 공부하고 있는데 볼수록 새로운 느낌?


    결국 자괴감을 이기지 못하고 처음부터 다시 시작하는 길을 선택했다. 처음 시작부터 이해하지 못하고 넘어간 부분들을

    차근차근 정리하고, 이해하고 넘어가야 할 것 같아서...(그런 의미에서 현재 읽고있는 2권의 책은 꽤 도움이 되는 것 같다)


    특히나 전반적으로 내용을 이해하지 못하게 하는 주된 이유 중 하나가 어려운 용어들과 복잡한 공식들이기도 하고 

    머신러닝의 기초가 되는 선형 회귀 등을 제대로 이해하지 않고 겁대가리 없이 덥썩 딥러닝으로 직행한 무모함도 충분히 

    일조를 했기에 다시 처음부터 하나 하나를 정리하는 것으로부터 시작하기로 했다. 그리고 


    패배적인 자기 만족일지는 모르겠으나 어차피 이 공부는 내가 이 분야의 전문가가 되기 위한 것이라기 보다는 나의 지적 만족을

    위한 것이니 쉬엄쉬엄 간들 어떠랴 싶다. 태공망 여상은 나이 80에 주문왕을 만나 그 재능을 펼치기 시작했다는데 그렇다면

    나에게는 아직 30년이란 시간이 남은 것 아닌가(뜻밖의 나이 공개가...ㅠ.ㅠ)


    느려도 황소 걸음이랬으니 차분하게 한걸음 한걸음 가보자.

    그 시작은 정규 방정식이다.


    정규방정식

    사실 그동안 비용함수를 최소화 하는 가중치를 찾기 위한 방법으로 경사하강법만을 알고 있었는데 이번에 핸즈 온 머신러닝을

    읽으면서 처음 정규방정식이란 것이 있다는 것을 알게 되었다.


    문제는 이 정규방적이라는 것이 행렬식으로 표현이 되어있어 문돌이의 사고방식으로는 이 것이 어떻게 경사하강법과 동일한

    기능을 하게 되는지 이해가 가지 않는 것이었다. 그래서 새로운 시작의 첫 출발을 단순 선형회귀의 비용함수로부터 정규방정식을

    도출하는 과정을 정리해보고자 한다.



    선형 회귀 비용함수로부터 정규방정식 도출하기


    복습

    선형회귀의 가설함수 식에서 편향을 제거하자. 방법은 그냥 b = 0으로 초기화 하는 것이다.




        •비용함수도 다시 한 번 확인하자.



















    사전 확인1 - ∑를 행렬로



    ∑ 로 표현되는 제곱의 합은 그 수들을 요소로 하는행렬과그 행렬의 전치행렬의 곱과 같다(복잡하니
    1
    행짜리 행렬로 확인해보자).




















    사전 확인2전치행렬의 성질


         •전치행렬은 다음과 같은 성질이 있다.




























    우선 cost함수는 W에 대한 함수이므로 함수 표기를 바꿔보자(함수명MSE는 최소제곱법의 영문 표기인 Mean Square Error의 약어이다).

    이제 명확하게 이 함수는 x와 y에 대한 함수가 아니라 W에 대한 함수로 보일 것이다.









    함수는W에 대한 함수인데 정작 함수 식에는 W가 안보이니 내부 함수도 원래대로 치환하자.






















    사전 확인한 내용을 상기하면서 번 식으로 변환해보자
    전치행렬의 성질에 따라 번 식으로 전개할 수 있다.
    W를 포함한 식들을 다시 정리하면 번 식이 된다.
    다시 한 번 전치행렬의 성질에 따라 식을 전개하면 번 식이 된다.
    이 변형은 최초의 시그마 식을 전개해서 진행해도 동일한 결과가 나온다.






    최종 정리된 식은 과 같고 이제 이 값을 미분하여비용함수가 최솟값을 갖는 W를 찾을 것이다.

    비용함수가 최솟값을 갖기 위해서는 비용함수를 미분한 값이 0이 되어야 한다.
    미분 과정을 명확하게 하기 위해 식을 한 번 정리해 주자(식 ).주의할 것은 W에 대해 미분한다는 점이다.
    행렬 A에 대해 자신과 자신의 전치행렬과의 곱은제곱과 같다고 했다.그리고 전치행렬의 성질에 따라W와 X의 곱의 전치행렬은 X의 전치행렬과 W의 전치행렬의 곱과 같다(W와 X의 순서가 바뀜에 주의).




    이제 거의 다 왔다.
    미분한 함수는 식 와 같고 이제 
    거추장스러운1/m도 없앨 수 
    있다(사실 진작에 1/m을 없애고
    보다 깔끔하게 식을 전개할 수도 
    있었으나 나같은문돌이는 갑자기 
    저런거 하나 없어져도 극도의
    멘붕에 시달릴 수 있기에 끝까지 
    가져왔다-.-).










    최종 미분식을 W에 대해 정리해보자.
        •이렇게 해서 단순 선형 회귀의 비용함수로부터 정규방정식을 도출해보았다.













    선형 회귀 비용함수로부터 정규방정식 도출하기2



    정규방정식은 다른 형태로도 
    도출할 수 있다우선 최초의 
    식을 전개해보자.














    이후 전개한 식을 W에 대해 미분한다.














    최종 정리한 후 시그마를 
    행렬로 변환해보자














    하지만 아직 이해하지 못한 것이 하나 있다.
    가설함수에서 편향을 제거하지않고 WX + b의 형태로 이 과정을 진행하게 되면 최종 정규방정식은 좌측과 같이 나온다.이 것이 앞서 도출해본 정규방정식과 동일한 식이란 것을 문돌이의 두뇌로는 이해하기 힘들다.ㅠ (분모와 분자 각각 - 뒤에 붙어있는 값들은 대체 어쩔...ㅠ.ㅠ)






    일단 정규방정식은 좌측의 식으로 

    알아두자

    정규방정식은 행렬식으로 경사
    하강법에 비해 많은 연산량이 필요
    하지도 않고 학습률 설정 등
    골치아픈 하이퍼파라미터의 
    설정을 신경쓰지 않아도 된다.
    하지만 행렬 연산이다보니 특성의 
    수가 늘어나면 계산속도가 많이 
    느려지게 된다.다만 샘플 수에 
    대해서는 선형적으로 비례한다고 
    한다.
    또한 정규방정식으로 학습된 선형 
    모델은 예측이 매우 빠르다고 한다
    (핸즈 온 머신러닝)



    정리


    이렇게 해서 새롭게 시작하는 인공지능 학습의 첫 단추를 꿰었다. 하지만 이렇게 차근차근 분석을 하면서도 여전히

    어떤 부분에 대해서는 완전하게 이해하지 못한 채 그저 직관적인 이해로 두루뭉술하게 넘어가고 있는 상황이다.

    사실 이러한 상태가 가장 환장하는 상태이다. 전체적인 흐름은 대충 이해가 가는데 어떤 디테일한 부분에서

    뭔가 막혀있는 듯한 느낌...



    첫 대상인 정규방정식도 정리를 하고 보니 아직은 부족한 상태라는 것을 알게 되었다.

    이러한 과정이 큰 도움이 되는 것 같다.


    아무튼 이번에는 용어 하나, 공식 하나도 집중해서 보면서 차근차근 진행을 해나가 보자. 

    머신러닝 reboot는 이제 시작이다!




    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

    TensorFlow







    목차



    3소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10 ~ cifar10_train.py (이번 글)





    소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10


    이제 가장 중요한 부분은 지나갔다.
    생각해보면 전체적인 흐름을 먼저 살펴보고 세부적인 내용들을 분석했어야 할 것 같은데 순서가 거꾸로 되어버렸다.
    아무래도 전체 포스팅을 마무리한 후 다시 한 번 되짚는 과정을 거쳐야 할 것 같다.


    앞서 분석한 내용들은 모델을 구성하고 loss값을 생성하고 optimizer를 적용하는 구체적인 내용들이었다.
    처음 딥러닝을 공부할 때는 각각의 단계가 거의 1줄 코딩이었던 것을 생각하면 이 소스는 매우 복잡해보인다.
    그러나 세부적인 설정들이 더 추가되었을 뿐 근본적인 맥락은 다를 바가 없다.


    자세한 내용은 복습 시간에 다시 살펴보고 오늘은 사용자와 인터페이스하는 소스를 살펴보도록 하자.


    cifar10_train.py


    소스 분석에 들어가기 전에 참고로 이 소스를 훈련시켰을 때의 정확도가 소스 첫머리의 주석에 표시되어있다.


    accuracy



    뭐 흙수저가 사용할 수 있을만한 장비는 아닌 듯하니 그냥 그런가보다 하고 넘어가자…-.-

    from __future__ import absolute_import
    from __future__ import division
    from __future__ import print_function
    
    from datetime import datetime
    import time
    
    import tensorflow as tf
    
    import cifar10
    


    첫 3줄은 앞서도 보았듯이 python 2와 3의 호환을 위한 것이고 datetime과 time은 이름에서도 알 수 있듯이 날짜와
    시간을 사용하기 위한 것으로 print를 하거나 수행 시간을 체크하기 위한 용도로 import 하였다. 마지막 2줄도 생략

    FLAGS = tf.app.flags.FLAGS
    
    tf.app.flags.DEFINE_string('train_dir', '/tmp/cifar10_train',
                               """Directory where to write event logs """
                               """and checkpoint.""")
    tf.app.flags.DEFINE_integer('max_steps', 1000000,
                                """Number of batches to run.""")
    tf.app.flags.DEFINE_boolean('log_device_placement', False,
                                """Whether to log device placement.""")
    tf.app.flags.DEFINE_integer('log_frequency', 10,
                                """How often to log results to the console.""")
    


    FLAG 역시 이전 포스팅에서 설명을 하였는데 그 아래 tf.app.flags.DEFINE_XXX로 지정한 이름으로 그 값을
    사용할 수 있다. 즉, FLAG.train_dir은 '/tmp/cifar10_train’라는 값을 가지고 있게 된다. 두 번째 줄에 보면 학습
    step을 1000000회로 설정하였다.


    train()

    # 학습을 실행시키는 함수
    def train():
      """Train CIFAR-10 for a number of steps."""
    
    # with tf.Graph().as_default() 문장은 지금까지 만들었던 모든 그래프 구성 요소(operation과 tensor들)을
    # 하나의 전역 Graph 안에서 사용하겠다는 의미이다.  
      with tf.Graph().as_default():
    # global_step은 학습의 step 카운트를 자동으로 관리해주는 tensor로 사용자가 별도로 step을 카운트
    # 할 필요가 없이 이 global_step을 이용하면 된다.
        global_step = tf.train.get_or_create_global_step()
    
        # Get images and labels for CIFAR-10.
        # Force input pipeline to CPU:0 to avoid operations sometimes ending up on
        # GPU and resulting in a slow down.
    # 학습을 수행할 장치를 지정. 첫 번째 CPU를 사용하도록 지정하고 있다. GPU를 사용하는 방법은
    # cifar10_multi_gpu_train.py 소스를 참조하면 된다. 비록 multi gpu를 사용하는 소스지만...-.-
        with tf.device('/cpu:0'):
    # 학습에 사용할 미니 배치 크기의 image와 label을 가져온다.
    # 자세한 내용은 cifar10.py 소스의 distorted_inputs함수 참조
    # http://mazdah.tistory.com/814
          images, labels = cifar10.distorted_inputs()
    
        # Build a Graph that computes the logits predictions from the
        # inference model.
    # 학습 모델 생성. 자세한 내용은 cifar10.py 소스의 inference함수 참조
    # http://mazdah.tistory.com/814
        logits = cifar10.inference(images)
    
        # Calculate loss.
    # 손실값 계산. 자세한 내용은 cifar10.py 소스의 loss함수 참조
    # http://mazdah.tistory.com/814
        loss = cifar10.loss(logits, labels)
    
        # Build a Graph that trains the model with one batch of examples and
        # updates the model parameters.
    # 실제 학습을 수행할 operation 생성. 자세한 내용은 cifar10.py 소스의 loss함수 참조
    # http://mazdah.tistory.com/814
        train_op = cifar10.train(loss, global_step)
    
    # 아래 나오는 tf.train.MonitoredTrainingSession에 사용하기 위한 로그 hooker
    # MonitoredTrainingSession.run() 호출에 대한 로그들을 hooking하는 역할을 한다.
    # Pythons에서는 클래스 선언 시 ( )안에는 상속할 클래스를 지정한다. 즉, _LoogerHook 클래스는
    # tf.train.SessionRunHook 클래스를 상속하여 만들어지게 되며 정의된 함수들은 이 클래스의
    # 함수들을 Overriding해서 구현한 함수들이다.
        class _LoggerHook(tf.train.SessionRunHook):
          """Logs loss and runtime."""
    
    # session을 이용할 때 처음 한 번 호출되는 함수
          def begin(self):
            self._step = -1
            self._start_time = time.time()
    
    # run() 함수가 호출되기 전에 호출되는 함수
          def before_run(self, run_context):
            self._step += 1
            return tf.train.SessionRunArgs(loss)  # Asks for loss value.
    
    # run() 함수가 호출된 후에 호출되는 함수
          def after_run(self, run_context, run_values):
            if self._step % FLAGS.log_frequency == 0:
              current_time = time.time()
              duration = current_time - self._start_time
              self._start_time = current_time
    
              loss_value = run_values.results
              examples_per_sec = FLAGS.log_frequency * FLAGS.batch_size / duration
              sec_per_batch = float(duration / FLAGS.log_frequency)
    
              format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f '
                            'sec/batch)')
              print (format_str % (datetime.now(), self._step, loss_value,
                                   examples_per_sec, sec_per_batch))
    
    # 분산 환경에서 학습을 실행할 때 사용하는 Session. 분산 환경에 대한 지원을 해준다.
    # (Hook를 이용한 로그 관리, 오류 발생시 복구 처리 등)
        with tf.train.MonitoredTrainingSession(
            checkpoint_dir=FLAGS.train_dir,
            hooks=[tf.train.StopAtStepHook(last_step=FLAGS.max_steps),
                   tf.train.NanTensorHook(loss),
                   _LoggerHook()],
            config=tf.ConfigProto(
                log_device_placement=FLAGS.log_device_placement)) as mon_sess:
          while not mon_sess.should_stop():
    # 드디어 마무리~ 학습 operation을 실제로 수행시킨다.
            mon_sess.run(train_op)
    



    main(argv=None)

    # CIFAR-10 데이터를 다운로드 받아 저장. cifar10.py 소스 참조
    #  http://mazdah.tistory.com/814
    cifar10.maybe_download_and_extract()
    
    # 학습 수행 중의 로그를 저장할 디렉토리 생성. 기존에 동일 디렉토리가 있다면 삭제 후 생성.
    if tf.gfile.Exists(FLAGS.train_dir):
      tf.gfile.DeleteRecursively(FLAGS.train_dir)
    tf.gfile.MakeDirs(FLAGS.train_dir)
    
    # 학습 시작
    train()
    



    정리


    소스 길이에 비해 분석하는 데 너무 많은 시간이 걸렸다…ㅠ.ㅠ
    지난 포스팅에서도 언급한 것처럼 매개 변수나 리턴값들이 모두 tensor 형태이고 TensorFlow의 API 문서에 있는
    내용들이 수학적인 내용을 많이 포함하고 있어 다른 언어나 프레임워크의 문서를 읽는 해석하는 것에 비해 원문
    해석도 꽤나 어려웠다.


    포스팅한 내용에 부정확한 내용이 있을지도 모르겠기에 일단 CIFAR-10 예제 코드를 실제로 돌려보고
    그 중간 로그나 결과 값들과 비교해가면서 다시 한 번 찬찬히 살펴볼 필요가 있을 것 같다. 그리고 추후에 이 소스에 
    쓰인 API들을 별도로 정리해보겠다.


    소스 중에는 아직 평가를 위한 cifar10_eval.py이 남아있는데 요건 우선 학습 관련 내용을 마무리하고 
    진행해보도록 하겠다.

    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

    Tensorflow









    소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10


    지난 포스팅에서 살펴보았던 cifar10_input.py는 데이터를 불러와서 이미지를 임의 조작한 후 배치 사이즈 크기로
    나누어 담아 리턴해주는 기능을 하였다. 전체 프로세스의 가장 첫 단계라고도 할 수 있다.


    오늘 살펴볼 cifar10.py는 가장 핵심적인 소스로 모델과 네트워크를 구성하는 내용이 주를 이루고 있다.
    그만큼 코드의 길이도 전체 소스 중 가장 길다.


    차근차근 살펴보도록 하자.


    cifar10.py

    from __future__ import absolute_import
    from __future__ import division
    from __future__ import print_function
    

    지난 포스팅과 마찬가지로 위 3줄의 import 문은 python 2와 3의 호환성을 위한 것이다.


    # OS의 자원을 사용할 수 있게 해주는 모듈
    import os
    # 정규표현식을 사용할 수 있게 해주는 모듈
    import re
    # python interpreter에 의해 관리되는 변수나 함수에 접근할 수 있도록 해주는 모듈
    import sys
    # tar 압축을 핸들링할 수 있게 해주는 모듈
    import tarfile
    

    여러 다양한 기능을 사용할 수 있게 해주는 import문들이다. os, sys, tarfile 등은 원격으로 cifar10 데이터셋을
    다운로드 받기 위해 쓰인다.


    # six 모듈은 python 2와 3의 함수를 함께 사용할 수 있도록 해줌. urllib는 URL 관련 모듈로 역시
    # 데이터 셋 다운로드에 사용된다.
    from six.moves import urllib
    #텐서플로우 모듈
    import tensorflow as tf
    
    # 지난 포스팅에서 살펴본 cifar10_input.py 참조
    import cifar10_input
    

    몇가지 모듈이 추가로 import 되었으나 대부분 CIFAR10 데이터 셋을 원격으로 다운로드 받기 위한 것으로 이미
    별도로 데이터 셋을 다운로드 받아두었다면 무시해도 좋을 것이다.


    소스 앞부분에 영문으로 중요한 함수에 대한 설명이 주석으로 달려있다. 일단 간단하게 그 내용을 살펴보면 
    다음과 같다.


    • inputs, labels = distorted_inputs( )
      : 학습에 사용할 데이터를 불러온다. 이 함수 안에서 cifar10_input.py에 있는 distorted_inputs( )
      함수를 호출하여 처리한다.
    • predictions = inference(inputs)
      : 파라미터로 전달되는 모델(inputs)에 대한 추론을 계산하여 추측한다.
    • loss = loss(predictions, labels)
      : 해당 라벨에 대해 예측값에 대한 총 손실 값을 구한다.
    • train_op = train(loss, global_step)
      : 학습을 수행한다.


    위의 4개 함수가 가장 핵심적인 내용이라 할 수 있다.
    이제 전체 코드를 차근차근 살펴보자.


    # tf.app.flags.FLAGS는 상수의 성격을 갖는 값을 관리하는 기능을 한다.
    # tf.app.flags.DEFINE_xxx 함수를 이용하여 첫 번째 파라미터에 사용할 이름을 넣고
    # 두 번째 파라미터에 사용할 값을 설정하면 이후 'FLAGS.사용할 이름' 형식으로 그 값을
    # 사용할 수 있다. 아래 첫 번째 코드의 경우 FLAGS.batch_size라는 코드로 128이라는 값을
    # 사용할 수 있다.
    FLAGS = tf.app.flags.FLAGS
    
    # Basic model parameters.
    tf.app.flags.DEFINE_integer('batch_size', 128,
                                """Number of images to process in a batch.""")
    tf.app.flags.DEFINE_string('data_dir', '/tmp/cifar10_data',
                               """Path to the CIFAR-10 data directory.""")
    tf.app.flags.DEFINE_boolean('use_fp16', False,
                                """Train the model using fp16.""")
    
    # Global constants describing the CIFAR-10 data set.
    # cifar10_input.py에 정의되어있던 값들
    IMAGE_SIZE = cifar10_input.IMAGE_SIZE  #24
    NUM_CLASSES = cifar10_input.NUM_CLASSES  #10
    NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = cifar10_input.NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN  #50000
    NUM_EXAMPLES_PER_EPOCH_FOR_EVAL = cifar10_input.NUM_EXAMPLES_PER_EPOCH_FOR_EVAL #10000
    
    
    # Constants describing the training process.
    # tf.train.ExponentialMovingAverage에서 사용할 가중치 값
    MOVING_AVERAGE_DECAY = 0.9999     # The decay to use for the moving average.
    # 학습 속도 감소 후의 epoch 수
    NUM_EPOCHS_PER_DECAY = 350.0      # Epochs after which learning rate decays.
    # 학습률 감소를 위한 변수
    LEARNING_RATE_DECAY_FACTOR = 0.1  # Learning rate decay factor.
    # 초기 학습률
    INITIAL_LEARNING_RATE = 0.1       # Initial learning rate.
    


    음…수학을 깊이 들어가긴 싫지만 얼레벌레 그냥 넘어가는 것도 그러니 몇 가지 개념은 좀 알아보고 가자.


    Exponential Moving Average

    우선 이동평균(Moving Average)라는 것은 특정 기간동안 내에 측정된 값의 평균을 의미한다.
    이 이동평균에는 단순이동평균, 가중이동평균, 그리고 여기서 사용하는 지수이동평균이 있는데
    이 지수이동평균은 가장 최근 값에 더 큰 가중치를 두어 평균을 계산하는 방식이라고 한다.


    일단 위 코드 중 MOVING_AVERAGE_DECAY 이후의 설정들은 모두 학습률 조정을 위한 것으로
    train( ) 함수에서 사용을 하게 된다. 기본적으로 학습이 진행됨에 따라 학습률을 기하급수적으로 감소시켜
    나가는 방법을 취하고 있다. 자세한 내용은 train( ) 함수 설명에서 다시 한 번 분석해보자.


    # If a model is trained with multiple GPUs, prefix all Op names with tower_name
    # to differentiate the operations. Note that this prefix is removed from the
    # names of the summaries when visualizing a model.
    # 멀티 GPU를 사용하여 병렬 처리할 때 작업 이름을 구분하기 위한 구분자...언제 써볼 수 있을까...ㅠ.ㅠ
    TOWER_NAME = 'tower'
    
    # CIFAR-10의 데이터 경로
    DATA_URL = 'https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz'
    


    이제부터는 함수를 하나 하나 살펴보도록 하겠다.


    _activation_summary(x)

    # 이 함수는 전체적으로 각 레이어들을 텐서 보드에 시각화하기 위한 summary를 만드는 작업을 한다.
    """Helper to create summaries for activations.
      Creates a summary that provides a histogram of activations.
      Creates a summary that measures the sparsity of activations.
      Args:
        x: Tensor
      Returns:
        nothing
      """
      # Remove 'tower_[0-9]/' from the name in case this is a multi-GPU training
      # session. This helps the clarity of presentation on tensorboard.
      tensor_name = re.sub('%s_[0-9]*/' % TOWER_NAME, '', x.op.name)
      tf.summary.histogram(tensor_name + '/activations', x)
      tf.summary.scalar(tensor_name + '/sparsity',
                                           tf.nn.zero_fraction(x))
    


    _variable_on_cpu(name, shape, initializer)

    # 파라미터로 전달받은 값을 이용하여 CPU를 이용하여 처리할 변수를 생성
    """Helper to create a Variable stored on CPU memory.
      Args:
        name: name of the variable
        shape: list of ints
        initializer: initializer for Variable
      Returns:
        Variable Tensor
      """
    # 0번째 CPU를 사용하겠다고 지정
      with tf.device('/cpu:0'):
    # python의 3항 연산 FLAGS.use_fp16이 true이면 tf.float16을 사용하고 false이면 
    # else 뒤의 tf.float32를 사용. 이 코드에서는 FLAGS.use_fp16를 false로 설정했으므로
    # tf.float32를 사용하게 됨
        dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
    # 파라미터로 전달된 변수가 이미 존재하면 재활용하고 존재하지 않으면 새로 만든다.
    # 참고로 tf.Variable는 무조건 새로운 변수를 만든다. 자세한 사용법은 아래 링크 참조
    # https://tensorflowkorea.gitbooks.io/tensorflow-kr/content/g3doc/how_tos/variable_scope/
        var = tf.get_variable(name, shape, initializer=initializer, dtype=dtype)
      return var
    


    _variable_with_weight_decay(name, shape, stddev, wd)

    # 위의 _variable_on_cpu(name, shape, initializer) 함수를 이용하여 정규화 처리를 한 변수를 생성.
    """Helper to create an initialized Variable with weight decay.
      Note that the Variable is initialized with a truncated normal distribution.
      A weight decay is added only if one is specified.
      Args:
        name: name of the variable
        shape: list of ints
        stddev: standard deviation of a truncated Gaussian
        wd: add L2Loss weight decay multiplied by this float. If None, weight
            decay is not added for this Variable.
      Returns:
        Variable Tensor
      """
    # 데이터 타입 설정
    # 세 번째 파라미터는 초기화 함수를 리턴하여 넘기는 것으로 truncated_normal_initializer는
    # 정규분포 기반의 초기화 함수로 표준편차의 양 끝단을 잘라낸 값으로 새로운 정규분포를 만들어 
    # 초기화 한다.
      dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
      var = _variable_on_cpu(
          name,
          shape,
          tf.truncated_normal_initializer(stddev=stddev, dtype=dtype))
    
    # L2 정규화 처리를 위한 코드. wd(아마도 Weight Decay)값이 None이 아닌 경우 if문
    # 안의 코드를 수행하여 정규화 처리를 하고 그래프에 추가한다.
    # tf.nn.l2_loss는 전달받은 텐서의 요소들의 제곱의 합을 2로 나누어 리턴한다.
      if wd is not None:
        weight_decay = tf.multiply(tf.nn.l2_loss(var), wd, name='weight_loss')
        tf.add_to_collection('losses', weight_decay)
      return var
    


    위 함수들은 실제로 학습을 진행하면서 결과 값을 예측하는 과정에 사용되는 함수들이다.
    자세한 내용들은 올바른 예측을 하기 위한 알고리즘을 구성하는 수학적인 내용이 포함되어있어
    당장에는 이해가 쉽지 않다. 예를 들어 tf.truncated_normal_initializer의 경우 정규분포
    그래프에서 2개 이상의 표준편차를 제거한 값들로 새롭게 만들어진 그래프로 초기화 한다고 해석이
    되는데 사실 내용자체도 이해가 되지 않고 더 심각한 것은 수학적 개념이 포함된 영어를 해석하자니
    제대로 해석이 되었는지도 모르겠다…ㅠ.ㅠ 일단은 학습을 최적화 시키고자 하는 목적으로 이러한
    장치들을 사용한다는 것만 알아두면 되겠다.


    distorted_inputs()

    # cifar10_input.py에 있는 같은 이름의 함수를 이용하여 학습할 데이터를 불러온다.
    """Construct distorted input for CIFAR training using the Reader ops.
      Returns:
        images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
        labels: Labels. 1D tensor of [batch_size] size.
      Raises:
        ValueError: If no data_dir
      """
    
    # 데이터 경로가 지정되어있지 않으면 에러~
      if not FLAGS.data_dir:
        raise ValueError('Please supply a data_dir')
    
    # 데이터 경로를 조합하여 최종적으로 사용할 이미지와 라벨을 가져옴
      data_dir = os.path.join(FLAGS.data_dir, 'cifar-10-batches-bin')
      images, labels = cifar10_input.distorted_inputs(data_dir=data_dir,
                                                      batch_size=FLAGS.batch_size)
    
    # FLAGS.use_fp16 값이 true이면 이미지와 라벨 텐서의 요소들을 tf.float16 타입으로 형변환 한다.
    # 하지만 코드에는 False로 지정되어있으므로 무시.
      if FLAGS.use_fp16:
        images = tf.cast(images, tf.float16)
        labels = tf.cast(labels, tf.float16)
      return images, labels
    


    inputs(eval_data)

    # 역시 cifar10_input.py에 있는 같은 이름의 함수를 이용하여 평가할 데이터를 불러온다.
    # eval_data라는 파라미터가 추가된 것 외에는 distorted_inputs 함수와 내용 동일
    """Construct input for CIFAR evaluation using the Reader ops.
      Args:
        eval_data: bool, indicating if one should use the train or eval data set.
      Returns:
        images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
        labels: Labels. 1D tensor of [batch_size] size.
      Raises:
        ValueError: If no data_dir
      """
      if not FLAGS.data_dir:
        raise ValueError('Please supply a data_dir')
      data_dir = os.path.join(FLAGS.data_dir, 'cifar-10-batches-bin')
      images, labels = cifar10_input.inputs(eval_data=eval_data,
                                            data_dir=data_dir,
                                            batch_size=FLAGS.batch_size)
      if FLAGS.use_fp16:
        images = tf.cast(images, tf.float16)
        labels = tf.cast(labels, tf.float16)
      return images, labels
    


    inference(images)

    # 이 소스의 핵심으로 예측을 위한 모델을 구성하는 함수
    """Build the CIFAR-10 model.
      Args:
        images: Images returned from distorted_inputs() or inputs().
      Returns:
        Logits.
      """
      # We instantiate all variables using tf.get_variable() instead of
      # tf.Variable() in order to share variables across multiple GPU training runs.
      # If we only ran this model on a single GPU, we could simplify this function
      # by replacing all instances of tf.get_variable() with tf.Variable().
      #
      # conv1
    # convolution 레이어 1
      with tf.variable_scope('conv1') as scope:
    # 커널(필터) 초기화 : 5 X 5 크기의 3채널 필터를 만들며 64개의 커널을 사용한다.
        kernel = _variable_with_weight_decay('weights',
                                             shape=[5, 5, 3, 64],
                                             stddev=5e-2,
                                             wd=None)
        conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
    
    


    이 부분은 CNN의 핵심이며 가장 중요한 부분이므로 좀 더 상세하게 알아보자.
    일단 필터(커널보다 친숙하므로 앞으로는 ‘필터’로만 표기하겠다. 또한 원칙적으로는 bias까지 +되어야 완성된
    필터라 할 수 있으나 우선은 bias를 무시하고 생각해보자)가 하는 역할이 무엇인지부터 알아보면 말 그대로 
    이미지에서 지정된 영역의 특징만을 ‘걸러내는’ 역할을 한다. 


    그러면 어떤 방식으로 특징을 걸러내는가?
    바로 머신러닝이나 딥러닝을 처음 배울때 배웠던 xW + b의 함수를 사용해서 처리한다. 일단 bias는 무시하기로
    했으니 xW만 생각해본다면 입력받은 이미지에서 필터와 겹치는 부분을 x라 보고 해당 위치의 필터를 W라 보아
    x1* W1 + x2 * W2 + … + xn * Wn이 되는 것이다. 만약 3 X 3 필터를 사용하였다면 아래와 같이 계산할 수
    있다.


    x1 * W1 + x2 * W2 + x3 * W3 + ... x9 * W9
    


    여기에 만일 입력 채널(이미지의 색상 채널)이 3이라면 각 채널마다 위의 계산을 적용한 후 각 채널별 출력값을
    최종 더해서 하나의 feature map을 만들게 된다. 결국 하나의 필터가 하나의 feature map을 만들게 되므로
    만일 필터를 여러개 사용한다면 feature map의 개수도 필터의 개수와 동일하게 만들어지고 이 수가 곧 
    feature map의 채널이 된다(그리고 이 각각의 채널에 bias를 +하게 된다). 


    이 내용을 이해 못해 수없이 구글링을 했으나 적절한 자료를 찾지 못했는데 아래 이미지를 보고 쉽게 이해할 수 있었다.


    CNN Filter feature map

    이미지 출처 : http://taewan.kim/post/cnn/


    이 코드를 가지고 계산을 해보면 24 X 24 크기의 3채널 이미지를 입력으로 받아 5 X 5 크기의 3채널 필터 64개를
    사용하고 padding이 원본 크기와 동일한 출력이 나오도록 SAME으로 지정되었으므로 24 X 24 크기에 64 채널을
    가진 출력이 나올 것이다. 여기에 배치 사이즈가 128이므로 최종 출력 텐서의 shape는 [128, 24, 24, 64]가 된다.


    # 바이어스 초기화
    # 채널의 개수가 64개이므로 bias도 64개 설정. biases는 64개의 요소가 0.0으로 채워진
    # vector
        biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
    
    # 가중치 + 바이어스. biases는 conv의 마지막 차수와 일치하는 1차원 텐서여야 한다.
        pre_activation = tf.nn.bias_add(conv, biases)
    
    # 활성화 함수 추가
        conv1 = tf.nn.relu(pre_activation, name=scope.name)
    
    # 텐서 보드에서 확인하기 위한 호출
        _activation_summary(conv1)
    
      # pool1
    # 풀링 레이어 1
    # pooling은 간단하게 말해 이미지를 축소하는 단계로 필터로 주어진 영역 내에서 특정한 값(평균,최대,최소)을
    뽑아내는 작업이다. 일단 최대값을 뽑는 것이 가장 성능이 좋다고 하여 max pooling을 주로 사용한단다.
    # 이 코드에서는 필터 크기가 3 X 3이므로 이 영역에서 가장 큰 값만을 뽑아 사용한다. stride는 2를 사용한다.
      pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
                             padding='SAME', name='pool1')
      # norm1
    # local response normalization라는 정규화 처리인데 ReLu 사용시 에러율 개선에 
    # 효과가 있다고 하는데 이 부분은 좀 더 확인이 필요함
      norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75,
                        name='norm1')
    
      # conv2
    # convolution 레이어 2
      with tf.variable_scope('conv2') as scope:
        kernel = _variable_with_weight_decay('weights',
                                             shape=[5, 5, 64, 64],
                                             stddev=5e-2,
                                             wd=None)
        conv = tf.nn.conv2d(norm1, kernel, [1, 1, 1, 1], padding='SAME')
        biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.1))
        pre_activation = tf.nn.bias_add(conv, biases)
        conv2 = tf.nn.relu(pre_activation, name=scope.name)
        _activation_summary(conv2)
    
      # norm2
    # local response normalization 2
      norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75,
                        name='norm2')
      # pool2
    # 풀링 레이어 2
      pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1],
                             strides=[1, 2, 2, 1], padding='SAME', name='pool2')
    
      # local3
    # fully connected layer 
      with tf.variable_scope('local3') as scope:
        # Move everything into depth so we can perform a single matrix multiply.
        reshape = tf.reshape(pool2, [FLAGS.batch_size, -1])
        dim = reshape.get_shape()[1].value
        weights = _variable_with_weight_decay('weights', shape=[dim, 384],
                                              stddev=0.04, wd=0.004)
        biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
        local3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
        _activation_summary(local3)
    
      # local4
    # fully connected layer 2
      with tf.variable_scope('local4') as scope:
        weights = _variable_with_weight_decay('weights', shape=[384, 192],
                                              stddev=0.04, wd=0.004)
        biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
        local4 = tf.nn.relu(tf.matmul(local3, weights) + biases, name=scope.name)
        _activation_summary(local4)
    
      # linear layer(WX + b),
      # We don't apply softmax here because
      # tf.nn.sparse_softmax_cross_entropy_with_logits accepts the unscaled logits
      # and performs the softmax internally for efficiency.
    # softmax layer
      with tf.variable_scope('softmax_linear') as scope:
        weights = _variable_with_weight_decay('weights', [192, NUM_CLASSES],
                                              stddev=1/192.0, wd=None)
        biases = _variable_on_cpu('biases', [NUM_CLASSES],
                                  tf.constant_initializer(0.0))
        softmax_linear = tf.add(tf.matmul(local4, weights), biases, name=scope.name)
        _activation_summary(softmax_linear)
    
      return softmax_linear
    


    이 함수의 코드는 Convolutional layer > ReLu layer > Pooling Layer > Norm layer > Convolutional layer 
    > ReLu layer > Norm layer > Pooling layer > Fully connected layer > Fully connected layer > 
    Softmax layer의 순으로 구성이 되어있는데 이 중 Norm layer가 정확히 어떤 역할을 하는지는 아직 잘 모르겠다.
    일단 ReLu를 보조하는 것 같은데 더 알아봐야겠다.


    loss(logits, labels)

    # 손실 값 계산을 위한 함수
    # 아래 주석에서 보이듯 logits 파라미터는 inference() 함수의 리턴 값이고 labels는 distorted_input()
    # 또는 input() 함수의 리턴 튜플 중 labels 부분이다. cross entropy를 이용하여 loss를 구한다.
    """Add L2Loss to all the trainable variables.
      Add summary for "Loss" and "Loss/avg".
      Args:
        logits: Logits from inference().
        labels: Labels from distorted_inputs or inputs(). 1-D tensor
                of shape [batch_size]
      Returns:
        Loss tensor of type float.
      """
      # Calculate the average cross entropy loss across the batch.
    # 여기서는 sparse_softmax_cross_entropy_with_logits 함수가 사용되고 있는데
    # softmax_cross_entropy_with_logits와의 차이라면 softmax_cross_entropy_with_logits
    # 함수가 확률분포를를 따른다면 sparse_softmax_cross_entropy_with_logits는 독점적인 확률로
    # label이 주어진다고 하는데...무슨 의미인지 잘 모르겠다...ㅠ.ㅠ 확인이 필요한 내용
      labels = tf.cast(labels, tf.int64)
      cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
          labels=labels, logits=logits, name='cross_entropy_per_example')
      cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
      tf.add_to_collection('losses', cross_entropy_mean)
    
      # The total loss is defined as the cross entropy loss plus all of the weight
      # decay terms (L2 loss).
      return tf.add_n(tf.get_collection('losses'), name='total_loss')
    


    _add_loss_summaries(total_loss)

    # 텐서 보드에 손실값 표시를 위해 손실 값에 대한 summary 추가하고
    # 손실값들의 이동 평균을 구하여 리턴. 여기서 사용하는 이동 평균은 가장 최근 값에 가중치를 두는
    # tf.train.ExponentialMovingAverage을 사용하여 구한다.
    """Add summaries for losses in CIFAR-10 model.
      Generates moving average for all losses and associated summaries for
      visualizing the performance of the network.
      Args:
        total_loss: Total loss from loss().
      Returns:
        loss_averages_op: op for generating moving averages of losses.
      """
      # Compute the moving average of all individual losses and the total loss.
      loss_averages = tf.train.ExponentialMovingAverage(0.9, name='avg')
      losses = tf.get_collection('losses')
      loss_averages_op = loss_averages.apply(losses + [total_loss])
    
      # Attach a scalar summary to all individual losses and the total loss; do the
      # same for the averaged version of the losses.
      for l in losses + [total_loss]:
        # Name each loss as '(raw)' and name the moving average version of the loss
        # as the original loss name.
        tf.summary.scalar(l.op.name + ' (raw)', l)
        tf.summary.scalar(l.op.name, loss_averages.average(l))
    
      return loss_averages_op
    


    train(total_loss, global_step)

    # 학습을 실행시키는 함수
    """Train CIFAR-10 model.
      Create an optimizer and apply to all trainable variables. Add moving
      average for all trainable variables.
      Args:
        total_loss: Total loss from loss().
        global_step: Integer Variable counting the number of training steps
          processed.
      Returns:
        train_op: op for training.
      """
      # Variables that affect learning rate.
    # 미리 정의한 변수들을 이용하여 러닝 rate를 조정할 파라미터를 결정한다. 
      num_batches_per_epoch = NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN / FLAGS.batch_size
      decay_steps = int(num_batches_per_epoch * NUM_EPOCHS_PER_DECAY)
    
      # Decay the learning rate exponentially based on the number of steps.
    # 학습 step이 증가할 수록 러닝 rate를 기하급수적으로 감소시키도록 처리한다.
    # tf.train.exponential_decay 함수는 아래 식의 결과를 리턴한다.
    # INITIAL_LEARNING_RATE * LEARNING_RATE_DECAY_FACTOR ^ (global_step / decay_steps)
      lr = tf.train.exponential_decay(INITIAL_LEARNING_RATE,
                                      global_step,
                                      decay_steps,
                                      LEARNING_RATE_DECAY_FACTOR,
                                      staircase=True)
      tf.summary.scalar('learning_rate', lr)
    
      # Generate moving averages of all losses and associated summaries.
      loss_averages_op = _add_loss_summaries(total_loss)
    
    # Optimizer 설정 및 텐서 보드에 표시하기 위한 summary 생성 후 추가
      # Compute gradients.
      with tf.control_dependencies([loss_averages_op]):
        opt = tf.train.GradientDescentOptimizer(lr)
        grads = opt.compute_gradients(total_loss)
    
      # Apply gradients.
      apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)
    
      # Add histograms for trainable variables.
      for var in tf.trainable_variables():
        tf.summary.histogram(var.op.name, var)
    
      # Add histograms for gradients.
      for grad, var in grads:
        if grad is not None:
          tf.summary.histogram(var.op.name + '/gradients', grad)
    
      # Track the moving averages of all trainable variables.
      variable_averages = tf.train.ExponentialMovingAverage(
          MOVING_AVERAGE_DECAY, global_step)
      variables_averages_op = variable_averages.apply(tf.trainable_variables())
    
    # tf.control_dependencies 함수는 오퍼레이션간의 의존성을 지정하는 함수로 with와 함께
    # 사용하면 파라미터로 전달된 오퍼레이션이 우선 수행된 뒤 다음 문장, 여기서는 with문 아래 있는
    # train_op = tf.no_op(name='train')이 수행된다. 
      with tf.control_dependencies([apply_gradient_op, variables_averages_op]):
        train_op = tf.no_op(name='train')
    
    # 이미 알다시피 여기까지는 그저 그래프를 만든 것 뿐, 이제 tf.Session을 통해 run을 하면
    # 이전까지 구성된 그래프가 실행된다. 실제로 실행시키는 내용은 cifar10_tranin.py에 들어있다. 
      return train_op
    


    maybe_download_and_extract()

    # 웹사이트로부터 CIFAR-10 데이터 셋을 다운로드 받아 사용할 경로에 압축을 풀게 하는 함수
    # 이미 별도로 데이터 셋을 받아놓은 경우는 필요 없음
    """Download and extract the tarball from Alex's website."""
      dest_directory = FLAGS.data_dir
      if not os.path.exists(dest_directory):
        os.makedirs(dest_directory)
      filename = DATA_URL.split('/')[-1]
      filepath = os.path.join(dest_directory, filename)
      if not os.path.exists(filepath):
        def _progress(count, block_size, total_size):
          sys.stdout.write('\r>> Downloading %s %.1f%%' % (filename,
              float(count * block_size) / float(total_size) * 100.0))
          sys.stdout.flush()
        filepath, _ = urllib.request.urlretrieve(DATA_URL, filepath, _progress)
        print()
        statinfo = os.stat(filepath)
        print('Successfully downloaded', filename, statinfo.st_size, 'bytes.')
      extracted_dir_path = os.path.join(dest_directory, 'cifar-10-batches-bin')
      if not os.path.exists(extracted_dir_path):
        tarfile.open(filepath, 'r:gz').extractall(dest_directory)
    


    정리


    핵심적인 내용들이 대부분 들어있는 소스이다보니 잊어버린 내용 되찾으랴 또 생소한 API 확인하랴 시간이
    많이 걸렸다.


    단지 시간만 많이 걸린 것이면 그나마 다행이지만 꽤 많은 부분을 이해하지 못한다는 것은 참으로 난감한 일이
    아닐 수 없다…ㅠ.ㅠ 그래도 기본적인 CNN의 흐름을 따라 어찌어찌 정리는 했지만 여전히 확인해야 할 내용들이
    많이 남아있다. 특히나 API의 경우 기본적으로 파라미터나 리턴 값들이 텐서를 기반으로 하고 있는데다가 설명
    또한 수학적인 내용이나 용어들을 포함하고 있다보니 java나 python 같은 프로그래밍 언어의 API 문서를
    대하는 것과는 그 이해의 차원이 다르다.


    일단 중요한 고비는 넘겼으니 다음 포스팅에서 학습을 진행하기 위한 메인 소스인 cifar10_train.py를
    살펴보고 그 다음 마지막으로 cifar10_eval.py를 살펴본 후 이 소스 코드에 등장했던 API들을 모두
    차근차근 번역해봐야겠다.

    블로그 이미지

    마즈다

    이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^