suyeongpark

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

이상엽/ 위상수학/ 연속사상

연속사상

연속사상

  • 실수의 연속함수로부터 위상구조의 연속성을 보존하는 연속사상을 정의한다.

Def 1. [실변수함수의 연속]

  1. 함수 f : \mathbb{R} \to \mathbb{R} x_{0} \in \mathbb{R} 에서 연속이다.
    • \Leftrightarrow \forall \epsilon > 0, \exists \delta > 0 : |x - x_{0}| < \delta \Rightarrow |f(x) - f(x_{0})| < \epsilon
    • 즉, x \in (x_{0} - \delta, x_{0} + \delta) \Rightarrow f(x) \in (f(x_{0}) - \epsilon, f(x_{0}) + \epsilon)
  2. f : \mathbb{R} \to \mathbb{R} 가 연속함수다
    • f 가 임의의 x_{0} \in \mathbb{R} 에서 연속이다.

Def 2. [연속사상]

  1. 사상 f : X \to Y x_{0} \in X 에서 연속이다.
    • f(x_{0}) 를 포함하는 임의의 열린집합 V(\subset Y) 에 대하여, x_{0} 를 포함하는 열린집합 U(\subset X) 가 존재해 f(U) \subset V 를 만족한다.
  2. f : X \to Y 가 연속사상이다.
    • f 가 임의의 x_{0} \in X 에서 연속이다.

ex) 집합 X = \{ 1, 2, 3 \} 위의 위상 \mathfrak{I} = \{ \emptyset, X, \{1\}, \{3\}, \{1, 2\}, \{1, 3\} \} 에 대하여, 사상 f : X \to X f(1) = 2, f(2) = 3, f(3) = 3 이라 정의하면, f 는 1과 3에서 연속이지만 2에서는 연속이 아니다. 즉, f 는 연속사상이 아니다.

  • 사상에서 연속임을 증명할 때는 공역에서 먼저 시작해서 그 조건을 만족하는 열린집합을 정의역에서 잡아줄 수 있으면 연속사상이 된다.
    • 이런 조건은 상당히 일반화된 것이기 때문에 직관적으로 이해하기는 쉽지 않다.

Thm 1. [연속사상의 또 다른 정의]

사상 f : X \to Y 에 대하여 다음 두 명제는 동치이다.

  1. f 는 연속사상이다.
  2. Y 의 임의의 열린집합 V 의 역상 f^{-1}(V) X 에서 열린집합이다.
  • 정의에 따라 연속임을 증명하려면 열린집합을 일일이 체크해야 하는데, 이게 너무 번거롭기 때문에 일반적으로 이 정의를 따라 연속임을 판명함.
  • 주의할 점은 역함수를 잡을 수 없는 경우 \emptyset 이 되는데, 이것 또한 위상의 정의상 위상의 원소가 되기 때문에 연속이 된다. 정의가 그러한 것

Cor. [닫힌집합과 연속사상]

f : X \to Y 는 연속사상이다. \Leftrightarrow Y 의 임의의 닫힌집합 C 의 역상 f^{-1}(C) X 에서 닫힌 집합이다.

ex) 실수의 보통위상공간사이의 사상 f(x) = {1 \over 1 + x^{2}} 는 연속사상이지만, 사상 g(x) = \begin{cases} 1, x \geq 0 \\ 0, x < 0 \end{cases} 는 연속사상이 아니다.

  • 정의역이 이산위상공간이면 공역이 무엇이든지 항상 연속사상이 정의된다.
    • 이산위상공간은 모든 부분집합이 열린집합이기 때문에 함수값들이 어떻게 되든간에 상관없이 항상 성립. 애초에 조건에서 근접한 것이 없었기 때문에 그 결과가 근접해 있든 아니든 참이 됨.
    • 이런 경우를 공허참이라 한다. 조건식 P -> Q에서 P가 거짓이면 Q는 무조건 참인 것이 같은 맥락.
  • 정의역과 공역이 같을 때 항등사상과 상수사상은 항상 연속사상이다.

Thm 2. [기저와 연속사상]

위상공간 사이의 사상 f : X \to Y Y 의 기저 \mathcal{B} 에 대해 다음 두 명제는 동치이다.

  1. f 는 연속사상이다.
  2. \mathcal{B} 의 임의의 원소 B 의 역상 f^{-1}(B) X 에서 열린집합이다.

ex) 실수의 아래끝위상공간사이의 사상 f(x) = x + 1 는 연속사상이지만, 사상 g(x) = -x 는 연속사상이 아니다.

Thm 3. [연속사상의 합성]

연속사상의 합성사상은 연속사상이다.

위상동형사상

  • 위상수학의 주요 목표 중 하나는 주어진 두 위상공간이 서로 위상동형인지 아닌지를 밝히는 것이다.

Def. [위상동형사상]

두 위상공간 X, Y 사이의 사상 f 가 다음 세 조건을 만족한다고 하자.

  1. f 는 전단사이다.
  2. f 는 연속이다.
  3. f^{-1} 는 연속이다.

이때 f 를 위상동형사상이라 하며, X Y 를 위상동형이라 하고 X  \simeq Y 라 표기한다.

  • 위상동형인 X, Y 는 1)에 의해 집합적으로 구별되지 않으며 2), 3)에 의해 위상적으로 구별되지 않는다.
  • 위상동형인 위상공간들이 공통적으로 갖는 성질을 불변량이라 한다.
    • 불변하는 성질
  • 위상동형은 동치관계이다.
    • 반사적/ 대칭적/ 추이적

부분공간

부분공간

  • 주어진 하나의 위상공간으로부터 새로운 위상공간을 만든다.

Thm. [부분위상]

위상공간 (X, \mathfrak{I}) A \subset X 에 대하여 \mathfrak{I}_{A} 를 다음과 같이 정의하면 \mathfrak{I}_{A} A 위의 위상이 된다.

\mathfrak{I}_{A} = \{ A \cap U | U \in \mathfrak{I} \}

Def. [부분공간]

Thm.에서 설정한 \mathfrak{I}_{A} 를 부분위상이라 하고, 위상공간 (A, \mathfrak{I}_{A}) (X, \mathfrak{I}) 의 부분공간이라 한다.

  • 전체공간에서는 열린집합이 아니었던 집합이 부분공간에서는 열린집합일 수 있다.
    • 위상공간의 부분 집합에 대하여 공집합과 X(전체 집합)는 서로간에 열린집합-닫힌집합의 관계가 된다. 공집합의 여집합은 X가 되고, X의 여집합은 공집합이 되기 때문. 다시 말해 X를 열린집합으로 잡으면 공집합은 닫힌집합이 되고, 공집합을 열린집합으로 잡으면 X는 닫힌 집합이 된다.
    • 만일 공집합과 X 외에 위상공간의 부분 집합에서 그러한 관계를 갖는 집합이 또 발생한다면, 기하적인 의미에서 그 둘은 떨어져 있는 관계가 된다.
    • 이러한 의미에서 열린집합이라 해서 항상 구간이 열려 있지 않고, –공집합은 원소 1개– 닫힌집합이라고 해서 항상 구간이 닫혀 있지는 않다. –정의상 열린집합의 여집합이기 때문

ex) 실수의 보통 위상공간 (\mathbb{R}, \mathfrak{I}) 의 부분공간 (\mathbb{Z}, \mathfrak{I}_{Z}) 에서 임의의 한 점 집합 \{ z \} (z \in \mathbb{Z})

부분공간의 성질

  1. 위상공간 (X, \mathfrak{I}) \mathfrak{I} 의 기저 \mathcal{B} 그리고 부분위상공간 (A, \mathfrak{I}_A) 에 대하여 \mathcal{B}_{A} = \{ A \cap B | B \in \mathcal{B} \} \mathfrak{I}_{A} 의 기저가 된다.
  2. 위상공간 (X, \mathfrak{I}) 와 그 부분위상공간 (A, \mathfrak{I}_A) 사이에는 항상 위상동형사사을 정의할 수 있다. 즉 (X, \mathfrak{I}) (A, \mathfrak{I}_{A}) 는 위상동형이다.
    • ex) 실수의 보통위상공간 (\mathbb{R}, \mathfrak{I}) (-{\pi \over 2}, {\pi \over 2}) \subset \mathbb{R} 에 대해 사상 f : (-{\pi \over 2}, {\pi \over 2}) \to \mathbb{R} f(x) = \tan x 라 정의하면 f 는 위상동형사상이다.
  3. 위상공간 (X, \mathfrak{I}) 과 그 부분위상공간 (A, \mathfrak{I}_{A}) 에 대해 A (X, \mathfrak{I}) 의 열린집합이면 (A, \mathfrak{I}_{A}) 의 모든 열린집합들은 동시에 (X, \mathfrak{I}) 의 열린집합이기도 하다.

임베딩(Embedding)

  • 공역의 부분공간으로의 사상이 집합적으로도 위상적으로도 겹침이 발생하지 않는 연속사상인 경우를 정의한다.
    • 겹침이 발생하지 않는다는 것은 위상 동형이라는 의미. 원래 함수에서 가까웠던 점은 변환된 함수에서도 가깝고, 원래 함수에서 멀었던 점은 변환된 함수에서 멀다면 겹침이 없는 것이지만, 원래 함수에서 가까웠던 점이 변환된 함수에서 멀어지거나 원래 함수에서 멀었던 점이 변환된 함수에서 가까워지면 겹침이 발생한 것이 된다.

Def. [임베딩]

X 에서 Y 로의 연속사상 f 가 다음 두 조건을 만족하면 임베딩이라 한다.

  1. f 는 단사이다.
  2. \tilde{f} : X \to f(X) 가 위상동형사상이다.

ex) X = \{ 0, 1, 2, ... \} 위의 이산위상 D 와 실수의 보통위상 \mathfrak{I} 에 대하여 두 위상공간 (X, D), (\mathbb{R}, \mathfrak{I}) 사이의 사상 f : X \to Y f(x) = \begin{cases} {1 \over x}, x \neq 0 \\ 0, x = 0 \end{cases} 라 정의하면 f 는 임베딩이 아니다.

  • 위 예는 원래 집합의 원소들은 떨어져 있는데, 변환된 함수의 결과에서는 위상적으로 겹침이 발생함

곱공간

곱공간

  • 주어진 두 위상공간으로부터 새로운 위상공간을 만든다.
    • 주어진 두 집합의 곱집합을 이용

Thm 1. [곱위상의 기저]

두 위상공간 (X, \mathfrak{I}_{X}), (Y, \mathfrak{I}_{Y}) 에 대하여 \mathcal{B} 를 다음과 같이 정의하면 \mathcal{B} 는 곱집합 X \times Y 상의 위상의 기저가 된다.

\mathcal{B} = \{ U \times V | U \in \mathfrak{I}_{X}, V \in \mathfrak{I}_{Y} \}

Def. [곱공간]

Thm 1.에서 설정한 \mathcal{B} 가 생성하는 위상 \mathfrak{I} X \times Y 위의 곱위상이라 하고, 위상공간 (X \times Y, \mathfrak{I}) X Y 의 곱공간이라 한다.

  • 두 거리공간 (X, d_{X}), (Y, d_{Y}) 로부터 유도되는 위상공간을 각각 (X, \mathfrak{I}_{X}), (Y, \mathfrak{I}_{Y}) 라 할 때, 곱거리공간 (X \times Y, d_{X} \times d_{Y}) 로부터 유도되는 위상공간 (X \times Y, \mathfrak{I}) (X, \mathfrak{I}_{X}) (Y, \mathfrak{I}_{Y}) 의 곱공간과 일치한다.
    • 사실은 애초에 거리 공간을 설정할 때 이게 가능하도록 설정한 것

Thm 2. [기저와 곱공간]

두 위상공간 (X, \mathfrak{I}_{X}), (Y, \mathfrak{I}_{Y}) 의 기저를 각각 \mathcal{B}_{X}, \mathcal{B}_{Y} 라 할 때, 집합족 \delta = \{ U \times V | U \in \mathcal{B}_{X}, V \in \mathcal{B}_{Y} \} X \times Y 의 기저이다.

ex) 네 실수 a, b, c, d 에 대해 \delta = \{ (a, b) \times (c, d) | a < b, c < d \} \mathbb{R}^{2} 의 기저이다.

사영사상

Def 1. [열린사상과 닫힌사상]

두 위상공간 (X, \mathfrak{I}_{X}), (Y, \mathfrak{I}_{Y}) 사이의 사상을 f : X \to Y 라 하자.

  1. X 의 임의의 열린집합 U 에 대하여 f(U) Y 의 열린집합이면 f 를 열린사상이라 한다.
  2. X 의 임의의 닫힌집합 C 에 대하여 f(C) Y 의 닫힌집합이면 f 를 닫힌사상이라 한다.
  • 1)전단사이고 2)연속인 3)열린사상은 위상동형사상이다.

Def 2. [사영사상]

두 위상공간 (X, \mathfrak{I}_{X}), (Y, \mathfrak{I}_{Y}) 과 곱공간 (X \times Y, \mathfrak{I}) 에 대하여 p_{1} (x, y) = x, p_{2}(x, y) = y 로 정의한 사상 p_{1}: X \times Y \to X, p_{2} : X \times Y \to Y 를 사영사상이라 한다.

Thm 1. [사영사상의 성질]

사영사상 p_{1} : X \times Y \to X, p_{2} : X \times Y \to Y 에 대하여 다음이 성립한다.

  1. p_{1}, p_{2} 는 연속사상이다.
  2. p_{1}, p_{2} 는 열린사상이다.
  • 위 조건은 만족하지만 전단사가 보장되지 않기 때문에 사영사상은 위상동형사상이 아니다.

Thm 2.

세 위상공간 X, Y, Z 에 대하여 다음 두 명제는 동치이다.

  1. f : Z \to X \times Y 가 연속이다.
  2. p_{1} \circ f : Z \to X p_{2} \circ f : Z \to Y 가 모두 연속이다.

이상엽/ 위상수학/ 위상공간

위상공간

도입

  • 위상수학의 본질은 연속에 대한 이해이며, 실수의 연속성으로부터 시작한다.
    • (연속의 핵심은 극한)

Def 1. [lim_{n \to \infty} x_{n} = L ]

L \in \mathbb{R} 이라 할 때, \forall \epsilon > 0, \exists N \in \mathbb{N} : \forall n \geq \mathbb{N}, |x_{n} - L| < \epsilon

이때 |x_{n} - L| < \epsilon \Leftrightarrow x_{n} \in (L - \epsilon, L + \epsilon)

  • x_{n} \in (L - \epsilon, L + \epsilon) x_{n} L 의 근방에 포함된다는 의미

Def 2. [근방]

N \subset \mathbb{R}, L \in \mathbb{R} 이라 할 때,

\exists(a, b) \subset N : L \in (a, b) 을 만족하면 N L 의 근방이라 한다.

  • 근방이란 L 을 포함하는 열린구간을 의미. 심지어 (-\infty, \infty) 도 정의상 근방이라고도 할 수 있다.
  • 근방 정의의 핵심은 L 로부터 얼마나 떨어져 있느냐가 아니라 연속성.

Thm 1. [근방(열린구간)의 성질]

  1. L \in \mathbb{R} 의 근방들의 유한교집합은 L 의 근방이다.
    • 닫힌 구간인 경우에는 성립하지 않는다. 닫힌 구간들이 1개의 원소를 공유하는 경우 그 교집합은 1개의 원소가 되기 때문에 근방이 되지 않음.
  2. L \in \mathbb{R} 의 근방들의 무한합집합은 L 의 근방이다.
    • 무한 교집합인 경우에는 성립하지 않는다. 교집합이 1개의 원소로 수렴하기 때문

Def 3. [열린집합]

열린구간들의 합집합으로 표현 가능한 집합을 열린집합이라 한다.

Thm 2. [열린집합의 성질]

  1. \emptyset 은 열린집합이다.
  2. 열린집합의 유한교집합은 열린집합이다.
  3. 열린집합의 무한합집합은 열린집합이다.
  • 열린구간의 일반화 버전
    • 열린집합을 0번 합하면 공집합, 1번 합하면 열린구간이 된다.

위상공간

  • 실수에서의 열린집합 성질을 바탕으로 이를 일반화하여 위상공간을 정의한다.
    • 임의의 집합 X 에 열린집합의 성질을 부여한 것이 위상공간

Def 1. [위상과 위상공간]

집합 X (\neq \emptyset) X 의 부분집합의 집합족 \mathfrak{I} 가 다음을 만족한다고 하자.

  1. \emptyset, X \in \mathfrak{I}
    • X 는 열린집합
  2. \forall U_{i} \in \mathfrak{I}, \cap_{i=1}^{n} U_{i} \in \mathfrak{I} (n < \infty)
    • 열린집합의 성질에서 유한 교집합과 같은 내용
  3. \forall U_{i} \in \mathfrak{I}, \cup_{i} U_{i} \in \mathfrak{I}
    • 열린집합의 성질에서 무한 합집합과 같은 내용

이때 \mathfrak{I} X 위의 위상(topology), (X, \mathfrak{I}) 를 위상공간이라 한다.

  • 1.(1).Def3에서 정의한 열린집합들의 집합족 \mathfrak{I} 에 대해 (\mathbb{R}, \mathfrak{I}) 를 실수의 보통위상공간이라 한다.
  • 정의에 의해 한 집합에는 다양한 위상이 존재함을 알 수 있다.

Def 2. [열린집합 개념의 확장]

\mathfrak{I} 가 집합 X 의 위상일 때 \mathfrak{I} 의 원소를 열린집합이라 한다. 즉, 위상공간 (X, \mathfrak{I}) 에 대해 O \in \mathfrak{I} O (\subset X)

ex) 집합 X(\neq \emptyset) 에 대하여 다음은 모두 X 위의 위상이다.

  1. \mathfrak{I} = \{ \emptyset, X \} : 밀착위상
  2. \mathfrak{I} = P(X) 이산위상
  • 모든 집합 X 에 대하여
    • 공집합과 자기 자신(X )을 포함하는 집합족도 X 의 위상이 되고, (최소) 이거를 밀착 위상이라고 한다.
    • 공집합과 자기 자신(X )과 자기 자신의 모든 부분집합을 포함하는 집합족도 X 의 위상이 된다. (최대) – 이게 멱집합이고 이걸 이산 위상이라고 한다.

Def 3. [닫힌집합]

\mathfrak{I} 가 집합 X 의 위상일 때 C^{c} = X - C \in \mathfrak{I} C 를 닫힌집합이라 한다. (열린집합의 여집합)

  • 닫힌집합이라 해서 열린집합이 아닌 것은 아니다. 즉, 열린집합이면서 동시에 닫힌집합인 것도 존재할 수 있다.
    • ex) 실수의 보통위상공간에서 \mathbb{R}

기저

기저

  • 기저로부터 위상을 효율적으로 파악할 수 있을 뿐 아니라 새로운 위상을 만드는 것도 가능하다.

Def. [기저]

집합 X 위의 위상 \mathfrak{I} \mathfrak{I} 의 부분집합 \mathcal{B} 에 대해 \mathfrak{I} 의 임의의 원소가 \mathcal{B} 의 원소의 합집합으로 표현될 수 있으면 \mathcal{B} \mathfrak{I} 의 기저라 한다.

  • \mathcal{B} \mathfrak{I} 의 기저일 떄, \mathcal{B} \subset \mathcal{C} \subset \mathfrak{I} \mathcal{C} \mathfrak{I} 의 기저이다.

Thm. [기저의 또 다른 정의]

위상공간 (X, \mathfrak{I}) \mathfrak{I} 의 부분집합 \mathcal{B} 에 대하여 다음 두 명제는 동치이다.

  1. \mathcal{B} \mathfrak{I} 의 기저이다.
  2. \forall p \in X, p \in U \in \mathfrak{I}, \exists B \in \mathcal{B} : p \in B \subset U

Cor. [기저의 성질]

집합 X 위에 정의된 위상의 기저 \mathcal{B} 는 다음 두 조건을 만족하며, 그 역도 성립한다.

  1. \forall p \in X, \exists B \in \mathcal{B} : p \in B
  2. \forall B_{1}, B_{2} \in \mathcal{B}, \forall p \in B_{1} \cap B_{2}, \exists B_{3} \in \mathcal{B} : p \in B_{3} \subset B_{1} \cap B_{2}

ex) 다음 집합이 생성하는 집합족은 모두 \mathbb{R} 위의 위상이다.

  1. L = \{ [a, b) \subset \mathbb{R} | a, b \in \mathbb{R}, a < b \}
  2. U = \{ (a, b] \subset \mathbb{R} | a, b \in \mathbb{R}, a < b \}

위상크기비교

  • 같은 집합위의 서로 다른 두 위상의 크기를 비교가능한 때가 있으며, 이는 각 위상의 기저를 이용해 효율적으로도 가능하다.

Def. [위상크기비교]

집합 X 위의 두 위상 \mathfrak{I}_{1}, \mathfrak{I}_{2} 에 대하여 \mathfrak{I}_{1} \subset \mathfrak{I}_{2} 이면 \mathfrak{I}_{1} \mathfrak{I}_{2} 보다 작다 (또는 \mathfrak{I}_{2} \mathfrak{I}_{1} 보다 크다)고 한다.

Thm. [기저를 이용한 위상크기비교]

\mathcal{B}_{1}, \mathcal{B}_{2} 가 각각 집합 X 위의 서로 다른 두 위상 \mathfrak{I}_{1}, \mathfrak{I}_{2} 의 기저라 하자. 이때 다음 두 명제는 동치이다.

  1. \mathfrak{I}_{1} \mathfrak{I}_{2} 보다 크다.
  2. \forall p \in X, \forall B_{2} \in \mathcal{B}_{2} ,with, p \in B_{2}, \exists B_{1} \in \mathcal{B}_{1} : p \in B_{1} \subset B_{2}

즉, \mathcal{B}_{1} \supset \mathcal{B}_{2} 이면 \mathfrak{I}_{1} \supset \mathfrak{I}_{2} 이다. 단, 역은 일반적으로 성립하지 않는다.

거리공간

거리공간

  • 위상공간에서 배제된 거리의 개념을 새로이 정의하고, 이를 집합에 부여한 공간을 고려해본다.

Def. [거리]

집합 X 에 대해 함수 d : X \times X \to \mathbb{R} 가 다음 네 조건을 만족한다고 하자.

  1. \forall x, y \in X, d(x, y) \geq 0
  2. d(x, y) = 0 \Leftrightarrow x = y
  3. \forall x, y \in X, d(x, y) = d(y, x)
  4. \forall x, y, z \in X, d(x, y) \leq d(x, z) + d(z, y)

이때 d X 위의 거리(함수), (X, d) 를 거리공간이라 한다.

ex) 다음은 모두 거리공간이다.

  1. \mathbb{R} d(x, y) = |x - y| 에 대해 (\mathbb{R}, d)  
    • 여기서 d 는 유클리드 거리라고 하며 (\mathbb{R}, d)  는 유클리드 공간이라 한다. (보통 d_{E} 로 씀)
  2. \mathbb{R}^{n} = \{ \vec{x} = (x_{1}, ... , x_{n}) | x_{1}, ... , x_{n} \in \mathbb{R} \} d(\vec{x}, \vec{y}) = \sqrt{(x_{1} - y_{1})^{2} + ... + (x_{n} - y_{n})^{2}} 에 대해 (\mathbb{R}^{n}, d)  
  3. 임의의 집합 X d(x, y) = \begin{cases} 1, x \neq y \\ 0, x = y \end{cases}  에 대해 (X, d)  

주어진 두 거리공간 (X_{1}, d_{1}), (X_{2}, d_{2}) 으로부터 다음과 같은 곱거리함수 d_{1} \times d_{2} 를 이용해 새로운 거리공간 (X_{1} \times X_{2}, d_{1} \times d_{2}) 을 만들 수 있다.

(d_{1} \times d_{2})((x_{1}, x_{2}), (y_{1}, y_{2})) = \sqrt{(d_{1}(x_{1}, y_{1}))^{2} + (d_{2}(x_{2}, y_{2}))^{2}}

(단, X_{1} \times X_{2} = \{ (x_{1}, x_{2} | x_{1} \in X_{1}, x_{2} \in X_{2} \} )

거리화 가능 공간

  • 모든 거리공간은 위상공간화 가능하다.
    • 하지만 위상공간이 거리공간으로 변환할 수 없는 것도 존재하기 때문에, 거리공간이 위상공간에 포함되는 개념이 된다. 거리는 위상공간에서 부차적인 요소이다.
    • 거리공간에서 위상공간의 기저가 될 수 있는 것을 만들어 준다.

Def. [열린구]

거리공간 (X, d) 과 임의의 점 x_{0} \in X , 양의 실수 r 에 대하여 X 의 부분집합

B_{d} (x_{0}, r) = \{ x \in X | d(x_{0}, x) < r \}

을 중심이 x_{0} 이고 반지름인 r 인 열린구라하며, 간략히 B_{r}(x_{0}) 로 표기하기도 한다. (임의의 점 x 에서 거리 r 안에 포함되는 모든 점을 가져온 것. r 미만 이기 때문에 열린 구가 된다. 이하이면 닫힌구, 거리와 같은 점을 모으면 구면이 된다)

  • \overline{B_{r}}(x_{0}) = \{ x \in X | d(x_{0}, x) \leq r \} : 닫힌구
  • S_{r}(x_{0}) = \{x \in X | d(x_{0}, x) = r \} : 구면

Thm. [거리공간의 위상공간 유도]

거리공간 (X, d) 에 대하여 모든 열린구들의 집합

\mathcal{B} = \{ B_{r}(x_{0}) | x_{0} \in X, r > 0 \}

는 항상 집합 X 위의 어떤 위상의 기저가 된다. \mathcal{B} 로부터 생성된 위상을 거리위상, 위상공간을 유도공간이라 한다. (모든 열린 구들의 집합이 어떤 위상의 기저가 된다. 그렇게 만든 기저로 위상공간의 모든 요소들을 만들어낼 수 있음)

  • 어떠한 거리공간으로부터도 유도될 수 없는 위상공간이 존재한다.
    • ex) X = \{ 1, 2 \} 에 대한 밀착위상공간
  • 서로 다른 두 거리공간으로부터 동일한 위상공간이 유도되기도 한다.
    • ex) \vec{x} = (x_{1}, x_{2}), \vec{y} = (y_{1}, y_{2}) (\in \mathbb{R}^{2})
    • d_{E}(\vec{x}, \vec{y}) = \sqrt{(x_{1} - y_{1})^{2} + (x_{2} - y_{2})^{2}}
    • d_{M}(\vec{x}, \vec{y}) = Max(|x_{1} - y_{1}|, |x_{2} - y_{2}|)
    • 일 때 (\mathbb{R}^{n}, d_{E}), (\mathbb{R}^{n}, d_{M})
    • d_{E} 는 원의 모양이 되고 d_{M} 는 정사각형 모양이 된다. 그런데 이 두 거리공간으로부터 유도되는 위상공간은 동일하다. 다시 말해 원과 사각형이 위상공간에서는 같은 것이라는 것. 이는 실수라는 무한집합을 이용하였기 때문. 이게 위상수학의 유명한 예.

관계를 다음과 같이 도식해 볼 수 있다.

 

내, 외부와 경계

집적점과 폐포

  • 실수의 극한에 대응하는 위상공간의 개념을 알아본다.
    • 집적점은 실수의 극한의 일반화된 버전
    • 열린구간의 경계에 해당한다.

Def 1. [집적점]

위상공간 (X, \mathfrak{I}) 에 대해 A X 를 부분집합이라 하자. 점 x \in X x 를 포함하는 임의의 열린집합 U 에 대하여

(U \setminus \{x\}) \cap A \neq \emptyset

를 만족하면 x A 의 집적점이라 한다. (x A 에 포함되는지 아닌지 여부는 중요하지 않다)

  • 즉 집합 A 의 집적점이란 A 의 원소들이 한없이 가까이 분포하고 있는 점이다.
  • 실수의 보통위상공간과 유리수집합 \mathbb{Q} 에 대해 모든 실수는 \mathbb{Q} 의 집적점이 될 수 있다.
    • 이처럼 위상공간의 모든 원소를 집적점으로 갖는 집합의 성질을 조밀성이라 한다.
    • 또한 조밀한 가산부분집합이 존재하는 위상공간은 분해가능공간이라 한다.

Thm 1. [닫힌집합의 의미 1]

위상공간 (X, \mathfrak{I}) 의 부분집합 A 에 대해 다음 두 명제는 동치이다.

  1. A 는 닫힌집합이다.
  2. A 의 모든 집적점들은 A 에 포함된다.
    • 닫힌집합은 열린집합의 여집합이기 때문에, 거꾸로 닫힙집합을 찾고 그것의 여집합을 하면 열린집합이 된다. –열린집합을 찾기 어려운 경우 이렇게 한다.
    • 열린집합이란 위상의 원소다.

Def 2. [도집합과 폐포]

위상공간 (X, \mathfrak{I}) 의 부분집합 A 에 대해 A 의 모든 집적점들의 집합을 A 의 도집합 A' 라 하고 A \cup A' A 의 폐포 \overline{A} 라 한다.

  • 집적점이 A 내부에 존재하지 않을 수 있기 때문에 A 의 모든 집적점들의 집합이나 그 집합과 A 의 합집합이 별도의 의미가 있게 된다.

Cor. [닫힌집합의 의미 2]

위상공간 (X, \mathfrak{I}) 의 부분집합 A 에 대해 다음 세 명제는 동치이다. (도집합과 폐포를 이용해서 닫힌집합을 정의할 수 있음)

  1. A 는 닫힌집합이다.
  2. A' \subset A
  3. \overline{A} = A

Thm 2. [폐포의 의미]

위상공간 (X, \mathfrak{I}) 의 부분집합 A 에 대해 다음 두 명제는 동치이다.

  1. x \in \overline{A}
  2. \forall 열린집합 U \ni x, U \cap A \neq \emptyset

내, 외부와 경계

Def. [내부, 외부, 경계]

위상공간 (X, \mathfrak{I}) 의 부분집합 A 에 대해

  1. A 에 포함되는 모든 열린집합의 합집합을 A 의 내부 Int(A) 라 한다.
    • 위상의 원소들 가운데 A 에 포함되는 모든 것을 A 의 내부라고 한다.
  2. A^{c} (= X \setminus A) 의 내부를 A 의 외부 Ext(A) 라 한다.
    • A 의 여집합의 내부(열린집합)가 A 의 외부가 된다.
  3. \overline{A} \cap \overline{A^{c}} A 의 경계 \partial A 라 한다.
    • A 의 폐포와 A 여집합의 폐포의 교집합이 A 의 경계가 된다. 폐포는 직접점(경계)를 포함하고 있기 때문에 실제로 경계가 된다.

Thm. [내부, 외부, 경계의 의미]

위상공간 (X, \mathfrak{I}) 의 부분집합 A 에 대해 다음이 성립한다. (이 부분 집합 A 는 임의의 부분집합이기 때문에 열린집합일 수도 있고 아닐 수도 있다)

  1. \exists 열린집합 U : x \in U \subset A \Leftrightarrow x \in Int(A)
    • A 의 내부에 속하는 점을 포함하면서 A 의 포함하는 집합이 존재한다.
  2. \exists 열린집합 x \in U \subset A^{c} \Leftrightarrow x \in Ext(A)
    • A 의 외부에 속하는 점을 포함하면서 A 의 포함하는 집합이 존재한다.
  3. \exists 열린집합 U \ni x, (U \cap A \neq \emptyset) \wedge (U \cap A^{c} \neq \emptyset) \Leftrightarrow x \in \partial A

Cor. Int(A) \cup Ext(A) \cup \partial A = X

  • 내부, 외부, 경계를 합하면 X 가 된다.

부의 대이동

부의 대이동

부제에 나오는 대로 달러와 금의 흐름에 대한 내용을 담은 책. 코로나 이후 달러와 금이 어떻게 흘러갈 것인가와 그로 인해 벌어질 부의 이동에 대한 내용이 담겨있다.

달러과 금에 대한 투자적 관점에 대한 설명과 함께 현재 일어나고 있는 경제 정책 기반 변화를 다루고 있다. 개인적으로는 이미 삼프로 TV에서 설명을 들었던 내용들이라서 특별하게 새로운 내용은 없었고, 팬심으로 읽었음.

앞으로 3년 경제전쟁의 미래

앞으로 3년 경제전쟁의 미래

삼프로 TV 애청자라면 잘 아는 오건영 팀장의 환율과 금리에 대한 설명을 일본, 한국, 유럽, 중국, 미국에서 일어난 굵직한 사건을 바탕으로 대중들이 이해하기 쉽게 설명하는 책.

자본주의라는 것은 결국 신뢰와 리스크 프리미엄으로 이루어진 시스템이고, 금리와 환율은 그 신뢰의 증표인 화폐에 대한 가격 (자국내 가격이 금리, 나라간 가격이 환율) 임을 생각해 본다면, 금리와 환율이 경제에서 어떤 역할을 하는지 이해할 수 있다. 

개인적으로는 책의 내용을 이미 유튜브 등에서 –오건영 팀장이 아닌 다른 사람들을 통해서도– 많이 접했던 내용이었지만, 초심자들이라면 충분히 도움이 될 듯. 

케라스 창시자에게 배우는 딥러닝/ 결론

핵심 개념 리뷰

AI를 위한 여러 방법

  • 딥러닝은 AI나 머신 러닝과 동의어가 아니다. 인공 지능은 역사가 깊고 광범위한 분야로 일반적으로 인지 과정을 자동화 하기 위한 모든 방법을 말한다. 다른 말로 하면 사고의 자동화이다.
    • 엑셀의 스프레드시트처럼 단순한 것부터 휴머노이드 로봇 같이 고수준인 것까지 해당된다.
  • 머신 러닝은 훈련 데이터를 사용하여 자동으로 프로그램(모델이라 부른다)을 개발하는 AI의 특정 하위 분야이다.
    • 데이터를 프로그램으로 바꾸는 과정을 학습이라 부른다.
    • 머신 러닝 역사는 오래됐지만 1990년대 들어서 각광 받기 시작했다.
  • 딥러닝은 머신 러닝의 여러 종류 중 하나이다. 기하학적 변환 함수들이 번갈아 가며 연속적으로 길게 연결된 모델이다. 이 연산들은 층이란 모듈을 구성한다.
    • 전형적인 딥러닝 모델은 층을 쌓아 올린 것이다. 더 일반적으로 말하면 층의 그래프이다.
    • 층은 훈련하는 동안 학습되는 가중치 파라미터를 가진다. 이 가중치에 모델의 지식이 저장된다. 학습 과정은 좋은 가중치를 찾는 것이다.
  • 딥러닝이 여러 머신 러닝 방법 중 하나일 뿐이지만 다른 방법들과 대등한 위치에 있지 않다. 딥러닝은 큰 성공을 거두었다.

머신 러닝 분야에서 딥러닝이 특별한 이유

  • 불과 몇 년 만에 딥러닝은 역사적으로 컴퓨터에서 매우 어렵다고 인식된 다양한 종류의 문제에서 큰 성과를 거두었다. 특히 이미지, 비디오, 사운드 등에서 유용한 정보를 추출하는 기계 인지 (machine perception) 분야이다.
    • 충분한 훈련 데이터(특히 사람이 적절히 레이블링한 훈련 데이터)가 주어지면 사람이 인식하는 거의 모든 것을 데이터에서 추출할 수 있다.
    • 이런 문제로 딥러닝이 지각 문제를 해결한다고 이야기하는데, 사실 지각을 아주 좁게 정의했을 때만 맞다.
  • (딥러닝 미래 전망 생략)

딥러닝에 대하여

  • 딥러닝에서 가장 놀라운 점은 단순함에 있다. 10년 전에는 경사 하강법으로 파라미터 기반의 단순한 모델을 훈련하여 기계 인지 문제에 놀라운 성과를 달성하리라 예상한 사람은 없지만 이제는 경사 하강법으로 충분히 큰 모두 모델을 충분히 많은 샘플에서 훈련하는 것이 필요한 전부라는 것을 안다.
  • 딥러닝에서 모든 것은 벡터이다. 모두가 기하학적 공간에 놓인 하나의 포인트이다. 
    • 모델 입력과 타깃이 먼저 벡터로 바뀌어야 한다. 초기 입력 벡터 공간과 타깃 벡터 공간으로 바꾸는 것이다. 딥러닝 모델의 각 층은 데이터에 간단한 기하학적 변화을 수행하여 통과시킨다.
    • 모델은 층을 연결하여 복잡한 기하학적 변환을 구성한다. 잘게 쪼개어보면 단순한 변환이 연결되어 있다.
    • 이런 복잡한 변환이 입력 공간을 타깃 공간으로 한 번에 하나의 포인트씩 매핑한다. 변환을 결정한느 파라미터는 층의 가중치이다. 모델이 얼마나 잘 작동하는지를 기반으로 반복적으로 업데이트 된다.
    • 기하학적 변환의 핵심 특징은 미분 가능해야 한다는 점이다. 경사 하강법으로 파라미터를 학습하기 위해 필수적이다. 이 말은 입력에서 출력으로 바뀌는 기하학적 변환이 부드럽고 연속적이어야 한다는 뜻이다. 이는 아주 큰 제약 사항이다.
  • 복잡한 기하학적 변환을 입력 데이터에 적용하는 전체 과정은 사람이 뭉쳐진 종이 공을 펼치는 것과 비슷하다. 뭉쳐진 종이 공은 모델이 처음 보게 되는 입력 데이터의 매니폴드이다.
    • 사람이 종이 공을 펼치는 움직임이 각 층에서 수행되는 간단한 기하학적 변환과 비슷하다. 종이 공을 펼치는 전체 손놀림은 모델 전체의 복잡한 변환이 된다.
    • 딥러닝 모델은 고차원 데이터의 복잡한 매니폴드를 펼치는 수학 장치이다.
  • 이것이 딥러닝의 마술 같은 기법이다. 데이터가 가진 의미를 벡터와 기하학적 공간으로 변환한 후 공간에서 다른 공간으로 매핑하는 복잡한 기하학적 변환을 점진적으로 학습한다. 원본 데이터게 있는 모든 형태의 관계를 찾기 위해 충분히 큰 고차원 공간이 필요한 전부이다.
  • 모든 것이 하나의 핵심 아이디어에서 출발한다. 
    • 의미는 어떤 두 가지 사이 관계에서 유도되고 이 관계는 거리 함수로 측정할 수 있다. 우리의 뇌가 기하학적 공간을 통해 의미를 해석하는지 여부는 완전히 다른 이야기다. 벡터 공간은 컴퓨터 입장에서는 계산하기 매우 효율적이다.
    • 지능을 표현하기 위해서는 그래프 같은 다른 데이터 구조가 더 좋다. 신경망은 초기에 그래프를 사용하여 의미를 인코딩하려는 아이디어에서 시작되었다. 이것이 신경망(neural network)이란 이름으로 부르게 된 이유이다. 다른 연구 분야에서는 연결주의(connectionism)이란 이름으로 부르기도 한다.
  • 요즘 신경망이란 이름은 역사적인 이유로만 남아 있다. 신경이나 네트워크와 관련되어 있지 않기 때문에 오해를 일으키기 쉬운 이름이다.
    • 특히 신경망은 뇌와 아무런 관련이 없다. 좀 더 적절한 이름은 층 기반 표현 학습(layered representations learning)이나 계층적 표현 학습(hierarchical representations learning)이다.
    • 또는 연속하여 기하학적 공간을 조작한다는 핵심 사실을 강조하기 위해 심층 미분 모델(deep differentiable model)이나 기하학적 변환 연결(chained geometric transform)도 가능하다.

핵심 기술

  • 딥러닝은 다음과 같은 많은 요인들이 누적되어 만들어진 결과이다. 
    • 알고리즘 혁심이 계속 늘어난다. 처음에 역전파를 개발한 이후 20년이 걸렸지만, 2012년 이후 많은 연구자가 딥러닝에 참여하여 갈수록 더 빨라지고 있다.
    • 지각에 관련된 많은 양의 데이터를 사용할 수 있다. 충분히 많은 데이터에서 충분히 큰 모델을 훈련하는 것이 필요한 전부이다. 이는 인터넷의 성장과 무어의 법칙이 적용된 저장 매체 덕분이다.
    • 고성능 병렬 컴퓨터 하드웨어를 값싸게 사용할 수 있다. NVIDIA에서 만든 GPU는 게임을 위한 장치였지만 딥러닝을 위해 새롭게 디자인 되었다.
    • 이런 컴퓨팅 파워를 활용할 수 있는 다양한 소프트웨어 스택(stack)이 마련되었다. CUDA 라이브러리, 자동 미분을 수행하는 텐서플로 같은 프레임워크, 딥러닝을 쉽게 사용하도록 도와주는 케라스 등이다.

일반적인 머신 러닝 작업 흐름

  • 머신 러닝 작업 흐름에서 정말 어려운 부분은 종종 모델을 설계하고 훈련하기 전에 있는 것들이다. 예측 대상, 활용 데이터, 성공 지표를 결정하기 위해 문제 영역을 이해하는 것이 성공적인 머신 러닝 애플리케이션을 위한 필수 조건이다. 케라스나 텐서플로 같은 도구들이 도와줄 수 없는 부분이다.
  • 전형적인 머신 러닝 작업 흐름을 요약하면 다음과 같다.
    1. 문제를 정의한다.
    2. 목표 달성을 측정하기 위해 신뢰할 수 있는 방법을 찾는다.
    3. 모델을 평가하기 위해 사용할 검증 과정을 준비한다.
    4. 데이터를 벡터화하고 신경망에 잘 맞는 형태로 전처리한다. (정규화 등)
    5. 상식 수준의 기본 모델보다 나은 첫 번째 모델을 만든다. 머신 러닝이 주어진 문제를 해결할 수 있는지 확인한다. 
    6. 하이퍼파라미터를 튜닝하고 규제를 추가하여 모델 구조를 점진적으로 개선한다. 테스트 데이터나 훈련 데이터를 사용하지 않고 검증 데이터의 성능만 사용하여 조정한다. 모델이 과대적합된 후 규제를 추가하거나 모델의 크기를 줄인다.
    7. 하이퍼파라미터를 튜닝하면 검증 세트에 과대적합 된다는 사실을 유념하라. 하이퍼파라미터가 검증 세트에 지나치게 특화될 수 있다. 이 때문에 테스트 세트를 따로 떼어 놓는다.

주요 네트워크 구조

  • 완전 연결 네트워크, 합성곱 네트워크, 순환 네트워크 이 세 종류의 네트워크 구조는 익숙해졌을 것이다. 각 네트워크 종류는 특정 입력 형식을 의미한다.
    • 네트워크 구조는 데이터의 구조에 대한 가정을 담고 있다. 좋은 모델을 탐색하기 위한 가설 공간이 된다.
    • 데이터 구조와 네트워크 구조의 가정 사이가 잘 맞는지에 따라 주어진 구조가 해당 문제에 잘 작동할지가 크게 좌우된다.
  • 이런 다양한 네트워크 종류는 더 큰 다중 네트워크를 만들기 위해 연결될 수 있다. 마치 레고 블록을 연결하는 것과 같다. 여기에서 딥러닝 층은 정보 처리를 위한 레고 블록이 된다. 다양한 입력한 적절한 네트워크 구조 사이의 관계를 간단히 정리해 보자.
    • 벡터 데이터: 완전 연결 네트워크(Dense 층)
    • 이미지 데이터: 2D 컨브넷
    • 사운드 데이터(예컨대 파형 데이터): 1D 컨브넷(권장)이나 RNN
    • 텍스트 데이터: 1D 컨브넷(권장)이나 RNN
    • 시계열 데이터: RNN(권장)이나 1D 컨브넷
    • 다른 종류의 시퀀스 데이터: RNN이나 1D 컨브넷. 데이터 순서에 중요한 의미가 있다면 RNN이 낫다.
    • 비디오 데이터: 3D 컨브넷(연속 동작을 감지할 필요가 있다면)이나 특성 추출을 담당하는 프레임별 2D 컨브넷과 그 뒤를 이어 시퀀스를 처리하는 RNN이나 1D 컨브넷의 조합
    • 볼륨을 가진 데이터: 3D 컨브넷

완전 연결 네트워크

  • 완전 연결 네트워크는 벡터 데이터(벡터의 배치)를 처리하는 Dense 층을 쌓은 것이다. 이런 네트워크는 입력 특성에 특별한 가정을 두지 않는다. 한 Dense 층의 유닛이 다른 층의 모든 유닛과 연결되어 있기 때문에 완전 연결이라 부른다.
    • 층은 모든 입력 특성 간의 관계를 매핑한다. 2D 합성곱은 이와 다르게 국부적인 관계만 바라본다.
  • 완전 연결 네트워크는 보스턴 주택 가격 데이터셋처럼 범주형 데이터에 많이 사용된다. (예컨대 입력 특성이 여러 속성 중 하나이다) 분류나 회귀 출력을 위해 다른 네트워크의 최종 단계에도 사용된다.
    • 예컨대 5장의 컨브넷과 6장의 순환 네트워크는 일반적으로 하나 또는 2개의 Dense 층으로 끝난다.
  • 다음을 기억하라. 이진 분류를 수행하려면 마지막 Dense 층이 하나의 유닛을 가져야 하고 시그모이드 활성화 함수를 사용해야 한다.
    • 손실은 binary_crossentropy를 사용한다. 타깃은 0 또는 1이 된다.
from keras import models, layers

model = models.Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(num_input_features,)))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy')
  • 단일 레이블 다중 분류를 수행하려면 (각 샘플이 정확히 하나의 클래스에만 속한다) 마지막 Dense 층이 클래스 개수만큼 유닛을 가져야 하고 softmax 활성화 함수를 사용해야 한다.
    • 타깃을 원-핫 인코딩 한다면 catrgorical_crossentropy를 손실로 사용한다. 타깃이 정수 숫자라면 sparse_categorical_crossentropy를 손실로 사용한다.
model = models.Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(num_input_features,)))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
  • 다중 레이블 다중 분류를 수행하려면 (하나의 샘플이 여러 개의 클래스에 속할 수 있다) 마지막 Dense 층이 클래스 개수만큼 유닛을 가져야 하고 시그모이드 활성화 함수를 사용해야 한다. 
    • 손실로는 binary_crossentropy를 사용해야 한다. 타깃은 k-핫 인코딩이 되어야 한다.
model = models.Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(num_input_features,)))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy')
  • 연속된 값을 가진 벡터에 대해 회귀를 수행하려면 마지막 Dense 층이 예측하려는 값의 개수만큼 유닛을 가져야 하고 (주택 가격 처럼 하나일 경우가 많다) 활성화 함수는 사용하지 않는다.
    • 회귀에는 여러 손실을 사용할 수 있다. 가장 널리 사용되는 것은 mean_squared_error(MSE)와 mean_absolute_error(MAE)이다.
model = models.Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(num_input_features,)))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_values))

model.compile(optimizer='rmsprop', loss='mse')

컨브넷

  • 합성곱 층은 입력 텐서의 여러 위치(패치)에 동일한 기하학적 변환을 적용하여 공간 방향의 지역 패턴을 찾는다. 이는 이동 불변성을 가진 표현을 만들어 합성곱 층을 데이터 효율적으로 만들고 모듈화한다.
    • 이 아이디어는 1D(시퀀스), 2D(이미지), 3D(볼륨) 등 어느 차원의 공간에도 적용가능하다.
    • Conv1D 층을 사용하여 시퀀스(특히 텍스트 데이터, 시계열 데이터는 이동 불변 가정을 따르지 않는 경우가 많기 때문에 잘 맞지 않는다)를 처리하고 Conv2D 층을 사용하여 이미지를 처리하고 Conv3D 층을 사용하여 볼륨 데이터를 처리한다.
  • 컨브넷 또는 합성곱 네트워크는 합성곱과 최대 풀링 층이 쌓여 구성된다. 풀링 층은 공간 방향으로 데이터를 다운샘플링한다.
    • 특성 맵의 수가 증가함에 따라 적절한 크기로 특성 맵 크기를 유지하여 후속 합성곱 층이 입력에서 더 큰 부분을 볼 수 있게 한다. 컨브넷은 Flatten 연산이나 전역 풀링 층으로 끝나는 경우가 많다.
    • 이 층은 공간 특성 맵을 벡터로 변환한다. 그 뒤에 분류나 회귀를 위한 Dense 층이 이어진다.
  • 일반적인 합성곱이 동일하지만 더 빠르고 효율적으로 표현을 학습하는 깊이별 분할 합성곱으로 조만간 대부분 (또는 완전히) 바뀔 것이다. (SeparableConv2D 층). 3D, 2D, 1D 입력에도 해당 된다.
    • 새로운 네트워크를 처음부터 구축할 때는 당연히 깊이별 분할 합성곱을 고려해야 한다.
    • SeparableConv2D 층은 Conv2D 대신 그대로 바꾸어 쓸 수 있으며, 더 가볍고 빠른 네트워크를 만들고 더 높은 성능을 낸다.
  • 다음은 전형적인 이미지 분류 네트워크이다 (여기서는 다중 분류)
model = models.Sequential()
model.add(layers.SeparableConv2D(32, 3, activation='relu', input_shape=(height, width, channels)))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())

model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

RNN

  • 순환 신경망(RNN)은 한 번에 하나의 타임스텝씩 입력 시퀀스를 처리하고 이 과정 동안 상태(state)를 유지한다. (상태는 하나의 벡터이거나 벡터의 집합이다. 즉, 기하학적 상태 공간의 한 포인트이다)
    • 시간 축을 따라 이동 불변성이 없는 패턴을 가진 시퀀스라면 1D 컨브넷 대신 사용하는 것이 바람직하다 (예컨대 최근 데이터가 오래된 과거보다 더 중요한 시계열 데이터)
  • 케라스에는 3개의 RNN 층이 있다. SimpleRNN, GRU, LSTM이다.
    • 대부분 실전 애플리케이션에는 GRU나 LSTM을 사용해야 한다. LSTM이 더 강력하지만 비용이 많이 들기 대문에 GRU는 좀 더 간단하고 값싼 LSTM의 대체물로 생각할 수 있다.
  • 여러 개의 RNN 층을 겹겹이 쌓으려면 마지막 층 이전의 모든 층은 전체 시퀀스를 출력해야 한다. (모든 입력 타임스탬프에 해당하는 출력이다) 추가적인 RNN 층을 쌓지 않는다면 전체 시퀀스 정보가 담긴 마지막 출력만 반환하는 것이 일반적이다.
model = models.Sequential()
model.add(layers.LSTM(32, input_shape=(num_timesteps, num_features)))
model.add(layers.Dense(num_classes, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy')
  • 다음은 벡터 시퀀스의 이진 분류를 위해 RNN 층을 쌓은 모델이다.
model = models.Sequential()
model.add(layers.LSTM(32, return_sequences=True, input_shape=(num_timesteps, num_features)))
model.add(layers.LSTM(32, return_sequences=True))
model.add(layers.LSTM(32))
model.add(layers.Dense(num_classes, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy')

딥러닝의 가능성

  • 딥러닝 모델을 만드는 것은 레고 블록을 조립하는 것과 같다는 것을 기억하라. 층을 서로 연결하여 어떤 것이라도 매핑할 수 있다. 적절한 훈련 데이터와 수용할 만한 복잡도가 있는 연속된 기하학적 변환을 통한 매핑이어야 한다. 가능한 조합은 무한하다. 
    • 전통적으로 기초적인 분류와 회귀 작업은 머신 러닝의 기본이다.
  • 다음은 입력과 출력의 종류에 따라 주목할 만한 애플리케이션을 정리한 것이다.
    • 벡터 데이터를 벡터 데이터로 매핑하기
      • 예측 의학
      • 행동 타기팅
      • 품질 제어
    • 이미지 데이터를 벡터 데이터로 매핑하기
      • 의료 진단 보조
      • 자율 주행 자동차
      • 보드 게임 AI
      • 식단 도우미
      • 나이 예측
    • 시계열 데이터를 벡터 데이터로 매핑하기
      • 날씨 예측
      • 뇌-컴퓨터 인터페이스
      • 행동 타기팅
    • 텍스트를 텍스트로 매핑하기
      • 스마트 답장
      • 질문 응답
      • 요약
    • 이미지를 텍스트로 매핑하기
      • 캡셔닝
    • 텍스트를 이미지로 매핑하기
      • 조건부 이미지 생성
      • 로고 생성/선택
    • 이미지를 이미지로 매핑하기
      • 초고해상도 변환
      • 공간 깊이 감지
    • 이미지와 텍스트를 텍스트로 매핑하기
      • 비주얼 QA
    • 비디오와 텍스트를 텍스트로 매핑하기
      • 비디오 QA

딥러닝의 한계

  • 딥러닝으로 구현할 수 있는 애플리케이션 영역은 무한하지만 사람이 레이블링한 데이터가 아주 많더라도 현재 딥러닝 기술로는 달성하기 어려운 애플리케이션이 많다.
    • 예컨대 제품 관리자가 작성한 소프트웨어 기능 정의서와 개발 팀이 이 요구사항에 맞춰 개발한 소스 코드로 이루어진 데이터를 이용하더라도 제품 설명서를 보고 소스 코드를 작성하는 딥러닝 모델은 훈련할 수 없다.
    • 일반적으로 프로그래밍처럼 논증이 필요하거나 장기 계획을 세워 과학적 방법을 적용하거나 알고리즘을 사용하여 데이터를 조작하는 일은 주입하는 데이터의 양의 상관 없이 딥러닝 모델로 달성할 수 없는 영역이다.
    • 심층 신경망으로는 정렬 알고리즘을 훈련하는 것 조차도 아주 어렵다.
  • 딥러닝 모델은 한 벡터 공간을 다른 벡터 공간으로 매핑하기 위해 단순하고 연속된 기하학적 변환을 연결한 것이다. 매니폴드 X에서 매니폴드 Y로 매핑하는 것이 할 수 있는 전부이다.
    • 딥러닝 모델은 일종의 프로그램으로 생각할 수 있지만 반대로 대부분의 프로그램은 딥러닝 모델로 표현할 수 없다.
  • (중간 설명 생략)
  • 딥러닝 모델이 표현할 수 있는 한계가 있고 대부분의 학습 대상 프로그램은 데이터 매니폴드에 대한 연속된 기하학적 변환으로 나타낼 수 없는 근본적인 문제를 해결하지 못한다.

머신 러닝 모델의 의인화 위험

  • 현대 AI에서 오는 실제 위험은 딥러닝 모델이 하는 일을 잘 이해하지 못하고 그 능력을 과대 평가하는 데서 온다. 
    • 사람들은 사진을 설명하는 캡션 생성 모델이 성공적으로 훈련될 때 모델이 그림의 내용을 ‘이해’하고 캡션을 만들었다고 오해하기 때문에 훈련 데이터에 있는 이미지와 조금만 달라져도 모델이 완전히 엉뚱한 캡션을 다는 것을 보고 놀라게 된다..

  • 특히 모델이 잘못 분류하도록 고안된 적대적인 샘플(adversarial example)을 딥러닝 네트워크에 주입할 때 이런 문제가 두드러진다.
    • 판다 이미지에 긴팔 원숭이 그래디언트를 더하면 신경망이 판다를 긴팔원숭이로 분류한다.
    • 이는 딥러닝 모델이 불안정하다는 증거이고, 입력-출력 매핑과 사람의 지각 사이에 큰 차이점을 보여준다.

  • 딥러닝 모델은 입력을 전혀 이해하지 못한다. 사람이 이미지, 소리, 언어를 이해하는 것은 인간의 지각 경험을 통해 형성된 것인데, 머신 러닝 모델은 이런 경험이 없으므로 사람과 같은 방식으로 입력을 이해할 수 없다.
    • 레이블된 많은 양의 훈련 샘플을 모델에 주입하여 데이터를 사람의 개념에 매핑하는 기하학적 변환을 학습한다.
    • 이 매핑은 사람이 경험에서 학습하여 마음 속에 내재된 진짜 모델을 단순하게 흉내낸 것이다. 이는 거울 속에 비친 흐릿한 이미지와 같다.

  • 머신 러닝 기술자는 항상 이를 기억해야 한다. 신경망이 수행하는 작업을 이해한다고 믿는 함정에 빠져서는 안 된다.

지역 일반화 vs 궁극 일반화

  • 딥러닝 모델이 수행하는 입력-출력 사이의 간단한 기하학적 변환과 사람이 생각하고 배우는 방식 사이에는 근본적인 차이가 있다.
    • 사람이 명시적인 훈련 샘플을 사용하는 대신 몸에 배인 경험에서 학습한다는 것만이 아니다. 학습 과정이 다른 것 이외에도 근본적인 표현에 차이가 있다.
  • 사람은 심층 네트워크 또는 곤충들처럼 자극과 반응을 매핑하는 것 이상을 수행한다. 현재 상황과 자기 자신, 다른 사람에 대한 복잡하고 추상적인 모델을 구성한다.
    • 이 모델을 사용하여 미래 가능성을 예측하고 장기 계획을 세운다. 우리는 경험하지 못한 어떤 것을 표현하기 위해 알고 있는 개념을 합친다.
  • 가상의 일을 다루는 이런 능력은 추상(abstraction)과 추론(reasoning)을 통해 우리 마음속에 있는 모델의 공간을 직접 경험할 수 있는 것 이상으로 확장한다.
    • 이것이 사람이 가진 인지 능력의 특징이다. 필자는 이른 궁극 일반화(extreme generalization)이라고 부른다. 
    • 데이터가 조금만 있거나 심지어 새로운 데이터가 전혀 없어도 이전에 경험한 적 없는 새로운 상황에 적응하는 능력이다.
  • (이하 예시 설명 생략)

  • 요약하면 기계 인지에 대한 발전에도 사람 수준의 AI는 아직 멀었다. 현재 모델은 과거 데이터와 비슷한 환경이 적용되는 지역 일반화만 수행할 수 있다.
    • 반면 사람의 인지 능력은 완전히 새로운 환경에 빠르게 적응하고 먼 미래으 상황을 계획할 수 있는 궁극 일반화이다.

정리

  • 딥러닝의 실제 성공은 연속된 기하학적 변환을 사용하여 공간 X에서 공간 Y로 매핑하는 능력이다. 이런 방식이 모든 산업을 획기적으로 변화시킬 것이다.
  • 하지만 사람 수준의 AI에는 한참 멀었다. 사람의 뇌와 경쟁할 수 있는 AI를 만들기 위해서는 단순한 입력-출력 매핑을 벗어나 추상과 추론을 해야 한다.

딥러닝의 미래

  • 필자가 기대하는 주요 방향은 다음과 같다.
    • 모델은 범용 목적의 컴퓨터 프로그램에 가까워질 것이다. 현재의 미분 가능한 층보다 더 뛰어난 모듈로 만들어질 것이다. 이것이 현재 모델의 근본적인 약점인 부족한 추상과 추론을 얻는 방법이 될 것이다.
    • 위 사항을 가능하게 만드는 새로운 형태의 학습 덕택에 미분 가능한 변환에서 탈피한 모델이 등장할 것이다.
    • 엔지니어는 모델에 덜 관여할 것이다. 끝도 없이 하이퍼파라미터를 튜닝하는 것은 우리의 일이 아니다.
    • 재사용 가능하고 모듈화된 프로그램 서브루틴을 사용한 메타러닝 시스템(meta-learning system)처럼 더 훌륭하고 체계적으로 이전에 학습한 특성과 구조를 재사용할 것이다.

프로그램 같은 모델

  • 머신 러닝 분야에서 기대하는 필수적인 변화는 순수한 패턴 인식을 수행하고 지역 일반화만 얻을 수 있는 모델을 탈피하는 것이다. 추상과 추론을 통해 궁극 일반화를 달성할 수 있는 모델이다.
    • 기본 형태의 추론을 할 수 있는 현재 AI 프로그램은 모두 프로그래머가 하드코딩한 것이다. 예컨대 탐색 알고리즘, 그래프 처리, 형식 논리에 기반한 소프트웨어이다.
    • 딥마인드의 알파고에서 가장 지능적인 부분은 전문 프로그래머가 설계하고 하드코딩한 것이다.(몬테 카를로 트리 탐색(Monte Carlo Tree Search)이 한 예)
    • 특정 서브모듈만 데이터에서 학습한다(가치 네트워크(value network)와 정책 네트워크(policy network))
    • 미래에는 이런 AI 시스템이 사람의 개입 없이 완전하게 학습될 것이다.
  • (예시 생략)
  • 우리는 하드코딩된 알고리즘의 지능(수동으로 만든 소프트웨어)과 학습된 기하학적 지능(딥러닝)에서 탈피할 것이다.
    • 그 대신 추론과 추상 능력을 가진 형식이 있는 알고리즘 모듈과 형식이 없는 직관가 패턴 인식 능력을 제공하는 기하학적 모듈이 합쳐질 것이다.
    • 전체 시스템은 사람이 거의 개입하지 않고 학습될 것이다.
  • 관련되어 크게 성장하리라 생각되는 AI의 하위 분야는 프로그램 합성(program synthesis)이다. 특히 신경망 프로그램 합성(neural program synthesis)이다.
    • 프로그램 합성은 탐색 알고리즘을 (유전 프로그래밍(genetic programming)처럼 아마도 유전 탐색 방식으로) 사용해서 거대한 프로그래밍 가능 공간을 탐색하여 자동으로 간단한 프로그램을 합성한다.
    • 프로그램이 입력-출력 쌍으로 제공되는 필요 사양을 만족시키면 탐색이 중지된다. 이는 머신 러닝이 하는 것과 같다.
    • 입력-출력 쌍으로 훈련 데이터가 주어지면 입력-출력을 매핑하고 새로운 입력에 일반화할 수 있는 모델을 찾는다.
    • 차이는 하드코딩된 프로그램(신경망)에서 모델 파라미터를 학습하는 것이 아니라 이산적인 탐색 과정(discrete search process)을 통해 소스 코드를 생성하는 것이다.
  • 이 분야가 다음 몇 년 안에 새로운 관심을 불러일으킬 것으로 확실히 예상된다. 특히 딥러닝과 프로그램 합성을 아우르는 분야가 등장하리라 기대한다. 
    • 범용 프로그래밍 언어로 프로그램을 생성하는 대신 for 루프 등 다양한 알고리즘 요소로 무장한 신경망을 생성할 것이다. 직접 소스 코드를 생성하는 것보다 훨씬 쉽고 유용하다.
    • 머신 러닝으로 풀 수 있는 문제의 범위, 즉 적절한 훈련이 주어졌을 때 자동으로 생성할 수 있는 프로그램 공간이 크게 확장될 것이다. 현재 RNN은 알고리즘-기하학적 혼합 모델의 선사시대 조상이라 볼 수 있다.

역전파와 미분 가능 층을 넘어서

  • 머신 러닝 모델이 좀 더 프로그램처럼 된다면 더는 미분 가능할 필요가 없다. 이 프로그램은 여전히 서브루틴으로 연속된 기하학적 변환 층을 사용하겠지만 전체 모델은 그렇지 않을 것이다.
    • 결국 고정되고 하드코딩된 네트워크의 가중치를 조정하기 위해 역전파를 사용하는 것은 미래에 모델을 훈련하는 방법이 되지 못할 것이다. 적어도 전부는 아닐 것이다.
  • 미분 가능하지 않은 시스템을 효율적으로 훈련할 방법을 찾아야 한다. 현재는 유전 알고리즘, 진화 전력, 일부 강화 학습, ADMM(Alternating Direction Method of Multipliers) 같은 방법이 있다.
    • 그렇다고 경사 하강법이 사라지는 것은 아니다. 그래디언트 정보는 미분 가능한 파라미터 모델보다 점점 더 발전할 것이다. 따라서 자동화된 개발은 역전파 그 이상이 필요할 것이다.
  • 역전파는 엔트-투-엔드(end-to-end)로 연속된 변환을 학습하는데 뛰어나지만 심층 네트워크의 모듈성을 십분 활용하지 않기 때문에 계산 효율성이 떨어진다.
    • 좀 더 효율적으로 만들기 위한 보편적이 한 가지 방법이 있는데 모듈화와 계층화이다.
    • 동기화 메커니즘을 추가하여 훈련 모듈을 독립적으로 분리하면 역전파를 좀 더 효율적으로 수행할 수 있다. 이런 전략이 딥마인드의 신태틱 그래디언트(synthetic gradient)에 관한 연구에 일부 반영되었다.
    • 필자는 가까운 미래에 이 부분에 더 많은 발전이 있을 것으로 기대한다. 그래디언트를 사용하지 않는 효율적인 탐색 과정을 거쳐 전체적으로 미분 가능하지 않은 (부분적으로는 미분 가능한) 모델이 훈련되는 미래를 상상한다.
    • 반면 미분 가능한 부분은 높은 효율의 역전파 방식을 사용하고 그래디언트의 이점을 살려 더 빨리 훈련될 것이다.

자동화된 머신 러닝

  • 미래에는 모델 구조가 전문 엔지니어에 의해 고안되는 것이 아니라 학습될 것이다. 구조를 학습하는 것은 프로그램 같은 머신 러닝 모델과 풍부한 구성 요소를 재사용하는 것에 밀접하게 관련되어 있다.
  • 현재는 딥러닝 엔지니어의 대부분 업무가 파이썬 스크립트로 데이터를 이리저리 처리하거나 작동하는 모델을 얻기 위해 심층 네트워크의 구조와 많은 하이퍼파라미터를 튜닝하는 것이다.
    • 용감한 엔지니어는 현존하는 최상의 모델을 얻기 위해 튜닝하기도 한다. 말할 필요도 없이 이는 최선이 아니다. 이 부분은 AI가 도울 수 있다.
    • 안타깝지만 데이터 전처리 부분은 자동화하기 어렵다. 도메인 지식은 물론 엔지니어가 하려는 것에 대한 명확한 고수준의 이해가 필요하기 때문이다.
  • 이에 반해 하이퍼파라미터 튜닝은 간단한 탐색 절차이다. 엔지니어가 원하는 바를 알고 있다면 튜닝되는 네트워크의 손실 함수로 정의될 수 있다. 이것이 모델 파라미터 튜닝을 담당하는 AutoML 시스템에서 널리 사용되는 방식이다.
    • 기본적으로 이런 시스템은 쌓여 있는 층의 개수와 순서, 유닛과 필터의 개수를 튜닝한다. 이것이 7장에서 이야기한 Hyperopt 같은 라이브러리가 하는 방식이다.
    • 좀 더 대담하게 가능한 제약을 최소화하기 위해 강화 학습이나 유전 알고리즘 등을 사용하여 아예 처음부터 적절한 구조를 학습할 수도 있다.
  • 또 다른 중요한 AutoML의 방향은 모델의 가중치와 함께 모델 구조를 학습하는 것이다. 조금씩 다른 구조를 실험할 때마다 밑바닥부터 새로운 모델을 훈련하는 것은 매우 비효율적이다.
    • 이런 이유 때문에 진짜 강력한 AutoML 시스템은 모델의 특성이 훈련 데이터에서 역전파로 튜닝됨과 동시에 구조를 찾을 것이다. 이런 방법은 이제 막 생겨나기 시작했다.
  • 이런 일이 이루어지더라도 머신 러닝 엔지니어의 직업이 사라지지 않는다. 오히려 엔지니어는 가치 창조 사슬 위로 이동할 것이다.
    • 비즈니스 목표를 잘 반영하는 손실 함수를 만들고 이 모델이 활동할 디지털 생태계(예컨대 모델의 예측을 소비하고 훈련 데이터를 생산하는 사용자들)에 미칠 영향을 이해하는데 더 많은 노력을 기울이게 될 것이다.

영구 학습과 모듈화된 서브루틴 재사용

  • 모델이 더 복잡해지고 풍부한 알고리즘 요소 위에서 구축되면 늘어난 복잡도 덕택에 작업 간의 재사용성이 증가할 것이다. 새로운 작업이나 새로운 데이터셋마다 새로운 모델을 처음부터 훈련하지 않아도 된다.
    • 많은 데이터셋은 새로운 복잡한 모델을 처음부터 만들기에 충분한 정보를 담고 있지 않다. 이전에 보았던 데이터셋에서 얻은 정보를 재사용할 필요가 있다.
    • 현재 작업과 이전 작업 사이에 중복되는 점이 많기 때문에 새로운 작업마다 처음부터 모델을 훈련하는 것이 비효율적이기도 하다.
  • 최근 몇 년간 한 가지 눈에 띄는 점이 반복적으로 관찰되었는데, 느슨하게 연관된 몇 개의 작업에서 같은 모델을 동시에 훈련하면 각 작업에서 더 뛰어난 모델을 만든다.
    • 예컨대 영어-독일어 번역과 프랑스어-이탈리어 번역에 동일한 기계 번역 신경망 모델을 훈련하면 각 언어 번역에서 더 뛰어난 모델을 얻게 된다.
    • 비슷하게 같은 합성곱 기반을 공유하는 이미지 분류 모델과 이미지 분할 모델을 함께 훈련하면 양쪽 작업에 더 뛰어난 모델을 만들 수 있다.
  • 아주 직관적인 현상인데, 겉으로 보기에 관련 없는 작업 사이에 일정량의 정보가 항상 중복되어 있다. 하나의 작업에서만 훈련된 모델보다 병합 모델이 많은 양의 정보를 활용할 수 있다.
    • 현재는 작업 간에 모델을 재사용할 때 시각 특성 추출 같은 공통 기능을 수행하는 모델의 사전 훈련된 가중치를 사용한다. 5장에서 이런 방식을 보았다.
  • 미레에는 이것이 좀 더 일반화 되어 어느 곳에나 등장할 것으로 기대한다. 이전에 학습된 특성(부분 모델의 가중치) 뿐만 아니라 모델 구조와 훈련 과정도 사용할 것이다.
    • 모델이 프로그램과 더욱 비슷해지면 프로그래밍 언어에 있는 함수나 클래스 처럼 모델의 프로그램 서브루틴을 재사용하기 시작할 것이다.
  • (이하 소프트웨어 개발 과정 설명 생략)

장기 비전

  • 머신 러닝 모델에 대한 장기 비전을 요약하면 다음과 같다.
    • 모델은 더 프로그램 같아질 것이다. 현재 우리가 수행하는 입력 데이터의 연속된 기하학적 변환 이상을 수행할 수 있는 능력을 가질 것이다.
      • 이런 프로그램은 사람이 주변 환경과 자신에 대해 가진 추상적인 정신 모델에 더 가까워질 것이며, 이런 모델은 풍부한 알고리즘 능력 때문에 더욱 강한 일반화 성능을 가질 것이다.
    • 특히 형식 추론, 탐색, 추상화 능력을 제공하는 알고리즘 모듈과 비형식적인 직관과 패턴 인식 능력을 제공하는 기하학적 모듈을 혼합할 것이다.
      • 알파고가 심볼릭과 기하학적 기반의 혼합된 AI 초기 버전을 보여주었다.
    • 이런 모델은 엔지니어가 하드코딩하는 대신 자동으로 만들어질 것이다. 재사용 가능한 서브 루틴의 전체 공통 라이브러리에 저장된 모듈을 사용한다.
      • 이 라이브러리는 수천 개의 작업과 데이터셋에서 고성능 모델을 학습하여 발전 시킨 것으로, 메타 학습 시스템에 의해 자주 발견되는 문제 해결 패턴은 재사용 가능한 서브루틴으로 바뀌어 전체 공통 라이브러리에 추가될 것이다.
      • 이 서브루틴은 소프트웨어 공학의 함수나 클래스와 매우 비슷하다. 이는 추상화를 구현하게 된다.
    • 전체 공통 라이브러리와 이와 연계된 모델 성장 시스템은 사람과 비슷한 궁극 일반화의 어떤 형태를 달성할 수 있을 것이다.
      • 새로운 작업이나 상황이 주어지면 시스템이 매우 적은 데이터를 사용하여 이 작업에 적절한 새로운 작동 모델을 조립할 수 있다.
      • 이는 일반화가 잘된 풍부한 프로그램적 요소와 비슷한 작업에서 얻은 폭넓은 경험 덕택이다.
      • 비슷한 방식으로 이전에 여러 게임을 플레이한 경험을 가지고 있다면 사람은 복잡한 새로운 비디오 게임을 빠르게 배울 수 있다. 이전 경험에서 유도된 모델은 자극과 행동 사이의 기본 매핑이 아니고 추상적이고 프로그램에 가깝기 때문이다.
    • 이런 식으로 영구 학습 모델 성장 시스템을 인공 일반 지능(Artificial General Intelligence, AGI)으로 생각할 수 있다.
      • 기술적 특이점을 추종하는 사람들이 생각하는 어떤 로봇 재앙도 기대하지 마라. 지능과 기술에 대한 아주 큰 오해가 깊게 쌓여 생긴 순수한 환상일 뿐이다. 이 책은 그런 견해를 따르지 않는다.

빠른 변화에 뒤처지지 않기

  • (이하 생략)
    • 캐글의 실전 문제로 연습하고
    • 아카이브(arXiv)에서 최신 논문을 읽고
    • 케라스 생태계를 활용해라.

맺음말

  • AI 분야의 배움은 아주 긴 여행이다. 이 분야는 확실히 아는 것보다 모르는 것이 훨씬 많다.
  • 지금까지 많은 발전이 있었지만 AI 분야에서 근본적인 질문들은 대부분 답을 찾지 못한 상태이다. 아지 많은 것들은 질문조차 되지 않았다.

케라스 창시자에게 배우는 딥러닝/ 생성 모델을 위한 딥러닝

  • 사람의 사고를 모방하려는 인공 지능의 잠재력은 이미지 인식 같은 소극적인 작업이나 자율 주행처럼 상당히 반응적인 작업을 넘어서고 있다. 인공 지능은 창작 활동으로 범위를 넓히고 있다.
  • (사례 생략)
  • 사실 지금까지 본 AI 예술 작품의 품질은 낮다. AI가 실제 작가나 화가, 작곡가와 견줄 수준은 못 된다. 사람을 대신한다는 것은 늘 핵심을 벗어난 생각이다.
    • 인공 지능이 사람의 지능을 어떤 다른 것으로 대체하는 것이 아니라 우리 생활과 일에 지능을 더한다. 이 지능은 다른 종류의 지능이다.
    • 여러 분야에서 특히 예술에서 AI는 사람의 능력을 증가시키는 도구로 사용될 것이다. 즉, 인공 지능이 아니라 확장된 지능(augmented intelligence)이다.
  • 예술 창작의 대부분은 간단한 패턴 인식과 기술로 만들어진다. 많은 사람은 이런 과정에 관심이 없거나 심지어 불필요하다고 생각하는데, 여기에 AI가 필요하다.
    • 사람의 지각, 언어, 예술 작품은 모두 통계적 구조를 가진다. 딥러닝 알고리즘은 이 구조를 학습하는데 뛰어나다.
    • 머신 러닝 모델은 이미지, 음악, 글의 통계적 잠재 공간(latent space)을 학습할 수 있다. 그 다음 이 공간에서 샘플을 뽑아 새로운 예술 작품을 만들 수 있다. 이 작품은 모델이 훈련 데이터에서 본 것과 비슷한 특징을 가질 것이다.
    • 당연히 이런 샘플링 자체가 예술 창작이라고 보기는 어렵다. 그냥 수학 연산에 불과하다.
  • 숙련된 예술가가 사용하면 알고리즘 창작은 의미 있고 아름다운 것으로 바뀔 수 있다. 잠재 공간 샘플링은 예술가의 능력을 높이는 붓이 될 수 있다.
  • (이하 생략)

LSTM으로 텍스트 생성하기

생성 RNN의 간단한 역사

  • 2014년 후반 머신 러닝 공동체에서도 소수의 사람들만이 LSTM 이란 용어를 알았다. 순환 네트워크를 사용하여 시퀀스 데이터를 성공적으로 생성한 애플리케이션은 2016년이 되어서야 주류가 되기 시작했다.
    • 하지만 이 기술은 1997년 LSTM 알고리즘이 개발된 이래 역사가 꽤 오래 되었다. 이 알고리즘은 초기에 글자를 하나씩 생성하는데 사용되었다.
  • 2002년 더글라스 에크(Douglas Eck)는 스위스의 슈미드후버(Schmidhuber)의 연구실에서 LSTM을 음악 생성에 처음 적용하여 가능성 있는 결과를 얻었다.
    • 에크는 현재 구글 브레인의 연구원으로 2016년에는 마젠타(Magenta)란 새로운 연구 그룹을 싲가해서 최신 딥러닝 기술을 사용하여 멋진 음악을 만드는 것에 집중하고 있다.
  • 2000년대 후반과 2010년대 초반에 알렉스 그레이브스(Alex Graves)는 순환 네트워크를 사용하여 시퀀스 데이터를 생성하는데 아주 중요한 선구적인 일을 했다.
    • 특히 2013년에 펜 위치를 기록한 시계열 데이터를 사용하여 순환 네트워크와 완전 연결 네트워크를 혼합한 네트워크로 사람이 쓴 것 같은 손글씨를 생성했으며, 이 작업이 전환점이 되었다.
  • (이하 생략)

시퀀스 데이터를 어떻게 생성할까?

  • 딥러닝에서 시퀀스 데이터를 생성하는 일반적인 방법은 이전 토큰을 입력으로 사용해서 시퀀스의 다음 1개 또는 몇 개의 토큰을 (RNN이나 컨브넷으로) 예측하는 것이다.
    • 예컨대 “the cat is on ma”란 입력이 주어지면 다음 글자인 타깃 “t”를 예측하도록 네트워크를 훈련한다.
    • 텍스트 데이터를 다룰 때 토큰은 보통 단어 또는 글자이다. 이전 토큰들이 주어졌을 때 다음 토큰의 확률을 모델링할 수 있는 네트워크를 언어 모델(language model)이라고 부른다. 언어 모델은 언어의 통계적 구조인 잠재 공간을 탐색한다.
  • 언어 모델을 훈련하고 나면 이 모델에서 샘플링을 할 수 있다(새로운 시퀀스를 생성한다).
    • 초기 텍스트 문자열을 주입하고 (조건 데이터(conditioning data)라고 부른다) 새로운 글자나 단어를 생성한다. (한 번에 여러 개의 토큰을 생성할 수도 있다).
    • 생성된 출력은 다시 입력 데이터로 추가된다. 이 과정을 여러 번 반복한다. 이런 반복을 통해 모델이 훈련한 데이터 구조가 반영된 임의의 길이를 가진 시퀀스를 생성할 수 있다. 사람이 쓴 문장과 거의 비슷한 시퀀스를 만든다.
    • 이 절의 예제는 LSTM 층을 사용한다. 텍스트 말뭉치(corpus)에서 N개의 글자로 이루어진 문자열을 추출하여 주입하고 N+1번째 글자를 예측하도록 훈련한다.
    • 모델의 출력은 출력 가능한 모든 글자에 해당하는 소프트맥스 값이다. 즉, 다음 글자의 확률 분포이다. 이 LSTM을 글자 수준의 신경망 언어 모델(character-elvel neural language model)이라고 부른다.

샘플링 전략의 중요성

  • 텍스트를 생성할 때 다음 글자를 선택하는 방법이 아주 중요하다.
    • 단순한 방법은 항상 가장 높은 확률을 가진 글자를 선택하는 탐욕적 샘플링(greedy sampling)이다. 이 방법은 반복적이고 예상 가능한 문자열을 만들기 때문에 논맂거인 언어처럼 보이지 않는다.
    • 좀 더 흥미로운 방법을 선택하면 놀라운 선택이 만들어지는데, 다음 글자의 확률 분포에서 샘플링하는 과정에 무작위성을 주입하는 방법이다. 이를 확률적 샘플링(stochastic sampling)이라고 부른다. (머신 러닝에서 확률적(stochastic)이란 뜻은 무작위(random)하다는 뜻이다.)
    • 이런 방식을 사용할 경우 ‘e’가 다음 글자가 될 확률이 0.3이라면 모델이 30% 정도는 이 글자를 선택한다.
    • 탐욕적 샘플링을 확률적 샘플링으로 설명할 수도 있다. 한 글자의 확률이 1이고 나머지 글자는 모두 0인 확률 분포를 보이는 경우다.
  • 모든 소프트맥스 출력은 확률적 샘플링에 사용하기 좋다. 이따금 샘플링 될 것 같지 않은 글자를 샘플링한다. 훈련 데이터에는 없지만 실제 같은 새로운 단어를 만들어 재미있고 창의적으로 보이는 문장을 생성한다.
    • 이 전략에는 한 가지 문제가 있는데, 샘플링 과정에서 무작위성의 양을 조절할 방법이 없다.
  • 왜 무작위성이 크거나 작아야 할까? 극단적인 경우를 생각해 보자.
    • 균등 확률 분포에서 다음 글자를 추출하는 완전한 무작위 샘플링이 있다. 모든 글자의 확률은 같다. 이 구조는 무작위성이 최대이다. 다른 말로 하면 이 확률 분포는 최대의 엔트로피를 가진다. 당연하게 흥미로운 것들을 생산하지 못한다.
    • 반대로 탐욕적 샘플링은 무작위성이 없기 때문에 흥미로운 것을 전혀 만들지 못한다. 탐욕적 샘플링의 확률 분포는 최소의 엔트로피를 가진다.
    • 모델의 소프트맥스 출력인 ‘실제’ 확률 분포에서 샘플링하는 것은 이 두 극단의 중간에 위치해 있다. 중간 지점에는 시도해 볼만한 더 높거나 낮은 엔트로피가 많다.
    • 작은 엔트로피는 예상 가능한 구조를 가진 시퀀스를 생성한다. (더 실제처럼 보인다) 반면 높은 엔트로피는 놀랍고 창의적인 시퀀스를 만든다.
    • 생성 모델에서 샘플링할 때 생성 과정에서 무작위성의 양을 바꾸어 시도해 보는 것이 좋다. 흥미는 매우 주관적이므로 최적의 엔트로피 값을 미리 알 수 없기 떄문이다. 얼마나 흥미로운 데이터를 생성할 것인지는 결국 사람이 판단해야 한다.
  • 샘플링 과정에서 확률의 양을 조절하기 위해 소프트맥스 온도(softmax temperature)라는 파라미터를 사용한다. 이 파라미터는 샘플링에 사용되는 확률 분포의 엔트로피를 나타낸다. 얼마나 놀라운 또는 예상되는 글자를 선택할지 결정한다.
    • temperature 값이 주어지면 다음과 같이 가중치를 적용하여 (모델의 소프트맥스 출력인) 원본 확률 분포에서 새로운 확률 분포를 계산한다.
import numpy as np

def reweight_distribution(original_distribution, temperature=0.5):
distribution = np.log(original_distribution) / temperature
distribution = np.exp(distribution)
return distribution / np.sum(distribution)
  • 높은 온도는 엔트로피가 높은 샘플링 분포를 만들어 더 놀랍고 생소한 데이터를 생성한다. 반면 낮은 온도는 무작위성이 낮기 때문에 예상할 수 있는 데이터를 생성한다.

글자 수준의 LSTM 텍스트 생성 모델 구현

  • 이런 아이디어를 케라스로 구현해 보자. 먼저 언어 모델을 학습하기 위해 많은 텍스트 데이터가 필요하다. ‘위키피디아’나 <반지의 제왕>처럼 아주 큰 텍스트 파일이나 텍스트 파일의 묶음을 사용할 수 있다.
    • 여기서는 19세기 후반 독일의 철학자 니체의 영어로 번역된 글을 사용하겠다.
    • 학습할 언어 모델은 일반적인 영어 모델이 아니라 니체의 문체와 특정 주제를 따르는 모델일 것이다.

데이터 전처리

  • 먼저 말뭉치를 내려받아 소문자로 바꾼다.
import keras
import numpy as np

path = keras.utils.get_file('nietzsche.txt', origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')
text = open(path).read().lower()
print('말뭉치 크기:', len(text))
  • 그 다음 maxlen 길이를 가진 시퀀스를 중복하여 추출한다. 추출된 시퀀스를 원-핫 인코딩으로 변환하고 크기가 (sequences, maxlen, unique_characters)인 3D 넘파이 배열 x로 합친다.
    • 동시에 훈련 샘플에 상응하는 타깃을 담은 배열 y를 준비한다. 타깃은 추출된 시퀀스 다음에 오는 원-핫 인코딩된 글자이다.
maxlen = 60
step = 3

sentences = []
next_chars = []

for i in range(0, len(text) - maxlen, step):
sentences.append(text[i: i+maxlen])
next_chars.append(text[i+maxlen])
print('시퀀스 개수:', len(sentences))

chars = sorted(list(set(text)))
print('고유한 글자:', len(chars))
char_indices = dict((char, chars.index(char)) for char in chars)

print('벡터화...')

x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)

for i, sentence in enumerate(sentences):
for t, char in enumerate(sentence):
x[i, t, char_indices[char]] = 1
y[i, char_indices[next_chars[i]]] = 1

네트워크 구성

  • 이 네트워크는 하나의 LSTM층과 그 뒤에 Dense 분류기가 뒤따른다. 분류기는 가능한 모든 글자에 대한 소프트맥스 출력을 만든다.
    • 순환 신경망이 시퀀스 데이터를 생성하는 유일한 방법은 아니다. 최근에는 1D 컨브넷도 이런 작업에 아주 잘 들어맞는다는 것이 밝혀졌다.
from keras import layers

model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation='softmax'))
  • 타깃이 원-핫 인코딩 되어 있기 때문에 모델을 훈련하기 위해 categorical_crossentropy 손실을 사용한다.
optimizer = keras.optimizers.RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)

언어 모델 훈련과 샘플링

  • 훈련된 모델과 시드(seed)로 쓰일 간단한 텍스트가 주어지면 다음과 같이 반복하여 새로운 텍스트를 생성할 수 있다.
    1. 지금까지 생성된 텍스트를 주입하여 모델에서 다음 글자에 대한 확률 분포를 뽑는다.
    2. 특정 온도로 이 확률 분포의 가중치를 조정한다.
    3. 가중치가 조정된 분포에서 무작위로 새로운 글자를 샘플링하낟.
    4. 새로운 글자를 생성된 텍스트의 끝에 추가한다.
  • 다음 코드는 모델에서 나온 원본 확률 분포의 가중치를 조정하고 새로운 글자의 인덱스를 추출한다. (샘플링 함수이다)
def sample(preds, temperature=1.0):
preds = np.asarray(preds).astype('float64')
preds = np.log(preds) / temperature
exp_preds = np.exp(preds)
preds = exp_preds / np.sum(exp_preds)
probas = np.random.multinomial(1, preds, 1)
return np.argmax(probas)
  • 마지막으로 다음 반복문은 반복적으로 훈련하고 텍스트를 생성한다. 에포크마다 학습이 끝난 후 여러 가지 온도를 사용하여 텍스트를 생성한다. 이렇게 하면 모델이 수렴하면서 생성된 텍스트가 어떻게 진화하는지 볼 수 있다.
    • 온도가 샘플링 전략에 미치는 영향도 보여준다.
import random
import sys

random.seed(42)
start_index = random.randint(0, len(text) - maxlen - 1)

for epoch in range(1, 60):
print('에포크', epoch)
model.fit(x, y, batch_size=128, epochs=1)

seed_text = text[start_index: start_index + maxlen]
print('--- 시드 텍스트: "' + seed_text + '"')

for temperature in [0.2, 0.5, 1.0, 1.2]:
print('------ 온도: ', temperature)
generated_text = seed_text
sys.stdout.write(generated_text)

for i in range(400):
sampled = np.zeros((1, maxlen, len(chars)))

for t, char in enumerate(generated_text):
sampled[0, t, char_indices[char]] = 1.

preds = model.predict(sampled, verbose=0)[0]
next_index = sample(preds, temperature)
next_char = chars[next_index]

generated_text += next_char
generated_text = generated_text[1:]

sys.stdout.write(next_char)
sys.stdout.flush()
print()
  • 시드 텍스트에 대한 온도별 결과는 다음과 같다.
--- 시드 텍스트: "the slowly ascending ranks and classes, in which,
through fo"
------ 온도: 0.2 the slowly ascending ranks and classes, in which, through for the stand of the stand and the spirit of the same that a soul the stand the stand of the stand of the same and decession and and the same and some soul the stand the stand of the standards and in the sensition of the most stand and all the stand the stand and the spirit of the stand in the possible of the same and power and in the stand of the same and the stand the stand and the same and the sa
------ 온도: 0.5 the slowly ascending ranks and classes, in which, through for the has so indigent to the prescious of the wasting to suffer of the communness, suffer the fact of the resurtion of the inger of she is a states and stand and stages of the man with lemmandent them and has and depromentarian to be the revering in bady and all will perhaps a sentiment of depress, our personal to be as he is a sense of all and opprenes the last thought of mind and same to becenti
------ 온도: 1.0 the slowly ascending ranks and classes, in which, through for the eastd have itherted athand reed, that he is wal a bork ummentation: on the metametistical, of disconvances, to the world will highert real strong comes things, fur converses, and prepaintary uman that he apan act to a be delighther ead and "only and will a good. the way parition of philosophing in single this istenate knowledge; peomphedinges nimchs, and the east and the the here es innecess
------ 온도: 1.2 the slowly ascending ranks and classes, in which, through for for our your partnog of imvelign--this can them, and realby" lubed there crimas consimerable dingmedsmalitatif powerom upon curwous, (any degr"bancenels of tle is enjoyede indoubble, ? a mourlour le oncus from stand whithrapting. envy; he tame world .fracking. an thoresises is sulngee, wno hights potheres to because perfecesamentated. in the unplly a lmee one high t sue brie oor noh dombine,
  • 여기서 볼 수 있듯이 낮은 온도는 아주 반복적이고 예상되는 텍스트를 만든다. 하지만 국부적인 구조는 실제와 매우 같다. 특히 모든 단어(단어는 글자의 지역 패턴으로 이루어진다)가 실제 영어 단어이다.
    • 높은 온도에서 생성된 텍스트는 아주 흥미롭고 놀라우며 창의적이기도 하다. 이따금 꽤 그럴싸해 보이는 완전히 새로운 단어를 창조한다.
    • 높은 온도에서는 국부적인 구조가 무너지기 싲가한다. 대부분의 단어가 어느 정도 무작위한 문자열로 보인다.
    • 확실히 이 네트워크에서는 텍스트 생성에 가장 좋은 온돈느 0.5이다.
    • 항상 다양한 샘플링 전략으로 실험해 보어야 한다. 학습된 구조와 무작위성 사이에 균형을 잘 맞추면 흥미로운 것을 만들 수 있다.
  • 더 많은 데이터에서 크고 깊은 모델을 훈련하면 이것보다 훨씬 논리적이고 실제와 같은 텍스트 샘플을 생성할 수 있다. 당연히 우연이 아닌 의미 있는 텍스트가 생성된다고 기대하지 말라.
    • 글자를 연속해서 나열하기 위한 통계 모델에서 데이터를 샘플링한 것뿐이다.
    • 언어는 의사소통의 수단이다. 의사소통이 의미하는 것과 의사소통이 인코딩된 메시지의 통계 구조 사이는 차이가 있다.
    • 이 차이를 검증하기 위해 다음과 같은 사고 실험을 해보자. 컴퓨터가 대부분의 디지털 통신에서 하는 것처럼 사람의 언어가 의사소통을 압축하는데 더 뛰어나다면 어떨까? 언어 의미가 줄지는 않겠지만 고유한 통계 구조가 사라질 것이다. 이는 방금과 같은 언어 모델을 학습하는 것을 불가능하게 만든다.

정리

  • 이전의 토큰이 주어지면 다음 토큰(들)을 예측하는 모델을 훈련하여 시퀀스 데이터를 생성할 수 있다.
  • 텍스트의 경우 이런 모델을 언어 모델이라고 한다. 단어 또는 글자 단위 모두 가능하다.
  • 다음 토큰을 샘플링할 때 모델이 만든 출력에 집중하는 것과 무작위성을 주입하는 것 사이에 균형을 맞추어야 한다.
  • 이를 위해 소프트맥스 온도 개념을 사용한다. 항상 다양한 온도에서 실험해서 적절한 값을 찾아야 한다.

딥드림

  • 딥드림(DeepDream)은 합성곱 신경망이 학습한 표현을 사용하여 예술적으로 이미지를 조작하는 기법이다. 2015년 여름 구글이 카페(Caffe) 딥러닝 라이브러리를 사용하여 구현한 것을 처음 공개했다. (텐서플로가 공개 되기 몇 달 전이다)
    • 딥드림이 생성한 몽환적인 사진은 순식간에 인터넷에 센세이션을 일으켰다. 알고리즘으로 변경된 환상적인 인공물, 새 깃털, 강아지 눈이 가득차 있다.
    • 이 딥드림은 다양한 종류의 강아지와 새가 있는 ImageNet 데이터셋에서 훈련된 컨브넷을 사용했다.

  • 딥드림 알고리즘은 5장에서 소개한 컨브넷을 거꾸로 실행하는 컨브넷 필터 시각화 기법과 거의 동일하다. 컨브넷 상위 층에 있는 특정 필터의 활성화를 극대화하기 위해 컨브넷의 입력에 경사 상승법을 적용했다. 몇 개의 사소한 차이를 빼면 딥드림도 동일한 아이디어를 사용한다.
    • 딥드림에서는 특정 필터가 아니라 전체 층의 활성화를 최대화한다. 한꺼번에 많은 특성을 섞어 시각화 한다.
    • 빈 이미지나 노이즈가 조금 있는 입력이 아니라 이미 가지고 있는 이미지를 사용한다. 그 결과 기존 시각 패턴을 바탕으로 이미지의 요소들을 다소 예술적인 스타일로 왜곡시킨다.
    • 입력 이미지는 시각 품질을 높이기 위해 여러 다른 스케일(옥타브(octave)라고 부른다)로 처리한다.

케라스 딥드림 구현

  • ImageNet에서 훈련한 컨브넷을 가지고 싲가하자. 케라스에는 이렇게 사용할 수 있는 컨브넷이 많다. VGG16, VGG19, Xception, ResNet50 등이다.
    • 이것 중 어느 것을 사용해도 딥드림을 구현할 수 있다. 당연히 어떤 컨브넷을 선택했느냐에 따라 시각화에 영향을 미친다. 각 컨브넷의 구조가 학습한 특성이 다르기 때문이다.
    • 원래 딥드림에서 사용한 컨브넷은 인셉션 모델이다. 실제로 인셉션이 멋진 딥드림 이미지를 잘 만든다. 여기서도 인셉션 V3 모델을 사용하겠다.
from keras.applications import inception_v3
from keras import backend as K

K.set_learning_phase(0)

model = inception_v3.InceptionV3(weights='imagenet', include_top=False)
  • 그다음 손실을 계산한다. 경사 상승법으로 최대화할 값이다.
    • 5장 필터 시각화에서 특정 층의 필터 값을 최대화했다. 여기서는 여러 층에 있는 모든 필터 활성화를 동시에 최대화한다.
    • 특별히 상위 층에 있는 활성화의 L2 노름에 대한 가중치 합을 최대화 하겠다.
    • 정확히 어떤 층들을 선택했는지에 따라 (당연히 최종 손실에 기여한 정도에 따라) 만들어 내는 시각 요소에 큰 영향을 미친다.
    • 어떤 층을 선택할지 파라미터로 손쉽게 바꿀 수 있어야 좋다. 하위 층은 기하학적인 패턴을 만들고 상위 층은 ImageNet에 있는 클래스로 보이는 시각 요소를 만든다. (예컨대 새나 강아지)
    • 먼저 임의로 4개의 층을 선택해 보겠다. 나중에 달느 설정을 다양하게 시도해 보는 것이 좋다.
layer_contributions = {
'mixed2': 0.2,
'mixed3': 3.,
'mixed4': 2.,
'mixed5': 1.5,
}
  • 이제 손실 텐서를 정의하겠다. 앞선 코드에서 선택한 츠으이 활성화에 대한 L2 노름의 가중치 합이다.
layer_dict = dict([(layer.name, layer) for layer in model.layers])

loss = K.variable(0.)

for layer_name in layer_contributions:
coeff = layer_contributions[layer_name]
activation = layer_dict[layer_name].output

scaling = K.prod(K.cast(K.shape(activation), 'float32'))
val = coeff * K.sum(K.square(activation[:, 2:-2, 2:-2, :])) / scaling
loss = loss + val
  • 그 다음 경사 상승법 과정을 준비한다.
dream = model.input

grads = K.gradients(loss, dream)[0]
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)

outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)

def eval_loss_and_grads(x):
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1]
return loss_value, grad_values

def gradient_ascent(x, iterations, step, max_loss=None):
for i in range(iterations):
loss_value, grad_values = eval_loss_and_grads(x)
if max_loss is not None and loss_value > max_loss:
break
print('...', i, '번째 손실:', loss_value)
x += step * grad_values
return x
  • 마지막으로 진짜 딥드림 알고리즘이다. 먼저 이미지를 처리하기 위한 스케일(옥타브라고도 부른다) 리스트를 정의한다. 스케일은 이전 스케일보다 1.4배 크다(40% 증가한다) 작은 이미지로 시작해서 점점 크기를 키운다. (아래 그림 참조)

  • 가장 작은 것에서 가장 큰 스케일까지 연속적인 각 단계에서 정의한 손실이 최대화되도록 경사 상승법을 수행한다. 경사 상승법이 실행된 후 이미지 크기를 40% 증가시킨다.
  • 스케일이 연속적으로 증가시키면서 (점점 뭉개지거나 픽셀 경계가 나타나므로) 이미지 상세를 많이 잃지 않도록 간단한 기교를 사용한다.
    • 스케일을 늘린 후 이미지에 손실된 디테일을 재주입한다. 원본 이미지가 크기를 늘렸을 때 어땠는지 알기 때문에 가능하다.
    • 작은 이미지 크기 S와 큰 이미지 크기 L이 주어지면 크기 L로 변경된 원본 이미지와 크기 S로 변경된 원본 이미지 사이의 차이를 계산한다.
    • 이 차이가 S에서 L로 변경되었을 때 잃어버린 디테일이다.
import scipy
from keras.preprocessing import image

def resize_img(img, size):
img = np.copy(img)
factors = (1, float(size[0]) / img.shape[1], float(size[1]) / img.shape[2], 1)
return scipy.ndimage.zoom(img, factors, order=1)

def save_img(img, fname):
pil_img = deprocess_img(np.copy(img))
image.save_img(fname, pil_img)

def preprocess_image(image_path):
img = image.load_img(image_path)
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = inception_v3.preprocess_input(img)
return img

def deprocess_img(x):
if K.image_data_format() == 'channels_first':
x = x.reshape((3, x.shape[2], x.shape[3]))
x = x.transpose((1, 2, 0))
else:
x = x.reshape((x.shape[1], x.shape[2], 3))

x /= 2.
x += 0.5
x *= 255.
x = np.clip(x, 0, 255).astype('uint8')
return x

import numpy as np

step = 0.01
num_octave = 3
octave_scale = 1.4
iterations = 20
max_loss = 10.
base_image_path = './datasets/deepdream/original_photo_deep_dream.jpg'

img = preprocess_image(base_image_path)

original_shape = img.shape[1:3]
successive_shapes = [original_shape]

for i in range(1, num_octave):
shape = tuple([int(dim/(octave_scale ** i)) for dim in original_shape])
successive_shapes.append(shape)

successive_shapes = successive_shapes[::-1]

original_img = np.copy(img)
shrunk_original_img = resize_img(img, successive_shapes[0])

for shape in successive_shapes:
print('처리할 이미지 크기', shape)
img = resize_img(img, shape)
img = gradient_ascent(img, iterations=iterations, step=step, max_loss=max_loss)
upsacled_shrunk_original_img = resize_img(shrunk_original_img, shape)
same_size_original = resize_img(original_img, shape)
lost_detail = same_size_original - upsacled_shrunk_original_img

img += lost_detail
shrunk_original_img = resize_img(original_img, shape)
save_img(img, fname='dream_at_scale_' + str(shape) + '.png')

save_img(img, fname='./datasets/deepdream/final_dream.png')
  • 샌프란시스코 만과 구글 캠퍼스 사이 작은 언덕에서 찍은 사진을 이용해서 아래와 같은 이미지를 얻었다.

  • 손실로 사용할 층을 바꾸어 보면서 어떤 일이 발생하는지 꼭 시도해 보라. 네트워크의 하위 층은 지역적이고 비교적 덜 추상적인 표현을 가지고 있기 때문에 딥드림 이미지에 기하학적 패턴이 많이 생긴다.
    • 상위 층은 강아지 눈, 새의 깃털처럼 ImageNet에 많이 등장하는 물체를 기반으로 뚜렷이 구분되는 시각 패턴을 만든다.
    • layer_contributions 딕셔너리의 파라미터를 랜덤하게 생성하여 여러 가지 층 조합을 빠르게 탐색할 수 있다.
    • 아래 그림은 층 조합을 다르게 하여 얻은 이미지 결과이다.

정리

  • 딥드림은 네트워크가 학습한 표현을 기반으로 컨브넷을 거꾸로 실행하여 입력 이미지를 생성한다.
  • 재미있는 결과가 만들어지고 때로는 환각제 때문에 시야가 몽롱해진 사람이 만든 이미지 같기도 하다.
  • 이 과정은 이미지 모델이나 컨브넷에 국한되지 않는다. 음성이나 음악에도 적용될 수 있다.

뉴럴 스타일 트랜스퍼

  • 딥드림 이외에 딥러닝을 사용해서 이미지를 변경하는 또 다른 주요 분야는 뉴럴 스타일 트랜스퍼(neural style transfer)이다. 2015년 여름 리온 게티스(Leon Gatys) 등이 소개했다.
    • 뉴럴 스타일 트랜스퍼 알고리즘은 처음 소개된 이후 많이 개선되었고 여러 변종도 생겼다. 스마트폰 사진 앱에도 쓰인다.
  • 뉴럴 스타일 트랜스퍼는 타깃 이미지의 콘텐츠를 보존하면서 참조 이미지의 스탕리을 타깃 이미지에 적용한다. 아래 그림을 참고하라

  • 여기에서 스타일은 질감, 색깔, 이미지에 있는 다양한 크기의 시각 요소를 의미한다. 콘텐츠는 이미지에 있는 고수준의 대형 구조를 말한다.
    • 예컨대 위 그림의 <별이 빛나는 밤>에서 파란색과 노란색의 원을 그리는 듯한 붓질을 하나의 스타일로 생각할 수 있다.
  • 텍스쳐 생성과 밀접하게 연관된 스타일 트랜스퍼의 아이디어는 2015년 뉴럴 스타일 트랜스퍼가 개발되기 이전에 이미 이미지 처리 분야에서 오랜 역사를 가지고 있다.
    • 딥러닝을 기반으로 한 스타일 트랜스퍼 구현은 고전적인 컴퓨터 비전 기법으로 만든 것과는 비견할 수 없는 결과를 제공한다. 창조적인 컴퓨터 비전 애플리케이션 분야에 새로운 르네상스를 열었다.
  • 스타일 트랜스퍼 구현 이면에 있는 핵심 개념은 모든 딥러닝 알고리즘의 핵심과 동일하다. 목표를 표현한 손실 함수를 정의하고 이 손실을 최소화 한다. 
    • 여기서 원하는 것은 다음과 같다. 참조 이미지의 스타일을 적용하면서 원본 이미지의 콘텐츠를 보존하는 것이다.
    • 콘텐츠와 스타일을 수학적으로 정의할 수 있다면 최소화할 손실 함수는 다음과 같을 것이다.
loss = distance(style(reference_image) - style(generated_image)) + distance(content(original_image) - content(generated_image))
  • 여기에서 distance는 L2 노름 같은 노름 함수이다. content 함수는 이미지의 콘텐츠 표현을 계산한다. style 함수는 이미지의 스타일 표현을 계산한다.
    • 이 손실을 최소화하면 style(generated_image)는 style(reference_image)와 가까워지고, content(generated_image)는 content(original_image)와 가까워진다.
    • 앞서 정의한 스타일 트랜스퍼의 목적을 달성할 수 있다.
  • 게티스 등은 심층 합성곱 신경망을 사용하여 style과 content 함수를 수학적으로 정의할 수 있다는 것을 보였다.

콘텐츠 손실

  • 앞서 배웠듯이 네트워크에 있는 하위 층의 활성화는 이미지에 관한 국부적인 정보를 담고 있다. 반면 상위 층의 활성화일수록 점점 전역적이고 추상적인 정보를 담게 된다.
    • 다른 방식으로 생각하면 컨브넷 층의 활성화는 이미지를 달느 크기의 콘텐츠로 분해한다고 볼 수 있다. 컨브넷 상위 층의 표현을 사용하면 전역적이고 추상적인 이미지 콘텐츠를 찾을 것이다.
  • 타깃 이미지와 생성된 이미지를 사전 훈련된 컨브넷에 주입하여 상위 층의 활성화를 계산한다.
    • 이 두 값 사이의 L2 노름이 콘텐츠 손실로 사용하기에 좋다. 상위 층에서 보았을 때 생성된 이미지와 원본 타깃 이미지를 비슷하게 만들 것이다.
    • 컨브넷의 상위 층에서 보는 것이 입력 이미지의 콘텐츠라고 가정하면 이미지의 콘텐츠를 보존하는 방법으로 사용할 수 있다.

스타일 손실

  • 콘텐츠 손실은 하나의 상위 층만 사용한다. 게티스 등이 정의한 스타일 손실은 컨브넷의 여러 층을 사용한다. 하나의 스타일이 아니라 참조 이미지에서 컨브넷이 추출한 모든 크기의 스타일을 잡아야 한다.
    • 게티스 등은 층의 활성화 출력의 그람 행렬(Gram matrix)을 스타일 손실로 사용했다. 그람 행렬은 층의 특성 맵들의 내적(inner product)이다.
    • 내적은 층의 특성 사이에 있는 상관관계를 표현한다고 이해할 수 있다. 이런 특성의 상관관계는 특정 크기의 공간적인 패턴 통계를 잡아낸다.
    • 경험에 비추어 보았을 때 이 층에서 찾은 텍스쳐에 대응된다.
  • 스타일 참조 이미지와 생성된 이미지로 층의 활성화를 계산한다. 스타일 손실은 그 안에 내재된 상관관계를 비슷하게 보존하는 것이 목적이다.
    • 결국 스타일 참조 이미지와 생성된 이미지에서 여러 크기의 텍스쳐거 비슷하게 보이도록 만든다.
  • 요약하면 사전 훈련된 컨브넷을 사용하여 다음 손실들을 정의할 수 있다.
    • 콘텐츠를 보존하기 위해 타깃 콘텐츠 이미지와 생성되 ㄴ이미지 사이에서 상위 층의 활성화를 비슷하게 유지한다. 이 컨브넷은 타깃 이미지와 생성된 이미지에서 동일한 것을 보아야 한다.
    • 스타일을 보존하기 위해 저수준 층과 고수준 층에서 활성화 안에 상관관계를 비슷하게 유지한다. 특성의 상관관계는 텍스쳐를 잡아낸다. 생성된 이미지와 스타일 참조 이미지는 여러 크기의 텍스쳐를 공유할 것이다.
  • 2015년 뉴럴 스타일 트랜스퍼 원본 알고리즘을 케라스로 구현해 보자. 딥드림 구현과 공통점이 많다.

케라스에서 뉴럴 스타일 트랜스퍼 구현하기

  • 뉴럴 스타일 트랜스퍼는 사전 훈련된 컨브넷 중 어떤 것을 사용해서도 구현할 수 있다. 여기서는 게티스 등이 사용한 VGG19 네트워크를 사용하겠다. 
    • VGG19는 VGG16 네트워크의 변종으로 합성곱 층이 3개 더 추가되었다.
  • 일반적인 다음과 같다.
    1. 스타일 참조 이미지, 타깃 이미지, 생성된 이미지를 위해 VGG19의 층 활성화를 동시에 계산하는 네트워크를 설정한다.
    2. 세 이미지에서 계산한 층 활성화를 사용하여 앞서 설명한 손실 함수를 정의한다. 이 손실을 최소화하여 스타일 트랜스퍼를 구현할 것이다.
    3. 손실 함수를 최소화할 경사 하강법 과정을 설정한다.
  • 스타일 참조 이미지와 타깃 이미지의 경로를 정의하는 것부터 시작해 보자. 처리할 이미지는 크기가 비슷하다. 크기가 많이 다르면 스타일 트랜스퍼를 구현하는 것이 더 어렵다.)
from keras.preprocessing.image import load_img, img_to_array, save_img

target_image_path = './datasets/styletransfer/portrait.png'
style_reference_image_path = './datasets/styletransfer/popova.jpg'

width, height = load_img(target_image_path).size
img_height = 400
img_width = int(width * img_height / height)
  • VGG19 컨브넷에 입출력할 이미지의 로드, 전처리, 사후 처리를 위해 유틸리티 함수를 정의한다.
import numpy as np
from keras.applications import vgg19

def preprocess_image(image_path):
img = load_img(image_path, target_size=(img_height, img_width))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = vgg19.preprocess_input(img)
return img

def deprocess_image(x):
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
  • VGG19 네트워크를 설정해 보자. 스타일 참조 이미지, 타깃 이미지 그리고 생성된 이미지가 담긴 플레이스홀더로 이루어진 배치를 입력으로 받는다.
    • 플레이스홀더는 심볼릭 텐서로 넘파이 배열로 밖에서 값을 제공해야 한다.
    • 스타일 참조 이미지와 타깃 이미지는 이미 준비된 데이터이므로 K.constant를 사용하여 정의한다. 
    • 반면 플레이스홀더에 담길 생성된 이미지는 계속 바뀐다.
from keras import backend as K

target_image = K.constant(preprocess_image(target_image_path))
style_reference_image = K.constant(preprocess_image(style_reference_image_path))
combination_image = K.placeholder((1, img_height, img_width, 3))

input_tensor = K.concatenate([target_image, style_reference_image, combination_image], axis=0)
model = vgg19.VGG19(input_tensor=input_tensor, weights='imagenet', include_top=False)
print('모델 로드 완료')
  • 콘텐츠 손신을 정의해보자. VGG19 컨브넷 상위 층은 타깃 이미지와 생성된 이미지를 동일하게 바라보아야 한다.
def content_loss(base, combination):
return K.sum(K.square(combination-base))
  • 다음은 스타일 손실이다. 유틸리티 함수를 사용하여 입력 행렬의 그람 행렬을 계산한다. 이 행렬은 원본 특성 행렬의 상관관계를 기록한 행렬이다.
def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram

def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height * img_width
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size **2))
  • 두 손실에 하나를 더 추가한다. 생성된 이미지의 픽셀을 사용하여 계산하는 총 변위 손실(variation loss)이다.
    • 이는 생성된 이미지가 공간적인 연속성을 가지도록 도와주며 픽셀의 격자 무늬가 과도하게 나타나는 것을 막아준다. 이를 일종의 규제항으로 해석할 수 있다.
def total_variation_loss(x):
a = K.square(x[:, :img_height-1, :img_width-1, :] - x[:, 1:, :img_width - 1, :])
b = K.square(x[:, :img_height-1, :img_width-1, :] - x[:, :img_height - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
  • 최소화할 손실은 이 세 손실의 가중치 평균이다. 콘텐츠 손실은 block5_conv2 층 하나만 사용해서 계산한다. 스타일 손실을 계산하기 위해 하위 층과 상위 층에 걸쳐 여러 층을 사용한다. 그리고 마지막에 총 변위 손실을 추가한다.
  • 사용하는 스타일 참조 이미지와 콘텐츠 이미지에 따라 content_weight 계수(전체 손실에 기여하는 콘텐츠 손실의 정도)를 조정하는 것이 좋다. content_weight가 높으면 생성된 이미지에 타깃 콘텐츠가 더 많이 나타나게 된다.
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])

content_layer = 'block5_conv2'
style_layers = [
'block1_conv1',
'block2_conv1',
'block3_conv1',
'block4_conv1',
'block5_conv1',
]

total_variation_weight = 1e-4
style_weight = 1.
content_weight = 0.025

loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
val = content_weight * content_loss(target_image_features, combination_features)
loss = loss + val

for layer_name in style_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (style_weight / len(style_layers)) * sl

loss += total_variation_weight * total_variation_loss(combination_image)
  • 마지막으로 경사 하강법 단계를 설정한다. 게티스의 원래 논문에서 L-BFGS 알고리즘을 사용하여 최적화를 수행했으므로 여기서도 이를 사용하겠다. 앞선 딥드림 에제와 가장 크게 차이나는 부분이다.
  • L-BFGS 알고리즘은 싸이파이에 구현되어 있는데 두 가지 제약 사항이 있다.
    • 손실 함수 값과 그래디언트 값을 별개의 함수로 전달해야 한다.
    • 이 하무는 3D 이미지 배열이 아니라 1차원 벡터만 처리할 수 있다.
  • 손실 함수 값과 그래디언트 값을 따로 계산하는 것은 비효율적이다. 두 계산 사이에 중복되는 계산이 많기 때문이다. 한번에 계산하는 것보다 거의 2배 가량 느리다.
    • 이를 피하기 위해 손실과 그래디언트 값을 동시에 계산하는 Evaluator란 이름의 파이썬 클래스를 만들겠다. 처음 호출할 때 손실 값을 반환하면서 다음 호출을 위해 그래디언트를 캐싱한다.
grads = K.gradients(loss, combination_image)[0]

fetch_loss_and_grads = K.function([combination_image], [loss, grads])

class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None

def loss(self, x):
assert self.loss_value is None
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value

def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values

evaluator = Evaluator()
  • 마지막으로 싸이파이 L-BFGS 알고리즘을 사용하여 경사 하강법 단계를 수행한다. 알고리즘마다 생성된 이미지를 저장한다. (여기서는 한 번 반복이 경사 하강법 단계 20번이다)
from scipy.optimize import fmin_l_bfgs_b
import time

result_prefix = 'style_transfer_results'
iterations = 20

x = preprocess_image(target_image_path)
x = x.flatten()

for i in range(iterations):
print('반복 횟수:', i)
start_time = time.time()
x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=20)
print('현재 손실 값:', min_val)
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
fname = result_prefix + '_at_iteration_%d.png' % i
save_img(fname, img)
print('저장 이미지', fname)
end_time = time.time()
print('%d 번째 반복 완료: %ds' % (i, end_time - start_time))
  • 다음 그림이 만들어진 이미지이다. 이 기법은 이미지의 텍스쳐를 바꾸거나 텍스쳐를 전이한 것임을 기억하라.
    • 스타일 이미지의 텍스쳐가 두드러지고 비슷한 패턴이 많을 때 잘 작동한다. 또 콘텐츠 타깃을 알아보기 위해 수준 높은 이해가 필요하지 않을 때 잘 작동한다.
    • 일반적으로 인물 사진의 스타일을 다른 인물 사진으로 옮기는 것처럼 아주 추상적인 기교는 만들지 못한다. 이 알고리즘은 AI보다는 고전적인 시그널 처리에 가깝기 때문에 마술 같은 결과는 기대하지 말 것.

  • 스타일 트랜스퍼 알고리즘은 느리지만 간단한 변환을 수행하기 때문에 작고 빠른 컨브넷을 사용하여 학습할 수 있다. 물론 적절한 양의 훈련 데이터가 있어야 한다. 
    • 먼저 고정된 스타일 참조 이미지에 대해 여기서 소개한 방법으로 입력-출력 훈련 샘플을 많이 생성한다. 그 다음 이 스타일 변환을 학습하는 간단한 컨브넷을 훈련하면 스타일 트랜스퍼를 빠르게 수행할 수 있다.
    • 이런 모델을 만들면 이미지가 주어졌을 때 순식간에 스타일을 바꿀 수 있다. 그냥 이 작은 컨브넷을 통과시키면 된다.

정리

  • 스타일 트랜스퍼는 참조 이미지의 스타일을 적용하면서 타깃 이미지의 콘텐츠를 보존하여 새로운 이미지를 만드는 방법이다.
  • 콘텐츠는 컨브넷 상위 층의 활성화에서 얻을 수 있다.
  • 스타일은 여러 컨브넷 층의 활성화 안에 내재된 상관관계에서 얻을 수 있다.
  • 딥러닝에서는 사전 훈련된 컨브넷으로 손실을 정의하고 이 손실을 최적화 하는 과정으로 스타일 트랜스퍼를 구성할 수 있다.
  • 이런 기본 아이디어에서 출발하여 다양한 변종과 개선이 가능하다.

변이형 오토인코더를 사용한 이미지 생성

  • 이미지의 잠재 공간에서 샘플링해서 완전히 새로운 이미지나 기존 이미지를 변형하는 방식이 현재 가장 인기 있고 성공적으로 창조적 AI 애플리케이션을 만들 수 있는 방법이다.
  • 이 분야의 주요 기법인 변이형 오토인코더(Variational AutoEncoders, VAE)와 적대적 생성 네트워크(Generative Adversarial Networks, GAN)의 상세 구현을 다루어 보겠다.
    • 여기서 소개한 기법은 이미지에만 해당되는 것이 아니다. GAN과 VAE를 이용해서 소리, 음악 또는 텍스트의 잠재 공간을 만들 수 있다.
    • 실전에서는 사진에 사용했을 때 가장 재미있는 결과를 만들어내므로 여기서는 이 부분에 집중하겠다.

이미지의 잠재 공간에서 샘플링하기

  • 이미지 생성의 핵심 아이디어는 각 포인트가 실제와 같은 이미지로 매핑될 수 있는 저차원 잠재 공간(당연하게 이는 벡터 공간이다)의 표현을 만드는 것이다.
    • 잠재 공간의 한 포인트를 입력으로 받아 이미지(픽셀의 그리드)를 출력하는 모듈을 (GAN에서는) 생성자(generator) 또는 (VAE 에서는) 디코더(decoder)라고 부른다.
    • 잠재 공간이 만들어지면 여기서 포인트 하나를 특정하여 또는 무작위로 샘플링 할 수 있다.
    • 그 다음 이미지 공간으로 매핑하여 이전에 본 적 없는 이미지를 생성한다. (아래 그림 참조)

  • GAN과 VAE는 이미지의 잠재 공간 표현을 학습하는 2개의 전략이고 각각 나름의 특징을 가진다.
    • VAE는 구조적인 잠재 공간을 학습하는데 뛰어나다. 이 공간에서 특정 방향은 데이터에서 의미 있는 변화의 방향을 인코딩한다.
    • GAN은 매우 실제 같은 이미지를 만든다. 여기에서 만든 잠재 공간은 구조적이거나 연속성이 없을 수 있다.

이미지 변형을 위한 개념 벡터

  • 6장에서 단어 임베딩을 다룰 때 이미 개념 벡터(concept vector)에 대한 아이디어를 얻었다. 이 아이디어와 동일하다. 잠재 공간이나 임베딩 공간이 주어지면 이 공간의 어떤 방향은 원본 데이터의 흥미로운 변화를 인코딩한 축일 수 있다.
    • 예컨대 얼굴 이미지에 대한 잠재 공간에 웃음 벡터가 있을 수 있다. 잠재 공간의 z 포인트가 어떤 얼굴의 임베딩된 표현이라면 잠재 공간의 z + s 포인트는 같은 얼굴이 웃고 있는 표현을 임베딩한 것이다.
    • 이런 벡터를 찾아내면 이미지를 잠재 공간에 투영해서 의미 있는 방향으로 이 표현을 이동한 후 이미지 공간으로 디코딩하여 복원하면 변형된 이미지를 얻을 수 있다.
  • 기본적으로 이미지 공간에서 독립적으로 변화가 일어나는 모든 차원이 개념 벡터이다.
    • 얼굴이라면 안경을 쓰고 벗거나 남자 얼굴을 여자 얼굴로 바꾸는 등의 벡터를 발견할 수 있다.
    • 아래 그림은 뉴질랜드 빅토리아 대학 디자인 스쿨의 톰 화이트가 찾은 개념 벡터인 웃음 벡터의 예이다. 유명 인사의 얼굴 데이터셋(CelebA 데이터셋)에서 훈련한 VAE를 사용했다.

변이형 오토인코더

  • 2013년 12월 킹마(Kingma)와 웰링(Welling) 그리고 2014년 1월 르젠드(Rezende), 무함마드(Mhamed), 위스트라(Wierstra)가 동시에 발견한 변이형 오토인코더는 생성 모델의 한 종류로 개념 벡터를 사용하여 이미지를 변형하는데 아주 적절하다.
    • 오토인코더는 입력을 저차원 잠재 공간으로 인코딩한 후 디코딩하여 복원하는 네트워크이다.
    • 변이형 오토인코더는 딥러닝과 베이즈 추론(Bayesian inferrence)의 아이디어를 혼합한 오토인코더의 최신 버전이다.
  • 고전적인 오토인코더는 이미지를 입력 받아 인코더 모듈을 사용하여 잠재 벡터 공간으로 매핑한다. 그 다음 디코더 모듈을 사용해서 원본 이미지와 동일한 차원으로 복원하여 출력한다. (아래 그림 참조)
    • 오토인코더는 입력 이미지와 동일한 이미지를 타깃 데이터로 사용하여 훈련한다. 다시 말해 오토인코더는 원본 입력을 재구성하는 방법을 학습한다.
    • 코딩(coding)(인코더의 출력)에 여러 제약을 가하면 오토인코더가 더 흥미로운 또는 덜 흥미로운 잠재 공간의 표현을 학습한다.
    • 일반적으로 코딩이 저차원이고 희소(0이 많은) 하도록 제약을 가한다. 이런 경우 인코더는 입력 데이터의 정보를 적은 수의 비트에 압축하기 위해 노력한다.

  • 현실적으로 이런 전통ㅈ거인 오토인코더는 특별히 유용하거나 구조화가 잘 된 잠재 공간을 만들지 못한다. 압축도 아주 뛰어나지 않다. 이런 이유로 시대의 흐름에서 대부분 멀어졌다.
    • VAE는 오토인코더에 약간의 통계 기법을 추가하여 연속적이고 구조적인 잠재 공간을 학습하도록 만들어졌다. 결국 이미지 생성을 위한 강력한 도구로 탈바꿈 되었다.
  • 입력 이미지를 잠재 공간의 고정된 코딩으로 압축하는 대신 VAE는 이미지를 어떤 통계 분포의 파라미터로 변환한다.
    • 이는 입력 이미지가 통계적 과정을 통해 생성되었다고 가정하여 인코딩과 디코딩하는 동안 무작위성이 필요하다는 것을 의미한다.
    • VAE는 평균과 분산 파라미터를 사용하여 이 분포에서 무작위로 하나의 샘플을 추출한다. 이 샘플을 디코딩하여 원본 입력으로 복원한다. (아래 그림 참조)
    • 이런 무작위한 과정은 안정성을 향상하고 잠재 공간 어디서든 의미 있는 표현을 인코딩하도록 만든다. 즉 잠재 공간에서 샘플링한 모든 포인트는 유효한 출력으로 디코딩 된다.

  • 기술적으로 보면 VAE는 다음과 같이 작동한다.
    1. 인코더 모듈이 입력 샘플 input_img를 잠재 공간의 두 파라미터 z_mean과 z_log_var로 변환한다.
    2. 입력 이미지가 생성되었다고 가정한 잠재 공간의 정규 분포에서 포인트 z를 z = z_mean + exp(0.5 * z_log_var) * epsilon처럼 무작위로 샘플링한다. (epsilon은 작은 값을 가진 랜덤 텐서이다)
    3. 디코더 모듈은 잠재 공간의 이 포인트를 원본 입력 이미지로 매핑하여 복원한다.
  • epsilon이 무작위로 만들어지기 때문에 input_img를 인코딩한 잠재 공간의 위치(z_mean)에 가까운 포인트는 input_img와 비슷한 이미지로 디코딩 될 것이다. 이는 잠재 공간을 연속적이고 의미 있는 공간으로 만들어준다.
    • 잠재 공간의 이런 저차원 연속성은 잠재 공간에서 모든 방향이 의미 있는 데이터 변화의 축을 인코딩 하도록 만든다. 결국 잠재 공간은 매우 구조적이고 개념 벡터로 다루기에 적합해진다.
  • VAE의 파라미터는 2개의 손실 함수로 훈련한다. 디코딩된 샘플이 원본 입력과 동일하도록 만드는 재구성 손실(reconstruction loss)과 잠재 공간을 잘 형성하고 훈련 데이터에 과대적합을 줄이는 규제 손실(reularization loss)이다. 
  • 케라스의 VAE 구현을 간단히 살펴보자. 개략적으로 보면 다음과 같다.
z_mean, z_log_var = encoder(input_img)
z = z_mean + exp(0.5 * z_log_var) * epsilon
reconstructed_img = decoder(z)
model = Model(input_img, reconstructed_img)
  • 그 다음 이 모델을 재구성 손실과 규제 손실을 사용하여 훈련한다.
  • 다음 코드는 이미지를 잠재 공간상 확률 분포 파라미터로 매핑하는 인코더 네트워크이다. 입력 이미지 x를 두 벡터 z_mean과 z_log_var로 매핑하는 간단한 컨브넷이다.
import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np

img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2

input_img = keras.Input(shape=img_shape)

x = layers.Conv2D(32, 3, padding='same', activation='relu')(input_img)
x = layers.Conv2D(64, 3, padding='same', activation='relu', strides=(2,2))(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)

shape_before_flattening = K.int_shape(x)

x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)

z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)
  • 다음은 z_mean과 z_log_var를 사용하는 코드이다. 이 두 파라미터가 input_img를 생성한 통계 분포의 파라미터라 가정하고 잠재 공간 포인트 z를 생성한다.
    • 여기에서 (케라스의 백엔드 기능으로 만든) 일련의 코드를 Lambda 층으로 감싼다. 케라스에서는 모든 것이 층이므로 기본 층을 사용하지 않는 코드는 Lambda로 (또는 직접 만든 층으로) 감싸야 한다.
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0., stddev=1.)
return z_mean + K.exp(0.5 * z_log_var) * epsilon

z = layers.Lambda(sampling)([z_mean, z_log_var])
  • 다음 코드는 디코더 구현이다. 벡터 z를 이전 특성 맵 차원으로 크기를 바꾸고 몇 개의 합성곱 층을 사용하여 최종 출력 이미지를 만든다. 최종 이미지는 원본 input_img와 차원이 같다.
decoder_input = layers.Input(K.int_shape(z)[1:])

x = layers.Dense(np.prod(shape_before_flattening[1:]), activation='relu')(decoder_input)
x = layers.Reshape(shape_before_flattening[1:])(x)
x = layers.Conv2DTranspose(32, 3, padding='same', activation='relu', strides=(2,2))(x)
x = layers.Conv2D(1, 3, padding='same', activation='sigmoid')(x)

decoder = Model(decoder_input, x)

z_decoded = decoder(z)
  • 일반적인 샘플 기준의 함수인 loss(y_true, y_pred) 형태는 VAE의 이중 손실에 맞지 않다. add_loss 내장 메서드를 사용하는 층을 직접 만들어 임의의 손실을 정의하겠다.
class CustomVariationalLayer(keras.layers.Layer):
def vae_loss(self, x, z_decoded):
x = K.flatten(x)
z_decoded = K.flatten(z_decoded)
xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)
kl_loss = -5e-4 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
return K.mean(xent_loss + kl_loss)

def call(self, inputs):
x = inputs[0]
z_decoded = inputs[1]
loss = self.vae_loss(x, z_decoded)
self.add_loss(loss, inputs=inputs)
return x

y = CustomVariationalLayer()([input_img, z_decoded])
  • 이제 모델 객체를 만들고 훈련할 준비가 되었다. 층에서 손실을 직접 다루기 때문에 compile 메서드에서 손실을 지정하지 않는다. (loss=None) 그 결과 훈련하는 동안 타깃 데이터를 전달하지 않아도 된다. (다음 코드와 같이 모델의 fit 메서드에 x_train만 전달한다.)
from keras.datasets import mnist

vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
vae.summary()

(x_train, _), (x_test, y_test) = mnist.load_data()

x_train = x_train.astype('float32') / 255.
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.astype('float32') / 255.

x_test = x_test.reshape(x_test.shape + (1,))

vae.fit(x=x_train, y=None, shuffle=True, epochs=10, batch_size=batch_size, validation_data=(x_test, None))
  • MNIST 데이터셋으로 모델의 훈련을 마치면 디코더 네트워크를 사용하여 잠재 공간의 임의의 벡터를 이미지로 변환할 수 있다.
import matplotlib.pyplot as plt
from scipy.stats import norm

n = 15
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))

for i, yi in enumerate(grid_x):
for j, xi in enumerate(grid_y):
z_sample = np.array([[xi, yi]])
z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2)
x_decoded = decoder.predict(z_sample, batch_size=batch_size)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i+1) * digit_size, j*digit_size: (j+1) * digit_size] = digit

plt.figure(figsize=(10,10))
plt.imshow(figure, cmap='Greys_r')
plt.show()
  • 샘플링된 숫자의 그리드(아래 그림 참고)는 다른 숫자 클래스 사이에서 완벽하게 연속된 분포를 보여준다. 잠재 공간의 한 경로를 따라서 한 숫자가 다른 숫자로 자연스럽게 바뀐다.
    • 이 공간의 특정 방향은 어떤 의미를 가진다. 예컨대 ‘6으로 가는 방향’, ‘9로 가는 방향’ 등이다.

정리

  • 딥러닝으로 이미지 데이터셋에 대한 통계 정보를 담은 잠재 공간을 학습하여 이미지를 생성할 수 있다. 잠재 공간에서 포인트를 샘플링하고 디코딩하면 이전에 본 적 없는 이미지를 생성한다. 이를 수행하는 주요 방법은 VAE와 GAN이다.
  • VAE는 매우 구조적이고 연속적인 잠재 공간의 표현을 만든다. 이런 이유로 잠재 공간 안에서 일어나는 모든 종류의 이미지 변형 작업에 잘 맞는다.
    • 다른 얼굴로 바꾸기, 찌푸린 얼굴에서 웃는 얼굴로 변형하기 등이다. 잠재 공간을 가로질러 이미지가 변환하는 잠재 공간 기반의 애니메이션에도 잘 맞는다.
    • 시작 이미지가 연속적으로 다른 이미지로 부드럽게 바뀌는 것을 볼 수 있다.
  • GAN은 실제 같은 단일 이미지를 생성할 수 있지만 구조적이고 연속적인 잠재 공간을 만들지 못한다.

적대적 생성 신경망 소개

  • 2014년 굿펠로우 드잉 소개한 적대적 생성 신경망(GAN)은 VAE와 다른 방법으로 이미지의 잠재 공간을 학습한다. GAN은 생성된 이미지가 실제 이미지와 통계적으로 거의 구분되지 않도록 강제하여 아주 실제 같은 합성 이미지를 생성한다.
  • GAN을 직관적으로 이해하는 방법은 가짜 피카소 그림을 만드는 위조범을 생각하는 것이다.
    • 위조범은 처음에 형편없이 위조하는데, 진짜 피카소 그림과 위조품을 섞어서 그림 판매상에게 보여주면 판매상은 각 그림이 진짜인지 평가하고 피드백을 준다.
    • 위조범은 새로운 위조품을 준비하고 시간이 지남에 따라 피카소의 스타일을 모방하는데 점점 더 능숙해지고, 판매상은 위조품을 구분하는데 점점 더 전문가가 된다. 결국 아주 훌륭한 피카소 위조품을 만들어 낼 것이다.
  • 위조범 네트워크와 전문가 네트워크가 바로 GAN이다. 두 네트워크는 상대를 이기기 위해 훈련한다. GAN 네트워크 2개는 다음과 같다.
    • 생성자 네트워크(generator network): 랜덤 벡터(잠재 공간의 무작위한 포인트)를 입력으로 받아 이를 합성된 이미지로 디코딩한다.
    • 판별자 네트워크(discriminator network)(또는 상대 네트워크): 이미지 (실제 또는 합성 이미지)를 입력으로 받아 훈련 세트에서 온 이미지인지, 생성자 네트워크가 만든 이미지인지 판별한다.
  • 생성자 네트워크는 판별자 네트워크를 속이도록 훈련한다. 훈련이 계속될수록 점점 더 실제와 같은 이미지를 생성하게 된다.
    • 실제 이미지와 구분할 수 없는 인공적인 이미지를 만들어 판별자 네트워크가 두 이미지를 동일하게 보도록 만든다.
    • 한편 판별자 네트워크는 생성된 이미지가 실제인지 판별하는 기준을 설정하면서 생성자의 능력 향상에 적응해 간다.
    • 훈련이 끝나면 생성자는 입력 공간에 있는 어떤 포인트를 그럴듯한 이미지로 변환한다.
    • VAE와 달리 이 잠재 공간은 의미 있는 구조를 보장하지 않는다. 특히 이 공간은 연속적이지 않다.

  • 놀랍게도 GAN은 최적화의 최솟값이 고정되지 않은 시스템이다. 이 책에서 다루는 어떤 훈련 설정과도 다르다. 보통 경사 하강법은 고정된 손실 공간에서 언덕을 내려오는 방법이다.
    • GAN에서는 언덕을 내려오는 매 단계가 조금씩 전체 공간을 바꾼다. 최적화 과정이 최솟값을 찾는게 아니라 두 힘 간의 평형점을 찾는 다이나믹 시스템이다.
  • 이런 이유로 GAN은 훈련하기 어렵기로 유명하다. GAN을 만들려면 모델 구조와 훈련 파라미터를 주의 깊게 많이 조정해야 한다.

GAN 구현 방법

  • 이 절에서는 케라스에서 가장 기본적인 형태의 GAN을 구현하는 방법을 설명하겠다. GAN은 수준 높은 기술이기 때문에 기술적인 내용을 깊이 설명하는 것은 이 책 범위를 벗어난다.
    • 구체적인 구현은 심층 합성곱 GAN(DCGAN)이다. 생성자와 판별자가 심층 컨브넷이다. 특히 생성자에서 이미지 업샘플링을 위해 Conv2DTranspose 층을 사용한다.
  • CIFAR10 데이터셋의 이미지로 GAN을 훈련하겠다. 이 데이터셋은 32×32 크기의 RGB 이미지 5만개로 이루어져 있고 10개의 클래스를 갖는다 (클래스마다 5,000개의 이미지가 있다) 문제를 간단하게 만들기 위해 “frog” 클래스의 이미지만 사용하겠다.
  • GAN 구조는 다음과 같다.
    1. generator 네트워크는(latent_dim,) 크기의 벡터를 (32, 32, 3) 크기의 이미지로 매핑한다.
    2. discriminator 네트워크는 (32, 32, 3) 크기의 이미지가 진짜일 확률을 추정하여 이진 값으로 매핑한다.
    3. 생성자와 판별자를 연결하는 gan 네트워크를 만든다. gen(x) = discriminator(generator(x))이다. 이 gan 네트워크는 잠재 공간의 벡터를 판별자의 평가로 매핑한다. 즉, 판별자는 생성자가 잠재 공간의 벡터를 디코딩하는 것이 얼마나 현실적인지 평가한다.
    4. 진짜/ 가짜 레이블과 함꼐 진짜 이미지와 가짜 이미지 샘플을 사용하여 판별자를 훈련한다. 일반적인 이미지 분류 모델을 훈련하는 것과 동일하다.
    5. 생성자를 훈련하려면 gan 모델의 손실에 대한 생성자 가중치의 그래디언트를 사용한다. 이 말은 매 단계마다 생성자에 의해 디코딩된 이미지를 판별자가 “진짜”로 분류하도록 만드는 방향으로 생성자의 가중치를 이동한다는 뜻이다. 다른 말로 하면 판별자를 속이도록 생성자를 훈련한다.

훈련 방법

  • GAN을 훈련하고 튜닝하는 과정은 어렵기로 유명하다. 알아 두어야 할 몇 가지 유용한 기법이 있다.
    • 딥러닝의 대부분이 그렇듯이 이는 과학보다 연금술에 가깝다. 이런 기법들은 이론에 바탕을 둔 지침이 아니고 경험을 통해 발견된 것이다.
    • 실제 일어난 현상을 직관적으로 이해하는 수준에서 검증되었다. 모든 문제에 반드시 적용해야 하는 것은 아니지만 경험상 잘 작동한다고 알려져 있다.
  • 다음은 이 절에서 GAN 생성자와 판별자를 구현하는데 사용할 몇 가지 기법이다. 이 목록이 GAN에 관련된 전체 팁은 아니다. GAN 논문들에서 더 많은 방법을 볼 수 있다.
    • 생성자의 마지막 활성화로 다른 종류의 모델에서 널리 사용하는 sigmoid 대신 tanh 함수를 사용한다.
    • 균등 분포가 아니고 정규 분포(가우시안 분포)를 사용하여 잠재 공간에서 포인트를 샘플링한다.
    • 무작위성은 모델을 견고하게 만든다. GAN 훈련은 동적 평형을 만들기 때문에 여러 방식으로 갇힐 가능성이 높다. 훈련하는 동안 무작위성을 주입하면 이를 방지하는데 도움이 된다. 무작위성은 두 가지 방법으로 주입한다. 판별자에 드롭아웃을 사용하거나 판별자를 위해 레이블에 랜덤 노이즈를 추가한다.
    • 희소한 그래디언트는 GAN 훈련을 방해할 수 있다. 딥러닝에서 희소는 종종 바람직한 현상이지만 GAN에서는 그렇지 않다. 그래디언트를 희소하게 만들 수 있는 것은 최대 풀링 연산과 ReLU 활성화 두 가지이다.
      • 최대 풀링 대신 스트라이드 합성곱을 사용하여 다운샘플링을 하는 것이 좋다.
      • 또 ReLU 활성화 대신 LeakyReLU 층을 사용하라. ReLU와 비슷하지만 음수의 활성화 값을 조금 허용하기 때문에 희소가 조금 완화된다.
    • 생성자에서 픽셀 공간을 균일하게 다루지 못하여 생성된 이미지에서 체스판 모양이 종종 나타난다. (아래 그림 참조) 이를 해결하기 위해 생성자와 판별자에서 스트라이드 Conv2DTranspose나 Conv2D를 사용할 때 스트라이드 크기로 나누어질 수 있는 커널 크기를 사용한다.

생성자

  • 먼저 벡터(훈련하는 동안 잠재 공간에서 무작위로 샘플링된다)를 후보 이미지로 변환하는 generator 모델을 만들어 보자.
    • GAN에서 발생하는 많은 문제 중 하나는 생성자가 노이즈 같은 이미지를 생성하는데서 멈추는 것이다.
    • 판별자와 생성자 양쪽에 모두 드롭아웃을 사용하는 것이 해결 방법이 될 수 있다.
import keras
from keras import layers
import numpy as np

latent_dim = 32
height = 32
width = 32
channels = 3

generator_input = keras.Input(shape=(latent_dim,))

x = layers.Dense(128 * 16 * 16)(generator_input)
x = layers.LeakyReLU()(x)
x = layers.Reshape((16, 16, 128))(x)

x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2DTranspose(256, 4, strides=2, padding='same')(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(channels, 7, activation='tanh', padding='same')(x)

generator = keras.models.Model(generator_input, x)
generator.summary()

판별자

  • 다음은 후보 이미지(진짜 또는 가짜)를 입력으로 받고 2개의 클래스로 분류하는 discriminator 모델을 만들겠다. 이 클래슨느 ‘생성된 이미지’ 또는 ‘훈련 세트에서 온 진짜 이미지’이다.
discriminator_input = layers.Input(shape=(height, width, channels))

x = layers.Conv2D(128, 3)(discriminator_input)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Flatten()(x)

x = layers.Dropout(0.4)(x)

x = layers.Dense(1, activation='sigmoid')(x)

discriminator = keras.models.Model(discriminator_input, x)
discriminator.summary()

discriminator_optimizer = keras.optimizers.RMSprop(lr=0.0008, clipvalue=1.0, decay=1e-8)
discriminator.compile(optimizer=discriminator_optimizer, loss='binary_crossentropy')

적대적 네트워크

  • 마지막으로 생성자와 판별자를 연결하여 GAN을 설정한다. 훈련할 때 생성자가 판별자를 속이는 능력이 커지도록 학습한다.
    • 이 모델은 잠재 공간의 포인트를 “진짜” 또는 “가짜”의 분류 결정으로 변환한다. 훈련에 사용되는 타깃 레이블은 항상 ‘진짜 이미지’이다. 
    • gan을 훈련하는 것은 discriminator가 가짜 이미지를 보았을 때 진짜라고 예측하도록 만들기 위해 generator의 가중치를 업데이트 하는 것이다.
    • 훈련하는 동안 판별자를 동결(학습되지 않도록)하는 것이 아주 중요하다. gan을 훈련할 때 가중치가 업데이트 되지 않는다. 판별자의 가중치가 훈련하는 동안 업데이트 되면 판별자는 항상 “진짜”를 예측하도록 훈련된다. 이는 우리가 원하는 바가 아니다.
discriminator.tainable = False

gan_input = keras.Input(shape=(latent_dim,))
gan_output = discriminator(generator(gan_input))
gan = keras.models.Model(gan_input, gan_output)

gan_optimizer = keras.optimizers.RMSprop(lr=0.0004, clipvalue=1.0, decay=1e-8)
gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy')

DCGAN 훈련 방법

  • 이제 훈련을 싲가한다. 훈련 반복의 내용을 요약 정리해 보자. 매 반복마다 다음을 수행한다.
    1. 잠재 공간에서 무작위로 포인트를 뽑는다(랜덤 노이즈)
    2. 이 랜덤 노이즈를 사용하여 generator에서 이미지를 생성한다.
    3. 생성된 이미지와 진짜 이미지를 섞는다.
    4. 진짜와 가짜가 섞인 이미지와 이에 대응하는 타깃을 사용하여 discriminator를 훈련한다. 타깃은 “진짜(실제 이미지일 경우)” 또는 “가짜(생성된 이미지일 경우)”이다.
    5. 잠재 공간에서 무작위로 새로운 포인트를 뽑는다.
    6. 이 랜덤 벡터를 사용하여 gan을 훈련한다. 모든 타깃은 “진짜”로 설정한다. 판별자가 생성된 이미지를 모두 “진짜 이미지”라고 예측하도록 생성자의 가중치를 업데이트 한다(gan 안에서 판별자는 동결되기 때문에 생성자만 업데이트한다) 결국 생성자는 판별자를 속이도록 훈련한다.
import os
from keras.preprocessing import image

(x_train, y_train), (_, _) = keras.datasets.cifar10.load_data()

x_train = x_train[y_train.flatten() == 6]

x_train = x_train.reshape((x_train.shape[0],) + (height, width, channels)).astype('float32') / 255.

iterations = 10000
batch_size = 20
save_dir = './datasets/gan_images/'

if not os.path.exists(save_dir):
os.mkdir(save_dir)

start = 0

for step in range(iterations):
random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))
generated_images = generator.predict(random_latent_vectors)

stop = start + batch_size
real_images = x_train[start: stop]
combined_images = np.concatenate([generated_images, real_images])

labels = np.concatenate([np.ones((batch_size, 1)), np.zeros((batch_size, 1))])
labels += 0.05 * np.random.random(labels.shape)

d_loss = discriminator.train_on_batch(combined_images, labels)

random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))
misleading_targets = np.zeros((batch_size, 1))
a_loss = gan.train_on_batch(random_latent_vectors, misleading_targets)

start += batch_size

if start > len(x_train) - batch_size:
start = 0

if step % 100 == 0:
gan.save_weights('gan.h5')

print('판별자 손실:', d_loss)
print('적대적 손실:', a_loss)

img = image.array_to_img(generated_images[0] * 255., scale=False)
img.save(os.path.join(save_dir, 'generated_frog' + str(step) + '.png'))

img = image.array_to_img(real_images[0] * 255., scale=False)
img.save(os.path.join(save_dir, 'real_frog' + str(step) + '.png'))
  • 훈련할 때 적대적 손실이 크게 증가하는 것을 볼 수 있을지 모른다. 반면 판별자의 손실은 0으로 향한다. 결국 판별자가 생성자를 압도할 수 있다. 이런 경우에는 판별자의 학습률을 낮추고 판별자의 드롭아웃 비율을 높여서 시도해 보라.

정리

  • GAN은 생성자 네트워크와 판별자 네트워크가 연결되어 구성된다. 판별자는 생성자의 출력과 훈련 데이터셋에서 가져온 진짜 이미지를 구분하도록 훈련된다. 생성자는 판별자를 속이도록 훈련된다. 놀랍게도 생성자는 훈련 세트의 이미지를 직접 보지 않는다. 데이터에 관한 정보는 판별자에서 얻는다.
  • GAN은 훈련하기 어렵다. GAN 훈련이 고정된 손실 공간에서 수행하는 단순한 경사 하강법 과정이 아니라 동적 과정이기 때문이다. GAN을 올바르게 훈련하려면 경험적으로 찾은 여러 기교를 사용하고 많은 튜닝을 해야 한다.
  • GAN은 매우 실제 같은 이미지를 만들 수 있다. 잠재 공간의 개념 벡터를 사용하여 이미지를 변형하는 등 실용적인 특정 애플리케이션에는 잘 맞지 않는다.

요약

  • 딥러닝을 사용한 창의적인 애플리케이션에서 심층 네트워크는 기존 콘텐츠에 설명을 다는 것을 넘어서 직접 콘텐츠를 생산하기 시작했다. 다음 내용들을 배웠다.
    • 한 번에 하나의 타임스텝씩 시퀀스 데이터를 생성하는 방법. 텍스트 생성이나 음표 하나씩 음악을 생성하거나 어떤 시계열 데이터를 생성하는 곳에 적용할 수 있다.
    • 딥드림의 작동 원리. 컨브넷 층 활성화를 최대화하기 위해 입력 공간에 경사 상승법을 적용한다.
    • 스타일 트랜스퍼 적용 방법. 콘텐츠 이미지와 스타일 이미지가 연결되어 재미있는 결과를 만든다.
    • GAN과 VAE가 무엇인지와 이를 사용하여 새로운 이미지를 만드는 방법. 잠재 공간의 개념 벡터를 사용하여 이미지를 변형하는 방법

케라스 창시자에게 배우는 딥러닝/ 딥러닝을 위한 고급 도구

Sequential 모델을 넘어서: 케라스의 함수형 API

  • 지금까지 소개한 모든 신경망은 Sequential 모델을 사용하여 만들었다. Sequential 모델은 네트워크 입력과 출력이 하나라고 가정한다. 이 모델은 층을 차례로 쌓아 구성한다.

  • 많은 경우에 이런 가정이 적절하다. 하지만 이런 가정이 맞지 않는 경우도 많은데 일부 네트워크는 개별 입력이 여러 개 필요하거나 출력이 여러 개 필요하다.
    • 층을 차례로 쌓지 않고 층 사이를 연결하여 그래프처럼 만드는 네트워크도 있다.
  • 예컨대 어떤 작업은 다양한 종류의 입력이 필요하다. 다양한 입력 소스(source)에서 전달된 데이터를 다른 종류의 신경망 층을 사용하여 처리하고 합친다.
  • 중고 의류의 시장 가격을 예측하는 모델을 상상해 보자. 이 모델은 사용자가 제공한 메타데이터(브랜드, 연도 등), 사용자가 제공한 텍스트 설명, 제품 사진을 입력으로 사용한다.
    • 메타데이터는 원-핫 인코딩으로 바꾸고 완전 연결 네트워크를 사용하여 가격을 예측할 수 있다. 텍스트 설명은 RNN이나 1D 컨브넷을 사용할 수 있고, 사진 이미지는 2D 컨브넷을 사용할 수 있다.
    • 만일 3가지 모델을 동시에 사용하려면 각각의 모델을 따로 훈련하고 각 예측을 가중 평균(weighted average) 하면 된다.
    • 각 모델에서 추출한 정보가 중복된다면 이 방식은 최적이 아닐 것이다. 가능한 모든 종류의 입력 데이터를 동시에 사용해서 정확한 하나의 모델을 학습하는 것이 더 나은 방법이다. 이 모델은 3가지 입려 가지(branch)가 필요하다.

  • 이와 비슷하게 어떤 작업은 입력 데이터에서 여러 개의 타깃 속성을 예측해야 한다. 예컨대 소설이나 짧은 글이 있을 때 자동으로 장르별로 분류하고, 또 글을 쓴 대략의 시대를 예측하려고 한다.
    • 2개의 모델을 따로 훈련할 수도 있지만 이 속성들은 통계적으로 독립적이지 않기 때문에 동시에 장르와 시대를 함께 예측하도록 학습해야 더 좋은 모델을 만들 수 있다.
    • 이 모델은 2개의 출력 또는 머리(head)를 가진다. 장르와 시대 사이의 상관관계 때문에 소설 시대를 알면 장르의 공간에서 정확하고 풍부한 표현을 학습하는데 도움이 된다. 그 반대도 마찬가지다.

  • 더불어 최근에 개발된 많은 신경망 구조는 선형적이지 않은 네트워크 토폴로지(topology)가 필요하다. 비순환 유향 그래프 같은 네트워크 구조이다.
    • 예컨대 (구글의 세게디 등이 개발한) 인셉션 모듈을 사용하는 인셉션 계열의 네트워크들이다. 이 모듈에서 입력은 나란히 놓인 여러 개의 합성곱 층을 거쳐 하나의 텐서로 출력이 합쳐진다.

  • 최근에는 모델에 잔차 연결을 추가하는 경향도 있다. (마이크로소프트의 허(He) 등이 개발한) ResNet 계열의 네트워크들이 이런 방식을 사용하기 시작했다.
    • 잔차 연결은 하위 층에 출력 텐서를 상위 층의 출력 텐서에 더해서 아래 층의 표현이 네트워크 위쪽으로 흘러갈 수 있도록 한다.
    • 하위 층에서 학습된 정보가 데이터 처리하는 과정에서 손실되는 것을 방지한다. 이렇게 그래프 구조를 띈 네트워크 종류가 많다.

  • 여러 경우에 다중 입력 모델, 다중 출력 모델, 그래프 구조를 띈 모델이 필요하지만 케라스의 Sequential 클래스를 사용해서는 만들지 못한다.
    • 케라스에는 훨씬 더 일반적이고 유연한 다른 방법인 함수형 API가 있다.

함수형 API 소개

  • 함수형 API(functional API)에서는 직접 텐서들의 입출력을 다룬다. 함수처럼 층을 사용하여 텐서를 입력받고 출력한다. (그래서 함수형 API라고 부른다)
from keras import Input, layers

input_tensor = Input(shape=(32,))
dense = layers.Dense(32, activation='relu')
output_tensor = dense(input_tensor)
  • 간단한 예를 통해 Sequential 모델과 함수형 API로 만든 동일한 모델을 나란히 비교해 보자.
from keras.models import Sequential, Model

# Sequential 모델
seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))

# 함수형 API로 만든 모델
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)

model = Model(input_tensor, output_tensor)
model.summary()
---
Layer (type) Output Shape Param # ================================================================= input_2 (InputLayer) (None, 64) 0 _________________________________________________________________ dense_5 (Dense) (None, 32) 2080 _________________________________________________________________ dense_6 (Dense) (None, 32) 1056 _________________________________________________________________ dense_7 (Dense) (None, 10) 330 ================================================================= Total params: 3,466 Trainable params: 3,466 Non-trainable params: 0
  • 입력 텐서와 출력 텐서만 가지고 Model 객체를 만드는 부분이 마술처럼 보인다. 
    • 무대 뒤에서 케라스는 input_tensor에서 output_tensor로 가는데 필요한 모든 층을 추출한다. 그다음 이들을 모아 그래프 데이터 구조인 Model을 만든다.
    • 물론 input_tensor를 반복 변환해서 output_tensor를 만들 수 있어야 한다. 관련되지 않은 입력과 출력으로 모델을 만들면 RuntimeError가 발생한다.
  •  Model 객체를 사용한 컴파일, 훈련, 평가 API는 Sequential 클래스와 같다.
import numpy as np

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

x_train = np.random.random((1000, 64))
y_train = np.random.random((1000, 10))

model.fit(x_train, y_train, epochs=10, batch_size=128)

score = model.evaluate(x_train, y_train)

다중 입력 모델

  • 함수형 API는 다중 입력 모델을 만드는데 사용할 수 있다. 일반적으로 이런 모델은 서로 다른 입력 가지를 합치기 위해 여러 텐서를 연결할 수 있는 층을 사용한다. 텐서를 더하거나 이어 붙이는 식이다.
    • 이와 관련된 케라스 함수는 keras.layers.add, keras.layers.concatenate 등이다.
  • 아주 간단한 다중 입력 모델을 살펴보자. 질문-응답(QA) 모델이다. 전형적인 질문-응답 모델은 2개의 입력을 가진다. 
    • 하나느 자연어 질문이고 다른 하나는 답변에 필요한 정보가 담겨 있는 텍스트(예컨대 뉴스 기사)이다. 그러면 모델은 답을 출력해야 한다. 
    • 가장 간단한 구조는 미리 정의한 어휘 사전에서 소프트맥스 함수를 통해 한 단어로 된 답을 출력하는 것이다.

  • 다음은 함수형 API를 사용하여 이런 모델을 만드는 예이다.
    • 텍스트와 질문을 벡터로 인코딩하여 독립된 입력 2개를 정의한다. 그 다음 이 벡터를 연결하고 그 위에 소프트맥스 분류기를 추가한다.
from keras.models import Model
from keras import layers, Input

text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500

text_input = Input(shape=(None,), dtype='int32', name='text')
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)
encoded_text = layers.LSTM(32)(embedded_text)

question_input = Input(shape=(None,), dtype='int32', name='question')
embedded_question = layers.Embedding(question_vocabulary_size, 32)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)

concatenated = layers.concatenate([encoded_text, encoded_question], axis=-1)

answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)

model = Model([text_input, question_input], answer)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
  • 그렇다면 입력이 2개인 모델은 어떻게 훈련할까? 두 가지 방식이 있다. 넘파이 배열의 리스트를 주입하거나 입력 이름과 넘파일 배열로 이루어진 딕셔너리를 모델의 입력으로 주입할 수 있다.
    • 물론 두 번째 방식은 입력 이름을 설정했을 때 사용할 수 있다.

import numpy as np
from keras.utils import to_categorical

num_samples = 1000
max_length = 100

text = np.random.randint(1, text_vocabulary_size, size=(num_samples, max_length))
question = np.random.randint(1, question_vocabulary_size, size=(num_samples, max_length))
answers = np.random.randint(0, answer_vocabulary_size, size=num_samples)
answers = to_categorical(answers)

# 리스트를 이용하여 학습
model.fit([text, question], answers, epochs=10, batch_size=128)

# 딕셔너리를 이용하여 학습 - 입력 이름을 지정했을 때만 사용할 수 있다.
model.fit({'text': text, 'question':question}, answers, epochs=10, batch_size=128)

다중 출력 모델

  • 같은 식으로 함수형 API를 사용하여 다중 출력(또는 다중 머리) 모델을 만들 수 있다. 간단한 예는 데이터에 있는 여러 속성을 동시에 예측하는 네트워크이다. 예컨대 소셜 미디어에서 익명의 사용자의 포스트를 입력으로 받아 그 사람의 나이, 성별, 소득 수준을 예측한다.
vocabulary_size = 50000
num_income_groups = 10

posts_input = Input(shape=(None,), dtype='int32', name='posts')
embedded_posts = layers.Embedding(vocabulary_size, 256)(posts_input)
x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D()(x)
x = layers.Dense(128, activation='relu')(x)

age_prediction = layers.Dense(1, name='age')(x)
income_prediction = layers.Dense(num_income_groups, activation='softmax', name='income')(x)
gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)

model = Model(posts_input, [age_prediction, income_prediction, gender_prediction])

  • 이런 모델을 훈련하려면 네트워크 출력마다 다른 손실 함수를 지정해야 한다.
    • 예컨대 나이 예측은 스칼라 회귀 문제지만 성별 예측은 이진 클래스 문제라 훈련 방식이 다르다.
    • 경사 하강법은 하나의 스칼라 값을 최소화하기 때문에 모델을 훈련하려면 이 손실들을 하나의 값으로 합쳐야 한다.
    • 손실 값을 합치는 가장 간단한 방법은 모두 더하는 것이다. 케라스에서는 compile 메서드에 리스트나 딕셔너리를 사용하여 출력마다 다른 손실을 지정할 수 있다. 계산된 손실 값은 전체 손실 하나로 더해지고 훈련 과정을 통해 최소화 된다.
# 리스트를 이용한 방식
model.compile(optimizer='rmsprop', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'])

# 딕셔너리를 이용한 방식 - 출력 층에 이름을 지정했을 때만 사용할 수 있다.
model.compile(optimizer='rmsprop', loss={'age':'mse', 'income':'categorical_crossentropy', 'gender':'binary_crossentropy'})
  • 손실 값이 많이 불균형하면 모델이 개별 손실이 가장 큰 작업에 치우쳐 표현을 최적화할 것이다. 그 결과 다른 작업들은 손해를 입는다.
    • 이를 해결하기 위해 손실 값이 최종 손실에 기여하는 수준을 지정할 수 있다. 특히 손실 값의 스케일이 다를 때 유용하다.
    • 예컨대 다음과 같이 가정해 보자. 나이 회귀 작업에 사용되는 평균 제곱 오차(MSE) 손실은 일반적으로 3-5 사이의 값을 갖는 반면 성별 분류 작업에 사용되는 크로스엔트로피 손실은 0.1 정도로 낮다.
    • 이런 환경에서 손실에 균형을 맞추려면 크로스엔트로피 손실에 가중치 10을 주고 MSE 손실에 가중치 0.25를 줄 수 있다.
# 리스트를 이용한 방식
model.compile(optimizer='rmsprop', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'], loss_weights=[0.25, 1., 10.])

# 딕셔너리를 이용한 방식
model.compile(optimizer='rmsprop', loss={'age':'mse', 'income':'categorical_crossentropy', 'gender':'binary_crossentropy'}, loss_weights={'age':0.25, 'income':1., 'gender':10.})
  • 다중 입력 모델과 마찬가지로 넘파이 배열의 리스트나 딕셔너리를 모델에 전달하여 훈련한다.
# 리스트를 이용한 방식
model.fit(posts_input, [age_prediction, income_prediction, gender_prediction], epochs=10, batch_size=64)

# 딕셔너리를 이용한 방식
model.fit(posts_input, {'age':age_prediction, 'income':income_prediction, 'gender':gender_prediction}, epochs=10, batch_size=64)

층으로 구성된 비순향 유향 그래프

  • 함수형 API를 사용하면 다중 입력이나 다중 출력 모델 뿐만 아니라 내부 토폴로지가 복잡한 네트워크도 만들 수 있다. 케라스의 신경망은 층으로 구성된 어떤 비순환 유향 그래프(directed acyclic graph)도 만들 수 있다.
    • 비순환이라는 부분이 중요한데 이 그래프는 원형을 띌 수 없다. 텐서 x가 자기 자신을 출력하는 층의 입력이 될 수 없다. 만들 수 있는 루프는 순환 층의 내부에 있는 것 뿐이다.
  • 그래프로 구현된 몇 개의 신경망 컴포넌트가 널리 사용된다. 가장 유명한 2개는 인셉션 모듈과 잔차 연결이다. 케라스에서 이 2개의 컴포넌트를 어떻게 구현하는지 살펴보자.

인셉션 모듈

  • 인셉션(Inception)은 합성곱 신경망에서 인기 있는 네트워크 구조이다. 일찍이 네트워크 안의 네트워크(network-in-network) 구조에서 영감을 받아 2013-2014년에 크리스티안 세게디(Christian Szegedy)와 그의 구글 동료들이 만들었다.
    • 나란히 분리된 가지를 따라 모듈을 쌓아 독립된 작은 네트워크처럼 구성한다.
    • 가장 기본적인 인셉션 모듈 형태는 3-4개의 가지를 가진다. 1 x 1 합성곱으로 시작해서 3 x 3 합성곱이 뒤따르고 마지막에 전체 출력 특성이 합쳐진다.
    • 이런 구성은 네트워크가 따로따로 공간 특성과 채널 방향의 특성을 학습하도록 돕는다. 한번에 학습하는 것보다 효과가 더 높다.
    • 더 복잡한 인셉션 모듈은 풀링 연산, 여러 가지 합성곱 사이즈(예컨대 일부 가지에서는 3 x 3 대신 5 x 5 를 사용한다), 공간 합성곱이 없는 가지(1 x 1 합성곱만 존재)로 구성될 수 있다.
    • 인셉션 V3에 있는 이런 모듈의 예가 아래 그림과 같다.

  • Note) 1 x 1 합성곱의 목적
    • 합성곱은 입력 텐서에서 타일 주변의 패치를 추출하고 각 패치에 동일한 연산을 수행한다. 이 경우 추출된 패치가 하나의 타일로 이루어졌을 때이다.
    • 이 합성곱 연산은 모든 타일 벡터를 하나의 Dense 층에 통과시키는 것과 동일하다. 즉, 입력 텐서의 채널 정보를 혼합한 특성을 계산한다. 공간 방향으로는 정보를 섞지 않는다. (한 번에 하나의 타일만 처리하기 때문) 
    • 이런 1 x 1 합성곱(또는 점별 합성곱(pointwise convolution)은 인셉션 모듈의 특징이다. 채널 방향의 특성 학습과 공간 방향의 특성 학습을 분리하는데 도움을 준다. 
    • 채널이 공간 방향으로 상관관계가 크고 채널 간에는 독립적이라고 가정하면 납득할 만한 전략이다.
  • 다음은 함수형 API를 이용해 위 그림의 모듈을 구현하는 예이다. 이 예에서 입력 x는 4D 텐서라고 가정한다.
from keras import layers

branch_a = layers.Conv2D(128, 1, activation='relu', strides=2)(x)

branch_b = layers.Conv2D(128, 1, activation='relu')(x)
branch_b = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_b)

branch_c = layers.AveragePooling2D(3, strides=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu')(branch_c)

branch_d = layers.Conv2D(128, 1, activation='relu')(x)
branch_d = layers.Conv2D(128, 3, activation='relu')(branch_d)
branch_d = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_d)

output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=1)
  • 인셉션 V3 전체 구조는 케라스의 keras.applications.inception_v3.InceptionV3에 준비되어 있으며, ImageNet 데이터셋에서 사전 훈련된 가중치를 포함하고 있다.
    • 이와 아주 비슷한 모델인 엑셉션(Xception)도 케라스의 애플리케이션 모듈에 포함되어 있다. 엑셉션은 극단적인 인셉션(extreme inception)을 말한다.
    • 이 합성곱 구조는 인셉션에서 일부 영감을 얻었다. 채널 방향의 학습과 공간 방향의 학습을 극단적으로 분리한다는 아이디어에 착안하여 인셉션 모듈을 깊이별 분리 합성곱으로 바꾼다.
    • 이 합성곱은 깊이별 합성곱(depthwise convolution) (각 입력 채널에 따로따로 적용되는 공간 방향 합성곱) 다음에 점별 합성곱(1 x 1 합성곱)이 뒤따른다.
    • 인셉션 모듈의 극한 형태로 공간 특성과 채널 방향 특성을 완전히 분리한다.
    • 엑셉션은 인셉션 V3와 거의 동일한 개수의 모델 파라미터를 가지지만 실행 속도가 더 빠르고 ImageNet이나 다른 대규모 데이터셋에서 정확도가 더 높다. 이는 모델 파라미터를 더 효율적으로 사용하기 때문이다.

잔차 연결

  • 잔차 연결(residual connection)은 엑셉션을 포함하여 2015년 이후 등장한 많은 네트워크 구조에 있는 그래프 형태의 네트워크 컴포넌트이다. 2015년 후반 ILSVRC ImageNet 경연 대회 우승 팀인 마이크로소프트의 허 등이 소개했다.
    • 대규모 딥러닝 모델에서 흔히 나타나는 두 가지 문제인 그래디언트 소실과 표현 병목(representational bottleneck)을 해결했다. 일반적으로 10개 층 이상을 가진 모델에 잔차 연결을 추가하면 도움이 된다.
  • 잔차 연결은 하위 층의 출력을 상위 층의 입력으로 사용한다. 순서대로 놓인 네트워크를 질러가는 연결이 만들어진다.
    • 하위 층의 출력이 상위 층의 활성화 출력에 연결되는 것이 아니고 더 해진다. 따라서 두 출력의 크기가 동일해야 한다.
    • 크기가 다르면 선형 변환을 사용하여 하위 층의 활성화 출력을 목표 크기로 변환한다 (예컨대 활성화 함수를 사용하지 않는 Dense 층이나 합성곱의 특성 맵이라면 활성화 함수가 없는 1 x 1 합성곱)
  • 다음 코드는 케라스에서 특성 맵의 크기가 같을 때 원본을 그대로 사용하는 잔차 연결을 구현한 예이다. 여기서는 입력 x가 4D 텐서라고 가정한다.
from keras import layers

x = ...

y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)

y = layers.add([y, x])
  • 다음은 특성 맵의 크기가 다를 때 선형 변환을 사용하여 잔차 연결을 구현한 예이다. (여기서도 x가 4D 텐서라고 가정한다)
from keras import layers

x = ...

y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.MaxPooling2D(2, strides=2)(y)

residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)

y = layers.add([y, residual])
  • Note) 딥러닝의 표현 병목
    • Sequential 모델에서 표현 학습을 위한 층은 다른 층위에 연달아 놓인다. 다시 말해 층은 이전 층의 활성화 출력 정보만 사용한다. 어떤 층이 너무 작으면 (예컨대 특성이 너무 저차원이면) 이 활성화 출력에 얼마나 많은 정보를 채울 수 있느냐에 모델 성능이 좌우된다.
    • 이 개념을 신호 처리에 비유할 수 있다. 일련의 연산으로 구성된 오디어 처리 파이프라인이 있다고 가정해 보자. 각 단계는 이전 연산의 출력으로 입력을 사용한다. 한 연산이 신호를 저주파 영역(예컨대 0-15kHz)으로 잘라냈다면 이후 연산이 줄어든 주파수를 복원할 수 없을 것이다. 손실된 정보는 영구 불변이다. 하위 층의 정보를 다시 주입하는 잔차 연결은 딥러닝 모델에서 이 이슈를 어느 정도 해결해 준다.
  • Note) 딥러닝의 그래디언트 소실 문제
    • 심층 신경망을 훈련하는데 사용되는 핵심 알고리즘인 역전파는 출력 손실에서 얻은 피드백 신호를 하위 층에 전파한다. 피드백 신호가 깊이 쌓인 층을 통과하여 전파되면 신호가 아주 작아지거나 완전히 사라질 수도 있다. 이렇게 되면 네트워크가 훈련되지 않는데, 이를 그래디언트 소실(vanishing gradient) 문제라고 한다.
    • 이 문제는 심층 신경망과 긴 시퀀스를 처리하는 순환 신경망에서 모두 나타난다. 양쪽 모두 피드백 신호가 일련의 긴 연산을 통과하여 전파되기 때문이다. 순환 신경망에서 LSTM 층이 이 문제를 해결하기 위해 사용하는 방식을 보았다. 이동 트랙이 주요 처리 트랙에 나란히 정보를 실어 날랐다. 잔차 연결은 피드포워드 심층 신경망에서 비슷한 역할을 하는데, 좀 더 단순하다. 주 네트워크 층에 나란히 단순한 선형 정보를 실어 나른다. 이는 그래디언트가 깊게 쌓은 층을 통과하여 전파하도록 도와준다.

층 가중치 공유

  • 함수형 API의 중요한 또 하나의 기능은 층 객체를 여러 번 재사용할 수 있다는 것이다. 층 객체를 두 번 호출하면 새로운 층 객체를 만들지 않고 각 호출에 동일한 가중치를 재사용한다.
    • 이런 기능 때문에 공유 가지를 가진 모델을 만들 수 있다. 이런 가지는 같은 가중치를 공유하고 같은 연산을 수행한다. 다시 말해 같은 표현을 공유하고 이런 표현을 다른 입력에서 함께 학습한다.
  • 예컨대 두 문장 사이의 의미가 비슷한지 측정하는 모델을 가정해 보자. 이 모델은 2개의 입력(비교할 2개의 문장)을 받고 0과 1사이의 점수를 출력한다.
    • 0은 관련 없는 문장, 1은 두 문장이 동일하거나 재구성된 것.
    • 이런 모델은 대화 시스템(dialog system)에서 자연어 질의에 대한 중복 제거를 포함하여 많은 애플리케이션에 유용하게 사용될 수 있다.
  • 이런 문제에서 두 입력 시퀀스가 바뀔 수 있다. 의미가 비슷하다는 것은 대칭적인 관계이기 때문이다.
    • A에서 B에 대한 유사도는 B에서 A에 대한 유사도와 같다. 이런 이유로 각 입력 문장을 처리하는 2개의 독립된 모델을 학습하는 것은 이치에 맞지 않다.
    • 대신 하나의 LSTM 층으로 양쪽을 모두 처리하는 것이 좋다. 이 LSTM 층의 표현(가중치)은 두 입력에 대해 함께 학습된다.
    • 이를 샴 LSTM(Siamese LSTM) 모델 또는 공유 LSTM 이라고 부른다.
  • 다음은 케라스의 함수형 API로 공유 층(재사용 층)을 사용하는 모델을 구현하는 예이다)
from keras import layers
from keras import Input
from keras.models import Model

lstm = layers.LSTM(32)
left_input = Input(shape=(None, 128))
left_output = lstm(left_input)

right_input = Input(shape=(None, 128))
right_output = lstm(right_input)

merged = layers.concatenate([left_output, right_output], axis=-1)
predictions = layers.Dense(1, activation='sigmoid')(merged)

model = Model([left_input, right_input], predictions)
model.fit([left_data, right_data], targets)
  • 당연하게 층 객체는 한 번 이상 사용할 수 있다. 같은 가중치를 재사용하면서 얼마든지 여러 번 호출 할 수 있다.

층과 모델

  • 함수형 API에서는 모델을 층처럼 생각할 수 있다. 모델을 ‘커다란 층’으로 생각해도 된다. Sequential 클래스와 Model 클래스에서 모두 동일하다. 
    • 이 말은 입력 텐서로 모델을 호출해서 출력 텐서를 얻을 수 있다는 뜻이다.
y = model(x)
  • 모델에서 입력 텐서와 출력 텐서가 여러 개이면 텐서의 리스트로 호출한다.
y1, y2 = model([x1, x2])
  • 모델 객체를 호출할 때 모델의 가중치가 재사용된다. 층 객체를 호출할 때와 정확히 같다. 층 객체나 모델 객체나 객체를 호출하는 것은 항상 그 객체가 가진 학습된 표현을 재사용한다.
  • 모델 객체를 재사용하는 간단한 실전 예는 듀얼 카메라에서 입력을 받는 비전 모델이다.
    • 두 카메라가 몇 센티미터(1인치) 간격을 두고 나란히 있다. 이런 모델을 깊이를 감지할 수 있다. 이는 많은 애플리케이션에서 유용한 기능이다.
    • 왼쪽 카메라와 오른쪽 카메라에서 시각적 특징을 추출하여 합치기 위해 2개의 독립된 모델을 사용할 필요가 없다. 두 입력에 저수준 처리 과정이 공유될 수 있다. 다시 말해 가중치가 같고 동일한 표현을 공유하는 층을 사용한다. 
    • 다음은 케라스에서 샴 비전 모델(공유 합성곱 기반 층)을 구현하는 예이다.
from keras import layers, applications, Input

xception_base = applications.Xception(weights=None, include_top=False)

left_input = Input(shape=(250, 250, 3))
right_input = Input(shape=(250, 250, 3))

left_features = xception_base(left_input)
right_features = xception_base(right_input)

merged_features = layers.concatenate([left_features, right_features], axis=-1)

정리

  • 차례대로 층을 쌓는 것 이상이 필요할 때는 Sequential API를 사용하지 않는다.
  • 함수형 API를 사용하여 다중 입력, 다중 출력, 복잡한 네트워크 토폴로지를 갖는 케라스 모델을 만드는 방법
  • 다른 네트워크 가지에서 같은 층이나 모델 객체를 여러 번 호출하여 가중치를 재사용하는 방법

케라스 콜백과 텐서보드를 사용한 딥러닝 모델 검사와 모니터링

  • 대규모 데이터셋에서 model.fit()이나 model.fit_generator()로 수십 번 에포크를 실행하는 것은 종이 비행기를 날리는 것과 비슷하다. 일단 손을 떠나면 종이 비행기 경로와 착률 지점을 제어할 방법이 없다.
    • 나쁜 결과를 피하려면 종이 비행기 대신 다른 것을 사용하는 것이 좋다. 
    • 드론은 주변 환경을 감지한 데이터를 조작부에 전달하여 현재 상태를 바탕으로 자동으로 운전한다. 
    • 앞으로 소개할 기법은 model.fit() 호출을 종이 비행기에서 스스로 판단하고 동적으로 결정하는 똑똑한 자동 드론으로 바꾸어 줄 것이다.

콜백을 사용하여 모델의 훈련 과정 제어하기

  • 모델을 훈련할 때 미리 예상할 수 없는 것들이 많다. 특히 최적의 검증 손실을 얻기 위해 얼마나 많은 에포크가 필요한지 알지 못한다. 
    • 지금까지는 적절한 훈련 에포크를 알아내기 위해 첫 번째 실행에서 과대적합이 시작될 때까지 충분한 에포크로 훈련하고 최적의 에포크 횟수를 찾아 처음부터 새로 훈련을 시작했다. 당연히 이런 방식은 낭비가 많다.
  • 더 좋은 처리 방법은 검증 손실이 더 향상되지 않을 때 훈련을 멈추는 것이다. 이는 케라스의 콜백을 사용하여 구현할 수 있다.
    • 콜백은 모델의 fit() 메서드가 호출될 때 전달되는 객체로, 훈련하는 동안 모델은 여러 지점에서 콜백을 호출한다.
    • 콜백은 모델의 상태와 성능에 대한 모든 정보에 접근하고 훈련 중지, 모델 저장, 가중치 적재 또는 모델 상태 변경 등을 처리할 수 있다.
  • 다음은 콜백을 사용하는 몇 가지 예이다.
    • 모델 체크포인트 저장: 훈련하는 동안 어떤 지점에서 모델의 현재 가중치를 저장한다.
    • 조기 종료(early stopping): 검증 손실이 더 향상되지 않을 때 훈련을 중지한다.
    • 훈련하는 동안 하이퍼파라미터 값을 동적으로 조정: 옵티마이저의 학습률 같은 경우이다.
    • 훈련과 검증 지표를 로그에 기록하거나 모델이 학습한 표현이 업데이트될 때마다 시각화: 앞서 보았던 케라스의 진행 표시줄(progress bar)이 하나의 콜백이다.
  • keras.callback 모듈은 많은 내장 콜백을 포함하고 있다. 다음은 대표적인 예이다.
keras.callbacks.ModelCheckpoint
keras.callbacks.EarlyStopping
keras.callbacks.LearningRateScheduler
keras.callbacks.ReduceLROnPlateau
keras.callbacks.CSVLogger

ModelCheckpoint와 EarlyStopping 콜백

  • EarlyStopping 콜백을 사용하면 정해진 에포크 동안 모니터링 지표가 향상되지 않을 때 훈련을 중지할 수 있다.
    • 예컨대 과대적합이 시작되자마자 훈련을 중지할 수 있다. 따라서 에포크 횟수를 줄여 다시 모델을 훈련할 필요가 없다.
    • 일반적으로 이 콜백은 훈련하는 동안 모델을 계속 저장해주는 ModelCheckpoint와 함께 사용한다. (선택적으로 지금까지 가장 좋은 모델만 저장할 수 있다. 에포크 마지막에 다다랐을 때 최고 성능을 달성한 모델이다.
import keras

callbacks_list = [
keras.callbacks.EarlyStopping(
monitor='val_acc',
patience=1,
),
keras.callbacks.ModelCheckpoint(
filepath='my_model.h5',
monitor='val_loss',
save_best_only=True,
)
]

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

model.fit(x, y, epochs=10, batch_size=32, callbacks=callbacks_list, validation_data=(x_val, y_val))

RecudeLROnPlateau 콜백

  • 이 콜백을 사용하면 검증 손실이 향상되지 않을 때 학습률을 작게 할 수 있다. 손실 곡선이 평탄할 때 학습률을 작게 하거나 크게 하면 훈련 도중 지역 최솟값에서 효과적으로 빠져나올 수 있다.
callback_list = [
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.1,
patience=10,
)
]

model.fit(x, y, epochs=10, batch_size=32, callbacks=callbacks_list, validation_data=(x_val, y_val))

자신만의 콜백 만들기

  • 내장 콜백에서 제공하지 않는 특수한 행도잉 훈련 도중 필요하다면 자신만의 콜백을 만들 수 있다. 콜백은 keras.callbacks.Callback 클래스를 상속받아 구현하면 된다.
    • 그 다음 훈련하는 동안 호출될 여러 지점을 나타내기 위해 약속된 다음 메서드를 구현한다.
on_epoch_begin -- 각 에포크가 시작될 때 호출한다.
on_epoch_end -- 각 에포크가 끝날 때 호출한다.

on_batch_begin -- 각 배치 처리가 시작될 때 호출한다.
on_batch_end -- 각 배치 처리가 끝날 때 호출한다.

on_train_begin -- 훈련이 시작될 때 호출한다.
on_train_end -- 훈련이 끝날 때 호출한다.
  • 이 메서드들은 모두 logs 매개변수화 함께 호출된다. 이 매개변수에는 이전 배치, 에포크에 대한 훈련과 검증 측정값이 담겨 있는 딕셔너리가 전달된다. 또 콜백은 다음 속성을 참조할 수 있다.
    • self.model: 콜백을 호출하는 모델 객체
    • self.validation_data: fit() 메서드에 전달될 검증 데이터
  • 다음은 매 에포크의 끝에서 검증 세트의 첫 번째 샘플로 모델에 있는 모든 츠으이 활성화 출력을 계산하여 (넘파이 배열로) 디스크에 저장하는 자작 콜백의 예이다.
import keras
import numpy as np

class ActivationLogger(keras.callbacks.Callback):
def set_model(self, model):
self.model = model
layer_outputs = [layer.output for layer in model.layers]
self.activations_model = keras.models.Model(model.input, layer_outputs)

def on_epoch_end(self, epoch, logs=None):
if self.validation_data is None:
raise RuntimeError('Requires validation_data')

validation_sample = self.validation_data[0][0:1]
activations = self.activations_model.predict(validation_sample)
f = open('activation_at_epoch_' + str(epoch) + '.npz', 'wb')
np.savez(f, activations)
f.close()

텐서보드 소개: 텐서플로의 시각화 프레임워크

  • 텐서보드의 핵심 목적은 훈련 모델의 내부에서 일어나는 모든 것을 시각적으로 모니터링 할 수 있도록 돕는 것이다. 모델의 최종 손실 외에 더 많은 정보를 모니터링하면 모델 작동에 대한 명확한 그림을 그릴 수 있다. 결국 모델을 더 빠르게 개선할 수 있다.
  • 텐서 보드는 다음과 같은 기능을 제공하고 모든 브라우저에서 작동한다.
    • 훈련하는 동안 측정 지표를 시각적으로 모니터링 한다.
    • 모델 구조를 시각화 한다.
    • 활성화 출력과 그래디언트의 히스토그램을 그린다.
    • 3D로 임베딩을 표현한다.

  • 6장에서 했던 IMDB 감성 분석 문제를 이용해서 예시를 들어보자.
import keras
from keras import layers
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 2000
max_len = 500
batch_size = 32

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)

x_train = sequence.pad_sequences(x_train, maxlen=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)

model = keras.models.Sequential()
model.add(layers.Embedding(max_features, 128, input_length=max_len, name='embed'))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
  • 텐서보드를 시작하기 전에 로그 파일이 저장될 디렉터리를 만들어야 한다.
  • 로그 디렉터리를 만들었다면 TensorBoard 콜백 객체와 함꼐 훈련을 시작해 보자. 이 콜백은 지정된 디스크 위치에 로그 이벤트를 기록할 것이다.
callbacks = [
keras.callbacks.TensorBoard(
log_dir='my_log_dir',
histogram_freq = 1,
embeddings_freq = 1,
)
]

history = model.fit(x_train, y_train, epochs=20, batch_size=128, validation_split=0.2, callbacks=callbacks)
  • 이제 명령행에서 콜백이 사용하는 로그 디렉터리를 지정하여 텐서보드 서버를 실행한다.
    • 텐서플로를 설치했다면 텐서보드 유틸리티는 자동으로 설치되었을 것이다.
tensorboard --logdir=my_log_dir
  • 그 다음 브라우저에서 http://localhost:6006 주소에 접속하면 모델의 훈련 결과를 확인할 수 있다.
    • 훈련 지표와 검증 지표를 실시간 그래프로 볼 수 있고 Histograms 탭에서 층의 활성화 출력을 히스토그램 그래프로 볼 수 있다.

  • Embeddings 탭에서 입력 어휘 사전에 있는 2,000개의 임베딩 위치와 공간상 관계를 조사할 수 있다.
    • 이 임베딩은 첫 번째 Embedding 층이 학습한 것이다. 임베딩 공간이 128차원이기 때문에 텐서보드는 우리가 선택한 차원 축소 알고리즘을 사용하여 자동으로 2D 또는 3D로 축소한다.
    • 주성분 분석(Principle Component Analysis, PCA) 또는 t-SNE(t-distributed Stochastic Neighbor Embedding)이다.
    • 아래 그림에서 점 구름(point cloud) 그래프에서 2개의 클러스터가 뚜렷히 구분된다. 긍정적인 의미의 단어와 부정적인 의미의 단어이다.
    • 이 그래프는 주어진 문제에 완전히 특화된 모델의 목적에 맞게 임베딩이 훈련되었다는 것을 확실히 보여준다. 이것이 사전 훈련된 보편적인 단어 임베딩이 드물게 사용되는 이유이다.

  • Graphs 탭은 케라스 모델을 구성하는 저수준 텐서플로 연산의 그래프를 시각화한다. 여기서 볼 수 있듯 예상보다 훨씬 복잡하다. 
    • 케라스에서 만든 모델은 간단해 보이지만 무대 위에서는 상당히 복잡한 그래프 구조가 만들어진다. 그래프의 많은 부분은 경사하강법과 관련이 있다.
    • 보이는 것과 만들어야 하는 것 사이의 이런 복잡도 차이 때문에 모든 것을 밑바닥부터 텐서플로를 사용하여 만드는 대신 케라스를 사용하여 모델을 만든다.
    • 케라스는 작업 흐름을 극적으로 간단하게 만들어준다.

  • 텐서플로 연산의 그래프 외에 카리스의 keras.utils.plot_model 유틸리티는 모델의 층 그래프를 깔끔하게 그려 주는 기능을 제공한다. 이를 사용하려면 pydot과 pydot-ng, graphviz 라이브러리가 필요하다.
from keras.utils import plot_model

plot_model(model, to_file='model.png')
  • 위 코드는 아래 그림의 png 이미지를 만든다.

  • 층 그래프에 크기 정보를 추가할 수 있다.
    • 다음은 plot_model 함수와 show_shapes 매개변수를 사용하여 모델의 그래프를 그린다.
from keras.utils import plot_model

plot_model(model, show_shapes=True, to_file='model.png')

정리

  • 케라스 콜백은 훈련하는 동안 모델을 모니터링하고 모델 상태를 바탕으로 자동으로 작업을 수행하는 손쉬운 방법이다.
  • 텐서플로를 사용하면 텐서보드를 이용하여 모델 상황을 브라우저에서 시각화할 수 있다. 케라스 모델에서는 TensorBoard 콜백을 통해 사용한다.

모델의 성능을 최대로 끌어올리기

고급 구조 패턴

  • 이전 절에서 중요한 디자인 패턴 중 하나인 잔차 연결에 대해 소개했는데, 이 외에도 꼭 알아야 할 디자인 패턴이 2개 더 있다. 정규화와 깊이별 분리 합성곱이 그것이다. 이 패턴은 특히 고성능 심층 컨브넷을 만들 때 유용하다. 하지만 보통 다른 종류의 구조에서도 많이 등장한다.

배치 정규화

  • 정규화(normalization)는 머신 러닝 모델에 주입되는 샘플들을 균일하게 만드는 광범위한 방법이다. 이 방법은 모델이 학습하고 새로운 데이터에 잘 일반화 되도록 돕는다.
    • 데이터 정규화의 일반적인 형태는 이미 이 책에서 여러 번 나왔다. 데이터에서 평균을 빼서 데이터를 원점에 맞추고 표준 편차로 나누어 데이터의 분산을 1로 만든다.
    • 데이터가 정규 분포를 따른다고 가정하고 이 분포를 원점에 맞추고 분산이 1이 되도록 조정한 것이다.
normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)
  • 이전 예제는 모델에 데이터를 주입하기 전에 정규화 했다. 하지만 데이터 정규화는 네트워크에서 일어나는 모든 변환 후에도 고려되어야 한다.
    • Dense나 Conv2D 층에 들어가는 데이터의 평균이 0이고 분산이 1이더라도 출력되는 데이터가 동일한 분포를 가질 것이라고 기대하기는 어렵다.
  • 배치 정규화(batch normalization)은 2015년 아이오페와 세게디가 제안한 층의 한 종류이다. (케라스는 BatchNormalization 클래스로 제공한다)
    • 훈련하는 동안 평균과 분산이 바뀌더라도 이에 적응하여 데이터를 정규화한다. 훈련 과정에 사용된 배치 데이터의 평균과 분산에 대한 지수 이동 평균(exponential moving average)을 내부에 유지한다.
    • 배치 정규화의 주요 효과는 잔차 연결과 매우 흡사하게 그래디언트의 전파를 도와주는 것이다. 결국 더 깊은 네트워크를 구성할 수 있다.
    • 매우 깊은 네트워크라면 여러 개의 BatchNormalization 층을 포함해야 훈련할 수 있다.
    • 예컨대 케라스에 포함된 고급 컨브넷 구조는 BatchNormalization 층을 많이 사용한다. 여기에는 ResNet50, 인셉션 V3, 엑셉션 등이 있다.
  • BatchNormalization 층은 일반적으로 합성곱이나 완전 연결 층 다음에 사용한다.
conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization())

dense_model.add(layers.Dense(32, 3, activation='relu'))
dense_model.add(layers.BatchNormalization())
  • BatchNormalization 클래스에는 정규화할 특성 축을 지정하는 axis 매개변수가 있다. 이 매개변수의 기본값은 입력 텐서의 마지막 축을 나타내는 -1이다.
    • data_format을 “channels_last”로 하여 Dense, Conv1D, RNN, Conv2D 층을 사용할 때는 맞는 값이다.
    • 하지만 data_format을 “channels_first”로 사용하는 경우에는 특성 축이 1이다. 이때는 BatchNormalization의 axis 매개변수는 1이 되어야 한다.
  • Note) 배치 재정규화
    • 배치 정규화의 최근 발전은 2017년 아이오페가 소개한 배치 재정규화(batch renormalization)이다. 이 방법은 추가적인 비용을 들이지 않고 배치 정규화보다 이득이 많다. 이 책을 쓸 시점에는 배치 정규화를 대체할 가능성이 높다.
    • 좀 더 최근에는 클람바우어(Klambauer) 등이 자기 정규화 신경망(self-normalizing neural networks)을 발표했다. 특정 활성화 함수(selu)와 초기화 방법(lecun_normal)을 사용하여 Dense 층의 출력을 정규화 한다. 이 방법이 흥미롭기는 하지만 지금은 완전 연결 네트워크에만 제한되어 있어 널리 사용되지 못하고 있다.

깊이별 분리 합성곱

  • Conv2D를 대체하면서 더 가볍고(훈련할 모델 파라미터가 더 적고) 더 빨라(부동 소수 연산이 더 적고) 모델의 성능을 몇 퍼센트 포인트 높일 수 있는 층이 있다면 어떨까? 이것이 깊이별 분리 합성곱(depthwise separable convolution) 층이 하는 일이다. (SeparableConv2D)
    • 이 층은 입력 채널별로 따로따로 공간 방향의 합성곱을 수행한다. 그 다음 아래 그림과 같이 점별 합성곱 (1×1 합성곱)을 통해 출력 채널을 합친다.
    • 이는 공간 특성의 학습과 채널 방향 특성의 학습을 분리하는 효과를 낸다. 입력에서 공간상 위치는 상관관계가 크지만 채널별로는 매우 독립적이라고 가정한다면 타당하다.
    • 이 방법은 모델 파라미터와 연산의 수를 크게 줄여주기 때문에 작고 더 빠른 모델을 만든다. 합성곱을 통해 더 효율적으로 학습하기 때문에 적은 데이터로도 더 좋은 표현을 학습하고 결국 성능이 더 높은 모델을 만든다.

  • 이 장점은 제한된 데이터로 작은 모델을 처음부터 훈련시킬 때 특히 더 중요하다. 다음은 작은 데이터셋에서 이미지 분류 문제(소프트맥스 분류)를 위한 가벼운 깊이별 분리 컨브넷을 만드는 예이다.
from keras.models import Sequential, Model
from keras import layers

height = 64
width = 64
channels = 3
num_classes = 10

model = Sequential()
model.add(layers.SeparableConv2D(32, 3, activation='relu', input_shape=(height, width, channels,)))
model.add(layers.SeparableConv2D(32, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())

model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

하이퍼파라미터 최적화

  • 딥러닝 모델을 만들 때 무작위로 보이는 결정을 많이 해야 한다.
    • 얼마나 많은 층을 쌓아야 할지, 얼마나 많은 유닛이나 필터를 두어야 할지, relu 활성화 함수를 사용해야 할지 아니면 다른 함수를 사용해야 할지, 어떤 층 뒤에 BatchNormalization을 사용해야 할지, 드롭아웃은 얼마나 해야 할지 등이다.
    • 이런 구조에 관련된 파라미터를 역전파로 훈련되는 모델 파라미터와 구분하여 하이퍼파라미터(hyperparameter)라고 부른다.
  • 실제로 경험 많은 머신 러닝 엔지니어와 연구자는 하이퍼파라미터에 따라 작동하는 것과 작동하지 않는 것에 대한 직관을 가지고 있다. 하이퍼파라미터 튜닝에 관한 기술을 갖고 있는 셈이다.
    • 하지만 공식적인 규칙은 없다. 주어진 문제에서 최대의 성능을 얻고 싶다면 사람이 임의로 선택한 결정에 만족해서는 안된다.
    • 직감이 좋다라고 하더라도 첫 번째 선택은 항상 최적치가 아니다. 옵션을 수정하고 모델을 반복적으로 다시 훈련하여 선택 사항을 개선해야 한다. 이것이 머신 러닝 엔지니어와 연구자들이 대부분의 시간을 쓰는 일이다.
    • 하지만 하루 종일 하이퍼파라미터를 수정하는 것은 사람이 할 일이 아니고, 기계에 위임하는 것이 더 낫다.
  • 가능한 결정 공간을 자동적, 조직적, 규칙적 방법으로 탐색해야 한다. 가능성 있는 구조를 탐색해서 실제 가장 높은 성능을 내는 것을 찾아야 한다. 하이퍼파라미터 자동 최적화가 이에 관련된 분야이다. 이는 하나의 연구 분야이며 중요한 분야이다.
  • 전형적인 하이퍼파라미터 최적화 과정은 다음과 같다.
    • 일련의 하이퍼파라미터를 (자동으로) 선택한다.
    • 선택된 하이퍼파라미터로 모델을 만든다.
    • 훈련 데이터에 학습하고 검증 데이터에서 최종 성능을 측정한다.
    • 다음으로 시도할 하이퍼파라미터를 (자동으로) 선택한다.
    • 이 과정을 반복한다.
    • 마지막으로 테스트 데이터에서 성능을 측정한다.
  • 주어진 하이퍼파라미터에서 얻은 검증 성능을 사용하여 다음 번에 시도할 하이퍼파라미터를 선택하는 알고리즘이 이 과정의 핵심이다.
    • 여러 가지 기법을 사용할 수 있는데, 베이지안 최적화(bayesian optimization), 유전 알고리즘(genetic algorithms), 간단한 랜덤 탐색(random search) 등이다.
  • 모델의 가중치를 훈련하는 것은 비교적 쉽다. 미니 배치 데이터에 대한 손실 함수 값을 계산하고 역전파 알고리즘을 사용하여 올바른 방향으로 가중치를 이동하면 된다. 반면 하이퍼파라미터를 업데이트하는 것은 매우 어려운 일이다. 다음을 생각해 보자.
    • 피드백 신호를 계산하는 것은 매우 비용이 많이 든다(이 하이퍼파라미터가 성능이 높은 모델을 만들어 낼까?) 새로운 모델을 만들고 데이터셋을 사용하여 처음부터 다시 훈련해야 한다.
    • 하이퍼파라미터 공간은 일반적으로 분리되어 있는 결정들로 채워진다. 즉 연속적이지 않고 미분 가능하지 않다. 그러므로 하이퍼파라미터 공간에 경사 하강법을 사용할 수 없다. 대신 경사 하강법보다 훨씬 비효율적인 그래디언트-프리(gradient-free) 최적화 기법을 사용해야 한다.
  • 이 분야가 어렵고 아직 초창기이기 때문에 모델 최적화에 사용할 도구가 매우 적다. 가장 단순하지만 종종 랜덤 탐색(반복적으로 랜덤하게 하이퍼파라미터를 선택)이 제일 좋은 방법일 때가 많다.
    • 랜덤 탐색보다 더 나은 도구는 하이퍼파라미터 최적화를 위한 파이썬 라이브러리인 Hyperopt(https://github.com/hyperopt/hyperopt)이다.
    • 이 라이브러리는 잘 작동할 것 같은 하이퍼파라미터 조합을 예측하기 위해 내부적으로 Parzen 트리 추정기를 사용한다.
    • 다른 라이브러리로 Hyperas(http://github.com/maxpumperla/hyperas)는 Hyperopt와 연동하여 케라스 모델에 사용할 수 있다.
  • 전체적으로 봤을 때 하이퍼파라미터 최적화는 어느 작업에서 최고의 모델을 얻거나 머신 러닝 경연 대회에서 우승하기 위한 강력한 도구이다.
    • 오래 전에는 사람들이 얕은 머신 러닝 모델에 넣을 특성을 직접 만들었다. 이는 매우 최적화되지 않은 방법이다.
    • 요즘에는 딥러닝이 계층적인 특성 엔지니어링 작업을 자동화한다. 수작업이 아니라 피드백 신호를 사용하여 특성이 학습된다.
    • 어찌보면 당연한데, 같은 식으로 수작업으로 모델 구조를 만드는 것이 아니라 이론에 근거하여 모델 구조를 최적화해야 한다.
    • 이 글을 쓰는 시점에는 하이퍼파라미터 자동 최적화 분야는 매우 초기 단계이고 미성숙하다. 딥러닝도 수년 전에는 그랬다. 하지만 다음 몇 년 동안 크게 성장할 것으로 기대한다.

모델 앙상블

  • 모델 앙상블(model ensemble)은 가장 좋은 결과를 얻을 수 있는 또 다른 강력한 기법이다. 앙상블은 여러 개의 다른 모델의 예측을 합쳐서 더 좋은 예측을 만든다.
    • 캐글 같은 머신 러닝 경연 대회에서는 우승자들이 대규모 모델 앙상블을 사용한다. 이런 앙상블은 아주 뛰어난 단일 모델보다도 성능이 좋다.
  • 앙상블은 독립적으로 훈련된 다른 종류의 좋은 모델이 각기 다른 장점을 가지고 있다는 가정을 바탕으로 한다.
    • 각 모델은 예측을 만들기 위해 조금씩 다른 측면을 바라본다. 데이터의 모든 면이 아니고 부분 특징이다.
    • (장님과 코끼리 예시)
  • 이 장님들이 훈련 데이터의 매니폴드를 이해하려는 머신 러닝 모델이다. 
    • 각자의 가정(고유한 모델 구조와 랜덤 가중치 초기화)을 이용하고 각자의 관점으로 이해한다. 각 모델은 데이터의 일부분에 맞는 정답을 찾지만 완전한 정답은 아니다.
    • 이들의 관점을 모으면 데이터를 훨씬 더 정확하게 묘사할 수 있다. 코끼리의 신체 부위는 다양하다. 장님 한 사람이 정확히 알아맞히지 못하지만 모두 모여 이야기하면 매우 정확하게 묘사할 수 있다.
  • 분류 예를 들어보자. 분류기 예측을 (앙상블 하기 위해) 합치는 가장 쉬운 방법은 추론할 때 나온 예측을 평균 내는 것이다.
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_c)
  • 이 방식은 분류기들이 어느 정도 비슷하게 좋을 때 잘 작동한다. 분류기 중 하나가 다른 모델보다 월등히 나쁘면 최종 예측은 앙상블에 있는 가장 좋은 분류기만큼 좋지 않을 수 있다.
  • 분류기를 앙상블하는 좋은 방법은 검증 데이터에서 학습된 가중치를 사용하여 가중 평균하는 것이다. 전형적으로 분류기가 좋을수록 높은 가중치를 가지고 나쁜 분류기일 수록 낮은 가중치를 갖는다.
    • 좋은 앙상블 가중치를 찾기 위해 랜덤 서치나 넬더-미드(Nelder-Mead) 방법 같은 간단한 최적화 알고리즘을 사용할 수 있다.
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_c
  • 이외에도 여러 변종이 있다. 예컨대 예측의 지수 값을 평균할 수 있다. 일반적으로 검증 데이터에서 찾은 최적의 가중치로 단순하게 가중 평균하는 방법이 좋은 기본값이다.
  • 앙상블이 잘 작동하게 만드는 핵심은 분류기의 다양성이다. 다양성이 앙상블의 힘이다. 장님들이 코끼리의 코만 만졌다면 모두 코끼리가 뱀 같다고 동의했을 것이다.
    • 이런 이유로 가능한 최대한 다르면서 좋은 모델을 앙상블해야 한다. 일반적으로 매우 다른 구조를 가지거나 다른 종류의 머신 러닝 방법을 말한다.
    • 같은 네트워크를 랜덤 초기화를 다르게 하여 따로따로 여러 번 훈련해서 앙상블하는 것은 거의 해볼 가치가 없다. 모델 사이 차이점이 랜덤 초기화와 모델에 주입되는 훈련 데이터의 순서라면 이 앙상블은 다양성이 낮고 하나의 모델보다 아주 조금만 성능이 향상될 것이다.
  • 모든 문제에 적용하지는 못하지만 실전에 잘 동작하는 한 가지 방법은 트리 기반 모델(랜덤 포레스트나 그래디언트 부스팅 트리)이나 심층 신경망을 앙상블하는 것이다.
  • 2014년 안드레이 콜레프(Andrei Kolev)와 필자는 캐글의 힉스 보손 붕괴(Higgs Boson decay) 감지 대회에서 여러 트리 모델과 심층 신경망을 사용하여 4위를 했다.
    • 특별하게 앙상블 모델 중 하나는 다른 방법을 사용해서 만들었다. (RGF(Regularized Greedy Forest) 모델이었다) 이 모델은 다른 모델보다 눈에 띄게 나빴다.
    • 당연히 앙상블에서 낮은 가중치를 할당했다. 하지만 놀랍게도 이 모델이 전체 앙상블의 성능을 크게 향상시켰다. 이 모델이 다른 모델과 매우 달랐기 때문이다. 다른 모델이 가지지 못한 정보를 제공한 것이다.
    • 이것이 앙상블의 핵심이다. 최상의 모델이 얼마나 좋은지보다 앙상블의 후보 모델이 얼마나 다양한지가 중요하다.
  • 최근 실전에서 매우 성공적으로 사용되는 기본 앙상블 스타일은 딥러닝과 얕은 모델을 섞은 넓고 깊은(wide and deep) 모델이다.
    • 이런 모델은 심층 신경망과 많은 선형 모델을 함께 훈련한다. 다양한 종류의 모델들을 함께 훈련하는 것은 모델 앙상블을 만드는 또 다른 방법이다.

정리

  • 고성능 심층 컨브넷을 만들려면 잔차 연결, 배치 정규화, 깊이별 분리 합성곱을 사용해야 한다. 미래에는 깊이별 분리 합성곱이 일반적인 합성곱을 완전히 대체할 것이다. 애플리케이션이 1D나 2D 또는 3D인지와 상관없이 아주 효율적으로 표현을 학습하기 때문이다.
  • 심층 네트워크를 만들 때 많은 하이퍼파라미터와 네트워크 구조를 선택해야 한다. 이 값들이 모여 모델의 성능을 결정한다. 이런 선택을 직관이나 랜덤한 선택에 의존하지 않고 최적의 선택을 찾기 위해 하이퍼파라미터 공간을 조직적으로 탐색하는 것이 좋다.
    • 현재는 이 과정에 비용이 많이 들고 좋은 도구도 없다. 하지만 Hyperopt와 Hyperas 라이브러리가 도움이 될 수 있다. 하이퍼파라미터를 최적화할 때 검증 세트에 과대적합된다는 것을 잊지 말라
  • 머신 러닝 경연 대회에서 우승하거나 어떤 문제에서 최상의 결과를 얻으려면 대규모로 모델을 앙상블하게 된다. 최적화가 잘 된 가중 평균으로 만든 앙상블은 보통 충분히 좋은 결과를 만든다.
    • 다양성이 중요하다는 것을 기억하라. 비슷한 모델을 앙상블하는 것은 거의 쓸모가 없다. 가장 좋은 앙상블은 가능한 서로 다른 모델로 만드는 것이다. (당연히 가능한 예측 성능이 높은 것 중에서 고른다)

요약

  • 이 장에서 배운 것들
    • 임의의 층 그래프를 구성하는 모델을 만드는 방법, 층을 재사용하는 방법(가중치 공유), 파이썬 함수 방식으로 모델을 사용하는 방법(모델 템플릿)
    • 케라스 콜백을 사용하여 훈련하는 동안 모델을 모니터링 하고 모델 상태를 바탕으로 작업을 수행한다.
    • 텐서보드를 사용하여 측정 지표, 활성화 출력의 히스토그램, 임베딩 공간을 시각화한다.
    • 배치 정규화, 깊이별 분리 합성곱, 잔차 연결
    • 하이퍼파라미터 최적화와 모델 앙상블을 사용하는 이유

케라스 창시자에게 배우는 딥러닝/ 텍스트와 시퀀스를 위한 딥러닝

  • 이 장에서는 텍스트(단어의 시퀀스 또는 문자의 시퀀스), 시계열 또는 일반적인 시퀀스(sequence) 데이터를 처리할 수 있는 딥러닝 모델을 살펴보겠다.
  • 시퀀스 데이터를 처리하는 기본적인 딥러닝 모델은 순환 신경망(recurrent neural network)과 1D 컨브넷(1D convnet) 두 가지이다.
    • 1D 컨브넷은 이전 장에서 다룬 2D 컨브넷의 1차원 버전이다.
  • 다음 애플리케이션들이 이런 알고리즘들을 사용한다.
    • 문서 분류나 시계열 분류, 예컨대 글의 주제나 책의 저자 식별하기
    • 시계열 비교, 예컨대 두 문서나 두 주식 가격이 얼마나 밀접하게 관련되어 있는지 추정하기
    • 시퀀스-투-시퀀스 학습, 예컨대 영어 문장을 프랑스어로 변환하기
    • 감성 분석, 예컨대 트윗이나 영화 리뷰가 긍정적인지 부정적인지 분류하기
    • 시계열 예측, 예컨대 어떤 지역의 최근 날씨 데이터가 주어졌을 때 향후 날씨 예측하기

텍스트 데이터 다루기

  • 텍스트는 가장 흔한 시퀀스 형태의 데이터이다. 텍스트는 단어의 시퀀스나 문자의 시퀀스로 이해할 수 있다. 보통 단어 수준으로 작업하는 경우가 많다.
    • 다음 절에 소개할 시퀀스 처리용 딥러닝 모델은 텍스트를 사용하여 기초적인 자연어 이해(natural language understanding) 문제를 처리할 수 있다.
    • 이런 모델은 문서 분류, 감성 분석, 저자 식별, (제한된 범위의) 질문 응답(Question Answering, QA) 등의 애플리케이션에 적합하다.
    • 물론 이런 딥러닝 모델이 사람처럼 진짜 텍스트를 이해하는 것은 아니다. 이런 모델은 문자 언어(written language)에 통계적 구조를 만들어 간단한 텍스트 문제를 해결한다.
    • 컴퓨터 비전이 픽셀에 적용한 패턴 인식(pattern recognition)인 것처럼 자연어 처리(natural language processing)를 위한 딥러닝은 단어, 문장, 문단에 적용한 패턴 인식이다.
  • 다른 모든 신경망과 마찬가지로 텍스트 원본을 입력으로 사용하지 못한다. 딥러닝 모델은 수치형 텐서만 다룰 수 있기 때문이다. 텍스트를 수치형 텐서로 변환하는 과정을 텍스트 벡터화(vectorizing text)라고 한다. 여기에는 여러 방식이 있다.
    • 텍스트를 단어로 나누고 각 단어를 하나의 벡터로 변환한다.
    • 텍스트를 문자로 나누고 각 문자를 하나의 벡터로 변환한다.
    • 텍스트에서 단어나 문자의 n-그램(n-gram)을 추출하여 각 n-그램을 하나의 벡터로 변환한다. n-그램은 연속된 단어나 문자의 그룹으로 텍스트에서 단어나 문자를 하나씩 이동하면서 추출한다.
  • 텍스트를 나누는 이런 단위(단어, 문자, n-그램)를 토큰(token)이라고 한다. 그리고 텍스트를 토큰으로 나누는 작업을 토큰화(tokenization)라고 한다.
    • 모든 텍스트 벡터화 과정은 어떤 종류의 토큰화를 적용하고 생성된 토큰에 수치형 벡터를 연결하는 것으로 이루어진다. 
    • 이런 벡터는 시퀀스 텐서로 묶여져서 심층 신경망에 주입된다. 
  • 토큰과 벡터를 연결하는 방법은 여러 가지가 있는데 여기서는 두 가지 주요한 방법을 소개하겠다.
    • 토큰의 원-핫 인코딩(one-hot encoding)과 토큰 임베딩(token embedding)(일반적으로 단어에 대해서만 사용되므로 단어 임베딩(word embedding)이라고도 부른다) 그것이다.

Note) n-그램과 BoW

  • 단어 n-그램은 문장에서 추출한 N개(또는 그 이하)의 연속된 단어 그룹이다. 같은 개념이 단어 대신 문자에도 적용될 수 있다.
  • “The cat sat on the mat”라는 문장을 예로 들어 2-그램 집합으로 분해하면 다음과 같다.
    • { “The”, “The cat”, “cat”, “cat sat”, “sat”, “sat on”, “on”, “on the”, “the”, “the mat”, “mat” }
  • 같은 문장을 3그램 집합으로 분해하면 다음과 같다.
    • { “The”, “The cat”, “The cat sat”, “cat”, “cat sat”, “cat sat on”, “sat”, “sat on”, “sat on the”, “on”, “on the”, “on the mat”, “the”, “the mat”, “mat” }
  • 이런 집합을 각각 2-그램 가방(bag of 2-gram) 또는 3-그램 가방(bag of 3-gram)이라고 한다.
    • 가방(bag)이란 용어는 다루고자 하는 것이 리스트나 시퀀스가 아니라 토큰의 집합이라는 사실을 의미한다. 
    • 이 토큰에는 특정한 순서가 없다. 이런 종류의 토큰화 방법을 BoW(Back-of-Words)라고 한다.
  • BoW가 순서 없는 토큰화 방법이기 때문에(생성된 토큰은 시퀀스가 아니라 집합으로 간주되고 문장의 일반적인 구조가 사라진다) 딥러닝 모델보다 얕은 학습 방법의 언어 처리 모델에 사용되는 경향이 있다.
    • n-그램을 추출하는 것은 일종의 특성 공학이다. 딥러닝은 유연하지 못하고 이런 방식을 계층적인 특성 학습으로 대체한다. 나중에 소개할 순환 신경망과 1D 컨브넷으로 단어와 문자 그룹에 대한 특성을 학습할 수 있다.
    • 이 방식들은 그룹들을 명시적으로 알려 주지 않아도 연속된 단어나 문자의 시퀀스를 봄으로써 학습한다.
  • 이런 이유 때문에 이 책에서는 n-그램을 더 다루지 않는다. 하지만 로지스틱 회귀나 랜덤 포레스트 같은 얕은 학습 방법의 텍스트 처리 모델을 사용할 때는 강력하고 아주 유용한 특성 공학 방법임을 기억해 두자.

단어와 문자의 원-핫 인코딩

  • 원-핫 인코딩은 토큰을 벡터로 변환하는 가장 일반적이고 기본적인 방법이다.
    • 3장에서 IMDB와 로이터 예제에서 살펴보았는데, 모든 단어에 고유한 정수 인덱스를 부여하고 이 정수 인덱스 i를 크기가 N인 이진 벡터로 변환한다.
    • 이 벡터는 i번째 원소만 1이고 나머지는 모두 0이다.
  • 물론 원-핫 인코딩은 문자 수준에서도 적용할 수 있다. 원-핫 인코딩이 무엇이고 어떻게 하는지 명확하기 위해 아래 단어와 문자에 대한 간단한 예를 만들었다.
### 단어 수준의 원-핫 인코딩 예
import numpy as np

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

token_index = {}

for sample in samples:
for word in sample.split():
if word not in token_index:
token_index[word] = len(token_index) + 1

max_length = 10

results = np.zeros(shape=(len(samples), max_length, max(token_index.values()) + 1))

for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = token_index.get(word)
results[i, j, index] = 1.

### 문자 수준의 원-핫 인코딩 예
import string

samples = ['The cat sat on the mat.', 'The dog ate my homework.']
characters = string.printable
token_index = dict(zip(characters, range(1, len(characters) + 1)))

max_length = 50
results = np.zeros((len(samples), max_length, max(token_index.values()) + 1))

for i, sample in enumerate(samples):
for j, character in enumerate(sample):
index = token_index.get(character)
results[i, j, index] = 1.
  • 케라스에는 원본 텍스트 데이터를 단어 또는 문자 수준의 원-핫 인코딩으로 변환해 주는 유틸리티가 있다. 특수 문자를 제거하거나 빈도가 높은 N개의 단어만 선택(입력 벡터 공간이 너무 커지지 않도록 하기 위한 일반적인 제한 방법)하는 등 여러 가지 중요한 기능들이 있기 때문에 이 유틸리티를 사용하는 편이 좋다.
from keras.preprocessing.text import Tokenizer

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

tokenizer = Tokenizer(num_words=1000)
tokenizer.fit_on_texts(samples)

sequences = tokenizer.texts_to_sequences(samples)

one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')

word_index = tokenizer.word_index
print('%s개의 고유한 토큰을 찾았습니다' % len(word_index))
  • 원-핫 인코딩의 변종 중 하나는 원-핫 해싱(one-hot hashing) 기법이다. 이 방식은 어휘 사전에 있는 고유한 토큰의 수가 너무 커서 모두 다루기 어려울 때 사용한다.
    • 각 단어에 명시적으로 인덱스를 할당하고 이 인덱스를 딕셔너리에 저장하는 대신 단어를 해싱하여 고정된 크기의 벡터로 변환한다. 일반적으로 간단한 해싱 함수를 사용한다.
    • 이 방식의 주요 장점은 명시적인 단어 인덱스가 필요 없기 때문에 메모리를 절약하고 온라인 방식으로 데이터를 이노딩할 수 있다.
    • 한 가지 단점은 해시 충돌(hash collision)이다. 2개의 단어가 같은 해시를 만들면 이를 바라보는 머신 러닝 모델은 단어 사이의 차이를 인식하지 못한다. 해싱 공간 차원의 해싱될 고유 토큰의 전체 개수보다 훨씬 크면 해시 충돌의 가능성은 감소한다.
samples = ['The cat sat on the mat.', 'The dog ate my homework.']

dimensionality = 1000
max_length

results = np.zeros((len(samples), max_length, dimensionality))

for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = abs(hash(word)) % dimensionality
results[i, j, index] = 1.

단어 임베딩 사용하기

  • 단어와 벡터를 연관 짓는 강력하고 인기있는 또 다른 방법은 단어 임베딩이라는 밀집 단어 벡터(word vector)를 사용하는 것이다.
    • 원-핫 인코딩으로 만든 벡터는 희소(sparse)하고 (대부분 0으로 채워진다) 고차원이다 (어휘 사전에 있는 단어의 수와 차원이 같다)
    • 반면 단어 임베딩은 저차원의 실수형 벡터(희소 벡터의 반대인 밀집 벡터)이다. 

  • 원-핫 인코딩으로 얻은 단어 벡터와 달리 단어 임베딩은 데이터로부터 학습된다.
    • 보통 256차원, 512차원 또는 큰 어휘 사전을 다룰 때는 1024차원의 단어 임베딩을 사용한다.
    • 반면 원-핫 인코딩은 2만 개의 토큰으로 이루어진 어휘 사전을 만들려면 20,000 차원 또는 그 이상의 벡터일 경우가 많다. 따라서 단어 임베디잉 더 많은 정보를 적은 차원에 저장한다.
  • 단어 임베딩을 만드는 방법은 두 가지이다.
    • (문서 분류나 감성 예측 같은) 관심 대상인 문제와 함께 단어 임베딩을 학습한다. 이런 경우에는 랜덤한 단어 벡터로 시작해서 신경망의 가중치를 학습하는 것과 같은 방식으로 단어 벡터를 학습한다.
    • 풀려는 문제가 아니고 다른 머신 러닝에서 미리 계산된 단어 임베딩을 로드한다. 이를 사전 훈련된 단어 임베딩(pretrained word embedding)이라고 한다.

Embedding 층을 사용하여 단어 임베딩 학습하기

  • 단어와 밀집 벡터를 연관 짓는 가장 간단한 방법은 랜덤하게 벡터를 선택하는 것이다.
    • 이 방식의 문제점은 임베딩 공간이 구조적이지 않다는 것이다. 예컨대 accurate와 exact 단어는 대부분 문장에서 비슷한 의미로 사용되지만 완전히 다른 임베딩을 가진다.
    • 심층 신경망이 이런 임의의 구조적이지 않은 임베딩 공간을 이해하기는 어렵다.
  • 단어 벡터 사이에 좀 더 추상적이고 기하학적인 관계를 얻으려면 단어 사이에 있는 의미 관계를 반영해야 한다. 단어 임베딩은 언어를 기하학적 공간에 매핑하는 것이다.
    • 예컨대 잘 구축된 임베딩 공간에서는 동의어가 비슷한 단어 벡터로 임베딩될 것이다. 일반적으로 두 단어 벡터 사이의 거리(L2 거리)는 이 단어 사이의 의미 거리와 관계되어 있다. (멀리 떨어진 위치에 임베딩된 단어 의미는 서로 달느 반면 비슷한 단어들은 가까이 임베딩 된다)
    • 거리 외에 임베딩 공간의 특정 방향도 의미를 가질 수 있다.
  • 예를 들어보자. 아래 그림에서 4개의 단어 cat, dog, wolf, tiger가 2D 평면에 임베딩되어 있다.
    • 예컨대 cat에서 tiger로 이동하는 것과 dog에서 wolf로 이동하는 것을 같은 벡터로 나타낼 수 있다. 이 벡터는 ‘애완동물에서 야생 동물로 이동’ 하는 것으로 해석할 수 있다.
    • 비슷하게 다른 벡터로 dog에서 cat으로 이동하는 것과 wolf에서 tiger로 이동하는 것을 나타내면 ‘개과에서 고양이과로 이동’하는 벡터로 해석할 수 있다.

  • 실제 단어 임베딩 공간에서 의미 있는 기하학적 변환의 일반적인 예는 ‘성별’ 벡터와 ‘복수(plural)’ 벡터이다. 
    • 예컨대 ‘king’ 벡터에 ‘female’ 벡터를 더하면 ‘queen’ 벡터가 되고 plural 벡터를 더하면 ‘kings’가 된다.
    • 단어 임베딩 공간은 전형적으로 이런 해석이 가능하고 잠재적으로 유용한 수천 개의 벡터를 특성으로 가진다.
  • 사람의 언어를 매핑해서 어떤 자연어 처리 작업에도 사용할 수 있는 이상적인 단어 임베딩 공간이 존재할까? 아마도 가능하겠지만 아직까지 이런 종류의 공간은 만들지 못했다. 
    • 사람의 언어에도 그런 것은 없다. 세상에는 많은 다른 언어가 있고 언어는 특정 문화와 환경을 반영하기 때문에 서로 동일하지 않다.
  • 실제로 좋은 단어 임베딩 공간을 만드는 것은 문제에 따라 크게 달라진다. 
    • 영어로 된 영화 리뷰 감성 분석 모델을 위한 완벽한 단어 임베딩 공간은 영어로 된 법률 문서 분류 모델을 위한 완벽한 임베딩 공간과 다를 것이다. 특정 의미 관계의 중요성이 작업에 따라 다르기 때문이다.
  • 따라서 새로운 작업에는 새로운 임베딩을 학습하는 것이 타당하다. 다행히 역전파를 사용하여 쉽게 만들 수 있고, 케라스를 사용하면 더 쉽다. Embedding 층의 가중치를 학습하면 된다.
from keras.layers import Embedding

embedding_layer = Embedding(1000, 64)
  • Embedding 층을 (특정 단어를 나타내는) 정수 인덱스를 밀집 벡터로 매핑하는 딕셔너리로 이해하는 것이 가장 좋습니다. 정수를 입력으로 받아 내부 딕셔너리에서 이 정수에 연관된 벡터를 찾아 반환한다. 딕셔너리 탐색은 효율적으로 수행된다.
    • 단어 인덱스 -> Embedding 층 -> 연관된 단어 벡터
  • Embedding 층은 크기가 (samples, sequence_length)인 2D 정수 텐서를 입력으로 받는다. 각 샘플은 정수의 시퀀스이다. 가변 길이의 시퀀스를 임베딩에 임베딩할 수 있다.
    • 예컨대 앞 예제의 Embedding 층에 (32, 10) 크기의 배치(길이가 10인 시퀀스 32개로 이루어진 배치)나 (64, 15) 크기의 배치를 주입할 수 있다.
    • 배치에 있는 모든 시퀀스는 길이가 같아야 하므로 (하나의 텐서에 담아야 하기 때문에) 작은 길이의 시퀀스는 0으로 패딩되고 길이가 더 긴 시퀀스는 잘린다.
  • Embedding 층은 크기가 (samples, sequence_length, embedding_dimensionality)인 3D 실수형 텐서를 반환한다.
    • 이런 3D 텐서는 RNN 층이나 1D 합성곱 층에서 처리된다.
  • Embedding 층의 객체를 생성할 때 가중치(토큰 벡터를 위한 내부 딕셔너리)는 다른 층과 마찬가지로 랜덤하게 초기화된다.
    • 훈련하면서 이 단어 벡터는 역전파를 통해 점차 조정되어 이어지는 모델이 사용할 수 있도록 임베딩 공간을 구성한다.
    • 훈련이 끝나면 임베딩 공간은 특정 문제에 특화된 구조를 많이 가지게 된다.
  • IMDB 영화 리뷰 감성 예측 문제에 적용해 보자. 우선 데이터를 준비한다.
    • 영화 리뷰에서 가장 빈도가 높은 1만 개의 단어를 추출하고 리뷰에서 20개 단어 이후는 버린다.
    • 이 네트워크는 1만 개의 단어에 대해 8차원의 임베딩을 학습하여 정수 시퀀스 입력(2D 정수 텐서)을 임베딩 시퀀스(3D 실수형 텐서)로 바꿀 것이다.
    • 그 다음 이 텐서를 2D로 펼쳐서 분류를 위한 Dense 층을 훈련하겠다.
from keras.datasets import imdb
from keras import preprocessing

max_features = 10000
maxlen = 20

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)

x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)

from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding

model = Sequential()
model.add(Embedding(10000, 8, input_length=maxlen))

model.add(Flatten())

model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
model.summary()

history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_split=0.2)

---
Train on 20000 samples, validate on 5000 samples Epoch 1/10 20000/20000 [==============================] - 1s 37us/step - loss: 0.6748 - acc: 0.6086 - val_loss: 0.6285 - val_acc: 0.6978 Epoch 2/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.5490 - acc: 0.7491 - val_loss: 0.5282 - val_acc: 0.7304 Epoch 3/10 20000/20000 [==============================] - 1s 34us/step - loss: 0.4623 - acc: 0.7865 - val_loss: 0.5007 - val_acc: 0.7430 Epoch 4/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.4223 - acc: 0.8091 - val_loss: 0.4940 - val_acc: 0.7496 Epoch 5/10 20000/20000 [==============================] - 1s 34us/step - loss: 0.3960 - acc: 0.8224 - val_loss: 0.4964 - val_acc: 0.7534 Epoch 6/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.3757 - acc: 0.8339 - val_loss: 0.5000 - val_acc: 0.7488 Epoch 7/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.3581 - acc: 0.8445 - val_loss: 0.5059 - val_acc: 0.7502 Epoch 8/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.3408 - acc: 0.8545 - val_loss: 0.5126 - val_acc: 0.7476 Epoch 9/10 20000/20000 [==============================] - 1s 32us/step - loss: 0.3243 - acc: 0.8641 - val_loss: 0.5191 - val_acc: 0.7482 Epoch 10/10 20000/20000 [==============================] - 1s 32us/step - loss: 0.3084 - acc: 0.8715 - val_loss: 0.5257 - val_acc: 0.7486
  • 약 75% 가량의 검증 정확도가 나오는데, 20개의 단어만 사용했음을 생각해 보면 괜찮은 결과이다.
    • 하지만 임베딩 시퀀스를 펼치고 하나의 Dense 층을 훈련했으므로 입력 시퀀스에 있는 각 단어를 독립적으로 다루었다. 단어 사이의 관계나 문장 구조를 고려하지 않았다.
    • 각 시퀀스 전체를 고려한 특성을 학습하도록 임베딩 층 위에 순환 층이나 1D 합성곱을 추가하는 것이 좋다. 이는 다음절에서 다루겠다.

사전 훈련된 단어 임베딩 사용하기

  • 풀려는 문제와 함께 단어 임베딩을 학습하는 대신 미리 계산된 임베딩 공간에서 임베딩 벡터를 로드할 수 있다. 이런 임베딩 공간은 뛰어난 구조와 유용한 성질을 가지고 있어서 언어 구조의 일반적인 측면을 잡아낼 수 있다.
    • 자연어 처리에서 사전 훈련된 단어 임베딩을 사용하는 이유는 이미지 분류 문제에서 사전 훈련된 컨브넷을 사용하는 이유와 거의 동일하다.
    • 충분한 데이터가 없어서 자신만의 좋은 특성을 학습하지 못하지만 꽤 일반적인 특성이 필요할 때이다. 이런 경우에는 다른 문제에서 학습한 특성을 재사용하는 것이 합리적이다.
  • 단어 임베딩은 일반적으로 (문장이나 문서에 같이 등장하는 단어를 관찰하는) 단어 출현 통계를 사용해서 계산된다.
    • 여기에는 여러 기법이 있는데 신경망을 사용하는 것도 있고 그렇지 않은 방법도 있다.
    • 단어를 위해 밀집된 저차원 임베딩 공간을 비지도 학습 방법으로 계산하는 아이디어는 요슈아 벤지오 등이 2000년대 초에 조사했다.
    • 연구나 산업 애플리케이션에 적용되기 시작한 것은 Word2Vec 알고리즘(https://code.google.com/archive/p/word2vec)이 등장한 이후이다.
    • 이 알고리즘은 2013년 구글의 토마스 미코로프(Tomas Mikolov)가 개발했으며, 가장 유명하고 성공적인 단어 임베딩 방법이다. Word2Vec의 차원은 성별처럼 구체적인 의미가 있는 속성을 잡아낸다.
  • 케라스의 Embedding 층을 위해 내려 받을 수 있는 미리 계산된 단어 임베딩 데이터베이스가 여럿 있는데, Word2Vec 외에 인기 있는 또 다른 하나는 2014년 스탠포드 대학의 연구자들이 개발한 GloVe(Global Vectors for Word Representation)(https://nlp.stanford.edu/projects/glove)이다.
    • 이 임베딩 기법은 단어의 동시 출현(co-occurrence) 통계를 기록한 행렬을 분해하는 기법을 사용한다.
    • 이 개발자들은 위키피디아 데이터와 커먼 크롤(Common Crawl) 데이터에서 가져온 수백만 개의 영어 토큰에 대해 임베딩을 미리 계산해 놓았다.
  • GloVe 임베딩을 케라스 모델에 어떻게 사용하는지 알아보자. Word2Vec 임베딩이나 다른 단어 임베딩 데이터베이스도 방법은 같다.

모든 내용을 적용하기: 원본 텍스트에서 단어 임베딩까지

  • 케라스에 포함된 IMDB 데이터는 미리 토큰화가 되어 있기 때문에, 이를 사용하는 대신 원본 텍스트 데이터를 내려받아 처음부터 시작해 보겠다.

원본 IMDB 텍스트 내려받기

  • http://mng.bz/0tIo에서 IMDB 원본 데이터셋을 내려받고 압축을 해제한다.
  • 훈련용 리뷰 하나를 문자열 하나로 만들어 훈련 데이터를 문자열의 리스트로 구성해 보자. 리뷰 레이블(긍정/부정)도 labels 리스트로 만들겠다.
import os

imdb_dir = './datasets/aclImdb/'
train_dir = os.path.join(imdb_dir, 'train')

labels = []
texts = []

for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)

for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), encoding="utf8")
texts.append(f.read())
f.close()

if label_type == 'neg':
labels.append(0)
else:
labels.append(1)

데이터 토큰화

  • 텍스트를 벡터로 만들고 훈련 세트와 검증 세트로 나누겠다. 사전 훈련된 단어 임베딩은 훈련 데이터가 부족한 문제에 특히 유용하다. (그렇지 않으면 문제에 특화된 임베딩이 훨씬 성능이 좋다)
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np

maxlen = 100
training_samples = 200
validation_samples = 10000
max_words = 10000

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)

word_index = tokenizer.word_index
print('%s개의 고유한 토큰을 찾았습니다'% len(word_index))

data = pad_sequences(sequences, maxlen=maxlen)
labels = np.asarray(labels)
print('데이터 텐서의 크기:', data.shape)
print('레이블 텐서의 크기:', labels.shape)

indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]

x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples:training_samples + validation_samples]
y_val = labels[training_samples:training_samples + validation_samples]

GloVe 단어 임베딩 내려받기

  • https://nlp.stanford.edu/projects/glove에서 2014년 영문 위키피디아를 사용하여 사전에 계산된 임베딩을 내려 받는다.
    • 이 파일의 이름은 glove.6B.zip 이고 파일 크기는 823MB이다.
    • 40만 개의 단어(또는 단어가 아닌 토큰)에 대한 100차원의 임베딩 벡터를 포함하고 있다.

임베딩 전처리

  • 압축 해제한 파일(.txt 팡리)을 파싱하여 단어(즉 문자열)와 이에 상응하는 벡터 표현(즉 숫자 벡터)을 매핑하는 인덱스를 만든다.
glove_dir = './datasets/glove/'

embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'), encoding="utf8")

for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs

f.close()

print('%s개의 단어 벡터를 찾았습니다.' % len(embeddings_index))
  • 그 다음 Embedding 층에 주입할 수 있도록 임베딩 행렬을 만든다. 이 행렬의 크기는 (max_words, embedding_dim)이어야 한다.
    • 이 행렬의 i번째 원소는 (토큰화로 만든) 단어 인덱스의 i번째 단어에 상응하는 embedding_dim 차원 벡터이다.
    • 인덱스 0은 어떤 단어나 토큰도 아닐 경우를 나타낸다.
embedding_dim = 100

embedding_matrix = np.zeros((max_words, embedding_dim))

for word, i in word_index.items():
if i < max_words:
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector

모델 정의하기

  • 이전과 동일한 구조의 모델을 사용한다.
from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense

model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 100, 100) 1000000 _________________________________________________________________ flatten_1 (Flatten) (None, 10000) 0 _________________________________________________________________ dense_1 (Dense) (None, 32) 320032 _________________________________________________________________ dense_2 (Dense) (None, 1) 33 ================================================================= Total params: 1,320,065 Trainable params: 1,320,065 Non-trainable params: 0

모델에 GloVe 임베딩 로드하기

  • Embedding 층은 하나의 가중치 행렬을 가진다. 이 행렬은 2D 부동소수 행렬이고 각 i번째 원소는 i번째 인덱스에 상응하는 단어 벡터이다.
    • 모델의 첫 번째 층인 Embedding 층에 준비된 GloVe 행렬을 로드하자.
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False
  • 추가적으로 Embedding 층을 동결한다.
    • 사전 훈련된 컨브넷 특성을 사용할 떄와 같은 이유이다. 모델의 일부는 사전 훈련되고 다른 부분은 랜덤하게 초기화 되었다면 훈련하는 동안 사전 훈련된 부분이 업데이트 되면 안 된다.
    • 이미 알고 있던 정보를 모두 잃게 된다. 랜덤하게 초기화된 층에서 대량의 그래디언트 업데이트가 발생하면 이미 학습된 특성을 오염시키기 때문이다.

모델 훈련과 평가

  • 모델을 컴파일하고 훈련한다.
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_data=(x_val, y_val))

model.save_weights('pre_trained_glove_model.h5')
  • 모델의 성능을 그래프로 그려보자.
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Trainging and validation accuarcy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Trainging and validation loss')
plt.legend()

plt.show()

  • 이 모델은 과대적합이 빠르게 시작된다.
    • 훈련 샘플 수가 작기 때문에 놀라운 일은 아니다. 같은 이유로 검증 정확도와 훈련 정확도 사이에 차이가 크다. 검증 정확도는 50% 후반을 달성한 것 같다.
    • 훈련 샘플 수가 적기 때문에 어떤 샘플 200개를 선택했는지에 따라 성능이 크게 좌우된다. 여기서는 샘플들을 랜덤하게 선택했다.
  • 사전 훈련된 단어 임베딩을 사용하지 않거나 임베딩 층을 동결하지 않고 같은 모델을 훈련할 수 있다.
    • 이런 경우 해당 작업에 특화된 입력 토큰의 임베딩을 학습할 것이다. 데이터가 풍부하게 있다면 사전 훈련된 단어 임베딩보다 훨씬 성능이 높다.
    • 훈련 샘플이 200개 뿐이지만 시도해 보자.
    • (그래프 그리는 코드는 계속 동일한 코드가 반복되므로 생략)
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_data=(x_val, y_val))

  • 검증 정확도는 50% 초반에 멈추어있다. 이 예제에서는 사전 훈련된 단어 임베딩을 사용하는 것이 더 낫다. 
  • 마지막으로 테스트 데이터에서 모델을 평가해 보자. 우선 테스트 데이터를 토큰화 해야 한다.
test_dir = os.path.join(imdb_dir, 'test')

labels = []
texts = []

for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)
for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), encoding="utf8")
texts.append(f.read())
f.close()

if label_type == 'neg':
labels.append(0)
else:
labels.append(1)

sequences = tokenizer.texts_to_sequences(texts)
x_test = pad_sequences(sequences, maxlen=maxlen)
y_test = np.asarray(labels)
  • 그 다음 첫 번째 모델을 로드하고 평가한다.
model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test, y_test)

정리

  • 이제 다음 작업을 할 수 있다.
    • 원본 텍스트를 신경망이 처리할 수 있는 형태로 변환한다.
    • 케라스 모델에 Embedding 층을 추가하여 어떤 작업에 특화된 토큰 임베딩을 학습한다.
    • 데이터가 부족한 자연어 처리 문제에서 사전 훈련된 단어 임베딩을 사용하여 성능 향상을 꾀한다.

순환 신경망 이해하기

  • 완전 연결 네트워크가 컨브넷처럼 지금까지 본 모든 신경망의 특징은 메모리가 없다는 것. 네트워크에 주입되는 입력은 개별적으로 처리되며 입력 간에 유지되는 상태가 없다.
    • 이런 네트워크로 시퀀스나 시계열 데이터 포인트를 처리하려면 네트워크에 전체 시퀀스를 주입해야 한다. 즉 전체 시퀀스를 하나의 데이터 포인트로 변환해야 한다.
    • 예컨대 IMDB 문제에서 영화 리뷰 하나를 큰 벡터 하나로 변환해서 처리했는데, 이런 네트워크를 피드포워드 네트워크(feedforward network)라고 한다.
  • 이와 반대로 사람이 문장을 읽는 것처럼 이전에 나온 것을 기억하면서 단어별로 또는 한눈에 들어오는 만큼씩 처리할 수 있다. 이는 문장에 있는 의미를 자연스럽게 표현하도록 도와준다.
    • 생물학적 지능은 정보처리를 위한 내부 모델을 유지하면서 점진적으로 정보를 처리한다. 
    • 이 모델은 과거 정보를 사용하여 구축되며 새롭게 얻은 정보를 계속 업데이트 한다.
  • 극단적으로 단순화 시킨 버전이지만 순환 신경망(Recurrent Neural Network, RNN)은 같은 원리를 적용한 것이다. 시퀀스의 원소를 순회하면서 지금까지 처리한 정보를 상태(state)에 저장한다.
    • 사실 RNN은 내부에 루프(loop)를 가진 신경망의 한 종류이다. RNN의 상태는 2개의 다른 시퀀스(2개의 다른 IMDB 리뷰)를 처리하는 사이에 재설정된다.
    • 하나의 시퀀스가 여전히 하나의 데이터 포인트로 간주된다. 즉 네트워크에 하나의 입력을 주입한다고 가정한다. 하지만 이 데이터 포인트가 한 번에 처리 되지 않는다는 점이 다르다. 그 대신 네트워크는 시퀀스의 원소를 차례대로 방문한다.

  • 루프와 상태에 대한 개념을 명확히 하기 위해 넘파이로 RNN 정방향 계산을 구현해 보자.
    • RNN은 크기가 (timesteps, input_features)인 2D 텐서로 인코딩된 벡터의 시퀀스를 입력 받는다.
    • 이 시퀀스는 타임스텝을 따라서 반복된다. 각 타임스텝 t에서 현재 상태와 ((input_features,)크기의)입력을 연결하여 출력을 계산한다.
    • 그 다음 이 출력을 다음 스텝의 상태로 설정한다. 첫 번째 타임스텝에서는 이전 출력이 정의되지 않으므로 현재 상태가 없다. 이때는 네트워크의 초기 상태(initial state)인 0 벡터로 상태를 초기화 한다.
  • 의사코드(pseudocode)로 표현하면 RNN은 다음과 같다.
state_t = 0

for input_t in input_sequence:
output_t = f(input_t, state_t)
state_t = output_t ---출력은 다음 반복을 위한 상태가 된다.
  • f 함수는 입력과 상태를 출력으로 변환한다. 이를 2개의 행렬 W와 U 그리고 편향 벡터를 사용하는 변환으로 바꿀 수 있다. 피드포워드 네트워크의 완전 연결 층에서 수행되는 변환과 비슷하다.
state_t = 0

for input_t in input_sequence:
output_t = activation(dot(W, input_t) + dot(U, state_t) + b)
state_t = output_t
  • RNN의 정방향 계산을 넘파이로 구현해 보자
import numpy as np

timesteps = 100
input_features = 32
output_features = 64

inputs = np.random.random((timesteps, input_features))

state_t = np.zeros((output_features,))

W = np.random.random((output_features, input_features))
U = np.random.random((output_features, output_features))
b = np.random.random((output_features,))

successive_outputs = []

for input_t in inputs:
output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)
successive_outputs.append(output_t)
state_t = output_t

final_output_sequence = np.stack(successive_outputs, axis=0)
  • 요약하면 RNN은 반복할 때 이전에 계산한 정보를 재사용하는 for 루프에 지나지 않는다.
    • 물론 이 정의에 맞는 RNN 의 종류는 많다. 이 예는 가장 간단한 RNN의 형태이다. 
    • RNN은 스텝(step) 함수에 의해 특화된다.

케라스의 순환 층

  • 넘파이로 간단하게 구현한 과정이 실제 케라스의 SimpleRNN 층에 해당한다. SimpleRNN이 한 가지 다른 점은 넘파이 예제처럼 하나의 시퀀스가 아니라 다른 케라스 층와 마찬가지로 시퀀스 배치를 처리한다는 것이다. 
    • 즉 (timesteps, input_features) 크기가 아니라 (batch_size, timesteps, input_features) 크기의 입력을 받는다.
  • 케라스에 있는 모든 순환 층과 마찬가지로 SimpleRNN은 두 가지 모드로 실행할 수 있다.
    • 각 타임스텝의 출력을 모은 전체 시퀀스를 반환하거나 (크기가 (batch_size, timesteps, output_features)인 3D 텐서), 입력 시퀀스에 대한 마지막 출력만 반환할 수 있다 (크기가 (batch_size, output_features)인 2D 텐서)
    • 이 모드는 객체를 생성할 때 return_sequences 매개변수로 선택할 수 있다. 
  • SimpleRNN을 사용하여 마지막 타임스텝의 출력만 얻는 예제를 살펴보자.
from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN

model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, None, 32) 320000 _________________________________________________________________ simple_rnn_1 (SimpleRNN) (None, 32) 2080 ================================================================= Total params: 322,080 Trainable params: 322,080 Non-trainable params: 0
  • 다음 예는 전체 상태 시퀀스를 반환한다.
model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_2 (Embedding) (None, None, 32) 320000 _________________________________________________________________ simple_rnn_2 (SimpleRNN) (None, None, 32) 2080 ================================================================= Total params: 322,080 Trainable params: 322,080 Non-trainable params: 0
  • 네트워크의 표현력을 증가시키기 위해 여러 개의 순환 층을 차례대로 쌓는 것이 유용할 때가 있다. 이런 설정에서는 중간층들이 전체 출력 시퀀스를 반환하도록 설정해야 한다.
model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_3 (Embedding) (None, None, 32) 320000 _________________________________________________________________ simple_rnn_3 (SimpleRNN) (None, None, 32) 2080 _________________________________________________________________ simple_rnn_4 (SimpleRNN) (None, None, 32) 2080 _________________________________________________________________ simple_rnn_5 (SimpleRNN) (None, None, 32) 2080 _________________________________________________________________ simple_rnn_6 (SimpleRNN) (None, 32) 2080 ================================================================= Total params: 328,320 Trainable params: 328,320 Non-trainable params: 0
  • IMDB 영화 리뷰 분류 문제에 적용해 보자. 우선 데이터를 전처리한다.
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000
maxlen = 500
batch_size = 32

print('데이터 로딩...')
(input_train, y_train), (input_test, y_test) = imdb.load_data(num_words=max_features)
print(len(input_train), '훈련 시퀀스')
print(len(input_test), '테스트 시퀀스')

print('시퀀스 패딩 (samples x time)')

input_train = sequence.pad_sequences(input_train, maxlen=maxlen)
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)
print('input_train 크기:', input_train.shape)
print('input_test 크기:', input_test.shape)
  • Embedding 층과 SimpleRNN 층을 사용하여 간단한 순환 네트워크를 훈련시켜보자
from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN, Dense

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(SimpleRNN(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(input_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 이제 훈련과 검증의 손실과 정확도를 그래프로 그린다.
    • (그래프 그리는 코드는 계속 동일한 코드가 반복되므로 생략)

  • 3장에서 이 데이터셋을 사용한 첫 번째 모델에서 얻은 테스트 정확도는 87%였다. 안타깝지만 간단한 순환 네트워크는 이 기준 모델 보다 성능이 높지 않다. (85%)
    • 이런 원인은 전체 시퀀스가 아니라 처음 500개의 단어만 입력에 사용했기 때문이다. 이 RNN은 기준 모델보다 얻은 정보가 적다.
    • 다른 이유는 SimpleRNN이 텍스트처럼 긴 시퀀스를 처리하는데 적합하지 않기 때문이다.
    • 더 잘 작동하는 다른 순환층이 있으므로 살펴보자

LSTM과 GRU 층 이해하기

  • 케라스에는 SimpleRNN 외에 다른 순환층도 있는데 LSTM과 GRU가 그것이다. 실전에서는 항상 이 둘 중 하나를 사용할 것이다. SimpleRNN은 실전에 쓰기에는 너무 단순하기 때문이다.
    • SimpleRNN은 이론적으로 시간 t에서 이전의 모든 타임스텝의 정보를 유지할 수 있지만 실제로 긴 시간에 걸친 의존성은 학습할 수 없는 것이 문제이다.
    • 층이 많은 일반 네트워크(피드포워드 네트워크)에서 나타나는 것과 비슷한 현상인 그래디언트 소실 문제(vanishing gradient problem) 때문이다. 
  • 피드포워드 네트워크에 층을 많이 추가할 수록 훈련하기 어려운 것과 같다. 1990년대 초 호크라이더(Hochreiter), 슈미트후버(Schmidhuber), 벤지오(Bengio)가 이런 현상에 대한 이론적인 원인을 연구했다. 이 문제를 해결하기 위해 고안된 것이 LSTM과 GRU 층이다.
  • LSTM 층을 살펴보자. 장, 단기 메모리(Long Short-Term Memory, LSTM) 알고리즘은 호크라이더와 슈미트후버가 1997년 개발했다. 이 알고리즘은 그래디언트 소실 문제에 대한 연구의 결정체이다.
    • 이 층은 앞서 보았던 SimpleRNN의 한 변종이다. 정보를 여러 타임스텝에 걸쳐 나르는 방법이 추가된다.
    • 처리할 시퀀스에 나란히 작동하는 컨베이어 벨트를 생각해보자. 시퀀스 어느 지점에서 추출된 정보가 컨베이어 벨트 위로 올라가 필요한 시점의 타임스텝으로 이동하여 떨군다.
    • 이것이 LSTM이 하는 일이다. 나중을 위해 정보를 저장함으로써 처리 과정에서 오래된 시그널이 점차 소실되는 것을 막아 준다.
  • 이를 자세히 이해하기 위해 SimpleRNN 셀(cell)부터 그려보자.
    • 가중치가 여러 개가 나오므로 출력(output)을 타나내는 문자 o로 셀에 있는 W와 U 행렬을 표현하겠다. (Wo와 Uo)

  • 이 그림에 타임스텝을 가로질러 정보를 나르는 데이터 흐름을 추가해 보자.
    • 타임스텝 t에서 이 값을 이동 상태 c_t라고 부르겠다. 여기서 c는 이동(carry)를 의미한다.
    • 이 정보를 사용하여 셀이 다음과 같이 바뀐다. 입력 연결과 순환 연결(상태)로부터 이 정보가 합성된다(완전 연결층과 같은 변환: 가중치 행렬과 점곱한 후 편향을 더하고 활성화 함수를 적용한다)
    • 그러고는 다음 타임스텝으로 전달될 상태를 변경시킨다(활성화 함수와 곱셈을 통해서)
    • 개념적으로 보면 데이터를 실어 나르는 이 흐름이 다음 출력과 상태를 조절한다.

  • 여기까지는 간단하다. 이제 복잡한 부분은 데이터 흐름에서 다음 이동 상태(c_t+1)이 계산되는 방식이다. 여기에는 3개의 다른 변환이 관련되어 있다. 3개 모두 SimpleRNN과 같은 형태를 가진다.
y = activation(dot(state_t, U) + dot(input_t, W) + b)
  • 3개의 변환 모두 자신만의 가중치 행렬을 가진다. 각각 i, f, k로 표시하겠다. 다음이 지금까지 설명한 내용이다.
output_t = activation(c_t) * activation(dot(input_t, Wo) + dot(state_t, Uo) + bo)

i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi)
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf)
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)
  • i_t, f_t, k_t를 결합하여 새로운 이동 상태 (c_t+1)을 구한다.
c_t + 1 = i_t * f_t * k_t
  • 아래 그림에 이를 추가했다.
    • (SimpleRNN이 단순히 이전 출력만을 이용하는 반면, LSMT에서는 carry 되는 값을 추가로 기존 결과에 곱해준다.)
    • (문제는 이 carray 되는 값을 어떻게 구하느냐 인데, 그것은 i, f, k라는 3개의 다른 가중치 행렬을 이용해서 output을 각각 구하고, 그것들의 합하여 최종적으로 carry 되는 값을 만든다)
    • (최종적으로 그렇게 구해진 carry되는 값을 원래 그 시점에 구해야 하는 output과 곱해서 최종 output을 만든다.)

  • 이 연산들이 하는 일을 해석하면 각 의미에 대해 통찰을 얻을 수 있다.
    • 예컨대 c_t와 f_t의 곱셈은 이동을 위한 데이터 흐름에서 관련이 적은 정보를 의도적으로 삭제한다고 할 수 있다.
    • 한편 i_t와 k_t는 현재에 대한 정보를 제공하고 이동 트랙을 새로운 정보로 업데이트 한다.
    • 하지만 결국 이런 해석은 큰 의미가 없다. 이 연산들이 실제로 하는 일은 연산에 관련된 가중치 행렬에 따라 결정되기 때문이다.
    • 이 가중치는 end-to-end 방식으로 학습된다. 이 과정은 훈련 반복마다 매번 새로 시작되며 이런저런 연산들에 특정 목적을 부여하기가 불가능하다.
  • RNN 셀의 사양(specification)은 가설 공간을 결정한다. 훈련할 때 이 공간에서 좋은 모델 파라미터를 찾는다.
    • 셀의 사양이 셀이 하는 일을 결정하지 않는다. 이는 셀의 가중치에 달려 있다.  같은 셀이라도 다른 가중치를 가지는 경우 매우 다른 작업을 수행한다.
    • 따라서 RNN 셀을 구성하는 연산 조합은 엔지니어링 적인 설계가 아니라 가설 공간의 제약 조건으로 해석하는 것이 낫다.
  • 연구자에게는 RNN 셀의 구현 방법 같은 제약 조건의 선택을 엔지니어보다 (유전 알고리즘이나 강화 학습 알고리즘 같으) 최적화 알고리즘에 맡기면 더 나아보일 것이다. 미래에는 이런 식으로 네트워크를 만들게 될 것이다.
    • 요약하면 LSTM 셀의 구체적인 구조에 대해 이해할 필요가 전혀 없고 우리가 해야 할 일도 아니다. LSTM 셀의 역할만 기억하면 된다. 바로 과거 정보를 나중에 다시 주입하여 그래디언트 소실 문제를 해결하는 것이다.

케라스를 사용한 LSTM 예제

  • 이제 LSTM 층으로 모델을 구성하고 IMDB 데이터에서 훈련해 보자.
    • 이 네트워크는 SimpleRNN에서 사용했던 모델과 비슷하다. LSTM 층은 출력 차원만 지정하고 다른 매개변수는 케라스의 기본값으로 남겨두었다.
    • 케라스는 좋은 기본값을 가지고 있어서 직접 매개변수를 튜닝하는데 시간을 쓰지 않고도 거의 항상 어느 정도 작동하는 모델을 얻을 수 있다.
from keras.models import Sequential
from keras.layers import Embedding, Dense,LSTM

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(input_train, y_train, epochs=10, batch_size=128, validation_split=0.2)

  • 이번에는 88% 정도의 검증 정확도를 달성했다. SimpleRNN 보다 확실히 더 낫다. LSTM이 그래디언트 소실 문제로부터 덜 영향을 받기 때문이다.
    • 3장의 완전 연결 네트워크보다 조금 더 낫다.
  • 하지만 많은 계산을 사용한 것치고는 획기적인 결과는 아니다. 왜 LSTM의 성능이 더 높지 않을까? 
    • 한 가지 이유는 임베딩 차원이나 LSTM 출력 차원 같은 하이퍼파라미터를 전혀 튜닝하지 않았기 때문이다. 또 하나는 규제가 없기 때문이다.
    • 솔직히 말하면 가장 큰 이유는 리뷰를 전체적으로 길게 분석하는 것(LSTM이 잘하는 일)은 감성 분류 문제에 도움이 되지 않기 때문이다.
    • 이런 간단한 문제는 각 리뷰에 어떤 단어가 나타나고 얼마나 등장하는지를 보는 것이 낫다. 바로 첫 번째 완전 연결 네트워크가 사용한 방법이다.
    • 하지만 훨씬 더 복잡한 자연어 처리 문제들에서는 LSTM 능력이 드러난다. 특히 질문-응답과 기계 번역 분야이다.

정리

  • NN이 무엇이고 동작하는 방법
  • LSTM이 무엇이고 긴 시퀀스에서 단순한 RNN보다 더 잘 작동하는 이유
  • 케라스의 RNN 층을 사용해서 시퀀스 데이터를 처리하는 방법

순환 신경망의 고급 사용법

  • 여기서는 순환 신경망의 성능과 일반화 능력을 향상시키기 위한 세 가지 고급 기술을 살펴보겠다. 온도 예측 문제로 세 가지 개념을 모두 시연해 보겠다. 이 문제는 시계열 데이터에서 일반적으로 나타나는 여러 어려운 점을 갖고 있는데, 전형적이고 꽤 도전적인 문제이다.
  • 다음 기법들을 적용하겠다.
    • 순환 드롭아웃(recurrent dropout): 순환 층에서 과대적합을 방지하기 위해 케라스에 내장되어 있는 드롭아웃을 사용한다.
    • 스태킹 순환 층(stacking recurrent layer): 네트워크의 표현 능력(representational power)를 증가시킨다. (그 대신 계산 비용이 많이 든다)
    • 양방향 순환 층(bidirectional recurrent layer): 순환 네트워크에 같은 정보를 다른 방향으로 주입하여 정확도를 높이고 기억을 좀 더 오래 유지시킨다.

기온 예측 문제

  • 지금까지 다룬 시퀀스 데이터는 IMDB 데이터셋이나 로이터 데이터셋처럼 텍스트 데이터이다. 시퀀스 데이터는 이런 언어 처리 뿐만 아니라 훨씬 많은 문제에서 등장한다.
    • 이 절에 있는 모든 예제는 날씨 시계열 데이터셋을 사용한다. 이 데이터는 독일 예나(Jena) 시에 있는 막스 플랑크 생물지구화학 연구소의 지상 관측소에서 수집한 것이다.
  • 다음 경로에서 데이터를 내려받고 압축을 불자.
import os

data_dir = 'E:/Study/Keras Deep Learning/datasets/jena/'
fname = os.path.join(data_dir, 'jena_climate_2009_2016.csv')

f = open(fname)
data = f.read()
f.close()

lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
  • line 수를 출력해 보면 42만 개가 나온다. 데이터 전체를 넘파이 배열로 바꾸자.
import numpy as np

float_data = np.zeros((len(lines), len(header) - 1))

for i, line in enumerate(lines):
values = [float(x) for x in line.split(',')[1:]]
float_data[i, :] = values
  • 온도를 그래프로 그려보자.
from matplotlib import pyplot as plt

temp = float_data[:, 1]
plt.plot(range(len(temp)), temp)

  • 기간을 좁혀서 처음 10일간 온도 데이터를 나타내면 다음과 같다.

데이터 준비

  • 이 문제의 정확한 정의는 다음과 같다. lookback 타임스텝만큼 이전으로 돌아가서 매 steps 타임스텝마다 샘플링한다. 이 데이터를 바탕으로 delay 타임스텝 이후의 온도를 예측할 수 있을까? 사용할 변수는 다음과 같다.
    • lookback = 1440: 10일 전 데이터로 돌아간다.
    • steps = 6: 1시간마다 데이터 포인트 하나를 샘플링한다.
    • delay = 144 : 24시간이 지난 데이터가 타깃이 된다.
  • 시작하기 전에 두 가지 작업을 해야 한다.
    • 신경망에 주입할 수 있는 형태로 데이터를 전처리한다. 데이터가 이미 수치형이므로 추가적인 벡터화는 필요하지 않다. 하지만 데이터에 있는 각 시계열 특성의 범위가 서로 다르므로 시계열 특성을 개별적으로 정규화하여 비슷한 범위를 가진 작은 값으로 바꾸자.
    • float_date 배열을 받아 과거 데이터의 배치와 미래 타깃 온도를 추출하는 파이썬 제너레이터(generator)를 만든다. 이 데이터셋에 있는 샘플은 중복이 많다. (샘플 N과 샘플 N+1은 대부분 타임스텝이 비슷하다) 모든 샘플을 각기 메모리에 적재하는 것은 낭비가 심하므로 대신 원본 데이터를 사용하여 그때그때 배치를 만들자.
  • 각 시계열 특성에 대해 평균을 빼고 표준 편차로 나누어 전처리한다. 
    • 처음 20만개만 훈련 데이터로 사용할 것이므로 전체 데이터에서 20만 개만 사용해서 평균과 표준편차를 구한다.
data = float_data[:200000]
mean = data.mean(axis=0)
float_data -= mean
std = data.std(axis=0)
float_data /= std
  • 아래 코드는 여기서 사용할 제너레이터이다. 이 제너레이터 함수는 (samples, targets) 튜플을 반복적으로 반환한다. samples는 입력 데이터로 사용할 배치고, targets은 이에 대응되는 타깃 온도의 배열이다. 이 제네레이터 함수에는 다음 매개변수가 있다.
    • data: 정규화한 부동 소수 데이터로 이루어진 원본 배열
    • lookback: 입력으로 사용하기 위해 거슬러 올라갈 타임스텝
    • delay: 타깃으로 사용할 미래의 타임스텝
    • min_index와 max_index: 추출할 타임스텝의 범위를 지정하기 위한 data 배열의 인덱스. 검증 데이터와 테스트 데이터를 분리하는데 사용한다.
    • shuffle: 샘플을 섞을지 시간 순서대로 추출할지 결정한다.
    • batch_size: 배치의 샘플 수
    • step: 데이터를 샘플링할 타임스텝 간격. 1시간에 하나의 데이터 포인트를 추출하기 위해 6으로 지정한다.
def generator(data, lookback, delay, min_index, max_index, shuffle=False, batch_size=128, step=6):
if max_index is None:
max_index = len(data) - delay - 1

i = min_index + lookback

while 1:
if shuffle:
rows = np.random.randint(min_index + lookback, max_index, size=batch_size)
else:
if i + batch_size >= max_index:
i = min_index + lookback

rows = np.arange(i, min(i + batch_size, max_index))
i += len(rows)

samples = np.zeros((len(rows), lookback // step, data.shape[-1]))
targets = np.zeros((len(rows),))

for j, row in enumerate(rows):
indices = range(rows[j] - lookback, rows[j], step)
samples[j] = data[indices]
targets[j] = data[rows[j] + delay][1]

yield samples, targets
  • 이제 generator 함수를 사용하여 훈련용, 검증용, 테스트용으로 3개의 제너레이터를 만들어 보자.
    • 각 제너레이터는 원본 데이터에서 다른 시간대를 사용한다. 훈련 제너레이터는 처음 20만 개 타임스텝을 사용하고, 검증 제너레이터는 그 다음 10만 개, 테스트 제너레이터는 나머지를 사용한다.
lookback = 1440
step = 6
delay = 144
batch_size = 128

train_gen = generator(float_data, lookback=lookback, delay=delay, min_index=0, max_index=200000, shuffle=True, step=step, batch_size=batch_size)
validation_gen = generator(float_data, lookback=lookback, delay=delay, min_index=200001, max_index=300000, shuffle=True, step=step, batch_size=batch_size)
test_gen = generator(float_data, lookback=lookback, delay=delay, min_index=300001, max_index=None, shuffle=True, step=step, batch_size=batch_size)

val_steps = (300000 - 200001 - lookback) // batch_size
test_steps = (len(float_data) - 300001 - lookback) // batch_size

상식 수준의 기준점

  • 예측 문제를 풀기 전에 상식 수준의 해법을 시도해 보자. 이는 정상 여부 확인을 위한 용도고 고수준 머신 러닝 모델이라면 뛰어넘어야 할 기준점을 만든다.
    • 이런 상식 수준의 해법은 알려진 해결책이 없는 새로운 문제를 다루어야 할 때 유용하다.
    • 일부 클래스가 월등히 많아 불균형한 분류 문제가 고전적인 예이다. 데이터셋에 클래스 A의 샘플이 90%, 클래스 B의 샘플이 10%가 있다면, 이 분류 문제에 대한 상식 수준의 접근법은 새로운 샘플을 항상 클래스 ‘A’라고 예측하는 것이다.
    • 이 분류기는 전반적으로 90%의 정확도를 낼 것이다. 이따금 이런 기본적인 기준점을 넘어서기가 아주 어려운 경우가 있다.
  • 이 경우 온도 시계열 데이터는 연속성이 있고 일자별로 주기성을 가진다고 가정할 수 있다. 그렇기 때문에 상식 수준의 해결책은 지금으로부터 24시간 후 온도는 지금과 동일하다고 예측하는 것이다.
    • 이 방법을 평균 절댓값 오차(MAE)로 평가해 보자.
def evaluate_naive_method():
batch_maes = []

for step in range(val_steps):
samples, targets = next(validation_gen)
preds = samples[:, -1, 1]
mae = np.mean(np.abs(preds - targets))
batch_maes.append(mae)

print(np.mean(batch_maes))

evaluate_naive_method()
---
0.28904772281241886
  • 출력된 MAE는 0.29이다. 이 온도 데이터는 평균이 0이고 표준 편차가 1이므로 결괏값이 바로 와닿지는 않는다. 평균 절댓값 오차 0.29에 표준 편차를 곱하면 섭씨 2.57이 된다.
  • 이제 딥러닝 모델이 더 나은지 시도해 보자

기본적인 머신 러닝 방법

  • 머신 러닝 모델을 시도하기 전에 상식 수준의 기준점을 세웠다. 비슷하게 RNN 처럼 복잡하고 연산 비용이 많이 드는 모델을 시도하기 전에 간단하고 손쉽게 만들 수 있는 머신 러닝 모델(예컨대 소규모의 완전 연결 네트워크)을 먼저 만드는 것이 좋다.
    • 이를 바탕으로 더 복잡한 방법을 도입하는 근거가 마련되고 실제적인 이득도 얻게 될 것이다.
  • 아래 코대는 데이터를 펼쳐서 2개의 Dense 층을 통과시키는 완전 연결 네트워크를 보여준다.
    • 전형적인 회귀 문제이므로 마지막 Dense 층에 활성화 함수를 두지 않았다. 손실 함수는 MAE 이다.
    • 상식 수준의 방법에서 사용한 것과 동일한 데이터와 지표를 사용했으므로 결과를 바로 비교해 볼 수 있다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Flatten(input_shape=(lookback // step, float_data.shape[-1])))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=validation_gen, validation_steps=val_steps)
  • 훈련 손실과 검증 손실의 그래프를 그려보자.
import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Trainging and validation loss')
plt.legend()

plt.show()

  • 일부 검증 손실은 학습을 사용하지 않은 기준점에 가깝지만 안정적이지 못하다.
    • 앞서 기준 모델을 만든 것이 도움이 된다. 이 문제는 기준 모델의 성능을 앞지르기가 쉽지 않다. 우리가 적용한 상식에는 머신 러닝 모델이 찾지 못한 핵심 정보가 많이 포함되어 있다.

첫 번째 순환 신경망

  • 첫 번째 완전 연결 네트워크는 잘 작동하지 않았다. 그렇다고 이 문제에 머신 러닝이 적합하지 않다는 뜻은 아니다. 
    • 앞선 모델은 시계열 데이터를 펼쳤기 때문에 입력 데이터에서 시간 개념을 잃어버렸다. 그 대신 인과 관계와 순서가 의미 있는 시퀀스 데이터를 그대로 사용해 보겠다.
    • 이런 시퀀스 데이터에 아주 잘 들어맞는 순환 시퀀스 모델을 시도해 보겠다. 이 모델은 앞선 모델과 달리 데이터 포인터의 시간 순서를 사용한다.
  • 이전 절에서 소개한 LSTM 층 대신 2014년에 정준영 등이 개발한 GRU 층을 사용하겠다.
    • GRU(Gated Recurrent Unit) 층은 LSTM 층과 같은 원리로 작동하지만 좀 더 간결하고 그래서 계산 비용이 덜 든다. (LSTM 만큼 표현 학습 능력이 높지는 않을 수 있다)
    • 계산 비용과 표현 학습 능력 사이의 트레이드오프(trade-off)는 머신 러닝 어디에서나 등장한다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=validation_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 상식 수준의 모델을 크게 앞질렀다. 이 그림은 시퀀스를 펼쳐서 처리하는 완전 연결 네트워크에 비해 순환 네트워크가 이런 종류의 작업에 훨씬 뛰어나다는 것과 머신 러닝의 가치를 보여준다.
  • 새로운 검증 MAE는 0.265 이하(크게 과대적합되기 시작하는 곳)이고 정규화되기 전인 섭씨로 복원하면 MAE는 2.35이다. 초기 에러 2.57보다 확실히 낫지만 더 개선할 수 있다.

과대적합을 감소하기 위해 순환 드롭아웃 사용하기

  • 훈련 손실과 검증 손실 곡선을 보면 모델이 과대적합인지 알 수 있다. 몇 번의 에포크 이후 훈련 손실과 검증 손실이 현저히 벌어지기 시작한다.
    • 이런 문제를 해결하기 위해 잘 알려진 드롭아웃 기법을 이미 보았다. 훈련 데이터를 층에 주입할 때 데이터에 있는 우연한 상관관계를 깨뜨리기 위해 입력 층의 유닛을 랜덤하게 끄는 기법이다.
    • 순환 신경망에 드롭아웃을 올바르게 적용하는 것은 간단하지 않다. 순환 층 이전에 드롭아웃을 적용하면 규제에 도움되는 것보다 학습에 더 방해되는 것으로 오랫동안 알려졌다.
  • 2015년 야린 갈(Yarin Gal)이 베이지안 딥러닝에 관한 박사 논문에서 순환 네트워크에 적절하게 드롭아웃을 사용하는 방법을 알아냈다.
    • 타임스텝마다 랜덤하게 드롭아웃 마스크를 바꾸는 것이 아니라 동일한 드롭아웃 마스크(동일한 유닛의 드롭 패턴)를 모든 타임스텝에 적용해야 한다.
    • GRU나 LSTM 같은 순환 게이트에 의해 만들어지는 표현을 규제하려면 순환 층 내부 계산에 사용된 활성화 함수에 타임스텝마다 동일한 드롭아웃 마스크를 적용해야 한다(순환 드롭 아웃 마스크)
    • 모든 타임스텝에 동일한 드롭 아웃 마스크를 적용하면 네트워크가 학습 오차를 타임스텝에 걸쳐 적절하게 전파시킬 것이다.
    • 타임스텝마다 랜덤한 드롭아웃 마스크를 적용하면 오차 신호가 전파되는 것을 방해하고 학습 과정에 해를 끼친다.
  • 야린 갈을 케라스를 사용해서 연구했고 케라스 순환 층에 이 기능을 구현하는데 도움을 주었다. 카레스에 있는 모든 순환 층은 2개의 드롭아웃 매개변수를 가지고 있다.
    • dropout은 층의 입력에 대한 드롭아웃 비율을 정하는 부동 소수 값이다. recurrent_dropout은 순환 상태의 드롭아웃 비율을 정한다.
  • GRU 층에 드롭아웃과 순환 드롭아웃을 적용하여 과대적합에 어떤 영향을 미치는지 살펴보자. 드롭아웃으로 규제된 네트워크는 언제나 완전히 수렴하는데 더 오래 걸린다. 에포크를 2배 더 늘려 네트워크를 훈련하자.
model = Sequential()
model.add(layers.GRU(32, dropout=0.2, recurrent_dropout=0.2, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=validation_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 30번 에포크까지 과대적합이 일어나지 않았다. 평가 점수는 안정적이지만 이전보다 더 나아지지는 않았다.

스태킹 순환 층

  • 과대적합은 더는 없지만 성능상 병목이 있는 것 같으므로 네트워크의 용량을 늘려야 한다.
    • 일반적인 머신 러닝 작업 흐름을 기억하자. 과대적합이 일어날 때까지 네트워크의 용량을 늘리는 것이 좋다. 너무 많이 과대적합되지 않는 한 아직 충분한 용량에 도달한 것이 아니다.
  • 네트워크의 용량을 늘리려면 일반적으로 층에 있는 유닛의 수를 늘리거나 층을 더 많이 추가한다.
    • 순환 층 스태킹은 더 강력한 순환 네트워크를 만드는 고전적인 방법이다.
    • 예컨대 구글 번역 알고리즘의 현재 성능은 7개 대규모 LSTM 층을 쌓은 대규모 모델에서 나온 것이다.
    • 케라스에서 순환 층을 차례대로 쌓으려면 모든 중간층은 마지막 타임스텝 출력만 아니고 전체 시퀀스(3D 텐서)를 출력해야 한다. return_sequence = True로 지정하면 된다.
model = Sequential()
model.add(layers.GRU(32, dropout=0.2, recurrent_dropout=0.2, return_sequences = True, input_shape=(None, float_data.shape[-1])))
model.add(layers.GRU(64, activation='relu', dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=validation_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 층을 추가하여 성능을 조금 향상시켰지만, 크지는 않다. 여기서 두 가지 결론을 얻을 수 있다.
    • 아직 충분히 과대적합을 만들지 못했기 때문에 검증 손실을 향상하기 위해 층의 크기를 늘릴 수 있다. 하지만 적지 않은 계산 비용이 추가된다.
    • 층을 추가한 만큼 도움이 되지 않았으므로 여기서는 네트워크의 용량을 늘리는 것이 도움이 되지 않는다고 볼 수 있다.

양방향 RNN 사용하기

  • 이 절에서 소개할 마지막 기법은 양방향 RNN(bidirectionarl RNN)이다. 양방향 RNN은 RNN의 한 변종이고 특정 작업에서 기본 RNN 보다 훨씬 좋은 성능을 낸다. 자연어 처리에서는 맥가이버 칼이라고 할 정도로 즐겨 사용된다.
  • RNN은 특히 순서 또는 시간에 민감하다. 즉 입력 시퀀스의 타임스텝 순서대로 처리한다.
    • 타임스텝을 섞거나 거꾸로 하면 RNN이 시퀀스에서 학습하는 표현을 완전히 바꾸어 버린다. 이는 온도 예측처럼 순서에 의미가 있는 문제에 잘 맞는 이유이기도 하다.
  • 양방향 RNN은 RNN이 순서에 민감하다는 성질을 사용한다. 앞서 보았던 GRU나 LSTM 같은 RNN 2개를 사용한다. 각 RNN은 입력 시퀀스를 한 방향으로 처리한 후 각 표현을 합친다.
    • 시퀀스를 양쪽 방향으로 처리하기 떄문에 양방향 RNN은 단방향 RNN이 놓치기 쉬운 패턴을 감지할 수 있다.
  • 놀랍게도 이 절에 있는 RNN 층이 시간의 순서대로 시퀀서를 처리하는 것은 근거 없는 결정이다. 시간의 반대 방향으로 입력 시퀀스를 처리하면 만족할만한 RNN 성능을 낼 수 있을까? 
    • 실제 해보고 결과를 확인해 보자. 해야 할 일은 입력 시퀀스를 시간 차원을 따라 거꾸로 생성하는 데이터 제너레이터를 만드는 것 뿐이다. (제너레이터 함수의 마지막 줄을 yield samples[:, ::-1, :], targets)로 바꾼다.
  • 결과는 다음과 같다.

  • 순서를 뒤집은 GRU는 상식 수준의 기준점보다 성능이 낮다. 이 경우에는 시간 순서대로 처리하는 것이 중요하다.
    • 같은 기법을 IMDB에 적용해 보자.
from keras.datasets import imdb
from keras.preprocessing import sequence
from keras.models import Sequential
from keras import layers

max_features = 10000
maxlen = 500

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)

x_train = [x[::-1] for x in x_train]
x_test = [x[::-1] for x in x_test]

x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)

model = Sequential()
model.add(layers.Embedding(max_features, 128))
model.add(layers.LSTM(32))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 시간 순서로 훈련한 LSTM과 거의 동일한 성능을 얻을 수 있다. 놀랍게도 이런 텍스트 데이터셋에는 순서를 뒤집어 처리하는 것이 시간 순서대로 처리하는 것과 거의 동일하게 잘 작동한다.
    • 이는 언어를 이해하는데 단어의 순서가 중요하지만 결정적이지 않다는 가정을 뒷받침한다. 거꾸로 된 시퀀스에서 훈련한 RNN은 원래 시퀀스에서 훈련한 것과는 다른 표현을 학습한다.
    • 이와 비슷하게 시작할 때 죽고 마지막 날 태어나는 삶처럼 실제 세상의 시간이 거꾸로 흘러간다면 우리의 정신 세계는 달라질 것이다.
    • 머신 러닝에서 다른 표현이 유용하다면 항상 사용할 가치가 있다. 이 표현이 많이 다를수록 더 좋다. 이 표현이 데이터를 바라보는 새로운 시각을 제공하고 다른 방식에서는 놓칠 수 있는 데이터의 특징을 잡아낸다.
    • 이런 표현은 작업의 성능을 올리는데 도움을 준다. 이것이 7장에서 살펴볼 앙상블(ensemble) 개념이다.
  • 양방향 RNN은 이 아이디어를 사용해서 시간 순서대로 처리하는 RNN의 성능을 향상 시킨다. 
    • 입력 시퀀스를 양쪽 방향으로 바라보기 때문에 드러나지 않은 다양한 표현을 얻어 시간 순서대로 처리할 때 놓칠 수 있는 패턴을 잡아낸다.

  • 케라스에서는 Bidirectional 층을 사용하여 양방향 RNN을 만든다. 이 클래스는 첫 번째 매개변수로 순환 층의 객체를 전달 받는다.
    • Bidirectional 클래스는 전달받은 순환 층으로 새로운 두 번째 객체를 만든다.
    • 하나는 시간 순서대로 입력 시퀀스를 처리하고 다른 하나는 반대 순서로 입력 시퀀스를 처리한다.
  • IMDB에 이를 적용해 보자.
model = Sequential()
model.add(layers.Embedding(max_features, 32))
model.add(layers.Bidirectionarl(layers.LSTM(32)))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 여기서 얻은 검증 정확도는 88% 정도로 이전 절에서 사용했던 일반 LSTM 보다 성능이 조금 더 높다. 이 모델은 조금 더 일찍 과대적합되는 것 같다.
    • 양방향 순환 층이 단방향 LSTM 보다 모델 파라미터가 2배 많기 때문에 놀라운 일은 아니다.
    • 규제를 조금 추가한다면 양방향 순환층을 사용하는 것이 이 작업에 더 적합해 보인다.
  • 이제 동일한 방식을 온도 예측 문제에 적용해 보자.
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Bidirectionarl(layers.GRU(32), input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=validation_gen, validation_steps=val_steps)
  • 이 네트워크는 GRU 층와 비슷한 성능을 낸다. 이유는 쉽게 이해할 수 있는데, 모든 예측 성능은 시간 순서대로 처리하는 네트워크의 절반에서 온다. 시간 반대 순서로 처리하는 절반은 이런 작업에 성능이 매우 좋지 않기 때문이다.

더 나아가서

  • 온도 예측 문제의 성능을 향상시키기 위해 시도해 볼 수 있는 것들이 많이 있다.
    • 스태킹한 각 순환 층의 유닛 수를 조정한다. 지금 설정은 대부분 임의로 한 것이라 최적화가 덜 되었을 것이다.
    • RMSprop 옵티마이저가 사용한 학습률을 조정한다.
    • GRU 대신 LSTM 층을 사용한다.
    • 순환 층 위에 용량이 큰 완전 연결된 회귀 층을 사용한다. 즉 유닛 수가 많은 Dense 층이나 Dense 층을 스태킹 한다.
    • 최종적으로 (검증 MAE 기준으로 보았을 때) 최선의 모델을 테스트 세트에서 확인해야 한다. 이를 잊으면 과대적합된 네트워크 구조를 만들게 될 것이다.
  • 늘 그렇듯이 딥러닝은 과학보다 예술에 가깝다. 어떤 문제에 적합하거나 그렇지 않은 가이드라인은 제시할 수 있지만 결국 모든 문제는 다르다. 경험을 바탕으로 다른 전략들을 시도해 보아야 한다. 
    • 현재는 문제를 해결하는 최선의 방법을 미리 알 수 있는 이론이 없다. 반복해서 시도해야 한다.

정리

  • 장에서 처음 배운 것처럼 새로운 문제를 해결할 때는 선택한 지표에서 상식 수준의 기준점을 설정하는 것이 좋다. 기준점을 가지고 있지 않으면 실제로 향상되었는지 알 수 없다.
  • 계산 비용을 추가할지 판단하기 위해 비용이 비싼 모델 전에 간단한 모델을 시도한다.
  • 시간 순서가 중요한 데이터가 있다면 순환 층이 적합하다. 시계열 데이터를 펼쳐서 처리하는 모델의 성능을 쉽게 앞지를 것이다.
  • 순환 네트워크에 드롭아웃을 사용하려면 타임스텝 동안 일정한 드롭아웃 마스크와 순환 드롭아웃 마스크를 사용해야 한다. 둘 다 케라스 순환 층에 포함되어 있다. 순환 층에 있는 dropout과 recurrent_dropout 매개변수를 사용하면 된다.
  • 스태킹 RNN은 단일 RNN 층보다 더 강력한 표현 능력을 제공한다. 하지만 계산 비용이 많이 들기 때문에 항상 시도할 가치가 있지는 않다. (기계 번역 같은) 복잡한 문제에서 확실히 도움이 되지만 작고 간단한 문제에서는 항상 그렇지 않다.
  • 양쪽 방향으로 시퀀스를 바라보는 양방향 RNN은 자연어 처리 문제에 유용하다. 하지만 최근 정보가 오래된 것보다 훨씬 의미 있는 시퀀스 데이터에는 잘 작동하지 않는다.

컨브넷을 사용한 시퀀스 처리

  • 1D 컨브넷은 특정 시퀀스 처리 문제에서 RNN과 견줄 만하다. 일반적으로 계산 비용이 훨씬 싸다. 1D 컨브넷은 전형적으로 팽창된 커널(dilated kernel)과 함께 사용된다.
    • 최근 오디오 생성과 기계 번역 분야에서 큰 성공을 거두었다. 이런 특정 분야의 성공 이외에도 텍스트 분류나 시계열 예측 같은 간단한 문제에서 작은 1D 컨브넷이 RNN을 대신하여 빠르게 처리할 수 있다고 알려져있다.

시퀀스 데이터를 위한 1D 합성곱 이해하기

  • 앞서 소개한 합성곱 층은 2D 합성곱이다. 이미지 텐서에 2D 패치를 추출하고 모든 패치에 동일한 변환을 적용한다. 같은 방식으로 시퀀스에서 1D 패치(부분 시퀀스)를 추출하여 1D 합성곱을 적용한다.

  • 이런 1D 합성곱 층은 시퀀스에 있는 지역 패턴을 인식할 수 있다. 동일한 변환이 시퀀스에 있는 모든 패치에 적용되기 때문에 특정 위치에서 학습한 패턴을 나중에 다른 위치에서 인식할 수 있다.
    • 이는 1D 컨브넷에 (시간의 이동에 대한) 이동 불변성(translation invariant)을 제공한다.
    • 예컨대 크기 5인 윈도우를 사용하여 문자 시퀀스를 처리하는 1D 컨브넷은 5개 이하의 단어나 단어의 부분을 학습한다. 
    • 이 컨브넷은 이 단어가 입력 시퀀스의 어느 문장에 있더라도 인식할 수 있다. 따라서 문자 수준의 1D 컨브넷은 단어 형태학(word morphology)에 관해 학습할 수 있다.

시퀀스 데이터를 위한 1D 풀링

  • 컨브넷에서 이미지 텐서의 크기를 다운샘플링하기 위해 사용하는 평균 풀링이나 맥스 풀링 같은 2D 풀링 연산을 배웠다.
    • 1D 풀링 연산은 2D 풀링 연산과 동일하다. 입력에서 1D 패치(부분 시퀀스)를 추출하고 최댓값(최대 풀링)을 출력하거나 평균값(평균 풀링)을 출력한다.
    • 2D 컨브넷과 마찬가지로 1D 입력의 길이를 줄이기 위해 사용한다(서브샘플링(subsampling))

1D 컨브넷 구현

  • 케라스에서 1D 컨브넷은 Conv1D 층을 사용해서 구현한다. Conv1D는 Conv2D와 인터페이스가 비슷하다.
    • (samples, time, features) 크기의 3D 텐서를 입력으로 받고 비슷한 형태의 3D 텐서를 반환한다.
    • 합성곱 윈도우는 시간 축의 1D 윈도우이다. 즉 입력 텐서의 두 번째 축이다.
  • 간단한 2개의 층으로 된 1D 컨브넷을 만들어 IMDB 감성 분류 문제에 적용해 보자.
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000
maxlen = 500
batch_size = 32

print('데이터 로딩...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), '훈련 시퀀스')
print(len(x_test), '테스트 시퀀스')

print('시퀀스 패딩 (samples x time)')

x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('input_train 크기:', x_train.shape)
print('input_test 크기:', x_test.shape)
  • 1D 컨브넷은 5장에서 사용한 2D 컨브넷과 비슷한 방식으로 구성한다.
    • Conv1D와 MaxPooling1D 층을 쌓고 전역 풀링 층이나 Flatten 층으로 마친다.
    • 이 구조는 3D 입력을 2D 출력으로 바꾸므로 분류나 회귀를 위해 모델에 하나 이상의 Dense 층을 추가할 수 있다.
  • 한 가지 다른 점은 1D 컨브넷에 큰 합성곱 윈도우를 사용할 수 있다는 것이다.
    • 2D 합성곱 층에서 3 x 3 합성곱 윈도우는 3 x 3 = 9 특성을 고려한다.
    • 1D 합성곱 층에서 크기 3인 합성곱 윈도우는 3개의 특성만 고려한다. 그래서 1D 합성곱에 크기 7이나 9의 윈도우를 사용할 수 있다.
  • 다음은 IMDB 데이터셋을 위한 1D 컨브넷의 예이다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Embedding(max_features, 128, input_length=maxlen))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()

model.compile(optimizer=RMSprop(lr=1e-4), loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 결과는 다음과 같다.

  • 검증 정확도는 LSTM 보다 조금 낮지만 CPU나 GPU에서 더 빠르게 실행된다. 여기에서 적절한 에포크 수(4개)로 모델을 다시 훈련하고 테스트 세트에서 확인할 수 있다.
    • 이 예는 단어 수준의 감성 분류 작업에 순환 네트워크를 대신하여 빠르고 경제적인 1D 컨브넷을 사용할 수 있음을 보여준다.

CNN과 RNN을 연결하여 긴 시퀀스를 처리하기

  • 1D 컨브넷이 입력 패치를 독립적으로 처리하기 때문에 RNN과 달리 (합성곱 윈도우 크기의 범위를 넘어서는) 타임스텝의 순서에 민감하지 않다.
    • 물론 장기간 패턴을 인식하기 위해 많은 합성곱 층과 풀링 층을 쌓을 수 있다. 상위 층은 원본 입력에서 긴 범위를 보게 될 것이다. 이런 방법은 순서를 감지하기에 부족하다.
    • 온도 예측 문제에 1D 컨브넷을 적용해서 이를 확인해 보자. 이 문제는 순서를 감지해야 좋은 예측을 만들어 낼 수 있다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Conv1D(32, 5, activation='relu', input_shape=(None, float_data.shape[-1])))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=val_gen, validation_steps=val_steps)
  • 다음은 훈련 MAE와 검증 MAE를 보여준다.

  • 검증 MAE는 0.4 대에 머물러 있다. 작은 컨브넷을 사용해서 상식 수준의 기준점을 넘지 못했다. 이는 컨브넷이 입력 시계열에 있는 패턴을 보고 이 패턴의 식나 축의 위치를 고려하지 않기 때문이다.
    • 최근 데이터 포인트일수록 오래된 데이터 포인트와는 다르게 해석해야 하기 때문에 컨브넷이 의미 있는 결과를 만들지 못한다.
    • 이런 컨브넷의 한계는 IMDB 데이터에서는 문제가 되지 않는데, 긍정 또는 부정적인 감성과 연관된 키워드 패턴의 중요성은 입력 시퀀스에 나타난 위치와 무관하기 때문이다.
  • 컨브넷의 속도와 경량함을 RNN의 순서 감지 능력과 결합하는 한 가지 전략은 1D 컨브넷을 RNN 이전에 전처리 단계로 사용하는 것이다.
    • 수천 개의 스텝을 가진 시퀀스 같이 RNN으로 처리하기에는 현실적으로 너무 긴 시퀀스를 다룰 때 특별히 도움이 된다.
    • 컨브넷이 긴 입력 시퀀스를 더 짧은 고수준 특성의 (다운 샘플된) 시퀀스로 변환한다.
    • 추출된 특성의 시퀀스는 RNN 파트의 입력이 된다.

  • 이 기법이 연구 논문이나 실전 애플리케이션에 자주 등장하지는 않는데 아마도 널리 알려지지 않았기 때문일 것이다. 이 방법은 효과적이므로 많이 사용되기를 바란다.
  • 온도 에측 문제에 적용해 보자. 이 전략은 훨씬 긴 시퀀스를 다룰 수 있으므로 더 오래 전 데이터를 바라보거나 시게열 데이터를 더 촘촘히 바라볼 수 있다.
    • 여기서는 그냥 step을 절반으로 줄여서 사용하겠다. 온도 데이터가 30분마다 1포인트씩 샘플링되기 때문에 결과 시계열 데이터는 2배로 길어진다.
step = 3
lookback = 1440
delay = 144

train_gen = generator(float_data, lookback=lookback, delay=delay, min_index=0, max_index=200000, shuffle=True, step=step, batch_size=batch_size)
val_gen = generator(float_data, lookback=lookback, delay=delay, min_index=200001, max_index=300000, shuffle=True, step=step, batch_size=batch_size)
test_gen = generator(float_data, lookback=lookback, delay=delay, min_index=300001, max_index=None, shuffle=True, step=step, batch_size=batch_size)


val_steps = (300000 - 200001 - lookback) // 128
test_steps = (len(float_data) - 300001 - lookback) // 128
  • 이 모델은 2개의 Conv1D 층 다음에 GRU 층을 놓았다.
model = Sequential()
model.add(layers.Conv1D(32, 5, activation='relu', input_shape=(None, float_data.shape[-1])))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GRU(32, dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.summary()

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=val_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 검증 손실로 비교해 보면 이 설정은 규제가 있는 GRU 모델 만큼 좋지는 않다. 하지만 훨씬 빠르기 때문에 데이터를 2배 더 많이 처리할 수 있다. 
    • 여기서는 도움이 많이 되지 않았지만 다른 데이터셋에서는 중요할 수 있다.

정리

  • 2D 컨브넷이 2D 공간의 시각적 패턴을 잘 처리하는 것처럼 1D 컨브넷은 시간에 따른 패턴을 잘 처리한다. 1D 컨브넷은 특정 자연어 처리 같은 일부 문제에 RNN을 대신할 수 있는 빠른 모델이다.
  • 전형적으로 1D 컨브넷은 컴퓨터 비전 분야의 2D 컨브넷과 비슷하게 구성한다. Conv1D 층과 Max-Pooling1D 층을 쌓고 마지막에 전역 풀링 연산이나 Flatten 층을 둔다.
  • RNN으로 아주 긴 시퀀스를 처리하려면 계산 비용이 많이 든다. 1D 컨브넷은 비용이 적게 든다. 따라서 1D 컨브넷을 RNN 이전의 전처리 단계로 사용하는 것은 좋은 생각이다. 시퀀스 길이를 줄이고 RNN이 처리할 유용한 표현을 추출해 줄 것이다.

요약

  • 이 장에서는 다음 기법들을 배웠다. 이 기법들은 텍스트에서 시계열까지 다양한 시쿠너스 데이터셋에 폭넓게 적용할 수 있다.
    • 텍스트를 토큰화 하는 방법
    • 단어 임베딩과 이를 사용하는 방법
    • 순환 네트워크와 이를 사용하는 방법
    • 더 강력한 시퀀스 처리 모델을 만들기 위해 RNN 층을 스태킹 하는 방법과 양방향 RNN을 사용하는 방법
    • 시퀀스를 처리하기 위해 1D 컨브넷을 사용하는 방법
    • 긴 시퀀스를 처리하기 위해 1D 컨브넷과 RNN을 연결하는 방법
  • 시계열 데이터를 사용한 회귀 (미래 값을 예측), 시계열 분류, 시게열에 있는 이상치 감지, 시퀀스 레이블링(문장에서 이름이나 날짜를 식별하기 등)에 RNN을 사용할 수 있다.
  • 비슷하게 기계 번역(SliceNet 같은 시퀀스-투-시퀀스 합성곱 모델), 문서 분류, 맞춤법 정정 등에 1D 컨브넷을 사용할 수 있다.
  • 시퀀스 데이터에서 전반적인 순서가 중요하다면 순환 네트워크를 사용하여 처리하는 것이 좋다. 최근의 정보가 오래된 과거보다 더 중요한 시계열 데이터가 전형적인 경우이다.
  • 전반적인 순서가 큰 의미가 없다면 1D 컨브넷이 적어도 동일한 성능을 내면서 비용도 적을 것이다. 텍스트 데이터가 종종 이에 해당한다. 문장 처음에 있는 키워드가 마지막에 있는 키워드와 같은 의미를 가진다.

현명한 투자자

현명한 투자자

워런 버핏의 스승으로 유명한 밴저민 그레이엄의 책. 가치 투자자의 대부로 꼽히기 때문에 책의 내용은 그에 대한 내용으로 가득차 있다.

기업을 소유한다는 개념으로 투자를 하라는 것이 대전제이고, 그 전제 하에 해야 하는 여러 행동들에 대한 내용과 투자를 전문적으로 하는 사람들이 볼만한 기술적인 내용도 담겨 있다. 그 유명한 안전마진이나 자산 배분(리밸런싱)에 대한 내용도 담겨 있음 –리밸런싱은 정보 이론의 아버지라 불리는 클로드 섀넌의 ‘섀넌의 도깨비’가 최초로 알고 있음

리밸런싱이라는 개념을 떠나면 주식 투자는 결국 미래를 맞추는 것이고 –정확히는 미래에 대한 다른 사람들(시장)의 예측을 예측하는 것– 본질적으로 미래는 참여자들의 상호작용에 의해 만들어지는 것인데, 그 미래를 타이밍까지 정확히 예측한다는 것은 불가능한 것이기 때문에 –지진도 스스로 얼마나 커질지 알 수 없다– 미래를 예측하는 완전한 해법을 찾는 것은 헛된일이다. –설령 일시적으로 완전한 전략이 나와도, 그 전략을 잡아 먹는 전략이 나올 것이기 때문에 완전한 해법이란 있을 수가 없다.

결국 자신의 인생은 자신이 살아가는 것이고 –그 누구도 내 삶을 대신 살아주지는 않는다– 다른 사람의 조언은 조언으로 받아들이고 자신의 길을 가야 한다. 나와 성공한 사람의 삶의 맥락이 다른데, 그 사람이 성공한 방법이 나에게도 통할 수는 없기 때문.

케라스 창시자에게 배우는 딥러닝/ 컴퓨터 비전을 위한 딥러닝

합성곱 신경망 소개

  • 이론적 배경을 알아보기 앞서 2장에서 완전 연결 네트워크로 풀었던 MNIST 숫자 이미지 분류에 컨브넷을 사용해 보자. 기본적인 컨브넷도 완전 연결 모델의 성능을 훨씬 앞지른다.
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3,3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
  • 컨브넷이 (image_height, image_width, image_channels) 크기의 입력 텐서를 사용한다는 점이 중요하다 (배치 차원은 포함하지 않는다.)
    • 이 예제에서는 MNIST 이미지 포맷인 (28, 28, 1) 크기의 입력을 처리하도록 컨브넷을 설정해야 한다. 이 때문에 첫 번째 층의 매개변수로 input_shape(28, 28, 1)을 전달했다.
  • 컨브넷의 구조를 출력해 보자.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  • Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 3D 텐서이다. 높이와 너비 차원은 네트워크가 깊어질수록 작아지는 경향이 있다. 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절된다 (32개 또는 64개)
  • 다음 단계에서 마지막 층의 ((3, 3, 64) 크기인) 출력 텐서를 완전 연결 네트워크에 주입한다. 이 네트워크는 이미 익숙하게 보았던 Dense 층을 쌓은 분류기이다. 이 분류기는 1D 벡터를 처리하는데, 이전 층의 출력이 3D 텐서이다. 그래서 3D 출력을 1D 텐서로 펼쳐야 한다. 그 다음 몇 개의 Dense 층을 추가한다.
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
  • 10개의 클래스를 분류하기 위해 마지막 층의 출력 크기를 10으로 하고 소프트맥스 활성화 함수를 사용한다. 전체 네트워크는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                36928     
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
  • 여기에서 볼 수 있듯이 (3, 3, 64) 출력이 (576,) 크기의 벡터로 펼쳐진 후 Dense 층으로 주입되었다.
  • 이제 MNIST 숫자 이미지에 이 컨브넷을 훈련하자.
from keras.datasets import mnist
from keras.utils import to_categorical

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
  • 테스트 데이터에서 모델을 평가해 보자.
test_loss, test_acc = model.evaluate(test_images, test_labels)
test_acc
---
0.9927999973297119
  • 2장의 완전 연결 네트워크는 97.8%의 테스트 정확도를 얻은 반면, 기본적인 컨브넷은 99.2%의 테스트 정확도를 얻었다.
  • 완전 연결된 모델보다 왜 간단한 컨브넷이 더 잘 작동할까? 이에 대해 알아보기 위해 Conv2D와 MaxPooling2D 층이 어떤 일을 하는지 살펴보자.

합성곱 연산

  • 완전 연결 층과 합성곱 층 사이의 근본적인 차이는 다음과 같다.
    • Dense 층은 입력 특성 공간에 있는 전역 패턴(예컨대 MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만 합성곱 층은 지역 패턴을 학습한다.
    • 이미지일 경우 작은 2D 윈도우로 입력에서 패턴을 찾는다. 앞선 예에서 윈도우는 모두 3×3 크기였다.

  • 이 핵심 특징은 컨브넷에 두 가지 흥미로운 성질을 제공한다.
    • 학습된 패턴은 평행 이동 불변성(translation invariant)을 갖는다. 
      • 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면 다른 곳에서도 이 패턴을 학습할 수 있다. 완전 연결 네트워크는 새로운 위치에 나타난 것을 새로운 패턴으로 학습해야 한다.
      • 이런 성질은 컨브넷이 이미지를 효율적으로 처리하게 만들어진다. (근본적으로 우리가 보는 세상은 평행 이동으로 인해 다르게 인식되지 않는다) 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있다.
      • (회전은 안 되는 것 같다)
    • 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있다.
      • 첫 번째 합성곱 층이 에지 같은 작은 지역 패턴을 학습한다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성되는 더 큰 패턴을 학습하는 식이다.
      • 이런 방식을 이용하여 컨브넷은 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습할 수 있다. (근본적으로 우리가 보는 세상은 공간적 계층 구조를 가진다)
  • 합성곱 연산은 특성 맵(feature map)이라고 부르는 3D 텐서에 적용된다. 이 텐서는 2개의 공간축(높이와 너비)과 깊이 축(채널 축이라고도 한다)으로 구성된다.
    • RGB 이미지는 3개의 컬러 채널을 가지므로 깊이 축의 차원이 3이 되고, MNIST 숫자처럼 흑백이미지는 깊이 축의 차원이 1이다.
    • 합성곱 연산은 입력 특성 맵에서 작은 패치들을 추출하고 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature amp)을 만든다.

  • 출력 특성 맵도 높이와 너비를 가진 3D 텐서이다. 출력 텐서의 깊이는 층의 매개변수로 결정되기 때문에 상황에 따라 다르다.
    • 이렇게 되면 깊이 축의 채널은 더 이상 RGB 입력처럼 특정 컬러를 의미하지 않는다. 그 대신 일종의 필터(filter)를 의미한다.
    • 필터는 입력 데이터의 어떤 특성을 인코딩한다. 예컨대 고수준으로 보면 하나의 필터가 ‘입력에 얼굴이 있는지’를 인코딩 할 수 있다.
  • MNIST 예제에서 첫 번째 합성곱 층이 (28, 28, 1) 크기의 특성 맵을 입력으로 받아 (26, 26, 32) 크기의 특성 맵을 출력한다. 즉 입력에 대해 32개의 필터를 적용한다.
    • 32개의 출력 채널 각각은 26×26 크기의 배열 값을 가진다. 이 값은 입력에 대한 필터의 응답 맵(response map)이다. 입력의 각 위치에서 필터 패턴에 대한 응답을 나타낸다.
    • 특성 맵이란 말이 의미하는 것은 다음과 같다. 깊이 축에 있는 각 차원은 하나의 특성(또는 필터)이고, 2D 텐서 output[:, :, n]은 입력에 대한 이 필터 응답을 나타내는 2D 공간상의 맵이다.

  • 합성곱은 핵심적인 2개의 파라미터로 정의된다.
    • 입력으로부터 뽑아낼 패치의 크기: 전형적으로 3×3 또는 5×5 크기를 사용한다. 이 예에서는 3×3을 사용했다.
    • 특성 맵의 출력 깊이: 합성곱으로 계산할 필터의 수이다. 이 예에서는 깊이 32로 시작해서 깊이 64로 끝났다.
  • 케라스의 Conv2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width)) 처럼 첫 번째와 두 번째 매개변수로 전달된다.
  • 3D 입력 특성 맵 위를 3×3 또는 5×5 크기의 윈도우가 슬라이딩(sliding)하면서 모든 위치에서 3D 특성 패치(window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동한다.
    • 이런 3D 패치는 (output_depth, ) 크기의 1D 벡터로 변환된다. (합성곱 커널 (convolution kernel)이라고 불리는 하나의 학습된 가중치 행렬과의 텐서 곱셈을 통하여 변환된다.)
    • 변환된 모든 벡터는 (height, width, output_depth) 크기의 3D 특성 맵으로 재구성된다. 
    • 출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응된다. (예컨대 출력의 오른쪽 아래 모서리는 입력의 오른쪽 아래 부근에 해당하는 정보를 담고 있다)
    • 3×3 윈도우를 사용하면 3D 패치 input[i-1:i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 만들어진다.
    • 아래 그림에 전체 과정이 표현되어 있다.
      • (입력의 깊이는 2차원인데, 출력의 깊이는 3차원이다)

  • 출력 높이와 너비는 입력의 높이, 너비와 다를 수 있는데, 여기에는 두 가지 이유가 있다.
    • 경계 문제, 입력 특성 맵에 패딩을 추가하여 대응할 수 있다.
    • 잠시 후에 설명할 스트라이드(stride)의 사용 여부에 따라 다르다.

경계 문제와 패딩 이해하기

  • 5×5 크기의 특성 맵을 생각해 보자. (총 25개의 타일이 있다고 가정)
    • 3×3 크기인 윈도우의 중앙을 맞출 수 있는 타일은 3×3 격차를 형성하는 9개 뿐이다. 따라서 출력 특성 맵은 3×3 크기가 된다.
    • 크기가 조금 줄어 들었다. 여기에서 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었다.
    • 앞선 예에서도 이런 경계 문제를 볼 수 있는데, 첫 번째 합성곱 층에서 28×28 크기의 입력이 26×26 크기가 되었다.

  • 입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면 패딩(padding)을 사용할 수 있다.
    • 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가한다. 그래서 모든 입력 타일에 합성곱 윈도우의 중앙을 위치시킬 수 있다.
    • 3×3 윈도우라면 위아래에 하나의 행을 추가하고 오른쪽, 왼쪽에 하나의 열을 추가한다. 5×5 윈도우라면 2개의 행과 열을 추가한다.

  • Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있다. 2개의 값이 가능한데, “valid”는 패딩을 사용하지 않는다는 뜻이고, “same”은 입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다는 뜻이다. 기본값은 “valid”이다.

합성곱 스트라이드 이해하기

  • 출력 크기에 영향을 미치는 다른 요소는 스트라이드이다.
    • 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것이다.
    • 두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터이다. 스트라이드의 기본값은 1이다.
    • 스트라이드가 1보다 큰 스트라이드 합성곱도 가능하다. 아래의 그림은 5×5 크기의 입력에 스트라이드 2를 사용한 3×3 크기의 윈도우로 합성곱하여 추출한 패치를 나타낸다.

  • 스트라이드를 2를 사용했다는 것은 특성 맵의 너비와 높이가 2의 배수로 다운샘플링되었다는 뜻이다. (경계 문제가 있다면 더 줄어든다)
    • 스트라이드 합성곱은 실전에서는 드물게 사용된다. 하지만 어떤 모델에서는 유용하게 사용될 수 있으므로 잘 알아둘 필요가 있다.
  • 특성 맵을 다운샘플링하기 위해 스트라이드 대신 첫 번째 컨브넷 예제에 사용된 최대 풀링(max pooling) 연산을 사용하는 경우가 많으므로 알아보자.

최대 풀링 연산

  • 앞선 컨브넷 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었다.
    • 예컨대 MaxPooling2D 층 이전에 특성 맵의 크기는 26×26이었는데, 최대 풀링 연산으로 13×13으로 줄어들었다.
    • 스트라이드 합성곱과 매우 비슷하게 강제적으로 특성 맵을 다운샘플링하는 것이 최대 풀링의 역할이다.
  • 최대 풀링은 입력 특성 맵에서 윈도우에 맞는 패치를 추출하고 각 채널별로 최댓값을 출력한다.
    • 합성곱과 개념적으로 비슷하지만 추출한 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신 하드코딩된 최댓값 추출 연산을 사용한다.
    • 합성곱과 가장 큰 차이점은 최대 풀링은 보통 2×2 윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 다운샘플링한다는 것이다.
    • 이에 반해 합성곱은 전형적으로 3×3 윈도우와 스트라이드 1을 사용한다.
  • 왜 이런 식으로 특성 맵을 다운샘플링할까? 왜 최대 풀링 층을 빼고 큰 특성 맵을 계속 유지하지 않을까? 이런 방식을 테스트해 보자. 합성곱만으로 이루어진 모델은 다음과 같다.
model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
  • 모델의 구조는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 22, 22, 64)        36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  • 이 설정에서 무엇이 문제일까? 두 가지가 있다.
    • 특성의 공간적 계층 구조를 학습하는데 도움이 되지 않는다. 세 번째 층의 3×3 윈도우는 초기 입력의 7×7 윈도우 영역에 대한 정보만 담고 있다. 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분하지 않을 것이다(7×7 픽셀 크기의 창으로 숫자를 보고 분류해 보라) 마지막 학성곱 층의 특성이 전체 입력에 대한 정보를 가지고 있어야 한다.
    • 최종 특성 맵은 22 x 22 x 64 = 30,976개의 가중치를 가지는데, 아주 많다. 이 컨브넷을 펼친 후 512 크기의 Dense 층과 연결한다면 약 15.8백만 개의 가중치 파라미터가 생긴다. 작은 모델치고 너무 많은 가중치고, 심각한 과대적합이 발생할 것이다.
  • 간단히 말해서 다운샘플링을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서다. 또 연속적인 합성곱 층이 (원본 입력에서 커버된느 영역 측면에서) 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적인 계층 구조를 구성한다.
  • 최대 풀링이 다운샘플링을 할 수 있는 유일한 방법은 아니다. 이미 알고 있듯 앞선 합성곱 층에서 스트라이드를 사용할 수 있다.
    • 최댓값을 취하는 최대 풀링 대신 입력 패치의 채널별 평균값을 계산하여 변환하는 평균 풀링(average pooling)을 사용할 수도 있다.
    • 하지만 최대 풀링이 다른 방법들보다 더 잘 작동하는 편이다. 그 이유는 특성이 특성 맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문이다. (그래서 특성의 지도이다)
    • 따라서 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용하다. 가장 납득할 만한 서브샘플링(subsampling) 전략은 먼저 (스트라이드가 없는 합성곱으로) 조밀한 특성 맵을 만들고 그 다음 작은 패치에 대해 최대로 활성화된 특성을 고르는 것이다.
    • 이런 방법이 입력에 대해 (스트라이드 합성곱으로) 듬성듬성 윈도우를 슬라이드하거나 입력 패치를 평균해서 특성 정보를 놓치거나 희석시키는 것보다 낫다.

소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기

  • 적은 데이터를 이용하여 이미지 분류 모델을 훈련하는 일은 흔한 경우이다. 보통 적은 샘플이란 수백 개에서 수만 개 사이를 의미한다.
    • 실용적인 예제로 4,000개의 강아지와 고양이 사진으로 구성된 데이터셋에서 강아지와 고양이 이미지를 분류해 보자. 2,000개의 사진을 훈련 데이터로, 검증과 테스트 데이터로 1,000개씩 사용하겠다.
  • 문제를 해결하기 위해 기본적인 전략 하나를 살펴보자. 보유한 소규모 데이터셋을 사용하여 처음부터 새로운 모델을 훈련하는 것이다.
    • 2,000개의 훈련 샘플에서 작은 컨브넷을 어떤 규제 방법도 사용하지 않고 훈련하여 기준이 되는 기본 성능을 만들겠다. 이 방법은 71%의 분류 정확도를 달성할 것이다. 이 방법의 주요 이슈는 과대적합이 될 것이다.
    • 그 다음 컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 데이터 증식(data augmentation)을 소개하겠다. 데이터 증식을 통해 네트워크의 성능을 82%로 향상시킬 수 있다.
  • 다음 절에서 작은 데이터셋에 딥러닝을 적용하기 위한 핵심적인 기술 두 가지를 살펴보겠다.
    • 사전 훈련된 네트워크로 특성을 추출하는 것(90%의 정확도를 얻게 된다)과 사전 훈련된 네트워크를 세밀하게 튜닝하는 것(최종 모델은 92% 정확도를 얻게 될 것이다). 
    • 이 세 가지 전략 (처음부터 작은 모델을 훈련하기, 사전 훈련도니 모델을 사용하여 특성 추출하기, 사전 훈련된 모델을 세밀하게 튜닝하기)은 작은 데이터셋에서 이미지 분류 문제를 수행할 때 여러분의 도구 상자에 포함되어 있어야 한다.

작은 데이터셋 문제에서 딥러닝의 타당성

  • 딥러닝은 데이터가 풍부할 때만 작동한다는 말을 듣는데 부분적으로만 맞는 이야기다.
    • 딥러닝의 근본적인 특징은 훈련 데이터에서 특성 공학의 수작업 없이 흥미로운 특성을 찾을 수 있는 것이다. 이는 훈련 샘플이 많아야만 가능하다. 입력 샘플이 이미지처럼 매우 고차원인 문제에서는 특히 그렇다.
  • 하지만 많은 샘플이 의미하는 것은 상대적이다. 우선 훈련하려는 네트워크의 크기와 깊이에 상대적이다. 
    • 복잡한 문제를 푸는 컨브넷을 수십 개의 샘플만 사용해서 훈련하는 것은 불가능하다. 하지만 모델이 작고 규제가 잘 되어 있으며 간단한 작업이라면 수백 개의 샘플로도 충분할 수 있다.
    • 컨브넷은 지역적이고 평행 이동으로 변하지 않는 특성을 학습하기 때문에 지각에 관한 문제에서 매우 효율적으로 데이터를 사용한다.
    • 매우 작은 데이터셋에서 어떤 종류의 특성 공학을 사용하지 않고 컨브넷을 처음부터 훈련해도 납득할만한 결과를 만들 수 있다.
  • 거기에 더해 딥러닝 모델은 태생적으로 매우 다목적이다. 말하자면 대규모 데이터셋에서 훈련시킨 이미지 분류 모델이나 스피치-투-텍스트(speech-to-text) 모델을 조금만 변경해서 완전히 다른 문제에 재사용할 수 있다.
    • 특히 컴퓨터 비전에서는 (보통 ImageNet 데이터셋에서 훈련 된) 사전 훈련된 모델들이 내려받을 수 있도록 많이 공개되어 있어서 매우 적은 데이터에서 강력한 비전 모델을 만드는데 사용할 수 있다.

데이터 내려받기

  • 여기서 사용할 강아지, 고양이 데이터셋은 케라스에 포함되어 있지 않고 캐글에 있기 때문에 캐글 계정이 필요하다. 다음 주소에서 데이터를 내려 받자.

  • 내려 받고 압축 해제 한 원본 데이터 파일을 다음 코드를 이용해서 분리한다.
import os, shutil

original_dataset_dir = './datasets/cats_and_dogs/train/' #원본 데이터셋 경로
base_dir = '.datasets/cats_and_dogs/cats_and_dogs_small/'
os.mkdir(base_dir)

train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

os.mkdir(train_dir)
os.mkdir(validation_dir)
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, 'cats')
train_dogs_dir = os.path.join(train_dir, 'dogs')

validation_cats_dir = os.path.join(validation_dir, 'cats')
validation_dogs_dir = os.path.join(validation_dir, 'dogs')

test_cats_dir = os.path.join(test_dir, 'cats')
test_dogs_dir = os.path.join(test_dir, 'dogs')

os.mkdir(train_cats_dir)
os.mkdir(train_dogs_dir)
os.mkdir(validation_cats_dir)
os.mkdir(validation_dogs_dir)
os.mkdir(test_cats_dir)
os.mkdir(test_dogs_dir)

for fname in ['cat.{}.jpg'.format(i) for i in range(1000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)

네트워크 구성하기

  • 이전보다 이미지가 크고 복잡하기 때문에 네트워크를 좀 더 만들겠다. Conv2D + MaxPooling2D 단계를 하나 더 추가한다.
    • 이렇게 하면 네트워크의 용량을 늘리고 Flatten 층의 크기가 너무 커지지 않도록 특성 맵의 크기를 줄일 수 있다.
    • 150×150 크기의 입력으로 싲가해서 Flatten 층 이전에 7×7 크기의 특성 맵으로 줄어든다.
  • 이진 분류 문제이므로 네트워크는 하나의 유닛(크기가 1인 Dense 층)과 sigmoid 활성화 함수로 끝난다. 이 유닛은 한 클래스에 대한 확률을 인코딩할 것이다.
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 층들을 거치면서 특성 맵의 차원이 어떻게 변하는지 살펴보자
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_5 (Conv2D)            (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_8 (MaxPooling2 (None, 7, 7, 128)         0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 6272)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 512)               3211776   
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
  • 컴파일 단계에서 이전과 같이 RMSprop 옵티마이저를 선택하겠다. 네트워크의 마지막이 하나의 시그모이드 유닛이기 때문에 이진 크로스엔트로피를 손실로 사용한다.
from keras import optimizers

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])

데이터 전처리

  • 데이터는 네트워크에 주입되기 전에 부동 소수 타입의 텐서로 전처리 되어 있어야 한다. 지금은 데이터가 JPEG 파일로 되어 있으므로 네트워크에 주입하려면 다음 과정을 따라야 한다.
    1. 사진 파일을 읽는다.
    2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩한다.
    3. 그 다음 부동 소수 타입의 텐서로 변환한다.
    4. 픽셀 값(0-255)의 스케일을 [0, 1] 사이로 조정한다.
  • 케라스에는 이런 단계를 자동으로 처리하는 유틸리티가 있다. 또 케라스에는 keras.preprocessing.image에 이미지 처리를 위한 헬퍼 도구들도 있다. 특히 ImageDataGenerator는 디스크에 있는 이미지 파일을 전처리된 배치 텐서로 자동으로 바꾸어 주는 파이썬 제너레이터를 만들어 준다.
from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=20, class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=20, class_mode='binary')
  • 이 제너레이터의 출력은 150×150 RGB 이미지의 배치((20, 150, 150, 3) 크기)와 이진 레이블의 배치 ((20, ) 크기)이다. 각 배치에는 20개의 샘플(배치 크기)이 있다. 
    • 제너레이터는 이 배치를 무한정 만들어낸다. 타깃 폴더에 있는 이미지를 끝없이 반복하기 때문. 따라서 반복 루프 안 어디에선가 break 문을 사용해야 한다.
  • 제너레이터를 사용한 데이터에 모델을 훈련시켜 보겠다.
    • fit_generator 메서는 fit 메서드와 동일하되 데이터 제너레이터를 사용할 수 있다. 이 메서드는 첫 번째 매개변수로 입력과 타깃의 배치를 끝없이 반환하는 파이썬 제너레이터를 기대한다.
    • 데이터가 끝없이 생성되기 때문에 케라스 모델에 하나의 에포크를 정의하기 위해 제너레이터로부터 얼마나 많은 샘플을 뽑을 것인지 알려주어야 한다. steps_per_epoch 매개변수에서 이를 설정한다.
    • steps_per_epoch개의 배치만큼 뽑은 후, 즉 steps_per_epoch 횟수만큼 경사 하강법 단계를 실행한 후에 훈련 프로세스는 다음 에포크로 넘어간다.
    • 여기서는 20개의 샘플이 하나의 배치이므로 2,000개의 샘플을 모두 처리할 때까지 100개의 배치를 뽑을 것이다.
  • fit_generator를 사용할 때 fit 메서드와 마찬가지로 validation_data 매개변수를 전달할 수 있다. 이 매개변수에는 데이터 제너레이터도 가능하지만 넘파이 배열의 튜플도 가능하다.
    • validation_data로 제너레이터를 전달하면 검증 데이터의 배치를 끝없이 반환하기 때문에 검증 데이터 제너레이터에서 얼마나 많은 배치를 추출하여 평가할지 validation_steps 매개변수에 지정해야 한다.
history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator, validation_steps=50)
  • 훈련이 끝나면 항상 모델을 저장하는 습관을 갖자
model.save('cats_and_dogs_samll_1.h5')
  • 훈련 데이터와 검증 데이터에 대한 모델의 손실과 정확도를 그래프로 나타내보자
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

  • 이 그래프는 과대적합의 특성을 보여준다.
    • 훈련 정확도가 시간이 지남에 따라 선형적으로 증가해서 거의 100%에 도달하는 반면 검증 정확도는 70-72% 에서 멈추었다.
    • 검증 손실은 다섯 번의 에포크만에 최솟값에 다다른 후 더 진전되지 않았고 훈련 손실은 거의 0에 도달할 때까지 선형적으로 계속 감소한다.
  • 비교적 훈련 샘플의 수(2,000개)가 적기 때문에 과대적합이 가장 중요한 문제이다.
    • 드롭아웃이나 가중치 감소(L2 규제)처럼 과대적합을 감소시킬 수 있는 여러 기법들을 배웠다. 여기서는 컴퓨터 비전에 특화되어 있어서 딥러닝으로 이미지를 다룰 때 일반적으로 사용되는 새로운 방법인 데이터 증식을 시도해 보겠다.

데이터 증식 사용하기

  • 과대적합은 학습할 샘플이 너무 적어 새로운 데이터에 일반화할 수 있는 모델을 훈련시킬 수 없기 때문에 발생한다. 무한히 많은 데이터가 주어지면 데이터 분포의 모든 가능한 측면을 모델이 학습할 수 있을 것이다.
  • 데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법으로 이 방법은 그럴듯한 이미지를 생성하도록 여러 가지 랜덤한 변환을 적용하여 샘플을 늘린다.
    • 훈련할 때 모델이 정확히 같은 데이터를 두 번 만나지 않도록 하는 것이 목표이다. 모델이 데이터의 여러 측면을 학습하면 일반화에 도움이 될 것이다.
  • 케라스에서는 ImageDataGenerator가 읽은 이미지에 여러 종류의 랜덤 변환을 적용하도록 설정할 수 있다.
datagen = ImageDataGenerator(rotation_range=20, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.1, zoom_range=0.1, horizontal_flip=True, fill_mode='nearest')
  • 추가적인 매개변수가 몇 가 더 있다. 이 코드를 살펴보면 다음과 같다.
    • rotation_range는 랜덤하게 사진을 회전시킬 각도 범위이다. (0-180 사이)
    • width_shift_range와 height_shift_range는 사진을 수평과 수직으로 랜덤하게 평행 이동 시킬 범위이다.
    • shear_range는 랜덤하게 전단 변환(shearing transformation)을 적용할 각도 범위이다.
    • zoom_range는 랜덤하게 사진을 확대할 범위이다.
    • horizontal_flip은 랜덤하게 이미지를 수평으로 뒤집는다. 수평 대칭을 가정할 수 있을 때 사용한다. (풍경/ 인물 사진 등)
    • fill_mode는 회전이나 가로/세로 이동으로 인해 새롭게 생성해야 할 픽셀을 채울 전략이다.
  • 증식된 이미지 샘플은 아래 그림과 같다.

  • 데이터 증식을 사용하여 새로운 네트워크를 훈련시킬 때 네트워크에 같은 입력 데이터가 두 번 주입되지 않는다. 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에 여전히 입력 데이터들 사이에 상호 연관성이 크다.
    • 즉 새로운 정보를 만들어 낼 수 없고 단지 기존 정보의 재조합만 가능하다. 그렇기 때문에 완전히 과대적합을 제거하기에 충분하지 않을 수 있다.
    • 과대적합을 더 억제하기 위해 완전 연결 분류기 직전에 Dropout 층을 추가하겠다.
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
  • 데이터 증식과 드롭아웃을 사용하여 이 네트워크를 훈련시켜 보자.
train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=32, class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=32, class_mode='binary')

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
  • 모델을 저장하자.
model.save('cats_and_dogs_samll_2.h5')
  • 결과 그래프를 다시 그려보자. 데이터 증식과 드롭아웃 덕분에 더는 과대적합되지 않는다. 훈련 곡선이 검증 곡선에 가깝꼐 따라가고 있다.
    • 검증 데이터에서 82% 정확도를 달성했는데, 규제하지 않은 모델과 비교했을 때 15% 정도 향상되었다.

  • 다른 규제 기법을 더 사용하고 네트워크의 파라미터를 튜닝하면 86%나 87% 정도까지 더 높은 정확도를 얻을 수 있다.
    • 하지만 데이터가 적기 때문에 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성하기는 어렵다.
    • 이런 상황에서 정확도를 높이기 위한 다음 단계는 사전 훈련된 모델을 사용하는 것이다.

사전 훈련된 컨브넷 사용하기

  • 작은 이미지 데이터셋에 딥러닝을 적용하는 일반적이고 매우 효과적인 방법은 사전 훈련된 네트워크를 사용하는 것이다.
    • 사전 훈련된 네트워크(pretrained network)는 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련되어 저장된 네트워크이다.
    • 원본 데이터셋이 충분히 크고 일반적이라면 사전 훈련된 네트워크에 의해 학습된 특성의 계층 구조는 실제 세상에 대한 일반적인 모델로 효율적인 역할을 할 수 있다. 새로운 문제가 원래 작업과 완전히 다른 클래스에 대한 것이라도 이런 특성은 많은 컴퓨터 비전 문제에 유용하다.
    • 예컨대 대부분 동물이나 생활 용품으로 이루어진 ImageNet 데이터셋에 네트워크를 훈련한다. 그 다음 이 네트워크를 이미지에서 가구 아이템을 식별하는 것 같은 다른 용도로 사용할 수 있다.
    • 학습된 특성을 다른 문제에 적용할 수 있는 이런 유연성은 이전의 많은 얕은 학습 방법과 비교했을 때 딥러닝의 핵심 장점이다. 이런 방식으로 작은 데이터셋을 가진 문제에도 딥러닝이 효율적으로 작동할 수 있다.
  • 여기서는 ImageNet 데이터셋에서 훈련된 대규모 컨브넷을 사용해 보겠다. 캐런 시몬연(Karen Simonyan)과 앤드류 지서먼(Andrew Zisserman)이 2014년 개발한 VGG16 구조를 사용하겠다.
    • VGG16은 조금 오래되었고 최고 수준의 성능에는 못 미치며 최근의 다른 모델보다는 조금 무겁다. 하지만 이 모델의 구조기 이전에 보았던 것과 비슷해서 선택했다.
    • 아마 VGG가 처음 보는 모델 애칭일지 모르겠으나 이런 이름에는 VGG, ResNet, Inception, Inception_ResNet, Xception 등이 있다. 컴퓨터 비전을 위해 딥러닝을 계속 공부하다 보면 이런 이름을 자주 만나게 될 것이다.
  • 사전 훈련된 네트워크를 사용하는 두 가지 방법이 있다. 특성 추출(feature extraction)과 미세 조정(fine tuning)이다.

특성 추출

  • 특성 추출은 사전에 학습된 네트워크의 표현을 사용하여 새로운 샘플에서 흥미로운 특성을 뽑아내는 것이다. 이런 특성을 사용하여 새로운 분류기를 처음부터 훈련한다.
  • 앞서 보앗듯이 컨브넷은 이미지 분류를 위해 두 부분으로 구성된다. 먼저 연속된 합성곱과 풀링 층으로 시작해서 완전 연결 분류기로 끝난다.
    • 첫 번째 부분을 모델의 합성곱 기반 층(convolutional base)이라 부르겠다.
    • 컨브넷의 경우 특성 추출은 사전에 훈련된 네트워크의 합성곱 기반 층을 선택하여 새로운 데이터를 통과시키고, 그 출력으로 새로운 분류기를 훈련한다.

  • 왜 합성곱 층만 재사용할까? 완전 연결 분류기도 재사용할 수 있을까?
    • 일반적으로 권장하지 않는데, 합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용이 가능하기 때문이다.
    • 컨브넷의 특성 맵은 사진에 대한 일반적인 콘셉트의 존재 여부를 기록한 맵이다. 주어진 컴퓨터 비전 문제에 상관없이 유용하게 사용할 수 있다.
    • 하지만 분류기에서 학습한 표현은 모델이 훈련된 클래스 집합에 특화되어 있다. 분류기는 전체 사진에 어떤 클래스가 존재할 확률에 관한 정보만 담고 있다.
    • 더군다나 완전 연결 층에서 찾은 표현은 더 이상 입력 이미지에 있는 객체의 위치 정보를 가지고 있지 않다. 완전 연결 층들은 공간 개념을 제거하지만 합성곱의 특성맵은 객체 위치를 고려한다. 객체 위치가 중요한 문제라면 완전 연결 층에서 만든 특성느 크게 쓸모가 없다.
  • 특정 합성곱 층에서 추출한 표현의 일반성(그리고 재사용성) 수준은 모델에 있는 층의 깊이에 달려 있다.
    • 모델의 하위 층은(에지, 색깔, 질감 등) 지역적이고 매우 일반적인 특성 맵을 추출한다. 반면 상위 층은 (강아지 눈, 고양이 귀 처럼) 좀 더 추상적인 개념을 추출한다.
    • 새로운 데이터셋이 원본 모델이 훈련한 데이터셋과 많이 다르다면 전체 합성곱 기반 층을 사용하는 것보다는 모델의 하위 층 몇 개만 특성 추출에 사용하는 것이 좋다.
  • ImageNet의 클래스 집합에는 여러 종류의 강아지와 고양이를 포함하고 있다. 이런 경우 원본 모델의 완전 연결 층에 있는 정보를 재사용하는 것이 도움이 될 것이다.
    • 하지만 새로운 문제의 클래스가 원본 모델의 클래스 집합과 겹치지 않는 좀 더 일반적인 경우를 다루기 위해 여기서는 완전 연결 층을 사용하지 않겠다.
    • ImageNet 데이터셋에 훈련된 VGG16 네트워크의 합성곱 기반 층을 사용하여 강아지와 고양이 이미지에 유용한 특성을 추출해 보겠다. 그런 다음 이 특성으로 강아지 vs 고양이 분류기를 훈련한다.
  • VGG16 모델은 케라스 패키지로 포함되어 있다. keras.applications 모듈에서 임포트 할 수 있는데, keras.applications 모듈에서 사용 가능한 이미지 분류 모델은 다음과 같다. (모두 ImageNet 데이터셋에서 훈련되었다.)
    • Xception
    • Inception V3
    • ResNet50
    • VGG16
    • VGG19
    • MobileNet
  • VGG16 모델을 만들어 보자.
from keras.application import VGG16

conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
  • VGG 함수에 3개의 매개 변수를 전달한다.
    • weights는 모델을 초기화할 가중치 체크포인트를 지정한다.
    • include_top은 네트워크의 최상위 완전 연결 분류기를 포함할지 안 할지 지정한다. 기본값은 ImageNet의 클래스 1000개에 대응되는 완전 연결 분류기를 포함한다. 별도의 완전 연결층을 추가하려 하므로 이를 포함시키지 않는다.
    • input_shape은 네트워크에 주입할 이미지 텐서의 크기이다. 이 매개변수는 선택사항인데, 이 값을 지정하지 않으면 네트워크가 어떤 크기의 입력도 처리할 수 있다.
  • 다음은 VGG16 합성곱 기반 층의 자세한 구조이다.
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 150, 150, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 150, 150, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 150, 150, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 75, 75, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 75, 75, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 75, 75, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 37, 37, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 37, 37, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 18, 18, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 18, 18, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 9, 9, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
  • 최종 특성 맵의 크기는 (4, 4, 512)이다. 이 특성 위에 완전 연결 층을 놓을 것이다.) 이 지점에서 두 가지 새로운 방식이 가능하다.
    • 새로운 데이터셋에서 합성곱 기반 층을 실행하고 출력을 넘파이 배열로 디스크에 저장한다. 그 다음 이 데이터를 독립된 완전 연결 분류기에 입력으로 사용한다. 합성곱 연산은 전체 과정 중에서 가장 비싼 부분이다. 이 방식은 모든 입력 이미지에 대해 합성곱 기반 층을 한 번만 실행하면 되기 때문에 빠르고 비용이 적게 든다. 하지만 이런 이유로 이 기법에는 데이터 증식을 사용할 수 없다.
    • 준비한 모델(conv_base) 위에 Dense 층을 쌓아 확장한다. 그 다음 입력 데이터에서 엔드-투-엔드로 전체 모델을 실행한다. 모델에 노출된 모든 입력 이미지가 매번 합성곱 기반층을 통과하기 때문에 데이터 증식을 사용할 수 있다. 하지만 이런 이유로 첫 번째 방식보다 훨씬 많은 비용이 든다.

데이터 증식을 사용하지 않는 빠른 특성 추출

  • ImageDataGenerator를 이용해서 이미지와 레이블을 넘파이 배열로 추출하자. conv_base 모델의 predict 메서드를 호출하여 이 이미지에서 특성을 추출한다.
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = 'E:/Study/Keras Deep Learning/datasets/cats_and_dogs/cats_and_dogs_small/'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

def extract_features(directory, sample_count):
features = np.zeros(shape=(sample_count, 4, 4, 512))
labels = np.zeros(shape=(sample_count))
generator = datagen.flow_from_directory(directory, target_size=(150,150), batch_size=batch_size, class_mode='binary')

i = 0
for inputs_batch, labels_batch in generator:
features_batch = conv_base.predict(inputs_batch)
features[i*batch_size: (i+1) * batch_size] = features_batch
labels[i*batch_size: (i+1) * batch_size] = labels_batch
i+=1

if i * batch_size >= sample_count:
break

return features, labels

train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(train_dir, 1000)
  • 추출된 특성의 크기는 (samples, 4, 4, 512)이다. 완전 연결 분류기에 주입하기 위해 먼저 (samples, 8192) 크기로 펼친다.
train_features = np.reshape(train_features, (2000, 4*4*512))
validation_features = np.reshape(validation_features, (1000, 4*4*512))
test_features = np.reshape(test_features, (1000, 4*4*512))
  • 그 후에 완전 연결 분류기를 정의하고 저장된 데이터와 레이블을 사용하여 훈련한다.
from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4*4*512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5), loss='binary_crossentropy', metrics=['acc'])

history = model.fit(train_features, train_labels, epochs=30, batch_size=20, validation_data=(validation_features, validation_labels))
  • 2개의 Dense 층만 처리하면 되기 때문에 훈련이 매우 빠르다. 
  • 훈련 손실과 정확도 곡선을 살펴보자.
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

  • 약 90%의 검증 정확도에 도달했다. 이전 절에서 처음부터 훈련시킨 작은 모델에서 얻은 것보다 훨씬 좋다. 
    • 하지만 이 그래프는 많은 비율로 드롭아웃을 사용했음에도 훈련을 시작하면서 거의 바로 과대적합되고 있다는 것을 보여준다. 작은 이미지 데이터셋에서는 과대적합을 막기 위해 필수적인 데이터 증식을 사용하지 않았기 때문이다.

데이터 증식을 사용한 특성 추출

  • 이 방법은 훨씬 느리고 비용이 많이 들지만 훈련하는 동안 데이터 증식 기법을 사용할 수 있다. conv_base 모델을 확장하고 입력 데이터를 사용하여 엔드-투-엔드로 실행한다.
  • 모델은 층과 동일하게 작동하므로 층을 추가하듯이 Sequential 모델에 다른 모델을 추가할 수 있다.
from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 이 모델의 구조는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 8192)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 256)               2097408   
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 257       
=================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0
  • 여기서 볼 수 있듯이 VGG16의 합성곱 기반 층은 14,714,688개의 매우 많은 파라미터를 갖고 있다. 합성곱 기반 층 위에 추가한 분류기는 200만개의 파라미터를 가진다.
  • 모델을 컴파일 하고 훈련하기 전에는 합성곱 기반 층을 동결하는 것이 아주 중요하다. 하나 이상의 층을 동결(freezing) 한다는 것은 훈련하는 동안 가중치가 업데이트되지 않도록 막는다는 뜻이다.
    • 이렇게 하지 않으면 합성곱 기반 층에 의해 사전에 학습된 표현이 훈련하는 동안 수정될 것이다.
    • 맨 위의 Dense 층은 랜덤하게 초기화되었기 때문에 매우 큰 가중치 업데이트 값이 네트워크에 전파될 것이다. 이는 사전에 학습된 표현을 크게 훼손하게 된다.
  • 케라스에서는 trainable 속성을 False로 설정하여 네트워크를 동결할 수 있다.
print('conv_base를 동결하기 전 훈련되는 가중치의 수: ', len(model.trainable_weights))
conv_base.trainable = False
print('conv_base를 동결한 후 훈련되는 가중치의 수: ', len(model.trainable_weights))
---
conv_base를 동결하기 전 훈련되는 가중치의 수: 30 conv_base를 동결한 후 훈련되는 가중치의 수: 4
  • 이렇게 설정하면 추가한 2개의 Dense 층 가중치만 훈련될 것이다. 층마다 2개씩 총 4개의 텐서가 훈련된다.
    • 변경 사항을 적용하려면 먼저 모델을 컴파일 해야 한다. 컴파일 단계 후에 trainable 속성을 변경하면 반드시 모델을 다시 컴파일해야 한다. 그렇지 않으면 변경 사항이 적용되지 않는다.
  • 앞서 사용했던 데이터 증식을 사용하여 모델 훈련을 시작할 수 있다.
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest')

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=20, class_mode='binary')

validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=20, class_mode='binary')

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=2e-5), metrics=['acc'])

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator, validation_steps=50, verbose=2)
  • 결과 그래프는 다음과 같다. 검증 정확도가 이전과 비슷하지만 처음부터 훈련시킨 소규모 컨브넷보다 과대적합이 줄었다.

미세 조정

  • 미세 조정(fine tuning)은 특성 추출에 사용했던 동결 모델의 상위 층 몇 개를 동결에서 해제하고 모델에 새로 추가한 층(여기서는 완전 연결 분류기)과 함께 훈련하는 것이다.
    • 주어진 문제에 조금 더 밀접하게 재사용 모델의 표현을 일부 조정하기 때문에 미세 조정이라고 한다.

  • 앞서 랜덤하게 초기화된 상단 분류기를 훈련하기 위해 VGG16의 합성곱 기반 층을 동결해야 한다고 했다. 같은 이유로 맨 위에 있는 분류기가 훈련된 후에 합성곱 기반의 상위 층을 미세 조정할 수 있다.
    • 분류기가 미리 훈련되지 않으면 훈련되는 동안 너무 큰 오차 신호가 네트워크에 전파된다. 이는 미세 조정될 층들이 사전에 학습한 표현을 망가뜨리게 될 것이다.
  • 미세 조정하는 단계는 다음과 같다.
    • 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가한다.
    • 기반 네트워크를 동결한다.
    • 새로 추가한 네트워크를 훈련한다.
    • 기반 네트워크에서 일부 층의 동결을 해제한다.
    • 동결을 해제한 층과 새로 추가한 층을 함께 훈련한다.
  • 처음 세 단계는 특성 추출을 할 때 이미 완료했다. 네 번째 단계를 진행해 보자. conv_base의 동결을 해제하고 개별 층을 동결하겠다.
    • 앞서 보았던 합성곱 기반 층의 구조를 다시 확인해 보자.
Layer (type) Output Shape Param # ================================================================= 
input_1 (InputLayer) (None, 150, 150, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 150, 150, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 150, 150, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 75, 75, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 75, 75, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 75, 75, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 37, 37, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 37, 37, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 37, 37, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 37, 37, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 18, 18, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 9, 9, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 4, 4, 512) 0 =================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
  • 마지막 3개의 합성곱 층을 미세 조정하겠다. 즉 block4_pool까지 모든 층은 동결되고 block5_conv1, block5_conv2, block5_conv3 층은 학습 대상이 된다.
  • 왜 더 많은 층을 미세 조정하지 않을까? 그렇게 할 수도 있지만 다음 사항을 고려해야 한다.
    • 합성곱 기반 층에 있는 하위 층들을 좀 더 일반적이고 재사용 가능한 특성들을 인코딩한다. 반면 상위 층은 좀 더 특화된 특성을 인코딩 한다. 새로운 문제에 재활용하도록 수정이 필요한 것은 구체적인 특성이므로 이들을 미세 조정하는 것이 유리하다. 하위 층으로 갈수록 미세 조정에 대한 효과가 감소한다.
    • 훈련해야 할 파라미터가 많을수록 과대적합의 위험이 커진다. 합성곱 기반 층은 1,500만개의 파라미터를 가지고 있으므로 작은 데이터셋으로 전부 훈련하려고 하면 매우 위험하다.
  • 앞선 예제 코드에 이어 미세 조정을 해보자.
conv_base.trainable = True

set_trainable = False

for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
  • 이제 네트워크의 미세 조저을 시작해보자. 학습률을 낮춘 RMSProp 옵티마이저를 사용한다. 학습률을 낮추는 이유는 미세 조정하는 3개의 층에서 학습된 표현을 조금씩 수정하기 위해서다. 변경량이 너무 크면 학습된 표현에 나쁜 영향을 끼칠 수 있다.
model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-5), metrics=['acc'])

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
  • 이전과 동일한 코드로 결과를 그려보면 다음과 같다.

  • 그래프가 불규칙하게 보이는데, 그래프를 보기 쉽게 하기 위해 지수 이동 평균(exponential moving averages)으로 정확도와 손실 값을 부드럽게 표현할 수 있다.
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1-factor))
else:
smoothed_points.append(point)
return smoothed_points

plt.plot(epochs, smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs, smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs, smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

  • 검증 정확도 곡선이 훨씬 깨끗하게 보인다. 정확도가 대략 1% 이상 향상되었다.
  • 손실 곡선은 실제 어떤 향상을 얻지 못했다(사실 악화되었다). 손실이 감소되지 않았는데 어떻게 정확도가 안정되거나 향상될 수 있었을까?
    • 답은 간단한데, 그래프는 개별적인 손실 값의 평균을 그린 것이다. 하지만 정확도에 영향을 미치는 것은 손실 값의 분포이지 평균이 아닌다.
    • 정확도는 모델이 예측한 클래스 확률이 어떤 임계 값을 넘었는지에 대한 결과이기 때문이다. 모델이 더 향상되더라도 평균 손실에 반영되지 않을 수 있다.
  • 마지막으로 테스트 데이터에서 이 모델을 평가해보자.
test_generator = test_datagen.flow_from_directory(test_dir, target_size=(150, 150), batch_size=20, class_mode='binary')

test_loss, test_acc = model.evaludate_generator(test_generator, steps=50)
print('test acc:', test_acc)
  • 92%의 테스트 정확도를 얻을 것이다. 이 데이터셋을 사용한 원래 캐글 경연 대회에서 꽤 높은 결과이다. 하지만 최신 딥러닝 기법으로 훈련 데이터의 일부분(약 10%)만 사용해서 이런 결과를 달성했다.

정리

  • 컨브넷은 컴퓨터 비전 작업에 가장 뛰어난 머신 러닝 모델이다. 아주 작은 데이터셋에서도 처음부터 훈련해서 괜찮은 성능을 낼 수 있다.
  • 작은 데이터셋에서는 과대적합이 큰 문제이다. 데이터 증식은 이미지 데이터를 다룰 때 과대적합을 막을 수 있는 강력한 방법이다.
  • 특성 추출 방식으로 새로운 데이터셋에 기존 컨브넷을 쉽게 재사용할 수 있다. 작은 이미지 데이터셋으로 작업할 때 효과적인 기법이다.
  • 특성 추출을 보완하기 위해 미세 조정을 사용할 수 있다. 미세 조정은 기존 모델에서 사전에 학습한 표현의 일부를 새로운 문제에 적응시킨다. 이 기법은 조금 더 성능을 끌어올린다.

컨브넷 학습 시각화

  • 딥러닝 모델을 ‘블랙 박스(black box)’ 같다고 자주 이야기하는데, 학습된 표현에서 사람이 이해하기 쉬운 형태를 뽑아내거나 제시하기 어렵기 때문이다.
    • 일부 딥러닝 모델에서는 이 말이 어느 정도 맞지만 컨브넷에서는 전혀 아니다. 컨브넷에서는 시각적인 개념을 학습한 것이기 때문에 시각화하기 아주 좋다.
  • 컨브넷의 표현을 시각화하고 해석하는 기법들은 다음과 같다.
    • 컨브넷 중간층의 출력(중간층에 있는 활성화)을 시각화하기: 연속된 컨브넷 층이 입력을 어떻게 변형시키는지 이해하고 개별적인 컨브넷 필터의 의미를 파악하는데 도움이 된다.
    • 컨브넷 필터를 시각화하기: 컨브넷의 필터가 찾으려는 시각적인 패턴과 개념이 무엇인지 상세하게 이해하는데 도움이 된다.
    • 클래스 활성화에 대한 히트맵(heatmap)을 이미지에 시각화하기: 이미지의 어느 부분이 주어진 클래스에 속하는데 기여했는지 이해하고 이미지에서 객체 위치를 추정(localization)하는데 도움이 된다.

중간층의 활성화 시각화하기

  • 중간층의 활성화 시각화는 어떤 입력이 주어졌을 때 네트워크에 있는 여러 합성곱과 풀링 층이 출력하는 특성 맵을 그리는 것이다. (층의 출력이 활성화 함수의 출력이라서 종종 활성화(activation)라고 부른다.)
    • 이 방법은 네트워크에 의해 학습된 필터들이 어떻게 입력을 분해하는지 보여준다.
    • 너비, 높이, 깊이(채널) 3개의 차원에 대해 특성 맵을 시각화하는 것이 좋다.
    • 각 채널은 비교적 독립적인 특성을 인코딩하므로 특성 맵의 각 채널 내용을 독립적인 2D 이미지로 그리는 것이 괜찮은 방법이다.
  • 앞서 만들었던 모델을 로드해서 시작해 보자.
from keras.models import load_model

model = load_model('cats_and_dogs_small_2.h5')
model.summary()

------
Layer (type) Output Shape Param # ================================================================= conv2d_9 (Conv2D) (None, 148, 148, 32) 896 _________________________________________________________________ max_pooling2d_9 (MaxPooling2 (None, 74, 74, 32) 0 _________________________________________________________________ conv2d_10 (Conv2D) (None, 72, 72, 64) 18496 _________________________________________________________________ max_pooling2d_10 (MaxPooling (None, 36, 36, 64) 0 _________________________________________________________________ conv2d_11 (Conv2D) (None, 34, 34, 128) 73856 _________________________________________________________________ max_pooling2d_11 (MaxPooling (None, 17, 17, 128) 0 _________________________________________________________________ conv2d_12 (Conv2D) (None, 15, 15, 128) 147584 _________________________________________________________________ max_pooling2d_12 (MaxPooling (None, 7, 7, 128) 0 _________________________________________________________________ flatten_3 (Flatten) (None, 6272) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 6272) 0 _________________________________________________________________ dense_5 (Dense) (None, 512) 3211776 _________________________________________________________________ dense_6 (Dense) (None, 1) 513 ================================================================= Total params: 3,453,121 Trainable params: 3,453,121 Non-trainable params: 0
  • 그 다음 네트워크를 훈련할 때 사용했던 이미지에 포함되지 않은 고양이 사진 하나를 입력 이미지로 선택한다.
img_path = '.datasets/cats_and_dogs/cats_and_dogs_small/test/cats/cat.1700.jpg'

from keras.preprocessing import image
import numpy as np

img = image.load_img(img_path, target_size=(150,150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.

print(img_tensor.shape)
  • 사진을 출력해 보자
import matplotlib.pyplot as plt

plt.imshow(img_tensor[0])
plt.show()

  • 확인하고 싶은 특성 맵을 추출하기 위해 이미지 배치를 입력으로 받아 모든 합성곱과 출링 층의 활성화를 출력하는 케라스 모델을 만들자. 
    • 이를 위해 케라스의 Model 클래스를 사용하겠다. 모델 객체를 만들 때 2개의 매개변수가 필요한데, 입력 텐서(또는 입력 텐서의 리스트)와 출력 텐서(또는 출력 텐서의 리스트)이다.
    • 반환되는 객체는 Sequential과 같은 케라스 모델이지만 특정 입력과 특정 출력을 매핑한다.
    • Model 클래스를 사용하면 Sequential과는 달리 여러 개의 출력을 가진 모델을 만들 수 있다.
from keras import models

layer_outputs = [layer.output for layer in model.layers[:8]] --상위 8개 층의 출력을 추출한다.
activation_model = models.Model(inputs=model.input, outputs=layer_outputs) --입력에 대해 8개 층의 출력을 반환하는 모델을 만든다.
  • 입력 이미지가 주입될 때 이 모델은 원본 모델의 활성화 값을 반환한다. 이 모델이 이 책에서는 처음 나오는 다층 출력 모델이다.
    • 지금까지 본 모델은 정확히 하나의 입력과 하나의 출력만을 가졌는는데, 일반적으로 모델은 몇 개의 입력과 출력이라도 가질 수 있다.
    • 이 모델은 입력과 층의 활성화마다 하나씩 총 8개의 출력을 가진다.
activations = activation_model.predict(img_tensor)  --층의 활성화마다 하나씩 8개의 넘파이 배열로 이루어진 리스트를 반환한다.
  • 예컨대 다음이 고양이 이미지에 대한 첫 번째 합성곱 층의 활성화 값이다.
first_layer_activation = activations[0]
print(first_layer_activation.shape)
----
(1, 148, 148, 32)
  • 32개의 채널을 가진 148×148 크기의 특성 맵이다. 원본 모델의 첫 번째 활성화 중에서 20번째 채널을 그려보자.
import matplotlib.pyplot as plt

plt.matshow(first_layer_activation[0, :, :, 19], cmap='viridis')

  • 이 채널은 대각선 에지를 감지하도록 인코딩 된 것 같다.
    • 합성곱 층이 학습한 필터는 결정적이지 않기 때문에 채널 이미지가 책과 다를 수 있다.
    • (실제 내가 돌려본 이미지는 전혀 다름)
  • 16번째 채널을 그려보자.

  • (마찬가지로 내가 돌려본 결과는 전혀 다르다)
  • 이제 네트워크의 모든 활성화를 시각화해 보자.
    • 8개의 활성화 맵에서 추출한 모든 채널을 그리기 위해 하나의 큰 이미지 텐서에 추출한 결과를 나란히 쌓겠다.
layer_names = []
for layer in model.layers[:8]:
layer_names.append(layer.name)

images_per_row = 16

for layer_name, layer_activation in zip(layer_names, activations):
n_features = layer_activation.shape[-1]
size = layer_activation.shape[1]
n_cols = n_features // images_per_row
display_grid = np.zeros((size*n_cols, images_per_row*size))

for col in range(n_cols):
for row in range(images_per_row):
channel_image = layer_activation[0, :, :, col * images_per_row + row]
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype('uint8')
display_grid[col*size : (col+1) * size, row*size : (row+1)*size] = channel_image

scale = 1. / size
plt.figure(figsize=(scale * display_grid.shape[1], scale * display_grid.shape[0]))
plt.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')

plt.show()

 

  • 몇 가지 주목해야 할 내용이 있다.
    • 첫 번째 층은 여러 종류의 에지 감지기를 모아 놓은 것 같다. 이 단계의 활성화에는 초기 사진에 있는 거의 모든 정보가 유지된다.
    • 상위 층으로 갈수록 활성화는 점점 더 추상적으로 되고 시각적으로 이해하기 어려워진다. ‘고양이 귀’, ‘고양이 눈’ 처럼 고수준 개념을 인코딩하기 시작한다. 상위 층의 표현은 이미지의 시각적 콘텐츠에 관한 정보가 점점 줄어들고 이미지의 클래스에 관한 정보가 점점 증가한다.
    • 비어 있는 활성화가 층이 깊어짐에 따라 늘어난다. 첫 번째 층에서는 모든 필터가 입력 이미지에 활성화 되었지만 층을 올라가면서 활성화되지 않은 필터들이 생긴다. 필터에 인코딩된 패턴이 입력 이미지에 나타나지 않았다는 것을 의미한다.
  • 심층 신경망이 학습한 표현에서 일반적으로 나타나는 중요한 특징을 확인했다. 층에서 추출한 특성은 층의 깊이를 따라 점점 더 추상적이 된다.
    • 높은 층의 활성화는 특정 입력에 관한 시각적 정보가 점점 더 줄어들고 타깃에 관한 정보(이 경우에는 강아지 또는 고양이 이미지의 클래스)가 점점 더 증가한다.
    • 심층 신경망은 입력되는 원본 데이터(여기서는 RGB 포맷의 사진)에 대한 정보 정제 파이프라인처럼 작동한다.
    • 반복적인 변환을 통해 관계없는 정보(예컨대 이미지에 있는 특정 요소)를 걸러내고 유용한 정보는 강조되고 개선된다(여기서는 이미지의 클래스)
  • 사람과 동물이 세상을 인지하는 방식이 이와 비슷하다.
    • 사람은 몇 초 동안 한 장면을 보고 난 후 그 안에 있었던 추상적인 물체(자전거, 나무)를 기억할 수 있다. 하지만 물체의 구체적인 모양은 기억하지 못한다.
    • 사실 기억을 더듬어 일반적인 자전거를 그려보면 조금이라도 비슷하게 그릴 수 없다.
    • 우리 뇌는 시각적 입력에서 관련성이 적은 요소를 필터링하여 고수준 개념으로 변환한다. 이렇게 완전히 추상적으로 학습하기 때문에 눈으로 본 것을 자세히 기억하기는 어렵다.
    • (결국 인간도 특징점을 찾아 추상화 하여 기억한다는 뜻. 사실 세상의 정보는 무한한데, 그 정보를 다루려면 그런 식으로 압축해서 다루는게 맞다. 이래서 CNN이 이미지 분석을 잘 하는 듯)

컨브넷 필터 시각화하기

  • 컨브넷이 학습한 필터를 조사하는 또 다른 방법은 각 필터가 반응하는 시각적 패턴을 그려 보는 것이다.
    • 빈 입력 이미지에서 시작해서 특정 필터의 응답을 최대화하기 위해 컨브넷 입력 이미지에 경사 상승법을 적용한다.
    • 결과적으로 입력 이미지는 선택된 필터가 최대로 응답하는 이미지가 될 것이다.
  • 전체 과정은 간단하다. 특정 합성곱 츠으이 한 필터 값을 최대화하는 손실 함수를 정의한다.
    • 이 활성화 값을 최대화하기 위해 입력 이미지를 변경하도록 확률적 경사 상승법을 사용한다.
    • 예컨대 여기에서는 ImageNet에 사전 훈련된 VGG16 네트워크에서 block3_conv1 층 필터 0번의 활성화를 손실로 정의한다.
from keras.applications import VGG16
from keras import backend as K

model = VGG16(weights='imagenet', include_top=False)

layer_name = 'block3_conv1'
filter_index = 0

layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
  • 경사 상승법을 구현하기 위해 모델의 입력에 대한 손실의 그래디언트가 필요하다. 이를 위해 케라스의 backend 모듈에 있는 gradients 함수를 사용하겠다.
grads = K.gradients(loss, model.input)[0]
  • 경사 상승법 과정을 부드럽게 하기 위해 사용하는 한 가지 기법은 그래디언트 텐서를 L2 노름(텐서에 있는 값을 제곱한 합의 제곱근)으로 나누어 나누어 정규화하는 것이다. 이렇게 하면 입력 이미지에 적용할 수정량의 크기를 항상 일정 범위 안에 놓을 수 있다.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
  • 이제 주어진 입력 이미지에 대한 손실 텐서와 그래디언트 텐서를 계산해야 한다. 케라스 백엔드 함수를 사용하여 처리하겠다.
    • iterate는 넘파이 텐서(크기가 1인 텐서의 리스트)를 입력으로 받아 손실과 그래디언트 2개의 넘파이 텐서를 반환한다.
iterate = K.function([model.input], [loss, grads])

import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])
  • 여기에서 파이썬 루프를 만들어 확률적 경사 상승법을 구성한다.
input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128. --잡음이 섞인 회색 이미지로 시작한다.

step = 1.

# 경사 상승법을 40회 실행
for i in range(40):
loss_value, grads_value = iterate([input_img_data]) --손실과 그래디언트를 계산
input_img_data += grads_value * step --손실을 최대화하는 방향으로 입력 이미지를 수정
  • 결과 이미지 텐서는 (1, 150, 150, 3) 크기의 부동 소수 텐서이다. 이 텐서 값은 [0, 255] 사이의 정수가 아니다. 따라서 출력 가능한 이미지로 변경하기 위해 후처리를 할 필요가 있다. 
    • 이를 위해 간단한 함수를 정의하여 사용하자.
def deprocess_image(x):
# 텐서의 평균이 0, 표준편차가 0.1이 되도록 정규화
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1

# [0, 1]로 클리핑
x += 0.5
x = np.clip(x, 0, 1)

# RGB 배열로 변환
x *= 255
x = np.clip(x, 0, 255).astype('uint8')

return x
  • 이제 모든 코드가 준비되었다. 이 코드를 모아서 층의 이름과 필터 번호를 입력으로 받는 함수를 만들겠다. 이 함수는 필터 활성화를 최대화하는 패턴을 이미지 텐서로 출력한다.
def generate_pattern(layer_name, filter_index, size=150):
layer_ouput = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

grads = K.gradients(loss, model.input)[0]

grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

iterate = K.function([model.input], [loss, grads])

input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.

step = 1.
for i in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step

img = input_img_data[0]
return deprecss_image(img)
  • 이 함수를 실행한 결과는 다음과 같다.
import matplotlib.pyplot as plt

plt.imshow(generate_pattern('block3_conv1', 0))

  • block3_conv1 층의 필터 0은 물방울 패턴에 반응하는 것 같다. 이제 재미있는 것을 만들어 보자. 모든 층에 있는 필터를 시각화해 보겠다.
    • 간단히 만들기 위해 각 층에서 처음 64개의 필터만 사용하겠다.
    • 또 각 합성곱 블록의 첫 번째 층만 살펴보겠다(block1_conv1, block2_conv1, block3_conv1, block4_conv1, block5_conv1)
    • 여기서 얻은 출력을 64×64 필터 패턴의 8×8 그리드로 정렬한다. 각 필터 패턴 사이에 검은색 마진을 둔다.
layer_name = 'block1_conv1'
size = 64
margin = 5

results = np.zeros((8*size + 7*margin, 8*size + 7*margin, 3), dtype='uint8')

for i in range(8):
for j in range(8):
filter_img = generate_pattern(layer_name, i + (j*8), size=size)
horizontal_start = i * size + i * margin
horizontal_end = horizontal_start + size
vertical_start = j * size + j * margin
vertical_end = vertical_start + size
results[horizontal_start: horizontal_end, vertical_start:vertical_end, :] = filter_img

plt.figure(figsize=(20,20))
plt.imshow(results)

  • 이런 필터 시각화를 통해 컨브넷 츠잉 바라보는 방식을 이해할 수 있다. 컨브넷의 각 층은 필터의 조합으로 입력을 표현할 수 있는 일련의 필터를 학습한다.
  • 이는 푸리에 변환(Fourier transform)을 사용하여 신호를 일련의 코사인 함수로 분해할 수 있는 것과 비슷하다. 이 컨브넷 필터들은 모델의 상위 층으로 갈 수록 점점 더 복잡해지고 개선된다.
    • 모델에 있는 첫 번째 층(block1_conv1)의 필터는 간단한 대각선 방향의 에지와 색깔(또는 어떤 색깔이 있는 에지)을 인코딩한다.
    • block2_conv1의 필터는 에지나 색깔의 조합으로 만들어진 간단한 질감을 인코딩한다.
    • 더 상위 층의 필터는 깃털, 눈, 나뭇잎 등 자연적인 이미지에서 찾을 수 있는 질감을 닮아가기 시작한다.

클래스 활성화의 히트맵 시각화하기

  • 한 가지 시각화 기법을 더 소개하겠다. 이 방법은 이미지의 어느 부분이 컨브넷의 최종 분류 결정에 기여하는지 이해하는데 유용하다.
    • 분류에 실수가 있는 경우 컨브넷의 결정 과정을 디버깅하는데 도움이 된다. 또 이미지에 특정 물체가 있는 위치를 파악하는데 사용할 수도 있다.
  • 이 기법의 종류를 일반적으로 클래스 활성화 맵(Class Activation Map, CAM) 시각화라고 한다.
    • 입력 이미지에 대한 클래스 활성화의 히트맵을 만든다. 클래스 활성화 히트맵은 특정 출력 클래스에 대해 입력 이미지의 모든 위치를 계산한 2D 점수 그리드이다.
    • 클래스에 대해 각 위치가 얼마나 중요한지 알려준다. 예컨대 강아지 vs 고양이 컨브넷에 한 이미지를 주입하면 CAM 시각화는 고양이 클래스에 대한 히트맵을 생성하여 이미지에서 고양이와 비슷한 부분을 알려준다.
  • 여기서 사용할 구체적인 구현은 “Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization”에 기술되어 있는 것이다.
    • 방법은 간단하다. 입력 이미지가 주어지면 합성곱 층에 있는 특성 맵의 출력을 추출한다.
    • 그 다음 특성 맵의 모든 채널 출력에 채널에 대한 클래스의 그래디언트 평균을 곱한다.
    • 이 기법을 직관적으로 이해하는 방법은 다음과 같다. ‘입력 이미지가 각 채널을 활성화하는 정도’에 대한 공간적인 맵을 ‘클래스에 대한 각 채널의 중요도’로 가중치를 부여하여 ‘입력 이미지가 클래스를 활성화하는 정도’에 대한 공간적인 맵을 만드는 것이다.
  • 사전 훈련된 VGG16 네트워크를 다시 사용하여 시연해 보겠다.
from keras.applications.vgg16 import VGG16

model = VGG16(weights='imagenet')
  • 다음 그림의 초원을 걷는 어미와 새끼 아프리카 코끼리의 이미지를 적용해 보겠다. 이 이미지를 VGG16 모델이 인식할 수 있도록 변환하다.
    • 이 모델은 224×224 크기의 이미지에서 훈련되었고 keras.applications.vgg16.preprocess_input 함수에 있는 몇 가지 규칙에 따라 전처리 되었다.

from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np

img_path = '.\datasets\creative_commons_elephant.jpg'

img = image.load_img(img_path, target_size=(224, 224))

x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
  • 이제 이 이미지에서 사전 훈련된 네트워크를 실행하고 예측 벡터를 이해하기 쉽게 디코딩한다.
preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3)[0])
---
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json 40960/35363 [==================================] - 0s 0us/step Predicted: [('n02504458', 'African_elephant', 0.90942144), ('n01871265', 'tusker', 0.08618243), ('n02504013', 'Indian_elephant', 0.0043545766)]
  • 이 이미지에 대한 상위 3개의 예측 클래스는 다음과 같다.
    • 아프리카 코끼리 90.9% 
    • 코끼리(tusker) 8.6%
    • 인도 코끼리 0.4%
    • (책에 있는 결과와 조금 다름)
  • 네트워크는 이 이미지가 아프리카 코끼리를 담고 있다고 인식했다. 예측 벡터에서 최대로 활성화된 항목은 ‘아프리카 코끼리’클래스에 대한 것으로 386번 인덱스이다.
np.argmax(preds[0])
---
386
  • 이 이미지에서 가장 아프리카 코끼리와 같은 부위를 시각화 하기 위해 Grad-CAM 처리 과정을 구현해 보자.
from keras import backend as K
import matplotlib.pyplot as plt

# 예측 벡터의 '아프리카 코끼리' 항목
african_elephant_output = model.output[:, 386]

# VGG16의 마지막 합성곱 층인 block5_conv3 층의 특성맵
last_conv_layer = model.get_layer('block5_conv3')

# block5_conv3의 특성 맵 출력에 대한 '아프리카 코끼리' 클래스의 그래디언트
grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]

# 특성 맵 채널별 그래디언트 평균값이 담긴 (512, ) 크기의 벡터
pooled_grads = K.mean(grads, axis=(0, 1, 2))

# 샘플 이미지가 주어졌을 때 방금 전 정의한 pooled_grads와 block5_conv3의 특성맵 출력을 구한다.
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])

# 두 마리 코끼리가 있는 샘플 이미지를 주입하고 2개의 넘파이 배열을 얻는다.
pooled_grads_value, conv_layer_output_value = iterate([x])

# '아프리카 코끼리' 클래스에 대한 '채널의 중요도'를 특성 맵 배열의 채널에 곱한다.
for i in range(512):
conv_layer_output_value[:, :, i] *= pooled_grads_value[i]

# 만들어진 특성 맵에서 채널 축을 따라 평균한 값이 클래스 활성화의 히트맵이다.
heatmap = np.mean(conv_layer_output_value, axis=-1)
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)

plt.matshow(heatmap)

  • 마지막으로 OpenCV를 사용하여 히트맵에 원본 이미지를 겹친 이미지를 만들어보자.
import opencv

img = cv2.imread(img_path)

heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)

superimposed_img = heatmap * 0.4 + img

cv2.imwrite('.\datasets\elephant_cam.jpg', superimposed_img)

  • 이 시각화 기법은 2개의 중요한 질문에 대한 답을 준다.
    • 왜 네트워크가 이 이미지에 아프리카 코끼리가 있다고 생각하는가?
    • 아프리카 코끼리 사진은 어디 있는가?
  • 코끼리 새끼의 귀가 강하게 활성화 된 점이 흥미로운데, 아마도 이것이 네트워크가 아프리카 코끼리와 인도 코끼리의 차이를 구분하는 방법일 것이다.

요약

  • 컨브넷은 시각적인 분류 문제를 다루는데 최상의 도구이다.
  • 컨브넷은 우리가 보는 세상을 표현하기 위한 패턴의 계층 구조와 개념을 학습한다.
  • 학습된 표현은 쉽게 분석할 수 있다. 컨브넷은 블랙 박스가 아니다.
  • 이미지 분류 문제를 풀기 위해 자신만의 컨브넷을 처음부터 훈련시킬 수 있다.
  • 과대적합을 줄이기 위해 데이터 증식하는 방법을 배웠다.
  • 사전 훈련된 컨브넷을 사용하여 특성 추출과 미세 조정하는 방법을 배웠다.
  • 클래스 활성화 히트맵을 포함하여 컨브넷이 학습한 필터를 시각화 할 수 있다.