728x90
반응형

 지난 포스트에서 퍼셉트론을 이용해 대표적인 논리 게이트인 AND 게이트, NAND 게이트, OR 게이트를 구현해보았다. 이번 포스트에선 또 다른 대표적인 논리 게이트인 XOR 게이트를 구현해보자.

 

 

1. XOR 게이트

  • XOR 게이트는 배타적(자기 자신을 제외하고 나머지는 거부한다.) 논리합이라는 회로로, 변수 중 단 하나만 True(참 = 1) 일 때, True(=1)을 반환한다.
  • XOR 게이트의 진리표는 다음과 같다.
0 0 0
0 1 1
1 0 1
1 1 0
  • 자, 위 진리표를 구현할 수 있는 가중치($w_1, w_2, b$)를 찾을 수 있는가?

$$ y = \begin{cases}
 0, \ \ (w_1*0 + w_2*0 + b \leq 0) \\ 
 1, \ \ (w_1*0 + w_2*1 + b \leq 0) \\ 
 1, \ \ (w_1*1 + w_2*0 + b \leq 0) \\ 
 0, \ \ (w_1*1 + w_2*1 + b > 0) 
\end{cases} $$

  • 위 공식에 들어 맞는 가중치를 찾는 것은 불가능하다.
  • 그 이유는 앞서 말한 퍼셉트론 역시 회귀분석과 마찬가지로 선형성을 이용해서 0보다 크고, 작은 지를 구분해내기 때문이다.
  • 위 말을 이해하기 쉽도록 위 진리표를 시각적으로 보자.
>>> import matplotlib.pyplot as plt

# 캔버스 설정
>>> fig = plt.figure(figsize=(8,8)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.title('XOR Gate, Visualization of True table', fontsize = 25)
>>> plt.ylim(-1, 4)
>>> plt.xlim(-1, 4)
>>> plt.axhline(color = 'k', alpha = 0.5)
>>> plt.axvline(color = 'k', alpha = 0.5)
>>> plt.xlabel("x1", fontsize = 20)
>>> plt.ylabel("x2", fontsize = 20, rotation = 0)

>>> plt.scatter([0, 1], [0, 1], s = 200, c = "green", marker="v")
>>> plt.scatter([0, 1], [1, 0], s = 200, c = "blue", marker="s")

>>> plt.show()

  • 위에서 퍼셉트론의 구분 방법은 선형성을 이용한다고 했다.
  • 선형성을 이용한다는 소리는 위 그래프에서 선 하나(퍼셉트론)로 파란 네모 점과 녹색 세모 점을 구분할 수 있어야 한다는 소리다.
  • 직선이 아닌 곡선을 사용하지 않는한, 그 어떠한 방법으로도 하나의 직선으로는 위 점을 서로 다른 2개의 집단으로 구분해낼 수 없다.
  • 즉, 퍼셉트론 하나로는 논리 게이트의 구현이 불가능하다는 것이고, 이는 퍼셉트론 하나로는 컴퓨터 같은 고등 연산을 수행할 수 없다는 것이다!

 

 

 

 

2. XOR 게이트의 해결 방법

 머리가 비상하게 좋은 친구라면(블로그 주인장은 그렇지 못하지만...), "앞서 만들었던 논리 게이트를 조합해서 이 문제를 해결할 수 있지 않을까?"라는 생각이 들지도 모른다.

 우리는 앞에서 AND, NAND, OR 게이트를 만들어보았고, 이들을 조합해서 문제를 해결할 수는 없을까?

  • 위 그림을 보면, NAND, OR, AND 논리 게이트를 조합하면 우리가 위에서 만들었던 XOR 진리표의 결과를 출력하는 것을 알 수 있다.
  • $x_1, x_2$는 일정하며, 어떤 논리 게이트를 결정하느냐에 따라 가중치($w_1, w_2, b$)만 바뀐다.
  • 반대로 말하면, 가중치만 다른 퍼셉트론을 조합해서 내가 원하는 기능을 얻을 수 있다는 것이다.
  • 자, 위 그림을 코드로 구현해보자. 이번에는 조금 더 코드 친화적으로 Numpy의 array를 이용해서 짜 보겠다.
>>> import numpy as np

>>> def step_function(x):
    
>>>     y = x > 0
    
>>>     return y.astype(np.int) 
        

>>> def Perceptron(x, dict_name, gate_name):

>>>     weight = dict_name[gate_name]
>>>     y = np.sum(x*weight['w']) + weight["b"]

>>>     return step_function(y)


>>> def XOR_gate(x):

>>>     array_x = np.array(x)
>>>     dict_W = {"AND":{"w":[0.5,0.5], "b":-0.7},
>>>               "NAND":{"w":[-0.5,-0.5], "b":0.7},
>>>               "OR":{"w":[0.5,0.5], "b":-0.2}}

>>>     y1 = Perceptron(array_x, dict_W, "NAND")
>>>     y2 = Perceptron(array_x, dict_W, "OR")
    
>>>     X = np.array([y1, y2])
>>>     Y = Perceptron(X, dict_W, "AND")
    
>>>     return step_function(Y)
>>> print("XOR Gate")
>>> print("----"*20)
>>> print("(0,0):", XOR_gate([0,0]))
>>> print("(0,1):", XOR_gate([0,1]))
>>> print("(1,0):", XOR_gate([1,0]))
>>> print("(1,1):", XOR_gate([1,1]))

XOR Gate
--------------------------------------------------------------------------------
(0,0): 0
(0,1): 1
(1,0): 1
(1,1): 0
  • 위 코드에서 새롭게 ster_function()이라는 함수가 생겼는데, 이는 계단 함수로, 앞서 우리가 if문을 이용해서 합산된 결과가 0보다 클 때, 1을 반환하고, 0보다 작거나 같으면, 0을 반환하던 부분이다.
  • 이를 활성화 함수(Activation Function)라 하는데, 활성화 함수를 통해 합산된 값을 출력할지, 출력한다면 어떻게 출력할지를 결정한다.
  • 계단 함수는 이후 활성화 함수를 자세히 다룰 때, 다시 이야기하도록 하겠다.
  • 위 코드를 보면, 오로지 가중치만 다른 퍼셉트론을 겹쳐서 사용했는데, XOR 게이트 문제를 해결한 것을 알 수 있다.
  • 이렇게 2개 이상의 퍼셉트론을 쌓는 것을 다층 퍼셉트론이라고 한다.

 

 

 

 

3. 논리를 비약시켜보자.

  • 위에서 우리는 2개 이상의 층(Layer)을 쌓아, 우리가 원하는 문제를 해결하였다.
  • 이 곳에 사용된 층들은 오로지 가중치만 다르며, 이 가중치가 갖는 의미도 꽤 다르다.
  • $w_1, w_2$ 같은 가중치는, 각 입력 신호에 부여되는 영향력(중요도)을 조절한다.
  • $\theta$는 임계점이라 하였는데, 좌변으로 이항 시켜, $b$로 만들어주었고, 이는 편향(bias)라고 한다. 편향은 뉴런이 얼마나 쉽게 활성화되느냐에 영향을 미치며, $b$의 크기에 따라 활성화 함수가 작동하는지, 마는지가 결정된다.
  • 지금까지 우리가 학습한 내용을 단순화시켜보면, 층을 많이 쌓고, 가중치를 맞게 설정해주면, 내가 원하는 결과를 얻을 수 있다는 것이다.
  • 층을 많이 쌓는다는 것이 바로 우리가 아는 딥러닝(Deep Learning)의 딥(Deep)을 의미하며, 층이 많이 쌓인 신경망은 각 노드에서 다음 노드로 이동하는 신호에 부여하는 가중치를 다르게만 한다면, 구체적으로 함수 식을 모른다 할지라도 우리가 원하는 결과를 알 수 있다는 것이다.
  • 그런데, 만약 그 가중치를 컴퓨터가 알아서 찾을 수 있다면 어떨까?
  • 여기서 조금 어려워질지도 모르겠는데, 기계가 어떠한 방법(손실 함수)을 이용해서 가장 적합한 가중치를 찾아낸다면, 우리가 스스로 공부를 해서 어떤 결과를 도출하는 것처럼 컴퓨터도 적합한 가중치를 찾기 위한 활동, 즉! 학습과 동일하게 보이는 행동을 통해 데이터만 가지고 원하는 결과를 도출할 수 있다는 것이다.
  • 여기서 "기계가 학습한다(Machine Learning)"이라는 말이 나오게 되는 것이며, 학습을 하되(가중치를 찾는 과정) 그것을 다층 레이어를 통해 찾아내는 것을 딥러닝(Deep Learning)이라 하게 되는 것이다.

 

 

 

 이번 포스트에선 퍼셉트론을 이용해 머신러닝이라는 단어와 딥러닝이라는 단어가 어떻게 만들어졌는지를 학습해보았다. 다음 포스트에서는 짧게 짚고 넘어갔던 활성화 함수에 대해 학습해보자.

728x90
반응형
728x90
반응형

 지난 포스트에서 기초적인 파이썬 코드를 사용하여, 퍼셉트론을 구현해보았다. 이번 포스트에서는 지난번에 생성한 퍼셉트론을 사용해서 논리 회로를 적용해보겠다.

 

 

논리회로(Logical circuit)

 1937년 클로드 섀넌(Claude Shannon)이 개발한 논리 회로(Logical Circuit)는 불 대수(Boolean algebra)를 물리적으로 구현한 것으로, 하나 이상의 논리적 입력값(True / False)이 들어가면, 그에 맞는 논리 연산(And / Or 등)을 수행하여 하나의 논리적 출력 값(True / False)을 얻는 전자 회로다.

 논리 회로는 다양한 불 대수의 조합을 통해 다양한 기능을 수행할 수 있는데, 이를 이용해서 컴퓨터 같은 고등 연산이 가능한 기계를 만들어 낼 수도 있다.

 여기서 AND, NOT, OR, XOR 등과 같은 기본이 되는 논리 연산을 수행하는 것을 논리 게이트(Logical gate)라 한다.

 만약, 앞에서 만들었던 퍼셉트론이 논리 게이트에 대해서도 적용 가능하다면, 퍼셉트론을 이용해서 컴퓨터도 만들 수 있지 않을까?

 

 

 

논리 게이트

1. AND 게이트

  • AND 게이트는 우리가 코드를 짤 때, 익숙한 논리 연산자 AND와 같으며, 이는 집합에서 교집합에 해당한다. 교집합은 두 집합이 모두 참(True)일 경우, 참(True)을 반환하는 것이다.
  • 두 집합에 대한 진리표를 만들어, 이를 보다 쉽게 이해해보자.
$x_1$ $x_2$ $y$
0 0 0
0 1 0
1 0 0
1 1 1
  • 위 표를 볼 때, 1은 True(참)이라 생각하고, 0은 False(거짓)이라고 생각해보자.
  • 위 표를 퍼셉트론의 공식에 맞게 고쳐보자.

$$ y = \begin{cases}
 0, \ \ \ w_1*0 + w_2*0 \leq \theta \\ 
 0, \ \ \ w_1*0 + w_2*1 \leq \theta \\ 
 0, \ \ \ w_1*1 + w_2*0 \leq \theta \\ 
 1, \ \ \ w_1*1 + w_2*1 > \theta  
\end{cases} $$

  • 위 공식을 보면, 변수인 가중치($w_1, w_2$)와 임계값($\theta$)을 어떻게 설정하느냐로 AND 게이트를 구현할 수 있다는 것을 알 수 있다.
  • 위 공식을 참으로 만드는 가중치와 임계값을 설정하여, AND 게이트를 구현해보자
# Perceptron
>>> def Perceptron(x1, x2, w1, w2, theta):
    
>>>     y = w1*x1 + w2*x2
    
>>>     if y <= theta:
>>>         return 0
>>>     elif y > theta:
>>>         return 1
    
    
# AND Gate를 구현해보자.
>>> w1, w2, theta = 0.5, 0.5, 0.8

>>> print("AND Gate")
>>> print("----"*20)
>>> print("(0, 0):", Perceptron(x1=0, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(0, 1):", Perceptron(x1=0, x2=1, w1=w1, w2=w2, theta=theta))
>>> print("(1, 0):", Perceptron(x1=1, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(1, 1):", Perceptron(x1=1, x2=1, w1=w1, w2=w2, theta=theta))

AND Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 0
(1, 0): 0
(1, 1): 1
  • 위 코드를 보면, 가중치와 임계값을 0.5로 정했을 뿐인데, AND Gate와 동일한 결과가 나온 것을 알 수 있다.

 

 

 

2. NAND 게이트

  • 이번에는 AND 게이트의 반대인 NOT AND 게이트인 NAND 게이트를 구현해보자.
  • NAND 게이트는 AND 게이트의 반대이므로, 진리표는 다음과 같다.
$x_1$ $x_2$ $y$
0 0 1
0 1 1
1 0 1
1 1 0
# NAND Gate를 구현해보자.
>>> w1, w2, theta = -0.5, -0.5, -0.8

>>> print("NAND Gate")
>>> print("----"*20)
>>> print("(0, 0):", Perceptron(x1=0, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(0, 1):", Perceptron(x1=0, x2=1, w1=w1, w2=w2, theta=theta))
>>> print("(1, 0):", Perceptron(x1=1, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(1, 1):", Perceptron(x1=1, x2=1, w1=w1, w2=w2, theta=theta))

NAND Gate
--------------------------------------------------------------------------------
(0, 0): 1
(0, 1): 1
(1, 0): 1
(1, 1): 0
  • NAND Gate는 AND Gate의 반대이므로, 가중치와 임계값을 모두 역수로 만들어주었다.

 

 

 

3. OR 게이트

  • 다음은 또 다른 기본 논리 게이트 중 하나인 OR 게이트를 구현해보자.
  • OR은 집합의 합집합에 해당하며, 둘 중 하나라도 True인 경우 True를 반환한다.
$x_1$ $x_2$ $y$
0 0 0
0 1 1
1 0 1
1 1 1
# OR Gate를 구현해보자.
>>> w1, w2, theta = 0.5, 0.5, 0.2

>>> print("OR Gate")
>>> print("----"*20)
>>> print("(0, 0):", Perceptron(x1=0, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(0, 1):", Perceptron(x1=0, x2=1, w1=w1, w2=w2, theta=theta))
>>> print("(1, 0):", Perceptron(x1=1, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(1, 1):", Perceptron(x1=1, x2=1, w1=w1, w2=w2, theta=theta))

OR Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 1
(1, 0): 1
(1, 1): 1
  • OR 게이트도 오로지 가중치만 바꾸었는데, 우리가 원하는 값을 반환한 것을 볼 수 있다!

 

 

 

4. 퍼셉트론 공식 정리 및 코드 체계화

  • 지금까지 퍼셉트론 코드를 짜보며, 이 코드를 보다 쉽게 바꿀 수 있다는 생각이 들지 않는가?
  • 먼저 퍼셉트론에 사용된 공식에서 임계값 $\theta$를 왼쪽으로 이동시켜, 절편으로 만들면 어떨까?

$$ y = \begin{cases}
 0, \ \ \  (w_1x_1 + w_2x_2 + b \leq 0)  \\ 
 1, \ \ \  (w_1x_1 + w_2x_2 + b > 0) 
\end{cases} $$

  • 좌변으로 이동하면서 $-\theta$로 부호가 음수로 바뀌었는데, 보기에 깔끔하지 않으니 부호가 양수인 $b$라는 절편으로 만들어보았다.
  • 그런데 위 공식을 보면, 어디서 많이 본 공식과 굉장히 유사하지 않은가? 그렇다. 회귀식과 퍼셉트론의 공식은 동일하며, 이로써 퍼셉트론도 선형성을 따지는 것임을 알 수 있다.
  • 위 내용을 코드에 반영하면서, 동시에 퍼셉트론으로 AND 게이트, NAND 게이트, OR 게이트를 구현할 때, 오로지 가중치만을 바꿨는데, 이를 보다 깔끔한 코드로 구현해보자.
>>> class Logical_gate:
    
>>>     def __init__(self, weight_dict):
        
>>>         self.weight = weight_dict


>>>     def Perceptron(self, x1, x2, key):

>>>         weight = self.weight[key]
>>>         w = weight["w"]
>>>         b = weight["b"]

>>>         y = w[0]*x1 + w[1]*x2 + b

>>>         if y <= 0:
>>>             return 0
>>>         elif y > 0:
>>>             return 1
        
>>>     def Run_Gate(self, key):

>>>         print(key + " Gate")
>>>         print("----"*20)
>>>         print("(0, 0):", self.Perceptron(0, 0, key))
>>>         print("(0, 1):", self.Perceptron(0, 1, key))
>>>         print("(1, 0):", self.Perceptron(1, 0, key))
>>>         print("(1, 1):", self.Perceptron(1, 1, key))
>>>         print("----"*20)
>>>         print("\n")
>>> weight_dict = {"AND":{"w":[0.5,0.5], "b":-0.5},
>>>                     "NAND":{"w":[-0.5,-0.5], "b":0.5},
>>>                     "OR":{"w":[0.5,0.5], "b":-0.2}}

>>> LG = Logical_gate()

>>> LG.Run_Gate("AND")
>>> LG.Run_Gate("NAND")
>>> LG.Run_Gate("OR")

AND Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 0
(1, 0): 0
(1, 1): 1
--------------------------------------------------------------------------------


NAND Gate
--------------------------------------------------------------------------------
(0, 0): 1
(0, 1): 0
(1, 0): 0
(1, 1): 0
--------------------------------------------------------------------------------


OR Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 1
(1, 0): 1
(1, 1): 1
--------------------------------------------------------------------------------
  • 위 코드는 기존의 퍼셉트론 코드에 비해 더 어려워 보이긴 하지만, 일단 만들어 놓으면, 사용하기는 훨씬 쉽다.
  • 새로운 논리 게이트를 구현하고자 하면, 딕셔너리인 weight_dict에 새로운 가중치($w_1, w_2, b$ 모두를 앞으로 단순하게 가중치로 부르겠다.)만 담으면 끝이다.

 

 

 

 지금까지 퍼셉트론을 이용해서 AND, NAND, OR 게이트를 가중치만 바꿔서 구현한 것을 살펴보았는데, 지금까지만 보면, 퍼셉트론이 논리 게이트에서도 잘 작동하는 것으로 보일 것이다.

 그러나, 앞서 말했듯 퍼셉트론도 선형성을 기반으로 결과를 도출하고, 그 결과가 활성화 함수인 계단 함수(이 부분은 뒤에서 자세히 다루겠다.)를 통해 출력되었는데, 이에 대한 반례가 존재한다.

 다음 포스트에서는 퍼셉트론의 논리 게이트 적용에서 반례인 XOR 게이트에 대해 학습해보고, 어째서 그런 문제가 발생하는지, 그리고 해결 방안은 무엇인지에 대해 학습해 보겠다.

728x90
반응형
728x90
반응형

 지난 포스트의 머신러닝, 딥러닝에 대한 설명이 잘 와 닿지 않았을 수 있다. 그러나 퍼셉트론(Perceptron)에 대해 학습해보면, 어떤 과정을 통해서 머신러닝이 이루어지고, 기계 학습이라는 단어의 학습이 정확히 무엇을 의미하는지 알 수 있을 것이다.

 

 

1. 신경 세포

 머신러닝 공부를 해보면 신경망(Neural Network)라는 단어를 종종 볼 수 있었을 것이다. 그리고 머신러닝에 사용되는 인공신경망이 사람의 신경을 흉내 내어 만들어졌다는 글도 나오는데, 대체 이게 무슨 소리일까?

ZUM 학습백과. 뉴런의 구조(http://study.zum.com/book/11779)

  • 위 그림은 인간의 신경 세포인 뉴런이다.
  • 뉴런은 가지돌기(수상돌기)에 여러 신호가 도착하면 신경 세포체에서 이를 합치고, 합쳐진 신호가 특정 임계 값을 넘으면, 출력 신호가 생성되어 축삭 돌기를 통해 다음 뉴런으로 신호가 전달된다.
  • 여기서 중요한 포인트는 다음과 같다.
    • A. 여러 신호가 신경 세포체로 전달된다.
    • B. 전달된 신호는 하나로 합쳐진다.
    • C. 합쳐진 신호가 특정 임계값을 넘으면, 전달된다.
  • 나중에 다룰 이야기지만, 위 이야기에서 정보가 하나로 합쳐져서 전달되는 과정이 활성화함수(Activation function)를 나타내는 부분이라고 생각하자(이 건 나중에 다시 이야기하니깐 대충 넘기자)

 

 

 

 

2. 퍼셉트론(Perceptron)

  • 퍼셉트론은 위에서 나온 신경 세포를 알고리즘화한 것으로, 딥러닝의 기본이 되는 개념이다.
  • 퍼셉트론은 N개의 입력 받아 1개의 신호를 출력한다.
  • 퍼셉트론은 받아들여 합친 신호가 임계값보다 크면 1을 출력하고, 임계값보다 작으면 0을 출력하여, 정보를 전달하거나 전달하지 않는다.

입력이 2개인 퍼셉트론

  • 위 퍼셉트론 그림에서 원은 노드(Node) 혹은 뉴런(Neuron)이라고 부르며, 선은 엣지(Edge)라고 부른다.
  • 노드에서 다음 노드로 정보가 전해질 때, 가중치($w_1$, $w_2$)가 곱해져 전달된다.
  • 전달되는 값과 가중치가 곱해진 값($w_1x_1$)의 합($w_1x_1 + w_2x_2$)이 임계값 $\theta$(theta)보다 크면 1이, 작으면 0이 출력되는데, 이는 위에서 언급한 신경세포에서 신호가 전달되는 과정을 그대로 따라한 것과 같다.
  • 이를 수식으로 나타내면 다음과 같다.

$$y = \begin{cases}
 0 \ \ \ (w_1x_1 + w_2x_2 \leq \theta) \\ 
 1 \ \ \ (w_1x_1 + w_2x_2 > \theta)
\end{cases}$$

  • 위 함수를 보면, 위 신경세포에서 설명한 신호가 움직이는 과정을 쉽게 표현한 것임을 알 수 있다.

 

 

 

3. 퍼셉트론의 구현

  • 앞서 입력 노드가 2개인 퍼셉트론의 함수를 적어보았다. 만약, 위 함수를 파이썬 코드로 구현한다면, 그것이 바로 퍼셉트론을 구현하는 것이 아니겠는가. 한 번 만들어보도록 하자.
# Perceptron 함수를 코드로 구현해보자
>>> def Perceptron(x1, x2, w1, w2, theta):

>>>     y = w1*x1 + w2*x2
            
>>>     if y <= theta:
>>>         return 0
                
>>>     elif y > theta:
>>>         return 1
  • $x_1$과 $x_2$는 퍼셉트론의 노드이므로, 0과 1의 값만 나올 수 있다.
  • $x_1=0,\ x_2=1,\ w_1=0.1,\ w_2=0.3,\ theta=0.6$으로 함수를 실행해보자 
>>> Perceptron(x1=0, x2=1, w1=0.1, w2=0.3, theta=0.6)
0
  • 위 결과를 보면, 0*0.1 + 1*0.3 <= 0.6으로 나와 0이 반환된 것을 알 수 있다.
  • 이를 보면, 가중치는 해당 노드에서 전달되는 정보의 중요도를 어느 정도나 강하게 줄 것인지를 따지는 것이고, 임계값은 받아들인 신호를 얼마나 타이트하게 평가할 것인가를 판단하는 수단이라는 것을 알 수 있다.

 

 

 지금까지 신경망의 기본이 되는 신경 세포, 퍼셉트론의 기본 개념에 대해 학습해보았다. 다음 학습에선 퍼셉트론을 이용하여 단순한 논리 회로를 적용하는 방법에 대해 학습해보도록 하겠다.

 

 

 해당 포스팅은 사이토 고키의 "밑바닥부터 시작하는 딥러닝"책을 참고하여 작성하였다. 해당 책은 꽤 얇고 내용도 이해하기 쉽게 써놨으므로, 머신러닝을 시작하는 초보가 읽기에 괜찮은 책이다. 시간 날 때, 한 번 읽어보기 바란다(참고로 PPL이 아니다! 내 돈 주고 내가 산 괜찮은 책 소개다!).

728x90
반응형

+ Recent posts