Gorio Tech Blog search

Gaussian Process 설명

|

Gaussian Process에 대해 알아보자!

Gaussian Process는 Random(Stochastic) Process의 한 예이다. 이 때 Random Process는 시간이나 공간으로 인덱싱된 Random Variable의 집합을 의미한다. GP의 정의는 아래와 같다.

Stochastic process such that every finite collection of those random variables has a multivariate normal distribution.

이는 또한 이 Random Variable들의 선형 결합 일변량 정규분포를 따른다는 말과 동일한 설명이고, GP의 일부를 가져와도 이는 항상 다변량 정규분포를 따른다는 것을 의미한다. 또한 다변량 정규분포는 주변 분포와 조건부 분포 역시 모두 정규분포를 따르기 때문에 이를 계산하기 매우 편리하다는 장점을 지닌다.

GP는 일종의 Bayesian Non-parametric method으로 설명되는데, 이 때 Non-parametric이라는 것은 parameter의 부재를 의미하는 것이 아니라 parameter가 무한정 (infinite) 있다는 것을 의미한다.

지금부터는 GP에 대해 이해하기 위해 단계적으로 설명을 진행할 것이다.


1. Basics of Gaussian Process

다변량 정규분포를 생각해보자.

[p(x, y) \sim \mathcal{N}( \begin{bmatrix} \mu_x \ \mu_y \end{bmatrix}, \begin{bmatrix} \Sigma_x \Sigma_{xy} \ \Sigma_{xy}^T \Sigma_y \end{bmatrix})]

앞서 언급하였듯이 이 다변량 정규분포를 이루는 확률변수의 어떠한 부분집합에 대해서도 주변 분포와 조건부 분포 모두 정규분포를 따른다. GP는 여기서 한발 더 나아가서, 이러한 다변량 정규분포를 무한 차원으로 확장시키는 개념으로 생각하면 된다.

이 무한의 벡터를 일종의 함수로 생각할 수도 있을 것이다. 연속형 값을 인풋으로 받아들이는 함수를 가정하면, 이는 본질적으로 input에 의해 인덱싱된 값들을 반환하는 무한한 벡터로 생각할 수 있다. 이 아이디어를 무한 차원의 정규분포에 적용하면 이것이 바로 GP의 개념이 된다.

따라서 Gaussian Process는 함수에 대한 분포라고 표현할 수 있다. 다변량 정규분포가 평균 벡터와 공분산 행렬로 표현되는 것처럼, GP 또한 평균 함수와 공분산 함수를 통해 다음과 같이 정의된다.

[P(X) \sim GP(m(t), k(x, x\prime))]

GP에 있어서 Marginalization Property는 매우 중요한 특성이다. 우리가 관심 없거나 관측되지 않은 수많은 변수에 대해 Marginalize할 수 있다.

GP의 구체적 예시를 다음과 같이 들 수 있을 것이다. 실제로 이는 가장 흔한 설정이다.

[m(x) = 0]

[k(x, x\prime) = \theta_1 exp( - \frac{\theta_2}{2} ( x - x\prime)^2 )]

여기서 공분산 함수로 Squared Exponential을 사용하였는데, $x$와 $x\prime$이 유사한 값을 가질 수록 1에 수렴하는 함수이다. (거리가 멀수록 0에 가까워짐) 평균 함수로는 0을 사용하였는데, 사실 평균 함수로 얻을 수 있는 정보는 별로 없기에 단순한 설정을 하는 것이 가장 편리하다.

유한한 데이터 포인트에 대해 GP는 위에서 설정한 평균과 공분산을 가진 다변량 정규분포가 된다.

다음 Chapter부터는 본격적으로 이론에 대한 부분을 정리하도록 하겠다. 2개의 논문을 정리하였는데, 첫 번째 논문Gaussian Process의 가장 기본적이고 중요한 내용을 담은 논문이며, 두 번째 논문은 좀 더 개념을 확장하고 직관적으로 Gaussian Process Regression에 대해 서술한 논문이다.


2. Gaussian Process in Machine Learning

본 논문은 GP가 회귀를 위한 Bayesian 프레임워크를 형성하기 위해 어떻게 사용되는지, Random(Stochastic) Process가 무엇이고 이것이 어떻게 지도학습에 사용되는지를 설명하는 것이 주 목적이다. 또한 공분산 함수의 Hyperparameter 설정에 관한 부분, 그리고 주변 우도와 Automatic Occam’s Razor에 관한 이야기도 포함한다.

(Occam’s Razor: 오캄의 면도날 원칙, 단순함이 최고다.)

2.1. Posterior Gaussian Process

GP는 함수에 대한 분포로 정의되며, 이러한 GP베이지안 추론Prior로 사용된다. 이 Prior는 학습 데이터에 의존하지 않으며 함수들에 대한 어떤 특성을 구체화한다. Posterior Gaussian Process의 목적은 학습데이터가 주어졌을 때, 이 Prior를 업데이트하는 방법을 도출해내는 것이다. 나아가 새로운 데이터가 주어졌을 때 적절히 예측 값을 반환하는 것이 목표가 될 것이다.

기존의 학습데이터와 새로운 테스트 데이터를 분리하여 다음과 같은 결합 분포를 상정해보자.

[\begin{bmatrix} \mathbf{f} \ \mathbf{f} \end{bmatrix} \sim \mathcal{N}( \begin{bmatrix} \mathbf{\mu} \ \mathbf{\mu_}\end{bmatrix}, \begin{bmatrix} \Sigma, \Sigma_* \ \Sigma_*^T, \Sigma_{**} \end{bmatrix})]

[\mathbf{\mu} = m(x_i), i = 1, … , n]

이제 우리가 알고 싶어하는 f*의 조건부 분포는 아래와 같은 형상을 지녔다. 아래 식은 테스트 데이터에 대한 사후분포에 해당한다.

[\mathbf{f_*} \mathbf{f} \sim \mathcal{N}(\mu_* + \Sigma_^T \Sigma^{-1}(\mathbf{f}-\mu), \Sigma_{**}-\Sigma_^T\Sigma^{-1}\Sigma_*)]

이와 같은 분포를 얻을 수 있는 이유는 결합 정규분포를 조건화하는 공식인 다음의 결과에 기인한다.

위에서 확인한 사후분포에 기반하여 Posterior Process를 구해보면 아래와 같다.

이 때 $\Sigma(X, x)$ 는 모든 학습 데이터와 $x$ 의 공분산 벡터를 의미한다. 이제 위 식을 자세히 뜯어보자. Posterior Process의 공분산 함수는 Prior의 공분산 함수에서 양의 값을 뺀 것과 같다. 즉 Posterior Process의 공분산 함수는 Prior의 그것보다 언제나 작은 값을 가진다는 의미이다. 이것은 논리적인데, 데이터가 우리에게 정보를 제공하였기 때문에 Posterior의 분산이 감소하는 것이다.

자 이제 학습 데이터의 Noise를 고려해야 한다. 이에 대해서 정규 분포를 설정하는 것이 일반적이다. Noise를 고려한 후 다시 정리하면 아래와 같다.

이제 Posterior Process에서 샘플링을 진행할 수 있다. 이렇게 평균 함수와 공분산 함수를 정의함으로써 학습 데이터가 주어졌을 때 PriorPosterior로 업데이트할 수 있게 되었다. 그러나 문제가 찾아왔다. 어떻게 평균 함수와 공분산 함수를 적절히 설정하는가? 그리고 Noise Level( $\sigma_n^2$ )은 어떻게 추정하는가?

2.2. Training a Gaussian Process

사실 일반적인 머신러닝 적용 케이스에서 Prior에 대해 충분한 정보를 갖고 있는 것은 드문 경우이다. 즉, 평균 함수와 공분산 함수를 정의하기에는 갖고 있는 정보가 부족하다는 것이다. 우리는 갖고 있는 학습 데이터에 기반하여 평균, 공분산 함수에 대해 적절한 추론을 행해야 한다.

Hyperparameter에 의해 모수화되는 평균, 공분산 함수를 가진 Hierarchical Prior를 사용해보자.

[f \sim \mathcal{GP}(m, k)]

[m(x) = ax^2 + bx + c]

[k(x, x\prime) = \sigma_{y}^2 exp(- \frac{( x - x\prime)^2}{2l^2} ) + \sigma_n^2 \delta_{ii\prime}]

이제 우리는 $\theta=[a, b, c, \sigma_y, \sigma_n, l]$ 이라는 Hyperparameter 집합을 설정하였다. 이러한 계층적 구체화 방법은 vague한 Prior 정보를 간단히 구체화할 수 있게 해준다.

우리는 데이터가 주어졌을 때 이러한 모든 Hyperparameter에 대해 추론을 행하고 싶다. 이를 위해서는 Hyperparameter가 주어졌을 때 데이터의 확률을 계산해야 한다. 이는 어렵지 않다. 주어진 데이터의 분포는 정규 분포임을 가정했기 때문이다. 이제 Log Marginal Likelihood를 구해보자.

[L = logp(\mathbf{y} \mathbf{x}, \theta) = -\frac{1}{2}log \Sigma - \frac{1}{2}(\mathbf{y} - \mu)^T \Sigma^{-1}(\mathbf{y}-\mu) - \frac{n}{2}log(2\pi)]

이제 편미분 값을 통해 이 주변 우도를 최적화(여기서는 최소화)하는 Hyperparameter의 값을 찾을 수 있다. 아래에서 $\theta_m$ 와 $\theta_k$ 는 평균과 공분산에 관한 Hyperparameter를 나타내기 위한 parameter이다.

위 값들은 Conjugate Gradients와 같은 Numerical Optimization에 사용된다.

GP는 Non-parametric 모델이기 때문에 Marginal Likelihood의 형상은 Parametric 모델에서 보던 것과는 사뭇 다르다. 사실 만약 우리가 Noise Level인 $\sigma_n^2$ 를 0으로 설정한다면, 모델은 정확히 학습 데이터 포인트와 일치하는 평균 예측 함수를 생성할 것이다. 하지만 이것은 주변 우도를 최적화하는 일반적인 방법이 아니다.

Log Marginal Likelihood 식은 3가지 항으로 구성되어 있는데, 첫 번째는 Complexity Penalty Term으로 모델의 복잡성을 측정하고 이에 대해 페널티를 부과한다. Negative Quadratic인 두 번째 항은 데이터에 적합하는 역할을 수행하며, 오직 이 항만이 학습 데이터의 Output인 $\mathbf{y}$ 에 의존적이다. 세 번째 항은 Log-Normalization Term으로 데이터에 대하여 독립적이며 사실 뭐 그리 중요한 항은 아니다.

GP에서 페널티와 데이터에 대한 적합의 trade-off는 자동적이다. 왜냐하면 Cross Validation과 같은 외부적 방법이 세팅될 Parameter가 존재하지 않기 때문이다. 실제로 이와 같은 특성은 보통의 머신러닝 알고리즘 상에 존재하는 Hyperparameter 튜닝에 소요되는 시간을 절약하게 해주기 때문에 학습을 더욱 간단하게 만드는 장점을 갖게 된다.

2.3. Conclusions and Future Directions

본 논문에서는 GP가 굉장히 변동성이 크고 유연한 비선형적 회귀를 구체화하는 데 편리하게 사용되는 과정에 대해 알아보았다. 본 논문에서는 오직 1가지 종류의 공분산 함수가 사용되었지만, 다른 많은 함수들이 사용될 수 있다. 또한 본 논문에서는 오직 가장 간단한 형태인 정규 분포의 Noise를 가정하였지만, 그렇지 않을 경우 학습은 더욱 복잡해지고 Laplace 근사와 같은 방법이 도입되거나 Sampling이 이루어져야만 non-Gaussian Posterior를 정규분포와 유사하게 만들 수 있다.

또 중요한 문제점은 계산 복잡성이다. 공분산 행렬의 역행렬을 구하기 위해서는 메모리 상에서는 $\mathcal{O}(n^2$ 의 복잡도가, 계산 상에서는 $\mathcal{O}(n^3)$ 의 복잡도가 발생한다. 리소스에 따라 다르지만, 행이 10,000개만 넘어가도 직접적으로 역행렬을 계산하기에는 많은 무리가 따른다. 따라서 근사적인 방법이 요구되는데, 본 논문이 나온 시점이 2006년임을 고려하면, 이후에도 많은 연구가 진행되었음을 짐작할 수 있을 것이다.

한 예로 이 논문이 있는데, 추후에 다루도록 할 것이다.


3. Gaussian Process Regression

본 Chapter에서는 두 번째 논문을 기반으로 좀 더 단계적으로 설명을 해볼 것이다.

논문의 내용을 설명하기 전에 전체적인 구조를 다시 한번 되짚어보도록 하자.

3.1. Overview

비선형 회귀 문제를 생각해보자. 우리는 데이터가 주어졌을 때 이를 표현하는 어떤 함수 f를 학습하고 싶고 이 함수는 확률 모델이기 때문에 신뢰 구간 또는 Error Bar를 갖게 된다.

[Data: \mathcal{D} = [\mathbf{x}, \mathbf{y}]]

Gaussian Process는 평균 함수와 공분산 함수를 통해 이 함수에 대해 분포를 정의한다. 이 함수는 Input Space $\mathcal{X}$ 를 $\mathcal{R}$ 로 mapping하며, 만약 두 공간이 정확히 일치할 경우 이 함수는 infinite dimensional quantity가 된다.

[p(f) = f(x) \sim \mathcal{GP}(m, k)]

그리고 베이즈 정리에 따라 위 확률은 Bayesian Regression에 사용된다.

[p(f \mathcal{D}) = \frac{p(f)p(\mathcal{D} f)}{p(\mathcal{D})}]

Posterior를 구하기 위해서는 당연히 PriorLikelihood가 필요한데, 이 때 PriorGaussian Process를 따른다고 가정한다. 이제 Likelihood를 구해야 한다.

우리가 수집한 데이터 $\mathcal{D}$ 는 일반적으로 Noise를 포함하고 있다. 따라서 우리의 정확한 목표는 $f(x)$ 를 추정하는 것이 아니라 Noise를 포함한 $y$ 를 추정하는 것이어야 한다. 평균 함수를 0으로 가정하고 $y$ 를 비롯하여 GPR에 필요한 모델들에 대해 정리해보자.

[y = f(x) + \epsilon]

[\epsilon \sim \mathcal{N}(0, \sigma_n^2)]

다음 Chapter에서도 나오겠지만 이 Noise의 분산을 공분산 함수 속으로 집어넣을 수 있다. (자세한 수식은 다음 Chapter를 참조하라) 그러면 사실 아래의 $K$ 는 $K + \sigma_n^2$ 를 의미하게 된다.

[f \sim \mathcal{GP}(0, K)]

fPriorGP고, Likelihood는 정규분포이므로 f에 대한 Posterior 또한 GP이다. 일단 주어진 데이터에 기반하여 Marginal Likelihood를 구해보자.

[p(\mathbf{y} \mathbf{x}) = \int p(\mathbf{y} f, \mathbf{x}) p(f \mathbf{x}) df]

[= \mathcal{N}(0, K)]

그런데 이 때 이전 Chapter와 마찬가지로 공분산 함수를 정의할 때 사용되는 Hyperparameter로 $\theta$ 를 정의하게 되면, Marginal Likelihood는 정확히 아래와 같이 표현할 수 있다.

[p(\mathbf{y} \mathbf{x}, \theta) = \mathcal{N}(0, K_{\theta})]

이 식에 Log를 취해서 다시 정리하면 Log Marginal Likelihood가 된다. ( $\theta$ subscript는 생략한다.)

[logp(\mathbf{y} \mathbf{x}, \theta) = -\frac{1}{2}log K - \frac{1}{2}\mathbf{y}^T K^{-1}\mathbf{y} - \frac{n}{2}log(2\pi)]

Numeric한 방법으로 위 목적 함수를 최적화(최소화)하는 $\theta$ 를 구하면 이는 공분산 함수의 최적 Hyperparameter가 된다. 이제 예측을 위한 분포를 확인해보자. 새로운 데이터 포인트 $x_*$ 가 주어졌을 때의 예측 값에 관한 사후분포이다.

[p(y_* x_, \mathcal{D}) = \int p(y_ x_*, f, \mathcal{D}) p(f \mathcal{D}) df]

[= \mathcal{N}( K_K^{-1}\mathbf{y}, K_{**} - K_ K^{-1} K_*^T )]

이제 위 분포를 바탕으로 Sampling을 진행하고, 평균과 분산을 바탕으로 그래프를 그리면 본 글의 가장 서두에서 본 것과 같은 아름다운 그래프를 볼 수 있다.

평균인 $K_*K^{-1}\mathbf{y}$ 는 다음과 같이 $\mathbf{y}$ 에 대한 선형결합으로 표현할 수도 있다.

[K_K^{-1}\mathbf{y} = \Sigma_{i=1}^n \alpha_i k(x_i, x_), \alpha = K^{-1}\mathbf{y}]

지금까지 설명한 내용이 바로 Gaussian ProcessFunction Space View로 이해한 것이다.

3.2. Definition of Gaussian Process

지금부터는 논문의 내용을 정리한 것이다. 사실 GP의 기본적인 설명은 끝났다고 봐도 무방하지만, 그럼에도 이 세심한 논문의 설명을 다시 한 번 읽어보지 않을 수가 없다. 정의에 대한 부분은 처음에 설명하였으므로 생략하도록 하겠다.

Chapter1에서 공분산 함수 $ k(x, x\prime) $에 대해서 설명하였는데, 본 논문에 맞추어 Notation을 살짝 변형하도록 하겠다. (이전 Chapter에서는 이 공분산 함수를 가장 단순한 버전인 $\Sigma$ 로 표현하였다.)

[k(x, x\prime) = \sigma_f^2 exp( - \frac{( x - x\prime)^2}{2l^2} )]

정말 기호만 살짝 바뀌었다. $x$가 $x\prime$과 유사할 수록 $f(x)$라 $f(x\prime)$과 상관성(Correlation)을 가진다고 해석할 수 있다. 이것은 좋은 의미이다. 왜냐하면 함수가 smooth해지고 이웃한 데이터 포인트끼리 더욱 유사해지기 때문이다.

만약 그 반대의 경우 2개의 데이터 포인트는 서로 마주칠 수도 없다. 즉 새로운 $x$값이 삽입될 때, 이와 먼 곳에 있는 관측값들은 그다지 큰 영향을 미칠 수 없다. 이러한 분리가 갖는 효과는 사실 length parameter인 $l$에 달렸있는데, 이 때문에 이 공분산 함수는 상당한 유연성을 지니는 식이 된다.

하지만 데이터는 일반적으로 Noise를 포함하고 있다. 이 때문에 언제나 측정 오차는 발생하기 마련이다. 따라서 $y$ 관측값은 $f(x)$에 더불어 Gaussian Noise를 포함하고 있다고 가정하는 것이 옳다.

[y = f(x) + \mathcal{N}(0, \sigma_n^2)]

많이 보았던 회귀식 같아 보인다. 이 Noise를 공분산 함수안에 집어넣으면 아래와 같은 형식을 갖추게 된다.

[k(x, x\prime) = \sigma_f^2 exp(- \frac{( x - x\prime)^2}{2l^2} ) + \sigma_n^2 \delta(x, x\prime)]

여기서 $\delta(x, x\prime)$은 Kronecker Delta Function이다.

많은 이들은 GP를 사용할 때 $\sigma_n$을 공분산 함수와 분리해서 생각하지만 사실 우리의 목적은 y* 를 예측하는 것이지 정확한 f* 를 예측하는 것이 아니기 때문에 위와 같이 설정하는 것이 맞다.

Gaussian Process Regression을 준비하기 위해 모든 존재하는 데이터포인트에 대해 아래와 같은 공분한 함수를 계산하도록 하자.

$K$의 대각 원소는 $\sigma_f^2 + \sigma_n^2$ 이고, 비대각 원소 중 끝에 있는 원소들은 $x$ 가 충분히 큰 domain을 span할수록 0에 가까운 값을 갖게 된다.

3.3. How to Regress using Gaussian Process

GP에서 가장 중요한 가정은 우리의 데이터가 다변량 정규 분포로부터 추출된 Sample로 표현된다는 것이므로 아래와 같이 표현할 수 있다.

[\begin{bmatrix} \mathbf{y} \ y* \end{bmatrix} \sim \mathcal{N}(0, \begin{bmatrix} K, K_^T \ K_, K_{**} \end{bmatrix})]

우리는 물론 조건부 확률에 대해 알고 싶다.

[p( y_* \mathbf{y} )]

이 확률은 데이터가 주어졌을 때 $y_*$ 에 대한 예측의 확실한 정도를 의미한다.

[y_* \mathbf{y} \sim \mathcal{N}( K_K^{-1}\mathbf{y}, K_{**} - K_ K^{-1} K_*^T )]

정규분포이므로, $y_*$ 에 대한 Best Estimate는 평균이 될 것이다.

[\bar{y}* = K*K^{-1}\mathbf{y}]

그리고 분산 또한 아래와 같다.

[var(y_) = K_{**} - K_ K^{-1} K_*^T]

이제 본격적으로 예제를 사용해보자. Noise가 존재하는 데이터에서 다음 포인트 $x_*$ 에서의 예측 값은 얼마일까?

6개의 관측값이 다음과 같이 주어졌다.

x = [-1.5, -1, -0.75, -0.4, -0.25, 0]

Noise의 표준편차 $\sigma_n$ 이 0.3이라고 하자. $\sigma_f$ 와 $l$ 을 적절히 설정하였다면 아래와 같은 행렬 $K$를 얻을 수 있다.

공분산 함수를 통해 아래 사실을 알 수 있다.

[K_{**} = 3]

[K_* = [0.38, 0.79, 1.03, 1.35, 1.46, 1.58]]

[\bar{y}_* = 0.95]

[var(y_*) = 0.21]

[x* = 0.2]

그런데 매번 이렇게 귀찮게 구할 필요는 없다. 엄청나게 많은 데이터 포인트가 존재하더라도 이를 한번에 큰 $K_*$ 과 $K_{**}$ 을 통해 계산해버리면 그만이다.

만약 1000개의 Test Point가 존재한다면 $K_{**}$ 는 (1000, 1000)일 것이다.

95% Confidence Interval은 아래 식으로 구할 수 있고 이를 그래프로 표현하면 아래 그림과 같다.

[\bar{y}* \pm 1.96\sqrt{var(y*)}]

3.4. GPR in the Real World

이전 Chapter에서 보았던 내용이 신뢰를 얻기 위해서는 사실 우리가 얼마나 공분산 함수를 잘 선택하느냐에 달려있다. $\theta = [l, \sigma_f, \sigma_n]$ 라는 Parameter 집합이 적절히 설정되어야만 결과가 합리적일 것이다.

$\theta$ 의 Maximum a Posteriori Estimate는 다음 식이 최댓값을 가질 때 찾을 수 있다.

[p(\theta \mathbf{x}, \mathbf{y})]

베이즈 정리에 따라 우리가 $\theta$ 에 대해 거의 아는 것이 없다고 가정할 때 우리는 다음과 같은 식을 최대화해야 한다.

[logp(\mathbf{y} \mathbf{x}, \theta) = - \frac{1}{2} \mathbf{y}^T K^{-1} \mathbf{y} - \frac{1}{2} log K - \frac{n}{2} log 2\pi]

다변량 최적화 알고리즘(예: Conjugate Gradients, Nelder-Mead simplex)을 이용하면 예를 들어 $l=1, \sigma_f=1.27$ 과 같은 좋은 값을 얻을 수 있다.

그런데 이건 그냥 단지 좋은 값 에 불과하다. 수많은 옵션 중에 딱 하나 좋은 답이 있으면 안되는가? 이 질문에 대한 답은 다음 장에서 찾을 수 있다.

좀 더 복잡한 문제에 대해 생각해보자. 아래와 같은 Trend를 갖는 데이터가 있다고 하자.

좀 더 복잡한 공분한 함수가 필요할 것 같다.

[k(x, x\prime) = \sigma_{f_1}^2 exp(- \frac{( x - x\prime)^2}{2l_1^2} ) + \sigma_{f_2}^2 exp(- \frac{( x - x\prime)^2}{2l_2^2} ) + \sigma_n^2 \delta(x, x\prime)]

위 식의 우항에서 첫 번째 부분은 예측변수의 작은 변동을 잡아내기 위함이고, 두 번째 부분은 좀 더 긴 기간 동안의 변동성을 포착하기 위해 설계되었다. ( $l_2 \approx 6l_1$ )

이 공분산 함수는 $K$ 가 positive definite이기만 하면 복잡한 데이터에 적합하게 무한대로 확장할 수 있다.

그런데 이 함수가 정말 시간적 흐름을 포착할 수 있을까? 보완을 위해 새로운 항을 추가해보자.

[k(x, x\prime) = \sigma_{f}^2 exp(- \frac{( x - x\prime)^2}{2l^2} ) + exp( -2sin^2[\nu \pi (x-x\prime)] ) + \sigma_n^2 \delta(x, x\prime)]

우항의 첫 부분은 마찬가지로 장기간의 트렌드를 포착하기 위해 설계된 부분이고, 두 번째 부분은 빈도를 나타내는 $\nu$ 와 함께 periodicity를 반영하게 된다. 위에서 살펴본 그림의 검은 실선이 위 공분산 함수를 이용하여 적합한 것이다.


4. Fitting Gaussian Process with Python

베이지안 방법론을 위한 대표적인 라이브러리로 PyMC3가 있지만 본 글에서는 scikit-learn 라이브러리를 이용하겠다.

회귀 문제에서는 공분산 함수(kernel)를 명시함으로써 GaussianProcessRegressor를 사용할 수 있다. 이 때 적합은 주변 우도의 로그를 취한 값을 최대화하는 과정을 통해 이루어진다. 이 Class는 평균 함수를 명시할 수 없는데, 왜냐하면 평균 함수는 0으로 고정되어 있기 때문이다.

분류 문제에서는 GaussianProcessClassifier를 사용할 수 있을 것이다. 언뜻 생각하면 범주형 데이터를 적합하기 위해 정규 분포를 사용하는 것이 이상하다. 이는 Latent Gaussian Response Variable을 사용한 뒤 이를 unit interval(다중 분류에서는 simplex interval)로 변환하는 작업을 통해 해결할 수 있다. 이 알고리즘의 결과는 일반적인 머신러닝 알고리즘에 비해 부드럽고 확률론적인 분류 결과를 반환한다. (이에 대한 자세한 내용은 Reference에 있는 2번째 논문의 7페이지를 참조하길 바란다.)

GP의 Posterior는 정규분포가 아니기 때문에 Solution을 찾기 위해 주변 우도를 최대화하는 것이 아니라 Laplace 근사를 이용한다.

이제부터 아주 간단한 예를 통해 라이브러리를 사용하는 법에 대해 소개하겠다. 본 내용은 scikit-learn 라이브러리 홈페이지에서 확인할 수 있다.

아래와 같은 함수를 추정하는 것이 우리의 목표이다.

import numpy as np

# X, y는 학습 데이터

def f(x):
    """The function to predict."""
    return x * np.sin(x)

X = np.linspace(0.1, 9.9, 20)
X = np.atleast_2d(X).T

# Observations and noise
y = f(X).ravel()
dy = 0.5 + 1.0 * np.random.random(y.shape)
noise = np.random.normal(0, dy)
y += noise

공분산 함수(kernel)를 정의하고 GPR 적합을 시작한다. 본 예시에는 kernel을 구성할 때의 Hyperparameter 최적화에 대한 내용은 포함되어 있지 않다.

from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import WhiteKernel, ConstantKernel as C, RBF

kernel = C(1.0, (1e-3, 1e3)) * RBF(10, (1e-2, 1e2))
gp = GaussianProcessRegressor(kernel=kernel,
                              n_restarts_optimizer=9,
                              optimizer='fmin_l_bfgs_b',
                              random_state=0)
gp.fit(X, y)

아주 드넓은 공간에서 함수 추정을 해보자.

x = np.atleast_2d(np.linspace(0, 10, 1000)).T
y_pred, sigma = gp.predict(x, return_std=True)

plt.figure()
plt.plot(x, f(x), 'r:', label=r'$f(x) = x\,\sin(x)$')
plt.errorbar(X.ravel(), y, dy, fmt='r.', markersize=10, label='Observations')
plt.plot(x, y_pred, 'b-', label='Prediction')
plt.fill(np.concatenate([x, x[::-1]]),
         np.concatenate([y_pred - 1.9600 * sigma,
                        (y_pred + 1.9600 * sigma)[::-1]]),
         alpha=.5, fc='b', ec='None', label='95% confidence interval')
plt.xlabel('$x$')
plt.ylabel('$f(x)$')
plt.ylim(-10, 20)
plt.legend(loc='upper left')
plt.show()

Reference

1) GP 논문1
2) GP 논문2
3) GP 설명 블로그
4) scikit-learn 홈페이지
5) PyMC3 홈페이지

Comment  Read more

Weight & Biases(wandb) 사용법(wandb 설치 및 설명)

|

이번 글에서는 Weight & Biases라고 하는, machine learning을 위한 개발 tool을 소개하고자 한다.

Tensorflow의 Tensorboard와 비슷한데, 이 도구는 tensorflow나 pytorch 등 여러 flatform에서 사용가능한 것이 특징이다. Dashboard, Sweeps, Artifacts 기능을 지원한다.
이 글에서는 PyTorch를 기준으로 설명한다. 그러나, Tensorflow에서의 사용법도 크게 다르지 않으니 참고하자.


초기 설정

먼저 홈페이지에 들어가서 회원가입을 하자. Google이나 Github ID로 가입할 수 있다.

그리고 wandb library를 설치한다.

pip install wandb

다음으로 github 로그인할 때처럼 wandb에도 로그인을 해야 한다. 명령창에 다음을 입력하자.

wandb login

# 결과:
wandb: You can find your API key in your browser here: https://app.wandb.ai/authorize
wandb: Paste an API key from your profile and hit enter:

해당 링크를 들어가서 API key를 복사한 다음 명령창에 붙여넣기하자.

그럼 로그인이 완료된다.

Successfully logged in to Weights & Biases!

Quickstart

Keras

다음 tutorial을 참고해 readme를 따라 그대로 실행해 보자.

git clone http://github.com/cvphelps/tutorial
cd tutorial
pip install -r requirements.txt
wandb signup # 이렇게 해도 가입 가능하다.
wandb init

그러면 현재 프로젝트를 설정할 수 있다. 맨 처음에는 아무 프로젝트도 없기 때문에 프로젝트 이름을 설정하고 새로 만들 수 있다.

이미 실행한 적이 있다면 프로젝트 목록 중에서 하나를 선택할 수 있다.

그리고 홈페이지를 확인해보면 프로젝트가 하나 생긴 것을 확인할 수 있다.

이제 튜토리얼을 따라 실행해 보자.

python tutorial.py
# 결과:

wandb: Tracking run with wandb version 0.9.1
wandb: Run data is saved locally in wandb/run-20200610_071808-2yir0lw7
wandb: Syncing run fiery-river-1
wandb: View project at https://app.wandb.ai/greeksharifa/wandb-tutorial
wandb: View run at https://app.wandb.ai/greeksharifa/wandb-tutorial/runs/2yir0lw7
wandb: Run `wandb off` to turn off syncing.

Train on 10047 samples, validate on 10000 samples
Epoch 1/8
10047/10047 [==============================] - 2s 235us/step - loss: 0.9085 - accuracy: 0.6605 - val_loss: 0.5802 - val_accuracy: 0.7800
Epoch 2/8
10047/10047 [==============================] - 2s 224us/step - loss: 0.5756 - accuracy: 0.7850 - val_loss: 0.5094 - val_accuracy: 0.8113
...
Epoch 8/8
10047/10047 [==============================] - 4s 379us/step - loss: 0.3548 - accuracy: 0.8686 - val_loss: 0.3881 - val_accuracy: 0.8606

wandb: Waiting for W&B process to finish, PID 15848
wandb: Program ended successfully.
wandb: Run summary:
wandb:      _timestamp 1591773513.0287454
wandb:           epoch 7
wandb:           _step 7
wandb:            loss 0.3548142489680274
wandb:    val_accuracy 0.8605999946594238
wandb:        _runtime 24.4707293510437
wandb:        accuracy 0.8686174750328064
wandb:        val_loss 0.3880709020137787
wandb:   best_val_loss 0.3880709020137787
wandb:      best_epoch 7
wandb: Syncing 5 W&B file(s), 9 media file(s), 0 artifact file(s) and 1 other file(s)
wandb:                                                                                
wandb: Synced fiery-river-1: https://app.wandb.ai/greeksharifa/wandb-tutorial/runs/2yir0lw7

그러면 이제 프로젝트 내에서 임의로 지정된 실행 이름으로 클라우드에 동기화가 된다. 브라우저에서 확인해보면 1 run이라고 표시된 것을 볼 수 있다. 눌러보자.

그럼 대충 위와 같은 화면이 나온다. 편리하다

사실 위의 코드는 keras를 사용한 것이다. PyTorch를 살펴보자.

PyTorch

예시로 Pytorch tutorial 중 mnist classification 모델을 가져와서 설명한다. 링크에서 git clone하여 받아온 후 mnist 디렉토리에서 작업을 시작하자.

원래 코드는 다음과 같다.

from __future__ import print_function
import argparse
...


class Net(nn.Module):
    ...


def train(args, model, device, train_loader, optimizer, epoch):
    ...


def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            ...

    test_loss /= len(test_loader.dataset)

    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))


def main():
    # Training settings
    parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
    ...
    args = parser.parse_args()
    use_cuda = not args.no_cuda and torch.cuda.is_available()

    torch.manual_seed(args.seed)

    ...

    model = Net().to(device)
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)

    ...
    if args.save_model:
        torch.save(model.state_dict(), "mnist_cnn.pt")


if __name__ == '__main__':
    main()

wandb를 사용하기 위해 import하자.

import wandb

main() 함수의 맨 앞부분에 다음 코드를 추가한다.

wandb.init()

args 변수 선언부 밑에 다음 코드를 추가한다.

wandb.config.update(args)

model 선언부 다음에 다음 코드를 추가한다.

wandb.watch(model)

이제 test() 함수를 다음과 같이 바꿔주자.

전체 코드는 다음과 같다.

from __future__ import print_function
import argparse
...
import wandb

class Net(nn.Module):
    ...


def train(args, model, device, train_loader, optimizer, epoch):
    ...

def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0

    example_images = []
    with torch.no_grad():
        for data, target in test_loader:
            ...

    test_loss /= len(test_loader.dataset)

    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))
    wandb.log({
        "Examples": example_images,
        "Test Accuracy": 100. * correct / len(test_loader.dataset),
        "Test Loss": test_loss})


def main():
    wandb.init()
    # Training settings
    parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
    ...
    args = parser.parse_args()
    use_cuda = not args.no_cuda and torch.cuda.is_available()

    wandb.config.update(args)
    ...
    
    model = Net().to(device)
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)
    wandb.watch(model)
    ...

    if args.save_model:
        torch.save(model.state_dict(), "mnist_cnn.pt")


if __name__ == '__main__':
    main()

그리고 run 해보자.

python main.py

명령창 마지막에 표시된 링크를 타고 가면 다음과 같이 깔끔하게 표시되는 화면을 볼 수 있다.

프로젝트에 wandb를 추가하는 방법은 대략 위와 같다.

이제 PyCharm 등에서 working tree를 살펴보면 wandb 디렉토리가 생성되어 있고, 여기에 log들이 저장되고 동시에 cloud에도 동기화된다. 내부에는 한 번의 실행당 하나의 sub-디렉토리가 있다.

이제 자세한 설정 방법들을 알아보자.


wandb.init()

명령창에서 wandb init을 실행하거나, python 코드 안에 wandb.init()을 추가하면, 현재 실행하는 프젝트를 처음에 지정해 줄 수 있다.

import wandb
wandb.init(project="project-name", reinit=True)

reinit=True 옵션을 주면 실행 시에 init()을 다시 수행한다.

만약 실행 시 LaunchError: Permission denied라는 에러를 본다면 wandb 로그인을 하지 않은 것이다. 여기를 참조하자.

실행 이름 설정

아무 것도 설정하지 않았을 때, 프로젝트 이름 내에서 매 실행당 생성되는 이름은 임의로 지정된다(ex. fiery-river-1, true-eon-2). 실행 이름을 설정하려면 다음과 같이 한다.

import wandb
wandb.init()
wandb.run.name = 'your-run-name'
# generted run ID로 하고 싶다면 다음과 같이 쓴다.
# wandb.run.name = wandb.run.id
wandb.run.save()

오프라인에 로그 기록

만약 인터넷이 연결되지 않는다면 오프라인으로 저장할 수 있다. 코드 맨 앞에 다음을 넣자.

import wandb
import os

os.environ["WANDB_API_KEY"] = YOUR_KEY_HERE
os.environ["WANDB_MODE"] = "dryrun"

YOUR_KEY_HERE에다가 authorize 페이지에서 볼 수 있는 key를 복붙해주자.

그러면 오프라인에 로그가 기록된다. 나중에 온라인에 동기화하고 싶다면 명령창에 다음을 입력한다.

wandb sync wandb/dryrun-folder-name

wandb.config

config를 wandb에 넣어둘 수 있다.

간단히는 다음과 같이 할 수 있다.

wandb.config.epochs = 4
wandb.config.batch_size = 32
# you can also initialize your run with a config
wandb.init(config={"epochs": 4})

효율적으로 쓰고자 하면 다음과 같이 dictionary로 넣어주면 된다.

wandb.init(config={"epochs": 4, "batch_size": 32})

wandb config를 새로 지정하거나, parameter를 일부 또는 전부를 업데이트하려면 다음과 같이 쓸 수 있다.

wandb.config.update({"epochs": 4, "batch_size": 32})

여러분이 python code에서 argparse를 쓰고 있다면 다음 흐름이 적절하다.

wandb.init()
wandb.config.epochs = 4

parser = argparse.ArgumentParser()
parser.add_argument('-b', '--batch-size', type=int, default=8, metavar='N',
                     help='input batch size for training (default: 8)')
args = parser.parse_args()
wandb.config.update(args) # adds all of the arguments as config variables

Tensorflow 등의 다른 흐름은 여기를 참고하자.


wandb.log(dict)

이미지나, accuracy, test_loss 등의 로그를 기록하고 싶다면 wandb.log()를 쓰자.

간단하게 loss 등의 로그를 보고 싶다면 코드에 다음과 같은 형식으로 추가해 주면 된다. 인자는 dictionary type이다.

wandb.log({
        "Test Accuracy": 100. * correct / len(test_loader.dataset),
        "Test Loss": test_loss})

Histogram

wandb.log({"gradients": wandb.Histogram(numpy_array_or_sequence)})
wandb.run.summary.update({"gradients": wandb.Histogram(np_histogram=np.histogram(data))})

Image

이미지는 numpy array나 PIL 등으로 전달할 수 있다. numpy array는 회색조면 마지막 차원은 1, RGB면 3, RGBA이면 4이다.

wandb.log({"examples": [wandb.Image(numpy_array_or_pil, caption="Label")]})
# or
example_images.append(wandb.Image(
                data[0], caption="Pred: {} Truth: {}".format(pred[0].item(), target[0])))
wandb.log({"Examples": example_images})

참고 사이트 목록:

  • https://docs.wandb.com/library/log
  • https://app.wandb.ai/stacey/deep-drive/reports/Image-Masks-for-Semantic-Segmentation–Vmlldzo4MTUwMw
  • https://colab.research.google.com/drive/1SOVl3EvW82Q4QKJXX6JtHye4wFix_P4J#scrollTo=I7sKQuBBgFZ_

Media

wandb.log({"examples": [wandb.Audio(numpy_array, caption="Nice", sample_rate=32)]})

matplotlib.pyplot

matplotlib으로 그릴 수 있는 custom plot들도 wandb log에 기록할 수 있다.

import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4])
plt.ylabel('some interesting numbers')
wandb.log({"chart": plt})

그런데 이 때 wandb의 내부 동작 과정에서 matplotlib의 제거된 method를 사용하는 경우 에러가 발생하는데, 이 때는 다음과 같이 Image를 이용해주면 된다.

import matplotlib as plt
import seaborn

fig, ax = plt.subplots(figsize=(12, 12))
sns.scatterplot(
    x="x", y="y", hue=df.label.tolist(), legend="full",
    palette="Paired_r",
    data=df)

wandb.log({'plot': wandb.Image(fig)})

wandb를 사용하는 예제는 여기에 많으니 참고하자.

Comment  Read more

Logistic Matrix Factorization 설명

|

본 글에서는 2014년에 Spotify에서 소개한 알고리즘인 Logistic Matrix Factorization에 대해 설명할 것이다. 먼저 논문 리뷰를 진행한 후, Implicit 라이브러리를 통해 학습하는 과정을 소개할 것이다. 논문 원본은 이곳에서 확인할 수 있다.


1. Logistic Matrix Factorization for Implicit Feedback Data 논문 리뷰

웹 상에서 얻을 수 있는 데이터는 대부분 암시적 피드백의 형태이기 때문에, 협업 필터링(Collaborative Filtering) 방법론에서도 이러한 암시적인 경우에 대응할 수 있는 알고리즘의 필요성이 대두되었다. 본 모델은 암시적 피드백에 적합한 새로운 확률론적 행렬 분해 기법인 LMF를 소개한다.

(전략)

1.1. Problem Setup and Notation

암시적 피드백은 클릭, 페이지 뷰, 미디어 스트리밍 수 등을 예로 들 수 있는데, 모든 피드백은 non-negative의 값을 가지게 된다. 기본 기호는 아래와 같다.

기호 설명
$U = (u_1, …, u_n)$ n명의 User
$I = (i_1, …, i_n)$ m개의 Item
$R = (r_{ui})_{n \times m}$ User-Item 관측값 행렬
$r_{ui}$ User $u$가 Item $i$와 몇 번 상호작용 했는지(예: 구매횟수)
(중략)

1.2. Logistic MF

$f$를 잠재 벡터의 차원이라고 할 때, 관측값 행렬 $R$은 $X_{n \times f}, Y_{m \times f}$라는 2개의 행렬로 분해될 수 있다. 이 때 $X$의 행은 User의 잠재 벡터를 의미하고, $Y$의 행은 Item의 잠재 벡터를 의미한다. 이전의 방법에서는 weighted RMSE를 최소화하는 방법으로 행렬 분해를 진행했는데, 본 논문에서는 확률론적인 접근법을 시도하였다.

$l_{u, i}$를 User $u$가 Item $i$와 상호작용하기로 결정한 사건이라고 하자. 이 때 우리는 이러한 사건이 일어날 조건부 확률의 분포가 User와 Item의 잠재 벡터와 그에 상응하는 bias의 내적의 합이 parameter의 역할을 하는 Logistic Function에 따라서 결정되는 것으로 생각할 수 있다.

[p(l_{ui} x_u, y_i, \beta_i, \beta_j) = \frac{exp(x_u y^T_i + \beta_u + \beta_i)}{1 + exp(x_u y^T_i + \beta_u + \beta_i)}]

$\beta$항은 물론 bias를 나타내며, User와 Item 각각의 행동 분산을 의미하게 된다. $r_{ui}$가 0이 아닐 때 이를 positive feedback으로, 0일 때를 negative feedback으로 간주하자. 이 때 우리는 Confidence를 정의할 수 있는데, 이를 $c = \alpha r_{ur}$로 표현할 수 있다. 이 때 $\alpha$는 Hyperparameter이다. $\alpha$를 크게하면 할수록, Positive Feedback에 더욱 큰 가중치를 부여하게 된다. $c$는 Log를 활용하여 다음과 같이 표현할 수도 있다.

[c = 1 + \alpha log(1 + r_{ui}/\epsilon)]

$R$의 모든 원소가 서로 독립적이라는 가정하게 Parameter인 $X, Y, \beta$가 주어졌을 때 관측값 행렬 $R$의 우도는 아래와 같이 표현할 수 있다.

[\mathcal{L}(R X,Y,\beta) = \prod_{u,i} p(l_{ui} x_u, y_i, \beta_u, \beta_i)^{\alpha r_{ui}} ( 1 - p(l_{ui} x_u, y_i, \beta_u, \beta_i))]

추가적으로, 우리는 학습 과정 상의 과적합을 막기 위해 User와 Item의 잠재 벡터에 0이 평균인 spherical Gaussian Prior를 부여한다.

[p(X \sigma^2) = \prod_u N(x_u 0, \sigma^2_uI)]
[p(Y \sigma^2) = \prod_i N(y_i 0, \sigma^2_iI)]

이제, Posterior에 Log를 취하고 상수항을 scaling parameter인 $\lambda$로 대체해주면 아래와 같은 식을 얻을 수 있다.

[log p(R X,Y,\beta) = \sigma_{u,i} \alpha r_{ui}(x_u y^T_i + \beta_u + \beta_i) - (1 + \alpha r_{ui}) log(1 + exp(x_u y^T_i + \beta_u + \beta_i)) - \frac{\lambda}{2} \Vert{x_u}\Vert^2 - \frac{\lambda}{2} \Vert{y_i}\Vert^2]

잠재벡터에 대한 0이 평균인 spherical Gaussian Prior는 단지 User와 Item 벡터에 대한 $l2$ 정규화를 의미한다. 이제 우리의 목표는 Log Posterior를 최대화하는 $X, Y, \beta$를 찾는 것이다.

[argmax_{X,Y,\beta} log p (X,Y,\beta R)]

위에서 제시된 목적 함수의 Local Maximum은 Alternating Gradient Ascent 과정을 거치면 찾을 수 있다. 각 Iteration에서 한 번은 User 벡터와 bias를 고정하고 Item 벡터에 대한 gradient를 업데이트하고, 그 다음에는 반대로 업데이트를 수행한다. User 벡터와 Bias의 편미분은 아래와 같다.

각 Iteration은 User와 Item의 수에 선형적인데, 만약 선형적 계산이 불가능한 상황이라면, 적은 수의 Negative Sample($r_{ui} = 0$)를 샘플링하고 이에 따라 $\alpha$를 감소시키는 방법을 쓸 수 있다.

이는 계산 시간을 굉장히 줄이면서도 충분히 최적점에 근접할 수 있는 장점을 지닌다. 또한 Adagrad 알고리즘을 활용할 경우 학습 시간을 획기적으로 줄이면서도 빠르게 수렴 지점에 도달할 수 있다. $x_u^t$를 Iteration $t$에서의 $x_u$의 값으로, $g_{x_u}^t$를 Iteration $t$에서의 $x_u$의 Gradient라고 할 때, $x_u$에 대하여 Iteration $t$에서 우리는 아래와 같이 Adagrad 알고리즘을 수행할 수 있다.

[x_u^t = x_u^{t-1} + \frac{\gamma g_u^{t-1}}{\sqrt{\sum_{t=1}^{t-1} g_u^{t^{2}} }}]

1.3. Scaling Up

Alternating Gradient Descent의 각 Iteration은 모든 잠재 벡터에 대해 Gradient를 계산하고, 그 Gradient의 양의 방향으로 업데이트하는 과정을 포함하게 된다. 각 Gradient는 단일 User와 Item에 의존하는 함수의 집합의 합을 포함하게 된다. 이러한 합의 식은 병렬적으로 수행될 수 있고, MapReduce 프로그래밍 패러다임에 적합한 방법이다.

계산 향상을 위해 본 모델은 다른 잠재 요인 모델에서 사용된 것과 유사한 sharding 테크닉을 사용하였다. 먼저 $R$을 $K \times L$ 블록으로 나눈다. 그리고 $X$는 $K$개의 블록으로, $Y$는 $L$개의 블록으로 나눈다. 병렬 요인인 $K$와 $L$은 메모리에 맞게 설정할 수 있다. Map 단계에서 같은 블록에 있는 모든 $r_{ui}, x_u, y_i$를 같은 mapper에 할당한다. 각 $u, i$ 쌍에 대해 아래 식을 병렬적으로 수행한다.

Reduce 단계에서는, 만약 $u$에 대한 반복을 수행하고 있다면, $u$를 key off하여 같은 User $u$에게 매핑되는 각 $v_{ui}, b_{ui}$가 같은 reducer에게 보내지도록 한다. 편미분이 계산되면 $x_u$와 $\beta_u$를 1.2. 절의 마지막 부분에 나온 식에 따라 업데이트 한다.


2. Implicit 라이브러리를 활용한 학습

Implicit 라이브러리는 이곳에서 확인할 수 있다. 본 장은 간략하게 메서드를 사용하는 방법에 대해서만 소개할 것이다. 학습의 자세한 부분에 대해서는 이전 글을 참조하길 바란다. 기본적인 틀은 유사하다.

LMF 알고리즘을 사용하기 위해서는 Sparse Matrix를 생성해주어야 한다. scipy 라이브러리를 통해 Sparse Matrix를 만든 후에는 간단하게 fit 메서드를 통해 적합해주면 된다.

현재로서는 GPU 학습은 지원하지 않는다. LMF를 학습할 때 조심해야 할 점은, 수렴 속도가 빨라 잠재 벡터의 수가 크고 learning_rate가 클 수록 반복 횟수가 일정 수준 이상일 때 잠재 벡터의 원소가 NaN이 되는 경우가 잦다는 것이다. 적절한 Hyper parameter 튜닝을 통해 이러한 경우를 조심해야 한다.

# 별 거 없다.
from implicit.lmf import LogisticMatrixFactorization

# 학습
lmf = LogisticMatrixFactorization(factors, learning_rate, regularization,
                                  iterations, neg_prop, num_thread, random_state)


# 잠재 벡터 얻기
item_factors = lmf.item_factors()
user_factors = lmf.user_factors()

# 유사한 Item/User과 Score 리스트 얻기
# Input은 Integer
similar_items() = lmf.similar_items(itemid)
similar_users() = lfm.similar_users(userid)
Comment  Read more

LightFM 설명

|

본 글에서는 2015년에 Lyst에서 발표한 Hybrid Matrix Factorization Model인 LightFM에 관한 내용을 다룰 것이며 순서는 아래와 같다.

1) 논문 요약 리뷰
2) LightFM 라이브러리 사용법 소개
3) HyperOpt를 이용한 Hyperparameter 튜닝법 소개

1. Metadata Embeddings for User and Item Cold-start Recommendations 논문 리뷰

1.1. Introduction

cold-start 상황에서 추천 시스템을 만드는 것은 아직까지도 쉽지 않은 일이다. 기본적인 행렬 분해(Matrix Factorization) 기법들은 이러한 상황에서 형편 없는 성능을 보여준다. 왜냐하면 Collaborative Interaction 데이터가 희소할 때는 User와 Item의 잠재 벡터를 효과적으로 추정하는 일이 굉장히 어렵기 때문이다.

Content-based 방법은 메타데이터를 통해 Item이나 User를 표현(Represent)한다. 이러한 정보는 미리 알고 있기 때문에 Collaborative 데이터가 존재하지 않아도 추천 로직은 성립할 수 있다. 그러나 이러한 모델에서는 Transfer Learning은 불가능하다. 왜냐하면 각 User는 독립적으로 추정되기 때문이다. 결과적으로 CB 모델은 Collaborative 데이터가 이용 가능하고 각 User에 대해 많은 양의 데이터를 필요로 할 때, 기존 행렬 분해 모델보다 더 안좋은 성능을 보인다.

패션 온라인 몰인 Lyst에서는 이러한 문제를 해결하는 것이 매우 중요했다. 매일 같이 수만 개의 상품이 등록되고, 웹 상에는 800만 개가 넘는 패션 아이템이 등록되어 있었기 때문이다. 많은 Item, 새로운 상품의 잦은 등록(Cold-Start), 고객의 다수가 신규 고객(Cold-Start)라는 3가지의 어려운 조건 속에서, 본 논문은 LightFM이라는 Hybrid형 모델을 제시한다.

본 모델은 Content-based와 Collaborative Filtering의 장점을 결합하였다. 본 모델의 가장 중요한 특징은 아래와 같다.

1) 학습데이터에서 Collaborative 데이터와 User/Item Feature를 모두 사용한다.
2) LightFM에서 생성된 Embedding 벡터는 feature에 대한 중요한 의미 정보를 포함하고 있고, 이는 tag 추천과 같은 일에서 중요하게 사용될 수 있다.

1.2. LightFM

모델 구성 자체는 어렵지 않다. 가장 특징적인 것은 기존의 Classic한 행렬 분해 모델들과 다르게, User Feature와 Item Feature를 학습 과정에 포함하는 데에 적합한 구조로 만들어져 있다는 것이다.

잠시 기호에 대해 설명하겠다.

기호 설명
$U$ User의 집합
$I$ Item의 집합
$F^U$ User Feature의 집합
$F^I$ Item Feature의 집합
$f_u$ $u$라는 User의 features, $f_u \subset F^U$
$f_i$ $i$라는 Item의 features, $f_i \subset F^I$
$e_f^U$ $f_u$의 각 User feature들에 대한 d-차원 Embedding 벡터
$e_f^I$ $f_i$의 각 Item feature들에 대한 d-차원 Embedding 벡터
$b_f^U$ $u$라는 User의 features, $f_u \subset F^U$
$b_f^I$ $i$라는 Item의 features, $f_i \subset F^I$

User $u$에 대한 잠재 벡터는 그 User의 Features의 잠재 벡터들의 합으로 구성되며, Item 또한 같은 방식으로 계산한다. Bias 항 또한 아래와 같이 계산된다.

\(q_u = \sum_{j \in f_u}e_j^U\) \(p_i = \sum_{j \in f_i}e_j^I\) \(b_u = \sum_{j \in f_u}b_j^U\) \(b_i = \sum_{j \in f_i}b_j^I\)

User $i$와 Item $i$에 대한 모델의 예측 값은, 이 User와 Item의 Representation(잠재 벡터)의 내적으로 이루어진다.

[\hat{r}_{ui} = sigmoid(q_u \odot p_i + b_u + b_i)]

최적화 목적함수는 parameter들이 주어졌을 때의 데이터에 대한 우도를 최대화 하는 것으로 설정된다. 이는 아래와 같다.

[L(e^U, e^I, b^U, b^I) = \prod_{(u,i) \in S^+} \hat{r}{ui} \times \prod{(u,i) \in S^-} (1- \hat{r}_{ui})]

여기서 $S^+$는 Positive Interaction, $S^-$는 Negative Interaction을 가리킨다.

이 식들만 봐서는 모델의 구조에 대해 완벽히 이해를 하지 못할 수도 있다. 아래 그림을 보면 이해가 될 것이다.

위 그림의 경우, User Feature를 예시로 든 것이고, Item Feature에 대해서도 같은 논리가 적용된다. $m$은 User의 수이다.

지금까지 논문에서 소개된 모델에 대해 알아보았다. Experiment 부분은 직접 읽어보도록 하고, 이제는 코드로 넘어가도록 하겠다.


2. LightFM 학습 및 HyperOpt를 활용한 Bayesian Optimization

2.1. Data Preparation

학습에 사용될 데이터는 Goodbook 데이터이다. 이 데이터셋에는 여러 독자(User)가 책(Item)에 대해 평점을 남긴 데이터이다. 사실 Implicit Feedback이 아닌 Explicit Feedback이기에 학습이 더욱 쉬울 수는 있지만, 그 부분은 잠시 접어두기로 하자. 데이터는 이곳에서 직접 다운로드할 수 있다.

학습에 사용한 파일은 ratings.csv와 books.csv인데, 아래와 같은 형상을 지녔다.

# ratings.csv
   user_id  book_id  rating
0        1      258       5
1        2     4081       4
2        2      260       5
3        2     9296       5
4        2     2318       3

# books.csv
   book_id                      authors  average_rating        original_title
0        1              Suzanne Collins            4.34      The Hunger Games
1        2  J.K. Rowling, Mary GrandPré            4.44      Harry Potter ...
2        3              Stephenie Meyer            3.57              Twilight
3        4                   Harper Lee            4.25 To Kill a Mockingbird
4        5          F. Scott Fitzgerald            3.89      The Great Gatsby

이 데이터를 그대로 LightFM에 Input으로 넣을 수는 없다. 다소 귀찮은 전처리 과정을 거쳐야 한다.

import pandas as pd
from lightfm.data import Dataset
from scipy.io import mmwrite

# Data Load
# ratings_source: build_interactions 재료, list of tuples
# --> [(user1, item1), (user2, item5), ... ]
# item_features_source: build_item_features 재료
# --> [(item1, [feature, feature, ...]), (item2, [feature, feature, ...])]
ratings = pd.read_csv('data/ratings.csv')
ratings_source = [(ratings['user_id'][i], ratings['book_id'][i]) for i in range(ratings.shape[0])]

item_meta = pd.read_csv('data/books.csv')
item_meta = item_meta[['book_id', 'authors', 'average_rating', 'original_title']]

item_features_source = [(item_meta['book_id'][i],
                        [item_meta['authors'][i],
                         item_meta['average_rating'][i]]) for i in range(item_meta.shape[0])]

코드를 보면 알 수 있겠지만, ratings_souceitem_features_source라는 iterable 객체가 필요하다. 먼저 전자는 LightFM Dataset clss의 build_interactions 메서드의 재료로 활용되며, 후자의 경우 build_item_features의 재료가 된다. 본 학습에서는 User Feature를 따로 사용하지는 않았지만, Item Feature와 사용법이 동일하니, 참고해두면 되겠다.

이렇게 재료가 준비가 되었으면 LightFM의 Dataset 클래스를 불러온 후, fit을 해준다.

dataset = Dataset()
dataset.fit(users=ratings['user_id'].unique(),
            items=ratings['book_id'].unique(),
            item_features=item_meta[item_meta.columns[1:]].values.flatten()
            )

여기서 중요한 것은, 이 때 argument로 들어가는 객체에 결측값은 없어야 한다는 것이다.
이후 build를 해주면 데이터셋은 완성되었다.

interactions, weights = dataset.build_interactions(ratings_source)
item_features = dataset.build_item_features(item_features_source)

# Save
mmwrite('data/interactions.mtx', interactions)
mmwrite('data/item_features.mtx', item_features)
mmwrite('data/weights.mtx', weights)

# Split Train, Test data
train, test = random_train_test_split(interactions, test_percentage=0.1)
train, test = train.tocsr().tocoo(), test.tocsr().tocoo()
train_weights = train.multiply(weights).tocoo()

2.2. Hyper Parameter Optimization with HyperOpt

hyperopt는 꽤 오래 전부터 사용되던 Hyper Parameter 최적화 라이브러리이다. skopt도 널리 사용되고 있지만, 앞으로 업데이트가 계속 진행될 지 확실하지 않으므로… 본 글에서는 hyperopt를 소개하도록 하겠다.

먼저 Search Space를 정의해 주어야 한다.

from hyperopt import fmin, hp, tpe, Trials

# Define Search Space
trials = Trials()
space = [hp.choice('no_components', range(10, 50, 10)),
         hp.uniform('learning_rate', 0.01, 0.05)]

자세한 정보는 이곳에서 확인할 수 있다. space는 아래에서 소개할 objective 함수의 argument로 활용된다. space는 반드시 리스트로 작성할 필요는 없고, 필요에 따라 Dictionary나 OrderedDict 같은 객체를 사용해주면 좋다.

다음으로는 목적 함수를 정의해보자.

# Define Objective Function
def objective(params):
    no_components, learning_rate = params

    model = LightFM(no_components=no_components,
                    learning_schedule='adagrad',
                    loss='warp',
                    learning_rate=learning_rate,
                    random_state=0)

    model.fit(interactions=train,
              item_features=item_features,
              sample_weight=train_weights,
              epochs=3,
              verbose=False)

    test_precision = precision_at_k(model, test, k=5, item_features=item_features).mean()
    print("no_comp: {}, lrn_rate: {:.5f}, precision: {:.5f}".format(
      no_components, learning_rate, test_precision))
    # test_auc = auc_score(model, test, item_features=item_features).mean()
    output = -test_precision

    if np.abs(output+1) < 0.01 or output < -1.0:
        output = 0.0

    return output

일반적으로 위 함수의 반환 값은 loss가 되는데, 본 모델의 경우 loss를 직접 반환하는 메서드가 존재하지 않기 때문에 evaluation metric을 불러온 후, 이를 음수화하는 작업을 거쳤다.

이제는 fmin 함수를 불러와서 최적화 작업을 진행해보자.
max_evals 인자는 최대 몇 번 모델 적합을 진행할 것인가를 결정하며, timeout 인자를 투입할 경우 최대 search 시간을 제한할 수도 있다. best_params는 가장 좋은 Hyperparameter 조합에 관한 정보를 담은 Dictionary이다.

best_params = fmin(fn=objective, space=space, algo=tpe.suggest, max_evals=10, trials=trials)

2.3. 결과 확인

학습만 하고 끝낼 수는 없다. 학습이 끝난 모델을 활용하여 유사한 책(Item)에 대한 정보를 얻어보자. 유사도 측정은 코사인 유사도를 활용하였다.

# Find Similar Items
item_biases, item_embeddings = model.get_item_representations(features=item_features)

def make_best_items_report(item_embeddings, book_id, num_search_items=10):
    item_id = book_id - 1

    # Cosine similarity
    scores = item_embeddings.dot(item_embeddings[item_id])  # (10000, )
    item_norms = np.linalg.norm(item_embeddings, axis=1)    # (10000, )
    item_norms[item_norms == 0] = 1e-10
    scores /= item_norms

    # best: score가 제일 높은 item의 id를 num_search_items 개 만큼 가져온다.
    best = np.argpartition(scores, -num_search_items)[-num_search_items:]
    similar_item_id_and_scores = sorted(zip(best, scores[best] / item_norms[item_id]),
                                        key=lambda x: -x[1])

    # Report를 작성할 pandas dataframe
    best_items = pd.DataFrame(columns=['book_id', 'title', 'author', 'score'])

    for similar_item_id, score in similar_item_id_and_scores:
        book_id = similar_item_id + 1
        title = item_meta[item_meta['book_id'] == book_id].values[0][3]
        author = item_meta[item_meta['book_id'] == book_id].values[0][1]

        row = pd.Series([book_id, title, author, score], index=best_items.columns)
        best_items = best_items.append(row, ignore_index=True)

    return best_items


# book_id 2: Harry Potter and the Philosopher's Stone by J.K. Rowling, Mary GrandPré
# book_id 9: Angels & Demons by Dan Brown
report01 = make_best_items_report(item_embeddings, 2, 10)
report02 = make_best_items_report(item_embeddings, 9, 10)

해리포터와 마법사의 돌 그리고 천사와 악마, 이 두 권의 책과 유사한 책에 관한 정보를 확인해 보자.

# 해리포터와 마법사의 돌
book_id                                              title                        author     score
      2           Harry Potter and the Philosopher's Stone   J.K. Rowling, Mary GrandPré  1.000000
   5006                                         Blue Smoke                  Nora Roberts  0.768227
   1674                                   Prince of Thorns                Mark  Lawrence  0.767087
   1376                                     The Ugly Truth                   Jeff Kinney  0.761519
    418                                       Spirit Bound                 Richelle Mead  0.760111
   1577  Being Mortal: Medicine and What Matters in the...                  Atul Gawande  0.755845
   2230                                 The Black Cauldron               Lloyd Alexander  0.739562
   5776                                         Frog Music                 Emma Donoghue  0.739197
   2083                                  The Darkest Night                Gena Showalter  0.735191
   1262                                   Children of Dune                 Frank Herbert  0.735112

# 천사와 악마
book_id                                 title                                            author     score
      9                      Angels & Demons                                          Dan Brown  1.000000
    666                           Anansi Boys                                       Neil Gaiman  0.876268
   3687                       Lord of Misrule                                      Rachel Caine  0.869406
    504                                   NaN                                   Francine Rivers  0.859091
    308                Can You Keep a Secret?                                   Sophie Kinsella  0.847986
    971                                   NaN                   Marcus Pfister, J. Alison James  0.847010
    138                    The Scarlet Letter Nathaniel Hawthorne, Thomas E. Connolly, Nina ...  0.840049
    552                            The Rescue                                   Nicholas Sparks  0.834288
    208  The Immortal Life of Henrietta Lacks                                    Rebecca Skloot  0.834270
    503                 2001: A Space Odyssey                                  Arthur C. Clarke  0.812411

결과에 대해서는 독자의 판단에 맡기겠다.


Reference

1) LightFM 공식 문서 2) LigghtFM 관련 블로그 3) Hyperopt 깃헙

Comment  Read more

GitHub 사용법 - 09. Overall(Git 명령어 정리, Git 사용법)

|

저번 글에서는 Conflict에 대해서 알아보았다.
이번 글에서는, 전체 Git 명령어들의 사용법을 살펴본다.


명령어에 일반적으로 적용되는 규칙:

  • 이 글에서 <blabla>와 같은 token은 여러분이 알아서 적절한 텍스트로 대체하면 된다.
  • 각 명령에는 여러 종류의 옵션이 있다. ex) git log의 경우 --oneline, -<number>, -p 등의 옵션이 있다.
  • 각 옵션은 많은 경우 축약형이 존재한다. 일반형은 -가 2개 있으며, 축약형은 -가 1개이며 보통 첫 일반형의 첫 글자만 따온다. ex) --patch = -p. 축약형과 일반형은 효과가 같다.
  • 각 옵션의 순서는 상관없다. 명령의 필수 인자와 옵션의 순서를 바꾸어도 상관없다.
  • 각 명령에 대한 자세한 설명은 git help <command-name>으로 확인할 수 있다.
  • ticket branch는 parent branch로부터 생성되어, 어떤 특정 기능을 추가하고자 만든 실험적 branch라 생각하면 된다.

Working tree(작업트리) 생성

git init

빈 디렉토리나, 기존의 프로젝트를 git 저장소(=git repository)로 변환하고 싶다면 이 문단을 보면 된다.

일반적인 디렉토리(=git 저장소가 아닌 디렉토리)를 git working tree로 만드는 방법은 다음과 같다. 명령창(cmd / terminal)에서 다음을 입력한다.

git init

# 결과 예시
Initialized empty Git repository in blabla/sample_directory/.git/

그러면 해당 디렉토리에는 .git 이라는 이름의 숨김처리된 디렉토리가 생성된다. 이 디렉토리 안에 든 것은 수동으로 건드리지 않도록 한다.

참고) git init 명령만으로는 인터넷(=원격 저장소 = remote repository)에 그 어떤 연결도 되어 있지 않다. 여기를 참조한다.

git clone

인터넷에서 이미 만들어져 있는 작업트리를 본인의 컴퓨터(=로컬)로 가져오고 싶을 때에는 해당 git repository의 https://github.com/blabla.git 주소를 복사한 뒤 다음과 같은 명령어를 입력한다.

git clone <git-address>

# 명령어 예시 
git clone https://github.com/greeksharifa/git_tutorial.git

# 결과 예시
Cloning into 'git_tutorial'...
remote: Enumerating objects: 56, done.
remote: Total 56 (delta 0), reused 0 (delta 0), pack-reused 56
Unpacking objects: 100% (56/56), done.

그러면 현재 폴더에 해당 프로젝트 이름의 하위 디렉토리가 생성된다. 이 하위 디렉토리에는 인터넷에 올라와 있는 모든 내용물을 그대로 가져온다(.git 디렉토리 포함).
단, 다른 branch의 내용물을 가져오지는 않는다. 다른 branch까지 가져오려면 추가 작업이 필요하다.


Git Repository 연결

이 과정은 git clone으로 원격저장소의 로컬 사본을 생성한 경우에는 필요 없다.

먼저 github 등에서 원격 저장소(remote repository)를 생성한다.

로컬 저장소를 원격저장소에 연결하는 방법은 다음과 같다.

git remote add <remote-name> <git address>

# 명령어 예시
git remote add origin https://github.com/greeksharifa/git_tutorial.git

<remote-name>은 원격 저장소에 대한 일종의 별명인데, 보통은 origin을 쓴다. 큰 프로젝트라면 여러 개를 쓸 수도 있다.

이것만으로는 완전히 연결되지는 않았다. upstream 연결을 지정하는 git push -u 명령을 사용해야 수정사항이 원격 저장소에 반영된다.

연결된 원격 저장소 확인

git remote --verbose
git remote -v

# 결과 예시
origin  https://github.com/greeksharifa/git_tutorial.git (fetch)
origin  https://github.com/greeksharifa/git_tutorial.git (push)

git remote -v의 결과는 <remote-name> <git-address> <fetch/push>로 이루어져 있다.
(fetch)는 새 작업을 다운로드하는 장소이고, (push)는 새 작업을 업로드하는 장소이다.

원격 저장소의 이름만을 보거나, 해당 이름의 자세한 정보를 알고 싶다면 git remote show나, git remote show <remote-name>을 입력한다.

git remote show
---
git remote show origin

# 결과 예시
origin
---
* remote origin
  Fetch URL: https://github.com/greeksharifa/git_tutorial.git
  Push  URL: https://github.com/greeksharifa/git_tutorial.git
  HEAD branch: main
  Remote branches:
    2nd-branch    tracked
    3rd-branch    tracked
    fourth-branch tracked
    main          tracked
  Local branches configured for 'git pull':
    2nd-branch merges with remote 2nd-branch
    main       merges with remote main
  Local refs configured for 'git push':
    2nd-branch pushes to 2nd-branch (up to date)
    main       pushes to main     (local out of date)

해당 원격 저장소의 url은 무엇인지, 어떤 branch가 있는지, 로컬 branch는 원격 저장소의 어떤 branch와 연결되어 있는지 등을 확인할 수 있다.

원격 저장소 이름 변경

git remote rename <old-remote-name> <new-remote-name>

# 명령어 예시
git remote rename origin official

원격 연결 삭제

git remote remove <remote-name>

Git 설정하기

git 설정에는 계정 설정이나 변경 등이 있다. 그리고, 모든 git 설정은 2종류가 있다.

  1. 해당 컴퓨터의 모든 git 프로젝트에 적용되는 전역(global) 설정
    • Linux에서는 ~/.gitconfig 파일에 저장된다. 윈도우에서는 C:/Users/<user-name>/.gitconfig에 있다.
  2. 특정 프로젝트에만 적용되는 로컬(local) 설정
    • 해당 프로젝트 root directory의 .git/config 파일에 저장된다.

컴퓨터를 공유해서 쓰는 것이 아니라면 보통은 global 설정을 주로 다루게 될 것이다.

설정된 값 보기:

git config --get <setting-name>
git config --get user.name

# 모든 설정값 보기
git config --list

설정값 설정하기: 보통 자신의 계정명과 계정을 설정하게 될 것이다. 최초 로그인 창이 뜰 수 있다.

git config --global <setting-name> <value>

# 명령어 예시
git config --global user.name 'greeksharifa'
git config --global user.name 'greeksharifa@gmail.com'

전역 설정이 아닌 해당 프로젝트에만 적용시키고 싶다면 --global 대신 --local을 사용한다.

git 기본 에디터 변경

git의 기본 에디터는 Vim인데, 이를 변경할 수 있다. bash 등이 있다.

# 명령어 예시
git config --global core.editor mate -w
git config --global core.editor subl -n -w
git config --global core.editor '"C:\Program Files\Vim\gvim.exe" --nofork'

더 자세한 설정들은 git help config를 입력해서 찾아보자.


인증 정보 저장: Credential

SSH protocol을 사용하여 원격 저장소에 접근할 때는 암호를 매번 입력하지 않아도 되지만 HTTP protocol을 사용한다면 매번 인증 정보를 입력해야 한다.
하지만 git에는 이런 인증 정보(credential)을 저장해 둘 수 있다.

인증 정보를 임시로(cache) 저장하려면 다음을 사용한다. 기본적으로 15분간 임시로 저장하며, timeout 시간을 설정해 줄 수도 있다. 아래는 1시간(3600초) 기준이다.

git config --global credential.helper cache
git config --global credential.helper 'cache --timeout=3600'

임시가 아니라 계속 저장해 두려면 cache 대신 store를 사용한다. 저장할 파일을 지정할 수도 있다.

git config --global credential.helper store
git config --global credential.helper 'store --file <file-path>'

Git 준비 영역(index)에 파일 추가

로컬 저장소의 수정사항이 반영되는 과정은 총 3단계를 거쳐 이루어진다.

  1. git add 명령을 통해 준비 영역에 변경된 파일을 추가하는 과정(stage라 부른다)
  2. git commit 명령을 통해 여러 변경점을 하나의 commit으로 묶는 과정
  3. git push 명령을 통해 로컬 commit 내용을 원격 저장소에 올려 변경사항을 반영하는 과정

이 중 git add 명령은 첫 단계인, 준비 영역에 파일을 추가하는 것이다.

git add <filename1> [<filename2>, ...]
git add <directory-name>
git add *
git add --all
git add .

# 명령어 예시
git add third.py fourth.py
git add temp_dir/*

*은 와일드카드로 그냥 쓰면 변경점이 있는 모든 파일을 준비 영역에 추가한다(git add *). 특정 directory 뒤에 쓰면 해당 directory의 모든 파일을, *.py와 같이 쓰면 확장자가 .py인 모든 파일이 준비 영역에 올라가게 된다.
git add .을 현재 directory(.)의 모든 파일을 추가하는 명령으로 git add --all과 효과가 같다.

git add 명령을 실행하고 이미 준비 영역에 올라간 파일을 또 수정한 뒤 git status 명령을 실행하면 같은 파일이 Changes to be committed 분류와 Changes not staged for commit 분류에 동시에 들어가 있을 수 있다. 딱히 오류는 아니고 해당 파일을 다음 commit에 반영할 계획이면 한번 더 git add를 실행시켜주자.

한 파일 내 수정사항의 일부만 준비 영역에 추가

예를 들어 fourth.py를 다음과 같이 변경한다고 하자.

# 변경 전
print('hello')

print(1)

print('bye')

#변경 후
print('hello')
print('git')

print('bye')
print('20000')

이 중 print('bye'); print('20000')을 제외한 나머지 변경사항만을 준비 영역에 추가하고 싶다고 하자. 그러면 git add <filename> 명령에 다음과 같이 --patch 옵션을 붙인다.

git add --patch fourth.py
git add fourth.py -p

# 결과 예시
diff --git a/fourth.py b/fourth.py
index 13cc618..4c8cfb6 100644
--- a/fourth.py
+++ b/fourth.py
@@ -1,5 +1,5 @@
 print('hello')
+print('git')

-print(1)
-
-print('bye')
\ No newline at end of file
+print('bye')
+print('20000')
\ No newline at end of file
stage this hunk [y,n,q,a,d,s,e,?]? 

그러면 수정된 코드 덩이(hunk)마다 선택할지를 물어본다. 인접한 초록색(+) 덩이 또는 인접한 빨간색 덩이(-)가 하나의 코드 덩이가 된다.

각 옵션에 대한 설명은 다음과 같다. ?를 입력해도 도움말을 볼 수 있다.

Option Description
y stage this hunk
n do not stage this hunk
q quit; do not stage this hunk or any of the remaining ones
a stage this hunk and all later hunks in the file
d do not stage this hunk or any of the later hunks in the file
s split the current hunk into smaller hunks
e manually edit the current hunk
? print help

여기서는 y, y, n을 차례로 입력하면 원하는 대로 추가/추가하지 않을 수 있다. (영어 원문을 보면 알 수 있듯이 (stage) = (준비 영역에 추가하다)와 같은 의미라고 보면 된다.)

-p 옵션으로는 인접한 추가/삭제 줄들이 전부 하나의 덩이로 묶이기 때문에, 이를 더 세부적으로 하고 싶다면 위 옵션에서 e를 선택하면 된다.

git add -p 명령을 통해 준비 영역에 파일의 일부 변경사항만 추가하고 나면 같은 파일이 Changes to be committed 분류와 Changes not staged for commit 분류에 동시에 들어가게 된다.


Commit하기

준비 영역에 올라간 파일들의 변경사항을 하나로 묶는 작업이라 보면 된다. Git에서는 이 commit(커밋)이 변경사항 적용의 기본 단위가 된다.

git commit [-m “message”] [–amend]

기본적으로, commit은 다음 명령어로 수행할 수 있다.

git commit

# 결과 예시:
All text in first line will be showed at --oneline

Maximum length is 50 characters.
Below, is for detailed message.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Your branch is up to date with 'origin/main'.
#
# Changes to be committed:
#       modified:   .gitignore
#       new file:   third.py
#
~
~

git commit을 입력하면 vim 에디터가 열리면서 commit 메시지 편집을 할 수 있다. 방법은:

  • i를 누른다. insert의 약자이다.
  • 이후 메시지를 마음대로 수정할 수 있다. 이 때 규칙이 있는데,
    • 첫 번째 줄은 log를 볼 때 --oneline 옵션에서 나타나는 대표 commit 메시지이다. 기본값으로, 50자 이상은 무시된다.
    • 그 아래 줄에 쓴 텍스트는 해당 commit의 자세한 메시지를 포함한다.
    • 맨 앞에 #이 있는 줄은 주석 처리되어 commit 메시지에 포함되지 않는다.
  • 편집을 마쳤으면 다음을 순서대로 누른다. ESC, :wq, Enter.
    • ESC는 vim 에디터에서 명령 모드로 들어가가, :wq는 저장 및 종료 모드 입력을 뜻한다. 잘 모르겠으면 그냥 따라하라.
  • 맨 밑에 있는 물결 표시(~)는 파일의 끝이라는 뜻이다. 빈 줄도 아니다.

commit의 자세한 메시지를 작성하기 귀찮다면(별로 좋은 습관은 아니다.), 간단한 메시지만 작성할 수 있다:

git commit -m "<message>"

# 명령 예시:
git commit -m "hotfix for typr error"

물론 이미 작성한 commit 메시지를 변경할 수 있다.

git commit --amend

그러면 vim 에디터에서 수정할 수 있다.

원래는 git addgit commit을 하는 것이 일반적이지만, 모든 파일을 추가하면서 commit을 한다면 다음 단축 명령을 쓸 수 있다: -a 옵션을 붙인다.

git commit -a -m "<commit-message>"

수정사항을 원격저장소에 반영하기: git push

upstream 연결

git remote add 명령으로 원격저장소를 연결했으면 git push <git-address> 명령으로 로컬 저장소의 commit을 원격 저장소에 반영할 수 있다. 즉, 최종 반영이다.

git push <git-address>
git push https://github.com/greeksharifa/gitgitgit.git

# 결과 예시
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 200 bytes | 200.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/greeksharifa/gitgitgit.git
 * [new branch]      main -> main

그러나 매번 git address를 인자로 주어가며 변경사항을 저장하는 것은 매우 귀찮으니, 다음 명령을 통해 upstream 연결을 지정할 수 있다. 이는 git remote add 명령을 통해 원격 저장소의 이름을 이미 지정한 경우의 얘기이다.

혹시 로컬에서 git을 처음 쓰거나 다른 사람의 작업트리를 처음 쓰는 경우라면 github id/pw를 입력해야 할 수 있다.

git push --set-upstream <remote-name> <branch-name>
git push -u <remote-name> <branch-name>

# 명령어 예시
git push --set-upstream origin main
git push -u origin main

# 결과 예시
Everything up-to-date
Branch 'main' set up to track remote branch 'main' from 'origin'.

git push --set-upstream <remote-name> <branch-name> 명령은 <branch-name> branch의 upstream을 원격 저장소 <remote-name>로 지정하는 것으로, 앞으로 git pushgit pull 명령 등을 수행할 때 <branch name><remote name>을 지정할 필요가 없도록 지정하는 역할을 한다. 즉, 앞으로는 commit을 원격 저장소에 반영할 때 git push만 입력하면 된다.

위와 같은 방법으로 지정하지 않은 branch나 원격 저장소에 push하고자 하는 경우, git push <remote-name> <branch-name>을 사용한다.

# 명령어 예시
git push origin ticket-branch

upstream 삭제

더 이상 필요 없는 원격 branch를 삭제할 때는 다음 명령을 사용한다.

git push --delete <remote-name> <remote-branch-name>

# 명령어 예시
git push --delete origin ticket-branch
git push -d origin ticket-branch

수정사항 반영하기

일반적으로 로컬 저장소의 commit을 원격 저장소에 반영하려면 다음 명령어를 입력한다.

git push <remote-name> <branch-name>

# 명령어 예시
git push origin main

위에서 --set-upstream 옵션을 사용해 업로드 branch와 장소를 지정했다면 git push만으로도 원격 저장소에 업로드가 가능하다.

git push

위와 같은 방식으로는 기본적으로 로컬 branch의 이름(<branch-name>)과 원격 저장소에 저장될 branch의 이름이 같게 된다. 이를 다르게 지정해서 업로드하려면 다음과 같이 쓴다.

git push <remote-name> <local-branch-name>:<remote-branch-name>

# 명령어 예시
git push origin fourth:ticket

목적지인 원격 저장소의 해당 branch에 현재 로컬 저장소에는 없는 commit이 존재한다면 push가 진행되지 않는다. 원격 저장소의 변경점을 먼저 로컬에 복사해야 한다. 이는 git pull 명령을 써서 해결한다. 여기를 참고한다.

모든 branch의 수정사항 반영하기

git push --all <remote-name>

모든 branch의 수정사항을 반영하므로 <branch-name>은 지정할 필요 없다.


원격 저장소의 수정사항을 로컬로 가져오기: git pull

사실 git pull 명령은 git fetchgit merge FETCH_HEAD를 합친 명령과 같다. 즉 원격 저장소의 수정사항을 먼저 확인한 다음, 로컬 저장소에는 없는 모든 commit들을 로컬로 가져오는 작업과 같다.

다음 상황을 가정하자:

	  A---B---C main on origin
	 /
    D---E---F---G main
	^
	origin/main in your repository

현재 로컬 저장소의 main branch에는 A, B, C commit이 존재하지 않는다. 이를 로컬에 반영하려면 git pull을 입력한다. 어디서 받아올지 지정되어 있지 않다면 git pull <remote-name> <remote-branch-name>을 입력한다.

	  A---B---C origin/main
	 /         \
    D---E---F---G---H main

수정사항 사이에 충돌이 없다면 자동으로 진행된다. 만약 충돌이 일어났다면, 먼저 충돌 사항을 해결한 다음 add/commit/push 과정을 거치면 된다.


Git Directory 상태 확인

git status

현재 git 저장소의 상태를 확인하고 싶다면 다음 명령어를 입력한다.

git status

# 결과 예시 1:
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

# 결과 예시 2:

On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   first.py

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   .gitignore
        deleted:    second.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        third.py

git status로는 로컬 git 저장소에 변경점이 생긴 파일을 크게 세 종류로 나누어 보여준다.

  1. Changes to be committed
    • Tracking되는 파일이며, 준비 영역(stage)에 이름이 올라가 있는 파일들. 이 단계에 있는 파일들만이 commit 명령을 내릴 시 다음 commit에 포함된다. (그래서 to be commited이다)
    • 마지막 commit 이후 git add 명령으로 준비 영역에 추가가 된 파일들.
  2. Changes not staged for commit:
    • Tracking되는 파일이지만, 다음 commit을 위한 준비 영역에 이름이 올라가 있지 않은 파일들.
    • 마지막 commit 이후 git add 명령의 대상이 된 적 없는 파일들.
  3. Untracked files:
    • Tracking이 안 되는 파일들.
    • 생성 이후 한 번도 git add 명령의 대상이 된 적 없는 파일들.

위와 같이 준비 영역 또는 tracked 목록에 올라왔는지가 1차 분류이고, 2차 분류는 해당 파일이 처음 생성되었는지(ex. third.py), 변경되었는지(modified), 삭제되었는지(deleted)로 나눈다.

수정된 파일을 보다 간략히 보려면 --short 옵션을 사용한다.

git status --short
git status -s

# 결과 예시
 M .gitignore
A  doonggoos.py
D  first.py
 M fourth.py
R  third.py -> what.py

추가된 파일은 A, 수정된 파일은 M, 삭제된 파일은 D, 이름이 바뀐 파일은 R로 표시된다.


특정 파일/디렉토리 무시하기: .gitignore

프로젝트의 최상위 디렉토리에 .gitignore라는 이름을 갖는 파일을 생성한다. 윈도우에서는 copy con .gitignore라 입력한 뒤, 내용을 다 입력하고, Ctrl + C를 누르면 파일이 저장되면서 생성된다.

.gitignore 파일을 열었으면 안에 원하는 대로 파일명이나 디렉토리 이름 등을 입력한다. 그러면 앞으로 해당 프로젝트에서는 git add 명령으로 준비 영역에 해당 종류의 파일 등이 추가되지 않는다.

예시는 다음과 같다.

dum_file.py             # `dum_file.py`라는 이름의 파일을 무시한다.
*.zip                   # 확장자가 `.zip`인 모든 파일을 무시한다.
data/                   # data/ 디렉토리 전체를 무시한다.
!data/regression.csv    # data/ 디렉토리는 무시되지만, data/regression.csv 파일은 무시되지 않는다. 
                        # 이 경우는 data/ 이전 라인에 작성하면 적용되지 않는다.
**/*.json               # 모든 디렉토리의 *.json 파일을 무시한다.

.gitignore 파일을 저장하고 나면 앞으로는 해당 파일들은 tracking되지 않는다. 즉, 준비 영역에 추가될 수 없다.
그러나 이미 tracking되고 있는 파일들은 영향을 받지 않는다. 따라서 git rm --cached 명령을 통해 tracking 목록에서 제거해야 한다.

전체 프로젝트에 .gitignore 적용하기

특정 프로젝트가 아닌 모든 프로젝트 전체에 적용하고 싶으면 다음 명령을 입력한다.

git config --global core.excludesfile <.gitignore-file-path>

# 명령 예시
git config --global core.excludesfile ~/.gitignore
git config --global core.excludesfile C:\.gitignore

그러면 해당 위치에 .gitignore 파일이 생성되고, 이는 모든 프로젝트에 적용된다. 일반적으로 git config --global 명령을 통해 설정하는 것은 특정 프로젝트가 아닌 해당 로컬에서 작업하는 모든 프로젝트에 영향을 준다. 여기를 참고하라.


History 검토

현재 존재하는 commit 검토: git log

저장소 commit 메시지의 모든 history를 역순으로 보여준다. 즉, 가장 마지막에 한 commit이 가장 먼저 보여진다.

git log

# 결과 예시
commit da446019230a010bf333db9d60529e30bfa3d4e3 (HEAD -> main, origin/main, origin/HEAD)
Merge: 4a521c5 2eae048
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Sun Aug 19 20:59:24 2018 +0900

    Merge branch '3rd-branch'

commit 2eae048f725c1d843cad359d655c193d9fd632b4
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Sun Aug 19 20:29:48 2018 +0900

    Unwanted commit from 2nd-branch

...
:

이때 commit의 수가 많으면 다음 명령을 기다리는 커서가 깜빡인다. 여기서 space bar를 누르면 다음 commit들을 계속해서 보여주고, 끝에 다다르면(저장소의 최초 commit에 도달하면) (END)가 표시된다.
끝에 도달했거나 이전 commit들을 더 볼 필요가 없다면, q를 누르면 log 보기를 중단한다(quit).

git log 옵션: –patch(-p), –max-count(-<number>), –oneline(–pretty=oneline), –graph

각 commit의 diff 결과(commit의 세부 변경사항, 변경된 파일의 변경된 부분들을 보여줌)를 보고 싶으면 다음을 입력한다.

git log --patch

# 결과 예시
commit 2eae048f725c1d843cad359d655c193d9fd632b4
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Sun Aug 19 20:29:48 2018 +0900

    Unwanted commit from 2nd-branch

diff --git a/first.py b/first.py
index 2d61b9f..c73f054 100644
--- a/first.py
+++ b/first.py
@@ -9,3 +9,5 @@ print("This is the 1st sentence written in 3rd-branch.")
 print('2nd')

 print('test git add .')
+
+print("Unwanted sentence in 2nd-branch")

현재 branch가 아닌 다른 branch의 log를 보고 싶다면 <branch-name>을 추가 입력해 준다.

git log -p origin/main

# 결과 예시
commit 2eae048f725c1d843cad359d655c193d9fd632b4
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Sun Aug 19 20:29:48 2018 +0900

    Unwanted commit from 2nd-branch

diff --git a/first.py b/first.py
index 2d61b9f..c73f054 100644
--- a/first.py
+++ b/first.py
@@ -9,3 +9,5 @@ print("This is the 1st sentence written in 3rd-branch.")
 print('2nd')

 print('test git add .')
+
+print("Unwanted sentence in 2nd-branch")

가장 최근의 commit들 3개만 보고 싶다면 다음과 같이 입력한다.

git log -3

commit의 대표 메시지와 같은 핵심 내용만 보고자 한다면 다음과 같이 입력한다.

git log --oneline

# 결과 예시
da44601 (HEAD -> main, origin/main, origin/HEAD) Merge branch '3rd-branch'
2eae048 Unwanted commit from 2nd-branch
4a521c5 Desired commit from 2nd-branch

참고로, 다음과 같이 입력하면 commit의 고유 id의 전체가 출력된다.

git log --pretty=oneline

# 결과 예시
da446019230a010bf333db9d60529e30bfa3d4e3 (HEAD -> main, origin/main, origin/HEAD) Merge branch '3rd-branch'
2eae048f725c1d843cad359d655c193d9fd632b4 Unwanted commit from 2nd-branch
4a521c56a6c2e50ffa379a7f2737b5e90e9e6df3 Desired commit from 2nd-branch

옵션들은 중복이 가능하다.

git log --oneline -5

--graph 옵션은 branch이 어디서 분기되고 합쳐졌는지와 같은 정보를 그래프로 보여준다. 분기된 지점이 없으면 일렬로 보인다.

git log --graph

# 결과 예시
* commit e8a20c960cfcd3f444d93b735f6bed7bd40ed7c5 (HEAD -> main, origin/main, origin/HEAD)
| Author: greeksharifa <greeksharifa@gmail.com>
| Date:   Fri May 29 23:25:35 2020 +0900
|
|     accelerate page load speed
|
* commit abbe725235f3144ef6df02c4b1b34cd1804ccd50
| Author: greeksharifa <greeksharifa@gmail.com>
| Date:   Fri May 29 22:22:49 2020 +0900
|
|     permalink test
|
...

--merges, --no-merges 옵션은 여기를 참고한다.

commit 검색하기

-S 옵션은 commit message나 수정사항 내에 주어진 문자열이 포함되어 있다면 해당 commit이 검색된다.
-G 옵션은 -S와 비슷하지만 정규식 표현으로 검색할 수 있다.

git log -S <string>
git log -G <regex-expression>

일부 commit만 확인하기

  • 가장 최신 commit을 제외하고 log를 보려면 git log HEAD^를 사용한다.
  • 가장 최신 2개의 commit을 제외하고 보려면 git log HEAD~2를 사용한다.
  • 특정 범위의 commit을 확인하려면 git log <commit-1>..<commit-2>를 이용한다.
  • 2개의 branch 사이의 차이를 확인하려면 git log <branch-name-1>..<branch-name-2>를 이용한다. 원격 저장소의 branch도 확인 가능하다.

commit과 commit의 변화 과정 전체를 검토: git reflog

git reflog

# 결과 예시:
87ab51e (HEAD -> main, tag: specific_tag) HEAD@{0}: commit: All text in first line will be showed at --onel
ine
da44601 (origin/main, origin/HEAD) HEAD@{1}: clone: from https://github.com/greeksharifa/git_tutorial.git

위와 같이 HEAD@{0}: commit과 HEAD@{1}: clone 이라는 변화를 볼 수 있다. git reflog는 commit 뿐 아니라 commit이 삭제되었는지, 재배치했는지, clone이나 rebase 같은 변화가 있었는지 등등 git에서 일어난 모든 변화를 기록한다.

특정 파일의 수정사항 history 보기: git blame

git blame <filename>의 형태로 사용한다. 파일 히스토리가 나타나는데,
해당 수정사항을 포함하는 commit id, 수정한 사람, 수정 일시, 줄 번호, 수정 내용을 볼 수 있다.

blame이라고 해서 누군가를 비난하는 것은 아니다.

git blame fourth.py

# 결과 예시
8506cef2 (greeksharifa      2020-05-27 21:42:19 +0900 1) print('hello')
dd65e051 (greeksharifa      2020-05-28 23:21:01 +0900 2) print('git')
8506cef2 (greeksharifa      2020-05-27 21:42:19 +0900 3)
dd65e051 (greeksharifa      2020-05-28 23:21:01 +0900 4) print('bye')
00000000 (Not Committed Yet 2020-05-30 14:26:53 +0900 5) print('20000')
00000000 (Not Committed Yet 2020-05-30 14:26:53 +0900 6)
00000000 (Not Committed Yet 2020-05-30 14:26:53 +0900 7) print('for test')
00000000 (Not Committed Yet 2020-05-30 14:26:53 +0900 8) print('for test 2')
00000000 (Not Committed Yet 2020-05-30 14:26:53 +0900 9) print('repeating test')

단, 수정사항을 묶어서 보여주지는 않는다.


다른 commit / branch와의 자세한 차이 확인: git diff

git diff 명령으로는 branch 간 차이를 확인하거나, commit 간 차이를 확인할 수 있다. 다음 예시들을 살펴보자.

git diff는 최신 commit과 현재 상태를 비교한다. 수정된 파일이 있으면 내용이 뜨고, 없으면 아무것도 출력되지 않는다.

git diff

# 결과 예시 1
(빈 줄)

# 결과 예시 2
diff --git a/fourth.py b/fourth.py
index 4c8cfb6..e69de29 100644
--- a/fourth.py
+++ b/fourth.py
@@ -1,5 +0,0 @@
-print('hello')
-print('git')
-
-print('bye')
-print('20000')
\ No newline at end of file

git diff <commit>은 해당 commit 이후 수정된 코드를 보여준다.

git diff <branch-name-1> <branch-name-2>는 두 branch 간 차이를 전부 보여준다. branch를 지정할 때 두 branch의 순서를 바꾸면 추가된 줄과 삭제된 줄이 뒤바뀌니 주의하자.
<branch-name-1>에서 <branch-name-2>로 이동할 때의 변화를 기준으로 +, -가 보여진다. 즉 <branch-name-1>에는 없고 <branch-name-2>에는 있는 코드라면 +로 표시된다.

git diff main 2nd-branch

# 결과 예시
diff --git a/.gitignore b/.gitignore
index 15c8c56..8d16a4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
-
+third.py
 .idea/
 *dummy*
diff --git a/first.py b/first.py
index baba21f..2d61b9f 100644
--- a/first.py
+++ b/first.py
@@ -1 +1,11 @@
-print("Hello, git!") 
+print("Hello, git!") # instead of "Hello, World!"
...

<branch-name-2>를 생략할 수도 있다. 위의 결과와는 +, -가 다르다.

git diff 2nd-branch

# 결과 예시
diff --git a/.gitignore b/.gitignore
index 8d16a4b..15c8c56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
-third.py
+
 .idea/
 *dummy*
diff --git a/first.py b/first.py
index 2d61b9f..baba21f 100644
--- a/first.py
+++ b/first.py
@@ -1,11 +1 @@
-print("Hello, git!") # instead of "Hello, World!"
-print("Hi, git!!")
...

difftool

diff의 결과를 보거나 수정하고자 할 때 본인이 쓰는 에디터가 아니라 git bash 내에서 수행하려면 difftool을 사용한다.

git difftool <branch-name-1>..<branch-name-2>
git difftool <commit-1>..<commit-2>

HEAD: branch의 tip

HEAD는 현 branch history의 가장 끝을 의미한다. 여기서 끝은 가장 최신 commit 쪽의 끝이다(시작점을 가리키지 않는다).
다른 의미로는 checkout된 commit, 또는 현재 작업중인 commit이다.

예를 들어, HEAD@{0}은 1번째 최신 commit(즉, 가장 최신 commit)을 의미한다. index는 많은 프로그래밍 언어가 그렇듯 0부터 시작한다. 비슷하게, HEAD@{1}은 2번째 최신 commit을 의미한다.

HEAD^는 HEAD의 직전, 즉 가장 최신 commit을 가리킨다.

범위를 나타낼 땐 ~를 사용한다. 예를 들어, HEAD~3은 가장 최신 commit(1번째)부터 3번째 commit까지를 가리킨다.

HEAD~2^HEAD^(가장 최신, 즉 1번째 commit)보다 2번 더 이전 commit까지 간 것이고, 범위(~)를 나타내므로 1~3번째 commit을 가리킨다. 헷갈리니까 3개의 commit을 다루고 싶으면 그냥 HEAD~3을 쓰자.


Tag 붙이기

태그는 특정한 commit을 찾아내기 위해 사용된다. 즐겨찾기와 같은 개념이기 때문에, 여러 commit에 동일한 태그를 붙이지 않도록 한다.

우선 태그를 붙이고 싶은 commit을 찾자.

# 명령어 예시 1
git log --oneline -3

# 결과 예시 1
87ab51e (HEAD -> main) All text in first line will be showed at --oneline
da44601 (origin/main, origin/HEAD) Merge branch '3rd-branch'
2eae048 Unwanted commit from 2nd-branch

# 명령어 예시 2
git log 87ab51e --max-count=1
git show 87ab51e

# 결과 예시 2
commit 87ab51eecef1a526cb504846ddcaed0459f685c8 (HEAD -> main)
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Thu May 28 14:49:13 2020 +0900

    All text in first line will be showed at --oneline

    Maximum length is 50 characters.
    Below, is for detailed message.

git tag

이제 태그를 commit에 붙여보자.

git tag <tag-name> 87ab51e

# 명령어 예시
git tag specific_tag 87ab51e

지금까지 붙인 태그 목록을 보려면 다음 명령을 입력한다.

git tag

# 결과 예시
specific_tag

해당 태그가 추가된 commit을 보려면 여기를 참조한다.


특정 commit 보기

git show

commit id를 사용해서 특정 commit을 보고자 하면 다음과 같이 쓴다.

git log 87ab51e --max-count=1
git show 87ab51e

# 결과 예시
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Thu May 28 14:49:13 2020 +0900

    All text in first line will be showed at --oneline

    Maximum length is 50 characters.
    Below, is for detailed message.

git show <tag-name>

git show <tag-name>

# 명령어 예시
git show specific_tag

# 결과 예시
commit 87ab51eecef1a526cb504846ddcaed0459f685c8 (HEAD -> main, tag: specific_tag)
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Thu May 28 14:49:13 2020 +0900

    All text in first line will be showed at --oneline

    Maximum length is 50 characters.
    Below, is for detailed message.

diff --git a/.gitignore b/.gitignore
index 8d16a4b..6ec8ec8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-third.py
 .idea/
 *dummy*
diff --git a/third.py b/third.py
new file mode 100644
index 0000000..0360dad
--- /dev/null
+++ b/third.py
@@ -0,0 +1 @@
+print('hello 3!')

Git Branch

branch 목록 업데이트하기

git fetch --all
git fetch -a

특정 원격 저장소의 것만을 업데이트하려면 다음과 같이 한다.

git fetch <remote-name>

branch 목록 보기

로컬 branch 목록을 보려면 다음을 입력한다.

git branch
git branch --list
git branch -l

# 결과 예시
* main

branch 목록을 보여주는 모든 명령에서, 현재 branch(작업 중인 branch)는 맨 앞에 asterisk(*)가 붙는다.

모든 branch 목록 보기:

git branch --all
git branch -a

# 결과 예시
* main
  remotes/origin/2nd-branch
  remotes/origin/3rd-branch
  remotes/origin/HEAD -> origin/main
  remotes/origin/main

remotes/가 붙은 것은 원격 branch라는 뜻이며, branch의 실제 이름에는 remotes/가 포함되지 않는다.

--verbose 옵션을 붙이면 최신 commit까지 출력해 준다.

git branch --all --verbose

# 결과 예시
  2nd-branch                   1be03c8 Remove files that were uploaded incorrectly
* main                       94d511c [ahead 3] fourth ticket
  remotes/origin/2nd-branch    1be03c8 Remove files that were uploaded incorrectly
  remotes/origin/3rd-branch    90ce4f2 Merge branch '3rd-branch'
  remotes/origin/HEAD          -> origin/main
  remotes/origin/fourth-branch 94d511c fourth tickek
  remotes/origin/main        da44601 Merge branch '3rd-branch'

main branch의 설명에 붙어 있는 [ahead 3]이라는 문구는 현재 로컬 저장소에는 3개의 commit이 있지만 아직 원격 저장소에 psuh되지 않았음을 의미한다.

원격 branch 목록만 보기:

git branch --remotes
git branch -r

# 결과 예시
  origin/2nd-branch
  origin/3rd-branch
  origin/HEAD -> origin/main
  origin/main

branch 이름 변경

먼저 현재 branch의 이름 변경하는 방법은 다음과 같다.

git checkout <old-branch-name>
git branch -m <new-branch-name>

지금 branch가 main(master)이라면 다른 branch의 이름을 바로 변경할 수 있다.

git branch -m <old-branch-name> <new-branch-name>

branch 이름 변경 시 로컬 저장소의 branch 이름도 변경

아래 예시는 master를 main으로 바꿨을 때의 코드이다.

git branch -m main main
git fetch origin
git branch -u origin/main main
git remote set-head origin -a

원격 branch 목록 업데이트

로컬 저장소와 원격 저장소는 실시간 동기화가 이루어지는 것이 아니기 때문에(일부 git 명령을 내릴 때에만 통신이 이루어짐), 원격 branch 목록은 자동으로 최신으로 유지되지 않는다. 목록을 새로 확인하려면 다음을 입력한다.

git fetch

별다른 변경점이 없으면 아무 것도 표시되지 않는다.


branch 전환

branch를 전환하려면 저장되지 않은 수정사항이 없어야 한다.
수정사항을 다른 데다 임시로 저장하려면 stash를 참고한다.

단순히 branch 간 전환을 하고 싶으면 다음 명령어를 입력한다.

git checkout <branch-name>

# 명령어 예시
git checkout main

# 결과 예시
Switched to branch 'main'
M       .gitignore
D       second.py
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

전환을 수행하면,

  • 변경된 파일의 목록과
  • 현재 로컬 브랜치가 연결되어 있는 원격 브랜치 사이에 얼마만큼의 commit 차이가 있는지

도 알려준다.

로컬에 새 branch를 생성하되, 그 내용을 원격 저장소에 있는 어떤 branch의 내용으로 하고자 하면 다음 명령을 사용한다.

git checkout --track -b <local-branch-name> <remote-branch-name>

# 명령어 예시
git checkout --track -b 2nd-branch origin/2nd-branch

# 결과 예시
Switched to a new branch '2nd-branch'
M       .gitignore
D       second.py
Branch '2nd-branch' set up to track remote branch '2nd-branch' from 'origin'.

출력에서는 2nd-branch라는 이름의 새 branch로 전환하였고, 파일의 현재 수정 사항을 간략히 보여주며, 로컬 branch 2nd-branchorigin의 원격 branch 2nd-branch를 추적하게 되었음을 알려준다.
즉 원격 branch의 로컬 사본이 생성되었음을 알 수 있다.

새 branch 생성

git branch <new-branch-name>

# 명령어 예시
git branch fourth-branch

위 명령은 branch를 생성만 한다. 생성한 브랜치에서 작업을 시작하려면 checkout 과정을 거쳐야 한다.

branch 생성과 같이 checkout하기

git checkout -b <new-branch-name> <parent-branch-name>

# 명령어 예시
git checkout -b fourth-branch main

# 결과 예시
Switched to a new branch 'fourth-branch'

새로운 branch는 생성 시점에서 parent branch와 같은 history(commit 기록들)을 갖는다.

원격 저장소의 branch를 로컬 저장소에 복사하며 checkout하기

git checkout -b <local-branch-name> --track <remote-branch-name>

# 명령어 예시
git branch -a
git checkout -b 3rd-branch --track remotes/origin/3rd-branch
git branch

# 결과 예시
  2nd-branch
* main
  remotes/origin/2nd-branch
  remotes/origin/3rd-branch
  remotes/origin/HEAD -> origin/main
  remotes/origin/fourth-branch
  remotes/origin/main


Switched to a new branch '3rd-branch'
Branch '3rd-branch' set up to track remote branch '3rd-branch' from 'origin'.


  2nd-branch
* 3rd-branch
  main

branch 병합: git merge

git merge <branch-name>를 사용한다. <branch-name> branch의 수정 사항들(commit)을 현재 branch로 가져와 병합한다. 이 방식은 완전 병합 방식이다.

git merge <branch-name>

# 명령어 예시
git merge ticket-branch

# 결과 예시
Updating 96c99dc..94d511c
Fast-forward
 .gitignore | 2 +-
 fourth.py  | 5 +++++
 second.py  | 9 ---------
 third.py   | 0
 4 files changed, 6 insertions(+), 10 deletions(-)
 create mode 100644 fourth.py
 delete mode 100644 second.py
 create mode 100644 third.py

이와 같은 방법을 history fast-forward라 한다(히스토리 빨리 감기).

병합할 때 ticket branch의 모든 commit들을 하나의 commit으로 합쳐서 parent branch에 병합하고자 할 때는 --squash 옵션을 사용한다.

# 현재 branch가 parent branch일 때
git merge ticket-branch --squash

--squash 옵션은 애초에 branch를 분리하지 말았어야 할 상황에서 쓰면 된다. 즉, 병합 후 parent branch 입장에서는 그냥 하나의 commit이 반영된 것과 같은 효과를 갖는다.

위와 같이 처리했을 때는 ticket branch가 더 이상 필요 없으니 삭제하도록 하자.

병합 시 현 branch의 작업만을 최우선으로 남겨둔다면 다음 옵션을 사용한다.

git merge -X ours <branch-name>

반대로 가져오고자 하는 branch의 작업을 최우선으로 남긴다면 다음을 쓴다.

git merge -X theirs <branch-name>

branch 삭제

git branch --delete <branch-name>
git branch -d <branch-name>

# 명령어 예시
git branch --delete ticket-branch

# 결과 예시
Deleted branch fourth-branch (was 94d511c).

branch 삭제는 해당 branch의 수정사항들이 다른 branch에 병합되어서, 더 이상 필요없음이 확실할 때에만 문제없이 실행된다.
아직 수정사항이 남아 있음에도 그냥 해당 branch 자체를 폐기처분하고 싶으면 --delete 대신 -D 옵션을 사용한다.

이미 원격 저장소에 올라간 branch를 삭제하려면 여기를 참조한다.


작업 취소하기

먼저 가능한 작업 취소 명령들을 살펴보자.

원하는 것 명령어
특정 파일의 수정사항 되돌리기 git checkout -- <filename>
모든 수정사항을 되돌리기 git reset --hard
준비 영역의 모든 수정사항을 삭제 git reset --hard <commit>
여러 commit 통합 git reset <commit>
이전 commit들을 수정 또는 통합, 혹은 분리 git rebase --interactive <commit>
untracked 파일을 포함해 모든 수정사항을 되돌리기 git clean -fd
이전 commit을 삭제하되 history는 그대로 두기 git revert <commit>

아래는 Git for Teams라는 책에서 가져온 flowchart이다. 뭔가 잘못되었을 때 사용해보도록 하자.

여러 명이 협업하는 프로젝트에서 이미 원격 저장소에 잘못된 수정사항이 올라갔을 때, 이를 강제로 되돌리는 것은 금물이다. ‘잘못된 수정사항을 삭제하는’ 새로운 commit을 만들어 반영시키는 쪽이 훨씬 낫다.

물론 branch를 잘 만들고, pull request 시스템을 적극 활용해서 그러한 일이 일어나지 않도록 하는 것이 최선이다.
혹시나 그런 일이 발생했다면, revert를 사용하라. 다른 명령들은 아직 원격 저장소에 push하지 않았을 때 쓰는 명령들이다.


특정 파일의 수정사항 되돌리기: checkout, reset

특정 파일을 지워 버렸거나 수정을 잘못했다고 하자. 이 때에는 다음 전제조건이 있다.

수정사항을 commit하지 않았을 때

commit하지 않았다면, 다음 두 가지 경우가 있다. git status를 입력하면 친절히 알려준다.

git status

#결과 예시
On branch main

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   third.py

no changes added to commit (use "git add" and/or "git commit -a")

마지막 줄에서 아직 commit된 것이 없다는 것을 확인해야 한다.

  1. 수정사항을 준비 영역에 올리지 않았을 때(git add를 안 수행했을 때)
    • git checkout -- <filename>
    • 그러면 파일이 원래대로 복구된다.
  2. 수정사항을 stage했을 때(git add를 수행했을 때)
    • 그러면 위 결과 예시처럼 no changes added to commit ...이라는 메시지가 없다. 다음 두 명령을 입력한다.
    • git reset HEAD <filename>
    • git checkout -- <filename> 을 입력한다.
    • 그러면 가장 최신(HEAD) commit에 저장되어 있는 파일의 원래 상태가 복구된다. commit하지 않았을 때 사용할 수 있는 이유가 이것이다.
    • 아니면 명령어 두 개를 합친 다음 명령을 써도 된다.
    • git reset --hard HEAD -- <filename>

git reset <filename>git add <filename>의 역방향이라고 보면 된다. 물론 git reset <commit> <filename>은 파일을 여러 commit 이전으로 되돌릴 수 있기 때문에 상황에 따라서는 다른 작업일 수 있다.

비슷하게, git reset -p <filename>git add -p <filename>의 역 작업이다.

git reset의 옵션은 여러 개가 있다.

  • git reset [-q | -p] [--] <paths>: <paths><filename>을 포함한다. 즉, filename 뿐만 아니라 디렉토리 등도 가능하다. 이 명령의 효과는 git add [-p]의 역 작업이다.
  • git reset [--soft | --mixed [-N] | --hard | --merge | --keep] -[q] [<commit>]
    • --hard: <commit> 이후 발생한 모든 수정사항과 준비 영역의 수정사항이 폐기된다.
    • --soft는 파일의 수정사항이 남아 있으며, 수정된 파일들이 모두 Changes to be committed 상태가 된다.
    • --mixed는 파일의 수정사항은 남아 있으나 준비 영역의 수정사항은 폐기된다. mixed가 기본 옵션이다.
    • --merge는 준비 영역의 수정사항은 폐기하고 <commit>HEAD 사이 수정된 파일들을 업데이트하지만 수정된 파일들은 stage되지 않는다.
    • --keep--merge와 비슷하나 <commit>때와 HEAD 때가 다른 파일에 일부 변화가 있는 경우에는 reset 과정이 중단된다.

모든 파일의 수정사항 되돌리기:

git reset --hard HEAD

branch 병합 취소하기

먼저 다음 flowchart를 살펴보자.

바로 직전에 한 병합(merge)를 취소하려면 다음 명령어를 입력한다.

git reset --merge ORIG_HEAD

병합 후 추가한 commit이 있으면 해당 지점의 commit을 지정해야 한다.

git reset <commit>

어디인지 잘 모르겠으면 reflog를 사용해보자.

이미 원격 저장소에 공유된 branch 병합을 취소하는 방법은 여기를 참고한다.


커밋 합치기: git reset <commit>

기본적으로, git reset은 branch tip을 <commit>으로 옮기는 과정이다. 그래서, git reset <option> HEAD는 마지막 commit의 상태로 준비 영역 또는 파일 내용을 되돌리는(reset) 작업이다.
또한, 바로 위에서 살펴봤듯이, git reset은 기본 옵션이 --mixed이며, 이는 옵션을 따로 명시하지 않으면 git reset은 파일의 수정사항은 그대로 둔 채 준비 영역에는 추가된 수정사항이 없는 상태로 만든다.

그래서 특정 이전 commit을 지정하여 git reset <commit>을 수행하면 해당 <commit>부터 HEAD까지의 파일의 수정사항은 작업트리(=프로젝트 디렉토리 전체)에 그대로 남아 있지만, 준비 영역에는 아무런 변화도 기록되어 있지 않다.
먼저 어떤 커밋들을 합칠지 git log --oneline으로 확인해보자.

# 결과 예시
c8c731b (HEAD -> main, origin/main, origin/HEAD) doong commit
87ab51e (tag: specific_tag) All text in first line will be showed at --oneline
da44601 Merge branch '3rd-branch'
2eae048 Unwanted commit from 2nd-branch
4a521c5 Desired commit from 2nd-branch

이제 가장 최신 2개의 commit을 합치고 싶으면, 현재 branch의 HEAD를 c8c731b에서 da44601로 옮기면 된다.

git reset da44601

그러면 직전 2개의 commit의 수정사항이 파일에는 그대로 남아 있지만, 준비 영역이나 commit 내역에선 사라진다. 이제 stage, commit, push 3단계를 수행하면 최종적으로 commit 2개가 1개로 합쳐진다.

<commit> id를 지정하는 것이 헷갈린다면 git reset HEAD~2로 실행하자. 이는 여기에서 볼 수 있듯이 범위로 2개의 commit을 포함한다.


git rebase

rebase는 일반적으로 history rearrange의 역할을 한다. 즉, 여러 commit들의 순서를 재배치하는 작업이라 할 수 있다. 혹은 parent branch의 수정사항을 가져오면서 자신의 commit은 그 이후에 추가된 것처럼 하는, 마치 분기된 시점을 뒤로 미룬 듯한 작업을 수행할 수도 있다.

그러나 rebase와 같은 기존 작업을 취소 또는 변경하는 명령은 일반적으로 충돌(conflict)이 일어나는 경우가 많다. 충돌이 발생하면 git은 작업을 일시 중지하고 사용자에게 충돌을 처리하라고 한다.

main branch의 commit을 topic branch로 가져오기

다음과 같은 상황을 가정하자. 각 알파벳은 하나의 commit이며, 각 이름은 branch의 이름을 나타낸다.
아래 각 예시는 git help에 나오는 도움말을 이용하였다.

          A---B---C topic
         /
    D---E---F---G main

commit F, G를 topic branch에 반영(포함)시키려 한다면,

                  A'--B'--C' topic
                 /
    D---E---F---G main

commit A’와 A는 프로젝트에 동일한 수정사항을 적용시키지만, 16진수로 된 commit의 고유 id(da44601 같은)는 다르다. 즉, 엄밀히는 다른 commit이다.

commit을 재배열하는 명령어는 다음과 같다. 현재 branch는 topic이라 가정한다.

git rebase main
git rebase main topic

commit A, B, C가 F, G와 코드 상으로 동일한 파일 또는 다른 일부분을 수정하지 않았다면, 이 rebase 작업은 자동으로 완료된다.

만약 topic branch에 이미 main branch로부터 가져온 commit이 일부 존재하면, 이 commit들은 새로 배치되지 않는다.

          A---B---C topic
         /
    D---E---A'---F main

에서

                   B'---C' topic
                  /
    D---E---A'---F main

로 바뀐다.

branch의 parent 바꾸기: –onto

topic을 next가 아닌 main에서 분기된 것처럼 바꾸고자 한다. 즉,

    o---A---B---o---C  main
         \
          D---o---o---o---E  next
                           \
                            o---o---o  topic

이걸 아래와 같이 바꿔보자.

    o---A---B---o---C  main
        |            \
        |             o'--o'--o'  topic
         \
          D---o---o---o---E  next

topic branch의 history에는 이제 commit D~E 대신 commit A~B가 포함되어 있다.

이는 다음과 같은 명령어로 수행할 수 있다:

git rebase --onto main next topic

다른 예시는:

                            H---I---J topicB
                           /
                  E---F---G  topicA
                 /
    A---B---C---D  main
git rebase --onto main topicA topicB
                 H'--I'--J'  topicB
                /
                | E---F---G  topicA
                |/
    A---B---C---D  main

특정 범위의 commit들 제거하기

    E---F---G---H---I---J  topic

topic branch의 5번째 최신 commit부터, 3번째 최신 commit 직전까지 commit을 topic branch에서 폐기하고 싶다고 하자. 그러면 다음 명령어로 사용 가능하다.

git rebase --onto <branch-name>~<start-number> <branch-name>~<end-number> <branch-name>

# 명령어 예시
git rebase --onto topic~5 topic~3 topic
    E---H'---I'---J'  topic

여기서 5(번째 최신 commit, F)은 삭제되고, 3(번째 최신 commit, H)은 삭제되지 않음을 주의하라. rebase가 되기 때문에 commit의 고유 id는 바뀐다(H -> H’)

충돌 시 해결법

일반적으로 rebase에서 수정하는 2개 이상의 commit이 같은 파일을 수정하면 충돌이 발생한다.

보통은 다음 과정을 거치면 해결된다.

  • 충돌이 일어난 파일에 적절한 조취를 취한다. 파일을 남기거나/삭제하거나, 또는 파일 일부분에서 남길 부분을 찾는다. 코드 중 다음과 비슷해 보이는 부분이 있을 것이다. 적절히 지워서 해결하자.
ㅤ<<<<<<<< HEAD
ㅤ<current-code>
ㅤ========
ㅤ<incoming-code>
ㅤ>>>>>>>> da446019230a010bf333db9d60529e30bfa3d4e3
  • git add <conflict-resolved-filename>
  • git rebase --continue

그냥 다 모르겠고(?) rebase 작업을 취소하고자 하면 다음을 입력한다.

git rebased --abort

rebase로 commit 합치거나 수정하기

다음과 같은 history가 있다고 하자.

c3eace0 (HEAD -> main, origin/main, origin/HEAD) git checkout, reset, rebase
f6c56ef what igt
bd80626 github hem
b7801a2 github overall
608a518 highlighter theme change

여러 개의 commit들을 합치거나, commit message를 수정하거나 하는 작업은 모두 rebase로 가능하다.
실행하면, vim 에디터가 열릴 것이다(ubuntu의 경우 nano일 수 있다). vim을 쓰는 방법은 여기를 참고한다.

rebase하는 부분에서는 다른 git command들과는 달리 수정할 commit 중 가장 오래된 commit이 가장 위에 온다.

git rebase --interactive <commit>
git rebase -i <commit>

# 명령 예시
git rebase -interactive 608a518
git rebase -i HEAD~4

# 결과 예시

pick c3eace0 (HEAD -> main, origin/main, origin/HEAD) git checkout, reset, rebase
pick f6c56ef what igt
pick bd80626 github hem
pick b7801a2 github overall
# Rebase 608a518..c3eace0 onto 608a518
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

설명을 잘 살펴보면 다음을 알 수 있다:

  • pick = p는 수정 사항과 commit을 그대로 둔다. 각 commit의 맨 앞에는 기본적으로 pick으로 설정되어 있다. 이 상태에서 아무 것도 안 하고 나간다면 이번 rebase는 아무 효과도 없다.
  • reword = rpick과 거의 같지만 commit message를 수정할 수 있다. commit message를 수정하고 앞의 pickrewordr로 바꾸면 commit의 메시지를 수정할 수 있다. 가장 최신의 commit에 r을 붙였다면 git commit --amend와 효과가 같다.
  • edit = e는 해당 commit을 수정할 수 있다. reset 등의 작업이 가능하다.
  • squash = s는 해당 commit이 바로 이전 commit에 흡수되며, commit message 또한 합쳐져서 하나로 된다. 합친 메시지들이 존재하는 에디터가 다시 열린다.
  • fixup = fsquash와 비슷하지만, 해당 commit의 message는 삭제된다.
  • exec = x는 commit들 아래 줄에 명령어를 추가하여 실행하게 할 수 있다.

수정한 예시는 다음과 같다. 약어를 써도 되고 안 써도 된다.

pick c3eace0 (HEAD -> main, origin/main, origin/HEAD) git checkout, reset, rebase
f f6c56ef what igt
f bd80626 github hem
fixup b7801a2 github overall
...(아래 주석은 지워도 되고 안 지워도 된다. 어차피 commit에서는 무시되는 도움말이다)

하나의 commit을 2개로 분리하기

가장 최신 commit이라면 git reset HEAD~1을 사용하여 직전 commit 상태로 되돌린 뒤 stage-commit을 2번 수행하면 되고, 그 이전 commit이라면 rebase에서 해당 commit을 edit으로 두고 같은 과정을 반복하면 된다.

# 명령어 예시
git rebase HEAD~4
# pick -> edit
git add -p <filename>
git commit -m <1st-commit-message>
git add -p <filename1> <filename2>
git commit -m <2nd-commit-message>
git rebase --continue

commit을 되돌리는 commit: git revert

예를 들어, 4a521c5이라는 commit이 코드 3줄을 수정하고, 2줄을 제거하는 commit이라고 하자. 나중에, 이 commit이 완전히 잘못된 내용임을 알았으나, 이미 원격 저장소에 push되었다고 하자. 이럴 때 해당 commit을 취소하는 작업을 git revert로 수행할 수 있다.
아니, 정확히는 commit을 되돌리는 역할을 하는 commit을 추가하는 commit을 새로 생성할 수 있다.

git revert <commit>

# 명령어 예시
git revert 4a521c5

# 결과 예시
[main 4a521c5] Revert "specific_commit_description"

공유된 branch 병합 취소하기

먼저 어디서 병합이 일어났는지를 살펴본다. git log --merges를 쓰면 병합 commit만을 볼 수 있다. 반대로 --no-merges는 병합 commit은 제외하고 log를 보여준다.

git log --merges

# 결과 예시
commit da446019230a010bf333db9d60529e30bfa3d4e3 (origin/main, origin/HEAD)
Merge: 4a521c5 2eae048
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Sun Aug 19 20:59:24 2018 +0900

    Merge branch '3rd-branch'

commit 90ce4f2ec8b5cd26af51e03401fb4541abfffbc2 (tag: v0.5, origin/3rd-branch)
Merge: e934e3e 317200f
Author: greeksharifa <greeksharifa.gmail.com>
Date:   Sun Aug 12 15:42:06 2018 +0900

    Merge branch '3rd-branch'

아니면 git log --graphgit reflog를 활용한다.

이제 다음 그림을 참고하자.

완전 병합인 경우 다음 명령을 사용한다.

git revert --mainline <branch-number> <commit>

# 명령어 예시
git revert --maineline 1 4a521c5

여기서 <branch-number>는 남길 branch의 번호이다. git log --graph에서 보여지는 선들 중에서 가장 왼쪽부터 1번이며, 보통은 1번을 남기게 된다.

병합 commit이 따로 없다면 잘못된 commit들을 개별적으로 처리해야 한다.

특정 commit을 포함하는 모든 branch의 목록을 보자.

git branch --contains <commit>

취소할 commit들이 인접해 있다면 다음 명령으로 하나의 취소 commit을 생성할 수 있다.

git revert --no-commit <last commit to keep>..<newest commit to reject>

# 결과 예시
git revert --no-commit 4a521c5..2eae048

변경 사항을 검토하고 취소 과정을 끝내자.

git revert --continue

인접해 있지 않다면 각 commit을 하나씩 취소 작업을 해야 한다. 심심한 위로의 말을 전한다.

git revert <commit-1>
git revert <commit-2>
...

history 완전 삭제하기: 완전범죄?

혹시나 비밀번호 같은 걸 원격 저장소에 올려버렸다면, 다른 팀원들이 봤든 안 봤든 최대한 흔적도 없이 날려버려야 한다. 이 때는 다음 명령들을 실행한다. 삭제할 파일이 password.crypt라고 하자.

git filter-branch --index-filter 'git rm --cached --ignore-unmatch password.crypt' HEAD
git reflog expire --expire=now --all
git gc --prune=now
git push origin --force --all --tags

각각 특정 파일을 저장소에서 완전히 삭제하고, history에서 없애고, 모든 commit되지 않은 수정사항을 작업트리에서 삭제하는 명령이다.

다른 팀원들에게는 rebase를 진행시키거나 아예 로컬 저장소를 밀어버린 다음 새로 clone해서 받으라고 말한다.

git pull --rebase=preserve

수정사항 임시 저장하기: git stash

지금 당장 branch를 전환해서 다른 branch의 내용을 봐야 하는데 commit할 만큼은 안 되는 수정사항이 작업트리에 남아 있을 때가 있다. 그럴 때는 잠시 넣어 두는 명령이 필요하다.

git stash
git stash save
git stash save "stash message"

# 결과 예시
Saved working directory and index state WIP on main: 94d511c fourth ticket

commit message처럼 간략한 메시지를 적고 싶다면 git stash save "<stash-message>"로 사용한다.

그러나 git stash [save] 명령은 untracked 파일들은 저장하지 않는다. 이 파일들까지 임시 저장하라면 다음과 같이 쓴다.

git stash save --include-untracked
git stash -u

반대로 stage된 파일을 stash하지 않으려면 git stash --keep-index로 사용한다.

git stashgit add와 비슷하게 --patch 옵션을 지원한다. 남길 부분을 파일 내에서 선택하고 싶다면 해당 옵션을 사용하라.

stash로 저장한 목록을 보려면 다음 명령을 입력한다.

git stash list

#결과 예시
stash@{0}: WIP on main: 94d511c fourth ticket
stash@{1}: WIP on main: 94d511c fourth ticket

stash의 내용이 기억나지 않으면 git stash stash@{<number>} 명령을 쓴다.

git stash stash@{1}

# 결과 예시
Merge: 94d511c 7060e4d f4a6d7f
Author: greeksharifa <greeksharifa@gmail.com>
Date:   Sat May 30 13:51:23 2020 +0900

    WIP on main: 94d511c fourth tickek

diff --cc .gitignore
index 15c8c56,15c8c56,0000000..f6f1686
mode 100644,100644,000000..100644
--- a/.gitignore
+++ b/.gitignore
@@@@ -1,3 -1,3 -1,0 +1,5 @@@@
  +
  +.idea/
  +*dummy*
+++
+++*.txt
diff --cc doonggoos.py
...

잠시 넣어 둔 stash를 다시 작업트리로 꺼내오려면 git stash apply stash@{<number>}를 사용한다.

git stash apply stash@{0}

# 결과 예시
On branch main
Your branch and 'origin/main' have diverged,
and have 3 and 2 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   doonggoos.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   .gitignore
        modified:   fourth.py

어떤 파일들이 변경되었는지 알려준다.

더 이상 안 쓸 stash를 제거하려면 git stash drop stash@{<number>}를 사용한다.

git stash drop stash@{0}

#결과 예시
Dropped stash@{0} (9f700348f8688c3cbc21c17e4bc3d231b3abd0c3)

작업트리 청소하기: git clean

untracked 파일을 그냥 없애버리고 싶다면 git clean -d를 쓴다.

tracking하지 않는 모든 정보를 지워버리려면 git clean -f -d를 사용한다. 말 그대로 강제(-f, force)다.

그냥 지워버려도 되는지 확인하고 싶다면 -n 옵션을 붙여서 실행시키면 된다. 그러면 어떤 파일들이 영향을 받는지 알려준다.

git clean -d -n

.gitignore에 명시한 등 무시되는 파일은 git clean으로 지워지지 않는다. 이런 파일들까지 싹 다 지우려면 -x 옵션을 붙인다.
대화형으로 실행하려면 -i 옵션을 붙이면 된다.


최초의 오류 commit 찾기: git bisect

git bisect는 일종의 디버깅 툴이다. 코드에 어떤 버그가 있지만 그게 언제 추가됐는지 정확히 모를 때 쓴다.
bisect를 쓰려면 우선 다음 조건이 필요하다.

  • 어떤 문제가 있는 시점을 알고(보통은 현재일 것이다)
  • 해당 문제가 없는 과거의 어떤 commit 시점을 알고 있을 때

그러면 git bisect를 통해 이분탐색을 수행하여 잘못된 코드가 어떤 commit에서 나타났는지 찾는다. 이분 탐색하며 중간 지점의 commit에서 다시 build해 보고,

  • 문제가 있으면 git bisect bad 입력, 해당 commit 이전을 탐색하고,
  • 문제가 없으면 git bisect good 입력, 해당 commit 이후를 탐색한다.
# 명령어 및 결과 예시
git bisect start                        # 시작
git bisect bad [<commit>]               # 어떤 시점(<commit>을 안 쓰면 현재)에 문제가 있고
git bisect good <commit>                # 어떤 시점에는 문제가 없음을 git에 알리기

Bisecting: 675 revisions left to test after this (roughly 10 steps)
# 그러면 675개의 수정 사항 중 이분 탐색을 수행한다. 2^10 = 1024이니 10단계만 테스트하면 된다.

git bisect good

Bisecting: 337 revisions left to test after this (roughly 9 steps)

git bisect <bad/good>
...

bisect 세션을 끝내고 원래 상태로 돌아가려면 git bisect reset을 입력한다.
만약 중간 지점으로 선택된 commit이 테스트할 수 없다면 bad / good 대신 git bisect skip을 입력해서 잠시 패스하고 근처의 다른 commit을 테스트 대상으로 할 수 있다.


branch에서 특정 commit만 다른 branch로 적용하기: git cherry-pick

git cherry-pick <commit> 명령은 branch의 병합 없이도 다른 branch의 특정 commit을 가져올 수 있다. ticket branch에 있는 96c99dc라는 commit을 main branch로 가져오고자 한다.

# 명령어 예시
git checkout main
git cherry-pick 96c99dc

# 결과 예시
[3rd-branch 32d6b93] example commit message
 Date: Sat May 30 18:51:51 2020 +0900
 1 file changed, 2 insertions(+), 3 deletions(-)

명령어 마음대로 설정하기: Git Alias

alias는 단축만 가능한 것은 아니지만, 단축할 때 많이 쓴다.

git reset HEAD -- <filename>이 입력하기 귀찮거나 자주 실수한다면, 직관적인 명령어로 바꿔 줄 수 있다.
git config alias.<another-name> '<original-command>' 형식으로 쓴다.

git config --global alias.unstage 'reset HEAD --'

이제 아래 두 명령은 동일한 효과를 갖는다.

git reset HEAD -- <filename>
git unstage <filename>

충돌 자동 해결: Reuse Recorded Resolution(git.rerere)

정확히는 전부 자동으로 해 주는 것은 아니고, 예전에 비슷한 충돌을 해결한 적이 있다면 같은 방식으로 자동으로 해결하도록 설정할 수 있다.

다음 설정으로 활성화한다.

git config --global rerere.enabled true
  • 처음 충돌이 났을 때 git rerere status로 충돌 파일을 확인한다. git rerere diff로 충돌을 해결한다.
  • 이후 처리 과정은 일반 충돌 처리 과정과 같다.
    • commit하고 나면 Recorded resolution for <filename>이라는 메시지를 볼 수 있다.
  • 다음으로 비슷한 충돌이 났을 때에는 다음 메시지를 확인할 수 있다.
    • Resolved <filename> using previous resolution. : 이미 충돌을 해결했다는 뜻이다.
    • 충돌 파일을 확인해봐도 충돌된 부분을 찾을 수 없다. 그냥 commit하면 된다.

Comment  Read more