페이지

2022년 7월 22일 금요일

2.3.3 텐서 점곱

 텐서 곱셈(tensor product)이라고도 부르는 (원소별 곱셈과 혼동하지 마세요) 점곱 연산(dot operation)은 가장 널리 사용되고 유용한 텐서 연산입니다. 원소별 연산과 반대로 입련 텐서의 원소들을 결합시킵니다.

넘파이, 케라스, 씨아노, 텐서플로에서 원소별 곱셈은 * 연산자를 사용합니다. 텐서플로에서는 dot 연산자가 다르지만 넘파이와 케라스는 점곱 연산에 보편적인 dot연산자를 사용합니다.

    import numpy as np

    z = np.dot(x, y)

    z = x . y

점곱 연산은 수학에서 어떤일을 할까요? 2개의 벡터 x 와 y의 점곱은 다음과 같이 계산을 합니다.

def naive_vector_dot(x, y):

   assert len(x.shape) == 1........... x는 넘파이 벡터입니다.

   assert len(y.shape) == 1........... y는 넘파이 벡터입니다.

    z = 0.

    for i in range(x.shape[0]):

        z += x[i] * y[i]

    return z

여기서 볼 수 있듯이 두 벡터의 점곱은 스칼라가 되므로 원소 개수가 같은 벡터끼리 점곱이 가능합니다.

행렬 x와 벡터 y사이에서도 점곱이 가능합니다. y와  x의 행 사이에서 점곱이 일어나므로 벡터가 반환됩니다. 다음과 같이 구현할 수 있습니다.

    import numpy as np

    def naive_matrix_vector_dot(x, y)

        assert len(x.shape) == 2 .......... x는 넘파이 행렬입니다.

        assert len(y.shape) == 1............ y는 넘파이 벡터입니다.

        assert x.shape[1] == y.shape[0] ......... x의 두 번째 차원이  y의 첫번째 차원과 같아야 합니다.

        z = np.zeros(x.shape[0]) .....이 연산은 x의 행과 같은 크기의  0이 채워진 벡터를 만듭니다.

        for i in range(x.shape[0]):

            for j in range(x.shape[1]):

                z[i] += x[i,j] * y[j]

        return z

행렬-벡터 점곱과 벡터-벡터 점곱 사이의 관계를 부각하기 위해 앞에ㅐ서 만든 함수르 재사용하겠습니다.

    def naive_matrix_vector_dot(x,y):

        z = np.zeros(x.shape[0])

        for i in range(x.shape[0])

            z[i] = naive_vector_dot(x[i, :],  y)

        return z

두 텐서 중 하나라도 ndim이 1보다 크면 dot 연산에 교환 법칙이 성립되지 않습니다. 다시 말하면 dot(x, y)와 dot(y, x)가 같지 않습니다.

물론 점곱은 임의의 축 개수를 가진 텐서에 일반화됩니다. 가장 일반적인 용도는 두 행렬 간의 점곱일 것입니다. x.shape[1] == y.shape[0]일 때 두 행렬 x와 y의 점곱(dot(x, y))이 성립됩니다. x의 행과 y의 열 사이 백터 점곱으로 인해(x.shape[0], y.shape[1]) 크기의 행렬이 됩니다. 다음은 단순한 구현 예 입니다.

def naive_matrix_dot(x, y):

    assert len(x.shape) == 2 .......... x는 넘파이 행렬입니다.

    assert len(y.shape) == 2 .......... y는 넘파이 행렬입니다.

    assert x.shape[1] == y.shape[0]   ........ x 의 두 번째 차원이 y의 첫번째 차원과 같아야 합니다!

    z = np.zeros((x.shape[0], y.shape[1])) ........ 이 연산은 0이 채워진 특정 크기의 벡터를 만듭니다.

    for i in range (x.shape[0]):   ...... x의 행을 반복합니다.

        for j in range(y.shape[1]): ..... y의 열을 반복합니다.

             row_x = x[i, :]

            column_y = y[:, j]

            z[i, j] = naive_vector_dot(row_x, column_y)

    return z

그림 2-5 와 같이 입력과 출력을 배치해 보면 어떤 크기의 점곱이 가능하지 이해하는데 도움이 됩니다.

x, y, z는 직사각형 모양으로 그려져 있습니다(원소들이 채워진 박스라고 생각하면 됩니다). x의 행 벡터와 y의 열 벡터가 같은 크기여야 하므로 자동으로 x의 너비는 y의 놉이와 동일해야 합니다. 새로운 머신 러닝 알고리즘을 개발할 때 이런 그림을 자주 그리게 될 것 입니다.

더 일반적으로 앞서 설명한 2D의 경우 처럼 크기를 맞추는 동일한 규칙을 따르면 다음과 같이 고차원 텐서 간의 점곱을 할 수 있습니다.

(a, b, c, d) . (d,) -> (a, b, c)

(a, b, c, d) . (d, e) -> (a , b, c, e)




2022년 7월 21일 목요일

2.3.2 브로드캐스팅

 앞서 살펴본 단순한 덧셈 구현인 naive_add는 동일한 크기의 2D 텐서만 지원합니다. 하지만 이전에 보았던 Dense 층에서는 2D 텐서와 벡터를 더했습니다. 크기가 다른 두 텐서가 더해질 때 무슨일이 일어날까요?

모호하지 않고 실행 가능하다면 작은 텐서가 큰 텐서의 크기에 맞추어 브로드캐스팅(broadcasting)됩니다. 브로드캐스팅은 두 단계로 이루어집니다.

1. 큰 텐서의 ndim에 맞도록 작은 텐서에(브로드캐스팅 축이라고 부르는) 축이 추가 됩니다.

2. 작은 텐서가 새 축을 따라서 큰 텐서의 크기에 맞도록 반복됩니다.

구체적인 옐르 살펴보겠습니다.

x의 크기는 (32, 10)이고 y의 크기는 (10,)라고 가정합시다. 먼저 y에 비어 있는 첫 번째 축을 추가하여 크기를 (1, 10)으로 만듭니다. 그런 다음 y를 이 축에 32번 반복하면 텐서 Y의 크기는 (32, 10)이 됩니다. 여기에는 Y[i, :] == y for i in range(0, 32)입니다. 이제 X와 Y의 크기가 같으므로 더할 수 있습니다.

구현 입장에서는 새로운 텐서가 만들어지면 매우 비효율적이므로 어떤 2D 텐서도 만들어지지 않습니다. 반복된 연산은 완전히 가상적입니다. 이 과정은 메모리 수준이 아니라 알고리즘 수준에서 일어납니다. 하지만 새로운 축을 따라 벡커가 32번 반복된다고 생각하는 것이 이해하기 쉽습니다. 다음은 단순하게 구현한 예입니다.

 def naive_add_matrixa_and_vector(x, y):

    assert len(x.shape) == 2 ..... x 는 2D 넘파이 배열입니다.

    assert len(y.shape) == 1 .... y는 넘파이 벡터입니다.

    assert x.shape[1] == y.shape[0]


    x = x.copy() ....... 입력 텐서 자체를 바꾸지 않도록 복사합니다.

    for i in range(x.shape[0]):

        for j in range(x.shape[1]):

            x[i, j] += y[j]

    return x

(a, b, ... n, n + 1, .... m) 크기의 텐서와 (n, n + 1, ,..... m) 크기의 텐서 사이에 브로드 캐스팅으로 원소별 연산을 적용할 수 있습니다. 이때 브로드캐스팅은 a부터 n - 1까지의 축에 자동으로 일어납니다.

다음은 크기가 다른 두 텐서에 브로드 캐스팅으로 원소별 maximum 연산을 적용하는 예입니다.

    import numpy as np

    x = np.random.random((64, 3, 32, 10)) ..... x 는 (64, 3,32, 10)크기의 랜덤 텐서입니다.

    y = np.random.random((32, 10)) .....  y 는 (32, 10) 크기의 랜덤 텐서입니다.


    z = np.maximum(x, y) .... 출력 z크기는 x와 동일하게 (64, 3, 32, 10)입니다.

2022년 7월 20일 수요일

2.3.1 원소별 연산

 relu 함수와 덧셈은 원소별 연산(element-wise operation)입니다. 이 연산은 텐서에 있는 가 원소에 독립적으로 적용됩니다. 이 말은 고도의 병렬 구현(1970-1990년대 슈퍼 컴퓨터의 구조인 벡터 프로세서(vector processor)에서 온 용어인 벡터화된 구현을 말합니다) 이 가능한 연산이라는 의미입니다. 파이썬으로 단순한 원소별 연산을 구현한다면 다음 relu연산 구현처럼 for 반복문을 사용할 것 입니다.

def naive_relu(x):

    assert len(x.shape) == 2 ....... x 는 2D 넘파이 배열입니다.


    x = x.copy() ......입력 텐서 자체를 바꾸지 않도록 복사합니다.

    for i in range(x.shape[0]):

        for j in range(x.shape[1]):

            x[ i , j ] = max(x[i, j], 0)

    return x

덧셈도 동일합니다.

def naive_add(x, y):

    assert len(x.shape) == 2 ....... x와 y는 2D 넘파이 배열립니다.

    assert x.shape == y.shape


    x = x.copy() ...... 입력 텐서 자체를 바꾸지 않도록 복사합니다.

    for i in range(x.shape[0]):

        for j in range(x.shape[1]):

            x[i , j] += y[i , j]

    return x

같은 원리로 원소별 곱셈, 뺄셈 등을 할 수 있습니다.

사실 넘파이 배열을 다룰 때는 최적화된 넘파이 내장 함수로 이런 연산들을 처리할 수 있습니다. 넘파이는 시스템에 설치된 BLAS(Basic Linear Algebra Subprogram)구현에 복잡한 일들을 위임합니다. BLAS는 고도로 병렬화되고 효율적인 저수준의 텐서 조작 루틴이며, 전형적으로 포트란(Fortran)이나 C언어로 구현되어 있습니다.

넘파이는 다음과 같은 원소별 연산을 엄청난 속도로 처리합니다.

import numpy as np

z = x + y ...... 원소별 덧셈

z = np.maximum(z, 0.) .....원소별 렐루 함수

2022년 7월 17일 일요일

2.3 신경망의 톱니바퀴: 텐서 연산

 컴퓨터 프로그램을 이진수의 입력을 처리하는 몇 개의 이항 연상(AND, OR, NOR등)으로 표현할 수 있는 것처럼, 심층 신경망이 학습한 모든 변환을 수치 데이터 텐서에 적용하는 몇 종류의 텐서 연산(tensor operation)으로 나타낼 수 있습니다. 예를 들어 텐서 넛셈이나 텐서 곱셈 등입니다.

첫 번째 예제에서는 Dense 층을 쌓아서 신경망을 만들었습니다. 케라스의 층은 다음과 같이 생성합니다.

keras.layers.Dense(512, activation='relu')

이 층은 2D 텐서를 입력으로ㅓ 받고 입력 텐서의 새로운 표현인 또 다른 2D 텐서를 변환하는 함수처럼 해석할 수 있습니다. 구체적으로 보면 이 함수는 다음과 같습니다.(W는 2D 텐서고, b는 벡터입니다. 둘 모두 층의 속성입니다).

output = relu(dot(W, input) + b)

좀 더 자세히 알아보겠습니다. 여기에선 3개의 텐서연산이 있습니다. 입력 턴서와 턴서 W사이의 점곱(dot), 접곱의 결과인 2D 텐서와 벡터 b사이의 덧셈(+), 마지막으로 relu(렐루) 연산입니다. relu(x)는 max(x, 0)입니다.


이 절은 선형대수학(linear algebra)을 다루지만 어떤 수학 기호도 사용하지 않습니다. 수학에 익숙하지 않은 프로그래머는 수학 방정식보다 짧은 코드를 보는 것이 수학 개념을 이해한는 데 훨씬 도움이 됩니다. 앞으로도 계속 넘파이 코드를 사용하여 설명합니다.

2.2.12 비디오 데이터

 비디오 데이터는 현실에서 5D 텐서가 필요한 몇 안 되는 데이터 중 하나입니다. 하나의 비디오는 프레임의 연속이고 각 프레임은 하나의 컬러 임지입니다. 프레임이 (height, width, color_depth)의 3D 텐서로 지정될 수 있기 때문에 프레임의 연속은 ( frames, height, width, color_depth)의 4D 텐서로 저장될 수 있습니다. 여러 비디오의 배치는 (sample, frames, height, width, color_depth)의 5D텐서로 저장될 수 있습니다.

예를 들어 60초 짜리 144 * 256 유튜브 비디오 클립을 초당 4프레임으로 샘플링하면 240 프레임이 됩니다. 이런 비디오 클립을 4개 가진 배치는 (4, 240, 144, 256, 3) 크기의 텐서에 저장될 것입니다. 총 106, 168, 168, 329개으 ㅣ값이 있습니다! 이 텐서의 dtype을 float32로 했다면 각 값이 32 비트로 저장될 것이므로 텐서의 저장 크기는 405MB가 됩니다. 아주 크네요! 실생활에서 접하는 비디오는 float32 크기로 저장되지 않기 때문에 용량이 적고, 일반적으로 높은 압축률로(MPEG 포맷 같은 방식을 사용하여)압축되어 있습니다.

2.2.11 이미지 데이터

 이미지는 전형적으로 높이, 너비, 컬러 채널의 3차원으로 이루어집니다.(MNIST 숫자처럼) 흑백이미지는 하나의 컬러 채널만을 가지고 있어 2D 텐서로 저장될 수 있지만 관례상 이미지 텐서는 항상 3D로 저장됩니다. 흑백 이미지의 경우 컬러 채널의 차원 크기는 1입니다. 256 * 256 크기의 흑백 이미지에 대한 128개의 배치는 (128, 256, 256, 1) 크기의 텐서에 저장될 수 있습니다. 컬러 이미지에 대한 128개의 배치라면 (128, 256, 256, 3) 크기의 텐서에 저장될 수 있습니다.

이미지 텐서의 크기를 지정하는 방식은 두 가지 입니다.(텐서플로에서는 사용하는) 채널 마지막(channel-last)방식과 (씨아노에서 사용하는) 채널 우선(channel-first)방식입니다. 구글의 텐서플로 머신 러닝 프레임워크는 (sample, height, width, color_depth)처럼 컬러 채널의 깊이를 끌어 놓습니다. 반면에 씨아노는 (sample, color_depth, height, width)처럼 컬러 채널의 깊이를 배치 축 바로 뒤에 놓스빈다. 씨아노 방식을 사용하면 앞선 예는 (128, 1, 256, 256)과( 128, 3, 256, 256)이 됩니다. 케라스 프레임워크는 두 형식을 모두 지원합니다.


2.2.10 시계열 데이터 또는 시퀀스 데이터

 데이터에서 시간이 (또는 연속된 순서가)중요할 때는 시간 축을 포함하여 3D 텐서로 저장됩니다. 각 샘플은 벡터(2D 텐서)의 시퀀스로 인코딩되므로 배치 데이터는 3D 텐서로 인코딩될 것입니다.

관례적으로 시간 축은 항상 두 번째 축(인덱스가 1인 축)입니다. 몇 가지 옐르 들어 보겠습니다.

- 주식 가격 데이터셋: 1분마다 현재 주식 가격, 지난 1분 동안에 최고 가격과 최소 가격을 저장합니다. 1분마다 데이터는 3D 벡터로 인코딩되고 하루 동안의 거래는 (390, 3) 크기의 2D 텐서로 인코딩됩니다(하로의 거래 시간은 390분 입니다). 250일치의 데이터는 (250, 390, 3) 크기의 3D 텐서로 저장될 수 있습니다. 여기에서 1일치 데이터가 하나의 샘플이 됩니다.

- 트윗 데이터셋: 각 트윗은 128개의 알파벳으로 구성된 280개의 문자 시퀸스 입니다. 여기에서는 각 문자가 128개의 크기인 이진 벡터로 인코딩될 수 있습니다(해당 문자의 인텍스만 1이고 나머지는 모두 0인 벡터), 그러면 각 트윗은 (280, 128) 크기의 2D 텐서로 인코딩될 수 있습니다. 100만 개의 트윗으로 구성된 데이터셋은 (1000000, 280, 128)크기의 텐서에 저장됩니다.