intro
To prepare for your dive into deep learning, you will need a few survival skills
딥러닝에 대한 다이빙을 준비하려면 다음과 같은 몇 가지 생존 기술이 필요합니다.
(i) techniques for storing and manipulating data
데이터 저장 및 조작 기술
(ii) libraries for ingesting and preprocessing data from a variety of sources
다양한 소스의 데이터를 수집하고 전처리하기 위한 라이브러리
(iii) knowledge of the basic linear algebraic operations that we apply to high-dimensional data elements
고차원 데이터 요소에 적용되는 기본 선형 대수 연산에 대한 지식
(iv) just enough calculus to determine which direction to adjust each parameter in order to decrease the loss function
손실 함수를 줄이기 위해 각 매개변수를 조정할 방향을 결정하기에 충분한 미적분
(v) the ability to automatically compute derivatives so that you can forget much of the calculus you just learned
방금 배운 미적분의 대부분을 잊어버릴 수 있도록 도함수를 자동으로 계산하는 기능
(vi) some basic fluency in probability, our primary language for reasoning under uncertainty
확신이 없거나 예측하기 어려운 상황에서 추론하기 위한 기본 언어인 확률의 기본적인 유창성
(vii) some aptitude for finding answers in the official documentation when you get stuck.
문제가 생겼을 때 공식 문서에서 답을 찾는 능력.
들어가기 전
딥러닝은 대량의 데이터를 기반으로 모델을 학습시키는 기술입니다. 그렇기 때문에 데이터 조작은 딥러닝을 이해하고 활용하는 데 있어서 매우 중요한 요소입니다. 데이터 조작은 데이터를 원하는 형태로 변형하고 조작하여 모델의 성능을 향상시키는 데 도움을 주는 작업입니다.
데이터 조작은 다양한 측면에서 이루어집니다. 첫째로, 데이터의 일부를 선택하거나 필요한 부분을 추출하는 "인덱싱과 슬라이싱" 작업이 있습니다. 이 작업을 통해 데이터셋의 특정 부분에 집중하거나 필요한 정보를 추출할 수 있습니다.
둘째로, 데이터를 조작하거나 변형하는 "연산" 작업이 있습니다. 이 작업은 데이터에 수학적인 연산을 적용하여 데이터를 변형하거나 새로운 정보를 생성하는 데 사용됩니다. 연산은 데이터를 처리하고 모델에 입력하기 전에 중요한 단계입니다.
셋째로, "브로드캐스팅"은 데이터의 크기나 형상을 자동으로 맞춰주는 기능입니다. 이를 통해 서로 다른 크기나 형상의 데이터를 연산할 수 있으며, 코드를 간결하게 유지할 수 있습니다.
마지막으로, "메모리 절약"은 대규모 데이터셋을 다룰 때 중요한 문제입니다. 메모리 절약은 데이터를 효율적으로 저장하고 처리하기 위해 다양한 기술과 알고리즘을 사용하는 것을 의미합니다.
이번 글에서는 데이터 조작의 인덱싱과 슬라이싱, 연산, 브로드캐스팅, 메모리 절약에 대해 자세히 알아보겠습니다. 이러한 데이터 조작 기술은 딥러닝을 수행하는 과정에서 필수적으로 사용되는 도구들입니다. 데이터 조작에 대한 이해는 딥러닝 모델을 구축하고 학습시키는 데 있어서 필수적인 기반 지식입니다. 그러므로 이번 글을 통해 데이터 조작에 대한 핵심 개념과 기술들을 익히도록 하겠습니다.
Data Manipulation (데이터 조작)
To conduct any task, we require a method to store and manage data. Two significant steps with data include acquisition and in-computer processing. We primarily deal with $n$-dimensional arrays, or tensors. Familiarity with the NumPy scientific computing package will simplify this.
어떤 작업을 수행하려면 데이터를 저장하고 관리하는 방법이 필요합니다. 데이터와 관련하여 중요한 두 단계는 획득 및 컴퓨터 내부에서의 처리입니다. 우리는 주로 $n$-차원 배열 또는 텐서를 다룹니다. NumPy 과학 계산 패키지에 익숙하다면 이를 이해하는데 도움이 됩니다.
Indexing and Slicing (인덱싱과 슬라이싱)
Tensor elements are accessible through indexing, with indexing starting at 0. Negative indexing allows for position-based element access from the end of the list. Slicing can be used for accessing a range of indices (e.g., X[start:stop]), the returned value includes the first index (start) but not the last (stop). When only one index (or slice) is specified for a $k^\mathrm{th}$ order tensor, it is applied along axis 0.
텐서 요소는 인덱싱을 통해 접근할 수 있으며, 인덱싱은 0부터 시작합니다. 음의 인덱싱을 사용하면 리스트의 끝에 상대적인 위치를 기반으로 요소에 접근할 수 있습니다. 슬라이싱은 일련의 인덱스에 접근하는 데 사용할 수 있습니다.
import torch
x = torch.arange(12, dtype=torch.float32)
X = x.reshape(3, 4)
X[-1], X[1:3]
실행결과
(tensor([ 8., 9., 10., 11.]),
tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
we can also write elements of a matrix by specifying indices.
인덱스를 저장하여 행렬의 요소를 쓸 수도 있습니다.
X[1, 2] = 17
X
실행결과
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 17., 7.],
[ 8., 9., 10., 11.]])
To assign multiple elements the same value, we apply the indexing on the left-hand side of the assignment operation.
여러 요소에 동일한 값을 할당하려면, 할당 연산의 왼쪽에 인덱싱을 적용합니다.
X[:2, :] = 12
X
실행결과
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
Operations (동작)
With the knowledge of tensor construction, reading, and writing elements, we can proceed with manipulating them using mathematical operations. Elementwise operations apply a standard scalar operation to each element of a tensor. Unary operators like $e^x$ can be applied elementwise as well.
텐서 구조, 읽기 및 쓰기 요소를 이해하게 되면, 우리는 수학적 연산을 사용하여 그것들을 조작하는 데 나아갈 수 있습니다. 요소별 연산은 텐서의 각 요소에 표준 스칼라 연산을 적용합니다. 단항 연산자들도 요소별로 적용될 수 있습니다.
torch.exp(x)
실행결과
tensor([162754.7969, 162754.7969, 162754.7969, 162754.7969, 162754.7969,
162754.7969, 162754.7969, 162754.7969, 2980.9580, 8103.0840,
22026.4648, 59874.1406])
Unary operators such as torch.exp(x) can be applied elementwise as well. Binary scalar operators, which map pairs of real numbers to a single real number, can also be extended to vectors of the same shape. This operation can be thought of as lifting the scalar function to an elementwise vector operation. Standard arithmetic operators have been lifted to elementwise operations for identically-shaped tensors.
다음과 같은 단항 연산자인 torch.exp(x)도 요소별로 적용될 수 있습니다. 또한 두 개의 실수를 하나의 실수로 매핑하는 이항 스칼라 연산자도 동일한 모양의 벡터로 확장될 수 있습니다. 이 작업은 스칼라 함수를 요소별 벡터 연산으로 '올리는' 것으로 생각할 수 있습니다. 표준 산술 연산자들은 동일한 모양의 텐서에 대한 요소별 연산으로 '올려졌습니다'.
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
실행결과
(tensor([ 3., 4., 6., 10.]),
tensor([-1., 0., 2., 6.]),
tensor([ 2., 4., 8., 16.]),
tensor([0.5000, 1.0000, 2.0000, 4.0000]),
tensor([ 1., 4., 16., 64.]))
We can also concatenate multiple tensors together, forming a larger tensor. This process depends on the axis along which we concatenate.
우리는 또한 여러 텐서를 연결하여 더 큰 텐서를 형성할 수 있습니다. 이 과정은 우리가 연결하는 축에 따라 달라집니다.
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
실행결과
(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))
X == Y
실행결과
tensor([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
Binary tensors can be constructed via logical statements, and all the elements in the tensor can be summed up to yield a tensor with only one element.
논리적 문장을 통해 이진 텐서를 구성할 수 있으며, 텐서의 모든 요소를 합하면 하나의 요소만 있는 텐서가 생성됩니다.
X.sum()
실행결과
tensor(66.)
Broadcasting (브로드캐스팅)
Under certain conditions, we can perform elementwise binary operations on tensors of different shapes by using the broadcasting mechanism. Broadcasting works as follows: (i) expand one or both arrays by copying elements along axes with length 1 to make the shapes compatible; (ii) perform an elementwise operation on the resulting arrays.
브로드캐스팅 메커니즘을 사용하면 형상이 다른 텐서에 대해 요소별 이진 연산을 수행할 수 있습니다. 브로드캐스팅은 다음과 같은 두 단계로 진행됩니다: (i) 길이가 1인 축을 따라 요소를 복사하여 하나 이상의 배열을 확장하여 두 텐서의 형상을 호환 가능하게 만듭니다. (ii) 결과로 나온 배열에 대해 elementwise 연산을 수행합니다.
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
실행결과
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
Since a
and b
are $3\times1$ and $1\times2$ matrices, respectively, their shapes do not match up. Broadcasting produces a larger $3\times2$ matrix by replicating matrix a
along the columns and matrix b
along the rows before adding them elementwise.
a
와 b
는 각각 $3\times1$ 및 $1\times2$ 행렬이므로 그 모양이 일치하지 않습니다. 브로드캐스팅은 행렬 a
를 열을 따라 복제하고 행렬 b
를 행을 따라 복제하여 더 큰 $3\times2$ 행렬을 생성한 후 요소별로 추가합니다.
a + b
실행결과
tensor([[0, 1],
[1, 2],
[2, 3]])
Saving Memory (메모리 절약하기)
In this example, when we execute Y = Y + X, a new memory allocation is performed for the result of Y + X. As a result, the variable Y is reassigned to point to this new memory location, indicated by a different id(Y). This behavior can be problematic for two reasons.
예를 들어, Y = X + Y라고 작성하면, Y가 이전에 가리키던 텐서를 참조 해제하고 새롭게 할당된 메모리를 가리키도록 한다. id() 함수를 사용하여 객체의 정확한 메모리 주소를 얻을 수 있는 Python의 id() 함수를 사용하여 이 문제를 설명할 수 있다. Y = Y + X를 실행한 후에는 id(Y)가 다른 위치를 가리킨다는 것을 알 수 있다. 이는 Python이 먼저 Y + X를 평가하고 결과에 대해 새로운 메모리를 할당한 다음 Y를 이 새로운 메모리 위치로 지정하기 때문이다.
before = id(Y)
Y = Y + X
id(Y) == before
실행결과
False
First, frequent memory allocation can be inefficient, especially when dealing with large tensors and performing updates multiple times per second in machine learning applications. Ideally, we want to perform these updates in place to avoid unnecessary memory allocation.
Second, if multiple variables point to the same parameters, not updating in place can lead to memory leaks or references to stale parameters. Therefore, it's important to update all the references consistently.
이는 두 가지 이유로 원하지 않을 수 있다. 첫째로, 우리는 불필요하게 메모리를 계속해서 할당하는 것을 원하지 않는다. 머신 러닝에서는 종종 수백 메가바이트의 매개변수가 있고 이들을 초당 여러 번 업데이트한다. 가능하면 이러한 업데이트를 in-place로 수행하고 싶다. 둘째로, 여러 변수에서 동일한 매개변수를 가리킬 수 있다. In-place 업데이트하지 않으면 이러한 참조를 모두 업데이트해야 하며, 그렇지 않으면 메모리 누수가 발생하거나 오래된 매개변수를 참조할 수 있다.
Fortunately, performing in-place operations is straightforward in Python. We can assign the result of an operation to a pre-allocated array Y using slice notation: Y[:] = . This allows us to update the values of Y without changing its memory location. Here's an example:
다행히도, in-place 연산을 수행하는 것은 쉽다. 미리 할당된 배열 Y에 연산의 결과를 할당하기 위해 슬라이스 표기법을 사용할 수 있다: Y[:] = <식>. 이 개념을 설명하기 위해, zeros_like를 사용하여 초기화한 후 텐서 Z의 값을 Y와 동일한 모양으로 덮어씌우는 예제를 살펴보자.
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
실행결과
id(Z): 139959156107680
id(Z): 139959156107680
[If the value of X is not reused in subsequent computations, we can further reduce the memory overhead by using X[:] = X + Y or X += Y.]
[만약 X의 값이 이후의 계산에서 재사용되지 않는다면, X[:] = X + Y 또는 X += Y를 사용하여 연산의 메모리 오버헤드를 줄일 수도 있다.]
before = id(X)
X += Y
id(X) == before
실행결과
True
In the final code snippet, we update the values of X in place using X += Y. This operation modifies X directly, avoiding the need for a new memory allocation if X is not reused later.
By leveraging in-place operations and updating tensors directly, we can effectively save memory and optimize the computational efficiency of our code.
위의 코드 스니펫에서는 X += Y를 사용하여 X의 값을 곧바로 업데이트하여 메모리 오버헤드를 줄인다. 이 작업은 X가 이후에 재사용되지 않는 경우 새로운 메모리 할당이 필요하지 않도록 한다.
in-place 연산과 텐서를 직접적으로 업데이트하여 메모리를 효과적으로 절약하고 코드의 계산 효율성을 최적화할 수 있다.