[Statistics] PCA(Principal Component Analysis)
Updated:
데이터 사이언티스트 되기 책: https://recipesds.tistory.com/
Linear Algebra
다음 통계학 개념들을 설명하기 전에 필요한 기초적인 선형대수 내용을 다음 4가지로 다룰 것이다.
- 행렬은 변환이다.
- 행렬의 Eigen Value, Eigen Vector는 행렬의 변환에 대한 항등원이다.
- 행렬의 변환에 의한 기존 좌표계의 기저Basis 변환
- 행렬은 그 자체로 데이터를 설명한다.
1. 행렬은 변환이다.
우선 행렬의 곱은 다른 측면에서 보면 벡터(입력)의 선형 변환이다. 아래 예시는 어떤 행렬에 (1,1)을 넣으면 (3,7)로 변환되는 예이다.
(1,1)벡터 1개의 예를 보면 위와 같은데 가로 0~1, 세로 0~1범위의 사각형에 있는 데이터들을 이 행렬을 통해 변화시키면 아래와 같다.
2. 행렬의 Eigen Value, Eigen Vector는 행렬의 변환에 대한 항등원이다.
먼저 Eigen Value, Eigen Vector에 대해 살펴보자. 선형 변환을 하는 행렬 A가 다음과 같다고 하자.
\[A = \begin{vmatrix} 2 & 1 \\ 3 & 4 \end{vmatrix}\]Eigen Vector는 수식으로 정의할 때 다음과 같이 정의한다.
\[Ax = \lambda x\]이러한 관계를 만족하면 $x$를 A에 대한 Eigen Vector라 하고 $\lambda$를 Eigen Value라고 한다. 의미를 해석하면 A변환에 대해서 어떤 x vector는 A로 변환했을 때, $\lambda$배만큼 크기만 변하고 벡터 자체는 변하지 않는다는 뜻이다.
Eigen Vector와 Eigen Value를 구하는 방법은 다음과 같다.
\[Ax = \lambda x \\ Ax = \lambda I x \\ Ax - \lambda I x = 0 \\ (A - \lambda I)x = 0\]이때 만약 $(A - \lambda I)$가 역행렬을 갖는다면 $x = (A - \lambda I)^{-1} 0 = 0$가 되므로 $x$ 벡터는 0벡터가 될 수 밖에 없다. 따라서 $x$는 0이 아닌 해를 가져야 하니가 $(A - \lambda I)$는 역행렬을 가지면 안된다. 역행렬을 갖지 않는 조건은 $det \vert (A - \lambda I) \vert = 0$이므로 이 식을 풀어서 $\lambda$를 구할 수 있고, 그 $\lambda$에 어울리는 벡터를 구할 수 있다. 이런 경우 $\lambda$는 유일하고, 그것을 만족하는 벡터 $x$는 무수히 많다.
\[\begin{align} &\begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = \lambda \begin{vmatrix} x_1 \\ x_2\end{vmatrix} \\ &\begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = \lambda \begin{vmatrix} 1 & 0 \\ 0 & 1\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} \\ &\begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = \begin{vmatrix} \lambda & 0 \\ 0 & \lambda\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} \\ &\begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} - \begin{vmatrix} \lambda & 0 \\ 0 & \lambda\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = 0 \\ &\begin{vmatrix} 2 - \lambda & 1 \\ 3 & 4 - \lambda\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = 0 \\ &det \left( \begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix} - \lambda \begin{vmatrix} 1 & 0 \\ 0 & 1\end{vmatrix} \right) = 0 \\ &det \begin{vmatrix} 2 - \lambda & 1 \\ 3 & 4 - \lambda\end{vmatrix} = (2 - \lambda)(4 - \lambda) - 1 \cdot 3 = 0 \\ &\lambda^2 - 6\lambda + 5 = 0 \\ &(\lambda - 1)(\lambda - 5) = 0 \\ &\therefore \lambda = 1 \ or \ 5 \end{align}\]위 예시로 Eigen Value가 1이거나 5인 것을 찾아냈다. 이제 아래 식에 Eigen Value를 넣어서 그 식을 만족하는 $(x_1, x_2)$ 벡터를 구하면 그 고유값의 고유 벡터인 Eigen Vector가 된다.
\[\begin{vmatrix} 2 - \lambda & 1 \\ 3 & 4 - \lambda\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = 0\]1) $\lambda = 1$
\[\begin{vmatrix} 2 - 1 & 1 \\ 3 & 4 - 1\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = 0 \\ x_1 + x_2 = 0 \\ x_1 = -x_2 \\ \therefore (1, -1)\]수 많은 $x_1, x_2$ 중 가장 다루기 편한 크기의 벡터로 정해 보면 $(1, -1)$이다.
2) $\lambda = 5$
\[\begin{vmatrix} 2 - 5 & 1 \\ 3 & 4 - 5\end{vmatrix} \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = 0 \\ -3x_1 + x_2 = 0 \\ 3x_1 = x_2 \\ \therefore (1, 3)\]이렇게 Eigen Value (1, 5) / Eigen Vector ((1, -1), (1, 3))의 pair를 찾았다. 처음의 정의를 다시 보면 어떤 변환 행렬 A에 대하여 (1, -1)과 (1, 3)은 $\lambda$만큼 크기만 바뀌고 방향은 그대로이다. Eigen Vector는 행렬 A에 의해 방향이 변환이 되지 않는 항등원 같은 Vector인 셈이다.
Gaussian Random Noise Data를 만들어서 행렬 A를 이용해 변환해보면 다음과 같다.
위 가우시안 노이즈를 변환하면 아래와 같다.
이것이 어떤 의미를 갖는가 하면, A를 통해서 데이터를 변환하게 되면 Eigen Vector들의 방향으로 데이터들이 늘어나서 자리하게 되는데, 이 방향이 Eigen Vector 방향에 Eigen Value 크기 방향이다.
3. 행렬의 변환에 의한 기존 좌표계의 기저 Basis 변환
행렬의 또 다른 측면은 기저 basis의 변환이다. 입력 벡터가 다음과 같다고 할 때,
\[\begin{vmatrix} x_1 \\ x_2\end{vmatrix}\]이 입력 벡터는 아래와 같이 (1, 0), (0, 1) 기저의 선형 조합으로 다시 쓸 수 있다.
\[\begin{vmatrix} x_1 \\ x_2\end{vmatrix} = x_1 \begin{vmatrix} 1 \\ 0\end{vmatrix} + x_2 \begin{vmatrix} 0 \\ 1\end{vmatrix}\]그렇다면 행렬 A에 대해 다음과 같이 다시 쓸 수 있다.
\[A \begin{vmatrix} x_1 \\ x_2\end{vmatrix} = A \left( x_1 \begin{vmatrix} 1 \\ 0\end{vmatrix} + x_2 \begin{vmatrix} 0 \\ 1\end{vmatrix} \right) = x_1 A \begin{vmatrix} 1 \\ 0\end{vmatrix} + x_2 A \begin{vmatrix} 0 \\ 1\end{vmatrix}\]이는 수평방향의 기저 (1, 0)를 A 변환한 후 $x_1$배, 수직방향의 기저 (0, 1)를 A 변환한 후 $x_2$배 한 것과 같다. 즉, 수평, 수직 방향의 기저를 변환한 것이다.
예를 들면 다음과 같다.
\[\begin{align} &= x_1\begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix}\begin{vmatrix} 1 \\ 0\end{vmatrix} + x_2\begin{vmatrix} 2 & 1 \\ 3 & 4\end{vmatrix}\begin{vmatrix} 0 \\ 1\end{vmatrix} \\ &= x_1\begin{vmatrix} 2 \\ 3\end{vmatrix} + x_2\begin{vmatrix} 1 \\ 4\end{vmatrix} \end{align}\](1, 0)이 (2, 3)으로, (0, 1)이 (1, 4)로 변경되었다. 즉, (2, 3)와 (1, 4)는 A에 의해 변횐되는 벡터의 기저가 된다는 것이다.
자세히 보면 새로운 기저는 행렬의 각 column이다. 이렇게 basis가 새로운 basis로 변형되면 새로운 기저의 선형합으로 나타낼 수 있는 공간이 새로 생긴다.
이 새로운 공간에 대한 모든 정보를 행렬 A가 갖고 있다.
결국 위와 같은 모양으로 모두 (1, 4), (2, 3)의 선형 조합 Linear Combination으로 표현이 가능한 데이터들이다.
4. 행렬은 그 자체로 데이터를 설명한다.
지금까지의 내용으로 행렬은 어떤 벡터 데이터를 변환시키는 일을 하는데, 결국에는 행렬과 관계된 벡터의 분포 즉, 모양새를 이미 설명하고 있다.
이제까지 살펴본 행렬 A에 대해 A로 인해 가질 수 있는 데이터의 모양새는 새로운 기저와 Eigen Vector, Eigen Value이다. 이때 고유값 Eigen Value 중 가장 큰 값을 갖는 것을 Dominant 하다고 한다. 이런 식으로 새로운 basis와 Eigen Vector를 합쳐서 보면 행렬이 데이터를 설명한다고 할 수 있다.
예를 들어 가로축 0~1, 새로축 0~3으로 uniform distributed 한 데이터를 변환하면 다음과 같다.
다른 특별한 예를 들어 모든 element가 양수이고, 대칭 행렬(Symmetry)인 경우의 행렬을 양의 정부호 행렬이라 하는데 다음과 같다.
\[\begin{vmatrix} 2 & 1 \\ 1 & 2\end{vmatrix}\]이제 Eigen Value와 Eigen Vector를 구해보면 Eigen Value는 1, 3이 된다. 이때의 각각 Eigen Vector는 (-1, 1), (1, 1)이다. 여기에 벡터의 크기를 1로 만드는 정규화를 하면 다음과 같다.
\[x_1 = \frac{1}{\sqrt{2}}\begin{vmatrix} -1 \\ 1\end{vmatrix}, x_2 = \frac{1}{\sqrt{2}}\begin{vmatrix} 1 \\ 1\end{vmatrix}\]Eigen Value가 3일 때가 Dominant Eigen Vector이다. 이 행렬에 대한 변환을 그림으로 표현하면 다음과 같다.
이렇게 데이터와 행렬을 같이 보면 새로운 기저, Eigen Value와 Vector를 확인할 수 있다. 이렇듯 행렬이 데이터를 설명한다는 것은 행렬을 보고 변환된 데이터를 상상할 수 있다는 의미로 받아들일 수 있다.
추가적으로 Determinant에 대해 조금 설명하면 어떤 행렬의 Determinant는 행렬의 Basis를 변으로 갖는 평행사변형의 면적이다. 이 면적으로 행렬의 크기를 정해서 사용하는데, 행렬의 절대값이라 할 수 있다. Determinant의 절대값은 선형 변환의 크기를 나타내고, 부호는 변환되는 데이터의 모양의 보존 여부를 의미한다. 즉, 다음과 같다.
변환된 면적 = $\vert det(A) \vert$ 변환 전 면적
Determinant가 0이라는 의미는 이 면적이 0이라는 뜻이다. 이것은 행렬을 구성하는 열 벡터가 같은 선상에 있다는 의미이다.
이렇게 되면 이 행렬은 무엇을 변환시키더라도 같은 선상에 있을 수 밖에 없다.
PCA(Principal Component Analysis)
주성분 분석(Principal Component Analysis)의 목적은 고차원의 데이터를 저차원의 데이터로 만드는데에 있다. 예를 들어 아래와 같이 2차원의 데이터가 있다고 하자. 이 2차원 데이터를 1차원 데이터로 합칠 수 있는지 확인해 보자.
가장 먼저 드는 생각은 $x_1$축으로 Projection하거나, $x_2$축으로 Projection하는 것이다.
하지만 위 사진과 같이 $x_1$축으로 Projection하든 $x_2$축으로 Projection하든 데이터가 겹치면서 정보를 잃게 된다. 2차원의 데이터를 겹치지 않고 1개의 차원으로 합치려면 아래와 같이 가운데를 가로지르는 축을 생각해 볼 수 있다.
이렇게 가로지르는 선에 Projection하면 겹치는 데이터 없이 하나의 선 위에 데이터를 표시할 수 있다. 결론적으로 이런 식으로 데이터의 차원을 축소할 수 있는 축을 찾아 정사영 시켜 차원을 낮추는 것인데, 이를 위해선 최대한 정보를 잃지 않으면서 차원을 축소시켜줄 축을 찾아야 한다.
이러한 선의 조건은 데이터의 분산이 최대한 넓게 나오는 것이다. 즉, 해당 축(선)에 데이터가 가장 넓은 방향으로 펼쳐져 있어야 사영했을 때 정보를 최대한 보존하게 된다. 이러한 선을 찾을 때 Eigen Vector와 Eigen Value가 사용된다. Eigen Vector를 다룬 다는 것은 앞서 살펴보았듯이 데이터의 모양을 설명하는 행렬이 있다는 것인데, 그 행렬이 공분산 행렬이다. 공분산 행렬은 다음과 같다.
\[\begin{vmatrix} S_X^2 & S_{XY} \\ S_{XY} & S_Y^2\end{vmatrix}\]이 행렬의 의미는 각 축으로 퍼진 정도를 나타낸다.
\[S_{XY} = Cov(X, Y) = \sigma_{xy} = E\lbrack (X-\mu_x)(Y-\mu_y) \rbrack = E(XY) - \mu_x\mu_y\]공분산 행렬의 각 element의 의미는 다음과 같다.
만약 공분산이 0인 경우 서로 uncorrelated하며, 각각의 분산에 비례하여 데이터들이 흩어져 있다.
만약 서로의 공분산이 있다면 상관관계가 생기고 양이든 음이든 방향성을 갖게 된다.
실제로 공분산 행렬이 설명하는 데이터는 x 데이터의 분산에 비해 어디에 위치하는지, y 데이터의 분산에 비해 어디에 위치하는지, 그리고 공분산에 비해 어디에 위치하는지를 설명한다. 데이터는 결국 공분산 행렬로 설명되고, Eigen Vector는 행렬이 벡터에 영향을 미치는 주축 방향이므로 공분산 행렬의 Eigen Vector는 데이터가 어떤 방향으로 가장 크게 분산되어 있는지를 알려준다. 그래서 공분산 행렬의 Eigen Vector 방향으로 데이터가 가장 길게 늘어서고, Eigen Value만큼의 크기를 갖는다. 따라서 Eigen Vector에 데이터를 사영한다면 잃어버리는 정보를 최소화 할 수 있다. 이렇게 공분산의 Eigen Vector가 바로 분산이 제일 큰 방향을 가리키는 것을 Principal Component(PC)라 한다.
위 그림은 2차원 데이터 상에서 2개의 Principal Component를 보여준다. 만약 n개 차원이라면 n개의 Principal Component가 존재한다. 따라서 많은 차원을 가장 효율적인 개수의 차원으로 줄일 수 있는데, 그 이유가 공분산 행렬이 대칭행렬이기 때문에 n차원의 공분산 행렬의 고유 백터들이 서로 Orthogonal(직교)하는데, 이 때문에 압축을 할 때 Eigen Value가 큰 순으로 Projection을 하면 서로 겹치지 않는 방향이면서, 분산이 큰 순으로 데이터를 압축할 수 있다.
실제 예시를 통해 PCA로 압축을 해보자. 다음과 같은 아기들에 대한 데이터가 있다고 하자.
몸무게 나이(개월)치아강도 손톱길이 출생지
0 33 48 19 1 서울
1 36 64 12 3 부산
2 34 53 18 2 경기도
3 40 70 5 2 제주도
4 32 44 20 3 서울
5 37 66 10 1 강원도
6 35 59 15 2 평양
7 34 60 17 3 경상도
8 37 60 15 3 전라도
9 33 44 23 2 충청도
아기들에 대한 몸무게, 나이(개월), 치아 강도, 손톱 길이, 출생지 데이터인데 출생지를 빼고 나머지 데이터를 생각한다면 4차원 데이터이다. 이 데이터를 다룰 때 4차원 데이터를 모두 고려하여 다룰 것이냐, 아니면 차원수를 줄여서 다룰 수 있을 것이냐를 생각해 볼 수 있다. 4차원이라고 함은 4개의 컬럼을 이야기하는 것이고 Feature라고 부른다. 지금은 4차원이지만, 100차원, 200차원이 되게 되면 매우 복잡해진다. 그래서 데이터를 다룰 때, Feature Selection, Feature Extraction이라는 작업을 하는 것이다.
Feature Selection: 불필요한 Feature를 버림
Feature Extraction: 원본 데이터의 Feature들의 조합으로 새로운 Feature를 생성하는 것. 보통 적은 차원으로 Mapping하는 것을 의미
이중 PCA는 새로운 Feature를 생성하는 Extraction의 개념인데, 차원 축소를 해서 Feature Extraction을 하게 된다. 어떤 Feature들의 조합으로 새로운 Feature를 생성할 지는 상관 분석을 통해 정해진다.
df_baby.corr()
>
몸무게 나이(개월) 치아강도 손톱길이
몸무게 1.000000 0.901324 -0.933167 -0.069720
나이(개월) 0.901324 1.000000 -0.933849 0.006149
치아강도 -0.933167 -0.933849 1.000000 0.112160
손톱길이 -0.069720 0.006149 0.112160 1.000000
0.9가 넘는 쌍이 (나이-몸무게), (치아강도-몸무게), (나이-치아강도)로 3개나 있다. 이 쌍들을 조사하기 위해 먼저 X와 Y의 형태로 데이터를 분리하고,
X = df_baby[['몸무게', '나이(개월)', '치아강도']].copy()
Y = df_baby['출생지']
몸무게-나이(개월)을 확인해보면 다음과 같다.
X.plot(x='몸무게', y='나이(개월)',kind='scatter')
몸무게-치아강도는 다음과 같다.
X.plot(x='몸무게', y='치아강도',kind='scatter')
마지막으로 치아강도-나이(개월)은 다음과 같다.
X.plot(x='치아강도', y='나이(개월)',kind='scatter')
이렇게 3가지 모두 상관관계가 있어보이니 이 3차원의 데이터를 1차원으로 만들어 관리해도 될 것 같다.
이 3가지 데이터를 표준 정규화를 한 후에 Covariance Matrix를 구하고 Eigen Vector에 Projection한 뒤 1차원으로도 잘 표현하고 있는지 확인해 보자.
from sklearn.preprocessing import StandardScaler
Xstdnorm = StandardScaler().fit_transform(X)
>
array([[-0.91304348, -1.01236401, 0.71942469],
[ 0.39130435, 0.82829783, -0.67945665],
[-0.47826087, -0.43715719, 0.5195845 ],
[ 2.13043478, 1.51854602, -2.07833799],
[-1.34782609, -1.47252947, 0.91926488],
[ 0.82608696, 1.05838056, -1.07913704],
[-0.04347826, 0.253091 , -0.07993608],
[-0.47826087, 0.36813237, 0.31974431],
[ 0.82608696, 0.36813237, -0.07993608],
[-0.91304348, -1.47252947, 1.51878546]])
데이터를 표준정규화를 할 때는 sklearn의 StandardScaler를 이용하면 편리하게 할 수 있다. 참고로 fit_transform은 fit은 입력 데이터의 표준편차 등을 찾아내는 것, transform은 데이터를 구한 표준편차를 이용해서 정규화하는 것이다.
이제 numpy를 이용해서 Covarinace Matrix를 구하면 다음과 같다.
columns = Xstdnorm.T
covariance_matrix = np.cov(columns)
> # columns
[[-0.91304348 0.39130435 -0.47826087 2.13043478 -1.34782609 0.82608696
-0.04347826 -0.47826087 0.82608696 -0.91304348]
[-1.01236401 0.82829783 -0.43715719 1.51854602 -1.47252947 1.05838056
0.253091 0.36813237 0.36813237 -1.47252947]
[ 0.71942469 -0.67945665 0.5195845 -2.07833799 0.91926488 -1.07913704
-0.07993608 0.31974431 -0.07993608 1.51878546]]
> # covariance matrix
[[ 1.11111111 1.0014712 -1.03685201]
[ 1.0014712 1.11111111 -1.0376103 ]
[-1.03685201 -1.0376103 1.11111111]]
Eigen Vector와 Value는 다음과 같다.
eig_vals, eig_vecs = np.linalg.eig(covariance_matrix)
>
Eigen Vectors
[[-0.57504939 -0.71089114 0.40490985]
[-0.57519393 0.7032635 0.41781862]
[ 0.58178187 -0.00736465 0.81331151]]
>
Eigen Values
[3.16182516 0.10964386 0.06186432]
3차원 데이터에 대한 Eigen Value와 Vector는 3개가 나온다. 우리는 이 중에서 가장 Eigen Value가 큰 Vector를 사용할 것이기 때문에 eig_vecs.T[0]
이다.
이제 가장 큰 Eigen Vector에 데이터를 Projection하면 다음과 같다. Projection하는 방법은 대상 벡터에 Inner Product를 하면 된다.
X_projected = Xstdnorm.dot(eig_vecs.T[0])
>
array([ 1.52589897, -1.09674677, 0.82875862, -3.30770303, 2.15686822,
-1.71163723, -0.16707962, 0.24929756, -0.73329366, 2.25563695]
최종 결과를 확인하면 다음과 같다.
df_result = pd.DataFrame(X_projected, columns=['PC1'])
df_result['label'] = Y
print(df_result.head(4))
PC1 label
0 1.525899 서울
1 -1.096747 부산
2 0.828759 경기도
3 -3.307703 제주도
한 축으로 시각화 하면 다음과 같다.
df_result['y-'] = 0
df_result.plot(x="PC1", y="y-", kind="scatter")
이렇게 압축된 데이터에 대해서 얼마나 잘 압축되었는지를 확인하려면 전체 Eigen Value들 중에 Projection한 Eigen Value가 얼마나 차지하는지로 확인한다.
eig_vals[0] / sum(eig_vals)
>
0.9485475467310901
결과를 보면 Dominant Eigen Vector가 전체의 95%를 설명하고 있는 것을 알 수 있다. 마지막으로 이렇게 압축한 데이터 나이(개월)-몸무게-치아강도 3가지를 압축했으니까, 이름을 다시 짓는다면 발달사항 정도로 압축해서 관리할 수 있다.
최종적으로 PCA를 이용하여 압축한 데이터는 다음과 같다.
df_baby['발달사항'] = df_result['PC1'].copy()
df_baby.drop(['몸무게', '나이(개월)', '치아강도'], axis=1, inplace=True)
손톱길이 출생지 발달상황
0 1 서울 1.525899
1 3 부산 -1.096747
2 2 경기도 0.828759
3 2 제주도 -3.307703
4 3 서울 2.156868
5 1 강원도 -1.711637
6 2 평양 -0.167080
7 3 경상도 0.249298
8 3 전라도 -0.733294
9 2 충청도 2.255637
정리하면서 PCA의 장점은 공분산 행렬의 Eigen Vector들이 서로 수직 한다는 데 있다. Eigen Vector들이 서로 수직 하기 때문에 각 Eigen Vector들끼리 완전히 관계를 끊어버릴 수 있고, 이렇게 함으로써 다중공선성을 없앨 수 있는데, 다중공선성을 이전에 언급했듯이 독립변수끼리 같이 비슷하게 같이 움직이는 독립변수 중에 대표적인 1개 독립변수 하나만 있으면 어떤 식으로 회귀 모델에 그 변수가 기여하는지 알 수 있는데, 이것들을 모두 모델에 포함하면 독립변수 하나만 있을 때 보다 그 두 개 변수의 합 때문에 그 변수의 합의 분산이 1개 있을 때 보다 커져버려서 회귀 모델의 계수의 분산을 크게 할 수 있다는 것이다.
Leave a comment