URI, URL, URN

URI, URL, URN?

  • URI : Uniform Resource Identifier (통합 자원 식별자)
  • URL : Uniform Resource Location
  • URN : Uniform Resource Name

1) URI

통합 자원 식별자(Uniform Resource Identifier, URI)는 인터넷에 있는 자원을 나타내는 유일한 주소이다. URI의 존재는 인터넷에서 요구되는 기본조건으로서 인터넷 프로토콜에 항상 붙어 다닌다.

URI의 하위개념으로 URL, URN 이 있다.

- 위키백과 (통합 자원 식별자)

A Uniform Resource Identifier (URI) is a unique sequence of characters that identifies a logical or physical resource used by web technologies.

- Wikipedia (Uniform Resource Identifier)

URI란 인터넷에 있는 자원(파일, 데이터, 웹 페이지 등) 각각을 가리키는 고유한 값이다.
URI가 어떤 자원의 위치(주소)를 담고 있으면 URL이라 하고, 위치가 아닌 고유한 이름을 담고 있으면 URN이라 한다.

예를 들어 정부 기관에서 나를 찾아오려고 할 때, 내 집 주소를 통해 찾아 오거나 내 이름과 주민등록번호를 통해 찾아올 수도 있다.
여기서 내 집 주소가 URL이고, 내 이름과 주민등록번호의 조합이 URN이라고 할 수 있다.

URI 표기 방식은 아래와 같다.

URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]

authority = [userinfo "@"] host [":" port]

          userinfo       host      port
          ┌──┴───┐ ┌──────┴──────┐ ┌┴┐
https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top
  └─┬─┘   └───────────┬──────────────┘└───────┬───────┘ └───────────┬─────────────┘ └┬┘
  scheme          authority                  path                 query           fragment

  ldap://[2001:db8::7]/c=GB?objectClass?one
  └┬─┘   └─────┬─────┘└─┬─┘ └──────┬──────┘
  scheme   authority   path      query

  mailto:John.Doe@example.com
  └─┬──┘ └────┬─────────────┘
  scheme     path

  news:comp.infosystems.www.servers.unix
  └┬─┘ └─────────────┬─────────────────┘
  scheme            path

  tel:+1-816-555-1212
  └┬┘ └──────┬──────┘
  scheme    path

  telnet://192.0.2.16:80/
  └─┬──┘   └─────┬─────┘│
  scheme     authority  path

  urn:oasis:names:specification:docbook:dtd:xml:4.1.2
  └┬┘ └──────────────────────┬──────────────────────┘
  scheme                    path

2) URL

URL(Uniform Resource Locator 또는 web address, 문화어: 파일식별자, 유일자원지시기)은 네트워크 상에서 자원이 어디 있는지를 알려주기 위한 규약이다.
즉, 컴퓨터 네트워크와 검색 메커니즘에서의 위치를 지정하는, 웹 리소스에 대한 참조이다.

쉽게 말해서, 웹 페이지를 찾기위한 주소를 말한다.

- 위키백과 (URL)

위에서 URL은 어떤 자원의 위치(주소)를 나타낸다고 하였다.
집 주소도 규칙을 갖고서 정해지듯이, URL도 규칙이 있다.

scheme://<user>:<password>@<host>:<port>/<url-path>

아래는 위의 표현보다는 친숙할 만한 HTTP URL의 표현 방식이다.

http://<host>:<port>/<path>?<searchpart>

뭐가 어떻게 되는건지 언뜻 봐서는 여전히 잘 모르겠다. 아래 예시를 보자.

https://www.google.com:443/search?q=URL
  • https:// -> URL의 맨 앞 부분으로, 프로토콜 이름이 들어간다.
    이는 접근하려는 자원에 어떤 방식으로 접근할 것인지를 나타낸다.
  • www.google.com -> 프로토콜 다음으로 도메인 이름 또는 IP 주소가 나온다.
    www.google.com 대신에 172.217.175.228을 입력해도 위의 URL과 동일한 결과를 얻을 수 있다.
  • :443 -> 도메인 이름 또는 IP 주소 뒤에는 포트(Port) 번호가 나온다.
    이는 해당 도메인으로 접근하되, 어느 문으로 들어갈 것인지 정해주는 것과 같다.
  • /search -> 이는 접근하려는 자원이 있는 디렉토리 경로를 나타낸다.
    자원이 이미지나 파일인 경우 경로 마지막에 .jpg나 .csv 같은 파일 확장자가 붙어 있을 것이다. 웹 페이지는 .html, .jsp 등이 표시되기도 하는데, 웹 프레임워크 등의 사용으로 이러한 확장자 표시가 생략될 수도 있다.
  • ?q=URL -> 이는 쿼리(query)로서 가져오려는 자원에 대하여 특정한 조건을 걸어서 가져오기 위한 것이다.
    예시의 경우에는 q의 값이 'URL'인 자원을 요청한다는 의미이다.

위에서 사용했던 정부 기관이 나에게 찾아 오는 상황에 비유하자면,
(1) 먼저 정부 기관에서 우리 집에 무슨 목적으로 찾아올지 정해진다(프로토콜. http, ftp 등).
(2) 그리고 집 주소가 어떻게 되는지 건물명이나 지번 주소 등으로 명확히 정한다(도메인 이름 or IP 주소).
(3) 그 다음 현관문으로 들어올지, 베란다 창문으로 들어올지, 배관으로 침투할지 정한다(포트 선택. 80, 443, 3306 등).
(4) 그런 후에 구체적으로 집 내부 어디로 접근 및 침투를 할지 경로 설정을 한다(디렉토리 경로. ex. /거실/안방/침대밑).
(5) 마지막으로 어떤 조건을 기준으로 누구를 데려갈지 결정한다(쿼리. ?성별=남성&키=180).


3) URN

URN(Uniform Resource Name, 통합 자원 이름)은 urn:scheme 을 사용하는 URI를 위한 역사적인 이름이다. URN은 영속적이고, 위치에 독립적인 자원을 위한 지시자로 사용하기 위해 1997년도 RFC 2141 문서에서 정의되었다.

- 위키백과 (URN)

A Uniform Resource Name (URN) is a Uniform Resource Identifier (URI) that uses the urn scheme.
URNs are globally unique persistent identifiers assigned within defined namespaces so they will be available for a long period of time, even after the resource which they identify ceases to exist or becomes unavailable.
URNs cannot be used to directly locate an item and need not be resolvable, as they are simply templates that another parser may use to find an item.

- Wikipedia (Uniform Resource Name)

정부에서 우리 집을 급습하여 나를 데려가려고 했다. 그러나 내가 집에 없었다면?
날 데려가려는 정부의 작전은 실패한 것이다. 따라서 정부는 나를 추적할 다른 방법을 쓰기로 했다.
그들은 내 이름과 주민등록번호로 나를 추적했고, 결국 나는 그들에게 잡혔다.

URL은 어떤 자원의 위치를 의미할 뿐, 그 자체가 해당 자원을 가리키는 것은 아니다.
그러므로 이 자원의 위치가 바뀐 후에 동일한 URL로 접근하면 해당 자원을 찾을 수 없다.
이런 이유로 자원의 위치가 이곳 저곳으로 바뀌어도 이를 찾을 수 있는 방법이 필요했고, 그 결과 URN이 나왔다.

URN의 표기 방식은 아래와 같다.

<URN> ::= "urn:" <NID> ":" <NSS>
  • NID : Namespace IDentifier. URN으로 지정할 자원의 분류를 나타낸다.
  • NSS : Namespace Specific String. NID 내에서 해당 자원이 갖는 고유값이다.

만약 나를 URN으로 표현한다면 urn:republicofkorea:************* 이런 식으로 표현할 수 있을 것이다(별표는 주민등록번호가 들어간다고 생각해주세요. 이해를 돕기 위한 예시일 뿐, 실제로 이렇게 표현되는 것은 아닙니다!).

인터넷에서 도서를 찾다가 보면 ISBN이라는 글자와 함께 그 옆에 숫자가 길게 써있는 것을 볼 수 있는데, 이것이 URN이다(참고로 ISBN은 International Standard Book Number, '국제표준도서번호'이다).

https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882

위 도서를 URN으로 표현하면 urn:isbn:9780132350884 이다.

데이터베이스 정규화(Database Normalization)

데이터베이스 정규화란?

관계형 데이터베이스의 설계에서 중복을 최소화하게 데이터를 구조화하는 프로세스를 정규화(Normalization)라고 한다.

데이터베이스 정규화의 목표는 이상이 있는 관계를 재구성하여 작고 잘 조직된 관계를 생성하는 것에 있다. 일반적으로 정규화란 크고, 제대로 조직되지 않은 테이블들과 관계들을 작고 잘 조직된 테이블과 관계들로 나누는 것을 포함한다.

정규화의 목적은 하나의 테이블에서의 데이터의 삽입, 삭제, 변경이 정의된 관계들로 인하여 데이터베이스의 나머지 부분들로 전파되게 하는 것이다.

- 위키백과 (데이터베이스 정규화)

잔뜩 어질러져 있는 방을 상상해보자.
정리되지 않은 침대와 책상, 방 곳곳에 널부러져 있는 다양한 옷과 양말들, 쓰러진 행거, 어제 밤에 다 먹고서 치워 놓지 않은 비어 있는 치킨 박스와 사이다 페트병 등등...
이런 와중에 옷이나 가구를 새로 사서 방에 두려고 한다. 과연 순순히 될까?
그 전에 방 정리부터 해야 뭘 사서 들여놓던가 할 수 있을 것이다.

데이터베이스도 이와 같다.

데이터베이스 정규화란 잔뜩 어질러진 방과 같은 데이터베이스의 내부를 일정한 규칙을 통해 깔끔하게 정리정돈하는 것이다.
이렇게 정리정돈을 함으로써 데이터베이스에서 중복되는 데이터를 줄이고, 새로운 데이터의 삽입 / 갱신 / 삭제가 잘못될 위험을 없앤다(이상 현상 제거).

또한 데이터베이스의 구조를 확장할 때에도 기존의 구조를 거의 건드리지 않고 할 수 있다.
(여기서의 데이터베이스는 관계형 데이터베이스를 말한다.)


이상 현상이란?

데이터베이스의 정규화가 제대로 되지 않으면 데이터를 삽입 / 갱신 / 삭제할 때 예상과 다른 결과를 얻을 수도 있다. 이를 이상 현상(Anormaly)이라고 한다.

1) 삽입 이상 (Insert Anormaly)

대학교 학부생 목록에 신입생 '나뉴맨'의 데이터를 추가하려고 한다. 그러나 아직 수강 신청을 하지 않아 수강 과목 정보가 없다.
이러면 이 신입생의 수강 과목 값을 null로 하지 않는 이상 데이터를 추가할 수가 없다.

이런 경우를 삽입 이상이라고 한다.

2) 갱신 이상 (Update Anormaly)

학부생 중 '박이상'의 전공이 컴퓨터에서 정보통신으로 변경되었다. 따라서 '박이상'의 전공 값을 갱신하였다.
'박이상'의 레코드가 여러 개 있기 때문에, '박이상'의 전공 정보를 변경하려면 여러 개의 레코드가 함께 바뀌어야 했다.
그러나 갱신 처리 도중에 문제가 생겨 일부 레코드만 갱신이 되었다. 이로 인해 '박이상'의 전공이 무엇인지 정확히 알 수 없게 되었다.

이런 경우를 갱신 이상이라고 한다.

3) 삭제 이상 (Delete Anormaly)

'김디비'가 회계원리가 어려워서 수강 취소를 신청했다. 그래서 '김디비'의 수강 과목이 회계원리인 데이터를 삭제하려고 한다.
그런데 이렇게 하려고 보니 '김디비'의 레코드를 통째로 삭제하게 생겼다. 이러면 '김디비' 학생이 우리 학교에서 영영 사라지는데...?

이런 경우를 삭제 이상이라고 한다.


데이터베이스 정규화는 어떻게 하지?

데이터베이스 정규화는 정규화되는 정도에 따라 제 1 정규형(1NF, the first Normal Form)부터 제 6 정규형과 함께 EKNF, BCNF, ETNF, DKNF가 있다.
보통 제 3 정규형가지 만족하는 데이터베이스를 "정규화 되었다(normalized)"고 말한다.
따라서 여기서는 제 3 정규형까지만 다루겠다.

1) 제 1 정규형

제 1 정규형에서 데이터 테이블에 있는 레코드는 각각의 컬럼마다 단 한 개의 값만 가져야 한다.
'내 친구 목록' 테이블에서 '존'은 취미 2개가 함께 써있다. 이를 분리하여 아래와 같이 별도의 레코드로 만들어야 한다.

2) 제 2 정규형

제 2 정규형은 제 1 정규형을 만족하는 테이블의 컬럼들 간에 부분 함수적 종속이 없어야 한다(달리 말하면 완전 함수적 종속을 만족해야 한다).

함수적 종속이란?
수학의 함수에서 $x$에 의해 $y$가 결정이 되듯이, 데이터베이스에서 어떤 관계의 부분 집합 X와 Y가 있고, X에 의해 Y가 결정될 수 있으면 이때 Y는 X에 함수적 종속이라고 한다.
여기서 X 자체만으로 Y가 결정되면 완전 함수적 종속이라고 하고, X 내의 또 다른 부분 집합으로도 Y가 결정될 수 있으면 부분 함수적 종속이라고 한다.

제 1 정규형을 만족한 '내 친구 목록' 테이블을 보면 각 레코드는 {이름, 취미}를 통해 구분이 된다.
그런데 나이, 집 주소, 거주 국가, 거주 도시는 {이름}만 갖고도 정해질 수 있다. {이름, 취미}가 아닌 {이름}만으로도 이 속성들이 결정되므로 부분 함수적 종속이 있다.
이를 제거하기 위해 아래와 같이 테이블을 분리한다.

3) 제 3 정규형

제 3 정규형은 제 2 정규형을 만족하는 테이블의 컬럼들 간에 이행적 함수 종속이 없어야 한다.

이행적 함수 종속이란, A이면 B이고, B이면 C일 때, A는 C인 관계(A->B, B->C, A->C)인 관계가 성립하는 것이다.
즉, 데이터베이스 테이블에서 기본 키가 아닌 다른 컬럼이 또 다른 컬럼을 결정하는 경우를 말한다.

A :인간은 동물이다.
B : 동물은 죽는다.
C : 인간은 죽는다.

제 2 정규형을 만족하는 '내 친구 목록' 테이블을 보면 {이름}을 통해 집 주소를 알 수 있다.
그리고 {집 주소}를 통해 거주 국가와 거주 도시를 알 수 있다.
따라서 이행적 함수 종속이 존재하므로 이 테이블을 아래와 같이 분리한다.


역정규화(Denormalization)

역정규화는 정규화의 단계를 낮추는 것이다.

정규화를 하면 할수록 테이블이 세부적으로 나누어진다. 이를 통해 데이터의 중복이 감소하고, 각 테이블의 의미도 보다 뚜렷해지는 등의 장점이 있다.
그 대신 세분화된 테이블로 인해 한 테이블 내에서 필요한 데이터를 한 번에 불러오기가 힘들어진다. 이로 인해 JOIN 등을 통해 여러 테이블로부터 필요한 데이터를 불러와야 하는데, 이는 쿼리의 요청 처리 시간이 늘어나는 문제가 있다.

이런 이유로 정규화의 단계를 낮춰 약간의 데이터 중복을 허용함으로써 JOIN과 같이 처리 시간을 늘리는 쿼리의 사용 횟수를 줄인다. 그 결과 전체적인 시스템의 속도가 향상되는데, 이것이 역정규화의 목적이다.

주의 : 역정규화(denormalization)는 비정규화(unnormalized form)와 다른 것이다. 역정규화를 위해서는 먼저 정규화가 되어야 한다.


<참고 자료>

데이터베이스 트랜잭션(Database Transaction)

데이터베이스 트랜잭션이란?

데이터베이스 트랜잭션(Database Transaction)은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다.

이론적으로 데이터베이스 시스템은 각각의 트랜잭션에 대해 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 영구성(Durability)을 보장한다. 이 성질을 첫글자를 따 ACID라 부른다. 그러나, 실제로는 성능향상을 위해 이런 특성들이 종종 완화되곤 한다.

어떤 시스템들에서는 트랜잭션들은 논리적 작업 단위(LUW, Logical Units of Work)로 불린다.
- 위키백과 (데이터베이스 트랜잭션)

중고 물품 직거래를 하는 상황을 상상해보자. 과정은 아래와 비슷할 것이다.

위 과정 중 하나라도 이루어지지 않는다면 직거래는 바로 끝(또는 PATO)날 것이다. 그리고 다시 새로운 거래 상대를 찾아 거래를 진행할 것이다.
반면에 모든 과정이 정상적으로 이루어진다면 직거래는 깔끔하게 끝날 것이다.

트랜잭션이 이렇다.

트랜잭션은 데이터베이스의 상태에 어떤 변화가 일어날 때 이 변화의 과정을 모아놓은 것이다.
단순하게는 블로그에 글을 써서 저장을 누르면 데이터베이스에 데이터가 들어가는 경우가 있을 것이고,
좀 더 복잡해지면 은행 송금처럼 보내는 사람과 받는 사람의 통장 유효 여부부터 잔액 확인 및 입·출금 처리 완료 확인까지가 한 묶음인 경우도 있을 것이다.


트랜잭션의 성질 (ACID)

트랜잭션의 성질이자 조건으로서 ACID로 표현되는 4가지가 있다.
Atomicity(원자성), Consistency(일관성), Isolation(고립성), Durability(지속성)이다.

1) Atomicity (원자성)

원자(原子, atom)는 일상적인 물질을 이루는 가장 작은 단위이다. 일상적인 물질들이 원소로 구성되어 있기 때문에, 이는 화학 반응을 통해 더 쪼갤 수 없는 단위와 동의어이다.
- 위키백과 (원자)

즉, 더 이상 쪼개어질 수 없는 성질이다(원자를 핵반응으로 더 쪼갤 수 있다고는 하는데, 여기서는 침묵하겠다).

하나의 트랜잭션은 그 자체로서 완전한 하나이다.
그 안에 아무리 많은 쿼리와 여러 과정들이 있더라도 트랜잭션에 속한 이상 하나라도 죽으면 다 같이 죽고, 모두가 살아남아야만 최종 목표(데이터베이스 조작)를 달성할 수 있다.

한 마디로 모 아니면 도, 0 아니면 1, All or Nothing이다.

2) Consistency (일관성)

자식에게 국가별 식사 예절을 가르치려는 부모가 있었다.
이 부모는 아이에게 한국식 식사 예절과 프랑스식 식사 예절을 가르쳤다.
한국에 있을 때 한국식 예절을 지키면 칭찬을 해줬고, 어기면 혼을 내거나 교정을 해줬다. 프랑스에서도 동일하게 하였다.
그 결과 아이는 한국에서는 한국식으로, 프랑스에서는 프랑스식으로 식사 예절을 잘 지키게 되었다.

자식이 있는 또 다른 부모가 있었다.
위 이야기의 부모처럼 아이에게 한국식 그리고 프랑스식 식사 예절을 가르쳤다.
그러나 아이가 식사 예절을 지키면 어느 때는 칭찬을 하고, 어느 때는 안했다. 예절을 안 지키면 어느 때는 혼을 내고, 어느 때는 내지 않았다. 프랑스에서도 동일하게 하였다.
그 결과 아이는 언제 어디서 어떤 식사 예절을 지켜야 할지 혼란스러웠고, 결국 한국과 프랑스 어딜 가든 식사 예절이 없는 아이가 되었다.

트랜잭션의 일관성은 트랜잭션이 수행이 완료된 후에도 데이터베이스가 일관된 상태를 유지하는 것을 의미한다. 이를 위해 데이터베이스 시스템의 고정적인 요소는 트랜잭션 전후 모두 동일해야 하며, 데이터베이스의 여러 가지 규칙과 제약을 지키는 유효한 데이터만 데이터베이스에 기록되어야 한다.

여기서 짚고 넘어갈 점은, 트랜잭션이 수행되는 도중에 데이터베이스의 내용이 변하면 트랜잭션의 결과는 변동 후의 데이터베이스가 아닌 트랜잭션이 시작된 순간, 즉 변동 전의 데이터베이스를 기준으로 적용된다.
변동 전 내용대로 일처리를 했는데 결과는 변동 후 내용을 기준으로 하라고 하면 어긋나는 부분이 생기기 때문이다.

각각의 트랜잭션이 시작부터 종료까지 자신에게 주어진 일관된 규칙을 따르므로, 그 결과 데이터베이스는 어긋남이 없이 일관된 상태를 유지할 수 있다.

한국식으로 식사를 시작했으면 식사를 마칠 때까지 한국식으로 먹는 것이다.
식사 도중에 마법에 걸려 장소가 갑자기 프랑스로 바뀌었으면 다음 식사부터 프랑스 식으로 하면 되는 것이다.
단, 식사 방식에 맞는 예절을 항상 지켜야 한다.

3) Isolation (고립성)

위에서 나온 식사 예절을 잘 지키는 가족을 다시 떠올려보자.
이 가족이 오랜만에 레스토랑에 외식을 하러 나왔다.
레스토랑이다보니 다른 여러 가족들도 있다.
편한 자리를 잡고 음식을 주문하여 이제 식사를 시작하려고 한다.
그런데 이때 식사 예절이 최악인 어느 가족이 이들의 테이블에 난입했다!
그리고 허락도 없이 예절 있는 가족의 음식을 집어먹으며 자리를 잔뜩 어지럽혔다.
그렇게 식사를 망쳤다.

지난번의 경험으로 인해 이번에는 칸막이가 있는 레스토랑으로 외식을 나왔다.
칸막이 덕분에 다른 사람도 보이지 않고, 방해하는 사람도 없어 아주 깔끔하게 식사를 했다.

출처_ mediamatic 홈페이지 캡처

트랜잭션의 고립성은 칸막이 있는 레스토랑에서의 식사와 같다.
식사를 하는 와중에 다른 가족들 누가 있는지 안 보이고 방해도 받지 않는 것처럼, 트랜잭션도 다른 트랜잭션들과 완전히 분리되어 자신의 할 일을 수행한다.

또한 현재 어떤 테이블에서 식사 중인 사람들이 식사를 완전히 다 마치고 자리를 뜨고 나서야 다음 사람들이 그 테이블에 가서 식사를 할 수 있듯이, 어떠한 대상에 대해 여러 트랜잭션이 작업을 해야 할 경우 그 대상에 도착한 순서대로 작업을 수행하고, 먼저 온 트랜잭션의 수행이 완료되어야 다음 트랜잭션이 작업을 시작할 수 있다.

4) Durability (지속성)

레스토랑에서 식사를 하고서 카드 결제를 하고 기분 좋게 집에 왔다.
그런데 그 레스토랑에서 전화가 왔다. 결제를 안 했단다. 뭐라고?
이야기를 들어보니 레스토랑 카운터의 컴퓨터가 잠시 먹통이 되어 껐다 켰는데, 주문 내역을 보니 나의 주문 내역은 있는데 결제 내역은 없다고 한다.
그래서 나의 카드 결제 내역을 보내서 재확인을 요청했고, 레스토랑이 카드사에 확인을 하니 기록이 있었다.

그런데 갑자기 번개가 쳐서 카드사 서버가 다운이 되었다가 몇 분 후에 복구되었다고 한다.
혹시나 레스토랑처럼 내 결제 내역이 없다고 하면 어쩌지? 전화를 해서 물어보니 다행히도 기록이 잘 남아 있었다. 그렇게 해결이 되었다.

트랜잭션의 지속성이란 각각의 트랜잭션이 수행되면 그 결과가 '영구적'으로 저장이 되는 것을 뜻한다.
여기서 '영구적'이라는 말은 데이터베이스가 있는 컴퓨터가 갑자기 꺼지든, 렉이 걸리든 상관 없이 트랜잭션의 수행 기록이 남아 있음을 의미한다.

위 이야기에서 레스토랑은 트랜잭션의 지속성이 없고, 카드사는 지속성이 있다고 볼 수 있다.


트랜잭션의 상태

트랜잭션의 작동 상태는 5가지로 구분된다.

  • 활동(Active) : 트랜잭션이 시작되어 실행 중인 상태
  • 부분 완료(Partially Commited) : 트랜잭션의 모든 과정이 완료되었고, COMMIT이 되기 직전인 상태
  • 완료(Commited) : 트랜잭션의 모든 과정이 완료되었고, COMMIT이 된 상태
  • 실패(Failed) : 트랜잭션 과정에 (단 1이라도) 오류가 발생하여 중지된 상태
  • 철회(Aborted) : 트랜잭션 오류로 인해 비정상 종료되어 ROLLBACK된 상태

트랜잭션의 연산

트랜잭션의 연산으로 COMMIT, ROLLBACK, SAVEPOINT 등이 있다. 추가로 COMMIT의 자동 수행 여부를 결정하는 AUTOCOMMIT 기능이 있다.

  • COMMIT : 트랜잭션이 제대로 수행된 후 그 결과를 확정하여 데이터베이스에 반영한다는 명령
  • ROLLBACK : 트랜잭션 수행 도중 오류가 발생하여 트랜잭션이 시작되기 전 상태로 되돌리는 명령
  • SAVEPOINT
    • 트랜잭션 과정을 여러 개로 나누어 주기 위한 명령
    • 이를 통해 트랜잭션 과정의 일부만 ROLLBACK 가능
  • AUTOCOMMIT : 각각의 SQL문을 하나의 트랜잭션으로 취급하여 매 SQL문마다 COMMIT이 되게 함
    • SET AUTOCOMMIT = OFF
      • 하나의 트랜잭션 단위로 COMMIT
    • SET AUTOCOMMIT = ON
      • 각각의 SQL문을 트랜잭션으로 취급하여 매 SQL문마다 COMMIT

(아래 예시는 MariaDB로 실행하였습니다.)
(불필요한 결과 출력은 생략하였습니다.)

1) COMMIT

MariaDB [nation]> SET autocommit = ON;
MariaDB [nation]> SELECT * FROM guests; -- 1
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
+----------+-------+
MariaDB [nation]> START TRANSACTION; -- autocommit ON 상태에서 이걸 쓰면 COMMIT이나 ROLLBACK을 하기 전까지는 쿼리 반영이 되지 않음
MariaDB [nation]> INSERT INTO guests VALUE (10, 'a');
MariaDB [nation]> SELECT * FROM guests; -- 2
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
|       10 | a     |
+----------+-------+
MariaDB [nation]> INSERT INTO guests VALUE (11, 'b');
MariaDB [nation]> UPDATE guests SET name='Name_Changed' WHERE name='John';
MariaDB [nation]> SELECT * FROM guests; -- 3
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
|       11 | b            |
+----------+--------------+
MariaDB [nation]> DELETE FROM guests WHERE guest_id = 11;
MariaDB [nation]> SELECT * FROM guests; -- 4
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
+----------+--------------+
MariaDB [nation]> COMMIT;
MariaDB [nation]> SELECT * FROM guests; -- 5
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
+----------+--------------+

START TRANSACTION으로 트랜잭션을 시작하여 COMMIT으로 끝을 내어 트랜잭션의 결과가 데이터베이스에 적용이 되었다.
(※ 참고 : autocommit을 OFF로 설정하면 START TRANSACTION을 쓰지 않아도 각 쿼리문들의 실행 결과가 COMMIT나 ROLLBACK을 하기 전까지 반영되지 않는다.)

2) ROLLBACK

MariaDB [nation]> SET autocommit = ON;
MariaDB [nation]> SELECT * FROM guests; -- 1
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
+----------+-------+
MariaDB [nation]> START TRANSACTION;
MariaDB [nation]> INSERT INTO guests VALUE (10, 'a');
MariaDB [nation]> SELECT * FROM guests; -- 2
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
|       10 | a     |
+----------+-------+
MariaDB [nation]> INSERT INTO guests VALUE (11, 'b');
MariaDB [nation]> UPDATE guests SET name='Name_Changed' WHERE name='John';
MariaDB [nation]> SELECT * FROM guests; -- 3
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
|       11 | b            |
+----------+--------------+
MariaDB [nation]> DELETE FROM guests WHERE guest_id = 11;
MariaDB [nation]> SELECT * FROM guests; -- 4
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
+----------+--------------+
MariaDB [nation]> ROLLBACK;
MariaDB [nation]> SELECT * FROM guests; -- 5
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
+----------+-------+

START TRANSACTION으로 트랜잭션을 시작하여 ROLLBACK으로 끝을 냈다. 그 결과 그 사이에 있던 모든 쿼리들에 대한 실행 결과가 트랜잭션의 시작 전으로 되돌아갔다.

3) SAVEPOINT

MariaDB [nation]> SELECT * FROM guests; -- 1
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
+----------+-------+
MariaDB [nation]> START TRANSACTION;
MariaDB [nation]> INSERT INTO guests VALUE (10, 'a');
MariaDB [nation]> SELECT * FROM guests; -- 2
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
|       10 | a     |
+----------+-------+
MariaDB [nation]> SAVEPOINT two; -- 이 지점 이전으로 상태를 되돌릴 예정
MariaDB [nation]> INSERT INTO guests VALUE (11, 'b');
MariaDB [nation]> UPDATE guests SET name='Name_Changed' WHERE name='John';
MariaDB [nation]> SELECT * FROM guests; -- 3
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
|       11 | b            |
+----------+--------------+
MariaDB [nation]> DELETE FROM guests WHERE guest_id = 11;
MariaDB [nation]> SELECT * FROM guests; -- 4
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
+----------+--------------+
MariaDB [nation]> ROLLBACK TO two;
MariaDB [nation]> SELECT * FROM guests; -- 5
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
|       10 | a     |
+----------+-------+

SAVEPOINT two를 설정 후, ROLLBACK을 해당 시점으로 지정하여 실행하였다. 그 결과 SAVEPOINT two 이전에 수행되었던 쿼리의 결과는 남고, 그 이후의 쿼리 결과들은 사라진 것을 확인할 수 있다.

4) AUTOCOMMIT

SET autocommit = ON;
MariaDB [nation]> SELECT * FROM guests; -- 1
+----------+-------+
| guest_id | name  |
+----------+-------+
|        1 | John  |
|        2 | Jane  |
|        3 | Jean  |
|        4 | Storm |
|        5 | Beast |
+----------+-------+
MariaDB [nation]> INSERT INTO guests VALUE (10, 'a');
MariaDB [nation]> INSERT INTO guests VALUE (11, 'b');
MariaDB [nation]> UPDATE guests SET name='Name_Changed' WHERE name='John';
MariaDB [nation]> SELECT * FROM guests; -- 2
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
|       11 | b            |
+----------+--------------+
MariaDB [nation]> ROLLBACK;
MariaDB [nation]> SELECT * FROM guests; -- 3
+----------+--------------+
| guest_id | name         |
+----------+--------------+
|        1 | Name_Changed |
|        2 | Jane         |
|        3 | Jean         |
|        4 | Storm        |
|        5 | Beast        |
|       10 | a            |
|       11 | b            |
+----------+--------------+

<참고 자료>

데이터베이스 관계(Database Relationships)

데이터베이스 관계란?

관계는 테이블 간에 둘 다 존재한다. 이 관계들은 일대일, 일대다, 다대다, 이렇게 세 가지 형태로 이루어진다. 대부분의 관계형 데이터베이스들은 각 로우의 각 컬럼이 하나의 값만을 보유할 수 있도록 설계되어 있다. (값은 원자적이다)
- 위키백과(관계형 데이터베이스 - 키 (Key) - 관계 (Relationships)

데이터베이스 관계란 데이터베이스 내 어떤 테이블들이 서로 어떻게 연결되어 있는지 나타낸 것이다.

데이터베이스 관계에는 일대일(1:1), 일대다(1:N), 다대다(N:N) 관계가 있다.

1. 일대일 관계 (One-to-one)

두 개의 테이블 A와 B가 있으면 A의 레코드 하나가 B의 레코드 하나와 연결된 관계이다.
예를 들면 한 사람의 주민등록번호와 여권 번호 같은 경우가 있다.

일대일 관계는 애초부터 하나의 테이블로 표현하는 경우가 많다.

2. 일대다 관계 (One-to-many)

테이블 A의 레코드 하나가 B의 레코드 여러 개와 연결된 관계이다.
예를 들면 한 사람이 여러 채의 아파트를 가진 경우가 있다.

3. 다대다 관계 (Many-to-many)

테이블 A의 레코드 하나가 B의 레코드 여러 개와,
테이블 B의 레코드 하나가 A의 레코드 여러 개와 연결된 관계이다.
예를 들면 대형마트의 고객과 판매 물품 간의 관계를 들 수 있다.

다대다 관계를 위처럼 표현할 수는 있으나, 실제로는 구현할 수가 없다.
만약 구현하려고 하면 한 고객의 구매 물품 번호에 여러 개의 값을 넣어야 할 수도 있는데, 이는 데이터베이스 제1 정규화를 깨뜨린다.

이런 이유로 실제로는 두 테이블 사이를 연결해주는 별도의 테이블$^*$$^1$을 만들어서 다대다 관계를 두 개의 일대다 관계로 풀어준다.

*1 이 별도의 테이블을 교차 엔티티라고 한다.


<참고 자료>

기준 모델(Baseline Model)

기준 모델이 뭐지?

baseline
an imaginary line used as a starting point for making comparisons
- Cambridge English Dictionary

a line serving as a basis
- Merriam-Webster

AI : 사장님. 저 일. 다했습니다. 저 잘했습니다. 삐릿삐릿.
사장 : 너는 뭘 믿고 너가 잘했다고 생각하니?
AI : 제가. '저 친구'보다는. 잘했습니다. 삐릿삐릿.
- 작자 미상 (사실은 블로그 작성자 머릿속...)

기준 모델은 머신 러닝 모델을 만들 때 말 그대로 '기준'이 되는 모델이다.
위에서 AI가 '저 친구'보다는 잘했다고 말한 것처럼, 학습 및 최적화를 거친 모델이라면 최소한 기준 모델보다는 성능이 좋아야 한다.

기준 모델은 무조건 이렇게 정해야 한다는 규칙은 없다.
다만 가장 간단하면서 흔하게 정해지는 유형은 다음과 같다.

  • 회귀 문제 : 타겟의 평균값 (또는 중앙값)
  • 분류 문제 : 타겟의 값 중 가장 많은 것 (타겟의 최빈 클래스)
  • 시계열 문제 : 이전 타임스탬프의 값

앞서 말했듯이 꼭 이를 기준 모델로 삼을 필요는 없다.
회귀 문제라면 선형 회귀를, 분류 문제라면 로지스틱 회귀를 기준 모델로 정할 수도 있다.
즉, 정하는 사람 마음이다.


기준 모델은 어떻게 쓰는거지?

기준 모델이 무엇인지 알았으니 구체적인 사용 예시를 코드와 함께 살펴보겠다.

여기서는 회귀와 분류 문제 각각에서 기준 모델을 설정 후, 이를 실제로 만들 모델(이하 실제 모델)과 어떤 식으로 비교하는지 설명하겠다.

(코드에 대한 구체적인 설명은 아래 참고 자료에 포함된 Colab 링크를 보시면 확인 가능합니다.)


  1. 데이터 전처리 및 가공
# ! pip install category_encoders
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

from category_encoders import TargetEncoder, OrdinalEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.compose import TransformedTargetRegressor

from sklearn.linear_model import RidgeCV, LogisticRegression

from sklearn.model_selection import train_test_split, RandomizedSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.metrics import mean_absolute_error, r2_score, accuracy_score, f1_score, classification_report
from scipy.stats import randint, uniform
# 자료 출처 : 서울 열린데이터 광장 - 서울특별시 부동산 실거래가 정보
# https://data.seoul.go.kr/dataList/OA-15548/S/1/datasetView.do

df = pd.read_csv('https://docs.google.com/uc?export=download&id=1WyeDe2Ry4ohJobcbnQJXHxFEAtcg-CC_', encoding='cp949')
df = df[df['지번코드'].map(str).apply(len) == 19]
tmp_idx = df[df['층정보'] < 0]['층정보'].index
df.loc[tmp_idx, ['층정보']] = df[df['층정보'] < 0]['층정보'].mul(-1)
df = df[(df['건축년도'] != 0) & ~(df['건축년도'].isna())]
df['층정보'] = df['층정보'].fillna(2)

df = df[['지번코드',  '건물면적',  '층정보',  '건물주용도코드',  '물건금액',  '건축년도']]
df['연식'] = (2020 - df['건축년도']).astype(int)
df['주택규모'] = df['건물면적'].apply(lambda x: 'S' if x <= 60 else 'L' if x > 85 else 'M')
df['고가주택여부'] = df['물건금액'].apply(lambda x: 1 if x > 900000000 else 0)
df[['층정보', '건축년도']] = df[['층정보', '건축년도']].astype(int)
df.columns = ['addr_code', 'size', 'floor', 'type', 'price', 'built_year', 'years', 'size_grade', 'is_high_price']
target = 'price'
df = df[df[target] < np.percentile(df[target], 95)]
df = df[df['size'] < np.percentile(df['size'], 95)]
df = df[df['years'] < np.percentile(df['years'], 95)]

df = df.reset_index(drop=True)

  1. 기준 모델 설정 및 평가
    회귀 : 타겟의 평균값 (평가지표 : MAE)
    분류 : 타겟의 최빈 클래스 비율 (평가지표 : 정확도)
# 기준 모델(회귀) : 타겟(price)의 평균값
baseline = df.price.mean()
baseline

470946234.08997595

# 기준 성능(회귀) - MAE
errors = baseline - df.price
baseline_mae = errors.abs().mean()
print(f'Baseline MAE: {baseline_mae:,.0f}')

Baseline MAE: 254,535,177

# 기준 모델(분류) : 타겟(is_high_price)의 최빈 클래스 비율
sns.countplot(x=df.is_high_price);
df.is_high_price.value_counts(normalize=True) 

# 기준 성능(분류) - 정확도 = 0.895
# 모든 예측값을 0으로 찍어도 정확도가 0.895가 나옴
# 따라서 실제 모델의 정확도는 0.895보다 높아야 유효

0 0.895478
1 0.104522
Name: is_high_price, dtype: float64


  1. 실제 모델 만들기 및 평가
# 물건가격 예측은 회귀, 고가주택 여부는 분류
# 회귀 : 릿지 회귀
# 분류 : 로지스틱 회귀

# 훈련 / 테스트 셋으로 분리
train, test = train_test_split(df, test_size=0.30, random_state=2)
train.shape, test.shape

((95474, 9), (40918, 9))

# 특성과 타겟 분리하기
target_cl = 'is_high_price' # 분류 문제 타겟
targets = [target, target_cl]
features = train.drop(columns=targets).columns

X_train = train[features]
y_train = train[target]
y_train_cl = train[target_cl]

X_test = test[features]
y_test = test[target]
y_test_cl = test[target_cl]
# 범주형 특성 인코딩을 위해 특성들을 구분
tge_col = ['addr_code', 'type'] # 범주 간의 순서가 없는 특성
ord_col = ['size_grade'] # 범주 간의 순서가 있는 특성(소/중/대)
# 1. 회귀 - 릿지 회귀
alphas = [0, 0.001, 0.01, 0.1, 1]

pipe_ridge = make_pipeline(
    TargetEncoder(cols=tge_col),
    OrdinalEncoder(cols=ord_col),
    RidgeCV(alphas=alphas, cv=5)
)

tt_ridge = TransformedTargetRegressor(regressor=pipe_ridge,
                                func=np.log1p, inverse_func=np.expm1)

tt_ridge.fit(X_train, y_train);

y_pred_ridge = tt_ridge.predict(X_test)
mae_ridge = mean_absolute_error(y_test, y_pred_ridge)
print(f'MAE: {mae_ridge:,.0f}')

MAE: 86,647,593

# 2. 분류 - 로지스틱 회귀

# class weights 계산
# n_samples / (n_classes * np.bincount(y))
custom = len(y_train_cl)/(2*np.bincount(y_train_cl))

pipe_logis = make_pipeline(
    TargetEncoder(cols=tge_col),
    OrdinalEncoder(cols=ord_col),
    StandardScaler(),    # 각 특성별 스케일을 통일시켜줌
    LogisticRegression(class_weight={False:custom[0],True:custom[1]}, random_state=2)
)

pipe_logis.fit(X_train, y_train_cl)

y_pred_logis = pipe_logis.predict(X_test)
accu_logis = accuracy_score(y_test_cl, y_pred_logis)
print(f'Accuracy Score: {accu_logis:,.3f}')

Accuracy Score: 0.953


  1. 결론
  • 회귀
    • 기준 모델 MAE = 254,535,177
    • 실제 모델 MAE = 86,647,593
  • 분류
    • 기준 모델 정확도 = 0.895
    • 실제 모델 정확도 = 0.953

회귀, 분류 모두 실제 모델이 기준 모델보다 성능이 뛰어나므로 실제 모델을 '당장 버릴 필요는 없다'고 볼 수 있다.


  1. 주의할 점

당연한 말이지만, 기준 모델과 실제 모델의 성능 비교 시 동일한 평가지표를 기준으로 하여 비교해야 한다.

나의 경우, 타겟의 최빈 클래스 비율을 실제 모델의 F1 Score와 비교를 하는 실수를 저질렀는데, 둘 모두 0에서 1 사이의 값으로 표현이 되다 보니 작업을 하는 도중 혼동이 있었다.

따라서 비교 대상이 무엇인지 명확하게 한 후에 비교를 진행해야 할 것이다.

또한 만든 모델이 기준 모델보다 성능이 좋다고 해서 항상 사용 가능하다고 볼 수는 없다.
위에서 '당장 버릴 필요는 없다'고 표현한 이유도, 당장은 기준 모델보다 성능이 좋아도 이 모델을 실전에 투입하는 것은 다른 문제이기 때문이다.

예를 들어 기준 모델의 정확도가 0.3이고 실제 모델은 0.31이 나왔다면 이를 바로 실전에서 쓸 수 있을까?
상황에 따라 다르겠지만, 아무래도 바로 쓰기에는 망설여질 것이다.


<참고 자료>

 

릿지 회귀(Ridge Regression)

릿지 회귀가 뭐지?

Ridge regression is a method of estimating the coefficients of multiple-regression models in scenarios where independent variables are highly correlated.
- Wikipedia(Ridge regression)

기존의 다중 선형 회귀선을 팽팽한 고무줄이라고 하면, 릿지 회귀는 이 고무줄을 느슨하게 만들어준 것이다.

다중 선형 회귀 모델은 특성이 많아질수록 훈련 데이터에 과적합되기 쉽다. 이는 마치 "나는 훈련에서 무조건 만점을 받겠어!"라고 하여 열심히 훈련을 해서 만점을 받았는데, 정작 실전에서는 뭐 하나 제대로 못하는 병사같다.
이에 반해 릿지 회귀는 훈련은 좀 덜 열심히 했지만, 실전에서는 꽤 쓸만한 병사라고 할 수 있겠다.


릿지 회귀는 어떻게 생겼지?

과적합된 다중 선형 회귀 모델은 단 하나의 특이값에도 회귀선의 기울기가 크게 변할 수 있다. 릿지 회귀는 어떤 값을 통해 이 기울기가 덜 민감하게 반응하게끔 만드는데, 이 값을 람다(lambda, $\lambda$)라고 한다.
릿지 회귀의 식은 아래와 같다.

$$\beta_{ridge} : argmin[\sum_{i=1}^n(y_i - \beta_0 - \beta_1x_{i1}-\dotsc-\beta_px_{ip})^2 + \lambda\sum_{j=1}^p\beta_j^2]$$

(n: 샘플 수, p: 특성 수, λ: 튜닝 파라미터(패널티))
(참고 - 람다 : lambda, alpha, regularization parameter, penalty term 모두 같은 뜻)

식의 앞 부분은 다중 선형 회귀에서의 최소제곱법(OLS, Ordinary Least Square)과 동일하다.
뒤쪽의 람다가 붙어 있는 부분이 기울기를 제어하는 패널티 부분이다.
뒷부분을 자세히 보면 회귀계수 제곱의 합으로 표현되어 있는데, 이는 L2 Loss$^*$$^1$와 같다. 이런 이유로 릿지 회귀를 L2 정규화(L2 Regularization)라고도 한다.

만약 람다가 0이면 위 식은 다중 선형 회귀와 동일하다.
반대로 람다가 커지면 커질수록 다중 회귀선의 기울기를 떨어뜨려 0으로 수렴하게 만든다. 이는 덜 중요한 특성의 개수를 줄이는 효과로도 볼 수 있다.

적합한 람다의 값을 구하는 방법은 아래에서 scikit-learn을 통해 예시를 들겠다.


scikit-learn을 이용하여 릿지 회귀 수행하기

scikit-learn을 통해 릿지 회귀를 사용하는 방법으로 Ridge와 RidgeCV가 있다.
둘 모두 같은 릿지 회귀이나, RidgeCV는 여러 alpha값(=람다)을 모델 학습 시에 한꺼번에 받아서 자기 스스로 각각의 alpha값에 대한 성능을 비교 후 가장 좋은 alpha를 선택한다. (CV : Cross Validation$^*$$^2$)

아래 예시에서는 먼저 다중 선형 회귀와 릿지 회귀의 차이를 간단히 살펴본 후, 보다 구체적인 Ridge 그리고 RidgeCV의 사용을 다루겠다.

  1. 다중 선형 회귀와 릿지 회귀 간단한 비교
    (사용 데이터 : Anscombe's quartet)
# 데이터 불러오기
import seaborn as sns

ans = sns.load_dataset('anscombe').query('dataset=="III"')
baseline = ans.y.mean() # 기준 모델
sns.lineplot(x='x', y=baseline, data=ans, color='red'); # 기준 모델 시각화
sns.scatterplot(x='x', y='y', data=ans);

 

# 다중 선형 회귀(OLS)
%matplotlib inline

ax = ans.plot.scatter('x', 'y')

# OLS 
ols = LinearRegression()
ols.fit(ans[['x']], ans['y'])

# 회귀계수와 intercept 확인
m = ols.coef_[0].round(2)
b = ols.intercept_.round(2)
title = f'Linear Regression \n y = {m}x + {b}'

# 훈련 데이터로 예측
ans['y_pred'] = ols.predict(ans[['x']])

ans.plot('x', 'y_pred', ax=ax, title=title);

 

# 릿지 회귀
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge

def ridge_anscombe(alpha):
    """
    alpha : lambda, penalty term
    """
    ans = sns.load_dataset('anscombe').query('dataset=="III"')

    ax = ans.plot.scatter('x', 'y')

    ridge = Ridge(alpha=alpha, normalize=True)
    ridge.fit(ans[['x']], ans['y'])

    # 회귀계수와 intercept 가져오기
    m = ridge.coef_[0].round(2)
    b = ridge.intercept_.round(2)
    title = f'Ridge Regression, alpha={alpha} \n y = {m}x + {b}'

    # 예측
    ans['y_pred'] = ridge.predict(ans[['x']])

    ans.plot('x', 'y_pred', ax=ax, title=title)
    plt.show()

# 여러 알파값을 넣어서 기울기의 변화 확인하기
alphas = np.arange(0, 2, 0.4)
for alpha in alphas:
    ridge_anscombe(alpha=alpha)

 

∴ 그래프를 보면 alpha = 0인 경우에는 OLS와 같은 그래프 형태로 같은 모델임을 확인할 수 있고. alpha 값이 커질수록 직선의 기울기가 0에 가까워 지면서 평균 기준모델(baseline)과 비슷해진다.


  1. Ridge & RidgeCV 구체적인 활용 방법

1) 데이터 불러오기 및 전처리
(사용할 데이터 : Melbourne Housing Market)

import pandas as pd
from sklearn.model_selection import train_test_split

# 데이터 불러오기
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/melbourne_house_prices/MELBOURNE_HOUSE_PRICES_LESS.csv')

# 범주형 특성 중 값의 종류가 너무 많은 특성은 제외
df.drop(columns=['Suburb','Address','SellerG','Date'], inplace=True)

# 결측치인 타겟 값 제거
df.dropna(subset=['Price'], inplace=True)

# 중복된 행 제거
df.drop_duplicates(inplace=True)
from category_encoders import OneHotEncoder

# 사용할 특성들과 타겟을 별도로 분리
target = 'Price'

data = df.drop(target, axis=1)
target = df[target]

# 훈련 / 테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(data, target, train_size=0.8, test_size=0.2, random_state=2)

# 범주형 특성을 수치형으로 변환하는 인코딩 수행
# 자세한 내용은 아래 참고 자료 링크(One-hot Encoder)에 있습니다
encoder = OneHotEncoder(use_cat_names = True)
X_train = encoder.fit_transform(X_train)
X_test = encoder.transform(X_test)

2) Ridge

from sklearn.linear_model import Ridge

alphas = [0, 0.001, 0.01, 0.1, 1]

# Ridge의 경우 alpha값을 이와 같이 따로 따로 넣어주어야 함
for alpha in alphas:
  ridge = Ridge(alpha=alpha, normalize=True)
  ridge.fit(X_train, y_train)
  y_pred = ridge.predict(X_test)

  mae = mean_absolute_error(y_test, y_pred)
  r2 = r2_score(y_test, y_pred)
  print(f'Test MAE: ${mae:,.0f}')
  print(f'R2 Score: {r2:,.4f}\n')

Test MAE: $255,214
R2 Score: 0.5877

Test MAE: $255,264
R2 Score: 0.5878

Test MAE: $254,701
R2 Score: 0.5874

Test MAE: $252,997
R2 Score: 0.5794

Test MAE: $279,498
R2 Score: 0.4742


3) RidgeCV

from sklearn.linear_model import RidgeCV
from sklearn.metrics import mean_absolute_error, r2_score

alphas = [0, 0.001, 0.01, 0.1, 1]

# RidgeCV는 alpha로 넣고자 하는 값들을 리스트로 전달하면 내부적으로 최적의 alpha값을 찾아냄
ridgecv = RidgeCV(alphas=alphas, normalize=True, cv=5)
# cv : cross-validation -> 데이터를 k등분한 후 각각에 대하여 검증 진행
# 검증 결과 가장 점수가 높은 모델을 채택
ridgecv.fit(X_train, y_train)
y_pred = ridgecv.predict(X_test)

mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f'Test MAE: ${mae:,.0f}')
print(f'R2 Score: {r2:,.4f}\n')

print(f'alpha: {ridgecv.alpha_}') # 최종 결정된 alpha값
print(f'cv best score: {ridgecv.best_score_}') # 최종 alpha에서의 점수(R^2 of self.predict(X) wrt. y.)

Test MAE: $255,264
R2 Score: 0.5878

alpha: 0.001
cv best score: 0.5705823371670962


*1 L2 Loss란?
L2 Loss는 L2 Norm을 기준으로 만들어진 손실 함수이다.
다만 L2 Norm이 각 원소 제곱의 합에 루트를 씌워준 것이라면 L2 Loss는 루트를 씌우지 않는다는 차이가 있다.
$$L2\ Norm = \sqrt{\sum_{i=1}^{n} x_i^2}$$
$$L2\ Loss = \sum_{i=1}^{n} (y - \hat{y})^2$$

*2 CV : Cross Validation(교차 검증)?
교차 검증은 주어진 데이터를 동일한 크기로 (고등어 자르듯이) 여러 등분한 후, 각각에 대하여 검증을 진행하는 것이다.
이는 모델이 한 가지 경우에만 잘 맞는, 즉 과적합을 줄이고자 행하는 작업이다.


<참고 자료>

랜덤 포레스트(Random Forest)

※ 결정 트리를 모른다면?(아래 클릭 시 링크로 이동)
결정 트리(Decision Tree)

랜덤 포레스트가 뭐지?

기계 학습에서의 랜덤 포레스트(영어: random forest)는 분류, 회귀 분석 등에 사용되는 앙상블 학습 방법$^*$$^1$의 일종으로, 훈련 과정에서 구성한 다수의 결정 트리로부터 분류 또는 평균 예측치(회귀 분석)를 출력함으로써 동작한다.
- 위키백과

랜덤 포레스트는 단순하게 말하면 결정 트리 여러 개를 모아 놓은 것이다.
이름 그대로 나무(Tree)가 모여 있으니 숲(Forest)이 되는 것이다.

랜덤(Random, 무작위)이라는 말은 왜 붙어 있을까? 이는 아래 내용을 참고하면 이해 가능할 것이다.

초기 랜덤 포레스트는 단일 트리를 확장할 때 가능한 결정(available decisions)중 임의의 부분집합(random subset)에서 검색하는 아이디어를 도입한 얄리 아미트(Yali Amit)와 도널드 게먼(Donald Geman)의 연구에 영향을 받았다. 또한 임의의 부분공간(random subspace)을 선택하는 틴 캄 호(Tin Kam Ho)의 아이디어 역시 랜덤 포레스트의 디자인에 영향을 미쳤다. - 위키백과(랜덤 포레스트 - 1 도입 - 1.2 역사 中)

랜덤 포레스트는 왜 만들어졌을까?
랜덤 포레스트는 결정 트리로부터 나온 것이다. 결정 트리는 한 개의 트리만을 사용하기에 한 노드에서의 에러가 그의 하부 노드까지 계속해서 영향을 주기도 하고, 트리의 깊이에 따라 과적합되는 문제 등이 있다. 이러한 이유로 새로운 데이터의 분류에 유연하게 대응하지 못하는데, 이를 보완하기 위해 나온 것이 랜덤 포레스트다.


랜덤 포레스트는 어떻게 하는거지?

랜덤 포레스트는 아래와 같은 순서로 진행된다.

  1. 초기 데이터셋(Original Dataset)으로부터 파생된 여러 개의 데이터셋을 만든다. 이 과정은 부트스트랩 샘플링$^*$$^2$을 통해 이루어진다.
  2. 1번에서 만들어진 각각의 데이터셋에 대한 결정 트리를 만든다. 여기서 주의할 점은 트리를 만들 때 트리의 각 분기마다 해당 데이터셋의 특성 중 일부를 무작위로 가져온다는 것이다.

이렇게 해서 만들어진 결정 트리들을 모아놓은 것이 바로 랜덤 포레스트이다.

그럼 랜덤 포레스트는 타겟을 어떻게 예측할까?

만약 어떤 특성을 가진 데이터의 타겟을 예측하기 위해 랜덤 포레스트에 넣는다고 하자.
이 랜덤 포레스트 안에는 여러 개의 결정 트리가 있고, 각각의 결정 트리는 이 데이터에 대한 타겟 예측을 내놓는다. 그 후 이들의 예측 모두를 종합하여 최종 타겟 예측 값을 결정하는데, 이를 배깅(Bagging, Bootstrap Aggregating)이라고 한다.

회귀 문제에서는 예측을 종합한 값의 평균이, 분류 문제에서는 다수결로 선택된 클래스가 최종 예측 값이 된다.

잠깐, 예측하기 전에 검증은 어떻게 하지?

랜덤 포레스트를 만들 때 필요한 여러 데이터셋을 만드는 과정에서 부트스트랩 샘플링을 사용한다고 하였다. 여기서 샘플링 과정에 포함되지 않는 데이터가 생기는데, 이를 Out-Of-Bag(OOB) 데이터라고 한다.
랜덤 포레스트의 검증은 이 OOB 데이터를 통해 이루어진다. 이를 통해 OOB 데이터에 대한 예측 결과와 실제 값의 오차, 즉 OOB 오차(Out-Of-Bag Error)를 얻을 수 있다.

이후 검증 결과를 바탕으로 랜덤 포레스트의 하이퍼 파라미터(개별 트리의 최대 깊이, 노드 분기를 위한 최소한의 샘플 수 등)를 조절하여 OOB 오차를 최소화시킨다.

scikit-learn을 통해 랜덤 포레스트 사용하기

  1. 데이터 불러오기 및 전처리
# 데이터 불러오기
import pandas as pd

target = 'vacc_h1n1_f'
train = pd.merge(pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train.csv'),
pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train_labels.csv')[target], left_index=True, right_index=True)
test = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/test.csv')
sample_submission = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/submission.csv')
# 데이터를 훈련 / 검증 세트로 분리
from sklearn.model_selection import train_test_split

train, val = train_test_split(train, train_size=0.80, test_size=0.20,
stratify=train[target], random_state=2)

train.shape, val.shape, test.shape

((33723, 39), (8431, 39), (28104, 38))

# 데이터 전처리
def  engineer(df):
    """특성을 엔지니어링 하는 함수입니다."""
    # 새로운 특성을 생성합니다.
    behaviorals = [col for col in df.columns if  'behavioral'  in col]
    df['behaviorals'] = df[behaviorals].sum(axis=1)

    dels = [col for col in df.columns if  ('employment'  in col or  'seas'  in col)]
    df.drop(columns=dels, inplace=True)

    return df

train = engineer(train)
val = engineer(val)
test = engineer(test)
# 데이터를 특성과 타겟으로 분리
features = train.drop(columns=[target]).columns  

X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]
X_test = test[features]
  1. 랜덤 포레스트 사용하기
from category_encoders import OrdinalEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline  

# 범주형 특성들의 값을 숫자로 대체하기 위하여 OrdinalEncoder 사용
# handle_missing="return_nan"은 범주형 특성에 있는 NaN을 항상 -2로 대체시킴
# 만약 handle_missing="value"(기본값)인 경우 NaN이 -2가 아닌 일반적인 범주 종류로 인식되어 값이 대체될 수도 있다.
pipe = make_pipeline(
    OrdinalEncoder(handle_missing="return_nan"),
    SimpleImputer(),
    RandomForestClassifier(max_depth=13, n_estimators=120, random_state=10, n_jobs=-1, oob_score=True)
)  
# RandomForestClassifier의 n_estimators는 랜덤 포레스트 내의 결정 트리 개수를 의미함
# 그 외 결정 트리의 과적합을 줄이기 위해 사용하는 max_depth, min_samples_split, min_samples_leaf도 설정 가능

pipe.fit(X_train, y_train)
print('훈련 정확도', pipe.score(X_train, y_train))
print('검증 정확도', pipe.score(X_val, y_val))
print('f1 score', f1_score(y_val, pipe.predict(X_val)))

훈련 정확도 0.8894523025828069
검증 정확도 0.8315739532677026
f1 score 0.5520504731861199

# OOB 스코어(OOB 오차의 반대 개념) 확인
pipe.named_steps['randomforestclassifier'].oob_score_

0.821516472437209


*1 앙상블 학습 방법?

앙상블(프랑스어: ensemble)은 전체적인 어울림이나 통일. ‘조화’로 순화한다는 의미의 프랑스어이며 음악에서 2인 이상이 하는 노래나 연주를 말한다.
- 위키백과(앙상블)

보통 우리가 아는 '앙상블'이라는 말의 의미는 위와 같다.

앙상블 방법은 한 종류의 데이터로 여러 머신러닝 학습 모델(weak base learner, 기본 모델)을 만들어 그 모델들의 예측 결과를 다수결이나 평균을 내어 예측하는 방법이다.
랜덤 포레스트는 결정 트리를 기본 모델로 사용하는 앙상블 방법이다.


*2 부트스트랩 샘플링?


부트스트랩 샘플링이란 어떠한 데이터셋에 대하여 샘플링을 할 때 복원 추출, 즉 뽑았던 것을 또 뽑을 수 있도록 한 것이다. 이렇게 샘플링을 반복하여 만들어지는 것이 부트스트랩 세트(Bootstrap Set)이다. 복원 추출을 했기 때문에 각각의 부트스트랩 세트 안에는 같은 샘플이 중복되어 나타날 수 있다.

부트스트랩 세트의 크기가 n이라 할 때 한 번의 추출 과정에서 어떤 한 샘플이 추출되지 않을 확률은 다음과 같다.

$\displaystyle \frac {n-1}{n}$

n회 복원 추출을 진행했을 때 그 샘플이 추출되지 않을 확률은 다음과 같다.

$\displaystyle \left({\frac {n-1}{n}}\right)^{n}$

n을 무한히 크게 했을 때 이 식은 다음과 같다.

$\displaystyle \lim _{{n\to \infty }}\left({1 - \frac {1}{n}}\right)^{n} = e^{-1} = 0.368$
※ 참고 : $\displaystyle e = \lim _{{n\to \infty }}\left(1+{\frac {1}{n}}\right)^{n}$

즉, 데이터가 충분히 크다고 가정했을 때 한 부트스트랩 세트는 표본의 63.2% 에 해당하는 샘플을 가진다.
여기서 추출되지 않는 36.8%의 샘플이 Out-Of-Bag 샘플이며 이것을 사용해 모델을 검증한다.


<참고 자료>

'Machine Learning > Trees' 카테고리의 다른 글

[ML] 결정 트리(Decision Tree)  (0) 2021.10.26

결정 트리(Decision Tree)

결정 트리가 뭐지?

결정 트리(decision tree)는 의사 결정 규칙과 그 결과들을 트리 구조로 도식화한 의사 결정 지원 도구의 일종이다.
- 위키백과 (결정 트리)

결정 트리 학습법(decision tree learning)은 어떤 항목에 대한 관측값과 목표값을 연결시켜주는 예측 모델로서 결정 트리를 사용한다. 이는 통계학과 데이터 마이닝, 기계 학습에서 사용하는 예측 모델링 방법 중 하나이다. 트리 모델 중 목표 변수가 유한한 수의 값을 가지는 것을 분류 트리라 한다. 이 트리 구조에서 잎(리프 노드)은 클래스 라벨을 나타내고 가지는 클래스 라벨과 관련있는 특징들의 논리곱을 나타낸다. 결정 트리 중 목표 변수가 연속하는 값, 일반적으로 실수를 가지는 것은 회귀 트리라 한다.
- 위키백과 (결정 트리 학습법)

결정 트리는 스무고개 같은 것으로 생각하면 간단하다. 어떠한 대상(표본의 특성)에 대하여 첫 질문(뿌리 노드)을 시작으로 여러 질문들(중간 노드, internal node)을 거쳐 마지막 질문(말단 노드, terminal node, leaf)을 끝으로 최종 답(타겟 예측 값)을 낸다.

다만 스무고개는 질문 횟수 20번으로 규칙이 정해져 있지만, 결정 트리는 사용자가 설정하기에 따라 20번보다 더 많을 수도, 적을 수도 있다. 여기서 이 질문 횟수를 결정 트리의 깊이(depth)라고 한다.

이름이 트리(Tree)인 이유는 말 그대로 나무처럼 생겨서 그렇다. 아래와 같이 생겼다.

출처 : 결정 트리 학습법 - 위키백과

위 예시에서 맨 위 '남자인가?'가 결정 트리의 시작점인 뿌리 노드(root node)이다.
빨강('사망')과 초록('생존')색에 아래로 더 이어진 것이 없는 것들은 말단 노드(external node, terminal node, leaf)라고 한다.
그리고 뿌리 노드와 말단 노드 사이에 있는 노드가 중간 노드(internal node)이다.
노드와 노드 사이를 연결하는 선들은 엣지(edge)라고 한다.

결정 트리 학습 알고리즘

선형 모델에서는 MSE, MAE 등을 비용 함수(Cost function)로 하여 이를 최소화한 모델을 만드는 것이 목적이었다.
결정 트리에서도 비용 함수 역할을 하는 것이 있는데, 바로 지니 불순도(Gini impurity)와 엔트로피(Entropy)이다.

  • 지니 불순도
    • '불순도'라는 이름 그대로 불순물이 얼마나 섞여 있는지를 나타낸다. 여기서 불순물과 불순물이 아닌 것의 기준은 그곳에 무엇이 더 많이 있는가이다. 즉, 더 많은 것이 주인이고 적은 쪽이 불순물이다.
    • 지니 불순도를 계산하는 식은 다음과 같다.
      • 지니불순도(Gini Impurity or Gini Index):
        $$I_G(p)=\sum_{i=1}^{J} p_i(1-p_i)=1-\sum _{i=1}^{J}{p_i}^2$$
        여기서 p는 주인이 차지한 영역의 비율(달리 말하면 확률)이다.
    • 만약 불순물이 최대, 즉 반반 치킨처럼 반반이라면 지니 불순도는 0.5이다. 반대로 모든 것이 같은, 즉 순수한 후라이드 또는 양념 치킨이라면 지니 불순도는 0이다.
    • 조금 더 구체적인 예를 들어보겠다. 결정 트리의 어느 노드에 와서 보니 빨간 휴지가 50개, 파란 휴지가 30개라고 한다. 이때 이 노드에서의 지니 불순도는
      $$1 - ( (\frac{50}{50+30})^2 + (\frac{30}{50+30})^2 ) = 0.46875$$ 이다.
  • 엔트로피
    • 엔트로피도 지니 불순도처럼 높으면 불순물이 많고 낮으면 적은 것이다.
    • 지니 불순도와의 차이점으로 엔트로피는 최대 1(반반), 최소 0(순수)이다.
    • 정보 획득(Information Gain) : 결정 트리에서 어떤 특성을 사용해 분할을 했을 때 엔트로피가 감소하는 양을 뜻한다. 예시는 아래와 같다.
      자식 노드의 가중 평균 엔트로피 = $\frac{17}{30}*0.787 + \frac{13}{30}*0.391 = 0.615$
      ∴ IG = 부모 엔트로피 - 자식 노드의 가중 평균 엔트로피 = 0.996 - 0.615 = 0.38

특성 중요도

선형 모델에서는 각 특성별로 타겟에 얼마나 영향을 주는지를 회귀 계수(Coefficient)로 표현하였다. 결정 트리에도 이와 비슷한 역할로 특성 중요도(Feature importance)가 있다.

이름 그대로 타겟 값을 구하는 과정에서 각 특성이 얼마나 중요한지를 나타낸 것으로, 구체적으로는 해당 특성이 얼마나 일찍 그리고 자주 트리의 분기에 사용되는지 결정한다.

회귀 계수와 다른 점으로 특성 중요도는 항상 양수값이다.

결정 트리의 특징

  1. 결정 트리는 회귀 문제와 분류 문제 모두에 대하여 사용 가능하다.
  2. 결정 트리는 분류 과정을 직관적으로 확인이 가능하다.
  3. 선형 모델과 달리 비선형, 비단조$^*$$^1$, 특성 상호 작용$^*$$^2$ 특징을 가진 데이터 분석이 가능하다.

결정 트리는 위와 같은 장점들이 있다.
그러나 훈련 데이터의 일반화가 제대로 되지 않으면 너무 복잡한 결정 트리가 되는 문제가 있는데, 한 마디로 과적합(Overfitting) 문제가 있다.

결정 트리 과적합 문제

scikit-learn의 DecisionTreeClassifier 또는 DecisionTreeRegressor를 이용하면 결정 트리 모델을 만들 수 있다. 이 둘에는 결정 트리의 과적합을 줄일 수 있는 파라미터들로 다음과 같은 것들이 있다.

  • max_depth : 결정 트리의 최대 깊이를 제한한다. 만약 제한을 하지 않을 경우 트리는 모든 말단 노드가 순수(불순도가 0)해질 때가지 분기를 만들 것이고, 이는 과적합 문제를 발생시킬 수 있다.
  • min_samples_split : 노드가 분할하기 위해서 필요한 최소한의 표본 데이터 수를 설정한다. 이를 통해 아주 적은 데이터만 있어도 노드가 분할하는 것을 막아 과적합을 줄일 수 있다. 만약 설정하지 않으면 작은 분기가 많이 만들어져 과적합 문제가 발생할 수도 있다.
  • min_samples_leaf : 말단 노드가 되기 위해서 필요한 최소한의 표본 데이터 수를 설정한다. 설정한 숫자보다 적은 데이터 수를 가진 노드가 생성되는 것을 막아 과적합 문제를 줄인다. 이를 설정하지 않으면 아주 적은 데이터로도 말단 노드가 생성되어 과적합 문제가 생길 수 있다.

결정 트리는 어떻게 하는거지?

위에서 언급한 scikit-learn의 DecisionTreeClassifier를 통해서 결정 트리를 만들어보겠다.

1) 데이터 불러오기 및 전처리

import pandas as pd
from sklearn.model_selection import train_test_split

target = 'vacc_h1n1_f'
train = pd.merge(pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train.csv'),
pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train_labels.csv')[target], left_index=True, right_index=True)
test = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/test.csv')
sample_submission = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/submission.csv')
train, val = train_test_split(train, train_size=0.80, test_size=0.20,
stratify=train[target], random_state=2)

train.shape, val.shape, test.shape

((33723, 39), (8431, 39), (28104, 38))

import numpy as np

def  engineer(df):
    """특성을 엔지니어링 하는 함수입니다."""
    # 높은 카디널리티를 가지는 특성을 제거
    selected_cols = df.select_dtypes(include=['number',  'object'])
    labels = selected_cols.nunique()  # 특성별 카디널리티 리스트
    selected_features = labels[labels <= 30].index.tolist()  # 카디널리티가 30보다 작은 특성만 선택합니다.
    df = df[selected_features]

    # 새로운 특성 생성
    behaviorals = [col for col in df.columns if  'behavioral'  in col]
    df['behaviorals'] = df[behaviorals].sum(axis=1)
    dels = [col for col in df.columns if  ('employment'  in col or  'seas'  in col)]
    df.drop(columns=dels, inplace=True)
    return df

train = engineer(train)
val = engineer(val)
test = engineer(test)
features = train.drop(columns=[target]).columns

# 훈련/검증/테스트 데이터를 특성과 타겟으로 분리합니다
X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]
X_test = test[features]

2) 결정 트리 구현

from sklearn.tree import DecisionTreeClassifier  

pipe = make_pipeline(
    OneHotEncoder(use_cat_names=True),
    SimpleImputer(),
    DecisionTreeClassifier(random_state=1, criterion='entropy')
)  

pipe.fit(X_train, y_train)
print('훈련 정확도: ', pipe.score(X_train, y_train))
print('검증 정확도: ', pipe.score(X_val, y_val))

훈련 정확도: 0.9908667674880646
검증 정확도: 0.7572055509429486

y_val.value_counts(normalize=True)

0 0.761001
1 0.238999
Name: vacc_h1n1_f, dtype: float64

∴ 결정 트리의 훈련 정확도는 99가 넘는데 검증 정확도는 타겟의 다수 범주(0) 비율과 거의 똑같다. 즉, 과적합 상태이다.

3) 과적합 해결하기

# max_depth 설정을 통해 과적합 줄이기
pipe = make_pipeline(
    OneHotEncoder(use_cat_names=True),
    SimpleImputer(),
    DecisionTreeClassifier(max_depth=6, random_state=2)
)  

pipe.fit(X_train, y_train)
print('훈련 정확도', pipe.score(X_train, y_train))
print('검증 정확도', pipe.score(X_val, y_val))

훈련 정확도 0.8283367434688491
검증 정확도 0.8269481674771676

# min_samples_leaf 설정을 통해 과적합 줄이기
pipe = make_pipeline(
OneHotEncoder(use_cat_names=True),
SimpleImputer(),
DecisionTreeClassifier(min_samples_leaf=10, random_state=2)
)  

pipe.fit(X_train, y_train)
print('훈련 정확도', pipe.score(X_train, y_train))
print('검증 정확도', pipe.score(X_val, y_val))

훈련 정확도 0.8577528689618361
검증 정확도 0.8029889692800379

∴ 과적합을 줄이기 위한 파라미터를 넣지 않았을 때와 비교하면 훈련 정확도는 낮아졌지만 검증 정확도는 높아졌다. 이전보다 과적합이 줄어들었다.
위 예시에는 담지 않았지만, min_samples_split 또한 비슷한 결과를 얻을 수 있다.

4) 특성 중요도 구하기

# 특성 중요도가 높은 순서대로 그래프에 표시하기
model_dt = pipe.named_steps['decisiontreeclassifier']
enc = pipe.named_steps['onehotencoder']
encoded_columns = enc.transform(X_val).columns

# DecisionTreeClassifier의 feature_importances_ 속성을 통해 특성 중요도를 알 수 있음
importances = pd.Series(model_dt.feature_importances_, encoded_columns)
plt.figure(figsize=(10,30))
importances.sort_values().plot.barh();

지면 관계상 일부 생략

*1 비단조(non-monotonic)란?

'단조(monotonic)'라는 말의 뜻을 알면 비단조는 바로 알 수 있다.
보통 일상 생활에서 '단조(單調)롭다'라는 말에서의 '단조'와 같은 '단조(單調)'이다.

수학적인 의미의 '단조'는 간단하게 말하면 계속해서 증가 혹은 감소하는 상태를 말한다. 중간에 올라가다가 내려가거나, 내려가다가 올라가지 않고 그냥 꾸준히 계속해서 상승 또는 하강하는 것이다.

*2 특성 상호 작용?
특성 상호 작용이란 특성 둘 이상이 만나면 타겟에 각자가 갖고 있던 영향만 끼치는 것이 아니라 여기에 추가적인 영향이 생기는 것으로 이해할 수 있다.
선형 회귀 모델의 경우 이러한 특성 상호 작용이 있으면 모델 성능이 떨어질 수 있으나, 결정 트리는 그렇지 않다.
구체적인 예시를 보겠다.

  • 기본가격 150,000

Location

  • good: +50,000
  • bad: 0

Size

  • big: +100,000
  • small: 0
# 예시 데이터 준비
> cols = ['location','size','price']
# location: 1:good, 0:bad
# size: 1:big, 0:small
# big은 small보다 100,000 비싸고, good은 bad보다 50,000 가격이 더 나갑니다.

features = [[1,  1],
[1,  0],
[0,  1],
[0,  0]]

price = [[300000],
[200000],
[250000],
[150000]]

X_house = pd.DataFrame(columns=cols[:2], data=features)
y_house = pd.DataFrame(columns=[cols[2]], data=price)
# 선형 회귀 모델
from sklearn.linear_model import LinearRegression

linear = LinearRegression()
linear.fit(X_house, y_house)
print('R2: ', linear.score(X_house, y_house))
print('Intercept: ', linear.intercept_[0])
print('Coefficients')
pd.DataFrame(columns=cols[:2], data=linear.coef_)

R2: 1.0
Intercept: 150000.0
Coefficients

  location size
0 50000.0 100000.0
# 회귀 트리 모델
import graphviz
## jupyterlab 사용시: jupyter labextension install @jupyter-widgets/jupyterlab-manager
from ipywidgets import interact
from sklearn.tree import DecisionTreeRegressor, export_graphviz

# 트리구조 그리는 함수
def  show_tree(tree, colnames):
    dot = export_graphviz(tree, feature_names=colnames, filled=True, rounded=True)
    return graphviz.Source(dot)
from sklearn.tree import DecisionTreeRegressor

tree = DecisionTreeRegressor(criterion="mae")
tree.fit(X_house, y_house)
print('R2', tree.score(X_house, y_house))
show_tree(tree, colnames=X_house.columns)

R2 1.0

good and big 인 경우 +100,000 규칙 추가(특성상호작용)

y_house.loc[0,  'price'] = 400000
y_house
  price
0 400000
1 200000
2 250000
3 150000
# 선형 회귀 모델
# 특성 상호 작용이 있는 경우 $R^2$ 점수가 떨어짐(성능 감소)
linear = LinearRegression()
linear.fit(X_house, y_house)
print('R2: ', linear.score(X_house, y_house))
print('Intercept: ', linear.intercept_[0])
print('Coefficients')
pd.DataFrame(columns=cols[:2], data=linear.coef_)

R2: 0.9285714285714286
Intercept: 125000.00000000003
Coefficients

  location size
0 100000.0 150000.0
# 회귀 트리 모델
# 특성 상호 작용이 있어도 트리 모델의 성능은 그대로임
tree = DecisionTreeRegressor(criterion="mae")
tree.fit(X_house, y_house)
print('R2', tree.score(X_house, y_house))
show_tree(tree, colnames=X_house.columns)

R2 1.0


<참고 자료>

'Machine Learning > Trees' 카테고리의 다른 글

[ML] 랜덤 포레스트(Random Forest)  (0) 2021.10.26

회귀 모델 평가 지표(Evaluation Metrics for Regression Models)

회귀 모델 평가 지표란?

Evaluation metrics are a measure of how good a model performs and how well it approximates the relationship
- towards data science

회귀 모델 평가 지표는 이 모델의 성능이 얼마나 좋은지, 모델에 사용된 특성과 타겟의 관계를 얼마나 잘 나타내는지 보여주는 값이다.

회귀 모델의 평가 지표에는 다음과 같은 것들이 있다.

  • MSE(Mean Squared Error, 평균 제곱 오차)
  • RMSE(Root MSE, 평균 제곱근 오차)
  • MAE(Mean Average Error, 평균 절대 오차)
  • $R^2$(R-squared, Coefficient of determination, 결정 계수)

MSE(Mean Squared Error, 평균 제곱 오차)

회귀 분석을 통해 얻은 예측 값과 실제 값의 차이(잔차)의 평균을 나타낸 것이다. ($y$ : 실제 값, $\hat{y}$ : 예측 값)

$$MSE = \frac {1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2$$

예측 값과 실제 값의 차이를 나타낸 것이므로 작으면 작을수록 좋다.

위 식에서 $(y - \hat{y})^2$를 SSE(Sum of Squares Error, 오차 제곱의 합)라고 하는데, 이는 데이터의 개수가 늘어나면 계속해서 오류의 값이 증가하는 문제가 있다. 이 문제를 해결하기 위해 평균으로 만든 것이 MSE이다.

MSE는 값 하나를 통해 모델의 성능을 파악할 수 있어 직관적이다.
동시에 MSE의 단점으로 아래와 같이 몇 가지가 있는데,

  1. 제곱을 했기 때문에 기존 값의 단위와 다른 단위를 갖는다(쉽게 말하면 값이 뻥튀기되었다).
  2. 이상치(Outlier)가 있는 경우 결과가 왜곡될 수 있다(1번과 동일 이유).
  3. 제곱을 하면 결과는 양수가 되기 때문에 예측 결과가 실제 값보다 높은 것인지, 낮은 것인지 알 수 없다.
  4. 값의 스케일에 의존적이다.$^*$$^1$

RMSE(Root MSE, 평균 제곱근 오차)

위에서 설명한 MSE의 값의 루트를 씌운 것이다.
MSE의 단점 중 제곱을 했기 때문에 발생한 단점을 보완하기 위해서 만들어졌다.

$$RMSE = \sqrt{\frac {1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2}$$

각각의 잔차에 루트를 씌우고서 더한 것이 아니라 전부 다 더한 값(MSE)에 루트를 씌웠기 때문에 제곱에 의한 왜곡이 여전히 존재하나, MSE보다는 덜하다.


MAE(Mean Average Error, 평균 절대 오차)

잔차에 절대값을 씌운 후 평균으로 만든 것이다.

$$MSE = \frac {1}{n}\sum_{i=1}^{n}|y_i - \hat{y_i}|$$

MSE나 RMSE와 마찬가지로 값 하나만 보고 모델의 성능을 직관적으로 파악할 수 있다.
또한 각각의 잔차에 단순히 절대값만 씌운 것의 평균이기에 기존 값의 단위를 그대로 유지하며, MSE에 비해 이상치에 의한 왜곡이 덜 하다는 장점이 있다.

하지만 절대값을 씌웠기 때문에 결과 값이 항상 양수이고, 이는 MSE와 마찬가지로 예측 결과가 실제보다 높은지 낮은지 알 수 없다.


$R^2$(R-squared, Coefficient of determination, 결정 계수)

$R^2$(이하 결정 계수)는 모델이 얼마나 예측을 잘 했는지를 0에서 1 사이로 나타낸 값이다. 1에 가까울수록 성능이 좋은 것이다.

$$1 - \frac{\sum_{i=1}^{n}(y_{i} - \hat{y_{i}})^{2}}{\sum_{i=1}^{n}(y_{i} - \bar{y_{i}})^{2}} = 1 - \frac{SSE}{SST} = \frac {SSR}{SST}$$

조금 더 구체적으로 말하자면, 실제 값의 분산과 예측 값의 분산을 비교하여 예측이 얼마나 잘 맞았는지 보는 것이다. 만약 결정 계수의 값이 0.7이라면 이는 모델이 실제 값의 분산을 70% 설명한다는 의미이다.

위의 MSE, RMSE, MAE와 달리 결정 계수는 값의 단위가 원래 무엇이었든 0부터 1 사이의 값으로 표현하기에 스케일에 의존적이지 않다.
또한 값 하나로 모델의 성능을 알아볼 수 있어 직관적이다.

그러나 과적합(Overfitting) 문제에 약하다는 단점이 있다. 특성이 많아질수록, 즉 모델이 복잡해질수록 모델은 훈련 데이터는 잘 맞추지만 테스트는 잘 망치는, 즉 과적합되는 문제가 있다. 결정 계수는 그저 자신의 값을 보여줄 뿐, 모델이 과적합인지 아닌지는 말해주지 않는다.
이러한 과적합 문제를 보완하기 위해서 Adjusted R-squared(수정된 결정 계수)가 있다.

※ 참고
- SSE(Sum of Squares Error, 관측치와 예측치 차이): $\sum_{i=1}^{n}(y_{i} - \hat{y_{i}})^{2}$
- SSR(Sum of Squares due to Regression, 예측치와 평균 차이): $\sum_{i=1}^{n}(\hat{y_{i}} - \bar{y_{i}})^{2}$
- SST(Sum of Squares Total, 관측치와 평균 차이): $\sum_{i=1}^{n}(y_{i} - \bar{y_{i}})^{2}$ , SSE + SSR

*1 스케일에 의존적이라는게 무슨 말?
예를 들어서 삼성전자와 애플의 주식 가격을 예측한다고 해보자.
여기서 한국 주식의 가격은 원화, 미국 주식의 가격은 달러이다(작성 시각 기준 삼성전자 70,200원, 애플 149.28달러).
이 둘에 대한 회귀 모델을 만들었고, MSE 값을 구했더니 둘 다 30이 나왔다고 해보자.
가격의 단위가 판이하게 다름에도 에러 값이 똑같이 나왔다.
그렇다고 해서 이 둘이 동등한 수준의 성능을 보여주는 것은 아니다.

이렇게 예측하고자 하는 값의 단위에 영향을 받는 것을 두고 스케일에 의존적이라고 한다.


<참고 자료>

다중 선형 회귀 모델(Multiple Linear Regression)

※ 단순 선형 회귀를 모른다면? (아래 클릭 시 링크로 이동)
단순 선형 회귀 모델(Simple Linear Regression Model)

다중 선형 회귀가 뭐지?

단순 선형 회귀와 다중 선형 회귀
선형 회귀의 가장 단순한 예제는 한 개의 스칼라 독립 변수 x와 한 개의 스칼라 의존 변수 y의 관계일 것이다. 이를 단순 선형 회귀라 부른다. 여기에서 독립 변수를 여러 개로 확장한 것이 다중 선형 회귀이다.

실세계의 거의 대부분의 문제는 여러 개의 독립 변수를 포함하며, 선형 회귀라 함은 보통 다중 선형 회귀를 일컫는다. 하지만 이러한 경우에도 여전히 응답 변수 y는 한 개의 스칼라 변수이다. 다변량 선형 회귀는 응답 변수 y가 벡터인 경우를 의미한다. 이러한 경우를 일반 선형 회귀라 부른다. 다중 선형 회귀와 다변량 선형 회귀는 다른 의미이므로, 혼동하지 않도록 주의해야 한다.
- 위키백과 (선형 회귀 中)

영향을 받은 결과와 영향을 준 요인의 개수를 '결과 : 요인'이라고 표현하면 단순 선형 회귀는 1:1이고 다중 선형 회귀는 1:N이다.
여기서 이 결과가 종속 변수(또는 타겟)이고, 요인을 독립 변수(특성)라고 한다.

선형 회귀에서의 가정이나 최소제곱법을 이용하여 결과값을 구하는 등의 특징은 단순 선형 회귀와 공유한다.
다만 다중 선형 회귀에서는 특성의 개수가 2개 이상이므로 다중공선성(Multicollinearity)$^*$$^1$을 주의해야 한다.


다중 선형 회귀는 어떻게 하는거지?

기본적인 원리는 단순 선형 회귀와 비슷하다.
다만 차이점이라면 특성의 개수가 2개 이상이므로 회귀 방정식(단순 선형 회귀에서는 직선을 나타내는 식)에 차이가 있다.

단순 선형 회귀에서의 회귀 방정식은 $$y=β_0+β_1x$$ 이고, 다중 선형 회귀에서의 회귀 방정식은 $$y=β_0+β_1x_1+β_2x_2+ ... +β_nx_n$$ 이다.
(자세한 계산 과정은 하단 참고 자료의 링크로)


scikit-learn을 활용한 다중 선형 회귀

  1. 데이터 불러오기 & 훈련/테스트 데이터 나누기
# 예시 데이터 불러오기 (House Sales in King County, USA)
import pandas as pd

df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/kc_house_data/kc_house_data.csv')
df.date = pd.to_datetime(df.date) # 날짜 형식으로 변환

df.head()
  id date price bedrooms bathrooms sqft_living sqft_lot floors waterfront view condition grade sqft_above sqft_basement yr_built yr_renovated zipcode lat long sqft_living15 sqft_lot15
0 7129300520 2014-10-13 221900.0 3 1.00 1180 5650 1.0 0 0 3 7 1180 0 1955 0 98178 47.5112 -122.257 1340 5650
1 6414100192 2014-12-09 538000.0 3 2.25 2570 7242 2.0 0 0 3 7 2170 400 1951 1991 98125 47.7210 -122.319 1690 7639
2 5631500400 2015-02-25 180000.0 2 1.00 770 10000 1.0 0 0 3 6 770 0 1933 0 98028 47.7379 -122.233 2720 8062
3 2487200875 2014-12-09 604000.0 4 3.00 1960 5000 1.0 0 0 5 7 1050 910 1965 0 98136 47.5208 -122.393 1360 5000
4 1954400510 2015-02-18 510000.0 3 2.00 1680 8080 1.0 0 0 3 8 1680 0 1987 0 98074 47.6168 -122.045 1800 7503
# 2015-01-01을 기준으로 훈련/테스트 데이터 분리
train = df[df.date < '2015-01-01']
test = df[df.date >= '2015-01-01']

  1. 타겟 및 기준 모델 설정
# 타겟 설정
target  =  'price'  
y_train  =  train[target]  
y_test  =  test[target]
# price 평균값으로 예측(기준모델)
predict = y_train.mean()
predict

539181.4284152258

# 기준모델로 훈련 에러(MAE) 계산
from sklearn.metrics import mean_absolute_error

y_pred = [predict] * len(y_train)
mae = mean_absolute_error(y_train, y_pred)

print(f'훈련 에러: {mae:.2f}')

훈련 에러: 233570.83

# 테스트 에러(MAE)
y_pred = [predict] * len(y_test)
mae = mean_absolute_error(y_test, y_pred)

print(f'테스트 에러: {mae:.2f}')

테스트 에러: 233990.69


  1. 다중 선형 회귀 모델 만들기
# 다중모델 학습을 위한 특성
features = ['bathrooms', 'sqft_living']
X_train = train[features]
X_test = test[features]
# 모델 fit
from sklearn.linear_model import LinearRegression  

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_train)
mae = mean_absolute_error(y_train, y_pred)

print(f'훈련 에러: {mae:.2f}')

훈련 에러: 170777.34

# 테스트 데이터에 적용
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)

print(f'테스트 에러: {mae:.2f}')

테스트 에러: 179252.53


  1. 절편과 회귀계수 확인
# 절편(intercept)과 계수들(coefficients)
model.intercept_, model.coef_

(-50243.56279640319, array([-5158.92591411, 286.13753555]))
∵ 특성이 2개이기 때문에 회귀 계수도 2개

## 회귀식
b0 = model.intercept_
b1, b2 = model.coef_  

print(f'y = {b0:.0f} + {b1:.0f}x\u2081 + {b2:.0f}x\u2082')

y = -50244 + -5159$x_1$ + 286$x_2$


  1. 훈련/테스트 데이터에 대한 평가 지표 결과 확인
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score  

def  evaluate(features, model, train, test):
    y_train = train[target]
    y_test = test[target]
    X_train = train[features]
    X_test = test[features]
    y_pred_train = model.predict(X_train)
    y_pred_test = model.predict(X_test)  

    mse = mean_squared_error(y_train, y_pred_train)
    rmse = mse ** 0.5
    mae = mean_absolute_error(y_train, y_pred_train)
    r2 = r2_score(y_train, y_pred_train)  

    mse_t = mean_squared_error(y_test, y_pred_test)
    rmse_t = mse_t ** 0.5
    mae_t = mean_absolute_error(y_test, y_pred_test)
    r2_t = r2_score(y_test, y_pred_test)  

    display(pd.DataFrame([['MSE', mse, mse_t],['RMSE', rmse, rmse_t],['MAE', mae, mae_t],['R2', r2, r2_t]], columns=['Metric',  'Score(Training)',  'Score(Test)']))
evaluate(features, model, train, test)
  Metric Score(Training) Score(Test)
0 MSE 6.709905e+10 7.108399e+10
1 RMSE 2.590348e+05 2.666158e+05
2 MAE 1.707773e+05 1.792525e+05
3 R2 5.076086e-01 4.599930e-01

 


∗1 다중공선성(Multicollinearity)이란?
다중공선성(多重共線性)문제(Multicollinearity)는 통계학의 회귀분석에서 독립변수들 간에 강한 상관관계가 나타나는 문제이다. 독립변수들간에 정확한 선형관계가 존재하는 완전공선성의 경우와 독립변수들간에 높은 선형관계가 존재하는 다중공선성으로 구분하기도 한다. 이는 회귀분석의 전제 가정을 위배하는 것이므로 적절한 회귀분석을 위해 해결해야 하는 문제가 된다.
- 위키백과

특성(독립 변수)들끼리 강한 상관 관계가 있는 경우를 의미한다.
만약 이를 배제하지 않고 분석에 그대로 사용하게 되면 결과가 왜곡될 위험이 크다.

<참고 자료>
[통계 이론] 선형 회귀 : 다중 회귀 분석
머신러닝 - 다중선형회귀(Multiple Linear Regression)

+ Recent posts