1 GSS TV 시청시간

General Social Survey는 1972년 이래로 미국인 생각하는 다양한 사회이슈에 대한 설문결과 조사를 제공하고 있다. 또한, GSS 데이터는 openCPU HTTP API 사례로 사용되고 있다.

목적은 TV 시청시간을 인구통계학적인 변수를 사용해서 예측하는데 이를 RESTful API로 제공하는 것을 목표로 해 보자.

\[\text{TV 시청시간} = f(\text{age, marital, sex, income}) + \epsilon\]

즉, 나이(age), 결혼상태(marital), 성별(sex), 가계소득(income)을 바탕으로 평균 TV시청시간을 예측하여 이를 RESTful API 서비스 형태로 제공하는 것이다. 각 변수에 대한 자세한 사항은 GSS Data Explorer를 참조한다.

“rwebapps/tvscore” RESTful API 적용사례는 openCPU 팩키지를 사용한 것으로 필요한 사용하는 것도 좋을 듯하다. 다만, openCPU는 과학기술 분야에 방점을 두고 있다. Deploying a scoring engine for predictive analytics with OpenCPU June 23, 2014 웹페이지에서 원본 데이터를 다운로드 받을 수 있다.

TV 시청시간 Restful API

  1. 훈련데이터를 대상으로 탐색적 데이터분석을 진행한다.
  2. caret을 기반으로 다양한 예측모형을 넣어 최적의 예측모형을 구축한다.
  3. 최종 선택한 예측모형을 배포한다.
  4. 선택된 예측모형에 기반하여 예측할 데이터를 준비한다.
  5. Restful API 서버에 모형을 배포한다.
  6. TV 시청시간 예측 서비스를 제공한다.

2 RStudio 프로젝트 구조

RStudio 프로젝트를 하나 생성하고 나서… gss_tvhours 디렉토리를 생성하고 RESTful API 서비스 개발에 필요한 모든 파일을 한 곳에 몰아 놓는다.

\rproject
 +--\code
    | predictive-model.R
 +--\data
    | GSS2012.sav
 +--\gss_tvhours
    | serve-model.R
    | glm_m.rds
    | server.R

3 TV 시청시간 예측 서비스

3.1 caret 예측모형

TV 시청시간 예측 문제는 예측변수가 연속형으로 회귀모형으로 간주할 수 있다. 그리고 GSS2012.sav 데이터의 입력변수가 많기 때문에 변수 선택과정을 건너뛰고, 앞서 선택한 4가지 변수만 예측모형에 개발에 넣어 가장 성능이 좋은 즉, RMSE가 적은 모형을 선택한다. 그리고 이 모형을 TV 시청시간 예측으로 배포한다.

일반화선형모형, Random Forest, CART, xgBoost 모형 아키텍처를 놓고 봤을 때 일반화선형모형이 나름 좋은 성능을 보이고 단순한 모형이라 이를 TV 시청시간 예측모형으로 선정한다.

# 0. 환경설정 -----
library(tidyverse)
library(foreign)
library(caret)
library(yardstick)

# 1. 데이터 -----
## 1.1. 데이터 가져오기 -----
gss_dat <- read.spss("data/GSS2012.sav", to.data.frame=TRUE, use.value.labels = FALSE) %>% 
    as_tibble()

## 1.2. 데이터 정제 -----
gss_df <- gss_dat %>% 
    select(tvhours, age, marital, sex, income) %>% 
    mutate(marital = as.factor(marital),
           sex = as.factor(sex)) %>% 
    filter(complete.cases(.))

## 1.3. 데이터 분할 -----
in_train <- createDataPartition(gss_df$tvhours, p = c(0.7, 0.3), list = FALSE)

training <- gss_df[in_train, ]
testing <- gss_df[-in_train, ]

# 2. 예측모형 -----
## 2.1. 교차타당도 제어조건 설정 -----------
ml_control <- trainControl(method = "repeatedcv", 
                           number = 5, 
                           repeats = 1,
                           verboseIter=FALSE)

## 2.2. 예측모형 아키텍처 --------------------------------
### 2.2.1. GLM --------------------------------
glm_m <- train(tvhours ~ .,
                data=training, 
                method = "glm",
                trControl = ml_control)

### 2.2.2. CART --------------------------------
cart_m <- train(tvhours ~ .,
               data=training, 
               method = "rpart",
               trControl = ml_control)

### 2.2.3. RF --------------------------------
rf_m <- train(tvhours ~ .,
              data=training, 
              method = "rf",
              trControl = ml_control)

### 2.2.4. xgBoost --------------------------------
xgboost_m <- train(tvhours ~ .,
              data=training, 
              method = "xgbLinear",
              trControl = ml_control)

## 2.3. 모형 아키텍처 선정 ----- 
model_arch <- testing %>%
    mutate(GLM = predict(glm_m, testing),
           CART = predict(cart_m, testing),
           RF = predict(rf_m, testing),
           XGB = predict(xgboost_m, testing))
    
glm_metric_df      <- metrics(model_arch, truth = tvhours, estimate = GLM)
cart_metric_df     <- metrics(model_arch, truth = tvhours, estimate = CART)
rf_metric_df       <- metrics(model_arch, truth = tvhours, estimate = RF)
xgboost_metric_df  <- metrics(model_arch, truth = tvhours, estimate = XGB)


bind_rows(glm_metric_df, cart_metric_df) %>% 
    bind_rows(rf_metric_df) %>% 
    bind_rows(xgboost_metric_df) %>% 
    DT::datatable() %>% 
    DT::formatRound(c("rmse", "rsq"), digits=2)
## 2.4. 모형 성능 ----- 
metrics(model_arch, truth = tvhours, estimate = GLM)
# A tibble: 1 x 2
   rmse    rsq
  <dbl>  <dbl>
1  2.56 0.0428
# 3. 모형 배포 ----- 
# glm_m %>% write_rds("gss_model/glm_m.rds")

3.2 RESTful API 서비스 개발

일반화선형모형으로 TV 시청시간 예측모형으로 선정했다면 이를 RESTful API 서비스로 변환시킨다. 이를 위해서 health_check 서비스로 RESTful API가 정상 동작이 되는지 확인하고 나서, /tvhours를 통해 서비스를 제공한다. 웹서비스를 통해 입력받는 데이터를 R 데이터프레임으로 변환시키고 나서, predict() 함수로 TV 시청시간을 예측하고 이를 반환한다.

# serve-model.R
# 0. 환경설정 -----
library(plumber)

# 1. 모형 불러오기 ----- 
glm_m <- read_rds("gss_tvhours/glm_m.rds")

#* @get /healthcheck
health_check <- function() {
    result <- data.frame(
        "input" = "",
        "status" = 200,
        "model_version" = "GSS TV Hours"
    )
    
    return(result)
}


#* @post /tvhours
#* @get /tvhours
predict_tvhours <- function(age=NA, marital=NULL, sex=NULL, income=NULL) {
    # 1. 데이터 자료형 변환
    age <- as.integer(age)
    marital <- as.factor(marital)
    sex <- as.factor(sex)
    income <- as.integer(income)
    
    # 2. 데이터프레임 변환    
    payload <- data.frame(age = age,
                          marital = marital,
                          sex = sex,
                          income = income)

    tvhours_prediction <- predict(glm_m, payload)
    return(tvhours_prediction)
}

3.3 TV 시청시간 예측 서비스

다음 코드로 RStudio에서 서비스를 실행시키면 TV 시청시간 서비스가 구동하게 되는데 웹브라우저 주소창에 http://localhost:8000/tvhours?age=36&marital=5&sex=2&income=12을 입력하거나 Postman, curl을 통해 TV 시청시간 결과값을 반환 받을 수 있다.

# server.R
library(plumber)

serve_model <- plumb("gss_tvhours/serve-model.R")
serve_model$run(port = 8000)

GSS TV 시청시간

4 TV 시청시간 AWS 클라우드 서비스 1

TV 시청시간 예측 서비스를 AWS EC2위에 배포(srv/shiny-server/gss-tvhours)하고 나서 Rscript server.R을 실행시키게 되면 plumber Restful API 서비스가 올라가기는 하지만 제대로 이용할 수 없다.

$ Rscript server.R

4.1 문제확인

MobaXterm: Enhanced terminal for Windows with X11 server, tabbed SSH client, network tools and much more을 다운로드 받아 설치하게 되면 telnet 명령어를 던질 수 있다. 이를 통해서 접속이 되지 않음이 확인된다.

[victor-PC1] $ telnet ec2-1xx-xxx-xxx-xx.ap-northeast-2.compute.amazonaws.com 8000
Trying 1x.xxx.xx.xxx...
telnet: Unable to connect to remote host: Connection refused

sudo iptables -L, netstat -an | grep 8000 명령얼르 통해서 8000 포트를 확인한다. 8000 포드는 loopback 인터페이스에 대응되는 것이 확인되어 이를 plumber 코드에 반영하면 된다.

ubuntu@ip-xxx-xx-xx-7:~$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
ubuntu@ip-xxx-xx-xx-7:~$ netstat -an | grep 8000
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN

4.2 문제해결

serve_model$run(host = "0.0.0.0", port = 8000)host = "0.0.0.0"를 반영시키면 문제를 해결할 수 있다.

# server.R
library(plumber)

serve_model <- plumb("gss_tvhours/serve-model.R")
serve_model$run(host = "0.0.0.0", port = 8000)

8000 포트에 netstat -an | grep 8000 명령어를 통해 정상적으로 대응된 모습은 다음과 같다.

ubuntu@ip-xxx-xx-xx-7:~$ netstat -an | grep 8000
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN

http://http://ec2-xx-xxx-xx-xxx.ap-northeast-2.compute.amazonaws.com:8000/tvhours?age=36&marital=5&sex=2&income=12 와 같은 방식으로 호출을 하게 되면 TV 시청시간에 대한 응답을 얻을 수 있다.

4.3 배포

serve_pm.sh 쉘스크립트 파일을 다음과 같이 작성하여 ubuntu@ip-xxx-xx-xx-7:/srv/shiny-server/gss_tvhours# ./serve_pm.sh & 실행시키면 배포가 된다.

## 파일명: serve_pm.sh
#!/usr/bin/env Rscript

library(plumber)
serve_model <- plumb("gss_tvhours/serve-model.R")
serve_model$run(host="0.0.0.0", port = 8000)