모든 강의 자료와 영상은 공개된 해당 링크들을 통해서 학습하고 있다.

강의자료: https://cs229.stanford.edu/syllabus-spring2022.html

강의 영상: https://www.youtube.com/watch?v=goDDnBbJQ4g

5강 강의노트: https://cs229.stanford.edu/lectures-spring2022/main_notes.pdf

 

최근 구글 스터디잼 이라는 프로그램에서도 인공지능 관련 강의와 실습으로 익혀보고 있는데, 이해하기 쉽게 설명해줘서 (영상, 그림 등) 훨씬 용이해졌다. 


 

요약

1. Generative Learning Algorithms
2. Discriminative vs. Generative Models
3. Gaussian Discriminant Analysis, GDA
4. Decision Boundary

 

 

Generative Learning Algorithms

이전 강의(로지스틱 회귀)는 $ p(y|x) $를 직접 모델링했다. 입력 x가 주어졌을 때 정답 y가 무엇인지 바로 예측하는 방식.

이런 방식을 판별 모델, discriminative model이라고 한다.

 

반대로 생성 모델은 바로 p(y|x)를 찾지 않는다. 대신 각 클래스가 데이터를 어떻게 만들어내는지 먼저 모델링한다.

예를 들어:

  • $ p(x|y=0) $: 개 클래스의 특징 분포
  • $ p(x|y=1) $: 코끼리 클래스의 특징 분포
  • $ p(y) $: 각 클래스가 나올 사전 확률

을 학습한다.

그다음 새로운 데이터 x가 들어오면, 이 데이터가 개 모델에서 더 그럴듯한지, 코끼리 모델에서 더 그럴듯한지를 비교한다.

수식으로는 Bayes rule을 사용한다.

 

$ p(y|x)=\frac{p(x|y)p(y)}{p(x)} $

 

2가지 방식

continuous x → Gaussian Discriminant Analysis, GDA

Discrete x → Naive Bayes / Spam Filtering

 

 

GDA

입력 $ x $는 여러 feature로 이루어진 벡터라고 본다.

$ x \in \mathbb{R}^d $

즉, 하나의 데이터는 $ d $개의 feature를 가진 $ d $차원 벡터다.

GDA는 각 클래스의 조건부 분포 $ p(x|y) $를 다변량 가우시안 분포로 모델링한다.  
따라서 먼저 다변량 가우시안 분포가 어떤 형태인지 정리해보자.

다변량 가우시안 분포에서 각 변수의 차원은 다음과 같다.

$ z \in \mathbb{R}^d $
$ \mu \in \mathbb{R}^d $
$ \Sigma \in \mathbb{R}^{d \times d} $

여기서 $ z $는 $ d $차원 데이터 벡터, $ \mu $는 $ d $차원 평균 벡터, $ \Sigma $는 $ d \times d $ 공분산 행렬이다.
확률변수 $ Z $가 평균 벡터 $ \mu $, 공분산 행렬 $ \Sigma $를 갖는 다변량 가우시안 분포를 따른다면 다음과 같이 쓴다.

$ Z \sim \mathcal{N}(\mu,\Sigma) $

이때 확률밀도함수는 다음과 같다.

$ p(z;\mu,\Sigma)=\frac{1}{(2\pi)^{d/2}|\Sigma|^{1/2}}\exp\left(-\frac{1}{2}(z-\mu)^T\Sigma^{-1}(z-\mu)\right) $

평균 벡터 $ \mu $는 분포의 중심을 나타낸다.

$ E[Z]=\mu $

공분산 행렬 $ \Sigma $는 데이터가 평균을 기준으로 어떤 방향으로 얼마나 퍼져 있는지를 나타낸다.  
공분산은 다음과 같이 정의한다.

$ Cov(Z) \triangleq E[(Z-E[Z])(Z-E[Z])^T] $

여기서 $ \triangleq $는 “이렇게 정의한다”는 의미다.
그리고 $ Z \sim \mathcal{N}(\mu,\Sigma) $일 때,

$ Cov(Z)=\Sigma $

이다.

따라서 $ \mu $는 데이터가 모여 있는 중심을 나타내고, $ \Sigma $는 그 중심을 기준으로 데이터가 퍼지는 모양을 나타낸다.

이제 이를 GDA에 적용한다.  
GDA는 $ y=0 $인 데이터와 $ y=1 $인 데이터가 각각 다른 평균을 갖는 가우시안 분포에서 나왔다고 가정한다.

$ x|y=0 \sim \mathcal{N}(\mu_0,\Sigma) $
$ x|y=1 \sim \mathcal{N}(\mu_1,\Sigma) $

즉, 두 클래스는 서로 다른 평균 $ \mu_0,\mu_1 $을 가지지만, 같은 공분산 행렬 $ \Sigma $를 공유한다고 가정한다.  
쉽게 말하면 두 클래스의 데이터 구름은 중심은 다르지만, 퍼지는 모양은 같다고 보는 것이다.

 

CS299 main notes, 37p

위 그림은 공분산 행렬 $ \Sigma $에 따라 다변량 가우시안 분포의 모양이 어떻게 달라지는지를 보여준다.

공분산 행렬이 단위행렬에 가까우면, 모든 feature 방향으로 데이터가 비슷하게 퍼진다.  
이 경우 밀도함수의 모양은 구형에 가까워진다.

반대로 특정 방향의 분산이 더 크면, 데이터는 그 방향으로 더 넓게 퍼지고 밀도함수는 납작한 타원체 모양이 된다.

또한 feature들 사이의 상관관계가 커질수록 타원체는 좌표축과 나란하지 않고 한쪽 방향으로 기울어진다.  
즉, $ \Sigma $는 단순히 분산의 크기만 나타내는 것이 아니라, feature들 사이의 상관관계와 데이터가 퍼지는 방향까지 함께 나타낸다.

 

예시) 암 진단 분류

GDA를 암 진단 예시로 생각해보자.  
각 환자에게는 여러 측정값이 있을 수 있다. 예를 들어 종양 크기, 혈압, 특정 검사 수치 등이 feature가 될 수 있다.

여기서는 이해를 쉽게 하기 위해 두 개의 feature만 사용한다고 하자.

$ x =
\begin{bmatrix}
x_1 \\
x_2
\end{bmatrix}
\in \mathbb{R}^2 $

여기서 $ x_1 $, $ x_2 $는 각각 환자의 측정값이다.

각 환자 데이터에는 레이블 $ y $가 함께 주어진다.

$ y \in \{0,1\} $

예를 들어 다음처럼 둘 수 있다.

$ y=0:\text{ benign } $ (양성)
$ y=1:\text{ malignant } $ (악성)

GDA는 각 클래스에서 데이터가 가우시안 분포를 따른다고 가정한다.

$ x|y=0 \sim \mathcal{N}(\mu_0,\Sigma) $
$ x|y=1 \sim \mathcal{N}(\mu_1,\Sigma) $

여기서 $ \mu_0 $는 양성 데이터의 평균 벡터이고, $ \mu_1 $은 악성 데이터의 평균 벡터이다.  
두 클래스는 서로 다른 평균을 가지지만, 같은 공분산 행렬 $ \Sigma $를 공유한다고 가정한다.

또한 클래스 자체의 사전 확률은 다음과 같이 둔다.

$ p(y=1)=\phi $
$ p(y=0)=1-\phi $

따라서 GDA에서 학습해야 하는 파라미터는 다음과 같다.

$ \mu_0,\mu_1,\Sigma,\phi $

새로운 환자 데이터 $ x $가 들어오면, GDA는 다음 두 값을 비교한다.

$ p(x|y=0)p(y=0) $
$ p(x|y=1)p(y=1) $

즉, 이 환자 데이터가 양성 분포에서 더 그럴듯하게 나왔는지, 악성 분포에서 더 그럴듯하게 나왔는지를 비교해 최종 클래스를 결정한다.

 

GDA와 판별 모델의 MLE 차이

 

GDA는 생성적 모델이다.  
따라서 $ p(y|x) $를 직접 모델링하지 않고, 데이터가 어떻게 생성되는지를 나타내는 결합분포를 모델링한다.

$ p(x,y)=p(x|y)p(y) $

즉, GDA는 클래스별 데이터 분포 $ p(x|y) $와 클래스 사전확률 $ p(y) $를 함께 학습한다.

GDA에서 학습해야 하는 파라미터는 다음과 같다.

$ \phi,\mu_0,\mu_1,\Sigma $

이 파라미터들은 전체 학습 데이터가 가장 그럴듯하게 생성되도록 maximum likelihood로 추정한다.

$ \max_{\phi,\mu_0,\mu_1,\Sigma}\prod_{i=1}^{n}p(x^{(i)},y^{(i)}) $
또는,
$ \max_{\phi,\mu_0,\mu_1,\Sigma}\prod_{i=1}^{n}p(x^{(i)}|y^{(i)})p(y^{(i)}) $

반면 로지스틱 회귀 같은 판별 모델은 $ x $의 분포를 모델링하지 않는다.  
입력 $ x $는 이미 관측된 값으로 두고, 그때의 label $ y $를 직접 예측한다.

$ p(y|x) $

따라서 판별 모델은 조건부 우도를 최대화한다.

$ \max_\theta \prod_{i=1}^{n}p(y^{(i)}|x^{(i)};\theta) $

정리하면, GDA는 “각 클래스가 데이터를 어떻게 만들어내는가”를 학습하고, 판별 모델은 “주어진 데이터가 어느 클래스인가”를 직접 학습한다.

 

 

(질의응답)

생성 모델의 가정은 일반화에 악영향을 주지 않을까?

GDA는 각 클래스의 데이터 분포 \(p(x|y)\)가 가우시안 분포를 따른다고 가정한다.  
그렇다면 이런 강한 가정이 새로운 데이터에 대한 일반화 성능을 오히려 떨어뜨릴 수도 있지 않을까?

답은 “가정이 실제 데이터와 얼마나 맞는지에 따라 다르다”이다.

만약 실제 데이터가 GDA의 가정과 잘 맞는다면, 즉 각 클래스의 데이터가 실제로 가우시안 분포에 가깝고 공분산 구조도 비슷하다면 GDA는 매우 효율적으로 학습할 수 있다. 적은 데이터만으로도 클래스별 분포를 잘 추정할 수 있기 때문이다.

반대로 실제 데이터가 가우시안 분포와 많이 다르다면, GDA의 강한 가정은 오히려 성능을 떨어뜨릴 수 있다. 모델이 실제 데이터 구조와 맞지 않는 방식으로 분포를 억지로 설명하려 하기 때문이다.

따라서 여기에는 trade-off가 있다.

너무 많은 가정을 두면, 틀릴 위험이 높아지게 되고, 
반대로 너무 적은 가정을 두면, 데이터에 대해 알고 있는 유용한 사전 지식을 활용하지 못한다.

 

 

GDA의 MLE 학습 과정

 

GDA는 데이터를 다음과 같이 생성한다고 가정한다.

$ y \sim Bernoulli(\phi) $

$ x|y=0 \sim \mathcal{N}(\mu_0,\Sigma) $

$ x|y=1 \sim \mathcal{N}(\mu_1,\Sigma) $

따라서 GDA가 학습해야 하는 파라미터는 다음 네 가지다.

$ \phi,\mu_0,\mu_1,\Sigma $

GDA는 생성 모델이므로 각 데이터 쌍 $ (x^{(i)},y^{(i)}) $의 결합 확률을 최대화한다.

$ p(x^{(i)},y^{(i)})=p(x^{(i)}|y^{(i)})p(y^{(i)}) $

전체 데이터에 대한 likelihood는 다음과 같다.

$ L(\phi,\mu_0,\mu_1,\Sigma)=\prod_{i=1}^{n}p(x^{(i)}|y^{(i)};\mu_0,\mu_1,\Sigma)p(y^{(i)};\phi) $

실제로는 계산을 쉽게 하기 위해 log-likelihood를 최대화한다.

$ \ell(\phi,\mu_0,\mu_1,\Sigma)=\sum_{i=1}^{n}\log p(x^{(i)},y^{(i)};\phi,\mu_0,\mu_1,\Sigma) $

이 식을 각 파라미터에 대해 미분해 0이 되는 지점을 찾으면 MLE 추정값을 얻는다.


$ \phi=\frac{1}{n}\sum_{i=1}^{n}1\{y^{(i)}=1\} $

$ \mu_0=\frac{\sum_{i=1}^{n}1\{y^{(i)}=0\}x^{(i)}}{\sum_{i=1}^{n}1\{y^{(i)}=0\}} $

$ \mu_1=\frac{\sum_{i=1}^{n}1\{y^{(i)}=1\}x^{(i)}}{\sum_{i=1}^{n}1\{y^{(i)}=1\}} $

$ \Sigma=\frac{1}{n}\sum_{i=1}^{n}(x^{(i)}-\mu_{y^{(i)}})(x^{(i)}-\mu_{y^{(i)}})^T $

$ \phi $는 클래스 1의 비율이고, $ \mu_0,\mu_1 $은 각 클래스의 평균 벡터이다.  
$ \Sigma $는 각 샘플이 자기 클래스 평균에서 얼마나 벗어나는지를 모아 계산한 shared covariance이다.

학습이 끝나면 새로운 데이터 $ x $에 대해 다음 값을 비교한다.

$ p(x|y=0)p(y=0) $
$ p(x|y=1)p(y=1) $

더 큰 값을 주는 클래스로 예측한다.

$ \hat{y}=\arg\max_{y\in\{0,1\}}p(x|y)p(y) $

GDA에서는 두 클래스가 같은 공분산 행렬 $ \Sigma $를 공유한다고 가정한다.  
이 가정 때문에 결정 경계를 유도하면 $ x $에 대한 이차항이 상쇄되고, 최종 decision boundary는 선형 형태가 된다.

$ \theta^T x+\theta_0=0 $

즉, GDA는 생성 모델이지만 shared covariance 가정 아래에서는 로지스틱 회귀처럼 선형 decision boundary를 만든다.

 

 

 

Prediction

 

GDA 학습이 끝나면 다음 파라미터들이 정해져 있다.

$ \hat{\phi},\hat{\mu}_0,\hat{\mu}_1,\hat{\Sigma} $

여기서 MLE는 $ y $를 찾는 과정이 아니라, 학습 데이터가 가장 그럴듯하게 생성되도록 하는 파라미터를 찾는 과정이다.

$ \hat{\phi},\hat{\mu}_0,\hat{\mu}_1,\hat{\Sigma}
=
\arg\max_{\phi,\mu_0,\mu_1,\Sigma}
L(\phi,\mu_0,\mu_1,\Sigma) $

반면 예측 단계에서는 새로운 입력 $ x $가 주어졌을 때, 가능한 클래스 중 확률이 가장 큰 $ y $를 선택한다.

$ \hat{y}=\arg\max_{y\in\{0,1\}}p(y|x) $

GDA가 직접 학습한 것은 $ p(y|x) $가 아니라 $ p(x|y) $와 $ p(y) $이다.  
따라서 예측할 때는 Bayes rule을 사용한다.

$ p(y|x)=\frac{p(x|y)p(y)}{p(x)} $

클래스를 비교할 때 분모 $ p(x) $는 모든 $ y $에 대해 동일하므로 생략할 수 있다.

$ \hat{y}=\arg\max_{y\in\{0,1\}}p(x|y)p(y) $

즉, 실제로는 다음 두 값을 비교한다.

$ p(x|y=0)p(y=0) $
$ p(x|y=1)p(y=1) $

더 큰 값을 주는 클래스로 예측한다.

이진 분류에서는 $ p(y=0|x)+p(y=1|x)=1 $이므로, 

$ p(y=1|x)>0.5 \Rightarrow \hat{y}=1 $
$ p(y=1|x)<0.5 \Rightarrow \hat{y}=0 $

두 클래스의 posterior probability가 같아지는 지점이 decision boundary이다.

$ p(y=1|x)=p(y=0|x) $

또는 이진 분류에서는 다음과 같이 쓸 수 있다.

$ p(y=1|x)=0.5 $

GDA에서는 두 클래스가 같은 공분산 행렬 $ \Sigma $를 공유한다고 가정한다.  
이 때문에 두 가우시안 확률을 비교할 때 $ x $에 대한 이차항이 상쇄되고, 최종 결정 경계는 선형 형태가 된다.

$ \theta^T x+\theta_0=0 $

 

GDA 정리

 

GDA는 생성적 학습 알고리즘의 대표적인 예시다.  
로지스틱 회귀처럼 $ p(y|x) $를 직접 학습하는 것이 아니라, 클래스 prior $ p(y) $와 클래스별 데이터 분포 $ p(x|y) $를 먼저 학습한다.

$ p(x,y)=p(x|y)p(y) $

특히 GDA는 입력 $ x $가 연속형 변수일 때, 각 클래스의 조건부 분포 $ p(x|y) $가 다변량 가우시안 분포를 따른다고 가정한다.

$ x|y=0 \sim \mathcal{N}(\mu_0,\Sigma) $
$ x|y=1 \sim \mathcal{N}(\mu_1,\Sigma) $

학습 단계에서는 MLE를 통해 다음 파라미터를 추정한다.

$ \phi,\mu_0,\mu_1,\Sigma $

예측 단계에서는 Bayes rule을 사용해 $ p(y|x) $를 계산하고, 더 확률이 큰 클래스를 선택한다.

$ \hat{y}=\arg\max_{y\in\{0,1\}}p(x|y)p(y) $

또한 두 클래스가 같은 공분산 행렬 $ \Sigma $를 공유한다는 가정 아래에서 GDA의 posterior probability는 로지스틱 함수 형태로 정리된다.

$ p(y=1|x)=\frac{1}{1+\exp(-(\theta^T x+\theta_0))} $

따라서 decision boundary는 다음과 같은 선형식으로 표현된다.

$ \theta^T x+\theta_0=0 $

즉, GDA는 생성 모델이지만 shared covariance 가정 아래에서는 선형 결정 경계를 만든다.  
2차원에서는 직선, 더 높은 차원에서는 초평면이 된다.

모든 강의 자료와 영상은 공개된 해당 링크들을 통해서 학습하고 있다.

강의자료: https://cs229.stanford.edu/syllabus-spring2022.html

강의 영상: https://www.youtube.com/watch?v=goDDnBbJQ4g

4강 강의노트: https://drive.google.com/file/d/1Mm35IalfEHsWrn4gVwsulVYc2Dg6YtcR/view


 

1. Exponential Family

2. Generalized Linear Model

3. Softmax Regression (Multi class Regression)

 

1. Exponential Family

$ p(y;\eta) = b(y)\exp\left(\eta^T T(y) - a(\eta)\right) $

 

$ \eta $ natural parameter 모델이 직접 예측하는 “raw score”
$ T(y) $ sufficient statistic y에서 실제로 필요한 요약값
$ a(\eta) $ log partition function 확률 전체 합이 1이 되게 맞춰주는 정규화 항
$ b(y) $ base measure y 자체에만 붙는 기본 항

 

Bernoulli Distribution

Bernoulli 분포는 $ y $가 0 또는 1만 가질 때 사용하는 분포이다.

$ y \in \{0, 1\} $
$ P(y=1)=\phi,\quad P(y=0)=1-\phi $

이를 하나의 식으로 쓰면 다음과 같다.

$ p(y;\phi)=\phi^y(1-\phi)^{1-y} $

이 식은 $ y=1 $이면 $ \phi $만 남고, $ y=0 $이면 $ 1-\phi $만 남는다.



이제 Bernoulli 분포를 exponential family의 표준형과 비교해보자.

$ p(y;\eta)=b(y)\exp\left(\eta^T T(y)-a(\eta)\right) $

먼저 Bernoulli 식을 지수함수 형태로 바꾼다.

$ p(y;\phi)=\exp\left(y\log\phi+(1-y)\log(1-\phi)\right) $

안쪽을 정리하면 다음과 같다.

$ y\log\phi+(1-y)\log(1-\phi) = y\log\frac{\phi}{1-\phi}+\log(1-\phi) $

따라서 Bernoulli 분포는 다음처럼 쓸 수 있다.

$ p(y;\phi)=\exp\left(\left(\log\frac{\phi}{1-\phi}\right)y+\log(1-\phi)\right) $

이를 exponential family 표준형과 비교하면,

$ T(y)=y $
$ \eta=\log\frac{\phi}{1-\phi} $
$ b(y)=1 $
$ a(\eta)=-\log(1-\phi) $

여기서 $ \eta $는 Bernoulli 분포의 natural parameter이며, 이는 확률 $ \phi $ 자체가 아니라 log odds이다.

$ \eta=\log\frac{\phi}{1-\phi} $

이 식을 $ \phi $에 대해 풀면,

$ e^\eta=\frac{\phi}{1-\phi} $
$ \phi=\frac{e^\eta}{1+e^\eta}=\frac{1}{1+e^{-\eta}} $

즉, sigmoid 함수가 나온다.

$ \phi=\frac{1}{1+e^{-\eta}} $

마지막으로 $ a(\eta) $는 $ \eta $의 함수로 써야 하므로, $ \phi=\frac{e^\eta}{1+e^\eta} $를 이용해 정리한다.

$ 1-\phi=\frac{1}{1+e^\eta} $
$ a(\eta)=-\log(1-\phi)=\log(1+e^\eta) $

따라서 Bernoulli 분포의 exponential family 표현은 다음과 같이 정리된다.

$ p(y;\eta)=\exp\left(\eta y-\log(1+e^\eta)\right) $

where

$ T(y)=y,\quad a(\eta)=\log(1+e^\eta),\quad b(y)=1 $

핵심은 Bernoulli 분포를 exponential family로 쓰면 natural parameter가 log odds가 되고, 이를 다시 확률로 바꾸면 sigmoid 함수가 나온다는 점이다. 이 연결이 로지스틱 회귀를 GLM 관점에서 이해하는 출발점이다.

 

 

Gaussian

분산이 1인 Gaussian 분포를 생각해보자.

$ p(y;\mu)=\frac{1}{\sqrt{2\pi}}\exp\left(-\frac{1}{2}(y-\mu)^2\right) $

제곱항을 전개하면 다음과 같다.

$ -\frac{1}{2}(y-\mu)^2 = -\frac{1}{2}y^2+\mu y-\frac{1}{2}\mu^2 $

따라서 확률밀도함수는 다음처럼 분리할 수 있다.

$ p(y;\mu)=\frac{1}{\sqrt{2\pi}}\exp\left(-\frac{1}{2}y^2\right)\exp\left(\mu y-\frac{1}{2}\mu^2\right) $

Exponential Family의 표준형은 다음과 같다.

$ p(y;\eta)=b(y)\exp\left(\eta T(y)-a(\eta)\right) $

이를 Gaussian 분포와 비교하면,

$ \eta=\mu $
$ T(y)=y $
$ a(\eta)=\frac{\eta^2}{2} $
$ b(y)=\frac{1}{\sqrt{2\pi}}\exp\left(-\frac{y^2}{2}\right) $

즉, 분산이 1인 Gaussian 분포에서는 natural parameter $ \eta $가 평균 $ \mu $와 같다.

 

Why do we care?

Exponential Family가 중요한 이유는 여러 확률분포를 하나의 공통 형태로 표현할 수 있기 때문이다.  
Bernoulli, Gaussian 같은 서로 다른 분포도 같은 문법으로 다룰 수 있다.
또한 로그우도가 다음처럼 단순한 형태로 정리된다.

$ \log p(y;\eta)=\log b(y)+\eta T(y)-a(\eta) $

Gaussian의 경우 이 구조에서 squared error가 자연스럽게 나온다.  
따라서 선형회귀에서 MSE를 최소화하는 것은 Gaussian likelihood를 최대화하는 것과 연결된다.

 

2. GLM, Generalized Linear Model

GLM은 선형회귀와 로지스틱 회귀를 하나의 틀로 설명하는 방법이다.  
핵심은 입력 $ x $로 직접 $ y $를 예측하는 것이 아니라, 먼저 선형모델로 분포의 natural parameter를 만든다는 점이다.

$ \eta = \theta^T x $

그다음 $ y|x;\theta $가 따르는 확률분포를 정하고, 그 분포의 평균을 최종 예측값으로 사용한다.

$ h_\theta(x)=E[y|x;\theta] $

즉, GLM의 흐름은 다음과 같다.

 

GLM을 구성할 때는 보통 세 가지를 정한다.

  1. $ y $의 타입에 맞는 exponential family 분포를 고른다.
  2. natural parameter를 $ \eta=\theta^T x $로 둔다.
  3. 예측값은 조건부 평균 $ E[y|x;\theta] $로 둔다.
Binary   Bernoulli 합격/불합격, 클릭/미클릭
Real-valued   Gaussian 집값, 온도, 점수
Counts   Poisson 방문자 수, 클릭 수
Positive real   Gamma, Exponential 대기 시간, 수명
Probability distribution   Dirichlet 확률 벡터

 

예를 들어 $ y $가 연속값이면 Gaussian 분포를 선택한다.

$ y|x;\theta \sim \mathcal{N}(\mu,\sigma^2) $

Gaussian에서는 natural parameter가 평균과 연결되므로,

$ \eta=\mu $

$ \eta=\theta^T x $

따라서 예측 함수는 다음과 같다.

$ h_\theta(x)=E[y|x;\theta]=\mu=\theta^T x $

 

즉, Gaussian을 선택하면 linear regression이 나온다.

반대로 $ y $가 0 또는 1이면 Bernoulli 분포를 선택한다.

 

Bernoulli를 선택하면 logistic regression이 나온다.

GLM의 학습은 maximum likelihood로 한다.

 

$ \max_\theta \sum_{i=1}^{n}\log p(y^{(i)}|x^{(i)};\theta) $

 

주어진 데이터가 가장 그럴듯하게 나오도록 $ \theta $를 조정하는 것이다.

한 줄로 정리하면, GLM은 $ y $의 타입에 맞는 분포를 고르고, 그 분포의 natural parameter를 $ \theta^T x $로 둔 뒤,

조건부 평균 $ E[y|x;\theta] $를 예측값으로 사용하는 모델 설계법이다.

 

$ GLM = Linear\ model + Exponential\ family + Maximum\ likelihood $

Logistic Regression

Logistic regression의 hypothesis는 다음과 같다.

$ h_\theta(x)=E[y|x;\theta]=P(y=1|x;\theta)=\frac{1}{1+e^{-\theta^T x}} $

이 식은 입력 $ x $가 주어졌을 때 $ y=1 $일 확률을 예측한다.  
즉, logistic regression은 $ \theta^T x $를 직접 예측값으로 사용하지 않고, sigmoid를 거쳐 0과 1 사이의 확률로 바꾼다.

$ \theta^T x = \log \frac{P(y=1|x)}{P(y=0|x)} $

따라서 $ \theta^T x $는 예측값 자체가 아니라 log odds이다.

GLM 관점에서 보면, 이는 $ y|x;\theta $를 Bernoulli distribution으로 두었기 때문에 나온다.

$ y|x;\theta \sim Bernoulli(\phi) $

Bernoulli의 natural parameter는 다음과 같다.

$ \eta=\log\frac{\phi}{1-\phi} $

GLM에서는 natural parameter를 선형모델로 둔다.

$ \eta=\theta^T x $

이를 $ \phi $에 대해 풀면,

$ \phi=\frac{1}{1+e^{-\eta}}=\frac{1}{1+e^{-\theta^T x}} $

따라서 logistic regression의 sigmoid는 임의로 선택한 함수가 아니라, Bernoulli 분포를 GLM으로 모델링하면 자연스럽게 나오는 함수다.

 

 

 

3. Softmax Regression: Multi-class GLM

 

Logistic regression은 $ y \in \{0,1\} $인 binary classification을 다룬다.  
반면 클래스가 여러 개라면 정답은 다음처럼 표현된다.

$ y \in \{1,2,\dots,k\} $

이 경우 $ y|x;\theta $를 categorical distribution 또는 multinomial distribution으로 모델링한다.

 

One-hot Encoding

 

Multi-class label은 보통 one-hot vector로 표현한다.  
예를 들어 클래스가 cat, dog, car, bus 네 개라면,

위 처럼 정답 클래스 위치만 1이고 나머지는 0이다.

Exponential family 관점에서는 $ T(y) $를 indicator vector로 둔다.

$ (T(y))_i = 1\{y=i\} $

 

 

Linear Scores

 

Softmax regression에서는 각 클래스마다 parameter vector를 둔다.

$ \theta_1,\theta_2,\dots,\theta_k $

각 클래스는 입력 $ x $에 대해 자기 score를 계산한다.

$ \theta_1^T x,\quad \theta_2^T x,\quad \dots,\quad \theta_k^T x $

이 값들은 raw score 또는 logit이라고 부른다.  
아직 확률은 아니기 때문에 음수도 가능하고, 전체 합이 1일 필요도 없다.

 

Softmax Function

 

각 score를 확률로 바꾸기 위해 exponential을 씌운 뒤 전체 합으로 나눈다.

$ P(y=i|x;\theta)=\frac{\exp(\theta_i^T x)}{\sum_{j=1}^{k}\exp(\theta_j^T x)} $

이 함수가 softmax function이다.

softmax의 출력은 각 클래스에 대한 확률분포가 된다.

$ [p_1,p_2,\dots,p_k] $

각 값은 0 이상이고, 전체 합은 1이다.

$ \sum_{i=1}^{k}p_i=1 $

최종 예측은 가장 확률이 큰 클래스를 선택한다.

$ \hat{y}=\arg\max_i P(y=i|x;\theta) $

정리하면, softmax regression은 각 클래스의 linear score를 계산한 뒤, softmax를 통해 multi-class probability distribution으로 변환하는 GLM이다.

 

 

Cross Entropy Loss

 

Softmax regression의 출력은 각 class에 대한 확률분포이다.

$ \hat{p}=[P(y=1|x),P(y=2|x),\dots,P(y=k|x)] $

정답 label도 one-hot vector로 표현할 수 있다.  
예를 들어 정답이 class 1이라면 다음과 같다.

$ p=[1,0,0,0] $

모델의 예측이 다음과 같다고 하자.

$ \hat{p}=[0.57,0.17,0.12,0.14] $

Cross entropy는 정답 분포 $ p $와 예측 분포 $ \hat{p} $ 사이의 차이를 재는 loss이다.

$ H(p,\hat{p})=-\sum_{i=1}^{K}p_i\log\hat{p}_i $

one-hot label에서는 정답 class만 1이고 나머지는 0이다.  
따라서 cross entropy는 정답 class에 부여한 확률만 남는다.

$ H(p,\hat{p})=-\log\hat{p}_{\text{true class}} $

위 예시에서는 정답이 class 1이므로,

$ H(p,\hat{p})=-\log(0.57) $

즉, 모델이 정답 class에 높은 확률을 줄수록 loss는 작아지고, 낮은 확률을 줄수록 loss는 커진다.

Softmax regression은 학습할 때 이 cross entropy를 최소화한다.  
이는 정답 class의 확률을 최대화하는 것과 같다.

모든 강의 자료와 영상은 공개된 해당 링크들을 통해서 학습하고 있다.

강의자료: https://cs229.stanford.edu/syllabus-spring2022.html

강의 영상: https://www.youtube.com/playlist?list=PLoROMvodv4rNyWOpJg_Yh4NSqI4Z4vOYy

3강 슬라이드: https://drive.google.com/file/d/1G7vMGP2OE4Qd-MT6Uc1cffg_pYeD77aq/view

 


 

 

1. 선형회귀 확률적 해석

 

선형회귀에서 오차(Error)를 '제곱'해서 최소화하는 이유

 

선형회귀에서는 실제값 $ y $가 예측값 $ \theta^T x $에 오차항 $ \epsilon $이 더해진 형태라고 본다.

$ y = \theta^T x + \epsilon $

여기서 $ \epsilon $은 모델이 설명하지 못한 요인이나 랜덤 노이즈를 의미한다. 
즉, i번째 데이터에 대해 오차는 다음과 같이 쓸 수 있다.

$ \epsilon^{(i)} = y^{(i)} - \theta^T x^{(i)} $

만약 오차를 단순히 더하면 양수 오차와 음수 오차가 서로 상쇄될 수 있다. 그래서 오차의 크기를 제대로 반영하기 위해 오차를 제곱한다.

$ \left(y^{(i)} - \theta^T x^{(i)}\right)^2 $

오차를 제곱하면 모든 오차가 양수가 되고, 큰 오차에는 더 큰 벌점이 주어진다. 
따라서 선형회귀에서는 전체 데이터에 대한 제곱 오차의 합을 최소화하는 방향으로 $ \theta $를 찾는다.

$ \frac{1}{2}\sum_{i=1}^{n}\left(y^{(i)} - \theta^T x^{(i)}\right)^2 $

이 방식은 단순히 오차 상쇄를 막기 위한 계산 트릭만은 아니다. 선형회귀에서는 보통 오차항이 평균 0을 중심으로 독립적으로 분포한다고 가정한다.

$ \epsilon^{(i)} \sim \mathcal{N}(0, \sigma^2) $

이는 작은 오차는 자주 발생하고, 큰 오차는 상대적으로 드물게 발생한다는 직관과 잘 맞는다. 
이 가정에 따르면 $ x^{(i)} $가 주어졌을 때 $ y^{(i)} $는 평균이 $ \theta^T x^{(i)} $인 정규분포를 따른다.

$ y^{(i)} \mid x^{(i)};\theta \sim \mathcal{N}(\theta^T x^{(i)}, \sigma^2) $

이제 주어진 데이터를 가장 그럴듯하게 설명하는 $ \theta $를 찾기 위해 likelihood를 최대화하면, 결과적으로 제곱 오차의 합을 최소화하는 문제와 같아진다.

$ \arg\max_\theta L(\theta) = \arg\min_\theta \frac{1}{2}\sum_{i=1}^{n}\left(y^{(i)} - \theta^T x^{(i)}\right)^2 $

따라서 선형회귀에서 오차를 제곱해 최소화하는 것은 오차가 정규분포를 따른다는 확률적 가정에서 자연스럽게 나오는 방법이다.

 

2.  Classification

집값을 예측하는 '회귀(Regression)'와 달리,

'분류(Classification)'는 데이터가 어느 카테고리에 속하는지(예: 스팸 메일인가 아닌가, 종양이 악성인가 양성인가)를 맞추는 문제이다.


$ y \in \{0, 1\} $

$ h_\theta(x) = \theta^T x $

하지만 선형 회귀는 분류 문제에 적합하지 않다.  
첫째, 예측값이 0보다 작거나 1보다 커질 수 있기 때문에 확률로 해석하기 어렵다.  
둘째, 일부 극단적인 데이터에 의해 직선이 크게 흔들리면 분류 기준도 같이 바뀔 수 있다.

따라서 분류 문제에서는 예측값을 0과 1 사이로 제한할 수 있는 새로운 모델이 필요하다.

3. Logistic Regression and Sigmoid

로지스틱 회귀(Logistic Regression)는 이름에는 회귀가 들어가지만, 실제로는 이진 분류를 위한 모델이다.  
핵심 아이디어는 선형 결합 $ \theta^T x $를 그대로 출력하지 않고, 시그모이드 함수에 통과시키는 것이다.

시그모이드 함수

$ g(z) = \frac{1}{1 + e^{-z}} $

어떤 실수 $ z $가 들어와도 결과를 항상 0과 1 사이의 값으로 바꾼다.



$ z \to \infty $ 이면 $ g(z) \to 1 $

$ z = 0 $ 이면 $ g(z) = 0.5 $

$ z \to -\infty $ 이면 $ g(z) \to 0 $

 

따라서 로지스틱 회귀의 예측 함수는 다음과 같이 정의된다.

$ h_\theta(x) = g(\theta^T x) = \frac{1}{1 + e^{-\theta^T x}} $

이 값은 입력 $ x $가 positive class, 즉 $ y = 1 $에 속할 확률로 해석할 수 있다.



$ h_\theta(x) = P(y = 1 \mid x; \theta) $

$ 1 - h_\theta(x) = P(y = 0 \mid x; \theta) $

그래서 일반적으로 다음 기준으로 분류한다.

$ h_\theta(x) \ge 0.5 \Rightarrow y = 1 $

$ h_\theta(x) < 0.5 \Rightarrow y = 0 $

 

모델의 파라미터 $ \theta $는 주어진 데이터를 가장 그럴듯하게 설명하도록 찾는다.  
이를 위해 하나의 데이터에 대한 확률을 다음처럼 쓸 수 있다.

$ P(y \mid x; \theta) = \left(h_\theta(x)\right)^y \left(1 - h_\theta(x)\right)^{1-y} $

이 식은 $ y=1 $이면 $ h_\theta(x) $만 남고, $ y=0 $이면 $ 1-h_\theta(x) $만 남도록 만든 표현이다.

전체 데이터에 대해 likelihood를 만들고 로그를 취하면 로그 우도 함수가 된다.

$ \ell(\theta) = \sum_{i=1}^{n} \left[ y^{(i)}\log h_\theta(x^{(i)}) + (1-y^{(i)})\log(1-h_\theta(x^{(i)})) \right] $

로지스틱 회귀는 이 로그 우도 함수가 최대가 되도록 $ \theta $를 학습한다.  
반대로 이 식에 마이너스를 붙여 최소화하면 Cross-Entropy Loss 또는 Log Loss가 된다.

 

4. Newton's method

로지스틱 회귀에서는 로그 우도 함수 $ L(\theta) $가 최대가 되는 파라미터 $ \theta $를 찾아야한다.



$ \theta \in \mathbb{R}^d $

$ L: \mathbb{R}^d \to \mathbb{R} $

즉, $ \theta $는 하나의 숫자가 아니라 $ d $차원 벡터이고, 로그 우도 함수 $ L(\theta) $는 그 벡터를 입력받아 하나의 실수 값을 출력한다.

최댓값이 되는 지점에서는 기울기가 0이 된다.

$ \nabla_\theta L(\theta) = 0 $

따라서 뉴턴 메서드는 로그 우도 함수 자체를 바로 푸는 것이 아니라, 그 기울기 함수의 근을 찾는 방식으로 이해할 수 있다.

$ F(\theta) = \nabla_\theta L(\theta) $

$ F(\theta) = 0 $

1차원에서 뉴턴 메서드는 다음과 같이 쓴다.

$ \theta^{(t+1)} = \theta^{(t)} - \frac{f(\theta^{(t)})}{f'(\theta^{(t)})} $

여기서 $ f(\theta) = L'(\theta) $로 두면,

$ \theta^{(t+1)} = \theta^{(t)} - \frac{L'(\theta^{(t)})}{L''(\theta^{(t)})} $

이 된다.

이를 다차원으로 확장하면, 1차 미분은 그래디언트 벡터가 되고 2차 미분은 헤시안 행렬이 된다.

$ \nabla_\theta L(\theta) \in \mathbb{R}^d $

$ H(\theta) = \nabla_\theta^2 L(\theta) \in \mathbb{R}^{d \times d} $

따라서 다차원 Newton Method의 업데이트 식은 다음과 같다.

$ \theta^{(t+1)} = \theta^{(t)} - H(\theta^{(t)})^{-1}\nabla_\theta L(\theta^{(t)}) $

같은 식을 이동량 $ \Delta^{(t)} $로 쓰면 더 직관적이다.

$ H(\theta^{(t)})\Delta^{(t)} = \nabla_\theta L(\theta^{(t)}) $

$ \theta^{(t+1)} = \theta^{(t)} - \Delta^{(t)} $

즉, 뉴턴 메서드는 현재 지점에서 그래디언트와 헤시안 정보를 이용해 “얼마나 이동해야 기울기가 0에 가까워질지”를 계산한다.

이 방법은 Gradient Descent보다 빠르게 수렴할 수 있다. 하지만 파라미터가 $ d $개라면 매 반복마다 $ d \times d $ 크기의 헤시안 행렬을 계산해야 하고, 역행렬 계산도 필요하다.

$ H(\theta) \in \mathbb{R}^{d \times d} $

$ H(\theta)^{-1} $

역행렬 계산 비용은 대략 $ O(d^3) $이므로, 파라미터 수가 적은 고전적인 통계 모델에서는 유용하지만, 

파라미터가 매우 많은 현대 딥러닝 모델에서는 직접 사용하기 어렵다. (2022)

 

Rough Companion

 

Method Per iteration Compute Steps to Error
SGD 1 data point $ \Theta(d) $ $ \epsilon $: $ O(\epsilon^{-2}) $
Mini-batch Gradient Descent $ b $ data points $ \Theta(bd) $ $ \epsilon $: between SGD and Batch GD
Batch Gradient Descent $ n $ data points $ \Theta(nd) $ $ \theta^{(t+1)} = \theta^{(t)} - \alpha \nabla J(\theta^{(t)}) $
Newton's Method $ n $ data points $ \Omega(nd^2) $ $ \epsilon $: $ O(\log\log(1/\epsilon)) $

 

 

 

 

모든 강의 자료와 영상은 공개된 해당 링크들을 통해서 학습하고 있다.

강의자료: https://cs229.stanford.edu/syllabus-spring2022.html

강의 영상: https://www.youtube.com/playlist?list=PLoROMvodv4rNyWOpJg_Yh4NSqI4Z4vOYy

2강 슬라이드: https://drive.google.com/file/d/1O_zLW3xvxv7w3R-kw7ZZMRPMcgY-gcim/view


 

요약

1. Linear Regression: 입력값 $ x $와 가중치 $ \theta $의 선형 결합으로 예측값 $ h_\theta(x)=\theta^Tx $를 만드는 회귀 모델을 정리한다.

2. Gradient Descent & LMS Algorithm: 비용 함수 $ J(\theta) $를 최소화하기 위해 기울기를 계산하고 파라미터 $ \theta $를 반복적으로 업데이트하는 방법을 살펴본다.

3. Batch vs Stochastic vs Mini-batch: 한 번의 업데이트에 사용하는 데이터 개수에 따라 Batch, SGD, Mini-batch 방식이 어떻게 달라지는지 비교한다.

 

0. Supervised Learning

Given Training Set

$ \{(x^{(1)}, y^{(1)}), \cdots, (x^{(n)}, y^{(n)})\} $

$ x^{(i)} \in \mathcal{X}, \quad y^{(i)} \in \mathcal{Y} $

좋은 $ h: \mathcal{X} \to \mathcal{Y} $ 찾기

 

y 가 discrete(이산형) 일때, classification

y 가 continous(연속형) 일때, regression

 

1. Linear regression

주택 가격 예시

$ h: Lot.area \to Price $ 찾기

 

$ h(x) = \theta_0 + \theta_1 x_1 $

 

선형이지만, 엄밀히는 아핀 모델(Affine Model)

절편($ \theta_0 $)이 포함된 아핀 모델의 계산 번거로움을 피하고자 

입력값에 $ x_0 = 1 $을 강제로 추가하는 트릭입니다. 

이를 통해 모든 식을 하나의 선형 결합($ \theta^T x $)으로 묶어, 단순화할 수 있습니다.

 

$ h(x) = \theta_0 x_0 + \theta_1 x_1 + \cdots + \theta_d x_d $

$ = \sum_{j=0}^{d} \theta_j x_j $

 

벡터 표기

 

$
\theta =
\begin{bmatrix}
\theta_0 \\
\theta_1 \\
\theta_2 \\
\vdots \\
\theta_d
\end{bmatrix}
$, $
x^{(i)} =
\begin{bmatrix}
x_0^{(i)} \\
x_1^{(i)} \\
\vdots \\
x_d^{(i)}
\end{bmatrix}
$

 

$ (x_0^{(i)} = 1) $

 

행렬 표기

 

$
X =
\begin{bmatrix}
(x^{(1)})^T \\
(x^{(2)})^T \\
\vdots \\
(x^{(n)})^T
\end{bmatrix}
\in \mathbb{R}^{n \times (d+1)}
$

 

그래서

$ h(x) = \theta_0 x_0 + \theta_1 x_1 + \cdots + \theta_d x_d $, 

$ h_\theta(x) \approx y $

에서

 

'어떤 가중치()를 써야 예측값이 실제 정답()과 가장 비슷해질까?'를 찾기 위해

비용 함수(Cost Function) $ J(\theta) $를 도입한다. (최소제곱법(Least Squares)을 기반)

 

$ J(\theta) = \frac{1}{2}\sum_{i=1}^{n}\left(h_\theta(x^{(i)}) - y^{(i)}\right)^2 $

 

$ min J(\theta) $

2. 경사 하강법 (Gradient Descent) & LMS Algorithm

$J(\theta)$를 최소화하기 위해, 미분을 통해 기울기를 구하고 파라미터 를 점진적으로 업데이트하는

LMS(Least Mean Squares)를 구한다.

 

$
\theta^{(0)} = 0
$

 

$
\theta_j^{(t+1)}
:=
\theta_j^{(t)}
-
\alpha
\frac{\partial}{\partial \theta_j}
J(\theta^{(t)})
$

(α: learning rate, 한 번에 얼마나 이동할지 정하는 값)

 

 

$
\frac{\partial}{\partial \theta_j}J(\theta)
=
\sum_{i=1}^{n}
\frac{1}{2}
\frac{\partial}{\partial \theta_j}
\left(h_\theta(x^{(i)}) - y^{(i)}\right)^2
$

여기서 $ h_\theta(x^{(i)}) - y^{(i)} $는 i번째 데이터에 대한 예측 오차이다.

 

그래서

 

$
h_\theta(x)
=
\theta_0 x_0
+
\theta_1 x_1
+
\theta_2 x_2
+
\cdots
+
\theta_d x_d
$

 

이렇게 표현해볼 수 있고,

미분하면 다음과 같다.

 

$
\frac{\partial}{\partial \theta_j}h_\theta(x)
=
x_j
$

 

$ \theta^{(t+1)} := \theta^{(t)} - \alpha \sum_{i=1}^{n} \left( h_\theta(x^{(i)}) - y^{(i)} \right) x^{(i)} $

 

α는 학습률이다. 보통 상수처럼 쓰인다. 

학습이 진행될수록 학습률을 감소시키며, 고전적인 머신러닝에서는 α를 사용자가 직접 조정해야 하는 하이퍼파라미터로 본다.

 

3. Batch vs Stochastic Mini Batch

 

Batch Gradient Descent (BGD)

Batch Gradient Descent는 파라미터를 한 번 업데이트할 때 전체 학습 데이터 $ n $개를 모두 사용한다.

$$ \theta^{(t+1)} := \theta^{(t)} - \alpha \frac{1}{n}\sum_{i=1}^{n} \left(h_\theta(x^{(i)}) - y^{(i)}\right)x^{(i)} $$

전체 데이터를 사용하므로 기울기 방향은 비교적 안정적이다. 하지만 데이터가 수백만 개 이상으로 커지면, 한 번 업데이트하는 데 필요한 계산 비용과 메모리 사용량이 매우 커진다.

Stochastic Gradient Descent (SGD)
Stochastic Gradient Descent는 한 번 업데이트할 때 무작위로 선택한 데이터 1개만 사용한다.

$$ \theta^{(t+1)} := \theta^{(t)} - \alpha \left(h_\theta(x^{(i)}) - y^{(i)}\right)x^{(i)} $$

매 업데이트가 빠르다는 장점이 있지만, 데이터 하나에만 의존하므로 업데이트 방향이 불안정하다. 그래서 최솟값을 향해 부드럽게 내려가기보다는 지그재그로 움직이는 경향이 있다.

Mini-batch Gradient Descent

Mini-batch Gradient Descent는 Batch와 SGD의 중간 방식이다. 전체 데이터가 아니라, 무작위로 선택한 작은 데이터 묶음 $ B_t $만 사용한다. 이때 mini-batch의 크기를 $ b $라고 하면 보통 $ b \ll n $이다.

$$ \theta^{(t+1)} := \theta^{(t)} - \alpha \frac{1}{b}\sum_{i \in B_t} \left(h_\theta(x^{(i)}) - y^{(i)}\right)x^{(i)} $$

즉, 전체 데이터로 계산한 정확한 기울기 대신 일부 데이터로 계산한 근사적인 기울기를 사용한다. 

계산 비용은 줄이면서도 SGD보다 안정적인 업데이트 방향을 얻을 수 있기 때문에, 실제 머신러닝에서는 mini-batch 방식이 가장 많이 사용된다.

 

 

후기

글 작성한다고, 강의를 한 번 더 보면서 수식과 함께 정리했다.

잘 이해하고 넘어가자.

4월 15일 쯤 부터 공부하고 있던 강의

AI가 계속 발전하면서, 개발에도 잘 활용하고 있지만,

내부구조가 어떻게 돌아가게 되는지, 학부때는 배우지 않았던 개념들, 

이 강의(CS229)는 Andrew Ng 교수님의 2018버전이 유명하지만,

조금 더 업데이트된 2022년 Tengyu Ma, Christopher Ré 교수님 버전을 듣게 되었다.

 

모든 강의 자료와 영상은 공개된 해당 링크들을 통해서 학습하고 있다.

강의자료: https://cs229.stanford.edu/syllabus-spring2022.html

강의 영상: https://youtube.com/playlist?list=PLoROMvodv4rNyWOpJg_Yh4NSqI4Z4vOYy&si=Wyde6SsfMflwc1Lk

1강 슬라이드: https://cs229.stanford.edu/lectures-spring2022/lecture1.pdf


 

머신러닝의 종류

 

데이터에 정답 label이 있는가?
없다면 데이터 안에서 어떤 구조를 찾는가?
혹은 모델이 직접 행동하면서 데이터를 모으는가?

 

이런 기준에 따라 머신러닝 문제는 크게

supervised learning, unsupervised learning, reinforcement learning으로 나뉜다.

 

1. Supervised Learning: 입력 x에서 정답 y를 예측하기

Supervised learning에서는 입력 x와 정답 y가 함께 주어진다. 주택 가격 예측에서는 집의 크기 x를 바탕으로 가격 y를 예측한다.

예를 들어 주택 가격 예측 문제를 생각해보자. 데이터셋에는 여러 개의 샘플이 있고, 각 샘플은 다음처럼 입력과 정답으로 구성된다.

(x⁽¹⁾, y⁽¹⁾), ..., (x⁽ⁿ⁾, y⁽ⁿ⁾)

 

여기서 x는 입력 feature이고, y는 예측하고 싶은 label 또는 output이다.

 

주택 가격 예측에서는 x가 집의 크기이고, y가 집의 가격이다.
즉 모델의 목표는 다음과 같은 함수를 찾는 것이다.

 

x → y

 

예를 들자면

집의 크기(x)로 → 집의 가격(y)을 예측하는 함수를 찾는 것이다.

이때 중요한 점은 supervised learning에서는 이미 정답 y가 주어져 있다는 것이다.

 

Feature가 많아진다는 것

x = living size
y = price

 

처음엔 집의 크기만으로 가격을 예측할 수 있지만, 

실제 문제에서는 단순히 크기 하나만으로 결정되지 않는다.

lot size, 층 수, condition, zip code, 등등

 

x = [x₁, x₂, x₃, ..., x_d]

x₁ = living size
x₂ = lot size
x₃ = number of floors
x₄ = condition
x₅ = zip code ...

 

입력 x는 하나의 숫자가 아니라 여러 feature를 가진 벡터가 된다.

 

이때 x ∈ R^d라고 표현한다.
 
x는 d차원 실수 벡터라는 뜻이다.

 

 

Regression vs Classification

Supervised learning은 다시 regression과 classification으로 나뉜다.

 

 

Regression

Regression은 예측해야 하는 값 y가 연속적인 값일 때 사용한다.

예를 들어 주택 가격 예측에서는 가격이 1억, 1.5억, 2.3억처럼 연속적인 숫자로 표현된다.

 

x = 집의 feature
y = 가격

 

Classification

Classification은 예측해야 하는 y가 이산적인 label일 때 사용한다.

 

예를 들어 집의 크기와 lot size를 보고 이 주거 형태가 house인지 townhouse인지 예측한다고 해보자.

(size, lot size) → house or townhouse?

 

y는 연속적인 숫자가 아니라 정해진 class 중 하나다.

y ∈ {house, townhouse}

 

y/n, 1,0 -1,1 등으로 표기하곤 한다.

 

 

Computer Vision, NLP

Computer Vision에서는 image classification, object localization, object detection 같은 문제가 supervised learning으로 다뤄질 수 있다.

 

Image classification

이미지 → 고양이 / 강아지 / 자동차 / 사람 ...

 

Object detection

이미지 안에서 객체가 무엇인지뿐 아니라, 어디에 있는지!

bounding box 그리거나 하는 그런것들,,

 

NLP

Machine Translation 같은 번역,,

 

 

2. Unsupervised Learning: label 없이 구조 찾기

Supervised learning과 달리 unsupervised learning에서는 데이터셋에 label이 없다.

 

 

즉 데이터가 다음처럼 주어진다.

x⁽¹⁾, x⁽²⁾, ..., x⁽ⁿ⁾

 

 y가 없다.

 

그래서 목표도 조금 다르다.
정답을 맞히는 것이 아니라, 데이터 안에 숨어 있는 흥미로운 구조를 찾는 것이 목표다.

 

대표적인 예시는 clustering이다.

예를 들어 유전자 데이터를 보고 비슷한 패턴을 가진 gene끼리 묶거나, 문서 데이터를 보고 비슷한 주제를 가진 글끼리 묶을 수 있다.

 

Supervised learning에서는 이미 house와 townhouse label이 표시되어 있다.
반면 unsupervised learning에서는 label 없이 점들만 있고, 알고리즘이 그 안에서 비슷한 그룹을 찾아야 한다.

 

Latent Semantic Analysis와 Word Embedding

NLP 쪽 예시로는 Latent Semantic Analysis, Word Embedding, LLM 등이 언급되었다.

 

Latent Semantic Analysis: 문서와 단어 사이의 관계를 보고 의미 구조를 찾는 방식

예를 들어 여러 문서가 있고, 각 문서에 어떤 단어가 얼마나 자주 등장하는지를 행렬처럼 표현!

 

Word Embedding

단어를 벡터로 표현하면, 의미가 비슷한 단어들은 비슷한 위치에 놓이고,

또 단어 사이의 관계가 벡터의 방향으로 표현될 수 있다.

깊게 다루진 않았지만, 유사한 단어 끼리 묶고, 유사한 관계는 비슷한 방향에 놓이도록 하는 방식인 듯 하다. 

Korea와 Seoul은 임의로 그려봤다.

 

Reinforcement Learning: 직접 행동하며 배우기

 

Supervised learning과 unsupervised learning은 기본적으로 주어진 데이터셋에서 학습한다.

Reinforcement learning은 데이터로 상호적으로 수집, 시도-실패 데이터로 학습 알고리즘에 통합하고 다음 전략을 수립한다.

 

행동 → 결과 관찰 → 피드백 → 전략 개선

 

이때 중요한 차이는 데이터가 고정되어 있지 않다는 것이다.
알고리즘이 어떤 행동을 하느냐에 따라 새롭게 수집되는 데이터가 달라진다.

 

예를 들어 원자력 발전소 제어 문제를 생각하면, 알고리즘이 매일 새로운 전략을 바꾸라고 지시하는 방식은 위험하고 비효율적일 수 있다.
현실에서는 일정 기간 동안 전략을 적용하고, 그 결과를 관찰한 뒤, 더 나은 전략을 찾는 방식이 필요할 것이다.

 

 

정리

Machine Learning
├── Supervised Learning
│   └── label y 있음
│   └── regression / classification
├── Unsupervised Learning
│   └── label y 없음
│   └── clustering / LSA
└── Reinforcement Learning
    └── 환경과 상호작용
    └── 행동 → 피드백 → 전략 개선

 

4강까지 듣는 중인데, 수학 개념을 중심으로 전개하고 풀어가는 과정이 많아서 중간중간 수학강의도 듣고 있다.

MLX로 일본어 음성을 생성하고(Qwen3-TTS), Firebase에 넣어 iOS 앱에서 재생시키기

들어가며

우리 앱의 가장 큰 문제점 중 하나는 tts가 애플 기본 tts여서,
듣는 맛이 없었다...
콘텐츠를 잘 넣는 것만으로는 부족했다.
그러던 찰나..
1월 말 쯔음 Qwen에서 목소리 복제, 생성 등이 가능한 TTS 모델이 나왔다는 소식을 접했고
바로 적용해보고자 파이프라인을 고안했다.

  • 앱 안에서 직접 TTS를 돌리지 않고
  • 내 로컬 Mac에서 mlx-audio로 음성을 미리 생성
  • 그 결과물을 Firebase Storage에 올린다.
  • Firestore 문서에 audioURL만 연결한다.
  • 앱은 그 URL이 있으면 스트리밍하고, 없으면 기존 Apple TTS로 재생

모델 선택지

현재 애플 기본 TTS구현이 가장 간단함일본어 억양이 기계적으로 들리는 경우가 있음
클라우드 TTS API품질이 안정적임호출 비용, 운영 복잡도, 벤더 종속
Qwen3-TTS 0.6B가벼움품질이 아쉬운 경우가 있었음
Qwen3-TTS 1.7B CustomVoice품질이 좋고 로컬 실행 가능생성 파이프라인과 후처리를 직접 만들어야 함

결국 mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-bf16를 선택했다.
이때 로컬에서 AI 모델 추론이라는 걸 처음 해봤는데,
맥(Apple Silicon)에서는 CUDA가 없으니, MLX로 돌릴 수 있다는 게 신기했다.
다만 일반적으로 AI 모델은 CUDA 기반에서 만들어져서 MLX에서 누군가 변환을 해줘야만 하기에, 종속성이 생기는 부분에서 아쉽긴 했다.

전체 구조

  1. Google Sheets 기반 원본 데이터를 CSV로 정리한다.
  2. 로컬에서 카테고리별 TTS 스크립트를 돌린다.
  3. 생성된 WAV를 M4A로 변환한다.
  4. 결과물을 Firebase Storage의 버전 경로에 업로드한다.
  5. Firestore의 Sentence, Word, WordMeaning 문서에 audioURL을 기록한다.
  6. 앱은 동기화 후 audioURL이 있으면 AVPlayer로 재생하고, 없거나 실패하면 Apple TTS로 재생

카테고리별로 생성기를 나눈 이유

처음엔 하나의 스크립트로 다 해결할 수 있을 것 같았다.
그런데 실제로는 문장, 단어, 예문이 전부 다른 문제를 일으켰다.
그래서 생성 파이프라인을 세 갈래로 나눴다.

카테고리 스크립트 특징
Sentence generate_audio_batch.py 긴 문장, 캐릭터 이름, 고유명사 치환이 중요
Word generate_audio_word.py 짧은 단어, 외래어, 잘림과 늘어짐 리스크가 큼
WordMeaning generate_audio_wordMeaning.py 예문 끝음 유실, 재시도 전략이 중요

이 분리가 생각보다 중요했다.
같은 모델이라도 입력 길이와 형태가 달라지면 실패 패턴도 완전히 달랐다.

1. 문장 TTS: 고유명사 한자 발음 오류부터 잡기

긴 문장 오디오는 전반적으로 가장 안정적이었다.
문제는 캐릭터 이름이나 작품 특유의 고유명사였다.

예를 들어 冨岡 같은 이름은 사람이 보면 당연히 とみおか인데, 모델은 완전히 다른 음독 비슷한 발음을 내는 경우가 있었다.
문장 전체는 자연스러운데 이름 하나가 틀리면 사용자는 바로 어색함을 느낀다.

그래서 문장 생성에서는 “고유명사 히라가나 치환” 전략을 썼다.

QUOTE_NAME_REPLACEMENTS = {
    "KIMETS_S004": [("冨岡", "とみおか"), ("冷める", "さめる")],
    "SPYFAM_S003": [("黄昏", "たそがれ")],
    "ATTACK_S006": [("ベルトルト", "べるとると")],
}

포인트는 모든 한자를 무조건 히라가나로 바꾸지 않았다는 점이다.
정상적으로 잘 읽는 단어는 그대로 두고, 실제로 틀린 케이스만 사전처럼 치환했다.
같은 텍스트면 같은 결과가 나오게 해서, 운영 중 다시 생성해도 결과가 크게 흔들리지 않게 했다.

2. 단어 TTS: 짧은 단어는 생각보다 훨씬 까다로웠다

가장 많이 고생한 쪽은 Word였다.

애니센텐스의 단어는 보통 짧다.
한두 음절짜리도 많고, 외래어 가타카나도 있다.
이런 입력은 모델이 아주 안정적으로 읽어줄 것 같지만, 실제로는 반대였다.
대표적인 실패 패턴은 이랬다.

  1. 한 음절만 길게 늘어진다.
  2. 공기 소리나 노이즈만 나온다.
  3. 앞이나 뒤가 잘려서 단어 일부만 남는다.

특히 ファン 같은 짧은 외래어는 단독 발화에서 공기음만 남는 경우가 반복됐다.
言い捨てる 같은 짧은 구문은 특정 음절만 늘어지는 문제가 있었다.

처음에는 텍스트 표기를 바꿔봤다.
히라가나로도 넣어보고, 한자로도 넣어보고, 발음형으로도 넣어봤다.
일부는 좋아졌지만 모든 케이스를 해결하지는 못했다.
그 다음에는 후처리를 단순화했다.
예전보다 후보 선택을 줄이고, trim과 길이 제한을 보수적으로 가져가면서 전체 안정성은 좋아졌다.
그래도 극단적인 케이스는 남았다.
결국 여기서 쓴 해법은 “문장으로 감싸서 생성한 뒤 필요한 부분만 잘라낸다”였다.
예를 들어 어떤 단어를 직접 읽히지 않고,

これは、X、です。

 
같은 임시 문장 안에 넣어서 생성한 뒤, 가운데 내가 뽑고자 하는 단어(X)만 추출했다.
 

WORD_TEXT_OVERRIDES = {
    "MYHERO_S008_W001": "これは、きのう、です。",
    "ATTACK_S018_W002": "これは、はず、です。",
    "HAIKYU_S009_W001": "これは、しゅっしん、です。",
}

 
그리고 분할 후에는 다시 가장 자연스러운 voiced segment를 고르고, 너무 길면 강제로 잘랐다.
이 방식은 정답은 아니겠지만, 모델 탓 할 수도 없고
개발자는 문제를 해결해야한다..!
결론적으로 대부분의 단어는 안정적으로 생성됐고, 모든 음성을 대응할 수 있었다.

로컬 생성물을 앱에 붙이는 방식

생성만 잘한다고 끝은 아니었다.
앱에 실제로 붙는 구조가 중요했다.

내가 택한 방식은 Firestore 문서에 audioURL을 두는 것이었다.

대상 필드는 세 군데였다.

  1. Sentences.audioURL
  2. Words.audioURL
  3. Words.meanings[].audioURL

앱은 Firestore 동기화 시 이 필드들을 함께 받아 SwiftData에 저장한다.
그 다음 재생 시점에 audioURL이 있으면 Firebase Storage에서 재생하고, 없거나 실패하면 기존 Apple TTS를 그대로 사용한다.

이 구조의 장점은 명확했다.

  1. 새 오디오가 없는 콘텐츠도 문제없이 동작한다.
  2. 일부 항목만 먼저 적용하는 점진 배포가 가능하다.
  3. 음성 파일이 깨져도 학습 기능 자체는 죽지 않는다.

 

앱 재생 쪽에서 신경 쓴 것

앱 쪽도 단순히 URL 하나 받아서 재생하는 수준에서 끝나지 않았다.

TTSManager에서는 다음 순서로 움직이게 했다.

  1. 먼저 로컬 캐시에 같은 audioURL 파일이 있는지 본다.
  2. 있으면 즉시 로컬 파일을 재생한다.
  3. 없으면 백그라운드로 다운로드를 시작한다.
  4. 동시에 Firebase Storage download URL을 해석해 스트리밍 재생한다.
  5. 재생 실패 시 Apple TTS로 폴백한다.

여기에
앱 진입시, “오늘의 문장 + 오늘 학습할 단어 2개” 오디오를 미리 선다운로드하는 prefetch도 넣었다.
 
덕분에 실제 학습 플로우에서 재생 버튼을 눌렀을 때 체감 속도가 좋아졌다.
사용자 입장에서 중요한 건 기술 스택이 아니라 경험이다.
버튼을 눌렀을 때 바로 재생되면 자연스럽게 느껴지는 TTS가 되고, 버벅이거나 실패하면 바로 문제를 느끼게된다.
그래서 생성 품질 못지않게 캐시와 prefetch가 중요했다.

기존 유저를 위한 마이그레이션

이 기능을 새로 붙일 때 한 가지 걱정이 있었다.

이미 앱을 쓰고 있던 유저들은 로컬에 예전 데이터가 남아 있다.
이 상태에서 서버 문서에 audioURL만 추가해도, 앱이 “버전이 안 바뀌었다”고 판단하면 새 필드를 못 받아올 수 있다.

그래서 앱 쪽에 1.1.0 타겟의 1회 강제 동기화 로직을 넣었다.
로컬 버전과 원격 버전이 같아도, 특정 앱 버전 이상에서는 한 번 강제로 전체 데이터를 다시 받아오게 만든 것이다.

이건 아주 작은 디테일 같지만 실제 운영에서는 중요했다.
새 필드 하나를 붙였다고 해서 모든 유저가 자동으로 그 필드를 잘 받는 건 아니기 때문이다.
기능을 넣는 것과, 이미 배포된 사용자에게 그 기능이 실제로 도달하게 만드는 것은 전혀 다른 문제였다.

배포는 버전 경로로 관리했다

오디오 파일은 Firebase Storage의 버전 경로로 관리했다.
예를 들면 이런 식이다.

tts/v0.0.2/sentence/...
tts/v0.0.2/word/...
tts/v0.0.2/wordMeaning/...

이 방식의 장점은 캐시 충돌을 피할 수 있다는 점이다.
기존 파일을 덮어쓰기보다, 변경분만 새 경로에 올리고 Firestore의 audioURL만 새 경로로 바꾸면 된다.

이 흐름 덕분에 “오디오만 교체”하는 작업이 앱 배포와 분리됐다.
운영 속도가 빨라졌고, 문제가 생겨도 이전 경로로 되돌리기가 쉬웠다.

마무리

팀원들의 반응

팀원들도 반응이 좋아서 뿌듯했다.
요즘 앱 사용자가 줄어들고 있어서 가슴이 아프지만..
개발하고 개선하고, 발전시키고 이런것들이 뿌듯하고 재밌다. 
 
다음에는 이 파이프라인을 더 자동화해서,
문장 생성, 업로드, 검수, 부분 재생성까지 더 짧은 루프로 돌릴 수 있게 다듬어보려 한다.

Google Sheets → Firestore → 앱 동기화, 그리고 BigQuery 분석까지.
AI 에이전트가 실행할 수 있는 운영 자동화 시스템을 만든 과정.


📋 목차

  1. [들어가며]
  2. [기존 운영 방식 — 수작업의 고통]
  3. [전환의 출발점 — Skill + Helper 스크립트 도입]
  4. [아키텍처 — 에이전트가 읽는 운영 매뉴얼]
  5. [대사 파이프라인 — Sheets에서 앱까지]
  6. [분석 파이프라인 — GA4에서 리포트까지]
  7. [브랜치별 진화 — 점진적 자동화의 기록]
  8. [보안 설계 — 키 감추기]
  9. [정리]

1. 들어가며

요즘 정말 1주일에 한 번씩은 AI 쪽에서 큰 게 나오는 것 같다. 팔로업하면서 거의 버티기로 하루하루를 보내고 있다.

Antigravity, Codex, Claude를 돌아가면서 활용하고,

꼬이지 않게SKILL.md(에이전트를 위한 작업 지침서)나 report.md(지금까지 작업한 것들을 요약해서 AI에게 문맥을 놓치지 않게 하는 기록) 같은 문서를 적극적으로 쓰고 있다.

이번 글은 2026년 초에 구축한 콘텐츠 운영 자동화 파이프라인에 대한 회고다.


애니센텐스는 애니메이션 명대사로 일본어를 학습하는 iOS 앱이다. 콘텐츠(대사, 단어, 이미지)는 Firebase에서 관리하고,

유저 데이터는 GA4 + BigQuery로 분석한다.

현재진행형인 앱이고,
앱 개발 자체보다 콘텐츠 운영이 병목이 있었다.

새 애니의 대사 하나를 추가하려면:

  1. Google Sheets에 대사/단어 데이터를 손으로 입력 (기획 친구의 입력)
  2. Firebase Console에 접속해 이미지를 Storage에 업로드
  3. Apps Script를 실행해 Sheets → Firestore로 데이터를 넣기
    • 이때 데이터 버전도 올리며 사용자가 앱 최신 버전을 업데이트 할 수 있도록
  4. BigQuery 콘솔에서 SQL을 직접 실행해 분석 리포트를 뽑기

매번 3~4개의 웹 콘솔을 오가며 반복하는 작업이었고, 헷갈리고 시간도 오래 잡아먹었다.

Google Sheets에 기획자 친구가 대사 넣어주면 Apps Script로 Firebase에 넣어주는 것도,

사실 이전의 수작업 방식을 더 수월하게 만든 작업이었다.

이것도 안 했다면, Google Sheets에 내용 복사해서 Firebase Console에서 수작업으로 하나하나 문서랑, 컬렉션이랑, 각종 필드(대사, 한국어, 애니 제목, 히라가나 발음 등등)를 넣었어야 했을 것이다. (으악)


2. 기존 운영 방식 — 수작업의 고통

문제점:

  • 시간 소모: 콘텐츠 1세트 추가에 최소 5분
  • 오타 위험
  • 컨텍스트 스위칭: Xcode ↔ Sheets ↔ Firebase Console ↔ BigQuery 콘솔, 4개의 탭을 왔다갔다 하는 것 자체가 인지 부하
  • 분석 병목: BigQuery SQL을 매번 콘솔에 복사 붙여넣기하고, 결과 ai에게 정리해달라고 하기..

3. 전환의 출발점 — Skill + Helper 스크립트 도입

2026년 1월, Codex와 Antigravity 같은 AI 코딩 에이전트를 적극 활용하기 시작하면서,

아무래도 개발자의 숙명인 반복작업 줄이기와 편한 플로우를 짜고자했다.

"Firebase CLI나 Console GUI 대신,
AI한테 "해 줘" 라고 말하면 되게끔...

 

이것이 Skill + Helper 스크립트 접근법의 핵심이다. 브라우저 자동화나 CLI 명령어 대신,

Python 스크립트로 Firebase/Sheets/BigQuery API를 직접 호출하는 방식을 택했다.
(글쓰고 있는 지금은 모바일에서도 "해 줘"가 가능한 상태이다.)

핵심 구조

.agent/
├── skills/
│   └── SKILL.md          ← 에이전트가 읽는 "운영 매뉴얼"
├── scripts/
│   ├── serviceAccountKey.json  ← 🔒 gitignore (인증 키)
│   ├── bigquery_helper.py      ← BigQuery SQL 실행기
│   ├── firebase_helper.py      ← Storage 업로드 + Firestore 수정
│   ├── sheets_helper.py        ← Google Sheets 읽기/쓰기
│   ├── fetch_sheet_data.py     ← Sheets → CSV 추출
│   └── queries.sql             ← 주간 분석 쿼리 모음
└── venv/                       ← 🔒 gitignore (가상환경)

4. 아키텍처 — 에이전트가 읽는 운영 매뉴얼

SKILL.md — 에이전트의 진입점

.agent/skills/SKILL.md는 에이전트가 작업을 시작할 때 가장 먼저 읽는 문서다.

---
name: project_automation
description: ChicagoPizza 프로젝트의 Firebase, BigQuery, Google Sheets 운영 자동화를 위한 스킬셋입니다.
---

이 파일 하나에 두 가지 자동화 영역이 정의되어 있다:

 

1️⃣ 콘텐츠 운영 자동화

섹션 내용
🛠 환경 가상환경 경로(.agent/venv), 인증 파일 위치(serviceAccountKey.json)
🔥 Firebase firebase_helper.py로 Storage 업로드 + Firestore 메타 수정 방법
📅 Sheets sheets_helper.py로 특정 스프레드시트 읽기/쓰기 방법

 

목적: 콘텐츠 배포 — 대사, 단어, 이미지를 Firebase에 올리고 앱으로 내려보내기

 

2️⃣ 데이터 분석 자동화

섹션 내용
📊 BigQuery bigquery_helper.py 사용법, 테이블명(analytics_XXXXXXXXX.events_*), 프로젝트 ID

 

목적: 사용자 행동 분석 — GA4 이벤트를 BigQuery로 쿼리해 주간 리포트 생성


Helper 스크립트 — 단일 인증, 다중 서비스

모든 헬퍼 스크립트가 동일한 서비스 계정 키 하나(serviceAccountKey.json)로 인증한다.

# 모든 헬퍼 스크립트의 공통 패턴 (firebase / sheets / bigquery)
cred_path = os.path.join(os.path.dirname(__file__), 'serviceAccountKey.json')

firebase_helper.py, sheets_helper.py, bigquery_helper.py 모두 이 한 줄로 시작한다.

키 파일 하나로 Firebase, Sheets API, BigQuery를 모두 인증하는 구조다.


5. 대사 파이프라인 — Sheets에서 앱까지

이 자동화의 중심은 대사 콘텐츠가 기획자의 스프레드시트에서 사용자의 앱까지 흘러가는 파이프라인이다.

전체 흐름

 

sync vs publish — 운영 사고 방지

데이터 넣기(sync)와 앱에 알리기(publish)를 분리했다.

# Step 1: 데이터만 Firestore에 넣기 (앱에는 아직 알리지 않음)
.agent/venv/bin/python .agent/scripts/sync_sheets_to_firestore.py --action all

# Step 2: 데이터 검수 후 안전하게 배포
.agent/venv/bin/python .agent/scripts/sync_sheets_to_firestore.py --action publish

sync는 Sheets의 4개 시트(Anime, todaySentence, word, wordMeaning)를 읽어 Firestore에 밀어넣는다.

publish는 그 위에 배포 신호만 보내는 한 줄짜리 함수다:

def update_data_version(db):
    """메타 문서의 버전을 +1 → 앱이 다음 실행 시 감지"""
    doc_ref = db.collection('SystemMetaData').document('GlobalVersion')
    current = doc_ref.get().to_dict().get('versionCode', 0)
    doc_ref.update({'versionCode': current + 1})

 

왜 분리했나?

  • sync만 하면 → Firestore에는 데이터가 들어가지만, 앱은 아직 모름
  • publish까지 해야 → Meta/DataVersion이 올라가고, 앱이 감지
  • 미완성 데이터가 앱에 올라가는 사고를 원천 차단할 수 있다.

 

앱 측 데이터 동기화 — Meta/DataVersion 기반

자동화로 Firestore 데이터가 바뀌면, 앱이 이를 감지하는 메커니즘도 필요하다.

// FirestoreManager.swift
private let metaCollection = "SystemMetaData"
private let dataVersionDocID = "GlobalVersion"
private let dataVersionField = "versionCode"
private let localDataVersionKey = "CacheVersionKey"

func syncIfNeeded(modelContext: ModelContext, force: Bool = false) async {
    let remoteVersion = try await fetchRemoteDataVersion()
    let localVersion = getLocalDataVersion()

    if !force, hasLocal, remoteVersion <= localVersion {
        // 최신 → fetch 건너뜀
        return
    }

    // 원격 버전이 높으면 전체 동기화
    await fetchAllData(modelContext: modelContext)
    setLocalDataVersion(remoteVersion)
}

 

운영 흐름:

  1. 스크립트가 Firestore에 새 데이터를 넣음
  2. Meta/DataVersion 문서의 dataVersion 값을 +1
  3. 앱이 다음 실행 시 로컬 버전과 비교
  4. 원격이 높으면 → 전체 데이터 재동기화

데이터를 넣는 것(sync)과 앱에 알리는 것(publish)을 구분함으로써, 실수로 중간 상태의 데이터가 앱에 올라가는 사고를 예방한다.


6. 분석 파이프라인 — GA4에서 리포트까지

GA4 → BigQuery 연결

앱에서 Firebase Analytics(GA4)로 보내는 이벤트를 BigQuery에서 분석하는 파이프라인도 자동화 범위에 포함된다.

// AuthManager.swift — Firebase UID를 BigQuery user_id로 연결
AnalyticsManager.shared.setUserID(authResult.user.uid)

BigQuery 분석 자동화

BigQuery 분석 쿼리(queries.sql)에서는 build_type = 'release'로 필터링해 개발 중 테스트 이벤트를 제외한다:

-- queries.sql — 주차별 성장 지표
SELECT 
  CASE 
    WHEN _TABLE_SUFFIX BETWEEN '20251228' AND '20260103' THEN 'Week 1'
    ...
  END as week,
  COUNT(DISTINCT user_pseudo_id) as users,
  ...
FROM `analytics_XXXXXXXXX.events_intraday_*`
WHERE (SELECT value.string_value FROM UNNEST(user_properties) 
       WHERE key = 'build_type') = 'release'

 

에이전트에게 "이번 주 STT 성공률 분석해줘"라고 하면:

  1. SKILL.md를 읽고 → BigQuery 섹션 확인
  2. queries.sql에서 적절한 쿼리 선택
  3. .agent/venv/bin/python bigquery_helper.py "SELECT ..." 실행
  4. 결과를 마크다운 테이블로 정리해서 보여준다.

예시: 에이전트에게 "이번 주 리포트 뽑아줘"라고 말하면

에이전트: SKILL.md를 확인했습니다. BigQuery 헬퍼로 쿼리를 실행합니다.

| week   | users | sessions | stt_attempts | stt_success_rate |
|--------|-------|----------|--------------|------------------|
| Week 7 |    12 |       34 |          189 |            78.3% |
| Week 8 |    15 |       45 |          234 |            82.1% |

주간 성장: 유저 +25%, STT 성공률 +3.8%p

이전에는 BigQuery 콘솔에 접속 → SQL 붙여넣기 → 결과 스크린샷이었는데, 이제는 한 마디로 리포트가 나온다.

그리고 전체 리포트를 markdown 파일로 만들어 달라고 한 후 개인적인 검수 후,
팀 노션에 공유하면 끝이다.


7. 브랜치별 진화 — 점진적 자동화의 기록

이 자동화는 한 번에 완성된 것이 아니다. 운영 문제를 하나씩 분해하고, 각 브랜치에서 점진적으로 확장했다.

Phase 1: chore/automation-setup (2026-02-04)

목표: 콘솔 수작업을 대체할 기본 도구 세트 확보

SKILL.md + 헬퍼 스크립트 5개(bigquery_helper, firebase_helper, sheets_helper, queries.sql 등)를 한 번에 도입. 섹션 3의 디렉토리 구조가 이때 만들어졌다.

이 시점에서 달라진 것:
"Firebase Console을 열어야 한다" → "에이전트에게 '이 이미지 업로드해줘'라고 말하면 된다"

Phase 2: feat/mailbox (2026-02-06)

목표: 데이터 동기화와 배포 신호를 분리

추가된 파일 역할
sync_sheets_to_firestore.py Sheets 데이터를 Firestore에 동기화

 

  • sync: Sheets의 데이터를 Firestore에 밀어넣기 (아직 앱에 알리지 않음)
  • publish: Meta/DataVersion을 +1 해서 앱에 "새 데이터 있어!" 알림
  • --publish 또는 --action publish 플래그로 두 단계를 명시적으로 분리

이 시점에서 달라진 것:
"데이터 넣자마자 앱에 반영" → "데이터 검수 후 publish 명령으로 안전하게 배포"


8. 보안 설계 — 키 감추기

자동화에서 가장 신경 쓴 부분은 민감정보 분리였다.

분류 추적 여부 예시
실행 로직 ✅ Git 추적 firebase_helper.py, SKILL.md, queries.sql
인증 키 ❌ gitignore serviceAccountKey.json, GoogleService-Info.plist
런타임 환경 ❌ gitignore .agent/venv/
생성물 ❌ gitignore generated_audios/

스크립트는 Git에 올리되, 키는 올리지 않는다.

코드를 공유해도 키가 노출되지 않고, 새 환경에서 키만 넣으면 즉시 적용 가능하다.


9. 정리

💡 1. 초기 자동화는 "콘솔 대체"가 목표

처음부터 거창한 CI/CD 파이프라인을 설계하지 않았다. "Firebase Console을 여는 대신, 에이전트한테 말하면 되게 하자"가 전부였다. 작은 문제를 먼저 해결하고, 그 위에 점진적으로 쌓아올렸다.

💡 2. 스킬 문서는 "에이전트용 README"

SKILL.md는 사실상 에이전트가 읽는 README다.

사람이 읽어도 이해되는 수준으로 쓰되, 실행 경로와 인증 위치를 명확히 적는 것이 중요하다.

에이전트가 "어디에 뭐가 있는지" 모르면 해맬 수 있다.

💡 3. sync/publish 분리는 운영 사고를 예방한다

데이터를 넣는 것과 앱에 알리는 것을 분리하지 않으면, "아직 입력 중인 미완성 데이터가 앱에 내려가는" 사고가 발생한다.

--publish 플래그 하나를 추가한 것이 전체 운영 안정성에 큰 영향을 미쳤다.

💡 4. "버전관리되는 자동화 코드" + "로컬 비밀자산" 분리

스크립트는 Git에 올리되, 키는 올리지 않는다.

  • 다른 기기에서도 키만 넣으면 동일한 자동화 환경을 재현할 수 있고
  • 코드 리뷰에서 "어떤 자동화가 있는지"를 코드로 확인할 수 있다

Before / After 요약


마치며

에이전트가 실행할 수 있는 표준화된 스크립트와 지침서를 만들었다.

 

AI 코딩 에이전트의 진짜 가치는 "코드를 대신 짜주는 것"뿐 아니라, 반복적인 운영 작업의 실행자가 되어주는 것에 있다.

그리고 그러기 위해 필요한 것은 거창한 인프라가 아니라, 잘 정리된 스크립트와 명확한 지침서였다.

 

1인 개발자의 병목은(어느정도 개발을 한다면 - AI를 써서 만들어내든..)
운영 반복 작업이다.

반복적이고 비용이 많이들어가는 부분에서의 개선은 항상 재밌는 지점이 아닐까나..

 

다음은 Qwen3-TTS를 활용한 TTS 오디오 생성·후 업로드 자동화에 대한 글을 쓰고자한다.

2026년 첫 글이 되겠습니다.

작년(2025)에 진행했던 프로젝트가 하나 있어

회고글을 작성해보도록 하겠습니다.

 

https://apps.apple.com/kr/app/%EC%95%A0%EB%8B%88%EC%84%BC%ED%85%90%EC%8A%A4/id6755728391

 

애니센텐스 앱 - App Store

App Store에서 Woowon Kang의 애니센텐스 앱을 다운로드하십시오. 스크린샷, 평가 및 리뷰, 사용자 팁, 애니센텐스 앱과 비슷한 다른 게임들도 만나볼 수 있습니다.

apps.apple.com

 

+ Recent posts