34.일반선형모델 - 페널티 로지스틱회귀분석.Rmd
0.02MB
34.일반선형모델---페널티-로지스틱회귀분석.html
1.40MB
33.통계데이터분석 - 일반선형모델 - 페널티 로지스틱회귀분석
2022-10-13
https://www.youtube.com/watch?v=LxHMfr120gU&list=PLY0OaF78qqGAxKX91WuRigHpwBU0C2SB_&index=34
- 목표: 페널티 로지스틱회귀분석 이해
1. 페널티 로지스틱회귀분석 개요
- 로지스틱 모델의 다수의 예측변수가 포함되어 있어서 과적합이 우려될 때 페널티회귀분석을 수행하여 예측변수의 개수가 축소된 보다 간명한 모델을 만들 수 있음
- 페널티 회귀분석의 지나치게 많은 예측변수를 갖는 로지스틱회귀모델의 페널티를 부과함
- 따라서 그 결과로 모델의 설명력에 덜 기여하는 예측변수 회귀계수를 0으로 만들거나 혹은 0에 가깝게 축소함
- 예측변수 회귀계수를 0으로 만드는 페널티 회귀분석 기법을 라소회귀분석
- 예측변수 회귀계수를 0에 가깝게 축소하는 페널티 회귀분석 기법을 릿지회귀분석이라고 함
- 라소회귀분석을 적용해 모델의 설명력에 덜 기여하는 예측변수 회귀계수를 0으로 만든다는 것은 결국 그 회귀계수를 갖는 예측변수를 모델에서 제거하는 것이기 때문에 모델의 복잡성이 줄어듦
- 예측정확도가 비슷할 경우 일반적으로 예측변수 개수가 작은 간명한 모델이 바람직함
- 예측변수의 개수가 작다는 것은 모델의 복잡도가 축소된다는 얘기고 그 얘기는 새 데이터에 대한 과적합 위험이 작은 보다 일반화 가능성이 높은 모델을 만들 수가 있다는 뜻이기 때문임
2. 페널티 로지스틱회귀분석 수행절차
- 여기서는 라소회귀분석으로 페널티 로지스틱회귀분석을 수행할 것임
- 라스회귀분석은
- 기여도가 낮은 예측변수의 회귀계수를 0으로 만들어 예측모델에서 제거하고
- 그렇게 하여 예측모델 내에는 통계적으로 유의하면서 높은 설명력을 갖는 예측변수만이 포함됨
- mlbench패키지 PimaIndiansDiabetes2데이터셋으로 라소회귀분석 기반 페널티 로지스틱회귀분석을 수행하고 성능을 평가해 보겠음
2-1. 데이터셋 구조 확인
- PimaIndiansDiabetes2데이터셋 구조
- PimaIndiansDiabetes2데이터셋은 환자의 당뇨병 여부와 관련된 임상데이터 저장
- 모두 768 개의 관측 값을 포함하고 9개 변수로 구성됨
- 9개 변수 중 마지막 diabetes변수는
- 예측모델에 의해 예측하고자하는 결과변수로써
- 당뇨병 여부를 나타내는 두 개의 범주를 갖고 있음
- 앞의 8개 변수는 예측변수임. 이 8개 예측변수로 당뇨병 여부를 예측하는 예측모델을 생성할 것임
library(mlbench)
data("PimaIndiansDiabetes2")
str(PimaIndiansDiabetes2)
## 'data.frame': 768 obs. of 9 variables:
## $ pregnant: num 6 1 8 1 0 5 3 10 2 8 ...
## $ glucose : num 148 85 183 89 137 116 78 115 197 125 ...
## $ pressure: num 72 66 64 66 40 74 50 NA 70 96 ...
## $ triceps : num 35 29 NA 23 35 NA 32 NA 45 NA ...
## $ insulin : num NA NA NA 94 168 NA 88 NA 543 NA ...
## $ mass : num 33.6 26.6 23.3 28.1 43.1 25.6 31 35.3 30.5 NA ...
## $ pedigree: num 0.627 0.351 0.672 0.167 2.288 ...
## $ age : num 50 31 32 21 33 30 26 29 53 54 ...
## $ diabetes: Factor w/ 2 levels "neg","pos": 2 1 2 1 2 1 2 1 2 2 ...
2-2. 데이터 전처리(결측값 제거) - na.omit()
- PimaIndiansDiabetes2데이터셋은 많은 결측값이 포함돼 있어, 이를 제거하는 전처리 작업이 필요함
- 결측값을 제외하고 PimaIndiansDiabetes3라는 새데이터셋으로 저장함
PimaIndiansDiabetes3 <- na.omit(PimaIndiansDiabetes2)
- 이 전체 데이터를 예측모델 생성을 위한 훈련데이터와 검증을 위한 테스트데이터로 분할할 것임
2-3. 데이터 전처리(훈련/테스트데이터분할) - createDataPartition()
- 여기서는 caret패키지 createDataPartition()로 훈련데이터와 테스트데이터를 7대 3의 비율로 분할함
- createDataPartition() 인수설명
- y: 데이터셋에 포함되어 있는 결과변수를 지정
- p: 훈련데이터의 비율 지정. 훈련/테스트데이터 비율을 7대 3으로 하기 때문에 0.7을 지정함
- list: 훈련데이터에 대응되는 데이터인덱스가 리스트가 아닌 행렬 구조로 출력되도록 FALSE 지정
library(caret)
set.seed(123)
train <- createDataPartition(y=PimaIndiansDiabetes3$diabetes,
p=0.7, list=FALSE)
head(train, 10)
## Resample1
## [1,] 1
## [2,] 2
## [3,] 3
## [4,] 4
## [5,] 5
## [6,] 6
## [7,] 7
## [8,] 9
## [9,] 11
## [10,] 13
- 이 인덱스로 훈련/테스트데이터로 분화를 수행함
- 훈련데이터를 제외한 나머지 데이터는 테스트데이터로 저장함
diabetes.train <- PimaIndiansDiabetes3[train,]
diabetes.test <- PimaIndiansDiabetes3[-train,]
- 전체 데이터셋으로부터 훈련/테스트데이터가 적절히 잘 분할됐는지 평가하기 위해 두 데이터셋에 포함된 당뇨병 환자 비율을 계산해 보겠음
- 먼저 훈련데이터에 포함된 당뇨병 환자수와 비율 계산함
table(diabetes.train$diabetes)
##
## neg pos
## 184 91
sum(table(diabetes.train$diabetes))
## [1] 275
prop.table(table(diabetes.train$diabetes))
##
## neg pos
## 0.6690909 0.3309091
- 결과해석
- 훈련데이터에는 모두 275개 관측값이 있고 그중 당뇨병 환자는 91명임
- 비율을 계산하면 당뇨병 환자비율은 약 33% 정도됨
- 테스트데이터에 대해서도 계산을 해보겠음
table(diabetes.test$diabetes)
##
## neg pos
## 78 39
sum(table(diabetes.test$diabetes))
## [1] 117
prop.table(table(diabetes.test$diabetes))
##
## neg pos
## 0.6666667 0.3333333
- 결과해석
- 테스트 데이터에는 117개 관측값이 있고 그중 당뇨병 환자는 39명임
- 비율로는 약 33% 정도임
- 훈련 데이터와 테스트데이터 모두 당뇨병 환자를 33%를 포함하고 있음
- 훈련 데이터와 테스트 데이터에 포함되어 있는 결과 변수의 당뇨병 환자 비율이 모두 동일하기 때문에 전체 데이터셋으로부터 “모델생성을 위한 훈련데이터와 테스트를 위한 테스트데이터가 잘 분할됐다”고 판단할 수 있음
- createDataPartition()이 이처럼 훈련/테스트데이터셋에 포함된 당뇨병 환자 비율이 동일하도록 작업함
- createDataPartition()는 전체 데이터셋을 훈련/테스트데이터셋으로 분할할 때 전체 데이터셋에 포함된 결과변수의 범주비율이 훈련/테스트 데이터셋에서도 동일하게 유지되도록 생성함
- 그래서 훈련/테스트데이터셋에 포함된 당뇨병 환자 비율 33%는 전체 데이터셋의 당뇨병 환자 비율이기도 함
2-4. 예측변수 행렬 / 결과변수 벡터 생성 및 교차검증 객체생성 - model.matrix(), cv.glmnet()
- 훈련/테스트데이터셋이 준비됐으므로 페널티 회귀분석으로 예측모델 생성 후 모델성능을 평가할 것임
- 페널티 회귀분석에서는 예측오차를 최소화하는 최적의 \(\lambda\)를 먼저 결정해야 함. \(\lambda\)는 페널티 크기를 지정해 줌
- glmnet패키지 cv.glmnet() 교차검증을 통해 최적의 \(\lambda\)를 산출함
- cv.glmnet() 인수설명
- 1st: 예측변수 행렬 지정
- 2nd: 결과변수 벡터 지정
- family: 결과변수 확률분포 지정. 여기서는 현재 현재 이항 로지스틱회귀분석을 수행하기 때문에 “binomial” 지정
- \(\alpha\): 라소회귀분석을 바탕으로 한 페널티 회귀분석을 수행할 예정이기 때문에 1을 지정함. 0을 지정하면 릿지회귀분석을 수행함
- cv.glmnet()은 포뮬러 형식의 모델 설정을 지원하지않아 예측변수/결과변수를 각각 별도 지정해야 함
- 예측변수는 행렬 형식, 결과변수는 벡터 형식으로 지정돼야 함
- 예측변수로 숫자만 취할 수 있어 데이터셋에 범주형 변수가 있으면 사전에 더미변수로 변환해야 함
- model.matrix()로 이러한 데이터 전처리 작업을 쉽게 수행할 수 있음
- model.matrix() 인수설명
- 1st: 포뮬러 형식으로 결과변수와 예측변수 간 관계를 지정함
- 2nd: 사용할 데이터셋을 지정함
- model.matrix()는
- 모델에 투입할 예측변수 행렬을 생성하고
- 범주형 변수를 더미변수로 자동으로 변환해 줌
x <- model.matrix(diabetes ~ ., diabetes.train)
head(x)
## (Intercept) pregnant glucose pressure triceps insulin mass pedigree age
## 4 1 1 89 66 23 94 28.1 0.167 21
## 5 1 0 137 40 35 168 43.1 2.288 33
## 7 1 3 78 50 32 88 31.0 0.248 26
## 9 1 2 197 70 45 543 30.5 0.158 53
## 14 1 1 189 60 23 846 30.1 0.398 59
## 15 1 5 166 72 19 175 25.8 0.587 51
- 첫 번째 열이 불필요하여 첫 번째 열(Intercept)을 제거하는 추가작업을 수행하겠음
- 결과변수 벡터를 별도로 생성해 y변수에 저장
- 결과변수는 범주형 변수이기 때문에 숫자 형식으로 변환함
- 사건발생과 미발생을 1과 0으로 코딩되도록 범주형 변수를 변환해 y 변수에 저장함
x <- model.matrix(diabetes ~ ., diabetes.train)[,-1]
y <- ifelse(diabetes.train$diabetes=="pos", 1, 0)
- 이렇게 생성한 예측변수 행렬과 결과변수 벡터를 cv.glmnet()에 x인수와 y인수로 지정
library(glmnet)
set.seed(123)
diabetes.cv <- cv.glmnet(x=x, y=y, family="binomial")
2-5. 교차검정객체 원소(lambda.min 및 lambda.1se)의 \(\lambda\)값 및 회귀계수 확인
- 이렇게 생성한 교차검정 객체에는 많은 정보가 포함돼 있음. 그 중 지금 찾고자 하는 예측오차를 가장 작게 만드는 \(\lambda\)는 교차검정 객체의 lambda.min원소에서 확인할 수 있음
diabetes.cv$lambda.min
## [1] 0.01087884
- 그런데 페널티 회귀분석을 수행하는 중요한 목표 중 하나는 일정 수준의 예측정확도를 보장하면서 가능하면 복잡하지 않은 간명한 모델을 구축하는 것임
- cv.glmnet()는 이를 위해 교차검정 객체의 lambda.1se원소에 또 다른 \(\lambda\)를 제공함
diabetes.cv$lambda.1se
## [1] 0.05805712
- 교차검정 객체의 lambda.1se원소에 제시된 \(\lambda\)는 최소예측오차 한계의 표준편차 범위 내의 정확도를 보이는 모델 중 가장 예측변수 개수가 작은, 간명한 모델을 생성함
- 따라서 lambda.1se의 \(\lambda\)를 사용하면 어느 정도 예측정확도를 희생할 지 모르지만 일반화 가능성은 높아짐. 즉, 과적합의 위험은 줄어든 모델을 생성할 수 있음
- lambda.min의 \(\lambda\)와 lambda.1se의 \(\lambda\)를 사용하여 생성한 회귀모델의 회귀계수 추출
coef(diabetes.cv, diabetes.cv$lambda.min)
## 9 x 1 sparse Matrix of class "dgCMatrix"
## s1
## (Intercept) -9.23133474
## pregnant 0.01006916
## glucose 0.03446189
## pressure .
## triceps 0.02678380
## insulin 0.00147175
## mass 0.04309304
## pedigree 1.14386810
## age 0.03039017
coef(diabetes.cv, diabetes.cv$lambda.1se)
## 9 x 1 sparse Matrix of class "dgCMatrix"
## s1
## (Intercept) -5.6548824026
## pregnant .
## glucose 0.0276723503
## pressure .
## triceps 0.0137868666
## insulin 0.0001604243
## mass 0.0120567460
## pedigree 0.3234881011
## age 0.0137023590
- 비교결과
- lambda.min의 \(\lambda\)를 사용한 경우, 7개의 예측변수를 갖는 회귀모델이 구축됐음
- lambda.1se의 \(\lambda\)를 사용한 경우, 그보다 작은 6개의 예측변수를 갖는 회귀모델이 생성돼 모델의 복잡성이 다소 줄어들었음
2-6. 교차검정객체 원소(lambda.min 및 lambda.1se)의 \(\lambda\)값에 따른 예측모델 생성
2-6-1. 예측오차를 최소화한 \(\lambda\)를 사용한 모델(lambda.min 사용모델)의 예측정확도 계산
- glmnet() 인수
- 예측모델은 glmnet()으로 생성. 조금 전 사용한 cv.glmnet()와 대부분의 인수를 공유함
- x, y: 각각 예측변수 행렬과 결과변수 벡터를 지정함
- family: 결과변수에는 확률분포인 binomial 지정
- alpha: 1을 지정해 라소회귀분석 수행
- lambda: 교차검증 객체에서 추출한 예측오차를 최소화하는 \(\lambda\)인 lambda.min원소에 있는 값을 지정
diabetes.gnet1 <- glmnet(x=x, y=y,
family="binomial",
alpha=1,
lambda=diabetes.cv$lambda.min)
- 이어서 model.matrix()로 테스트 데이터셋에 포함된 예측변수 행렬 생성
diabetes.test.x <- model.matrix(diabetes ~ ., diabetes.test)[,-1]
- 이어서 predict()로 테스트 데이터에 대한 예측확률 계산
- predict() 인수설명
- 1st: 예측모델을 지정함
- newx: 테스트 데이터로부터 생성한 예측변수 행렬을 지정함
- type: response를 지정해 확률로 결과값이 출력되도록 함
diabetes.pred1 <- predict(diabetes.gnet1,
newx=diabetes.test.x,
type="response")
- 이어서 예측확률 0.5를 기준으로 당뇨병 여부를 판정함
- 예측활률이 0.5점 보다 크면 당뇨병 환자로, 그렇지 않으면 정상으로 판정함
diabetes.pred1 <- ifelse(diabetes.pred1 > 0.5, "pos", "neg")
- table()로 실제 판정결과와 예측모델에 의한 예측결과 비교
table(diabetes.test$diabetes, diabetes.pred1,
dnn=c("Actual", "Predicted"))
## Predicted
## Actual neg pos
## neg 69 9
## pos 20 19
- 결과해석: 대각선에 있는 수치는 실제 판정결과와 예측모델의 예측결과가 일치하는 케이스 갯수임
- 예측정확도 계산
mean(diabetes.test$diabetes==diabetes.pred1)
## [1] 0.7521368
- 결과해석: 예측 정확도는 75.2%로 선출되었음
2-6-2. 간명도를 높인 모델(lambda.1se 사용모델)의 예측정확도 계산
- 앞서와 동일한 방식으로 계산하고 \(\lambda\)값으로, lambda.min이 아닌 lambda.1se에 포함된 \(\lambda\)값 지정
diabetes.gnet2 <- glmnet(x=x, y=y,
family="binomial",
alpha=1,
lambda=diabetes.cv$lambda.1se)
# 이어서 model.matrix()로 테스트 데이터셋에 포함된 예측변수 행렬 생성
diabetes.test.x <- model.matrix(diabetes ~ ., diabetes.test)[,-1]
# 이어서 predict()로 테스트 데이터에 대한 예측확률 계산
diabetes.pred2 <- predict(diabetes.gnet2,
newx=diabetes.test.x,
type="response")
# 이어서 예측확률 0.5를 기준으로 당뇨병 여부를 판정함
diabetes.pred2 <- ifelse(diabetes.pred2 > 0.5, "pos", "neg")
# table()로 실제 판정결과와 예측모델에 의한 예측결과 비교
table(diabetes.test$diabetes, diabetes.pred2,
dnn=c("Actual", "Predicted"))
## Predicted
## Actual neg pos
## neg 71 7
## pos 22 17
# 예측정확도 계산
mean(diabetes.test$diabetes==diabetes.pred2) # 예측 정확도는 75.2%로 선출되었음
## [1] 0.7521368
- 결과해석
- 간명도를 높인 예측모델의 경우 예측정확도는 75.2%로 계산됨
- 우연히도 앞서 예측오차를 최소화하는 \(\lambda\)를 이용한 예측정확도와 동일한 값이 산출됨
2-7. 모든 예측변수가 포함된 일반적인 이항 로지스틱회귀분석 수행 후 앞선 페널티 회귀분석 결과와 비교
- glm()로 예측모델 생성
diabetes.logit <- glm(diabetes ~ .,
data=diabetes.train,
family=binomial(link="logit"))
- predict()로 예측확률 계산
diabetes.logit.pred <- predict(diabetes.logit,
newdata=diabetes.test,
type="response")
- 예측확률을 당뇨병 환자 여부를 나타내는 두 개의 범주로 변환
diabetes.logit.pred <- ifelse(diabetes.logit.pred > 0.5, "pos", "neg")
- table()로 혼동행렬 생성
table(diabetes.test$diabetes, diabetes.logit.pred,
dnn=c("Actual", "Predicted"))
## Predicted
## Actual neg pos
## neg 66 12
## pos 20 19
- 끝으로 예측정확도 계산
mean(diabetes.test$diabetes==diabetes.logit.pred)
## [1] 0.7264957
- 결과해석
- 예측정확도는 72.6%로 산출되었음
- 예측모델의 모든 예측변수를 투입했음에도 불구하고 앞서 생성한 2개의 페널티 로지스틱회귀모델의 예측정확도에 비해서 더 낮게 계산됨
- 따라서 여기에서는
- 모델이 간명도 관점에서 뿐만 아니라
- 모델의 예측정확도 관점에서도
- 일반적인 이항 로지스틱회귀분석을 이용한 예측모델보다 페널티 회귀분석을 이용한 예측모델이 더 우수하다고 판단내릴 수 있음
- 앞서 생성한 두 개의 페널티 로지스틱회귀모델의 예측정확도가 모두 동일했음
- 따라서 투입된 예측변수의 개수를 고려할 때 lambda.1se에 포함된 \(\lambda\)를 이용한 페널티 로지스틱회귀모델이 3개의 모델을 가운데는 일반화 관점에서 가장 우수한 모델이라고 얘기할 수 있음