Info) 아래 포스팅은 LGAimers에서 제공하는 AI 전문가 과정 중 이화여자대학교 강제원 교수님의 [지도학습(분류/회귀)] 강의에 대한 요약과 생각들을 정리한 것입니다. 정리된 내용들은 강의를 토대로 작성되었으나 수강자인 저의 이해도에 따라 일부 틀린 부분이 있을 수 있다는 점 알려드립니다. 잘못된 점이 발견될 시에는 즉각 수정하도록 하겠습니다. 또한, 강의의 모든 내용을 정리하기는 어렵기 때문에 인상깊었던 내용을 중심으로 정리하였습니다. 따라서, 설명의 맥락이 끊길 수도 있다는 점 양해 부탁드립니다. 

0. Introduction

이번 part2는 linear regression에 대한 강의입니다. 선형회귀(linear regression)는 지도학습의 가장 기초적인 알고리즘이라 할 수 있습니다. 그래서 그 이론적 배경과 사용되는 concept이 다른 복잡한 모델들에 비해서 그나마 저에게 쉽게 다가왔습니다. 강의는 linear regression의 바탕이 되는 아이디어에 대해 짧게 설명하고 여러(?) 비용함수를 통해 linear regression을 optimization하는 과정을 중점적으로 다루었습니다. mean squared error와 gradient descent에 대해서는 기존에 linear regression을 공부하며 익히 들어보았지만 정확히 그 logic에 대해서는 잘 알지 못했던 터라 이번 강의가 굉장히 흥미로웠습니다. 확실히 머신러닝 분야는 어느 하나 허투루 넘길 수 있는 부분이 하나도 없는 것 같습니다. 앞으로 공부해야 될 것이 산더미인 것 같아 슬프기도 하지만 한편으로는 새로운 호기심을 끊임없이 자극하는 것 같아 설레기도 합니다.

1. Linear Model

linear regression의 기본적인 틀이라고 할 수 있는 linear model은 입력 feature와 모델 파라미터의 선형결합(linear combination)으로 이루어져 있습니다. 아래 수식의 가운데를 보면 theta(*기호를 어떻게 넣는지 몰라 그냥 말로 풀어 쓰겠습니다*)값과 x값의 곱의 합으로 구성되어 있는데 앞에 있는 theta는 모델이 최적화되는 과정에서 계속 바뀌게 되는 'model parameter'라고 부릅니다. 때로는 각 x값이 1증가하였을 때 output이 얼마나 많이 변하는지를 보여주기도 하기 때문에 'weight(가중치)'라고 부르기도 합니다. 일반적으로 linear model은 입력변수인 x에 대해 선형의 모습을 모이지만 반드시 그럴 필요는 없습니다. 아래 2번째 식처럼 x에 대해 커널 함수를 적용할 경우 입력변수는 비선형으로 바뀌게 되지만 매개변수인 theta에 대해서는 선형이 유지됩니다. 즉, h(x)의 linear model은 theta에 대해서는 선형이지만 x에 대해서는 그렇지 않다고 할 수 있습니다. 정확하게 기억이 나지는 않지만 BMI와 체지방률 사이의 선형회귀 모델이 아래 2번째 식과 같이 비선형적인 구조를 가진 것으로 알고 있습니다. 

출처: 강제원 교수님 강의자료

2. Cost function - Minimizing MSE

주어진 입력과 출력을 통해 추론한 linear model이 얼마의 오차를 가지며 데이터에 근접하게 fit되었는지 확인하기 위해 비용 함수(cost function)를 사용할 수 있습니다. 모델이 얼마나 좋은 성능(good)을 보이고 있는지 확인할 수 있는 지표라고 할 수 있습니다. 선형 회귀에서는 평균제곱오차(mean-squared-error)를 cost function으로 사용하고 있습니다. 여기서 잠깐 헷갈리는 부분 하나만 정리하고 가겠습니다. cost function과 비슷하게 사용되는 용어로서 손실 함수(loss function)가 있습니다. 둘이 의미상으로 비슷하여 혼용되어 사용되는 경우가 많은데 정확하게는 loss function은 각각의 값들이 가지는 오차를 말하고 이 오차들이 모여 이루어진 오차들의 합을 cost function이라고 구분할 수 있습니다. 제가 헷갈려서 적어놓고 가는 것이니 그냥 넘어가셔도 됩니다. 다시 본론으로 돌아와 mean-squared-error은 아래 식과 같이 정리할 수 있습니다. 왼쪽 식에 써져있는 것처럼 mse는 말그대로 오차의 제곱의 평균입니다. 오른쪽 식을 보게 되면 오차는 실제값에서 예측값을 뺀 것이라 할 수 있는데 모델이 좋은 성능을 보여 주어진 데이터에 잘 적합되었다면 이 오차가 0에 가까워질 것입니다. 그렇게 되면 cost function 또한 0에 가까워진다고 할 수 있습니다. 즉, 우리의 목표는 cost function인 mse를 최소화하는 것으로서 이를 충족하는 모델 파라미터 theta 값들을 찾아내는 것입니다. 

출처: https://suboptimal.wiki/explanation/mse/

3. Optimization - Normal Equation

우리의 목표는 이 녀석을 최소화하는 theta(여기서는 w로 표현되어 있는값)를 찾아내야 합니다.

그렇다면 이제는 어떻게 mse값을 최소화할 수 있는 최적의 theta들을 찾아낼수 있을지 고민해봐야 합니다. 이를 위해 최소제곱법(least squared method)을 활용할 수 있습니다. 이때 문제를 해결하는 방적식을  nomal equation이라고 하며 그 유도 과정은 복잡하여 여기서는 생략하도록 하겠습니다. 어쨌든 어쩌어찌 방정식을 잘 풀어내면 최적 파라미터 theta에 대한 식을 만들어 낼 수 있습니다. 아래와 같이 X의 의사역행렬(pseudoinverse)값에 y를 곱한 값으로 정의될 수 있습니다. 

출처: http://mlwiki.org/index.php/Normal_Equation

하지만 이러한 normal equation 방법으로 최적의 파라미터값을 찾아내는 것은 현실적으로 쉽지 않을 때가 많습니다. 가령, 데이터의 sample 숫자가 늘어나는 경우 X의 inverse값을 계산하는데 복잡하여 너무 오랜 시간이 걸리게 됩니다. 또한, X의 역행렬이 존재하지 않을 경우도 있습니다. 이러한 문제를 해결하기 위해 경사하강법(gradient descent)을 활용할 수 있습니다.

개인적으로 gradient descent는 머신러닝/딥러닝 알고리즘들의 기초가 되는 핵심적인 방법론이지 않을까 생각됩니다. gradient descent의 이론적 아이디어에 대해 간단히 말씀드리면 iterative(반복적)하게 최적의 파라미터 theta를 찾아가는 과정이라고 할 수 있을 것 같습니다. 

4. Optimization - Gradient Descent

경사하강법(gradient descent)에서의 descent는 '함수의 변화도' 또는 '기울기'입니다. 즉, 함수를 미분하는 과정에서 얻는 값으로 해당 함수의 변화하는 정도를 나타내는 값이라 할 수 있습니다. 바로 이 descent가 경사하강법의 핵심적인 아이디어입니다. 함수의 기울기는 함수의 최대값 또는 최소값에서 0이 됩니다. 이 원리를 이용하여 mse가 최소가 되는 지점을 찾아낼 수 있는 것입니다. 정리하자면 기울기가 0이 되는 지점 혹은 함수의 변화가 큰 방향으로 이동하다 더 이상 변화가 이루어지지 않는 지점까지 iterative(반복적)하게 이동하는 것이 gradient descent라고 할 수 있습니다. 아래 왼쪽 그림은 제 설명을 잘 표현해준 그림입니다. 물론 여기서는 cost function이 간단한 2차함수 형태이기 때문에 gradient가 0이 되는 지점이 전역해(global optimum)라고 당당히 말할 수 있습니다. 하지만 실제로 cost function은 오른쪽 그림처럼 복잡한 형태를 가지게 됩니다. 따라서, gradient 값이 0이 되어 학습을 종료했는데 알고 보니 안장점(saddle point)이거나 지역해(local optimum)인 경우가 있습니다. 그래서 이를 해결하기 위한 고민이 또 이루어져야 하는데 이는 다음 시간에 알아볼 예정입니다. 

출처: https://sebastianraschka.com/faq/docs/gradient-optimization.html

gradient 값을 구한 후에는 이를 계속 파라미터 theta값에 업데이트 해야 합니다. 이때 단순히 업데이트하는 것이 아닌 학습률(learning rate)인 알파값을 곱하여 업데이트하게 됩니다. 이는 업데이트 속도를 조절하기 위한 것으로 local optimum에 빠지지 않기 위한 방지책입니다.학습률(learning rate)는 사용자가 직접 지정해줘야 하는 하이퍼 파라미터값으로 learning rate가 너무 작으면 최소값으로 수렴하는데 너무 많은 시간이 걸리게 되고 반대로 learning rate 값이 너무 크면 값이 수렴하지 못하고 발산하게 됩니다. 따라서 적절한 learning rate를 지정해주는 것이 중요하며 이는 hyper parameter tuning의 과정을 통해 찾아낼 수 있습니다.  

출처: 강제원 교수님 강의자료

아래 그림은 하이퍼 파라미터인 learning rate의 크기에 따라 학습이 어떻게 진행되는지 보여주고 있습니다. 

출처: https://www.codingninjas.com/codestudio/library/nesterov-accelerated-gradient

마지막으로 gradient descent algorithm을 정리하자면, 임의의 theta에서 시작하여 cost function이 최소가 될 때까지 계속 theta를 업데이트 해나가며 최적의 파라미터 theta를 찾아내는 것이라고 할 수 있습니다. 물론 그 과정에서 지역해에 빠지거나 시간이 너무 오래 걸린다는 단점이 존재하게 됩니다. 이번 시간은 여기서 마무리하고 다음 차시에서 앞서 언급한 문제점들을 해결하는 다양한 gradient descent algorithm의 변형들을 살펴보겠습니다. 

Info) 아래 포스팅은 LGAimers에서 제공하는 AI 전문가 과정 중 이화여자대학교 강제원 교수님의 [지도학습(분류/회귀)] 강의에 대한 요약과 생각들을 정리한 것입니다. 정리된 내용들은 강의를 토대로 작성되었으나 수강자인 저의 이해도에 따라 일부 틀린 부분이 있을 수 있다는 점 알려드립니다. 잘못된 점이 발견될 시에는 즉각 수정하도록 하겠습니다. 또한, 강의의 모든 내용을 정리하기는 어렵기 때문에 인상깊었던 내용을 중심으로 정리하였습니다. 따라서, 설명의 맥락이 끊길 수도 있다는 점 양해 부탁드립니다. 

0. Introduction

강제원 교수님의 [지도학습] 강의를 듣기 전에 [품질과 신뢰성] 강의를 들었을 때는 생소한 개념들과 용어 때문에 머리가 어질어질했었는데 지도학습은 기존에 머신러닝을 공부하면서 귀에 피가 날 정도로 많이 들었던 내용이라 왠지 자신감이 생기고 가볍게 듣고 넘어갈 수 있겠다는 생각이 들었습니다. 하지만 강의를 듣기 전의 저의 호기로웠던 태도는 순식간에 긴장으로 바뀌게 되었습니다. 왜냐하면 제 예상과 달리 한번쯤 들어봤던 내용들인데 영어로 되어있고 수학 수식과 함께 설명이 되다보니 아는 내용도 어렵게 느껴졌습니다. 저는 선형대수학이나 미적분에 대해서 공부해본 적이 없던터라 온갖 요상한 기호들로 정리된 수식들이 낯설고 어렵게 다가왔습니다. 그래도 교수님께서 친절히 하나하나 설명을 해주셔서 그나마 어찌어찌 따라갈만 했습니다. 여튼 이번 Part1 강의에서 배운 내용들을 복습차원에서 간단히 정리해보고자 합니다. 

1. Supervised Learning vs Unsupervised Learning

Machine Learning은 크게 supervised learning과 unsupervised learning으로 구분할 수 있습니다. 종종 강화학습(reinforcement learning)이나 기타 다른 학습 방법들도 분류의 기준에 포함이 되지만 대다수의 교과서나 아주 rough하게 정리된 내용들을 보면 통상 이 2가지 learning으로 구분하는 경우가 많습니다. supervised와 unsupervised를 구분하는 기준은 data에 label이 있는냐 없느냐입니다. 입력 X 와 출력 y가 쌍으로 구성되어 있다면 labeled data로 학습하는 supervised learning이고 반대로 출력값(output)이 없다면 unlabeled data로 학습하는 unsupervised learning입니다. 쉽게 말하면 답을 알려주고 학습시키느냐 아니면 정해진 답 없이 학습을 시키느냐로 설명할 수 있을 것 같습니다. 

2. Supervised Learning의 과정

지도학습(supervised learning)의 대표적인 기법들에는 회귀(regression)과 분류(classification)이 있습니다. labled data가 연속형(continuous)이라면 regression, 범주형(discrete)이라면 분류라고 볼 수 있습니다. supervised learning이 학습하는 과정을 간단히 살펴보자면 아래 그림과 같습니다. 

출처: 강제원 교수님 강의자료

학습은 크게 두 가지 과정을 거치게 됩니다. 첫번째는 training 과정으로 주어진 학습 데이터(training data)로 학습을 진행하는 것입니다. 그리고 두번째는 testing 과정으로 학습된 모델을 통해 unseen input(혹은 testing data)을 입력하여 예측을 진행하는 것입니다. 이때, 첫번째 training 과정이 핵심인데 supervised learning에서는 training data를 입력 받아 output을 출력하게 되면 출력된 output을 desired output(우리가 가지고 있는 정답(label))과 비교하여 error를 찾아내게 됩니다. 그리고 이 error를 줄이는 방향으로 parameter들을 변경해 나가며 model을 교정하게 됩니다. 즉, supervised learning에서는 우리가 가지고 있는 정답과 모델이 내놓은 예측값을 비교해가며 모델이 정답에 가까운 예측값을 출력할 수 있도록 학습을 시키는 것이라고 정리할 수 있습니다. 

3. Supervised Learning의 X, Y 그리고 Hypothesis g

위의 과정을 수학적으로 정리해보면 다음과 같습니다. 우선 모델에 입력되는 X는 d차원의 입력 feature가 존재하는 백터이며, 모델이 출력하는 Y는 classification 문제의 경우 yes/no 또는 1/-1 등 2개의 label로 이루어진 binary decision이라고 할 수 있습니다. 그리고 X와 Y의 관계식이라고 할 수 있는 함수 f가 존재하며 이를 target function f라고 정의할 수 있습니다. 하지만, 우리는 현실적으로 이 세상의 모든 sample들을 전수조사할 수 없기 때문에 일부 sample로부터 target function f에 approximate할 수 있는 hypothesis g를 구할 수 있을 뿐입니다. 이 hypotheis g를 최대한 target function에 가깝게 하는 것이 machine learning 특히 이 강의에서는 supervised learning이 해야하는 목표라고 할 수 있습니다. 

4. Model Generalization

모든 data sample을 관찰하고 학습할 수 없다는 현실적인 문제는 data의 결핍이 존재할 수 밖에 없다는 사실로 이어지게 됩니다. 따라서, 우리가 보지 못하고 학습하지 못하는 data들에 대해서도 모델이 좋은 성능을 가지고 예측할 수 있도록 최대한 model을 일반화(generalization)해야 할 필요가 있습니다. generalization한다는 것은 training error, testing error을 최소화 하여 궁극적으로 generalizaion error 또한 최소화하는 것이라고 할 수 있습니다. 이 error들은 아래의 2가지 과정을 적절히 조합해 최소화해 나갈 수 있습니다. 

1) test error 와 train error 사이의 간극을 최대한 줄이기 (Overfitting 문제 해소)

test error와 train error의 차이를 줄인다는 것은 결국 학습한 모델이 일반적인(general) 성능을 가지게 되어 variance가 작아지는 것을 의미합니다. 즉, 모델이 train data를 잘 학습하고 동시에 test data도 잘 분류해내는 것입니다. 학습이 잘못되게 되면 train data에 대해서는 좋은 성능을 보이지만(train error는 작지만), test data에 대해서는 성능이 나쁜(test error는 상당히 큰) 경우가 생깁니다. 이를 variance가 큰 상태인 overfitting(과적합)되었다고 하고 regularization을 통해서 이 문제를 해소할 수가 있습니다. 

2) train error를 0에 가깝게 만들기 (Underfitting 문제 해소)

train error가 최소가 되도록 하는 것은 모델의 정확도가 높아지는 것을 의미합니다. 모델이 주어진 학습 데이터를 잘 학습하여 regression 또는 classification 문제를 잘 해결하는 것이 중요합니다. 하지만, 이러한 과정이 잘 이루어지지 않으면 편향성(bias)이 높아지는 underfitting(과소적합) 문제가 발생하게 됩니다. 즉, 모델이 너무 simple하여 성능(정확도)이 그리 높지 않다는 것입니다. 

위 2가지 과정을 정리하자면 test error ≒ train error ≒ 0 즉, test error, train error를 모두 0에 근사하게 만드는 것이 최적의 학습이라고 할 수 있습니다. 그리고 이 최적의 학습을 model generalization이라고 정리할 수 있습니다. 

5. Bias와 Variance의 Trade-off 관계

아래 그림은 overfitting과 underfitting을 설명하는 대표적인 예시입니다. 왼쪽은 underfitting된 모델로 비선형적인 데이터 분포를 선형 모델로 퉁치려는 모습입니다. 따라서 편향된(high bias & low variance) 모델이라 할 수 있고 반대로 오른쪽 그림은 주어진 데이터에 너무 과도하게 fit된 모습을 보이고 있습니다. 때문에 분산이 큰(high variance & low bias) 모델이라 할 수 있겠습니다. 결국 우리가 해야되는 것은 가운데 그림처럼 적절한 편향성(bias)과 적절한 분산(variance)을 가지는 optimum level의 모델을 찾아야 하는 것입니다. 

출처: https://opentutorials.org/module/3653/22071

이를 Bias와 Variance의 trade-off관계라고 합니다. bias가 작아지면 variance가 커지고 variance가 커지면 bias가 작아지는 상충관계를 보이게 됩니다. 따라서 이 둘 사이의 적절한 균형을 찾는 것이 위 4절에서 이야기한 model generalization이라고 할 수 있겠습니다. 아래 그림은 방금 설명한 bias와 variance사이의 trade-off를 보여주는 그림인데 아마 대부분 이 글을 보시는 분들은 꿈에 나올 정도로 지겹게 보셨으리라 생각됩니다. 

그럼 이만 part1에 대한 정리를 마치도록 하겠습니다. 

Intro) 아래 내용은 R의 기본적인 문법이나 자주 사용되는 라이브러리에 대해 앞으로 정리한 것입니다. 강필성 교수님의 [데이터 분석을 위한 프로그래밍 언어] 강의와 기타 자료들을 토대로 한 내용이지만 야매로 정리하다 보니 오류가 있을 수도 있습니다. 피드백 주시면 빠르게 수정하도록 하겠습니다.

1) matrix의 개요

matrix는 일반적으로 행렬이라고 번역되는 데이터 형태로 '차원(dimension)을 가지는 vector'라고 정의할 수 있습니다. 정의에서 알 수 있듯이 matrix의 뿌리는 vector이며 따라서 vector를 matrix로 변환할 수 있고 vector 뿐만 아니라 list 또한 vector로 변환이 가능합니다.  아래와 같이 A라는 vector와 B라는 list를 만들고 각각에 2X5차원을 부여하게 되면 matrix 형태로 변환이 됩니다. 

#### matrix의 차원 개념 ####
# 1부터 10까지의 vector생성
A <- 1:10 
dim(A)
print(A)

# vector를 차원을 가지는 matrix로 변환
dim(A) <- c(2,5)
print(A)

vector를 matrix 형태로

#### matrix의 차원 개념 ####
# 1부터 10까지의 list생성
B <- list(1,2,3,4,5,6)
print(B)
dim(B)

# list를 차원을 가지는 matrix로 변환
dim(B) <- c(2,3)
print(B)

list를 matrix 형태로

matrix의 특징을 살펴보면 vector와 크게 차이가 없습니다. 기본적으로 인덱스는 1부터 시작하게 됩니다. 앞서 살펴보았던 vector, list 모두 첫번째 인덱스 값은 1이었고 vector 또한 마찬가지입니다. 한편, matrix는 차원을 가지기 때문에 행(row)과 열(column)의 개념을 가지게 됩니다. 따라서, 값을 주어 이를 matrix 형태로 표현하기 위해서는 값들을 어떤 식으로 배열할 것인지가 중요하게 됩니다. R에서 matrix는 열 우선(column-major order)의 배열 구조를 default로 하고 있습니다. 물론, matrix() 함수의 파라미터를 행 우선으로 하여 matrix를 생성할 수 있습니다. 이는 아래에서 직접 R스크립트를 통해 살펴보도록 하겠습니다.

열을 기준으로 하여 matrix가 구성되어 있다

아래 2개의 그림을 통해 열 우선의 배열(colmun-major order)과 행 우선의 배열(row-major order)을 시각적으로 확인해보겠습니다. 

출처: https://huilife.tistory.com/15

2) matrix 생성

matrix를 만드는 방법을 알아보도록 하겠습니다. 앞선 포스트에서 vector와 list를 만드는 다양한 방식들이 있었듯이 matrix  또한 생성하는 방법이 여러가지가 있습니다. 세 방식 모두 matrix() 함수를 사용하지만 세부적인 파라미터 값을 달리하거나 순서를 달리하여 matrix를 생성하게 됩니다. 첫 번째는 가장 일반적인 방법으로 1) matrix의 모든 요소값과 함께 행과 열을 지정해주는 것입니다. 아래와 같이 matrix() 안에 1:12까지의 element, nrow =4, ncol =3를 넣게 되면 1부터 12까지 4X3 형태의 matrix가 완성된다. 이때 행과 열의 값은 element의 값과 호환이 되어야 한다. 가령, 1부터 20까지의 element를 가지고 matrix를 만들기 위해서는 1X20, 2X10, 4X5, 5X4, 10X2, 20X1와 같이 matrix의 크기가 element의 수와 일치해야 한다. 또한, 이러한 점 때문에 행의 수나 열의 수가 결정되면 자동적으로 반대의 열과 행의 수가 결정되기 때문에 굳이 nrow 값과 ncol 값 두 가지 다 넣지 않더라도 하나의 값만 가지고도 matrix를 생성할 수 있다. 아래 R스크립트 결과를 통해 각각의 경우를 살펴보겠습니다. 

앞서 matrix()의 파라미터를 통해 열 우선 배열을 행 우선 배열로 설정할 수 있다고 했는데 다음의 결과를 통해 확인해보겠습니다. 위의 결과와 달리 1,2,3,4,5,6,7,8... 의 배열이 행을 기준으로 나열되어 있습니다. 이처럼 byrow = TRUE를 설정해주면 열 기준의 배열을 행 기준의 배열로 바꿀 수 있습니다. 

matrix를 생성하는 두 번째 방법은 2) 빈 matrix를 만들고 각 element를 직접 채우는 것입니다. 그다지 추천할 만한 방법은 아니지만 이러한 방식으로도 가능하다는 점을 알려드리고 이 원리를 이용하면 생성된 matrix의 element를 다른 값으로 바꿀 수도 있습니다. 아래 R스크립트 결과를 보면 2X2 형태의 빈 matrix를 만들고 각각의 위치에 원하는 값을 직접 입력하게 됩니다. 

이처럼 각 위치 값을 인덱싱하여 값을 부여할 수 있다는 것은 새로운 값으로 대체할 수도 있다는 것을 의미합니다. 1행 1열의 값이 원래 1로 설정되어 있는데 이를 10으로 바꿔보도록 하겠습니다.  

#### matrix 생성 ####
# 행과 열을 지정해 matrix 생성
A = matrix(1:12, nrow=4, ncol=3)
A

# nrow 값만 가지고 matrix 생성
A = matrix(1:12, nrow=4)
A

# ncol 값만 가지고 matrix 생성
A = matrix(1:12, ncol=3)
A

# byrow 인자를 통해 행 우선 배열의 matrix 생성
B = matrix(1:12, nrow=4, byrow = T)
B

# 빈 matrix를 채우는 방식으로 생성
C = matrix(nrow=2,ncol=2)
C[1,1] = 1
C[1,2] = 2
C[2,1] = 3
C[2,2] = 4
C

# 1행 1열의 값을 10으로 바꾸기
C[1,1] = 10
C

'Python-R' 카테고리의 다른 글

[R] 데이터 타입 - List  (0) 2022.07.01
[R] 데이터 타입 - Vector  (0) 2022.06.30

Intro) 아래 내용은 R의 기본적인 문법이나 자주 사용되는 라이브러리에 대해 앞으로 정리한 것입니다. 강필성 교수님의 [데이터 분석을 위한 프로그래밍 언어] 강의와 기타 자료들을 토대로 한 내용이지만 야매로 정리하다 보니 오류가 있을 수도 있습니다. 피드백 주시면 빠르게 수정하도록 하겠습니다.

1) list 특징과 생성

list에 대해 살펴보도록 하겠습니다. list는 vector와 달리 이질적(heterogeneous)인 요소들로 구성이 가능합니다. 이 점 때문에 list를 handling하는 방식이 vector와는 살짝 차이가 있습니다. 리스트에서 인덱싱할 때는 2가지 방법이 있습니다. 대괄호([])를 2번 사용하거나 1번 사용할 수 있는데 이때 출력되는 결과물에 차이가 있습니다. 아래 스크립트를 직접 실행해보면서 차이를 보도록 하겠습니다. listA[[1]]와 같이 2번 사용하게 되면 결과는 element로 출력됩니다. 반면, listA[1]와 같이 1번 사용하게 되면 결과는 list형태로 출력이 됩니다.

# listA 생성
listA <- list(1, 2, "a")
print(listA)

# listA에 대괄호[] 2개를 사용하면 element로 호출
listA[[1]]

# listA에 대괄호[] 1개를 사용하면 list로 호출
listA[1]

# c()함수를 사용해 인덱싱 가능
listA[c(1,2)]

한편, list에도 vector처럼 이름을 부여할 수 있고 이름을 통해 인덱싱할 수 있습니다. 이름을 통해 호출할 때에는 대괄호([])를 2번 사용하거나 '$'기호를 사용해서 인덱싱할 수 있습니다. 아래 스크립트에 대한 결과물을 통해 확인이 가능합니다. 

# 벡터와 마찬가지로 이름을 부여할 수 있고 이름을 통해 호출 가능
names(listA)
names(listA) <- c("First", "Second", "Third")

# 이름을 통해 호출 할 때는 대괄호[] 2개를 사용하거나 $를 사용하여 호출
listA[["Third"]]
listA$Third

다음 스크립트는 list를 만드는 다양한 방법들을 보여주고 있습니다. 일반적으로 첫번째 방식이 가장 보편적으로 사용되지만 다양한 방법을 통해서도 list를 생성할 수 있다는 점을 알아두시면 됩니다. 

# list를 만드는 다양한 방법
# name과 value를 함께 list로 만들기
A <- list(name="lee", salary = 10000, union = TRUE)
print(A)

# value만을 가지고 list 만들고
B <- list("lee", 10000, TRUE)
names(B) <- c('name', 'salary', 'union')
print(B)

# vector()에 mode를 list로 하여 list를 만들고 이름과 값을 각각 부여 
C <- vector(mode="list")
C[["name"]] <- "lee"
C[["salary"]] <- 10000
C[["union"]] <- TRUE
print(C)

리스트 A, B, C 모두 값은 결과를 보임

2) list 응용

리스트에 새로운 값을 추가하거나 삭제하는 방법은 아래 스크립트를 통해 확인할 수 있습니다. C$office <- "frontier" 로 'office'라는 name에 'frontier'라는 value 값을 넣을 수 있고 반대로 C$salary <- NULL 을 통해 기존에 있던 salaray 값을 삭제할 수 있습니다.

# C에 새로운 value와 name 추가
C$office <- "frontier"
C

# C에서 salary에 해당하는 값 삭제
C$salary <- NULL
C

리스트에 새로운 값 추가하고 삭제한 결과

list 안에 list가 포함된 재귀적 list를 생성하고 이를 다시 vector로 분해하여 반환하는 unlist()까지 살펴보겠습니다. unlist()를 사용할 때 use.names = FALSE로 설정해주면 name 없이 value만 문자형 vector로 반환하게 됩니다. 

# 재귀list 생성
tmplist <- list(a = list(1:5, c("a","b","c")), b = "Z", c = NA)
tmplist

# list에서 element를 분해하여 문자형 vector로 반환
unlist(tmplist)
unlist(tmplist, use.names = FALSE)

# list에 함수 적용하기 전 A 생성
A <- list(1:3,25:29)
A

# lapply는 list형태로 반환하고 sapply는 vector나 matrix형태로 반환
lapply(A,median)
sapply(A,median)

'Python-R' 카테고리의 다른 글

[R] 데이터 타입 - Matrix  (0) 2022.07.06
[R] 데이터 타입 - Vector  (0) 2022.06.30

Intro) 데이터 분석을 할 때 저는 주로 Python을 이용하지만 가끔 R을 필요로 할 때가 있습니다. 아무래도 상황에 따라 R에서 코드가 더 간단할 때도 있고, 특히 고급통계 분석이나 시각화의 경우 육안으로 깔끔한 분석이 가능하기에 R이 편할 때도 있습니다. 그런데 R과 Python 간에는 미묘한 syntex 차이가 있어서 Python을 메인으로 사용하는 입장에서 오랜만에 R을 사용할 때면 헷갈리는 점들이 있습니다. 그래서 나중에 헷갈리는 부분들을 빠르게 찾아보기 위해 R의 기본적인 문법이나 자주 사용되는 라이브러리에 대해 앞으로 정리해보고자 합니다. 강필성 교수님의 [데이터 분석을 위한 프로그래밍 언어] 강의와 기타 자료들을 토대로 한 내용이지만 야매로 정리하다 보니 오류가 있을 수도 있습니다. 피드백 주시면 빠르게 수정하도록 하겠습니다.

1) 데이터 타입의 종류

R의 데이터 타입 중 Vector에 대해 간단히 살펴보도록 하겠습니다. Vector 타입에 대해 알아보기 전에 우선 데이터 타입에는 무엇이 있는지 살펴보겠습니다. 아래 그림처럼 scalar부터 다차원 배열의 tensor까지 여러 종류의 데이터 타입이 있지만 기초적인 데이터 분석에서는 vector와 matrix 그리고 dataframe이 가장 중요합니다.

출처: https://wikidocs.net/37001

데이터 타입을 구분하는 2가지 기준이 있습니다. 번째는 변수(variables)들이 동질적(Homogeneous)인지 이질적(Heterogeneous)인지, 번째는 관측치(observation) 개인지 여러 개인지입니다. 동질적(Homogeneous)이라는 것은 예를 들어 변수들이 모두 숫자형으로 형태가 모두 같아야 하는 것이고 이질적(Heterogeneous)이라는 것은 숫자형, 문자형, 명목형 변수 여러 형태가 섞여있는 것을 말합니다. 기준을 가지고 데이터 타입을 구분하면 아래 표와 같을 있습니다. 아래 표에서 조금 헷갈릴 수도 있는게 관측치가 1개이고 동질적이면 scalar 아닌가 생각될 있습니다. scalar라고 해도 되고 길이가 1 Vector라고 있습니다

2) 데이터 타입 - Vector(1)

R에서 '<-' 통해 A라고 선언한 변수에 c(15, 25, 35) 같이 값의 결합을 넣어주면 벡터가 완성됩니다. Python에서 변수를 선언하고 값을 대입하는 방식과는 달라 조금은 생소하게 느껴집니다. 이때 주의할 점은 이전 [데이터 타입의 종류] 공부할 살펴 보았듯이 vector 동질적(homogeneous) 값으로 구성되어야 합니다. 따라서 B 벡터와 같이 숫자형과 문자형 값을 함께 주게 되면 R 숫자인 값을 문자형으로 처리하여 'character' 인식하게 됩니다

# A와 B 벡터에 값 부여
A <- c(15,25,35)
B <- c(1, "A", 0.5)

# A벡터는 숫자형 B벡터는 문자형으로 인식
mode(A)
mode(B)

파이썬에서와 마찬가지로 R에서 인덱싱하는 방식은 비슷합니다. [1], [2:3] 같이 대괄호 안에 호출하고 싶은 값의 인덱스를 넣어주면 됩니다. , R에서는 다른 언어들과 달리 인덱스가 1부터 시작하게 됩니다. 점만 주의해주시면 됩니다. 그리고 A[c(2,3)] 같이 concatenating 함수를 이용해 인덱싱할 수도 있습니다. 그리고 벡터 값에 이름을 부여하여 이름을 통해서도 인덱싱할 수도 있습니다. 아래 스크립트를 콘솔 창에 직접 입력하여 하나씩 확인해보시면 쉽게 이해하실 있을 것입니다

# 벡터 값을 추출하는 방법(=파이썬의 인덱싱)
A[1]
A[2:3]
A[c(2,3)]

# 벡터에 이름 부여
names(A)
names(A) <- c("First", "Second", "Third")

# 인덱스 또는 이름을 통해 값 호출 가능
A[1]
A["First"]

벡터에 이름을 부여하면 그냥 num에서 Named num으로 바뀜

'Python-R' 카테고리의 다른 글

[R] 데이터 타입 - Matrix  (0) 2022.07.06
[R] 데이터 타입 - List  (0) 2022.07.01

Warning) 아래 코드들은 Google Colab 환경에서 작성된 것이기에 거의 그럴 일은 없지만 다른 환경에서는 정상적으로 작동되지 않을 수도 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 4. Lexicon 사전을 통해 Critic리뷰 감성 분석

STEP 4에서는 STEP 3에서 만든 2가지 함수를 통해 본격적으로 영화 '탑건'의 Critic 리뷰를 분석해보도록 하겠습니다. STEP 2에서 웹스크래핑한 결과인 movie_review_df에 각각 함수를 적용한 결과를 새로운 열로 추가하도록 하겠습니다. SentiWordNet 분석 결과는 ['Senti_swn']이라는 컬럼 이름으로 VADER 분석 결과는 ['Senti_vader']이라는 컬럼으로 생성하였습니다. 그리고 상위 10개만 출력해보도록 하겠습니다. (출력은 10개로 했지만 캡쳐가 잘려서 8개 결과 밖에 보이지 않습니다.)

# swn_polarity()를 이용하여 리뷰에 대한 감정분석 결과를 새로운 열로 추가
movie_review_df['Senti_swn'] = movie_review_df['Review'].apply(lambda x : swn_polarity(x))
# vader_polarity()를 이용하여 리뷰에 대한 감정분석 결과를 새로운 열로 추가
movie_review_df['Senti_vader'] = movie_review_df['Review'].apply(lambda x : vader_polarity(x))
movie_review_df[:10]

분석 결과를 새로운 열로 추가한 movie_review_df

분석 결과를 보면 swn기반과 vader 기반의 결과가 서로 다른 경우도 적지 않아 보입니다. 두 분석의 결과가 다른 것이 총 몇 개 정도 되는지 한번 알아보도록 하겠습니다. True는 두 분석 결과가 같은 경우이고 False는 분석 결과가 서로 다른 것을 나타냅니다. 총 409개의 리뷰 중 118개의 리뷰는 서로 다른 분석 결과를 보이고 있습니다. 

(movie_review_df['Senti_swn'] == movie_review_df['Senti_vader']).value_counts()

두 분석 결과가 다른 경우 확인

통상적으로 VADER 분석이 SentiWordNet 분석에 비해 정확도가 더 높은 것으로 알려져 있습니다. 실제로 몇몇 리뷰를 직접 확인해본 결과 VADER 분석이 더 우수한 분석 결과를 보인 것으로 확인 되었습니다. 따라서 VADER 분석 결과를 기준으로 하여 Good/Bad를 나누도록 하겠습니다. 분석 결과가 1이면 Good 출력, 0이면 Bad를 출력하는 good_bad_dc() 함수를 만들어서 활용하겠습니다. 

# 각 감정 분석 함수를 통해 구한 값을 비교하여 최종 Good/Bad 출력하는 함수
def good_bad_dc(review):
  if review['Senti_vader'] == 1:
    return 'Good'
  else:
    return 'Bad'

good_bad_dc()을 적용한 결과를 새로운 컬럼 ['Good/Bad']으로 하여 추가하도록 하겠습니다. 그리고 movie_review_df를 새로 출력해보겠습니다. 

good_bad_list = []
for i in range(movie_review_df.shape[0]):
  good_bad_list.append(good_bad_dc(movie_review_df.loc[i,:]))
movie_review_df['Good/Bad'] = good_bad_list
movie_review_df

Good/Bad 컬럼을 추가한 movie_review_df

여기서 잠깐 캡쳐한 사진 마지막 8번 리뷰를 한 번 살펴보겠습니다. 'The Jacobin'의 Eileen Jones씨가 남긴 리뷰인데 swn, vader 둘 다 해당 리뷰에 대해 Positive 평가를 내렸습니다. 그러나 실제로 리뷰 내용을 살펴보면 매우 부정적인 리뷰임을 알 수 있습니다. 로튼 토마토 사이트에서 직접 확인해본 결과 아래와 같이 'rotten(섞은)'이라는 마크를 남기며 혹평을 한 것으로 확인되었습니다. 아마도 맥락을 확인하지 않고 단어의 긍부정 여부만 가지고 판단하다보니 생긴 문제인 것 같습니다. 

총 409개의 리뷰 중 몇 개가 Good으로 분류되고 Bad로 분류되는지 확인해보도록 하겠습니다. 우선 개수를 파악해보고 비율로서의 결과도 출력해보겠습니다. 

movie_review_df['Good/Bad'].value_counts()

# 영화에 대한 Good/Bad 비율
movie_review_df['Good/Bad'].value_counts(normalize=True)*100

총 409개의 리뷰 중 74%인 305개가 긍정 리뷰로 평가되었고 25%인 104개의 리뷰가 부정 리뷰로 평가되었습니다. 로튼 토마토 사이트는 영화에 대한 평점을 전문가와 대중 평점으로 나누어 점수를 토마토의 신선도에 비유해 표현합니다. 신선도가 높을수록 좋은 평가를 받은 것이며 전문가와 대중은 각각 어떤 평가를 내렸는지 서로 비교해볼 수 있는 장점이 있습니다. 이번 프로젝트에서는 아쉽게도 대중평가는 살펴보지 못하고 전문가 리뷰만 살펴보았습니다. 그럼에도 감성 분석 알고리즘이 정확히 분석을 했는지 알아보기 위해 로튼 토마토 평점 지수를 가져와 비교해보도록 하겠습니다. 

영화 '탑건-메버릭'의 전문가 평점 지수는 무려 97%(프로젝트를 수행했던 당시)입니다. 정확히 어떤 방식으로 점수를 산정하는지는 모르겠지만 대략 97%의 전문가가 좋은 평가를 한 것이라고 유추해볼 수는 있습니다. 하지만 앞서의 분석 결과를 살펴보면 긍정 리뷰는 74%로 실제 평점 지수와 상당한 차이가 나타납니다. 차이가 발생한 이유에는 여러가지가 있겠지만 context를 고려한 분석이 된다면 더 좋은 결과를 산출할 수 있을 것으로 예상됩니다. 재미로 시작한 프로젝트이지만 정확도가 낮고 날카로운 분석이 되지 못한 점이 조금은 아쉬움이 남습니다. 다음에는 더 향상된 성능을 가진 분석 모델을 가져와 실험해보도록 하겠습니다. 이상으로 [로튼 토마토 리뷰 감성 분석] 미니 프로젝트를 마치도록 하겠습니다. 

Warning) 아래 코드들은 Google Colab 환경에서 작성된 것이기에 거의 그럴 일은 없지만 다른 환경에서는 정상적으로 작동되지 않을 수도 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 3. 로튼 토마토 사이트에 있는 Critic리뷰 감성 분석

STEP 2에서 영화 '탑건'의 리뷰 데이터를 스크래핑하는 작업을 진행하였습니다. 지금부터는 리뷰 데이터를 토대로 감성 분석을 진행하여 영화 '탑건'이 Critic(비평가)들로부터 전반적으로 어떤 평가를 받고 있는지 살펴보도록 하겠습니다. 이번 리뷰 분석에서는 알고리즘 모델에 학습시킬 output변수가 따로 없기 때문에 비지도학습 기반의 감성 분석(Lexicon based approach)을 사용하도록 하겠습니다. 

앞으로 진행될 과정에 대해 간략히 설명 드리면, 2가지 감성 사전(SentiWordNet, VADER)을 이용해서 리뷰 분석을 진행하고 2가지 분석 결과를 적절히 혼합하여 최종적으로 리뷰의 평가를 GOOD/BAD로 분류하게 됩니다. SentiWordNet, VADER 이 2가지 사전을 이용하는 큰 이유는 없습니다. SentiWordNet이 VADER에 비해 예측 정확도가 현저히 떨어진다고 알고 있는데 단지 이를 한번 확인해보고자 해서 다음과 같이 진행하게 되는 것입니다.

STEP 3에서는 SentiWordNet, VADER 각각의 사전을 통해 리뷰를 분석할 수 있는 함수를 만드는 것으로 마무리될 것입니다. 함수를 이용한 분석은 STEP 4에서 진행하겠습니다. 지금부터 감성 분석에 사용된 방법들은 [파이썬 머신러닝 완벽 가이드, 권철민 저]를 기반으로 하고 있습니다. 


(1) SentiWordNet Lexicon을 이용한 리뷰 감성 분석 함수 생성

SentiWordNet을 이용하기 위해 우선 nltk의 모든 데이터와 패키지를 다운로드하겠습니다. 다 설치하려다 보니 시간이 조금 걸립니다.

import nltk 
nltk.download('all')

nltk 설치가 완료되었으면 필요한 모듈들을 import 하겠습니다. (1) 토큰화된 단어들의 원형을 가져오기 위해  nltk 패키지에 있는 WordNetLemmatizer를 import하고 (2) SentiWordNet기반의 Lexicon사전을 import하고 (3) 문장 토큰화, 단어 토큰화, 그리고 품사 태깅을 위해 각각 필요한 모듈을 가져오겠습니다. 

from nltk.stem import WordNetLemmatizer #(1)
from nltk.corpus import sentiwordnet as swn #(2)
from nltk import sent_tokenize, word_tokenize, pos_tag #(3)

본격적으로 SentiWordNet 기반의 분석 함수를 만들기 전에 penn_to_wn() 함수를 만들겠습니다. 이 함수를 만드는 이유는 WordNet에 사용할 수 있는 품사형태로 만들어주기 위해서입니다. 품사를 구분하는 방식은 언어나 학자마다 다른데 NLTK에서는 PennTreeBank형식의 태그를 가져오게 됩니다. 따라서, NLTK의 품사 태깅된 결과를 WordNet의 품사 태깅으로 바꾸어 주어 이후에 Lemmatize와 Synset 객체를 만들 수 있도록 해야 합니다.

[파이썬 머신러닝 완벽 가이드]의 저자는 형용사, 명사, 부사, 동사 이렇게 4가지 품사만 변환하도록 하였습니다. 이 과정에서 인칭 대명사, 한정사, 'to' 등 일부 단어들이 손실됩니다. 아마 형용사, 명사, 부사, 동사들이 감성에 많은 영향을 주는 단어이기 때문에 이 4가지 품사만을 한정하여 분석에 활용하고자 한 것으로 생각됩니다. 

def penn_to_wn(tag):
    if tag.startswith('J'):
        return wn.ADJ
    elif tag.startswith('N'):
        return wn.NOUN
    elif tag.startswith('R'):
        return wn.ADV
    elif tag.startswith('V'):
        return wn.VERB
    return

이제 대망의 swn_polarity() 함수를 살펴보겠습니다. 정말 복잡한 함수입니다. 해당 함수의 로직을 중간 중간 주석을 통해 설명하였습니다. 그 과정이 상당히 길고 복잡하기에 한 번에 이해가 안 될 수도 있습니다. 가장 중요한 부분이라고 할 수 있는 두 번째 for문에 대해 정리하면 다음과 같습니다. 주석 처리된 각 단계마다 어떤 결과가 도출되는지 예시를 통해 설명하도록 하겠습니다. 문장 토큰화가 완료된 상태라 가정하고 아래 한 문장을 가지고 각 단계의 출력을 보여드리겠습니다.

ex) She wishes he came to visit every week because he tells great stories and makes her favorite dinner.

(1) 단어 토큰화 후 (단어, NTLK 기반의 품사 태그)의 튜플 추출

[('She', 'PRP'), ('wishes', 'VBZ'), ('he', 'PRP'), ('came', 'VBD'), ('to', 'TO'), ('visit', 'VB'), ('every', 'DT'), ('week', 'NN'), ('because', 'IN'), ('he', 'PRP'), ('tells', 'VBZ'), ('great', 'JJ'), ('stories', 'NNS'), ('and', 'CC'), ('makes', 'VBZ'), ('her', 'PRP$'), ('favorite', 'JJ'), ('dinner', 'NN'), ('.', '.')]

(2) NTLK 품사 태그를 WordNet 품사 태그로 변환

[('She', None), ('wishes', 'v'), ('he', None), ('came', 'v'), ('to', None), ('visit', 'v'), ('every', None), ('week', 'n'), ('because', None), ('he', None), ('tells', 'v'), ('great', 'a'), ('stories', 'n'), ('and', None), ('makes', 'v'), ('her', None), ('favorite', 'a'), ('dinner', 'n'), ('.', None)]

(3) 명사, 형용사, 부사, 동사 아닌 것은 패스하고 lemmatizer로 원형 단어 추출

['wish', 'come', 'visit', 'week', 'tell', 'great', 'story', 'make', 'favorite', 'dinner']


(4) 추출된 원형 단어와 품사 태그를 입력하여 Synset 객체 생성 (output 일부 생략)

[[Synset('wish.v.01'), Synset('wish.v.02'), Synset('wish.v.03'), Synset('wish.v.04'), Synset('wish.v.05'), Synset('wish.v.06')],  [Synset('come.v.01'), Synset('arrive.v.01'), Synset('come.v.03'), Synset('come.v.04'), Synset('come.v.05'), Synset('come.v.06'), ....(중략)...[Synset('favorite.s.01'), Synset('favored.s.01')], [Synset('dinner.n.01'), Synset('dinner.n.02')]]


(5) sentiwordnet의 감성 분석으로 SentiSynset 추출, 이때 synset 리스 중 첫 번째 값을 사용

[SentiSynset('wish.v.01'), SentiSynset('come.v.01'), SentiSynset('visit.v.01'), SentiSynset('week.n.01'), SentiSynset('state.v.01'), SentiSynset('great.s.01'), SentiSynset('narrative.n.01'), SentiSynset('make.v.01'), SentiSynset('favorite.s.01'), SentiSynset('dinner.n.01')]


(6) 감성 분석 결과는 (긍정 - 부정)을 더해가며 계산

[wish.v.01]에 대한 pos점수: 0.125, neg점수: 0.375 , [come.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [visit.v.01]에 대한 pos점수: 0.0, neg점수: 0.0,[week.n.01]에 대한 pos점수: 0.0, neg점수: 0.0, [state.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [great.s.01]에 대한 pos점수: 0.0, neg점수: 0.0, [narrative.n.01]에 대한 pos점수: 0.0, neg점수: 0.0, [make.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [favorite.s.01]에 대한 pos점수: 0.125, neg점수: 0.0 , [dinner.n.01]에 대한 pos점수: 0.0, neg점수: 0.0

def swn_polarity(text):
    # 감성 지수 초기값 
    sentiment = 0.0
    tokens_count = 0

    lemmatizer = WordNetLemmatizer() # WordNetLemmatizer 객체 생성
    raw_sentences = sent_tokenize(text) # 문장 토큰화
    
    for raw_sentence in raw_sentences:
        # (1)단어 토큰화 후 (단어, NTLK 기반의 품사 태그) 추출 
        tagged_sentence = pos_tag(word_tokenize(raw_sentence))

        for word , tag in tagged_sentence:
            # (2)NTLK 품사 태그를 WordNet 품사 태그로 변환
            wn_tag = penn_to_wn(tag) 

            # (3)명사, 형용사, 부사, 동사 아닌 것은 패스하고 lemmatizer로 원형 단어 추출
            if wn_tag not in (wn.NOUN , wn.ADJ, wn.ADV, wn.VERB): 
                continue                  
            lemma = lemmatizer.lemmatize(word, pos=wn_tag)
            if not lemma:
                continue

            # (4)추출된 원형 단어와 품사 태그를 입력하여 Synset 객체 생성 
            synsets = wn.synsets(lemma , pos=wn_tag)
            if not synsets:
                continue

            # (5)sentiwordnet의 감성 분석으로 감성 synset 추출, 이때 synset 리스 중 첫번째 값을 사용
            synset = synsets[0]
            swn_synset = swn.senti_synset(synset.name())
            
            # (6)감성 분석 결과는 (긍정 - 부정)을 더해가며 계산
            sentiment += (swn_synset.pos_score() - swn_synset.neg_score())        
            tokens_count += 1
    
    if not tokens_count:
        return 0
    
    # 총 score가 0 이상일 경우 긍정(Positive) 1, 그렇지 않을 경우 부정(Negative) 0 반환
    if sentiment >= 0 :
        return 1
    
    return 0

(2) VADER Lexicon을 이용한 리뷰 감성 분석 함수 생성

vader_polarity() 함수는 간단합니다. SentimentIntensityAnalyzer() 객체만 생성해주면 polarity_scores() 메서드를 통해 알아서 감성 점수 결과값을 가져오게 됩니다. 쉽게 말하면 앞에서 SentiWordNet 기반의 복잡한 함수 과정을 한 번에 메서드 하나로 해결이 가능하다는 것입니다. 이상으로 STEP 3를 마치고 다음 STEP 4에서는 앞서 생성한 2개의 함수를 통해 본격적으로 감성분석에 들어가도록 하겠습니다. 

from nltk.sentiment.vader import SentimentIntensityAnalyzer

def vader_polarity(review,threshold=0.1):
    analyzer = SentimentIntensityAnalyzer()
    scores = analyzer.polarity_scores(review)
    
    # compound 값에 기반하여 threshold 입력값보다 크면 1, 그렇지 않으면 0을 반환 
    agg_score = scores['compound']
    final_sentiment = 1 if agg_score >= threshold else 0
    return final_sentiment

Warning) 아래 코드들은 Google Colab을 기반으로 작성된 것이기에 다른 환경에서는 정상적으로 작동이 안 될 수 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 2. 로튼 토마토 사이트에 있는 Critic리뷰 스크래핑 

(1) Selenium 및 webdriver 설치

감성분석을 수행하기 하기 전 필요한 데이터(리뷰)들을 수집하는 스크래핑 과정을 보여드리겠습니다. 간략히 로직에 대해 설명드리면, 로튼 토마토 사이트 내 특정 영화 리뷰에 대한 URL을 입력하면 해당 영화 Critic 리뷰를 모두 긁어오는 것입니다. 보통 웹 스크래핑에서 BeautifulSoup과 Selenium이 양대산맥으로서 많이 사용되는 패키지들인데 지금은 Selenium을 사용하도록 하겠습니다. 앞서 STEP 1에서 설명드린 바와 같이 로튼 토마토에서는 모든 리뷰를 가져오기 위해 next 버튼을 누르며 20개 간격으로 리뷰를 스크래핑해야 하기 때문에 웹브라우저 조작이 가능한 Selenium이 더 편리하다고 할 수 있습니다.

우선 웹스크래핑을 위해 Selenium을 설치하도록 하겠습니다. (1) Selenium 설치 후, (2) 우분투 환경을 업데이트해주고 (3) chromium의 chromedriver을 설치해줍니다. (4) usr 디렉터리 안에 있는 chromedriver 폴더를 usr/bin 디렉터리로 복사해주고 나서 (6) '/usr/lib/chromium-browser/chromedriver' 폴더를 환경변수 지정해줍니다. 그러면 이 폴더 안에 있는 파이썬 파일들을 쉽게 import 할 수 있습니다.

!pip install Selenium #(1)
!apt-get update #(2)
!apt install chromium-chromedriver #(3)
!cp /usr/lib/chromium-browser/chromedriver /usr/bin #(4)

import sys #(5)
sys.path.insert(0, '/usr/lib/chromium-browser/chromedriver') #(6)

(2) 필요한 라이브러리 import 및 chorme_options 설정

Selenium 설치와 환경설정을 완료했으면 필요한 라이브러리를 import하고 chrome_options를 미리 설정하도록 하겠습니다. chrome_options를 설정하는 이유는 웹 스크래핑을 수행할 때 웹 브라우저를 띄우지 않으면서 스크래핑을 진행하기 위해서입니다. 웹 브라우저 창을 띄우면서 스크래핑을 진행하면 스크래핑하는 창과 코드가 실행되는 창을 왔다 갔다 하도록 코드를 짜줘야 하기 때문에 더 복잡하고 비효율적이게 됩니다. 다소 설명이 이상한 것 같긴 한데... 여하튼 '--headless', '--no-sandbox' 등 옵션 변수들을 추가해주도록 하겠습니다. 

import time
import pandas as pd
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException
from selenium.webdriver.common.by import By

chrome_options = webdriver.ChromeOptions()

chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')​

(3) page_click() 함수 만들기

다음으로는 page_click(), review_scraping() 함수를 만들도록 하겠습니다. 전자는 웹페이지 안에서 next 버튼을 눌러 다음 페이지로 이동하도록 하는 함수이고 후자는 리뷰를 긁어모으는 과정을 함수로 만든 것입니다. review_scraping()함수 안에서 page_click() 함수가 실행된다고 보시면 됩니다.

(※ page_click() 함수는 굳이 만들지 않고 review_scraping() 함수 안에서 직접 실행시키셔도 상관없습니다.)  

page_click()에서 주의하실 점은 find_element를 통해 next 버튼에 해당하는 element를 찾아내어 click한 후에는 꼭 time.sleep()을 1,2초 정도 해주시기 바랍니다. 다음 페이지로 이동하는데 보통 1초 내외의 시간이 소요되는데 이 속도보다 더 빠르게 스크래핑을 시작하게 되면 NoSuchElementException 등의 에러가 발생할 수 있기 때문입니다.

def page_click(wd):
  next_bt = wd.find_element(By.XPATH, '//*[@id="content"]/div/div/div/nav[1]/button[2]/span')
  next_bt.click()
  time.sleep(1)

(4) review_scraping() 함수 만들기

본격적으로 reveiw_scraping()함수를 만들어보도록 하겠습니다. webdriver 객체를 chrome으로 설정하여 생성합니다. 그리고 함수 인자로 받을 url값을 통해 로튼토마토 리뷰 페이지로 이동합니다. 스크래핑으로 통해 받을 내용은 'reviewer(리뷰 작성자)', 'publication(작성자가 속한 매체)', 'review(리뷰)' 총 3가지이며 최종 반환 값으로서 3가지 항목에 대한 내용을 pandas의 dataframe 형식으로 만들겠습니다. 스크래핑하는 과정과 dataframe으로 변환하는 과정은 아래 코드의 주석들을 통해 설명해놓았으니 참고하시기 바랍니다.

첫 페이지에서 20개의 리뷰를 모두 추출하여 dataframe으로 변환하였으면 앞서 만든 page_click() 함수를 호출하여 다음 페이지로 넘어갑니다. 그리고 while문 반복을 통해 20개의 리뷰를 추출하고 페이지를 넘어가는 과정을 반복합니다. 더 이상 페이지를 넘어갈 수 없다면(next 버튼이 존재하지 않는다면) 예외처리를 통해 반복을 break 하겠습니다. 리뷰가 정상적으로 스크래핑이 되고 있는지 육안으로 확인하기 위해 스크래핑 시작, 페이지 이동을 알리는 메시지들과 최종적으로 스크래핑을 진행한 영화의 리뷰가 총 몇 개 나왔는지 메시지를 출력하겠습니다.

def review_scraping(url):
  # webdriver 객체를 chrome으로 하여 설정
  wd = webdriver.Chrome('chromedriver', options = chrome_options) 
  wd.implicitly_wait(3) 

  # 해당 url로 이동
  wd.get(url)

  movie_review_df = pd.DataFrame() # 빈 move_review_df 생성
  page_no = 1

  print("[스크래핑 시작]")
  
  # 매 페이지마다 반복적으로 리뷰를 스크래핑하기 위한 while문
  while True:
    try:
      # 스크래핑할 3가지 항목에 대한 빈 리스트 생성 
      reviewer_list = []
      publication_list = []
      review_list = []
	  
      # find_elements를 통해 추출한 것들 중 text만 추출하여 빈 리스트에 추가
      reviewers = wd.find_elements(By.CSS_SELECTOR, 'div.col-xs-8.critic-info > div.col-sm-17.col-xs-32.critic_name > a.unstyled.bold.articleLink')
      reviewer_list += [reviewer.text for reviewer in reviewers]
      #print(reviewer_list)

      publications = wd.find_elements(By.CSS_SELECTOR, 'div.col-sm-17.col-xs-32.critic_name > a:nth-child(3) > em')
      publication_list += [publication.text for publication in publications]
      #print(publication_list)

      reviews = wd.find_elements(By.CLASS_NAME, 'the_review')
      review_list += [review.text for review in reviews]
      #print(review_list)
	  
      # 20개의 리뷰를 스크래핑하여 만든 리스트들을 dataframe 형식으로 변환
      new_movie_review_df = pd.DataFrame({'Reviewer': reviewer_list,
                                          'Publication': publication_list,
                                          'Review' : review_list})
      movie_review_df = pd.concat([movie_review_df, new_movie_review_df], ignore_index = True)

      # 해당 페이지의 리뷰 스크래핑이 완료되면 다음 페이지로 이동 
      page_click(wd)
      page_no += 1
      print('[다음 {}페이지로 이동]'.format(page_no)) # 페이지 이동할 때마다 메시지 출력
	
    # 예외처리
    except ElementNotInteractableException as ex:
      print("[모든 스크래핑이 완료되었습니다.]")
      break

    except NoSuchElementException as ex:
      print("[모든 스크래핑이 완료되었습니다.]")
      break
  
  # 더이상 넘어갈 페이지가 없어 스크래핑이 완료되면 아래 메시지 출력 
  mv_title = url.split('/')[-2].replace('_', ' ').title()
  review_cnt = movie_review_df.shape[0]
  print("\n영화 [{}]에 대한 {}개의 리뷰 스크래핑 완료".format(mv_title, review_cnt))
  
  # 스크래핑한 결과물인 movie_review_df를 반환
  return movie_review_df

(5) review_scraping() 함수를 통해 스크래핑 진행

리뷰 스크래핑을 위한 모든 준비가 되었으니 본격적으로 리뷰를 가져와 보도록 하겠습니다. 최근 '탑건-메버릭'이라는 영화가 개봉되었는데 영화 '탑건'을 예시로 하여 스크래핑을 진행해보도록 하겠습니다. (리뷰 작성일 기준 총 409개의 리뷰가 있었습니다.)

url = 'https://www.rottentomatoes.com/m/top_gun_maverick/reviews'
movie_review_df = review_scraping(url)

review_scraping() 메시지 출력 결과
movie_review_df 출력 결과 (11행까지)

+ Recent posts