들어가며

지난 시간에는 도커와 컨테이너의 정의 그리고 가상 머신과 비교한 장점에 대해서 알아보았다. 이번에는 도커가 어떻게 컨테이너를 만들고 관리하는 아키텍처에 대해서 알아보도록 하겠다.

도커 아키텍쳐

도커는 기본적으로 clinet - server로 나누어져 있다. 클라이언트에서 docker 커맨드를 사용하면 도커 서버는 REST API형식으로 명령을 전달받아 수행하게 된다. 이 때문에 원격으로 도커 서버에 명령을 전달하거나 여러 개의 도커 서버에 접근 할 수 있으며 하나의 서버 또한 여러개의 클라이언트에서 명령을 받을 수 있다. 이다음은 내부 구조에 대해서 하나씩 알아보도록 하겠다.

https://docs.docker.com/get-started/overview/#docker-architecture

Docker daemon(dockerd)

도커 데몬은 클라이언트의 명령을 REST API로 받아 컨테이너, 이미지, 네트워크 그리고 볼륨을 관리한다. (여기서 볼륨은 컨테이너에서 생성된 데이터들을 의미한다)

Docker client

위에서 언급했든 dockerd에 명령을 전달하기 위한 수단이다. docker명령어를 사용하면 Docker API가 REST API형식으로 dockerd의 소켓에 전달된다.

Images

도커 컨테이너를 만들기 위한 읽기 전용 템플릿이다. 이미지는 직접 제작, Registry에서 다운로드 그리고 기존 이미지를 확장하여 사용할 수 있다. 이미지는 이미지를 만들기 위한 커맨드가 집합된 Dockerfile을 통해서 만들어질 수 있으며 이미지는 레이어 단위로 만들어지게 되는데 만약 Dockerfile에 어떤 부분을 수정했다면 해당 부분의 레이어만 교체하므로 Rebuilding이 가볍게 되어 있는 구조이다. 

Container

컨테이너는 이미지의 instance이며 독립적으로 프로세스를 실행할 수 있는 공간이다. 역으로 컨테이너의 상태를 이미지로 저장하여 사용할 수 있다. 참고로 독립성은 기존에 있던 LUX의 namespace를 이용하여 제공한다. 

 

마치며

기본적인 아키텍처에 대해서 알아보았다. 이후에는 실제로 커맨드를 통해서 도커를 실습하여 익혀보자

'Docker' 카테고리의 다른 글

[Docker] - 도커 이미지 빌드(Dockerfile)  (0) 2021.02.18
[Docker] - Docker란 무엇인가?  (0) 2021.02.14

들어가며

최근 해커톤을 진행하면서 도커 형식으로 주최 측에서 개발환경을 제공해주었다. 도커에 대해서는 어렴풋이 들어봤지만 실제로 개발에 활용하면서 탁월한 유용성을 체감했다. 좀 더 잘 활용하기 위해 세세한 기초부터 도커에 대해 알아보자.

 

 

도커의 정의

도커란 컨테이너를 기반으로하는 가상화 플랫폼이자 이를 운영하는 회사명이다. 가상화 플랫폼이라는 말은 기존의 Virtual Machine처럼 일종의 가상 머신을 만들어 운영하는 것을 의미한다. 그렇다면 컨테이너는 무엇일까?

 

컨테이너

컨테이너는 코드와 코드 실행에 필요한 종속성들을 패키지로 만든 소프트웨어 단위이다. 그래서 기존의 애플리케이션을 다른 환경으로 빠르게 옮겨 실행시킬 수 있다고 한다. 기존에는 어떤 애플리케이션을 작성하고 이를 다른 컴퓨터에 옮기려고 하면 기초 설정들을 다시 세팅해야 했다. 예를 들어 pytorch를 사용한 인공지능 모델 학습을 다른 서버로 이전해야 한다고 하자. 그러면 pytorch를 설치하는 건 기본이고 파이썬, conda 등 여러 가지를 다른 서버에 설치한 다음에 애플리케이션을 실행시킬 수 있다. 이러한 세팅을 프로비저닝(provisioning)이라고 한다. 

 

즉 컨테이너는 기기의 인프라에 관계없이 어플리케이션을 실행시킬 수 있도록 하여 프로비저닝을 해소할 수 있는 소프트웨어이다. 

 

컨테이너 vs 가상머신 

 

얼핏 들어보면 가상머신과 별다른 게 없다고 생각할 수 있다. 가상 머신 또한 OS를 포함해서 통째로 개발환경을 옮길 수 있기 때문이다. 도커 홈페이지에서는 이와 관련해서도 자세히 설명이 되어있다. 영어로 되어있으므로 간단하게 한글로 인용하겠다.

https://www.docker.com/resources/what-container

가상 머신

가상 머신은 하이퍼바이저를 통해서 물리적 하드웨어 단위로 추상화하여 하나의 서버에 여러 개의 서버를 실행시키는 것처럼 할 수 있다. 가상 머신은 OS부터 기초적인 바이너리 파일과 라이브러리까지 포함하므로 이는 몇십 기가바이트 정도로 많은 용량을 차지하며 초기 부팅이 느리다는 단점이 있다.

 

컨테이너

컨테이너는 코드와 이에 필요한 종속성들만 포함하여 애플리케이션 단위로 가상화를 하여 하나의 OS 커널을 공유한다. 각각의 컨테이너는 독립적인 유저 프로세스이며 몇십 메가바이트 정도의 매우 작은 용량이 필요하므로 공유하는데 용이하다. 그리고 가상 머신에 비해서 빠른 속도를 가지고 있다.

 

 

마치며

기본적인 도커의 정의에 대해서 알아보았다 다음에는 개발자로서 도커의 아키텍처에 대해서 알아보도록 하자

 

'Docker' 카테고리의 다른 글

[Docker] - 도커 이미지 빌드(Dockerfile)  (0) 2021.02.18
[Docker] - Docker Architecture(도커의 구조)  (0) 2021.02.14

들어가며

파이썬, 자바등의 많은 프로젝트에서는 이식성을 위한 코드의 집합인 패키지, 모듈이 있다. 당연히 안드로이드에도 이를 지원하는 AAR(Android Archive)가 존재한다. 공식 사이트에도 설명이 되어있지만 갱신되지 않거나 모호한 부분이 있어 차근차근 모듈을 만들고 테스트하는 방법에 대해서 알아보도록 하자.

안드로이드의 모듈은 코드뿐만 아니라 이미지, 텍스트 등의 데이터를 포함해서 부르기 때문에 여기선 라이브러리와 동일한 의미로 언급한다.

 

1. 새로운 모듈 만들기

우선 기존의 프로젝트에서 아래의 경로를 통해 새로운 모듈을 만든다.

New Module을 클릭하면 여러가지 타입이 나오는데 간단한 AAR을 만들기 위해서 Android Library를 선택하고 next를 클릭한다.

다음에는 모듈이름과 패키지명, Minimum SDK를 설정할 수 있다. 패키지명은 지금은 중요하지 않지만 추후에 배포를 할 시에는 중요하다. 지금은 기본설정으로 진행해보자.

새로 생성을 하면 Project 패널에 모듈명으로 폴더가 생성되어 있으며 내부에는 기존 app과 같은 구조로 표현되어있다.

ToastID와 관련된  build.gradle 파일을 살펴보면 기존의 app build.gradle과 매우 유사하다, 하지만 plugins에 'com.android.application'대신 'com.android.library'라고 되어 있는데 이는 ToastID디렉토리를 라이브러리로 사용하겠다는 뜻이다. 그리고 라이브러리에는 applicationID가 없지만 app에는 있다. 이는 라이브러리에서는 applicationID를 사용할 수 없기 때문이다.

Android 공식 사이트에는 새로운 모듈을 만들었을 때 gradle.build설정을 따로 해야한다고 하지만 현재는 자동으로 라이브러리를위한 gradle.build를 만들기 때문에 신경쓰지 않아도 된다. (패치했으면 document는 업데이트 해줬으면 좋겠다...)

https://developer.android.com/studio/projects/android-library
 

Android 라이브러리 만들기  |  Android 개발자  |  Android Developers

Android 라이브러리를 생성하는 방법을 알아보세요.

developer.android.com

build.gradle(ToastID)
build.gradle(app)

간단한 테스트를 하기 위해서 ToastID클래스와 아이디를 토스트하는 메서드를  작성해보았다.

자 그럼 모듈을 기존의 app에 연결하여 어플리케이션을 빌드해보자

 

2. 로컬 모듈 연결하기

build.gradle(app)에 들어가서 모듈과의 연결을 설정할 수 있다. 아는 사람도 많겠지만 dependencies에서 외부 저장소 라이브러리를 가져오거나 지금 할 것처럼 프로젝트 내부의 모듈을 연결 할 수 있다. 프로젝트 내부 연결을 위해선 implementation project(':모듈명')를 dependencies에 추가하면된다.

추가후 Sync Now를 클릭해서 동기화 해주면 기존의 app 내부의 MainActivity에서 ToastID모듈 내부의 toast 메서드를 참조할 수 있는것을 볼 수 있다. 이제 모듈을 마음껏 테스트해보고 필요시 배포를 할 수 있다.

 

마치며

자바에 대해서 어느정도 지식이 있는 사람은 "그냥 JAR을 디렉토리에 옮겨서 import하면 쉬운데 build.gradle을 어렵게 설정하면서까지 해야하는거지?"라고 생각할 수도 있다. 물론 이 방법도 가능하다. 그러나 눈치챈 사람도 있겠지만 AAR은 코드 뿐만 아니라 drawable,layout 등의 안드로이드에 특화된 데이터까지 모듈화가 가능한게 큰 장점이다. 이를 잘 사용해서 프로젝트 협업을 잘 하거나 안드로이드 개발자들을 위한 오픈소스를 쉽게 사용하도록 제공해보도록 하자.

들어가며

안드로이드의 가장 기본적인 알림을 사용해보자. 아마 프로젝트 하다가 어떻게 쓰는지 궁금한 사람들이 검색을 했을 테니 최대한 간결하게 코드를 통해 설명하겠다. 코드를 세세하게 찾아보고 싶은 사람은 android developers의 문서를 참조하기 바란다.

 

사용방법

1. Notification Channel만들기

각 어플리케이션에서 Notification알림을 실행하려면 우선 채널을 만들어야한다. 

아래 코드를 복사해서 MainActivity에 추가하고 onCreate에서 해당 함수를 호출해주자

*채널명과 설명은 본인 어플리케이션에 맞게 설정해주자

private void createNotificationChannel() {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = "공지사항 채널";
            String description = "공지사항 채널";
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel("공지사항 채널", name, importance);
            channel.setDescription(description);
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
    }

2. 새로운 Notification을 만들고 이를 NotificationManager에 추가해주자

URLData.activity는 호출을 위한 액티비티 Context를 추가해주면된다.(ex MainActivity.this)

플래그를 설정해주고 PendingIntent를 호출하자.

builder를 만들 때 setContentIntent에서 이를 추가하고 Notification을 위한 다른 정보를 추가해주자(제목, 내용 등)

마지막으로 액티비티에서 NotificationManagerCompat를 호출해주고 notify로 builder로 만든 Notification을 추가해주면 끝이다.

Intent intent = new Intent(URLData.activity, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(URLData.activity, 0, intent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(URLData.activity, "공지사항 채널")
                                    .setSmallIcon(R.drawable.app_icon)
                                    .setContentTitle(urlDataList.get(index).urlName)
                                    .setContentText("새로운 공지사항이 등록되었습니다!!")
                                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                                    .setContentIntent(pendingIntent)
                                    .setAutoCancel(true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(URLData.activity);
notificationManager.notify('1', builder.build());

들어가며

안드로이드 코드를 작성하다보면 view를 다룰 때 꼭 한번 context를 parameter로 요청하는 경우가 있다.

그냥 단어만 알고있던 이것에 대해서 한 번 알아보고 어떤 쓸모가 있는지 알아보고 다른 인스턴스에 접근하는 방법을 탐구해보자. ( 인스턴스 접근방법을 알고 싶다면 맨 아래 문단을 참고하기 바랍니다. )

Context의 정의

출처: https://developer.android.com/reference/android/content/Context

위는 Android Developers에 게시된 문서의 정의부분이다. 읽어보니 안드로이드의 시스템 요소와 자원에 접근하기 위한 추상 클래스라고 한다. 문서를 조금더 찾아보니 Activity, Application, Service의 base class로 사용된다고 한다. 이를 알고 나니 기본적인 시스템 추상클래스라는 느낌을 알겠다.

용례

  • 어플리케이션의 resource 획득
  • 새로운 activity 시작
  • view 생성
  • system service 획득

사용 예시에는 위와 같은 경우가 있는데 이는 getResource(), startActivity() 등의 함수를 어느정도 다루어봤다면 당연히 알 것이다. 이 정도로 끝났다면 나는 이번 포스팅을 작성하지 않았을 것이다. 생각해보니 이를 이용하면 CustomView를 만들때 해당 뷰가 속한 액티비티의 함수를 CustomView에서 자유롭게 호출할 수 있을것 같았다.

다른 액티비티에서 변수, 함수 호출

아래는 내가 만든 CustomButton이다. 우선 생성자를 호출할 때 뷰는 모든 Context를 인자로 받아야하는데 이를 MainActivity클래스로 캐스팅하여 변수로 저장했다.

그 다음 버튼을 클릭할 때 startForegroundService를 호출하도록 실험해봤는데.... 된다..... MainActivity는 싱글톤 클래스고 Context를 MainActivity으로 캐스팅해서 안 될줄 알았는데 된다....

다른 액티비티나 커스텀 뷰에서 메인 액티비티의 변수나 함수에 접근하는데 stack overflow에 서칭도 해보고 고민을 많이 했는데 너무 간단하게 된다.... 아무튼 오늘 좋은 지식을 하나 얻었다.

 

들어가며

지난 시간까지 케라스의 Sequential API를 이용해서 간단한 선형 신경망을 만들어 회귀와 분류를 진행했습니다.

만약 여러개의 입력, 출력층을 만들고 싶거나 좀 더 복잡한 신경망을 만들고 싶다면 어떻게 해야할까요? 이번에는 함수형 API를 사용해서 이 문제를 해결해보겠습니다.

 

목차

1. 함수형 API란?

2. Wide & Deep 신경망

3. 여러개의 입력,출력층

 

1. 함수형 API란?

함수형 API는 말 그대로 코드에서 함수처럼 신경망 층을 다루는 API입니다. 

input_B = keras.layers.Input(shape=[6], name="deep_input")
hidden1 = keras.layers.Dense(30, activation="relu")(input_B)

두 번째 줄을 보면 새로운 Dense층을 선언하면서 뒤에 인자로 기존에 선언한 input_B를 입력하는 것을 볼 수 있습니다.

이렇게 되면 hidden1층은 input_B를 자신의 입력층으로 정한다는 뜻입니다. 생각보다 매우 간단합니다.

 

2. Wide & Deep 신경망

이를 이용해서 새로운 신경망 구조를 만들어 보겠습니다.

Wide & Deep 신경망은 2016년 헝쯔 청의 논문에서 소개되었습니다. 이 그림은 입력층의 일부 또는 전체가 곧바로 출력층에 연결되는 경우가 있습니다. 이 경우 간단한 규칙과 복잡한 규칙을 모두 학습할 수 있습니다.

출처 : https://bcho.tistory.com/1187

함수형 API를 이용해서 코드를 작성해봅시다.

from tensorflow import keras

input_ = keras.layers.Input(shape=X_train.shape[1:])
hidden1 = keras.layers.Dense(30, activation="relu")(input_)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.concatenate([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.models.Model(inputs=[input_], outputs=[output])

각 층들이 필요한 입력층은 인자로 선언함으로서 간단하게 구현하는 모습입니다. concatenate층은 두개의 층을 하나로 합치는 층을 의미합니다. 이를 통해서 Wide & Deep모델을 만들 수 있습니다.

summary() 메서드를 통해서 정보를 보면 각층은 인자에 맞게 잘 연결된 모습을 볼 수 있습니다.

colab.research.google.com/drive/1bdwyrHcNh9LXi9jqU_zmDtnoBL6zy2EL?usp=sharing

 

Google Colaboratory

 

colab.research.google.com

3. 여러개의 입력, 출력층

여러개의 출력층을 사용하는 경우는 매우 다양합니다.

예를 들어 사람의 얼굴에서 여러가지 감정의 추정값을 만들거나 이미지 인식에서 물체의 폭, 너비 등의 데이터를 추출할 때입니다. 그렇다면 여러개의 출력층은 어떻게 만들까요? 

결론적으로 keras.models.Model을 선언할 때 출력층은 여러개로 만들면됩니다.

 

코드로 작성해봅시다.

input_A = keras.layers.Input(shape=[5], name="wide_input")
input_B = keras.layers.Input(shape=[6], name="deep_input")
hidden1 = keras.layers.Dense(30, activation="relu")(input_B)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name="main_output")(concat)
aux_output = keras.layers.Dense(1, name="aux_output")(hidden2)
model = keras.models.Model(inputs=[input_A, input_B],
                           outputs=[output, aux_output])

위 코드를 보면 마지막 Model에서 outputs으로 두개의 층을 선언하는 모습을 볼 수 있습니다. aux_output은 층의 중간에서 데이터가 잘 학습되고 있는지 관찰하기 위한 보조출력입니다.

 

마치며

이렇게 함수형  API를 이용해서 복잡한 모델을 간단하게 만들어보았습니다. 하지만 모델 내부에 조건문을 선언하거나 반복문을 선언하는 등 세부적인 프로그래밍은 불가능합니다. 다음에는 이를 해결하기 위한 서브클래싱  API를 만들어 보겠습니다.

들어가며

이번 글에서는 문제를 해결하는 가장 단순한 방법인 완전탐색 또는 Brute-Force알고리즘에 대해서 알아보도록 하겠습니다.

 

목차

  • 정의
    • 사용개념(재귀호출)
    • 재귀 호출을 완전탐색에 사용하는 방법
  • 문제 예시 : Boggle Game
  • 시간복잡도 분석

정의

Brute-Force는 직역하면 무식하게 풀기라는 의미입니다. 문제가 주어졌을 때 일어날 수 있는 모든 경우의 수를 계산해서 원하는 출력값을 얻는 것을 의미합니다. 모든 경우의 수를 탐색하므로 완전탐색(Exhaustive Solving)이라고도 불립니다. 이는 컴퓨팅 자원을 극심하게 소모하기 때문에 입력값이 작을 것으로 기대될때만 사용하는 것이 좋습니다.

 

간단하게 예를 들어보겠습니다. 열 명의 학생을 한 줄로 세우려고 하는데 서로 사이가 안 좋은 학생들을 붙여서 세우면 안된다는 조건이 있습니다. 이 조건을 만족하면서 열 명을 세우는 경우의 수를 구하는 문제는 어떻게 풀어야 할까요? 가장 단순한 방법은 열명의 학생을 한 줄로 세우는 모든 경우의 수를 만들고 사이가 안 좋은 학생들이 떨어져 있는 경우의 수를 세면됩니다.  10!은 대략 360만 가지의 경우의수를 가지지만 이는 컴퓨터가 1초안에 처리할 수 있기 때문에 완전탐색을 사용하는것이 적합한 문제입니다. 

사용개념(재귀호출)

자 이제 정의를 알아보았으니 코드로 어떻게 구현해야 할 지 고민할 차례입니다. 여기에서 우리는 재귀호출이라는 개념을 사용할 수 있습니다. 

우선 재귀호출의 정의부터 차근차근 설명하겠습니다.

재귀호출은 "자기 자신을 호출하는 함수"입니다. 

.

출처 ; https://dojang.io/mod/page/view.php?id=584

자신을 호출하면 함수 하나가 할 일을 반복해서 호출할 수 있습니다. 반복문이랑 비슷하다고 생각하면됩니다.

 

하지만 조건없이 계속 호출한다면 위 처럼 무한으로 반복되기에 중간에 멈추기 위한 종료조건(Base Case)이 필요합니다.

재귀 호출을 완전탐색에 사용하는 방법

다시 위 예에서 10명을 한줄로 세우는 경우의수로 돌아가봅시다.

10명을 한 줄로 세우는 경우의 수를 출력하는 문제를 푸는 과정은 다음과 같습니다.

1. 10명 중 한 명을 선택해서 줄에 추가

2. 9명 중 한 명을 선택해서 줄에 추가

3. 8명 중 한 명을 선택해서 줄에 추가

.

.

10. 1명 중 한 명을 선택해서 줄에 추가

 

비슷한 점이 보이시나요? 10번의 과정은 선택하는 인원의 차이만 있지 방법은 모두 똑같습니다.

이 처럼 경우의 수를 구하는 문제는 비슷한 부분문제들로 쪼갤 수 있습니다. 

그렇다면 비슷한 부분문제를 코드로 작성하는 방법은 무엇이 있을까요?

바로 재귀함수입니다!

 

결국 경우의 수는 부분문제로 쪼갤 수 있고 이를 코드로 작성하는데는 재귀함수가 적합하므로 완전탐색 알고리즘은 재귀함수로 작성합니다.

 

예제 문제 : Boggle Game

완전 탐색의 정의와 문제 풀이방법을 알아보았으니 실제로 문제를 풀어보도록 하겠습니다.

문제: Boggle Game

설명 : N x N의 필드에 각 칸마다 하나의 문자가 새겨져 있고 상하좌우, 대각선 방향으로 이어서 하나의 단어를 찾을 수 있는지 찾는 문제입니다. (위의 경우 coding을 찾아야합니다. )

 

완전탐색으로 해결하기

문제를 해결하기 위한 과정을 만들어봅시다.

1. 하나의 칸을 선택하고 단어의 첫 번째 글자가 있는지 확인

2. 상하좌우,대각선 방향 등 총 8가지의 방향에 두 번째 글자가 있는지 확인

3. 두 번째 글자에서 다시 하좌우,대각선 방향 등 8가지 방향에 세 번째 글자가 있는지 확인

.

.

.

위 과정을 보니 계속해서 글자가 일치하는지 확인하고 8가지 방향을 탐색하는 과정을 반복합니다.

이렇게 반복하는 과정을 정의했으니 재귀함수로 이 정의를 구현하고 문제를 해결해봅시다.

 

const int dx[8] = { -1, -1, -1,  1,  1,  1,  0,  0};
const int dy[8] = { -1,  0,  1, -1,  0,  1, -1,  1};
/*
함수 정의 : 보드의 위치를 받아 글자가 일치하는지 확인하고 다음 8방향 탐색
*/
bool hasWord(int x, int y, int pos) {
	//종료조건1: 넘겨받은 보드의 위치가 범위를 초과한 경우 
    if (x < 0 || x >= 5 || y < 0 || y >= 5)
        return false;
	//종료조건2: 글자가 일치하지 않는 경우
    if (Board[x][y] != Word[pos])
        return false;
	//종료조건3: 글자수와 탐색 횟수가 일치하는 경우 = 모든 단어의 글자가 일치하는 경우
    if (Word.size() == pos + 1)
        return true;

    bool ret = false;

    for (int d = 0; d < 8; ++d) {
    	//8방향의 결과값들을 하나씩 확인 후 하나라도 true를 반환하면 true반환
        //모든 방향의 결과값들이 false일 경우 false를 반환
        if (hasWord(x + dx[d], y + dy[d], pos + 1)) {
            ret = true;
            break;
        }
    }
    
    return ret;
}

시간복잡도 분석

보글게임을 문제를 해결했으니 이제 위 알고리즘이 얼마나 시간이 걸리는지 계산하기 위해서 시간복잡도를 계산해보겠습니다.

시간복잡도는 항상 최악의 경우를 생각해서 작성해야합니다. 예를 들어 찾고자하는 단어가 aaaab이고 보드에는 전부 a만 있다고 가정합시다. 이런 경우 위 알고리즘은 모든경우를 탐색하기에 8^4의 경우의 수를 탐색합니다. 즉 단어의 길이가 n 일때 8^(n-1)개의 경우의 수를 탐색하기에 하나의 단어를 찾는데 O(8^n)의 시간복잡도가 기대됩니다. 이는 기하급수적으로 경우의 수가 상승하므로 단어의 길이가 길고 보드의 크기가 클 경우 다른 알고리즘을 사용하는 것이 좋습니다.

들어가며

지난 시간에는 케라스로 간단한 이미지 분류기 모델을 구축,훈련,평가 그리고 결과를 시각화하는 방법에 대해서 알아보았습니다. 이번 시간에는 케라스를 이용해서 회귀 다층퍼셉트론 모델을 만들어보겠습니다.

 

목차

1. Regression MLP 구현 방안

2. Sequential API를 이용해서 모델 구현

3. 시각화

1. Regression MLP 구현 방안

Regression MLP를 구현하는 방법은 이전 글에서 이미지 분류기 모델을 만들었다면 매우 간단히 해결할 수 있습니다.

회귀는 수치를 예상하는 것이므로 훈련전에 스케일링을 하고 모델의 출력층에서 활성화함수를 사용하지 않으면 쉽게 구현할 수 있습니다. Sequentail API를 이용해서 직접구현해보겠습니다. (Sequential API를 모른다면 이전 글을 참조하시기바랍니다.)

2. Sequential API를 이용해서 모델 구현

여기서는 예시로 california_housing데이터를 이용해서 주택 가격을 예측하는 모델을 구현해보겠습니다.

우선, 데이터를 다운로드하고 세트를 나눈 다음 스케일링을 해보겠습니다.

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

housing = fetch_california_housing()

#훈련, 검증 그리고 테스트 세트 추출
X_train_full, X_test, y_train_full, y_test = train_test_split(housing.data, housing.target, random_state=42)
X_train, X_valid, y_train, y_valid = train_test_split(X_train_full, y_train_full, random_state=42)

#회귀를 위한 스케일링 전처리
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_valid = scaler.transform(X_valid)
X_test = scaler.transform(X_test)

데이터가 준비되었으니 은닉층 1개(뉴런 수는 30)와 출력층 1개로 구성된 MLP를 Sequential API로 구축,컴파일 그리고 훈련과 평가를 수행합니다.

model = keras.models.Sequential([
    keras.layers.Dense(30, activation="relu", input_shape=X_train.shape[1:]),
    keras.layers.Dense(1)
])
model.compile(loss="mean_squared_error", optimizer=keras.optimizers.SGD(lr=1e-3))
history = model.fit(X_train, y_train, epochs=20, validation_data=(X_valid, y_valid))
mse_test = model.evaluate(X_test, y_test)
X_new = X_test[:3]
y_pred = model.predict(X_new)

3. 시각화

위 데이터를 시각화해봅시다.

plt.plot(pd.DataFrame(history.history))
plt.grid(True)
plt.gca().set_ylim(0, 1)
plt.show()

들어가며

지난 글까지 인공 신경망과 학습 모델들에 대해서 알아보고 이를 사이킷런으로 구현해 보았습니다. 

이번에는 google 의 tensorflow에서 지원하는 keras라이브러리를 사용해서 간편하게 신경망을 구현, 학습, 그리고 평가하는 방법에 대해서 알아보겠습니다.

 

목차

1. 텐서플로2 설치

2. Sequential API로 이미지 분류기 구현하기

    1. 데이터 다운로드

    2. Sequential Model 구축

    3. 모델 알아보기

    4. 모델 컴파일하기

    5.모델 학습하기

    6. 학습 시각화하기

    7. 테스트 데이터로 평가하기

    8. 예측하기

 

Tensorflow 2.0 설치하기

tensorflow 2.0을 설치하는 환경이 여러 가지가 있으므로 이는 tensorflow.org에 게재된 공식문서를 참조하는 것이 좋습니다.

https://www.tensorflow.org/install/pip?hl=ko

 

pip를 사용하여 TensorFlow 설치

TensorFlow 2 패키지 사용 가능 tensorflow - CPU와 GPU 지원이 포함된 안정적인 최신 출시(Ubuntu 및 Windows) tf-nightly - 미리보기 빌드(불안정). Ubuntu 및 Windows에는 GPU 지원이 포함되어 있습니다. 이전 버전의 TensorFlow TensorFlow 1.x의 경우 CPU와 GPU 패키지는 다음과 같이 구분됩니다. tensorflow==1.15 - CPU 전용 출시 tensorflow-gpu=

www.tensorflow.org

Seauential API로 이미지 분류기 만들기

1. 데이터 다운로드

이미지 분류기를 만들기 전에 분류할 이미지 데이터를 다운해봅시다.

여기서는 fashion_mnist 데이터를 사용합니다. keras 라이브러리를 통해서 쉽게 다운이 가능합니다.

from tensorflow import keras

fashion_mnist = keras.datasets.fashion_mnist
(X_train_full, y_train_full), (X_test, y_test) = fashion_mnist.load_data()

데이터를 간단히 살펴봅시다.

하나의 패션 샘플은 28x28의 배열로 이루어져있으며 하나의 픽셀은 0~255의 정수로 이루어져 있습니다.

이미 데이터는 어느정도 전처리가 진행되어있으니 바로 keras를 통해 모델을 구축해봅시다.

2. Sequential Model 구축

keras.models.Sequential은 케라스로 MLP를 구현하는 가장 기본적이고 쉬운 방법입니다.

Sequential 클래스를 선언하고 층을 순서대로 추가함으로써 MLP구현이 가능합니다.

model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
model.add(keras.layers.Dense(300, activation="relu"))
model.add(keras.layers.Dense(100, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))

Flatten은 입력데이터의 shape를 일렬로 변경하는 클래스입니다.(reshape(-1,1)과 동일합니다.) Flatten은 선언하면 뉴런층이 생성되는 것이 아니라 단순히 데이터의 형태를 변경합니다. InputLayer(input_shape=[28, 28])를 통해서 대체가 가능합니다.

 

Dense는 이전 뉴런과 완전연결된 밀집 뉴런층을 의미합니다. 이를 선언하고 모델에 추가함으로써 모델 내부에 새로운 뉴런층을 생성할 수 있으며 첫 번째 파라미터로 뉴런 수를 서정하고 activation 파라미터를 통해서 활성화 함수를 지정할 수 있습니다.

 

마지막 Dense는 softmax를 활성화 함수를 사용하고 있는데 이는 우리가 원하는 모델은 분류기이기 때문에 각 샘플의 타깃레이블에 대한 예측확률을 측정하기 위해서입니다.

3. 모델 알아보기

모델을 생성했으니 이를 분석해봅시다.

model.summary()

summary를 통해서 모델의 정보를 대략 알 수 있습니다. 

hidden1 = model.layers[1]
hidden1.name #'dense'추출
weights, biases = hidden1.get_weights()

model에서 인덱스를 통해서 레이어를 추출하고 name을 통해서 이름을 알고 get_weight() 메서드를 통해서 weight와 bias를 알 수 있습니다. (set_weight()를 통해서 설정도 가능합니다.)

4. 모델 컴파일하기

keras모델은 층을 생성한 후 비용함수, 최적화방법을 설정하여 어떻게 학습을 진행할지 설정해주어야하는데 이를 컴파일이라고 합니다.

model.compile(loss="sparse_categorical_crossentropy",
              optimizer="sgd",
              metrics=["accuracy"])

분류기이기 때문에 CEE를 비용함수로 정하고 최적화 방법은 SGD를 사용하였습니다. 만약 SVM같은 다른 모델을 사용하고싶으면 loss="hinge"로 지정하는 등 다른 비용함수를 정하면 되며 최적화 방식은 optimizer를 설정하여 변경 할 수 있습니다.(AdaGrad 등이 있습니다.)

metrics를 통해서 학습을 진행할 때마다 출력할 평가방식을 지정할 수 있습니다.

5.모델 학습하기

history = model.fit(X_train, y_train, epochs=30,
                    validation_data=(X_valid, y_valid))

fit 메서드를 통해서 학습시킬수 있으며 이 때 훈련 샘플,레이블 에폭수 그리고 검증 샘플,레이블을 입력할 수 있습니다.

fit 메서드를 실행하면 위와 같이 학습을 진행하며 에폭이 끝날 때마다 훈련데이터와 검증데이터의 손실함수 그리고 metrics에서 지정한 평가점수를 출력합니다.

학습이 끝나면 fit 메서드는 학습과정의 데이터를 저장한 history 클래스를 리턴합니다.

6. 학습 시각화하기

fit 메서드에서 리턴받은 history를 통해서 쉽게 구현가능합니다.

import pandas as pd

pd.DataFrame(history.history).plot(figsize=(8, 5))
plt.grid(True)
plt.gca().set_ylim(0, 1)
save_fig("keras_learning_curves_plot")
plt.show()

출처 : https://github.com/ageron/handson-ml2/blob/master/10_neural_nets_with_keras.ipynb

7. 테스트 데이터로 평가하기

evalute()메서드를 통해서 평가 할 수있습니다.

model.evaluate(X_test, y_test)
'''
result :
10000/10000 [==============================] - 0s 21us/sample - loss: 0.3378 - accuracy: 0.8781
'''

8. 예측하기

predict()메서드를 통해서 새로운 샘플을 예측하고 클래스별 확률을 알 수 있습니다.

X_new = X_test[:3]
y_proba = model.predict(X_new)
y_proba.round(2)
'''
result:

array([[0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.01, 0.  , 0.99],
       [0.  , 0.  , 0.99, 0.  , 0.01, 0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.  , 1.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ]],
      dtype=float32)
'''

predict_calsses()메서드를 통해서 예측 클래스만 출력할 수 있습니다.

y_pred = model.predict_classes(X_new)
y_pred
'''
result:
array([9, 2, 1])
'''

들어가며

지난 글에서는 로지스틱 함수를 사용하여 두 개의 클래스를 구별하는 이진 분류기에 대해서 다루어보았습니다. 이번에는 여러개의 클래스를 한 번에 분류할 수 있는 소프트맥스 회귀에 대해서 알아보도록 하겠습니다.

목차

1. 소프트맥스의 정의

2. 비용함수(크로스 엔트로피 비용 함수)

3. 코드 구현

1. 소프트맥스의 정의

소프트맥스 회귀 또는 다항 로지시틱 회귀는 소프트 맥스 함수(softmax function)을 사용하여 각 클래스일 확률을 추정하여 이 중 가장 높은 확률을 가진 클래스를 선택하는 것입니다. 이를 쉽게 알아보겠습니다.

 

우선 각 클래스는 자신만의 파라미터 벡터가 존재하며 새로운 샘플이 입력되었을 경우 각 클래스의 벡터와 샘플의 곱을 계산하여 이 중 가장 큰값을 선택하는 방식입니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

따라서 위와 같은 식이 성립되고 s(x)는 대게 확률이 아니라 일정값이 출력됩니다. 따라서 이를 확률값으로 추출하기 위해서 소프트맥스 함수를 사용합니다.

결국 추정된 클래스는 확률이 가장높은 = s(x)의 값이 가장 높은 클래스가 정해지기에 사실상 클래스만 추정하고 확률은 필요없을 때 마지막에 소프트맥스 함수를 사용하는것은 생략해도됩니다.

2. 비용함수

클래스를 추정하는 방법은 간단히 알아보았으니 이번에는 모델을 학습시키기 위해서 비용함수를 구하는 방법에 대해서 알아보도록 하겠습니다.

여기서 사용할 비용함수는 크로스 엔트로피 함수(cross-entropy function)으로서 타킷 클래스와 예측확률값의 로그값을 곱하것들의 합에 부호를 바꾼것을 의미합니다.

이렇게 한다면 타킷 클래스에 대한 확률값이 감소할 수록 비용함수는 증가하는 구조를 가지고 있기에 비용함수로 적절합니다.

(y_k(i)는 해당 클래스라면 1 다른 클래스라면 0을 나타내는 one-hot 인코딩된 레이블을 의미합니다.)

 

자 이제 비용함수를 구했으니 이에 대한 gradient vector(편도함수)를 구해보도록 하겠습니다.

gradient vector를 보니 비용함수는 복잡했지만 이는 단순히 예측확률에 타깃값을 제외하고 샘플을 곱한 것입니다. 이렇게 단순해진이유는 사실 고의적으로 수학적 기교를 이용해서 의도했기 때문입니다. (안그러면 굳이 확률을 구하기위해 exp를 하거나 비용함수로 로그값을 사용하지 않아도됩니다.) 따라서 매번 학습을 진행할 때 간단한 연산을 통해 빠르게 학습이 가능합니다.

3.코드구현

마지막으로 소프트맥스 회귀를 코드로 구현해보겟습니다. 소프트맥스 회귀는 따로 모듈이 존재하지 않고 LogisticRegression에서 multi_class="multinomial', solver="lbfgs"를 설정함으로써 구현가능합니다.

y = iris["target"]

softmax_reg = LogisticRegression(multi_class="multinomial",solver="lbfgs", C=10, random_state=42)
softmax_reg.fit(X, y)

예측값을 통해 결정경계 표시 그래프 생성

x0, x1 = np.meshgrid(
        np.linspace(0, 8, 500).reshape(-1, 1),
        np.linspace(0, 3.5, 200).reshape(-1, 1),
    )
X_new = np.c_[x0.ravel(), x1.ravel()]


y_proba = softmax_reg.predict_proba(X_new)
y_predict = softmax_reg.predict(X_new)

zz1 = y_proba[:, 1].reshape(x0.shape)
zz = y_predict.reshape(x0.shape)

plt.figure(figsize=(10, 4))
plt.plot(X[y==2, 0], X[y==2, 1], "g^", label="Iris-Virginica")
plt.plot(X[y==1, 0], X[y==1, 1], "bs", label="Iris-Versicolor")
plt.plot(X[y==0, 0], X[y==0, 1], "yo", label="Iris-Setosa")

from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(['#fafab0','#9898ff','#a0faa0'])

plt.contourf(x0, x1, zz, cmap=custom_cmap)
contour = plt.contour(x0, x1, zz1, cmap=plt.cm.brg)
plt.clabel(contour, inline=1, fontsize=12)
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.legend(loc="center left", fontsize=14)
plt.axis([0, 7, 0, 3.5])
save_fig("softmax_regression_contour_plot")
plt.show()

들어가며

지난 시간에는 선형회귀에 이어 다항회귀에 대해서 알아보았습니다. 이번에는 회귀를 통해서 분류(Clustering)을 진행할 수 있는 알고리즘인 Logistic Regression 에 대해서 알아보도록 하겠습니다. 

 

목차

1. Logistic Regression

    1.1 확률추정

    1.2 훈련과 비용함수

    1.3 결정경계

 

1. Logistic Regression

로지스틱 회귀란 기본적으로 회귀 알고리즘을 사용하지만 마지막 결과값에서 로지스틱 함수를 통해서 확률을 계산하고 이를 토대로 분류를 진행하는 알고리즘입니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

위 수식을 보면 파라미터와 샘플의 곱까지는 기존의 회귀와 동일하지만 이를 감싸는 새로운 함수를 볼 수 있습니다. 이것이 바로 로지스틱 함수입니다.

로지스틱 함수는 변수를 입력할 경우 이를 0~1의 범위로 변환해주는 함수 입니다. 즉 이 함수를 통해서 확률값을 계산하고 마지막으로 분류(어떤 클래스에 대한 확률이 가장 높은지 계산)할 수 있습니다.

위 계산식 처럼 마지막 결과값이 50%이상인경우는 1 50%미만인 경우는 0으로 출력하여 분류를 할 수 있습니다.

1.1 확률추정

확률계산하는 방법을 알았으니 이제 파라미터가 학습을 하기 위해서는 비용함수를 구해야합니다. 

하나의 훈련 샘플에 대한 비용함수는 클래스가 0인 경우 결괏값이 1로 갈 수록 증가해야하며 1인 경우는 결괏값이 0으로 갈수록 증가해야합니다. 따라서 아래의 공식이 적합합니다.

1.2 훈련과 비용함수

하나의 샘플에 대한 비용함수를 알았으니 전체 비용함수는 모든 샘플에 대한 비용함수의 평균을 구하면 됩니다.

Logistic 비용함수(log loss)는 선형 회귀와는 다르게 정규방정식이 존재하지 않습니다. 그러나 log loss는 볼록함수이기 때문에 경사항강법으로 global minimum에 수렴하는 것이 보장됩니다.

 

꽃잎에 대한 데이터가 담긴 Iris데이터를 예로 들어 구현해보겠습니다.

# iris데이터를 불러옵니다.
from sklearn import datasets
import numpy as np
iris = datasets.load_iris()
list(iris.keys())

# 꽃잎의 너비와 샘플의 클래스가 Iris-Vriginica인 경우의 인덱스를 1로 아니면 0으로하는 리스트를 생성
X = iris["data"][:, 3:]  # petal width
y = (iris["target"] == 2).astype(np.int)  # 1 if Iris-Virginica, else 0

# 선형 알고리즘을 사용하기 위해 solver="liblinear" 호출후 데이터를 학습시킵니다.
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(solver="liblinear", random_state=42)
log_reg.fit(X, y)

# 그래프로 출력해봅니다.
X_new = np.linspace(0, 3, 1000).reshape(-1, 1)
y_proba = log_reg.predict_proba(X_new)

plt.plot(X_new, y_proba[:, 1], "g-", linewidth=2, label="Iris-Virginica")
plt.plot(X_new, y_proba[:, 0], "b--", linewidth=2, label="Not Iris-Virginica")

코드를 통해서 꽃잎의 넓이를 통해서 종류를 예측하는 모델을 생성했습니다.

1.3 결정경계

위 그래프는 이전 그래프에서 내용을 조금 추가한 것입니다. 여기서 중요한 점은 두 곡선이 교차하는 결정경계입니다. 훈련된 모델을 Petal width가 1.6 근처인 지점에서 결과값이 바뀌는 것을 알 수 있습니다.

들어가며

지난글에서는 학습곡선을 통해서 모델 학습이 과대적합인지 과소적합인지 파악하여 모델을 결정하는방법에 대해서 알아보았습니다. 과소적합인 경우에는 더 좋은 모델과 하이퍼파라미터를 선택하면되지만 과대적합인 경우에는 어떻게 해결해야할까요? 이번 글에서는 비용함수에 변수를 추가하여 모델을 규제하여 과대적합을 방지하는 방법에 대해서 알아보겠습니다.

 

목차

1. 과대적합 발생이유

2. 과대적합 해소방법

    1.릿지 회귀(티호노프 규제)

    2.라쏘 회귀(Lasso Regression)

    3.엘라스틱넷

    4.조기종료

 

3.1 과대적합 발생이유

과대적합이 발생하는 이유는 각 파라미터 변수들의 절댓값이 크게 설정되기 때문입니다. 아래 그림은 이후에 설명할 규제의 정도에 따른 학습결과인데 미리 이를 예로 설명하겠습니다. 오른쪽 그림은 차수를 10차로 설정한 그림입니다. 파란색 곡선을 보면 다른 곡선보다 데이터를 따라가기 위해서 과하게 출렁이는 모습을 볼 수 있습니다. 이런 경우가 바로 파라미터(가중치, 각 항의 계수)들의 절댓값이 크기에 발생합니다. 

3.2. 과대적합 해소방법

위에서 파라미터의 절댓값이 크기에 과대적합이 발생하는 사실을 알았습니다. 그렇다면 과대적합을 해소하기 위해서는 인위적으로 파라미터의 절댓값을 감소시켜야합니다. 이를 위한 방법은 여러가지가 있습니다. 이를 차례대로 알아보도록 하겠습니다.

3.2.1 릿지회귀( 티호노프 회귀 )

첫 번째 방법은 학습에 사용하는 비용함수에 L2노름을 추가하는 것입니다. 이렇게 하면 편도함수를 구할 때 기존의 MSE 편도함수 뿐만아니라 알파와 해당 파라미터값을 곱한 값을 합하여 구하게 될 것입니다. 이는 곧 파라미터 갱신에 사용되므로 결과적으로 L2노름을 사용하지 않았을 때보다 파라미터의 절댓값 변화가 낮아지게되는 것입니다.

위 그림을 정확히 설명하자면 릿지 회귀에서 알파값에 따른 곡선의 변화를 나타냅니다. 알파값이 증가할 수록 각 파라미터가 낮아져 곡선의 복잡성이 낮아져 오버피팅이 줄어드는 것을 확인할 수 있습니다. 참고로 릿지 회귀는 특성의 범위에 민감하게 반응하므로 학습이전에 StandardScaling이 필요합니다.

코드구현

코드로는 sklearn.linear_model에서 직접 Ridge모듈을 선택하거나 SGDRegressor에서 penalty를 l2로 설정하는 두 가지 방법이 있습니다. Ridge에서 solver를 cholesky로 설정할 경우 정규방정식을 통해서 학습을 진행합니다.

1. Ridge

from sklearn.linear_model import Ridge
ridge_reg = Ridge(alpha=1, solver="cholesky", random_state=42)
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])
#result = array([[1.55071465]])

2. SGDRegressor

sgd_reg = SGDRegressor(max_iter=50, tol=-np.infty, penalty="l2", random_state=42)
sgd_reg.fit(X, y.ravel())
sgd_reg.predict([[1.5]])
# resutl = array([1.49905184])

3.2.2 Lasso 회귀

Lasso는 least absolute shrinkage and selection operator의 줄임말로서 쉽게 해석하면 중요한 특성에 맞춰서 축약한다는 소리입니다. 우선 Lasso는 릿지회귀와 비슷하게 비용함수에 변수를 추가하는데 이 때는 L1노름을 추가합니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

이제 위 비용함수가 어떻게 특성을 축약하는지 알아봅시다. 위 비용함수를 파라미터 갱신에 사용한다고 생각합시다. 편도함수를 구하면 새로 추가한 L1노름 부분은 알파값과 해당 파라미터의 양,음에 따라서 부호가 정해질 것입니다. 즉 갱신을 할 때 모든 파라미터들은 알파값이라는 동일한 값을 통해서 갱신이 진행됩니다. 따라서 초기값이 작은 파라미터의 경우 먼저 0으로 수렴하게 됩니다. 아래의 그림으로 더 쉽게 설명하겠습니다.

왼쪽 위의 경우에서 둥근 등고선은 MSE비용함수를 나타내고 삼각형은 L1노름을 나타냅니다. 이 부분에서 노란 삼각형은 초기에 (0.25,-1)부분에서 파라미터가 시작한 경우 갱신이 진행되는 과정을 설명하고있는 것입니다. L1노름의 경우에는 기울기의 절댓값이 1로 각 파라미터의 절댓값이 감소하는 것이 보입니다. (기울기가 1인 이유는 변수 알파에 의해서만 갱신이 되기 때문입니다.) 이렇게 진행되다가 세타1이 먼저 0으로 수렴하고 이후 다시 세타2가 0으로 수렴하고 있는 모습입니다. 즉 L1노름이 계속 갱신하게 되면 별로 중요하지 않은 파라미터는 먼저 0으로 수렴하여 삭제되고 이 후 살아남은 파라미터가 모델을 학습하는 모습을 볼 수 있습니다. 최종적으로 오른쪽 두 개의 그래프를 살펴보면 Lasso는 별로 중요하지 않은 세타2는 삭제한 반면 릿지는 모든 파라미터를 활용하여 최적화가되는 모습을 볼 수 있습니다.

 

이러한 특성때문에 중요한 몇개의 특성만으로 모델을 표현하고 싶은 경우(특성 축소)에 라쏘를 선택할 수 있지만 릿지만큼의 정확성을 기대하는것은 힘들 수 있습니다.

 

코드구현

라쏘 또한 릿지함수와 마찬가지로 sklearn.linear_model에서 Lasso모듈을 사용하여 구현할 수 있습니다.

 

from sklearn.linear_model import Lasso

plt.figure(figsize=(8,4))
plt.subplot(121)
plot_model(Lasso, polynomial=False, alphas=(0, 0.1, 1), random_state=42)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.subplot(122)
plot_model(Lasso, polynomial=True, alphas=(0, 10**-7, 1), tol=1, random_state=42)

save_fig("lasso_regression_plot")
plt.show()

3.2.3 엘라스틱넷

엘라스틱넷은 한 마디로 릿지와 Lasso회귀를 섞어놓은 것을 의미합니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

릿지와 라쏘를 이해했다면 위 공식은 아주 쉬울것입니다. 혼합비율 상수 r을 사용하여 릿지와 라쏘를 조율하여 둘 다 사용하고있는 모습입니다. 보통 라쏘보다 엘라스틱넷이 더 선호되는데 그 이유는 만약 샘플수가 특성수 보다 많은 경우나 특성들이 강한 상관관계를 지닌경우 Lasso가 제대로 작동하지 않기 때문입니다.

 

코드구현

from sklear.linear_model import ElasticNet

elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5)
elastic_net.fit(X,y)
elastic_net.predict([[1.5]])
# array([1.54333xxx])

3.2.4 조기종료 (early stopping)

조기종료는 말 그대로 학습을 진행하는 와중 학습을 종료하는 것입니다. 이는 아주 간단한 개념이면서도 좋은 결과를 얻을 수 있는데 점진적으로 학습이 진행될때 검증 세트의 결과값이 최소이며 앞으로 더 좋은 결과가 기대대지 않을경우 해당 최소 구간에서 학습을 종료하는 것을 의미합니다.

들어가며

이전 글에서는 데이터 간의 비선형관계를 구하기 위한 다항회귀에 대해서 알아보았습니다. 이제 데이터에 적합한 차수(degree( 2차, 3차 등 ))를 어떻게 구할지 고민해야합니다. 이번에는 학습곡선을 통해서 다항회귀를 할때 차수를 적절하게 구하는 방법에 대해서 알아보도록 하겠습니다.

목차

1. 차수의 중요성

2. 학습곡선을 통한 적합성파악

3. 모델의 일반화 오차

 

1. 차수의 중요성

출처 : https://github.com/ageron/handson-ml2/blob/master/04_training_linear_models.ipynb

위 그림은 이전 글(다항회귀)에서 사용한 2차곡선형의 데이터 샘플입니다. 만약 Degree를 300이나 1로 지정할 경우 어떻게 되는지 한번 알아보도록 하겠습니다.

from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

for style, width, degree in (("g-", 1, 300), ("b--", 2, 2), ("r-+", 2, 1)):
    polybig_features = PolynomialFeatures(degree=degree, include_bias=False)
    std_scaler = StandardScaler()
    lin_reg = LinearRegression()
    polynomial_regression = Pipeline([
            ("poly_features", polybig_features),
            ("std_scaler", std_scaler),
            ("lin_reg", lin_reg),
        ])
    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()

코드 구현결과를 보면 300차인 경우 2차에 비해서 데이터에 최대한 맞추려고 계속 지그재그로 진행하는 모습이 보입니다. 

반면 1차의 경우 2차에 비해서 데이터에 비적합한 모습을 보입니다. 즉 너무 degree가 높은 경우 데이터에 과대적합되며 너무 낮은 경우 과소적합이 될 수 있습니다. 이번에는 학습곡선을 이용해서 이 적합도를 한 눈에 파악하도록 구현하겠습니다.

2. 학습곡선을 통한 적합성파악

데이터에서 훈련,검증 세트를 추출하고 훈련세트를 하나부터 점차 학습시키면서 RMSE를 구하여 이를 그래프로 나타내는 학습곡선을 구현하도록 하겠습니다. (여기서는 PolynomialFeatures를 사용하지 않으므로 degree를 1차로 진행하는 직선방정식이라고 생각하시면됩니다.)

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split

def plot_learning_curves(model, X, y):
    X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=10)
    train_errors, val_errors = [], []
    for m in range(1, len(X_train)):
        model.fit(X_train[:m], y_train[:m])
        y_train_predict = model.predict(X_train[:m])
        y_val_predict = model.predict(X_val)
        train_errors.append(mean_squared_error(y_train[:m], y_train_predict))
        val_errors.append(mean_squared_error(y_val, y_val_predict))

    plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
    plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")
    plt.legend(loc="upper right", fontsize=14)   # not shown in the book
    plt.xlabel("Training set size", fontsize=14) # not shown
    plt.ylabel("RMSE", fontsize=14)
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3])                         # not shown in the book
save_fig("underfitting_learning_curves_plot")   # not shown
plt.show() 

곡선을 보니 사이즈가 15이하인 경우까지는 학습이 점차 진행되어 훈련세트와 검증세트의 괴리가 줄어드는 것이 보입니다. 그러나 그 이후 만나는 지점이 1.5 ~ 2.0으로 둘 다 상당한 오차가 발생하는 것으로 보입니다. 이는 전형적인 과소적합의 형태입니다. 이번에는 degree를 10정도로 지정하여 학습곡선을 그려보겠습니다.

from sklearn.pipeline import Pipeline

polynomial_regression = Pipeline([
        ("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
        ("lin_reg", LinearRegression()),
    ])

plot_learning_curves(polynomial_regression, X, y)
plt.axis([0, 80, 0, 3])           # not shown
save_fig("learning_curves_plot")  # not shown
plt.show()   

이번에는 두 곡선의 평균점이 1내외로 이전보다는 나아진것으로 보입니다. 그러나 45부터는 훈련 세트가 증가해도 검증세트와 훈련세트의 RMSE 괴리가 줄어들지 않는 것을 알 수 있습니다. 이는 전형적인 과대적합 모델입니다. 이렇게 degree를 바꿔가며 학습곡선을 파악하면 최적화된 degree를 구할 수 있습니다.

3. 모델의 일반화 오차

모델의 예측값과 결과값 사이의 오차는 일반적으로 3가지 요인을 들 수 있습니다.

1. 편향 : 데이터를 분석할 모델을 잘못 가정하는 것을 의미합니다. 모델 자체의 종류를 잘못 선택하거나 위에서처럼 하이퍼파라미터(degree)를 잘못 설정한 경우입니다. 보통 모델이 복잡할 수록 편향 또한 증가합니다.

2. 분산 : 데이터의 분포정도에 따른 오차발생 정도입니다. 보통 모델이 간단할 경우 예측값과의 분산 정도가 증가합니다.

3. 가우시안 노이즈 : 데이터 자체의 노이즈 정도입니다.

들어가며

이전 글에서는 샘플과 결괏값 사이의 관계를 1차식으로 구하는 선형 회귀에 대해서 알아보았습니다. 선형회귀만으로는 2차 이상의 관계를 구할 수 없으므로 이번에는 다항관계를 구하는 다항회귀에 대해서 알아보도록 하겠습니다.

목차

  1. 정의
  2. 코드 구현
  3. 다항회귀의 특징

1.1정의

앞서 말했듯 다항회귀는 2차 이상의 방정식을 통한 관계 구현입니다. 이를 구하기 위해서는 기존 특성의 거듭제곱을 구하고 이를 새로운 특성으로서 샘플에 추가한 후 선형회귀(LinearRegression)를 구하는 것입니다. 

이렇게 한 경우 x^2과 x^1을 서로 다른 특성으로 인식하여 선형회귀는 두 개의 특성에 맞는 결과를 구하기 위해 정규방정시을 사용합니다. 코드구현을 통해서 쉽게 알아보도록 하겠습니다.

 

1.2 코드구현 

1. 2차방정식 형태의 데이터 생성

import numpy as np
import numpy.random as rand

np.random.seed(42)

m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)

plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
save_fig("quadratic_data_plot")
plt.show()

2. PolynomialFeatures모듈을 사용하여 거듭제곱 형태의 특성 추가

from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly_features.fit_transform(X)
X[0] 
#result = array([-0.75275929])

X_poly[0]
#result = array([-0.75275929,  0.56664654])

3. LinearRegression모듈을 통해서 선형관계 분석

lin_reg = LinearRegression()
lin_reg.fit(X_poly, y)
lin_reg.intercept_, lin_reg.coef_

#resutl = (array([1.78134581]), array([[0.93366893, 0.56456263]]))

4. 최종 결과 

1.3 다항회귀의 특징

PolynomialFeatures 모듈에 데이터를 입력할 때 두 개 이상의 데이터를 입력할 경우(x, y, z ... ) 이 모듈은 각 데이터만의 거듭제곱이 아니라 서로 다른 데이터들의 교차항(xy, x^2 y, xy^2 ... )을 추가하므로 특성 사이의 관계를 파악하는데 용이합니다.

하지만 입력 데이터의 수와 거듭제곱(degree)의 수가 증가할 수록 교차항이 기하급수적으로 증가하여 학습 시간 또한 기하급수적으로 증가하므로 이를 유의해야합니다.

목차

  • 들어가며
  • 선형회귀의 정의
  • 파라미터 계산
    • 정규방정식
      • 정규방정식의 장단점
    • 경사하강법
      • 정의
      • 학습률로인한 장단점
      • 배치 경사 하강법 
      • 확률적 경사 하강법 
      • 미니배치 경사 하강법 

 

들어가며

이번 글에서는 머신러닝의 모델 중 하나인 선형회귀의 정의와 모델의 파라미터를 최적화하는 방법들에 대해서 설명하도록 하겠습니다.

 

선형회귀의 정의

선형회귀란 주어진 데이터( x는 샘플, y는 예측값)들을 사용해서 샘플과 예측값과의 관계를 직선으로 표현하는것을 의미합니다. 직선의 방정식을 구하기 위해서는 하나의 샘플에서 각 특성에 곱할 기울기들과 편향이 필요합니다. 우리는 주어진 데이터들을 통해서 해석적으로 또는 경사하강법같은 계산을 통해서 파라미터(기울기, 편향)을 파악할 수 있습니다.

 

파라미터 계산

1. 정규방정식

선형회귀는 최적화된 파라미터를 공식을 통해서 해석적으로 구할 수 있습니다. 이를 정규방정식이라고 부릅니다.

파이썬으로 이를 구현해보겠습니다.

 

1.1. 선형으로 보이는 데이터를 생성합니다.

https://colab.research.google.com/drive/1cf_cY1zLCwaKzp73BuoYKZqfhHEtu7ga#scrollTo=txyqCXxO5-tp&line=6&uniqifier=1

 

Google Colaboratory

 

colab.research.google.com

1.2. 정규방정식으로 파라미터를 계산합니다. 

[4,3]의 결괏값을 원했지만 가우시안 노이즈 때문에 살짝 오차가 존재합니다.

1.3. 모델의 예측을 그래프에 나타내보겠습니다.

1.4. 동일한 작업을 사이킷런 라이브러리를 통해 작업합니다.

정규방정식의 장단점

우선 정규방정식의 장점은 선형회귀에 사용되는 여러방법중 가장 정확하게 파라미터를 계산할 수 있다는 것입니다. 이는 해석적으로 계산하기에 당연한 결과입니다.

단점은 이를 계산하기 위한 컴퓨팅자원의 한계입니다.

1. 정규방정식으로 계산을 하기 위해서는 램에 모든 샘플들을 올린 다음 계산할 필요가 있습니다. 만약 샘플의 크기가 매우 크기에 램이 부족하다면 이런 방법은 어려움이 있습니다.

2. 시간복잡도의 기하학적 증가

정규방정식의 시간복잡도는 O(n^2.4) ~ O(n^3) [ n : 행렬의 크기 ] 정도로 샘플의 행렬이 증가할 경우 기하학적으로 계산시간이 증가하게 됩니다. 

 

따라서 소규모의 샘플 계산인 경우 정규방정식을 사용하는 것이 좋지만 자원의 한계에 부딪힐 정도로 샘플의 수가 많은 경우 다른 방법을 사용하는 것이 좋습니다.

2. 경사하강법( Gradinet Descent)
2.1.정의

경사하강법은 비용함수를 최소화하기 위해 비용함수의 편도함수를 이용해서 파라미터를 조정하는것입니다.

위 정의를 하나씩 풀어 봅시다. 우선 비용함수는 쉽게 말해서 예측값과 실제 정답의 차이를 의미하는 함수를 의미합니다. 이 함수는 여러종류가 있지만 여기서는 MSE(평균 제곱 오차,Mean Squared Error)로 다루어보도록 하겠습니다.

위 식은 각 샘플의 예측값과 정답의 차이를 제곱하여 평균을 나타내는 것을 의미합니다. 

즉 비용함수(손실함수)가 낮을 수록 예측이 정확하다는 것을 의미합니다. 비용함수를 낮추기 위해서는 함수의 편도함수를 구하여 기울기의 방향을 파악하고 비용함수가 낮아지는 방향으로 파라미터를 수정하면됩니다.

좋습니다. 비용함수의 편도함수로 파라미터를 조정할 방향을 알아보았습니다. 이번에는 방향을 알았으니 얼마나 움직여야할지 크기(학습률)에 대해서 알아보겠습니다.

2.2학습률로 인한 장단점

우선 학습률이 작을 때 입니다.

학습률이 작을 경우 매번 스텝마다 매우 작게 움직이기 때문에 파라미터가 최적화되기에 오랜시간이 소요됩니다.

 

이번에는 학습률이 평균보다 클 때입니다.

학습률이 매우 큰 경우 비용함수의 최소점에 수렴하지 못하고 지그재그로 이동하는 모습을 볼 수 있습니다. 

경사하강법의 문제점

학습률에 상관없이 경사하강법에는 비용함수가 볼록,오목함수가 아닌 경우에 전역 극소값이 아니라 지역 극솟값에만 머무르게 되어 학습이 정체되는 현상이 발생할 수 있습니다. 이러한 경우는 모멘텀 기반 방식으로 해결할 수 있습니다.
2.3 배치 경사 하강법

배치 경사 하강법은 경사 하강법의 가장 일반적인 구조로 한번 비용함수의 기울기를 계산할 때마다 모든 샘플을 사용하는것을 의미합니다.  이를 구현하기 위해서 모든 샘플에 대한 MSE의 편도함수를 우선 구해봅시다.

편도함수는 해석적으로 풀이할 경우 두 번째 공식을 사용하여 구할 수 있습니다. ( 배치 경사하강법 이므로 모든 샘플 데이터를 사용합니다.)

 

이를 코드로 구현해봅시다.

eta = 0.1  # 학습률
n_iterations = 1000 #반복횟수
m = 100 # 샘플의 갯수

weight = np.random.randn(2, 1)  # 초기 랜덤 파라미터 설정

for step in range(n_iterations): 
    gradients = 2/m * X_b.T.dot(X_b.dot(weight) - y) #기울기 계산
    weight = weight - eta * gradients # 학습률의 크기만큼 기울기 방향으로 이동
     
     #파라미터의 변화 파악
    if (step+1) % 200 == 0:
        print('Step :{:04d}, weight = \n {}'.format(step+1, weight))
    
print('최종 결과값 : \n{}'.format(weight))

약 1000번의 반복을 통해서 정규방정식으로 구한 파라미터의 최적화에 근사한 값을 도출했습니다. 반복횟수를 조절해서 시간의 소요를 조절할 수 있지만 한번의 계산에 모든 샘플을 사용하기에 이 또한 정규방정식처럼 램 용량을 초과할 수 있습니다. 이번에는 이를 해결하기 위해 확률적 경사 하강법에 대해서 알아보도록 하겠습니다.

2.4 확률적 경사하강법

확률적 경사 하강법(Stochastic Gradient Descent)은 매 스텝에서 랜덤하게 하나의 데이터(샘플)을 선택해 Gradient Descent(GD)를 계산하는 방식입니다. 이 방식은 배치 경사하강법에 비해 불안정하게 최적값으로 수렴하지만 시간과 사용되는 램용량을 단축할 수 있습니다. 

 

불안정하게 수렴할 경우 local minimum에서 탈출할 가능성이 있지만 반대로 global minimum에는 도착하지 못할 가능성이 있습니다. 이를 해결하기 위해서 SGD에서는 학습률을 점진적으로 감소시키는 Learning Rate Decay(learning rate schedule)기법을 사용합니다.

Learning Rate Decay기법

학습을 시작할 떄는 학습률을 크게 설정하고 점진적으로 이를 줄여 전역 최솟값에 도달하는 방식입니다.

 

이를 코드로 구현해보도록 하겠습니다.

 

n_epochs = 50
t0, t1 = 5, 50

def learning_schedule(t):
    return t0 / (t + t1)

weight = np.random.randn(2, 1)  # random init

for epoch in range(n_epochs):
    for i in range(m):
        if epoch == 0 and i < 20:
            y_predict = X_new_b.dot(weight)
            style = 'b-' if i > 0 else 'r--'
            plt.plot(X_new, y_predict, style)
            
        random_index = np.random.randint(m)
        xi = X_b[random_index:random_index+1]
        yi = y[random_index:random_index+1]
        gradients = 2 * xi.T.dot(xi.dot(weight) - yi)
        eta - learning_schedule(epoch * m + i)
        weight = weight - eta * gradients
        weight_path_sgd.append(weight)
        
    if (epoch+1) % 10 == 0:
        print('Epoch :{:03d}, weight = \n {}'.format(epoch+1, weight))
        
        
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()

에폭마다 학습률을 점진적으로 낮추면서 진행한 결과 파라미터가 최적값으로 수렴하는 것을 확인 할 수 있습니다.

SGD를 이용한 Regressor는 사이킷런에서 모듈을 지원하므로 이를 사용해 간단하게 계산할 수 있습니다.

from sklearn.linear_model import SGDRegressor

sgd_reg = SGDRegressor(max_iter=50, penalty=None, eta0=0.1, random_state=42)
sgd_reg.fit(X, y.ravel())
print(sgd_reg.intercept_, sgd_reg.coef_)
2.5 미니배치 경사하강법

미니배치 경사 하강법(Mini-batch Gradient Descent)은 각 스텝에서 전체 Train Set을 미니배치(mini-batch), 즉 작은 데이터셋을 추출한 뒤 Gradient를 계산하는 방법입니다. 

미니배치 경사 하강법은 SGD에 비해 덜 불규칙하게 감소하지만, local minimum에 빠질 확률은 높은 경우가 있습니다.

 

최종비교

위에서 설명햇듯이 3가지 방식을 비교해보면 배치경사하강법은 매번 파라미터 갱신 때마다 전체 샘플을 계산하므로 안정적으로 수렴하는 반면 SGD는 하나씩만 사용하기에 불안정한 모습을 볼 수 있습니다. 그리고 갱신때마다 일부 샘플만을 사용하는 미니배치의 경우에는 SGD보다는 좀 더 안정적으로 수렴하는 모습을 볼 수 있습니다.

+ Recent posts