페이지

2018년 8월 7일 화요일

3. 분류 3.1 MNIST

이 장에서는 고등학생과 미국 인구조사국 직원들이 손으로 쓴 70,000개의 작은 숫자 이미지를 모은 MNIST 데이터셋을 상요하겠습니다. 각 이미지에는 어떤 숫자를 나타내는지 레이블되어 있습니다. 이 데이터셋은 학습용으로 아주 많이 사용되기 때문에 머신러닝 분야의 'Hello World'라고 불립니다. 새로운 분류 알고리즘이 나올 때마다 MNIST 데이터셋에서 얼마나 잘 작동하는지 봅시다. 머신러닝을 배우는 사람이라면 머지않아 MNIST 데이터셋을 맞닥뜨리게 될 것입니다.

사이킷런에서 제공하는 여러 헬퍼 함수를 사용해 잘 알려진 데이터셋을 내려받을 수 있습니다. MNIST도 그 중 하나입니다. 다음은 MNIST 데이터셋을 내려받는 코드입니다.

사이킷런에서 읽어 들인 데이터셋들은 일반적으로 비슷한 딕셔너리 구조를 가지고 있습니다. 

3.3.3 정밀도와 재현율
사이킷런은 정밀도와 재현율을 포함하여 분류기의 지표를 계산하는 여러 함수를 제공합니다.

이제 '5-감지기'가 정확도에서 봤을 때만큼 멋져 보이지는 않네요. 5로 판별된 이미지 중 77%만 정확합니다. 더군다나 전체 숫자 5에서 80%만 감지했습니다.
정밀도와 재현율을 F1점수라고 하는 하나의 숫자로 만들면 편리할 때가 많습니다. 특히 두 분류기를 비교할 때 그렇습니다. F1점수는 정밀도와 재현율의 조화평균(harmonic mean)입니다.

F1 = 2/(1/정밀도 + 1/재현율) = 2 * (정밀도 * 재현율)/(정밀도 + 재현율) = TP/ (TP + (FN+FP)/2)

F1 점수를 계싼하려면 f1_score()함수를 호출하면 됩니다.

정밀도와 재현율이 비슷한 분류기에스는 F1점수가 높습니다. 하지만 이게 항상 바람직한 것은 아닙니다. 상황에 따라 정밀도가 중요할 수도 있고 재현율이 중요할수도 있습니다. 예를 들어 어린아이에게 안전한 동영상을 걸러내는 분류기를 훈련시킨다고 가정해보겠습지다. 재현율은 높으나 정말 나쁜 동양상이 몇 개 노출되는 것보다 좋은 동영상이 많이 제외되더라도(낮은 재현율) 안전한 것들만 노출시키는 (높은 정밀도)분류기를 선호할 것입니다(이런 경우에는 분류기의 동양상 선택 결과를 확인하기 위해 사람이 참여하는 분석 파이프라인을 추가할지도 모릅니다). 다른 예로, 감시 카메라를 통해 좀도둑을 잡아내는 분류기를 훈련시킨다고 가정해보겠습니다. 분류기의 재현율이 99%라면 정확도가 30%만 되더라도 괜찮을지 모릅니다(아마도 경비원이 잘못된 호출을 종종 받게 되겠지만, 거의 모든 좀도둑을 잡을 것입니다).

안됐지만 이 둘을 모두 얻을 수는 없습니다. 정밀도를 올리면 재현율이 줄고 그 반대도 마찬가지입니다. 이를 정밀도/재현율 트레이드오프 라고 합니다.

3.3.4 정밀도/재현율 트레이드오프
SGDClassifier가 분류를 어떻게 결정하는지 살펴보며 이 트레이드오프를 이해해보겠습니다.
이 분류기는 결정함수(decision function)를 사용하여 각 샘플의 점수를 계산합니다. 이 점수가 임곗값보다 크면 샘플을 양성 클래스에 할당하고 그렇지 않으면 음성 클래스를 할당합니다.[그림 3-3]에 가장 낮은 점수부터 가장 높은 점수까지 몇 개의 숫자를 나열했습니다. 결정 임곗값이 가운데(두 개의 숫자 5사이)화살표라고 가정해보겠습니다. 임곗값 오른쪽에 4개의 진짜 양성(실제 숫자5)과 하나의 거짓 양성(실제 숫자6)이 있습니다. 그렇기 때문에 이 임곗값에서 정밀도는 80%(5개 중 4개)입니다. 하지만 실제 숫자 5는 6개고 분류기는 4개만 감지했으므로 재션율은 67%(6개중 4개)입니다. 이번 입곗값을 높이면(임곗값을 오른쪽 화살표로 옮기면) 거짓 양성(숫자 6)이 진짜 음성이 되어 정밀도가 높아집니다(이 경우에 100%가 됩니다). 하지만 진짜 양성 하나가 거짓 음성이 되었으므로 재현율이 50%로 줄어듭니다. 반대로 임곗값을 내리면 재현율이 높아지고정밀도가 줄어듭니다.

사이킷런에서 임곗값을 직접 지정할 수는 없지만 예측에 사용한 점수는 확인할 수 있습니다. 분류기의 predict()메서드 대신 decision_function()메서드를 호출하면 각 샘플의 점수를 얻을 수 있습니다. 이 점수를 기반으로 원하는 임곗값을 정해 예측을 만들수 있습니다.

SDGClassifier의 임곗값이 0이므로 위 코드는 predict()메서드와 같은 결과(즉, True)를 반환합니다.

이 결과는 임곗값을 높이면 재현율이 줄어든다는 것을 보여줍니다. 이미지가 실제로 숫자 5이고 임곗값이 0일때는 분류기가 이를 감지했지만, 임곗값을 200,000으로 높이면 이를 놓치게 됩니다.

그렇다면 적절한 임곗값을 어떻게 정할 수 있을까요? 이를 위해서는 먼저 cross_val_predict()함수를 사용해 훈련 세트에 있는 모든 샘플의 점수를 구해야 합니다. 하지만 이번에는 예측 결과가 아니라 결정 점수를 반환받도록 지정해야 합니다.

이 점수로 precision_recall_curve()함수를 사용하여 가능한 모든 임곗값에 대해 정밀도와 재현율을 계산할 수 있습니다.



2018년 8월 3일 금요일

2. 장고의 클래스 기반 뷰와 하이퍼링크 API 사용

2.1 모델 직렬화기를 사용해 중복 코드 제거하기
GameSerializer 클래스는  Game 모델에서 사용한 것과 같은 이름을 가진 여러 속성을 선언하고 타입 및 max_length 값과 같은 정보를 반복한다. GameSerializer 클래스는 rest_Framework.serializers.Serializer의 서브 클래스이며, 수동으로 적절한 타입으로 매핑한 속성을 선언하고 create와 update메서드를 오버라이드한다.

이제 rest_framework.serializers.ModelSerializer 클래스에서 상속받을 GameSerializer 클래스의 새 버전을 만들것이다. ModelSeializer 클래스는 기본 필드 집합과 기본 유효성 검사기 집합을 자동으로 채운다. 또한 이 클래스는 create와 update메서드에 대한 기본 구현을 제공한다.

참고 웹 프레임워크에 대한 경험이 있는 경우, Serializer 및 ModelSerializer 클래스는 Form 및 ModelForm 클래스와 비슷하다는 것을 알아차릴 것이다.

이제 gamesapi/games 폴더로 가서 serializers.py파일을 연다. 이 파일의 코드를 GameSerializer 클래스의 새 버젼을 선언하는 아래 코드로 바꾼다.

새 GameSerializer 클래스는 model과 fields의 두 속성을 선언하는 Meta 내부 클래스를 선언한다. model 속성은 serializer와 관련된 모델, 즉 Game 클래스를 지정한다. fields 속성은 관련 모델의 직렬화에 포함시키는 필드 이름을 문자열의 튜플로 지정한다.

여기서는 일반 동작으로도 충분하기 때문에 create 또는 update 메서드를 오버라이드할 필요가 없다. ModelSerializer 슈퍼 클래스는 두 메서드 모두에 대한 구현을 제공한다.
우리는 GameSerializer 크랠스에서 필요하지 않은 사용구 코드를 줄였다. 단지 튜플에 원하는 필드 잡합을 지정하기만 하면 됐다. 이제 Game 클래스에는 게임필드와 관련된 타입만 들어가 있다.

Ctrl+C를 눌러 장고의 개발 서버를 종료하고 다음 명령을 실행해 다시 시작하라.
python manage.py runserver

2.2 API 뷰를 작성하기 위한 래퍼 작업
games/view.py파일의 코드는 JSONResponse 클래스와 2개의 함수 기반 뷰를 선언했다. 이들 함수는 응답이 JSON 데이터를 반환해야 할 때 JSONResponse를 반환하고, 응답이 HTTP 상태 코드일 때 django.Http.Response.HttpResponse인스턴스를 반환했다.

HTTP 요청 헤더에 지정된 허용 콘텐트 타입에 관계 없이 뷰 함수들은 항상 응답 본문에 동일한 콘텐트를 제공한다. 다음 두 명령을 실행해 Accept 요청 헤더에 대해 다른 값(text/html와 application/json)을 가진 모든 게임을 얻어보라

http :8000/games/ Accept:text/html
http :8000/games/ Accept:application/json

다음은 이와 동일한 curl명령이다.
curl -H 'Accept: text/html' -iX GET :8000/games/
curl -H 'Accept: application/json' -iX GET :80800/games/

위의 명령은 GET http://localhost:8000/games/라는 HTTP 요청을 작성해 보낸다.
첫 번째 명령은 Accept요청 헤더에 대해 text/html 값을 정의한다. 두번째 명령은 Accept 요청 헤더에 대해 application/json 값을 정의한다.

두 명령 모두 동일한 결과를 생성하므로이들 뷰 함수는 HTTP요청에 있어서 Accept 요청 헤더에 지정된 값을 고려하지 않는다. 두 명령의 헤더 응답에는 다음 행이 포함될 것이다.

Content-Type: application/json

두번째 요청은 text/html 만 허용하지만 응답에는 JSON 본문, 즉 application/json 콘텐트가 포함되게 지정됐다. 따라서 레스트풀 API 의 첫번째 버젼은 JSON과 다른 콘텐트를 렌더링할 준비가 돼 있지 않다. 우리는 API에서 다른 내용을 렌더링할 수 있게 약간 변경할 것이다.

레스트풀 API 에서 자원 또는 자원콜렉션이 지원하는 메서드를 잘 모르겠다면 OPTIONS HTTP 동사와 함께 그 자원 또는 자원 콜렉션의 URL이 들어간 HTTP요청을 작성해보내면 된다. 레스트풀 API에 자원 또는 자원 콜렉션에 대한 OPTION HTTP동사가 구현돼 있다면, 응답에 지원하는 메서드 또는 HTTP동사를 쉼표로 구분한 리스트를 Allow헤더의 값으로 나타내 준다. 또한 응답 헤더에는 요청에서 파싱할 수 있는 컨텐트 형식 및 응답에서 렌더링할 수 있는 콘텐트 형식 등 지원되는 다른 옵션에 대한 추가 정보도 나타난다.

예를 들어, 게임 컬렉션이 지원하는 HTTP 동사를 알고 싶다면 다음 명령을 실행하면 된다.

http OPTION :8000/games/

다은은 이와 동일하 curl 명령이다.
curl -ix OPTIONS :8000/games/

위의 명령은 OPTIONS http://localhost:8000/games/라는 HTTP 요청을 작성해 보낸다. 이 요청은 views.game_list 함수, 즉 games/views.py 파일 내에 선언된 game_list함수를 찾아 실행한다. 이 함수는 reqeust.method가 'GET' 또는 'POST'와 같은때만 코드를 실행한다. 여기서는 request.mthod가 'OPTIONS'와 같으므로 이 함수는 코드를 실행하지 않으며, 응답을 반환하지 않는다. 즉, HttpResponse 인스턴스를 반환하지 않는 것이다. 결과적으로 장고의 개발 서버 콘솔 출력으로 다음의 Internal Server Error가 나열될 것이다.

아래 행에는 장고에 디버그 모드가 활성화돼 있기 때문에 오류에 대한 자세한 정보가 들어간 세부적인 HTML 문서의 출력용 헤더가 나타나 있다. 500 Internal Server Error 상태 코드를 받은것이다.

분명히 우리는 보다 일관된 API를 제공하고자 하며, OPTIONS 동사가 게임 자원또는 게임 켈렉션에 대한 요청을 받을 때 정확한 응답을 제공하길 원한다.

게임 자원에 대해 OPTIONS 동사가 들어간 HTTP 요청을 작성해 보내면, views.game_detail 함수는 request.metohd가 'GET', 'PUT', 'DELETE'과 같을  때만 코드를 실행하기 때문에 동일한 오류를 나타내며, 서보 비슷한 응답을 보게 될 것이다.

아래 명령은 id또는 기본 키가 3인 게임 자원에 대해 제공되는 옵션을 보려고 할 때 설명이 들어간 오류를 생성할 것이다. 여기서 3은 여러분이 구성에 존재하는 게임의 기본 키 값으로 대체하는 것을 잊지말라.

http OPTIONS :8000/games/3/

다음은 이와 동일한 curl 명령이다.

curl -iX OPTIONS :8000/games/3/

레스트풀 API에 대해 분석한 이 문제를 해결하려면 games/view.py파일을 약간 변경해야 한다. 함수 기반 뷰에 대해서는 rest_framework.decorators에 선언된 @api_view데터레이터를 사용할 것이다. 이 테커레이터는 우리 함수가 처리할 수 있는 HTTP 동사를 지정할 수 있게 해준다. 뷰 함수로 처리해야 하는 요청에 지정 문자열ㄹ 리스트에 포함되지 않은 HTTP 동사가 @api_view 데커레이터의 http_method_names 인자로 들어가면, 기본 행위는 405 Method Not Allowed 상태 코드를 반환한다. 이렇게 되면 함수 뷰 내에서 고려되지 않은 HTTP 동사를 받을 때마다 지원되지 않는 HTTP 동사나 메서드에 대한 응답을 데커레이터가 처리하기 때문에 예기치 않는 오류가 발생하지 않을 것이다.

게다가 지원되는 HTTP 동사가 있는 문자열 리스트를 지정하면 데커레이터는 지원되는 메서드와 파서 및 렌더링 기능을 사용해 OPTIONS HTTP 동사에 대한 응답을 자동으로 만든다. API의 실제 버전은 JSON을 출려으로 렌더링할 수 있다. 데커레이터를 사용하면 장고가 뷰 함수를 호출할 때 항상 rest_framwrork.request.Request 클래스의 인스턴스를 request인자로 받을 수 있게 한다. 또한 데커레이터는 함수 뷰가 파싱 문제를 일으킬 수 있는 request.data 특성에 접근할 때 ParserError예외도 처리한다.

내부적으로 @api_view 데터레이터는 함수기반 뷰를 rest_Framework.views.  APIView 클래스의 서브크랠스로 변환하는 래퍼다. 이 클래스는 장고 레스트프레임워크의 모든 뷰에 대한 베이스 클래스다. 예상하다시피 클래스 기반 뷰로 작업하고 싶다면 이 클래스를 상속하는 클ㄹ래스를 만들면 되며, 데커레이터를 사용하는 함수 기반 뷰에 대해 분석한 것과 같은 이점을 얻을 수 있다. 앞으로 나오는 예제에서는 클래스 기반 뷰들로 작업할 것이다.

2.3 기본 파싱 및 렌더링 옵션 사용과 JSON으로 이동
APIView 클래스는 gamesapi/setting.py 파일에서 적절한 값을 지정하거나 서브 클래스의 클래스 속성을 오버라이드함으로써오버라이드 가능한각 뷰의 기본 설정을 지정한다. 앞서 설명한 것처럼 내부적으로 APIView클래스를 사용하면 데터레이터가 이러한 기본 설정을 적용한다. 따라서 데커레이터를 사용할 때마다 기본 파서 클래스와 기본 렌더러 클래스가 함수 뷰와 연결될 것이다.

기본적으로 DEFAULT_PARSER_CLASSES의 값은 다음과 같은 크래스의 튜플이다.

(
     'rest_framework.parsers.JSONParser',
     'rest_framework.parsers.FormParser',
     'rest_framework.parsers.MultiPartParser'
)

테커리이터를 사용해 reqeust.data 속성에 접근할 때 API는 적절한 파서를 통해 다음 콘텐트 타입 중 하나를 처리하게 된다.
- application/json
- application/x-www.form-urlkencoded
- application/form-data

함수에서 rest_framework.parsers.JSONParser 클래스의 사용을 제거해야 구성된 모든 파서로 작업하고 JSON으로만 작동하는 파서로 작업하는 것을 중단할 수 있다.

함수에서 reuest.data속성에 접근하면, 장고 레스트 프레임워크는 들어오는 요청의 Content-Type 헤더 값을 검사하고 요청 내용을 파싱할 적절한 파서를 결정한다. 이전에 설명한 기본값을 사용하면 장고 레스트 프레임워크는 위에 나열한 콘텐트 타입을 파싱할 수 있다. 하지만 요청에서 Content-Type 헤더에 적절한 값을 지정하는 것이 아주 중요하다.

game_list 함수는 request.mthod 가 'POST'일 때 다음 두 행을 실행한다.

game_data = JSONParser().parser(request)
game_serializer = GameSerizlizer(data=game_data)

JSONParser를 사용하는 첫 번째 행을 제거하고, GameSerializer의 데이터 인자로 request.data를 전달한다. 위의 행을 다음 행으로 대체한다.

game_serializer = GameSerializer(data=request.data)

game_detail 함수는 request.method가 'PUT'일때 다음 두 행을 싱행한다.

game_data = JSONParser().parser(request)
game_serilizer = GameSerializer(game, data=game_data)

game_list 함수에서 코드에 대해 이전 과 동일하게 편집한다. JSONParser를 사용하는 첫 번째 행을 제거하고, GameSerializer(game, data= request.data)

기본적으로 DEFAULT_RENDERER_CLASSES의 값은 다음과 같은 클래스의 튜플이다.
(
    'rest_framework.renderers.JSONRenderer',
    'rest_framework.renderers.BrowsableAPIRenderer',
)

데커레이터를 사용해 rest_framework.response.Response로 작업할 때, API는 적절한 렌더러를 통해 요청에서 다음 콘텐트 타입을 렌더링 할 수 있다.
- application/json
- text/html

기본적으로 DEFAULT_CONTENT_NEGOTIATION_CLASS의 값은 rest_framework.negotiation.DefaultcontentJNegotiation클래스다. 데커레이터를 사용하면 API는 이러한 콘텐트 협상 클래스를 사용해 들어오는 요청에 따라 응답에 적합한 렌더러를 선택한다. 이런 식으로 요청에서 text/html을 허용하게 지정하면 콘텐트 협상 클래스가 rest_framework.renderers.BrowsableAPIRenderer를 선택해 응답을 렌더일하고 applilcation/json대신 text/html을 생성한다.

함수 속에서는 JSONResponse와 HttpResponse 클래스를 rest_framework.response.
Response 클래스로 데체해야 한다. Response클래스는 이전에 설명한 콘텐트 협상 기능을 사용하고 수신된 데이터를 적절한 콘텐트 타입으로 렌더링하고 나서 클라이언트에게 반환하다.

이제 gameapi/games 폴더로 가서 views.py파일을 연다. 이 파일 코드 중에서 다음과 같이 JSONResponse클래스를 제거하고 함수에 @api_view데커레이터를 붙이며 rest_Framework.response.Response클래스를 사용한다.

응답 헤더에는 Allow 키가 보이는데 이곳에 GET, POST, OPTIONS, PUT 과 같이 자원에서 지원하는 쉼표 구분의 HTTP동사 리스트가 나타나 있다. 응답 본문은 자원 파싱 콘텐트 타입과 렌더링 콘텐트 타입을 지정하며, 이 OPTIONS요청에 수신된 것과 동일한 콘텐트가 자원 모음, 즉 게임 컬렉션에 적용된다.

1장에서는 POST와 PUT명령을 작성해 보낼 때 curl에서 -d옵션 뒤에 지정한 데이터를 기본적인 application/x-www-form-urlencoded대신 application/json로 보내기 원해 -H "Content-Type: application/json"옵션에 지정한 application/x-www-formurlencoded와 multipart/form-data 데이터도 파싱할 수 있다. 따라서 우리는 API로 변경된 사항이 들어간 데이터를 application/xwww-form-urlencoded로 보내는 POST 명령을 작성해 전송 할 수 있다.

새 게임을 생성하는 HTTP요청을 작성해 보내보자. 여기서는 다음과 같이 HTTPie에 대해 -f옵션을 사용하고, 양식 필드로 명령 행에서 데이터 항목을 직렬화하여 Content-Type 헤더 키를 application/x-www-form-urlencoded값으로 설정할 것이다.

http -f POST :8000/games/ name='Toy Story 4' game_category='3D RPG' played=false release_data='2016-05-18T03:02:00.776594Z'

다음은 이와 동일한 curl 명령이다. -H 옵션을 사용하지 않으면 curl이 기본적인 application/x-www-form-urlencoded으로 데이터를 전송한다.

curl -ix POST -d '{"name":"Toy Story 4", "game_category":"3D RPG", "played":"false", "release_date":"2016-05-18T03:02:00.776594Z"}' :8000/games/

이명령은 Content-Type헤더 키가 application/x-www-form-urlencoded 값으로 지정되고, 데이터는 다음과 같이 설정된 POST http://localhost:8000/games/라는 HTTP요청을 자성해 보낸다.

이 요청에는 /games/가 들어가므로 '^games/$'가 적용돼 views.game_list 함수, 즉 games/views.py파일 내에서 선언하고 업데이트한 game_detail 함수를 실행한다. 요청에 대한 HTTP동사가 'POST'이기 때문에 request.method 속성은 'POST'와 같으므로 이 함수는 request.data를 data 인자로 해서 GameSerializer 인스턴스를 생성하는 코드를 실행한다. rest_framework.parsers.FromParser클래스는 요청에서 받은 데이터를 파싱해서 새 Game을 생성하고 데이터가 유효하면 새 Game을 저장한다. 새 Game이 데이터베이스에 성공적으로 저장되면 함수는 HTTP 201 Created 상태 코드와 함케 응답 본문에 JSON으로 직렬화된 최근 저장의 Game을 반환한다. 다음 행은 HTTP요청에 대한 응답의 예를 보여주는데, 여기에는 JSON응답 속의 새 Game객체가 들어가 있다.

앞서 코드를 변경한 후에 지원되니 않는 HTTP동사로 HTTP요청을 작성하고 보낼때 어떤 일이 발생되는지 알아보기 위해 다음 명령을 실행해 보자.

http PUT :8000/games/

이 명령은 PUT http://localhost:8000/games/라는 HTTP 요청을 작성해 보낸다. 이 요청은 views.game_list 함수, 즉 games/views.py파일 내에 선언한 game_list 함수를 찾아 실행하려 할 것이다. 이 함수에 추가한 @api_view 테커레이터는 허용된 HTTP동사가 있는 문자열 리스트에 'PUT'을 포함시키지 않으므로 기본 행위로 405 Method Not Allowed 상태 코드를 반환한다. 다음 행은 위 요엋의 응답이 들어간 출력을 보여준다. 여기 JSON 콘텐트에는 detail키로 PUT 메서드가 허용됮 않음을 나타내는 문자열 값이 들어 있다.

2.4 브라우저블 API
우리는 최근 편집을 통해 우리의 API가 장고 레스트 프레임워크에서 구성된 기본 콘텐트 렌더러를 사용할 수 있게 만들었으므로 API가 text/html 콘텐트를 렌더링할 수 있다. 우리는 장고 레스트 프레임워크에 포함된 기능인 브라우저블 API를 활용할 수 있는데, 이 API를 사요하면 요청 헤더에서 Content-type키의 값으로 text/html을 지정할 때마다 각 자원에 대한 친숙한 HTML출력을 생성해 준다.

웹 브라우저에서 API 자원의 URL을 입력할 때마다 웹 브라우저에서 HTML응답이 필요하므로 장고 레스트 프레임워크는 Bootstrap을 사용해 HTML응답을 제공한다. 이 응답에는 JSON의 자원 콘텐트를 표시하는 영역, 다양한 요청을 수행하는 버튼, 자원에게 데이터를 제출하는 양식이 들어갈 것이다. 장고 레스트프레임워크의 모든 기능과 마찬가지로 브라우저블 API를 생성하는데 사용되는 템플릿과 테마를 사용자 정의할 수 있다.

웹 브라우저를 열고 http://localhost:8000/games/를 입력해 보라, 브라우저블 API는 /games/에 대한 GET요청을 자성해 보내고 그 실행 결과, 즉 헤더 및 JSON 게임리스트를 나타낼 것이다. 다음 스크린 샷은 웹브라우저에서 URL을 입력한 후에 자원 디스크립션과 함께 렌더링된 웹 페이지를 보여준다.

브라우저를 API는 자원에 대해 허용된 메서드 정보를 사용해서 이들 메서드를 실행하는 데 필요한 버튼을 제공한다. 브라우저를 API는 자원 디스크립션의 오른쪽에 OPTIONS 버튼과 GET 드롭다운 버튼을 나타낸다. OPTIONS 버튼을 사용해 /games/에 대한, 즉 현재 자원에 대한 OPTIONS요청을 할 수 있다. GET 드롭다운 버튼을 사용하면 /games/에 GET 요청도 할 수 있다. 아래쪽 화살표를 클릭하거나 탭하면 json옵션을 선택할 수 있고, 이 선택으로 브라우저를 API는 헤더가 없는 /games/에 대한 GET 요청의 원시 JSON 결과를 나타낼 것이다.

브라우저블 API는 렌더링된 웹 페이지으 ㅣ맨 아래에 /games/에 대한 POST 요청을 생성할 수 있는 컨트롤을 제공한다. Media type 드롭다운 메뉴에서 다음과 같이 이 API에 구성된 지원 파서를 서택할 수 있다.
- application/json
- application/x-www-form-urlencoded
- multipart/form-data

Content 텍스트 박스에는 Media type 드롭다운에 지정된 형식의 POST 요청으로 보낼 데이터를 지정할 수 있다. Media type 드롭다운에서 application/json을 선택하고 Content 텍스트 박스에 다음 JSON콘텐트를 입력해 보자.

POST를 클릭하거나 탭하라. 브라우저블 API는 앞서 지정한 데이터와 함께 /games/에 대한 POST 요청을  JSON으로 작성해 전송하므로 웹 브라우저에 그 호출 결과가 나타날 것이다.

2.5 복잡한 PostgreSQL 데이터베이스와 대화하는 레스트 풀 API 디자인
지금까지 레스트풀 API는 단 하나의 데이터베이스 테이블에서 CRUD 작업을 수행했다. 이제 장고 레스트 프레임워크로 보다 복잡한 레스트풀 API를 작성해 복잡한 데이터베이스 모델과 대화하게 해야 하는데, 이런 모델을 이용하면 게임 카테고리에 따라 그룹화한 게임에 대해 플레이어 점수를 동록할 수 있다. 이전 레스트풀 API에서는 문자열 필드를 사용해 게임의 카테고리를 지정했다. 여기서는 특정 게임 카테고리에 속한 모든 게임을 쉽게 얻을 수 있게 게임과 게임 카테고리 간의 관꼐를 맺게 할 것이다.

- 게임 카테고리(GameCategory 모델)
- 게임(Game 모델)
- 플레이어(Player 모델)
- 플레이어 점수(PlayerScore 모델)

게임카테고리 에는 이르만 있으면 되며 게임에 대해서는 다음 데이터가 필요하다.

- 게임카테고리 대한 왜리키
- 이름
- 출시일
- 플레이어가 적어도 한번 게임을 했는지 여부를 나타내는 bool값
- 게임을 데이터베이스에 삽입한 날짜와 시간이 찍힌 타임 스탬프

플렝이어(Player)에 대해서는 다음 데이터가 필요하다.

- 성별 값
- 이름
- 플레이어를 데이터베이스에 삽입한 날짜와 시간이 찍힌 타임스탬프

플레이어가 얻은 점수(PlayerScore)에 대해서는 다음 데이터가 필요하다.

- 플레이어에 대한 왜래 키(Player)
- 게임에 대한 외래키(Game)
- 점수 값
- 플레이어가 이 점수 값을 달성한 날짜

우리가 관련 자원으로 잦ㄱ업할 때 장고 레스트 프레임워크가 제공하는 다양한 옵션을 분석하기 위해 모든 자원과 이들의 관계를 활요할 것이ㅏㄷ. 관련 자원을 표시하기 우해 동일한 구성을 사용하는 API를 작성하는 대신 개발 중인 api의특징 요구하상에 따라 가장 적합한 옵션을 선택할 수 있는 다양한 구성을 사용할 것이다.

2.6 각 HTTP 메서드가 수행하는 작업 이해
다음 표는 새 API가 지원해야 하는 메서드의 HTTP동사, 범위, 의미를 보여준다. 각 메서드는 HTTP 동사와 범위로 구성되며, 모든 메서드는 모든 자원과 컬렉션에 대해 잘 정의된 의미를 갖는다.

______________________________________________________________________

API 에서 기존 자원의 필드를 하나씩 업데이트 할 수 있게 만들어야 하므로 PATCH 메서드를 구현해야 한다. PUT 메서드는 전체 자원을 대체하기 위한 것이고, PATCH메서드는 기존 자원에 델타를 적용하기 위한 메서드다. 더욱이 레스트풀 API는 모든 자원과 자원 컬렉션에 대해 OPTIONS 메서드도 지원해야 한다.

이전 API에서도 볼수 있듯이 우리는 가장 적절한 ORM을선택하고 구성하는 데 시간을 허비하지 않을 것이다. 가능한 빨리 레스트 풀 api를 만무리해 상호작용하기 만 하면 된다. 장고 레스트 프레임워크에 포함된 모든 기능과 재사용 가능한 요소를 사용하면 api를 쉽게 만들 수 있다. 우리는 PostgreSQL데이터베이스로 작업할 것이다. 하지만 PostgreSQL을 설치하는 데 시간을 허비하지 않으려면 장고 레스트 프레임워크 ORM 구성을 변경하지 말고 그냥 기본 SQLite 데이터베이스로 작업을 계속하면 된다.

앞의 표에서 많은 메서드와 범위가 있다. 다음 리스트는 이 표에 언급된 각 범위에 대한 URL을 열거한 것이다. 여기서 {id}는 숫자 id 또는 자원의 기본키로 대체해야 한다.

- Collection of game categories: /game-categories/
- Game category: /gaeme-category/{id}/
- Collection fo games : /games/
- Game: /game/{id}
- Collection of players: /players/
- Player: /palyer/{id}/
- Collection of scores: /palyer-scores/
- Score: /player-score/{id}

2.7 모델과의 관계 선언

이제 게임 카테고리, 게임, 플레이어, 점수, 그 관계를 표현하고 유지하기 위해 사용할 모델을 만들것이다. /games/models.py 파일을 열고 그 내용을 아래 코드로 바꾼다. 각 모델과 관련된 필드를 선언하는 행은 코드 리스트에서 굵게 나타냈다.

이 코드는 django.db.models.Model 클래스의 서브 클래스인 다음 4개 모델, 즉 4개의 클래스를 선언한다.

- GameCategory
- Game
- Player
- PlayerScore

장고는 각 모델과 관련된 데이터베이스 테이블을 생성할 때 id라는 자동 증가 정수 기본 키 열을 자동으로 추가한다. 이 코드에서는 많은 속성에 대해 필드 타입, 최대 길이, 기본값을 지정했다. 각 클래스는 ordering 속성을 선언하는 Meta 내부 클래스를 선언한다. PlayerScore 클래스 내에서 선언한 Meta 내부 클래스는 '-score'를 oredering 튜플의 값으로 지정한다. 대시는 필드 이름의 접두어로 사용되며, 기본적인 오름차순이 아닌 내림차순으로 score를 정렬한다.

GameCategory, Game, Player 클래스에서는 각 모델의 이름 또는 타이틀을 제공하는 name 속성의 내용을 반환하는 __str__메서드를 선언한다. 따라서 장고는 해당 모델에 대해 사람이 읽을 수 있는 표현을 제공해야 할 때마다 이 메서드를 호출한다.

Game 모델은 다음 행으로 game_category필드를 선언한다.
위의 행은 django.db.models.ForeingKey클래스를 사용해 GameCategory모델에 다대일 관계를 제공한다. related_name 인자에 지정한 'games' 값은 GameCategory모델에서 Game모델로의 역방향 관계를 만든다. 이 값은 관련 GameCategory 객체에서 Game 객체로의 관계에 사용할 이름을 나타낸다. 이제 특정 게임 카테고리에 속한 모든 게임에 접근할 ㅜㅅ 있다. 게임 카테고리를 삭제할 때마다 이 카테고리에 속한 모든 게임이 삭제돼야 하므로 on_delete인자에 대해 models.CASCADE값을 지정했다.

PlayerScore 모델은 다음 행으로 player필드를 선언한다.

위의 행은 django.db.models.ForeignKeuy클래스를 사용해 Player 모델에 다대일 관계를 제공한다. related_name인자에 지정한 'scores'값은 Player 객체에서 PlayerScore객체로의 고나계에 사용할 이름을 나타낸다. 이제 특정 플렝어가 기록한 모든 점수에 접근할 수 있다. 플레이어를 삭제할 때마다 이 플레이어가 얻은 모든 점수를 삭제해야하므로 on_delete 인자에 대해 models.CASCADE값을 지정했다.

PlayerScore모델은 다음 행으로 game필드를 선언한다.

위의 행에서 django.db.models.ForeignKey 클래스를 사용해 Game모델에 다대일 관계를 제공한다. 여기서는 역방향 관계가 필욯없으므로 이 관계를 만들지 않는다. 따라서 related_name 인자로 값을 지정하지 않았다. 게임을 삭제할 때마다 이 게임의 모든 증록 점수가 삭제돼야 하므로 on_delete인자에 대해 models.CASCADE값을 지정했다.

이예제로 작업할 새 가상 환경을 만들었거나 책의 샘플 코드를 다운로드 한 경우, 기본 데이터베이스를 삭제할 필요는 없다. 하지만 이 API예제 코드를 변경하는 경우에는 gamesapi/db.sqlite3 파일과 games/migrations 폴더를 삭제해야 한다.

그리고 나서 최근에 코딩한 새 모델의 초기 마이그레이션을 만들어야 한다. 아래의 파이썬 스크립트를 실행하면 데이터베이스를 처음으로 동기화할 수 있다. 위의 예제 API에서 배웠듯이 기본적으로 장고는 SQLite 데이터베이스를 사용하낟. 이 예젱서 PostgreSQL데이터베이스로 작업할 것이다. 하지만 SQLite를 사용하고 싶다면 PostgreSQL과 관련된 단계와 장고에서의 구성을 생략하고 migratijons generation 명령으로 건너뛰면 된다.

여러분의 컴퓨터 또는 개발 서버에서 PostgreSQL 데이터베이스르 ㄹ사용한 적이 없다면 PostgreSQL 데이터베이스를 다운로드하고 설치해야 한다. 이 데이터베이스 관리 시스템은 웹페이지에서 다운로드해 설치할 수 있다.

PostgreSQL의 bin 폴더가 PATH 환경 변수에 포함돼 있는지확인해야 한다. 현재 터미널이나 명령 프롬프트에서 psql 명령 행 유틸리티를 실행할 수 있어야 하기 때문이다. 이 폴더가 PATH에 포함되지 않으면, psycopg2패키지를 설치하려고 할 때 pg_config파일을 찾을 수 없다는 오류가 발생한다. 또한 이후 단계에서 사용할 PostgreSQL명령 행 도구에 대해서도 전체 경로를 사용해야 한다.

PostgreSQL 명령행 도구를 사용해 games라는 새 데이터베이스를 생성할 것이다. 이 이름을 가진 PostgreSQL 데이터베이스가 이미 있는 경우에는 모든 명령과 구성에 있어서 다른 이름을 사용해야 한다. PostgreSQL GUI 도구로 동일한 작업을 수행할 수 있다. 리눅스에서 개발할 경우, postgres사용자로 명령을 실행해야 한다. 맥 os 또는 원도우에서는 다음 명령을 실행해 games라는 새 데이터베이스를 생성한다. 이 명령은 출력을 나타내지 않을 것이다.

createdb games

리눅스에서는 다음 명령을 실행해 postgres사용자를 사용한다.

sudo -u postgres createdb games

이제 psql 명령행 도구를 사용해 장고에서 사용할 특정 사용자를 생성하고 필요한 역할을할당하는 SQL문을 실행할 것이다. 맥OS 또는 원도우에서 psql을 시작하려면 다음 명령을 실행하라.
psql

맥OS에서 PostgreSQL을 설치한 방식에 따라 위의 명령이 작동하지 않는 경우, 다음 명령을 실행해 postgres로 psql을 시작하면 된다.

sudo -u postgres psql

리눅스에서 postgres를 사용하려면 다음 명령을 실행하라.

sudo -u psql

그런 다음, 아래 SQL문을 실행하고 마지막으로 \q를 입력해 psql 명령 행 도구를 종료하라. user_name을 새 데이터베이스에서 사용할 사용자 이름으로 바꾸고 password를 여러분이 원하는 암호로 지정하라, 우리는 장고 설정에서 지정한 사용자 이름과 암소르 ㄹ사용할 것이다. 만약 PostgreSQL의 특정 사용자로 이미 작업중이며 그 사용자의 데이터베이스에 대한 권한을 이미 부여한 경우라면 아래 단계를 수행할 필요가 없다.

CREATE ROLE user_name WITH LOGIN PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE games TO user_name;
ALTER USER user_name CREATEDB;
\q

기본 SQLite 데이터베이스 엔진과 데이터베이스 파일 이름은 gamesapi/settings.py파이썬 파일에 지정돼 있다. 이 예제에서 SQLite대신 PostgreSQL을 사용하기로 했다면 DATABASES 딕셔너리 선언을 아래 행으로 바꾸어야 한다. 아래 중첩된 딕셔너리는 default라는 데이터베이스를 django.db.backends.postgresql데이터베이스 엔진, 원하는 데이터베이스 이름, 그 외 설정으로 맞춘다. 여기서는 games라는 데이터베이스르 생성한다. 'NAME' 키의 값에 원하는 데이터베이스 이름을 지정하고 PostgreSQL구성에 따라 사용자, 비밀번호, 호스트 및 포트를 구성하라. 이전 단계를 수행했다면, 다음과 같이 이들단계에서 지정한 설정을 사용한다.

DATABASES = {
     'default':{
        'ENGINE': 'django.db.backends.postgresql',
        # games 를 원하는 데이터베이스 이름으로 바꾼다.
        'NAME':'games',
        # username을 원하는 사용자 이름으로 바꾼다.
         'USER': 'user_name',
        # password 를 원하는 암호로 바꾼다.
        'PASSWORD': 'password',
        # 127.0.0.1을 PostgreSQL로 바꾼다.
         'HOST': '127.0.0.1',
         # 기본 포트를 사용하지 않을 경우에는
         #  5432을 PostgreSQL 설정 포트로 바꾼다.
         'PORT': '5432'
     }
 }

PostgreSQL을 사용하기로 했다면, 위의 변경사항을 적용한 후에 Paycopg2 패키지(psycopg2)를 설치해야 한다. 이 패키지는 Python-PostgreSQL 데이터 어탭터이며 장고는 이 패키지를 사용해 PostgreSQL데이터베이스와 대화한다.

맥 OS 설치에서 PosgreSQL의 bin 폴더가 PATH 환경 변수에 포함돼 있는지 확인해야 한다. 예를 들어, bin 포더의 경로가 /Applications/Postgres.app/Contents/Versions/Latest/bin인 경우에 이 폴더를 PATH환경 변수에 추가하려면 다음 명령을 실행한다.

export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin

PostgresSQL 의 bin 폴더가 PATH환경 변수에 포함돼 있는지 확인한 후에는 다므명령을 실행해 이 패키지를 설치해야 한다.

pip install psycopg2
다음과 같이 출력의 마지막 행은 psycopg2 패키지가 성고적으로 설치됐음을 나타낸다.

이제, 다음 파이썬 스크립트를 실행에 데이터베이스르 처음으로 동기화할 수 있는 마이그레이션을 생성한다.

python maange.py makemigrations games

다음 행은 위의 명령을 실행한 후에 생성된 출력을 보여준다.

위의 코드는 많은 migrations.CreateModel의 작업 리스트를 정의하는 Migration 이라는 django.db.migrations.Migration 클래스의서브 클래스를 정의 한다. 각 migrations.CreateModel은 각 관련 모델에 대한 테이블을 생성한다. 장고는 각 모델에 대해 id 필드를 자동으로 추가했다. operations는 리스트에 나타난 순서대로 실행된다. 이 크도는 GameCategory에 대한 외래 키가 있는 Game에 Game, GameCategory, Player, PlayerScore, 그리고 마지막으로 game_category 필드를 추가하는데 그 이유는 GameCategory모델에서 앞서 Game 모델을 생성했기 때문이다. 이 코드는 모델을 만들때 PlayerScore에 대한 외래키를 만든다.

이제 다음 파이썬 스크립트를 실행해 생성된 모든 마이그레이션을 적용한다.

python manage.py migrate

다음 행이 이 명령을 실행한 후 생성된 출력을 보여준다.

윙의 명령을 실행한 후에는 PostgreSQL 명령 행 또는 장고가 생성한 데이블을 점검하기 위해 PostreSQL 데이터베이스의 내용을 쉽게 점검 가능한 다른 애플리케이션을 사용할 수도 있다. SQLite로 작업할 경우, 1장 '장고를 이용한 레스트 풀API 개발'테이블을 점검하는 방법을 이미 배웠다.

다음 명령을 실행해 생성된 테이블을 나열해보라

psql --username=user_name --dbname-games --command="\dt"

다음 행은 생성된 모든 테이블 이름이 나타난 출력을 보여준다.
이전 예에서 살펴봤듯이 장고는 games 애플리케이션과 관련된 다음 4개의 테이블이름에 games_접두어를 사용한다. 장고의통합 ORM은 우리 모델에 포함된 정보를 기반으로 이러한 테이블과 외래 키를 생성했다.

- games_game: Game 모델 유지
- games_gamecategory: GameCategory 모델 유지
- games_player: Player 모델 유지
- games_playerscore:PlayerScore 모델 유지

다음 명령은 HTTP 요청을 작성하고 레스트풀 API에 보내어 4개의 테이블에 대해 CRUD작업을 한 후 그 테이블 내용을 확인할 수 있게 한다. 이 명령은 사용자가 명령을 실행하는 곳과 같은 컴퓨터에서 PostgreSQL을 실행 중이라고 가정한 것이다.

PostgresSQL 명령 행 유틸리티로 작업하는 대신, GUI 도구를 사용해 PostgreSQL 데이티베이스의 내용을 확인할 수 있다. 또한 선호하는 IDE에 포함된 데이터베이스 도구를 사용해 PostfreSQL 데이터베이스의 내용을 확인할 수도 있다.

장고는 나중에 사용할 웹프레임워크와 인증 기능을 지원하는  데 필요한 추가 테이블을 생성한다.

2.8 관계 및 하이퍼링크를 사용해 직렬화 및 역직렬화 관리하기
우리는 새 레스트풀 웹 API는 GameCategory, Game, Player, PlayerScore 인스턴스를 JSON표현으로 직렬화 및 역직렬화할 수 있어야 한다. 여기서 JSON에 대한 직렬화 및 역직렬화를 관리하기 위해 직렬화 클래스를 작성할 때 여러 모델 간의 관계에 특히 주의해야 한다.

이전 API의 마지막 버전에서는 직렬화를 보다 쉽게 생성하고 상용구 코드를 줄이기 위해 rest_framework.serializers.ModelSerializer클래스의 서브 클래스를 만들었다. 이 경우에는 ModelSerializer에서 상속받은 클래스를 선언하지만 나머지 클래스는 rest_Framework.serializers.HyperlinkModelSerializer 클래스에서 상속받는다.

HyperlinkedModelSerializer는 기본 키 관계 대신 하ㅣ퍼링크 관계를 사용하는 ModelSerializer 형식이므로 기본 키 값이 아닌 하이퍼링크를 사용해 다른 모델 인스턴스에 대한 관계를 나타낸다. 게다가 HyperlinkModelSerializer는 자원의 URL을 값으로 사용해 url이라는 필드를 생성했다. ModelSerializer의 경우에서 보듯이 HyperlinkedmodelSerializer 클래스는 create및 update메서드의 기본 구현을 제공한다.

이제 gamesapi/games 폴더로 가서 serializers.py파일을 연다. 이 파일의 코드를 필요 임포트 및 GameCategorySerializer 클래스를 선언하는 아래 코드로 바꾼다. 나중에 이 파일에 더 많은 클래스를 추가할 것이다. 이 샘플 코드는 restful_python_chapter_02_03 폴더에 들어 있다.

GameCategorySerializer 클래스는 HyperlinkedModelSerializer 클래스의 서브 클래스다. 일대다 관계이면서 읽기 전용이므로 GameCategorySerializer 클래스 many와 read_only가 True로 된 serializers.HyperlinkedRelatedField 인스턴스로 games속성을 선언한다. game_category 필드를 models.ForeignKey 인스턴스로 만들때 related_name 문자열 값으로 지정한 games 이름을 Game 모델에서 사용한다. 이렇게 하면 games 필드는 케임 카테고리에 속한 각 게임에 대한 하이퍼링크 배열을 제공할 것디아. view_name값은 'game-detail'인데, 그 이유는 사용자가 하이퍼링크를 클릭하거나 탭할 때 브라우저블 API가 그 하이퍼링크를 렌더링해 게임 세부사항 뷰를 사용할 수 있게 해야 하기 때문이다.

GameCategorySerializer 클래스는 model과 fields의 두 속성을 선언하는 Meta 내부 클래스를 선언한다. model 속성의 직렬화기와 관련된 모델, 즉 GameCategory클래스를 지정한다. fields속성은 관련 모델의 직렬화에 포함시키려는 필드 이름을 나타내는 값의 튜플을 지정한다. 기본 키와 URL을 포함하고자 하므로 튜플의 멤버로 'pk'와 'url'을 모두 지정하는 코드가 필요하다. 여기서는 일반 동작으로도 충분하므로 create또는 update 메서드를 오버라이드할 필요가 없다. HyperlinkedModelSerializer 슈퍼클래스는 두 메서드 모두에 대한 구현을 제공한다.

이제 serializer.py 파일에 아래 코드를 추가해 GameSerializer 클래스를 선언하라. 이샘플코드는 restful_python_chapter_02_03 폴더에 들어 있다.

GameSerializer 클래스는 HyperlinkedModelSerializer 클래스의 서브 클래스다. GameSerializer 클래스는 game_Category 속성을 serializers.SlugRelatedField의 인스턴스로 선언하드데, 이때 이 인스턴스의 queryset인자는 GameCategory.objects.all()로 설정하고 slug_field인자는 'name'으로 설정한다. SlugRelatedField는 고유 슬러그 속성, 즉 디스크립션에 의해 관계의 대상을 나타내는 읽기/쓰기 필드다. 우리는 game_category 필드를  Game 모델의 models.ForegnKey 인스턴스로 생성했고 게임 카테고리의 이름을 관련 GameCategory에 대한 디스크립션(슬러그 필드)으로 표시하려고 한다.따라서 slug_field로 'name'을 지정했다. 관련 게임 카터고리에 대한 가능한 옵션을 브라우저를 API의 양식에 나타내야 하는 경우, 장고는 queryset인자에 지정된 표현식을 사용해 가능한 모든 인스턴스를 얻어 지정된 슬러그 필드를 나타낼 것이다.

GameCategorySerializer 클래스는 model과 fields의 두 속성을 선언하는 Meta 내부 클래스를 선언한다. model속성은 serializer와 관련된 모델, 즉 Game클래스를 지정한다. fields속성은 관련 모델의 직렬화에 포함시키려는 필드 이름을 값에 대한 문자열의 튜플로 지정한다. URL을 포함하기만 하면 'url'을 튜플의 멤버로 지정한 코드가 된다. game_category필드는 관련 GameCategory의 name필드를 지정한다.

이제 serializers.py 파일에 다음 코드를 추가해 ScoreSerializer 클래스를 선헌한다. 이 샘플 코드는 restful_python_chapter_02_03 폴더에 있다.

ScoreSerializer 클래스는 HyperlinkedModelSerializer 클래스의 서브 클래스다. 우리는 ScoreSerializer 클래스를 사용해 Player와 관련된 PlayerScore 인스턴스를 직렬화하느데, 이 말은 Player를 직렬화할 때 특정 플레이어의 모든 점수를 표시한다는 뜻이다. 관련 Game의 세부사항 모두를 표시하고 싶지만 Player가 ScoreSerializer 직렬화를 사용할 것이기 때문에 관련 Player를 포함시키지 않는다.

ScoreSerializer 클래스는 game속성을 이전에 코딩한 GameSerialzer클래스의 인스턴스로 선언한다. PlayerScore 모델의 models.ForeignKey 인스턴스로 game 필드를 생성했고 GameSerializer 클래스에서 코딩한 게임에 대한 것과 같은 데이터를 직렬화할 것이다.

ScoreSerializer 클래스는 model과 fields의 두 속성을 선언하는 Meta 내부 클래스를 선언한다.  model 속성은 직렬화기와 관련된 모델, 즉 PlayerScore 클래스를 지정한다. 이전에 설명했듯이 플레이어의 직렬화를 피하기 위해 문자열의 fileds 튜플에 'player'필드 이름을 포함시키지 않는다. 우리는 PlayerSerializer를 마스터로 사용하고 ScoreSerializer를 세부 사항으로 사용할 것이다.

이제 serializer.py파일에 다으 코드를 추가해 PlayerSerializer클래스를 선언하다. 이 샘플 코드 파일은 restful_python_chapter_02_03 폴더에 들어 있다.

PlayerSerizlier 클래스는 HyperlinkModelSerizlizer클래스의 서브 클래스다. PlayerSerializer 클래스를 사용해 Player 인스턴스를 직렬화하고 앞서 선언한 ScoreSerializer 클래스를 사용해 Player와 관련된 모든 PlayerScore 인스턴스를 직렬화한다.

PlayerSerializer 클래스는앞서 코딩한 ScoreSerializer 클래스의 인스턴스로 score속성을 선언한다. 일대다 관계이므로 many인자는 True로 설정한다. Player 필드를 PlayerScore모델의 models.ForeignKey 인스턴스로 생성할 때 related_name문자열 값으로 지정한 scores 이름을 사용한다. 이렇게 하면 score 필드는 앞서 선언한 ScoreSerializer를 사용해 Palyer에 대한 속한 각 PlayerScore를 렌더링한다.







2018년 8월 1일 수요일

2.7 모델 세부 튜닝

가능성 있는 모델들을 추렸다고 가정하겠습니ㅏㄷ. 이제 이 모델들을 세뷰 튜닝해야 합니다. 그 방법을 몇 개 살펴 봅시다.

2.7.1 그리드 탐색
가장 단순한 방법은 만족할 만한 하이퍼파라미터 조합을 찾을 때까지 수동으로 하이퍼파라미터를 조정하는 것입니ㅏㄷ. 이는 매우 지루한 작업이고 많은 경우의 수를 탐색하기에는 시간이 부족할 수도 있습니다.

대신 사이킷런의 GridSearchCVㄹ르 사용하는 것이 좋습니다. 탐색하고자 하는 하이퍼파라미터와 시도해볼 값을 지정하기만 하면 됩니다. 그러면 가능한 모든 하이퍼파라미터 조합에 대해 교차 검증을 사용해 평가하게 됩니다. 예를 들어 다음 코드 RandomForestRegressor에 대한 최적의 하이퍼파라미터 조합을 탐색합니다.

TIP 어떤 하이퍼파라미터 값을 지정해야 할지 모를 때는 연속된 10의 거듭 제곱 수로 시도해 보는 것도 좋습니다.(더 세밀하게 탐색하려면 위 예제의 n_estimators 하이퍼파라미터처럼 더 작은 값을 지정합니다.)

param_grid 설정에 따라 사아킷런이 먼저 첫 번째 dict에 있는 n_estimators와 max_features 하이퍼파라미터의 조합인 3*4=12개를 평가하고(지금은 이 하이퍼파라미터가 무엇을 의미하는지 걱정하지 않아도 됩니다. 7장에서 설명하겠습니다), 그런 다음 두 번째 dict에 있는 하이퍼파라미터의 조합인 2*3=6개를 시도합니다. 하지만 두 번째 bootstrap하이퍼파라미터를 True(기본값)가 아니라 False로 설정합니다.

모두 합하면 그리드 탐색의 RandomForestRegressor하이퍼파라미터 값의 12 + 6 = 18개 조합을 탐색하고, 각각 다섯 번 모델을 훈련시킵니다(5-겹 교차 검증을 사용하기 때문에). 다시 말해 전체 훈련 횟수는 18*5 = 90이 됩니다.

NOTE_ GridSearchCV가 (기본값인)refit=True로 초기화되었다면 교차 검증으로 최저의 추정기를 찾은 다음 전체 훈련 세트로 다시 훈련시킵니다. 일반적으로 데이터가 많을수록 성능이 향상되므로 좋은 방법입니다.

이 예에서는 max_features하이퍼파라미터가 8, n_estimators 하이퍼파라미터가 30일때 최적의 솔류션입니다. 이때 RMSE 점수가 49,694로 앞서 기본 하이퍼파라미터 설정으로 얻은 52,564 점 보다 조금 더 좋습니다.

2.7.2 랜덤 탐색
그리드 탐색 방법은 이전 예제와 같이 비교적 적은 수의 조합을 탐구할 때 괜찮습니다. 하지만 하이퍼파라미터 탐색 공간이 커지면 RandomizedSearchCV를 사용하는 편이 더 좋습니다.
RandomizedSearchCV는 GridSEarchCV와 거의 같은 방식으로 사용하지만 가능한 모든 조합을 시도하는 대신 각 반복마다 하이퍼파라미터의 임의의 수를 대입하여 지정한 횟수만큼 평가합니다. 이 방식의 주요 장점은 다음 두 가지 입니다.
- 랜덤 탐색을 1,000회 반복하도록 실행하면 하이퍼파라미터마다 각기 다른 1,000개의 값을 탐색합니다(그르드 탐색에서는 하이퍼파라미터마다 몇 개의 값만탐색합니다).
- 단순히 반복 횟수를 조절하는것만으로 하이퍼파라미터 탐색에 투입할 컴퓨팅 자원을 제어할 수 있습니다.

2.7.3 양상블 방법
모델을 세밀하게 튜닝하는 또 다른 방법은 최상의 모델을 연결해 보는 것입니다.(결정 트리의 앙상블인 랜덤 포레스트가 결정 트리 하나보다 더 성능이 좋은 것처럼) 모델의그룹(또는 앙상블)이 최상의 단일 모델보다 더 나은 성능을 발휘할 때가 많습니다. 특히 개개의 모델이 각기 다른 형태의 오차를 만들 때 그렇습니다. 이 주제는 7장에서 자세히 살펴보겠습니다.

2.7.4 최상의 모델과 오차 분석
최상의 모델을 분석하면 문제에 대한 좋은 통찰을 얻는 경우가 많습니다. 예를 들어 RandomRorestRegressor가 정확한 예측을 만들기 위한 각 특성의 상대적인 중요도를 알려줍니다.
중요도 다음에 그에 대응하는 특성 이름을 표시해보겠습니다.
이 정보를 바탕으로 덜 중요한 특성들을 제외할 수 있습니다.(예를 들어 ocean_proximity 카테고리 중 하나만 실제로 유용하므로 다른 카테고리는제외할 수 있습니다).

시스템이 특정한 오차를 만들었다면 왜 그런 문제가 생겼는지 이해하고 문제를 해결하는 방법이 무엇인지 찾아야 합니다(추가 특성을 포함시키거나, 반대로 불필요한 특성을 제거하거나, 이상치를 제외하는 등).

2.7.5 테스트 세트로 시스템 평가하기
어느정도 모델을 튜닝하면 마침내 만족할 만한 모델을 얻게 됩니다. 그럼 이제 테스트 세트에서 최종 모델을 평가할 차례입니다. 이 과정에 특별히 다른 점은 없습니다. 테스트 세트에서 예측 변수와 레이블을 얻은 후 full_pipeline을 사용해 데이터를 변환하고(fit_transform()이 아니라 transfor()을 호출해야 합니다) 테스트 세트에서 최종 모델을 평가합니다.

하이퍼파라미터 튜닝을 많이 했다면 교차 검증을 사용해 측정한 것보다 조금 성능이 낮은 것이 보통입니다(우리 시스템이 검증 데이터에서 좋은 성능을 내도록 세밀하게 튜닝되었기 때문에 새로운 데이터셋에는 잘 작동하지 않을 가능성이 큽니다). 이 예제에서는 성능이 낮아지진 않았지만, 이런 경우가 생기더라도 테스트 세트에서 성능 수치를 좋게 하려고 하이퍼파라미터를 튜닝하려 시도해서는 안됩니다. 그렇게 향상된 성능은 새로운 데이터에 일반화됙 어렵습니다.

이제 프로젝트 론칭 직전 단계에 왔습니다. (학습한 것, 한일과 하지 않은 일, 수립한 가정, 시스템 제한 사항 등을 가종하면서) 솔루션과 문서를 출시하고, 깔끔한 도표와 기억하기 쉬운 제목으로(예를 들면'수입의 중간값이 주택 가격 예측의 가장 중요한 지표다') 멋진 발표자료를 만들어야 합니다.

2.6 모델 선택과 훈련

2.6.1 훈련 세트에서 훈련하고 평가하기
이전 단계의 작업 덕분에 생각보다 훨씬 간단해 졌습니다. 먼저 앞 장에서 했던 것처럼 선형회귀모델을 훈련시켜보겠습니다.

2.6.2 교차 검증을 사용한 평가
결정트리 모델을 평가하는 밥버을 생각해보겠습니다. 우선 train_test_split 함수를 사용해 훈련 세트를 더 작은 훈련 세트와 검증 세트로 나누어, 더 작은 훈련 세트에서 모델을 훈련시키고 검증 세트로 모델을 평가하는 방법이 있습니다. 조금 수고스럽지만 너무 어렵지 않으며 매우 잘 작동합니다.

훌륭한 대안으로 사이킷런의 교차 검증 기능을 사용하는 방법도 있습니다. 다음 코드는 K-겹교차 검증을 수행합니다. 훈련 세트를 폴드라 불리는 10개의 서브셋으로 무작위로 분할합니다. 그런 다음 결졍 트리 모델을 10번 훈련하고 평가하는데, 매번 다른 폴드를 선택해 평가에 사용하고 나머지 9개 폴드는 훈련에 사용합니다. 10개의 평가 점수가 담긴 배열이 결과가 됩니다.

CAUTION_ 사이킷런의 교차 검증 기능은 scoring 매개변수에 (낮을수록 좋은) 비용 함수가 아니라(클수록 좋은)효용 함수를 기대합니다. 그래서 평균 제곱오차(MSE)의 반댓값(즉, 음숫값)을 계산하는 neg_mean_squared_error 함수를 사용합니다. 이런 이유로 앞선 코드에서 제곱근을 계산하기 전에 -scores로 부호르 바꿨습니다.

결정 트리 결과가 이전만큼 좋아 보이지 않습니다. 실제로 선형 회귀모델보다 나쁩니다! 교차 검증으로 모델의 성능을 추정하는 것뿐만 아니라 이 추정이 얼마나 정확한지 측정할 수 있습니다. 결정 트리 점수가 대략 평균 71.379에서 +-2,458 사이입니다. 검증 세트를 하나만 사용했다면 이런 정보를 얻지 못했을 것입니다. 하지만 모델을 여러 번 훈련시켜야 해서 비용이 비싸므로 교차검증을 언제나 쓸 수 있는 것은 아닙니다.

비교를 위해 선형 회귀모델의 점수를 계산해 보겠습니다.

확실히 결정 트리 모델이 과대적합되어 선형 회귀모델보다 성능이 나쁩니다.

마지막으로 RandomForestRegressor 모델을 하나 더 시도해보겠습니다. 7장에서 보게 되겠지만 랜덤 포레스트 특성을 무작위로 선택해서 많은 결정 트리를 만들고 그 예측을 평균 내는 방식으로 작동합니다. 여러 다른 모델을모아서 하나의 모델을 만드는 것을 앙상블 학습이라고 하며 머신러닝 알고리즘의 성능을 극대화하는 방법 중 하나입니다. 다른 모델들과 기본적으로 비슷하므로 코드 설명은 대부분 건너뛰도록 하겠습니다.

훈련 세트에 대한 점수가 검증 세트에 대한 점수보다 훨씬 낮으므로 이 모델도 여전히 훈련세트에 과대적합되어 있습니다. 과대적합을 해결하는 방법은 모델을 간단히 하거나, 제한을 을 하거나(즉, 규제), 더 많은 훈련 데이터를 모드는 것이빈다. 그러나 랜덤 포레스트를 더 깊이 들어가기 전에, 여러 종류의 머신러닝 알고리즘으로 하이퍼파라미터 조정에 너무 많은 시간을 들이지 않으면서 다양한 모델(다양한 커널의 서프트 벡터머신, 신경망 등)을 시도해봐야 합니다. 가능성 있는 2~5개 정도의 모델을 선정하는 것이 목적입니다.

TIP 실험한 모델을 모두 저장해두면 필요할 때 쉽게 모델을 복원할 수 있습니다. 교차 검증 점수와 실제 예측값은 물론 하이퍼파라미터와 훈련된 모델 파라미터 모두 저장해야 합니다. 이렇게 하면 여러 모델의 점수와 모델이 만든 오차를 쉽게 비교할 수 있습니다. 파이썬의 pickle 패키지나( 넘파이 배열을 저장하는 데 아주 효율적인)sklearn.externals.joblib을 사용하여 사이킷런 모델을 간단하게 저장할 수 있습니다.