00 - PyTorch 기초

Open In Colab

PyTorch란 무엇인가요?

PyTorch는 오픈 소스 머신러닝 및 딥러닝 프레임워크입니다.

PyTorch는 어디에 사용되나요?

PyTorch를 사용하면 파이썬 코드를 사용하여 데이터를 조작 및 처리하고 머신러닝 알고리즘을 작성할 수 있습니다.

누가 PyTorch를 사용하나요?

세계 최대 기술 기업 중 상당수가 Meta (Facebook), Tesla, Microsoft를 비롯하여 OpenAI와 같은 인공지능 연구 기업에서도 연구를 수행하고 제품에 머신러닝을 도입하기 위해 PyTorch를 사용합니다.

산업 및 연구 분야에서 사용되는 PyTorch

예를 들어, Andrej Karpathy(Tesla의 AI 책임자)는 Tesla가 자율 주행 컴퓨터 비전 모델을 구동하기 위해 PyTorch를 어떻게 사용하는지에 대해 여러 차례 강연(PyTorch DevCon 2019, Tesla AI Day 2021)을 한 바 있습니다.

PyTorch는 농업과 같은 다른 산업 분야에서도 트랙터의 컴퓨터 비전을 구동하는 데 사용됩니다.

왜 PyTorch를 사용해야 하나요?

머신러닝 연구자들은 PyTorch 사용을 선호합니다. 2022년 2월 현재, PyTorch는 머신러닝 연구 논문과 그에 부수되는 코드 저장소를 추적하는 웹사이트인 Papers With Code에서 가장 많이 사용되는 딥러닝 프레임워크입니다.

또한 PyTorch는 백그라운드에서 GPU 가속(코드 실행 속도 향상)과 같은 많은 것들을 처리해 줍니다.

따라서 여러분은 데이터 조작과 알고리즘 작성에 집중할 수 있으며, PyTorch가 이를 빠르게 실행되도록 보장합니다.

Tesla나 Meta (Facebook) 같은 기업들이 수백 개의 애플리케이션을 구동하고, 수천 대의 자동차를 운전하며, 수십억 명의 사람들에게 콘텐츠를 전달하기 위해 모델을 구축하는 데 PyTorch를 사용한다면, 개발 측면에서도 그 성능은 분명히 입증된 것입니다.

이 모듈에서 다룰 내용

이 과정은 여러 섹션(노트북)으로 나누어져 있습니다.

각 노트북은 PyTorch 내의 중요한 아이디어와 개념을 다룹니다.

이후의 노트북은 이전 노트북의 지식을 바탕으로 구성됩니다(번호는 00, 01, 02 순으로 시작하여 끝까지 이어집니다).

이 노트북은 머신러닝과 딥러닝의 기본 구성 요소인 텐서(tensor)를 다룹니다.

구체적으로 다음 내용을 다룹니다:

주제 내용
텐서 소개 텐서는 모든 머신러닝 및 딥러닝의 기본 구성 요소입니다.
텐서 생성하기 텐서는 거의 모든 종류의 데이터(이미지, 단어, 숫자 표)를 나타낼 수 있습니다.
텐서에서 정보 가져오기 정보를 텐서에 넣을 수 있다면, 다시 꺼내고 싶을 수도 있습니다.
텐서 조작하기 머신러닝 알고리즘(신경망 등)은 더하기, 곱하기, 결합하기 등 다양한 방식으로 텐서를 조작하는 과정을 포함합니다.
텐서 모양(shape) 다루기 머신러닝에서 가장 흔한 문제 중 하나는 모양 불일치(잘못된 모양의 텐서를 다른 텐서와 혼합하려는 경우)를 다루는 것입니다.
텐서 인덱싱 파이썬 리스트나 NumPy 배열에서 인덱싱을 해보셨다면 텐서와 매우 유사하지만, 훨씬 더 많은 차원을 가질 수 있습니다.
PyTorch 텐서와 NumPy 혼합하기 PyTorch는 텐서(torch.Tensor)를 다루고, NumPy는 배열(np.ndarray)을 선호합니다. 때로는 이 둘을 혼합하여 사용해야 할 때가 있습니다.
재현성 머신러닝은 매우 실험적이며 작동을 위해 많은 무작위성을 사용하기 때문에, 때로는 그 무작위성이 너무 무작위적이지 않기를 원할 때가 있습니다.
GPU에서 텐서 실행하기 GPU(그래픽 처리 장치)는 코드를 더 빠르게 만들어주며, PyTorch는 GPU에서 코드를 쉽게 실행할 수 있게 해줍니다.

도움을 받을 수 있는 곳

이 과정의 모든 자료는 GitHub에 있습니다.

문제가 발생하면 해당 페이지의 Discussions 페이지에서 질문할 수 있습니다.

또한 PyTorch와 관련된 모든 것에 대해 매우 도움이 되는 장소인 PyTorch 개발자 포럼도 있습니다.

PyTorch 임포트하기

참고: 이 노트북의 코드를 실행하기 전에 PyTorch 설치 단계를 거쳐야 합니다.

하지만 Google Colab에서 실행 중이라면, 모든 것이 작동할 것입니다(Google Colab에는 PyTorch 및 기타 라이브러리가 이미 설치되어 있습니다).

먼저 PyTorch를 임포트하고 사용 중인 버전을 확인해 보겠습니다.

import torch
torch.__version__
'1.10.0+cu111'

좋습니다. PyTorch 1.10.0(2021년 12월 기준)이 설치되어 있는 것 같네요. 즉, 이 자료를 학습할 때 PyTorch 1.10.0과의 호환성이 가장 높을 것이며, 버전 번호가 그보다 훨씬 높으면 약간의 불일치를 느낄 수 있습니다.

문제가 발생하면 GitHub Discussions 페이지에 게시해 주세요.

텐서 소개

이제 PyTorch를 임포트했으니 텐서에 대해 배워볼 시간입니다.

텐서는 머신러닝의 기본 구성 요소입니다.

텐서의 역할은 데이터를 수치적으로 나타내는 것입니다.

예를 들어, 이미지를 [3, 224, 224] 모양의 텐서로 나타낼 수 있는데, 이는 [색상_채널, 높이, 너비]를 의미합니다. 즉, 이미지는 3개의 색상 채널(빨강, 초록, 파랑), 224픽셀의 높이 및 224픽셀의 너비를 가집니다.

입력 이미지에서 이미지의 텐서 표현으로 전환되는 예시. 이미지는 3개의 색상 채널뿐만 아니라 높이와 너비를 나타내는 숫자로 분해됩니다.

텐서 용어(텐서를 설명하는 데 사용되는 언어)로 말하면, 이 텐서는 색상_채널, 높이, 너비에 대해 세 개의 차원을 가집니다.

하지만 너무 앞서 나갔네요.

직접 코딩하며 텐서에 대해 더 자세히 알아봅시다.

텐서 생성하기

PyTorch는 텐서를 사랑합니다. 얼마나 사랑하느냐 하면 torch.Tensor 클래스만을 위한 전용 문서 페이지가 있을 정도입니다.

첫 번째 숙제는 torch.Tensor 문서를 10분 동안 읽어보는 것입니다. 하지만 나중에 하셔도 됩니다.

코드를 작성해 봅시다.

가장 먼저 생성할 것은 스칼라(scalar)입니다.

스칼라는 단일 숫자이며, 텐서 용어로는 0차원 텐서입니다.

참고: 이것이 이 과정의 트렌드입니다. 구체적인 코드를 작성하는 데 집중할 것입니다. 하지만 종종 PyTorch 문서를 읽고 익숙해지는 연습을 과제로 내드릴 것입니다. 결국 이 과정을 마치고 나면 의심할 여지 없이 더 많은 것을 배우고 싶어질 것이기 때문입니다. 그리고 문서는 여러분이 매우 자주 찾게 될 장소입니다.

# 스칼라
scalar = torch.tensor(7)
scalar
tensor(7)

위의 출력이 tensor(7)로 나오는 것을 보셨나요?

이는 scalar가 단일 숫자이지만 torch.Tensor 유형임을 의미합니다.

ndim 속성을 사용하여 텐서의 차원을 확인할 수 있습니다.

scalar.ndim
0

텐서에서 숫자를 가져오고 싶다면 어떻게 해야 할까요?

즉, torch.Tensor를 파이썬 정수로 바꾸려면 말이죠.

그렇게 하려면 item() 메서드를 사용할 수 있습니다.

# 텐서 내의 파이썬 숫자 가져오기 (단일 요소 텐서에서만 작동함)
scalar.item()
7

좋습니다. 이제 벡터(vector)를 살펴봅시다.

벡터는 단일 차원 텐서이지만 많은 숫자를 포함할 수 있습니다.

예를 들어, 집의 [침실 수, 욕실 수]를 설명하기 위해 벡터 [3, 2]를 가질 수 있습니다. 또는 집의 [침실 수, 욕실 수, 주차 공간 수]를 설명하기 위해 [3, 2, 2]를 가질 수 있습니다.

여기서 중요한 경향은 벡터가 나타낼 수 있는 것이 유연하다는 것입니다(텐서도 마찬가지입니다).

# 벡터
vector = torch.tensor([7, 7])
vector
tensor([7, 7])

멋지네요. vector에는 제가 가장 좋아하는 숫자인 두 개의 7이 들어 있습니다.

차원이 몇 개일 것 같나요?

# 벡터의 차원 수 확인
vector.ndim
1

음, 이상하네요. vector에는 두 개의 숫자가 들어 있지만 차원은 하나뿐입니다.

비결을 하나 알려드리죠.

PyTorch에서 텐서의 차원 수는 바깥쪽 대괄호([)의 개수로 알 수 있으며, 한쪽만 세면 됩니다.

vector에는 대괄호가 몇 개 있나요?

텐서의 또 다른 중요한 개념은 shape 속성입니다. 모양(shape)은 내부의 요소가 어떻게 배열되어 있는지를 알려줍니다.

vector의 모양을 확인해 봅시다.

# 벡터의 모양 확인
vector.shape
torch.Size([2])

위의 결과는 torch.Size([2])를 반환하는데, 이는 우리 벡터의 모양이 [2]임을 의미합니다. 이는 대괄호 안에 두 개의 요소([7, 7])를 넣었기 때문입니다.

이제 행렬(matrix)을 살펴봅시다.

# 행렬
MATRIX = torch.tensor([[7, 8], 
                       [9, 10]])
MATRIX
tensor([[ 7,  8],
        [ 9, 10]])

와! 더 많은 숫자네요! 행렬은 벡터만큼 유연하지만 차원이 하나 더 추가되었습니다.

# 차원 수 확인
MATRIX.ndim
2

MATRIX는 두 개의 차원을 가집니다(한쪽 면의 바깥쪽 대괄호 개수를 세어보셨나요?).

어떤 모양(shape)을 가질 것 같나요?

MATRIX.shape
torch.Size([2, 2])

MATRIX가 깊이로 두 개의 요소, 너비로 두 개의 요소를 가지므로 torch.Size([2, 2])라는 출력을 얻습니다.

이제 텐서(tensor)를 만들어 볼까요?

# 텐서
TENSOR = torch.tensor([[[1, 2, 3],
                        [3, 6, 9],
                        [2, 4, 5]]])
TENSOR
tensor([[[1, 2, 3],
         [3, 6, 9],
         [2, 4, 5]]])

우와! 정말 멋진 텐서네요.

텐서는 거의 무엇이든 나타낼 수 있다는 점을 강조하고 싶습니다.

방금 만든 것은 스테이크와 아몬드 버터 매장(제가 가장 좋아하는 두 음식입니다)의 판매 수치일 수도 있습니다.

요일, 스테이크 판매량, 아몬드 버터 판매량을 보여주는 Google 스프레드시트의 간단한 텐서

차원이 몇 개일 것 같나요? (힌트: 대괄호 세기 비결을 사용하세요)

# TENSOR의 차원 수 확인
TENSOR.ndim
3

그 모양은 어떨까요?

# TENSOR의 모양 확인
TENSOR.shape
torch.Size([1, 3, 3])

좋습니다. torch.Size([1, 3, 3])이 출력되네요.

차원은 바깥쪽에서 안쪽 순서입니다.

즉, 3x3인 차원이 1개 있다는 뜻입니다.

다양한 텐서 차원의 예시

참고: scalarvector에는 소문자를 사용하고 MATRIXTENSOR에는 대문자를 사용한 것을 눈치채셨을 것입니다. 이는 의도적인 것입니다. 실제로 스칼라와 벡터는 ya와 같은 소문자로 표시되는 경우가 많습니다. 그리고 행렬과 텐서는 XW와 같은 대문자로 표시됩니다.

또한 행렬(matrix)과 텐서(tensor)라는 이름이 혼용되어 사용되는 것을 볼 수 있습니다. 이는 흔한 일입니다. PyTorch에서는 종종 torch.Tensor를 다루기 때문입니다(그래서 텐서라는 이름이 붙었습니다). 하지만 내부 내용의 모양과 차원에 따라 실제로 무엇인지가 결정됩니다.

요약해 봅시다.

이름 무엇인가요? 차원 수 소문자 또는 대문자 (보통/예시)
스칼라(scalar) 단일 숫자 0 소문자 (a)
벡터(vector) 방향이 있는 숫자(예: 방향이 있는 풍속)이지만 다른 많은 숫자도 가질 수 있음 1 소문자 (y)
행렬(matrix) 숫자의 2차원 배열 2 대문자 (Q)
텐서(tensor) 숫자의 n차원 배열 어떤 수든 가능, 0차원 텐서는 스칼라, 1차원 텐서는 벡터 대문자 (X)

스칼라 벡터 행렬 텐서의 모습

무작위 텐서

우리는 텐서가 어떤 형태의 데이터를 나타낸다는 것을 확인했습니다.

그리고 신경망과 같은 머신러닝 모델은 텐서 내의 패턴을 조사하고 찾기 위해 조작합니다.

하지만 PyTorch로 머신러닝 모델을 구축할 때 (우리가 했던 것처럼) 손으로 텐서를 직접 만드는 경우는 드뭅니다.

대신, 머신러닝 모델은 종종 대량의 무작위 숫자 텐서로 시작하여 데이터를 처리하면서 이러한 무작위 숫자를 조정하여 데이터를 더 잘 나타내도록 합니다.

본질적으로:

무작위 숫자로 시작 -> 데이터 확인 -> 무작위 숫자 업데이트 -> 데이터 확인 -> 무작위 숫자 업데이트...

데이터 과학자로서 여러분은 머신러닝 모델이 무작위 숫자를 어떻게 시작(초기화)하고, 데이터를 어떻게 확인(표현)하며, 어떻게 업데이트(최적화)할지 정의할 수 있습니다.

나중에 이러한 단계를 직접 실습해 볼 것입니다.

지금은 무작위 숫자로 텐서를 생성하는 방법을 알아봅시다.

torch.rand()를 사용하고 size 매개변수를 전달하여 이를 수행할 수 있습니다.

# 크기가 (3, 4)인 무작위 텐서 생성
random_tensor = torch.rand(size=(3, 4))
random_tensor, random_tensor.dtype
(tensor([[0.4090, 0.2527, 0.8699, 0.2002],
         [0.8421, 0.1428, 0.1431, 0.0111],
         [0.2281, 0.0345, 0.6734, 0.3866]]), torch.float32)

torch.rand()의 유연성은 size를 우리가 원하는 대로 조정할 수 있다는 점입니다.

예를 들어, 일반적인 이미지 모양인 [224, 224, 3] ([높이, 너비, 색상_채널])의 무작위 텐서를 원한다고 가정해 봅시다.

# 크기가 (224, 224, 3)인 무작위 텐서 생성
random_image_size_tensor = torch.rand(size=(224, 224, 3))
random_image_size_tensor.shape, random_image_size_tensor.ndim
(torch.Size([224, 224, 3]), 3)

0과 1

때로는 텐서를 0이나 1로 채우고 싶을 때가 있습니다.

이는 마스킹(masking) 작업에서 많이 발생합니다(예: 한 텐서의 일부 값을 0으로 마스킹하여 모델이 학습하지 않도록 알림).

torch.zeros()를 사용하여 0으로 가득 찬 텐서를 만들어 봅시다.

여기서도 size 매개변수가 사용됩니다.

# 모두 0인 텐서 생성
zeros = torch.zeros(size=(3, 4))
zeros, zeros.dtype
(tensor([[0., 0., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]]), torch.float32)

torch.ones()를 대신 사용하여 모두 1인 텐서를 만드는 것도 동일하게 수행할 수 있습니다.

# 모두 1인 텐서 생성
ones = torch.ones(size=(3, 4))
ones, ones.dtype
(tensor([[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]]), torch.float32)

범위 및 유사 텐서 생성

때로는 1에서 10 또는 0에서 100과 같은 숫자 범위가 필요할 수 있습니다.

torch.arange(start, end, step)을 사용하여 이를 수행할 수 있습니다.

여기서: * start = 범위의 시작 (예: 0) * end = 범위의 끝 (예: 10) * step = 각 값 사이의 간격 (예: 1)

참고: 파이썬에서는 range()를 사용하여 범위를 생성할 수 있습니다. 하지만 PyTorch에서 torch.range()는 더 이상 사용되지 않으며(deprecated) 미래에 오류가 발생할 수 있습니다.

# torch.arange() 사용, torch.range()는 권장되지 않음
zero_to_ten_deprecated = torch.range(0, 10) # 참고: 미래에 오류가 발생할 수 있음

# 0에서 10까지의 값 범위 생성
zero_to_ten = torch.arange(start=0, end=10, step=1)
zero_to_ten
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:2: UserWarning: torch.range is deprecated and will be removed in a future release because its behavior is inconsistent with Python's range builtin. Instead, use torch.arange, which produces values in [start, end).
  
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

때로는 다른 텐서와 동일한 모양을 가진 특정 유형의 텐서가 필요할 수 있습니다.

예를 들어, 이전 텐서와 모양이 같은 모두 0인 텐서가 있을 수 있습니다.

그렇게 하려면 각각 input과 동일한 모양의 0 또는 1로 채워진 텐서를 반환하는 torch.zeros_like(input) 또는 torch.ones_like(input)을 사용할 수 있습니다.

# 다른 텐서와 유사한 0으로 된 텐서 생성 가능
ten_zeros = torch.zeros_like(input=zero_to_ten) # 같은 모양을 가짐
ten_zeros
tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

텐서 데이터 타입

PyTorch에서는 다양한 텐서 데이터 타입을 사용할 수 있습니다.

어떤 것은 CPU에 특화되어 있고 어떤 것은 GPU에 더 적합합니다.

어떤 것이 무엇인지 익히는 데는 시간이 좀 걸릴 수 있습니다.

일반적으로 어디서나 torch.cuda가 보이면, 해당 텐서는 GPU에서 사용되고 있는 것입니다(Nvidia GPU는 CUDA라는 컴퓨팅 툴킷을 사용하기 때문입니다).

가장 일반적인 유형(및 일반적으로 기본값)은 torch.float32 또는 torch.float입니다.

이를 “32비트 부동 소수점”이라고 합니다.

하지만 16비트 부동 소수점(torch.float16 또는 torch.half)과 64비트 부동 소수점(torch.float64 또는 torch.double)도 있습니다.

더 복잡하게는 8비트, 16비트, 32비트, 64비트 정수도 있습니다.

그리고 더 많이요!

참고: 정수(integer)는 7과 같이 딱 떨어지는 숫자이고, 부동 소수점(float)은 7.0과 같이 소수점이 있습니다.

이 모든 이유가 있는 것은 컴퓨팅의 정밀도(precision) 때문입니다.

정밀도는 숫자를 설명하는 데 사용되는 세부 정보의 양입니다.

정밀도 값(8, 16, 32)이 높을수록 숫자를 표현하는 데 더 많은 세부 정보와 데이터가 사용됩니다.

이는 딥러닝과 수치 컴퓨팅에서 중요한데, 수많은 연산을 수행하기 때문에 계산해야 할 세부 정보가 많을수록 더 많은 컴퓨팅 파워를 사용해야 하기 때문입니다.

따라서 정밀도가 낮은 데이터 타입은 일반적으로 계산 속도가 빠르지만 정확도와 같은 평가 지표에서 약간의 성능 손실이 발생합니다(계산은 빠르지만 정확도는 떨어짐).

리소스: * 사용 가능한 모든 텐서 데이터 타입 목록은 PyTorch 문서를 참조하세요. * 컴퓨팅에서의 정밀도에 대한 개요는 Wikipedia 페이지를 읽어보세요.

특정 데이터 타입으로 텐서를 생성하는 방법을 알아봅시다. dtype 매개변수를 사용하여 수행할 수 있습니다.

# 텐서의 기본 데이터 타입은 float32
float_32_tensor = torch.tensor([3.0, 6.0, 9.0],
                               dtype=None, # 기본값은 None이며, torch.float32 또는 전달된 데이터 타입으로 설정됨
                               device=None, # 기본값은 None이며, 기본 텐서 유형을 사용함
                               requires_grad=False) # True이면 텐서에서 수행된 연산이 기록됨

float_32_tensor.shape, float_32_tensor.dtype, float_32_tensor.device
(torch.Size([3]), torch.float32, device(type='cpu'))

모양 문제(텐서 모양이 일치하지 않음) 외에도 PyTorch에서 겪게 될 가장 흔한 두 가지 문제는 데이터 타입과 장치(device) 문제입니다.

예를 들어, 텐서 중 하나는 torch.float32이고 다른 하나는 torch.float16인 경우입니다(PyTorch는 종종 텐서가 동일한 형식인 것을 선호합니다).

또는 텐서 중 하나는 CPU에 있고 다른 하나는 GPU에 있는 경우입니다(PyTorch는 텐서 간의 계산이 동일한 장치에서 수행되는 것을 선호합니다).

나중에 이 장치에 대한 이야기를 더 자세히 다룰 것입니다.

지금은 dtype=torch.float16인 텐서를 만들어 보겠습니다.

float_16_tensor = torch.tensor([3.0, 6.0, 9.0],
                               dtype=torch.float16) # torch.half도 작동함

float_16_tensor.dtype
torch.float16

텐서에서 정보 가져오기

텐서를 생성한 후에는 (또는 다른 사람이나 PyTorch 모듈이 대신 생성한 후에는) 텐서에서 정보를 얻고 싶을 수 있습니다.

이전에 살펴보았지만, 텐서에 대해 알아내고 싶은 가장 일반적인 세 가지 속성은 다음과 같습니다. * shape - 텐서의 모양은 무엇인가? (일부 연산에는 특정 모양 규칙이 필요함) * dtype - 텐서 내의 요소는 어떤 데이터 타입으로 저장되어 있는가? * device - 텐서는 어떤 장치에 저장되어 있는가? (보통 GPU 또는 CPU)

무작위 텐서를 생성하고 세부 정보를 알아봅시다.

# 텐서 생성
some_tensor = torch.rand(3, 4)

# 세부 정보 확인
print(some_tensor)
print(f"텐서의 모양: {some_tensor.shape}")
print(f"텐서의 데이터 타입: {some_tensor.dtype}")
print(f"텐서가 저장된 장치: {some_tensor.device}") # 기본값은 CPU
tensor([[0.7799, 0.8140, 0.0893, 0.2062],
        [0.7525, 0.3845, 0.8207, 0.4587],
        [0.9277, 0.8166, 0.9052, 0.0953]])
Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu

참고: PyTorch에서 문제가 발생하면 위의 세 가지 속성 중 하나와 관련된 경우가 매우 많습니다. 따라서 오류 메시지가 나타나면 스스로에게 “뭐, 뭐, 어디”라는 짧은 노래를 불러보세요. * “내 텐서의 모양은 뭐고? 데이터 타입은 뭐고, 어디에 저장되어 있지? 모양은 뭐고, 타입은 뭐고, 어디 어디 어디

텐서 조작 (텐서 연산)

딥러닝에서 데이터(이미지, 텍스트, 비디오, 오디오, 단백질 구조 등)는 텐서로 표현됩니다.

모델은 해당 텐서를 조사하고 입력 데이터의 패턴 표현을 생성하기 위해 텐서에 대해 일련의 연산(수백만 개 이상일 수 있음)을 수행하여 학습합니다.

이러한 연산은 종종 다음과 같은 멋진 춤과 같습니다. * 덧셈 * 뺄셈 * 곱셈 (요소별) * 나눗셈 * 행렬 곱셈

그리고 이것이 전부입니다. 물론 여기저기 몇 가지 더 있지만 이것이 신경망의 기본 구성 요소입니다.

이러한 구성 요소를 올바른 방식으로 쌓으면 (마치 레고처럼!) 가장 정교한 신경망을 만들 수 있습니다.

기본 연산

덧셈(+), 뺄셈(-), 곱셈(*)과 같은 몇 가지 기본적인 연산부터 시작하겠습니다.

여러분이 생각하는 대로 작동합니다.

# 값의 텐서를 생성하고 숫자를 더하기
tensor = torch.tensor([1, 2, 3])
tensor + 10
tensor([11, 12, 13])
# 10을 곱하기
tensor * 10
tensor([10, 20, 30])

위의 텐서 값이 tensor([110, 120, 130])으로 끝나지 않은 점에 유의하세요. 이는 텐서 내부의 값이 재할당되지 않는 한 변경되지 않기 때문입니다.

# 텐서는 재할당되지 않는 한 변경되지 않음
tensor
tensor([1, 2, 3])

숫자를 빼보고 이번에는 tensor 변수를 재할당해 보겠습니다.

# 빼고 재할당하기
tensor = tensor - 10
tensor
tensor([-9, -8, -7])
# 더하고 재할당하기
tensor = tensor + 10
tensor
tensor([1, 2, 3])

PyTorch에는 기본 연산을 수행하기 위한 torch.mul()(곱셈의 약자) 및 torch.add()와 같은 많은 내장 함수도 있습니다.

# torch 함수를 사용할 수도 있음
torch.multiply(tensor, 10)
tensor([10, 20, 30])
# 원본 텐서는 여전히 변경되지 않음
tensor
tensor([1, 2, 3])

하지만 torch.mul() 대신 *와 같은 연산자 기호를 사용하는 것이 더 일반적입니다.

# 요소별 곱셈 (각 요소는 해당 위치의 요소와 곱해짐, 인덱스 0->0, 1->1, 2->2)
print(tensor, "*", tensor)
print("결과:", tensor * tensor)
tensor([1, 2, 3]) * tensor([1, 2, 3])
Equals: tensor([1, 4, 9])

행렬 곱셈 (Matrix Multiplication)

머신러닝 및 딥러닝 알고리즘(신경망 등)에서 가장 흔한 연산 중 하나는 행렬 곱셈입니다.

PyTorch는 torch.matmul() 메서드에 행렬 곱셈 기능을 구현합니다.

기억해야 할 행렬 곱셈의 주요 규칙 두 가지는 다음과 같습니다. 1. 내부 차원(inner dimensions)이 일치해야 합니다. * (3, 2) @ (3, 2)는 작동하지 않음 * (2, 3) @ (3, 2)는 작동함 * (3, 2) @ (2, 3)은 작동함 2. 결과 행렬은 외부 차원(outer dimensions)의 모양을 가집니다. * (2, 3) @ (3, 2) -> (2, 2) * (3, 2) @ (2, 3) -> (3, 3)

참고: 파이썬에서 “@”는 행렬 곱셈 기호입니다.

리소스: torch.matmul()을 사용하는 행렬 곱셈의 모든 규칙은 PyTorch 문서에서 확인할 수 있습니다.

텐서를 생성하고 요소별 곱셈과 행렬 곱셈을 수행해 보겠습니다.

import torch
tensor = torch.tensor([1, 2, 3])
tensor.shape
torch.Size([3])

요소별 곱셈과 행렬 곱셈의 차이는 값의 합산입니다.

값이 [1, 2, 3]tensor 변수의 경우:

연산 계산 코드
요소별 곱셈 [1*1, 2*2, 3*3] = [1, 4, 9] tensor * tensor
행렬 곱셈 [1*1 + 2*2 + 3*3] = [14] tensor.matmul(tensor)
# 요소별 행렬 곱셈
tensor * tensor
tensor([1, 4, 9])
# 행렬 곱셈
torch.matmul(tensor, tensor)
tensor(14)
# 권장되지는 않지만 행렬 곱셈에 "@" 기호를 사용할 수도 있음
tensor @ tensor
tensor(14)

행렬 곱셈을 수동으로 할 수도 있지만 권장하지 않습니다.

내장된 torch.matmul() 메서드가 더 빠릅니다.

%%time
# 수동 행렬 곱셈
# (for 루프를 사용한 연산은 계산 비용이 많이 들므로 가급적 피하세요)
value = 0
for i in range(len(tensor)):
  value += tensor[i] * tensor[i]
value
CPU times: user 146 µs, sys: 38 µs, total: 184 µs
Wall time: 227 µs
%%time
torch.matmul(tensor, tensor)
CPU times: user 27 µs, sys: 7 µs, total: 34 µs
Wall time: 36.7 µs
tensor(14)

딥러닝에서 가장 흔한 오류 중 하나 (모양 오류)

딥러닝의 대부분은 행렬을 곱하고 연산을 수행하는 것이고, 행렬에는 결합할 수 있는 모양과 크기에 대한 엄격한 규칙이 있기 때문에 딥러닝에서 겪게 될 가장 흔한 오류 중 하나는 모양 불일치(shape mismatch)입니다.

# 모양이 올바른 방식이어야 함
tensor_A = torch.tensor([[1, 2],
                         [3, 4],
                         [5, 6]], dtype=torch.float32)

tensor_B = torch.tensor([[7, 10],
                         [8, 11], 
                         [9, 12]], dtype=torch.float32)

torch.matmul(tensor_A, tensor_B) # (오류 발생)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-37-aceec990e652> in <module>()
      8                          [9, 12]], dtype=torch.float32)
      9 
---> 10 torch.matmul(tensor_A, tensor_B) # (오류 발생)

RuntimeError: mat1 and mat2 shapes cannot be multiplied (3x2 and 3x2)

tensor_Atensor_B 사이의 내부 차원을 일치시켜 행렬 곱셈이 작동하도록 만들 수 있습니다.

이를 수행하는 방법 중 하나는 전치(transpose)(주어진 텐서의 차원을 전환함)를 사용하는 것입니다.

PyTorch에서는 다음 중 하나를 사용하여 전치를 수행할 수 있습니다. * torch.transpose(input, dim0, dim1) - 여기서 input은 전치할 텐서이고 dim0dim1은 교체할 차원입니다. * tensor.T - 여기서 tensor는 전치할 텐서입니다.

후자를 시도해 봅시다.

# tensor_A와 tensor_B 확인
print(tensor_A)
print(tensor_B)
tensor([[1., 2.],
        [3., 4.],
        [5., 6.]])
tensor([[ 7., 10.],
        [ 8., 11.],
        [ 9., 12.]])
# tensor_A와 tensor_B.T 확인
print(tensor_A)
print(tensor_B.T)
tensor([[1., 2.],
        [3., 4.],
        [5., 6.]])
tensor([[ 7.,  8.,  9.],
        [10., 11., 12.]])
# tensor_B가 전치되었을 때 연산이 작동함
print(f"원본 모양: tensor_A = {tensor_A.shape}, tensor_B = {tensor_B.shape}\n")
print(f"새로운 모양: tensor_A = {tensor_A.shape} (이전과 동일), tensor_B.T = {tensor_B.T.shape}\n")
print(f"곱셈 수행: {tensor_A.shape} * {tensor_B.T.shape} <- 내부 차원 일치\n")
print("출력:\n")
output = torch.matmul(tensor_A, tensor_B.T)
print(output) 
print(f"\n출력 모양: {output.shape}")
Original shapes: tensor_A = torch.Size([3, 2]), tensor_B = torch.Size([3, 2])

New shapes: tensor_A = torch.Size([3, 2]) (same as above), tensor_B.T = torch.Size([2, 3])

Multiplying: torch.Size([3, 2]) * torch.Size([2, 3]) <- inner dimensions match

Output:

tensor([[ 27.,  30.,  33.],
        [ 61.,  68.,  75.],
        [ 95., 106., 117.]])

Output shape: torch.Size([3, 3])

torch.matmul()의 줄임말인 torch.mm()을 사용할 수도 있습니다.

# torch.mm은 matmul의 줄임말
torch.mm(tensor_A, tensor_B.T)
tensor([[ 27.,  30.,  33.],
        [ 61.,  68.,  75.],
        [ 95., 106., 117.]])

전치가 없으면 행렬 곱셈의 규칙이 충족되지 않고 위와 같은 오류가 발생합니다.

시각화 자료는 어떨까요?

행렬 곱셈의 시각적 데모

http://matrixmultiplication.xyz/ 에서 이와 같은 자신만의 행렬 곱셈 시각화 자료를 만들 수 있습니다.

참고: 이와 같은 행렬 곱셈은 두 행렬의 내적(dot product)이라고도 합니다.

신경망은 행렬 곱셈과 내적으로 가득 차 있습니다.

피드포워드 레이어(feed-forward layer) 또는 완전 연결 레이어(fully connected layer)라고도 하는 torch.nn.Linear() 모듈(나중에 실제 작동하는 모습을 볼 것입니다)은 입력 x와 가중치 행렬 A 사이의 행렬 곱셈을 구현합니다.

\[ y = x\cdot{A^T} + b \]

여기서: * x는 레이어의 입력입니다(딥러닝은 torch.nn.Linear() 및 기타 레이어를 서로 겹쳐 쌓은 것입니다). * A는 레이어에 의해 생성된 가중치 행렬입니다. 이것은 무작위 숫자로 시작하여 신경망이 데이터의 패턴을 더 잘 나타내도록 학습함에 따라 조정됩니다(가중치 행렬이 전치되기 때문에 “T”에 유의하세요). * 참고: 가중치 행렬을 나타내기 위해 WX와 같은 다른 문자가 사용되는 것을 종종 볼 수 있습니다. * b는 가중치와 입력을 약간 오프셋하는 데 사용되는 편향(bias) 용어입니다. * y는 출력입니다(입력에서 패턴을 발견하기를 바라며 입력을 조작한 결과입니다).

이것은 선형 함수(고등학교나 다른 곳에서 \(y = mx+b\)와 같은 것을 본 적이 있을 것입니다)이며, 직선을 그리는 데 사용될 수 있습니다!

선형 레이어를 가지고 놀아봅시다.

아래에서 in_featuresout_features의 값을 변경하고 어떤 일이 일어나는지 확인해 보세요.

모양과 관련하여 눈에 띄는 점이 있나요?

# 선형 레이어는 무작위 가중치 행렬로 시작하므로 재현 가능하게 만듭시다(나중에 자세히 설명)
torch.manual_seed(42)
# 이것은 행렬 곱셈을 사용함
linear = torch.nn.Linear(in_features=2, # in_features = 입력의 내부 차원과 일치해야 함
                         out_features=6) # out_features = 출력 값을 설명함
x = tensor_A
output = linear(x)
print(f"입력 모양: {x.shape}\n")
print(f"출력:\n{output}\n\n출력 모양: {output.shape}")
Input shape: torch.Size([3, 2])

Output:
tensor([[2.2368, 1.2292, 0.4714, 0.3864, 0.1309, 0.9838],
        [4.4919, 2.1970, 0.4469, 0.5285, 0.3401, 2.4777],
        [6.7469, 3.1648, 0.4224, 0.6705, 0.5493, 3.9716]],
       grad_fn=<AddmmBackward0>)

Output shape: torch.Size([3, 6])

질문: 위에서 in_features를 2에서 3으로 변경하면 어떻게 되나요? 오류가 발생하나요? 오류에 대응하기 위해 입력(x)의 모양을 어떻게 변경할 수 있을까요? 힌트: 위에서 tensor_B에 무엇을 해야 했나요?

행렬 곱셈을 처음 접한다면 처음에는 혼란스러운 주제일 수 있습니다.

하지만 몇 번 연습하고 신경망을 직접 분석해 보면 어디에나 있다는 것을 알게 될 것입니다.

기억하세요, 행렬 곱셈이 여러분에게 필요한 전부입니다.

행렬 곱셈이 여러분에게 필요한 전부입니다

신경망 레이어를 파헤치고 직접 구축하기 시작하면 어디에서나 행렬 곱셈을 발견하게 될 것입니다. 출처: https://marksaroufim.substack.com/p/working-class-deep-learner

최소, 최대, 평균, 합계 등 찾기 (집계)

텐서를 조작하는 몇 가지 방법을 살펴보았으니, 이제 텐서를 집계하는(많은 값에서 적은 값으로 가는) 몇 가지 방법을 살펴보겠습니다.

먼저 텐서를 생성한 다음 그 텐서의 최대값, 최소값, 평균 및 합계를 구합니다.

# 텐서 생성
x = torch.arange(0, 100, 10)
x
tensor([ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90])

이제 집계를 수행해 봅시다.

print(f"최소값: {x.min()}")
print(f"최대값: {x.max()}")
# print(f"평균: {x.mean()}") # 오류 발생
print(f"평균: {x.type(torch.float32).mean()}") # float 데이터 타입 없이는 작동하지 않음
print(f"합계: {x.sum()}")
Minimum: 0
Maximum: 90
Mean: 45.0
Sum: 450

참고: torch.mean()과 같은 일부 메서드는 텐서가 torch.float32(가장 일반적임) 또는 다른 특정 데이터 타입이어야 하며, 그렇지 않으면 연산이 실패한다는 것을 알 수 있습니다.

torch 메서드를 사용하여 위와 동일하게 수행할 수도 있습니다.

torch.max(x), torch.min(x), torch.mean(x.type(torch.float32)), torch.sum(x)
(tensor(90), tensor(0), tensor(45.), tensor(450))

위치별 최소/최대

각각 torch.argmax()torch.argmin()을 사용하여 최대값 또는 최소값이 발생하는 텐서의 인덱스를 찾을 수도 있습니다.

이는 실제 값이 아닌 가장 높은(또는 가장 낮은) 값이 있는 위치만 원할 때 유용합니다(나중에 softmax 활성화 함수를 사용할 때 이 내용을 보게 될 것입니다).

# 텐서 생성
tensor = torch.arange(10, 100, 10)
print(f"텐서: {tensor}")

# 최대값 및 최소값의 인덱스 반환
print(f"최대값이 있는 인덱스: {tensor.argmax()}")
print(f"최소값이 있는 인덱스: {tensor.argmin()}")
Tensor: tensor([10, 20, 30, 40, 50, 60, 70, 80, 90])
Index where max value occurs: 8
Index where min value occurs: 0

텐서 데이터 타입 변경

앞서 언급했듯이 딥러닝 연산의 일반적인 문제는 텐서의 데이터 타입이 서로 다른 것입니다.

하나의 텐서가 torch.float64이고 다른 하나가 torch.float32이면 오류가 발생할 수 있습니다.

하지만 해결 방법이 있습니다.

torch.Tensor.type(dtype=None)을 사용하여 텐서의 데이터 타입을 변경할 수 있습니다. 여기서 dtype 매개변수는 사용하려는 데이터 타입입니다.

먼저 텐서를 생성하고 데이터 타입을 확인해 보겠습니다(기본값은 torch.float32입니다).

# 텐서 생성 및 데이터 타입 확인
tensor = torch.arange(10., 100., 10.)
tensor.dtype
torch.float32

이제 이전과 동일하게 다른 텐서를 생성하지만 데이터 타입을 torch.float16으로 변경해 보겠습니다.

# float16 텐서 생성
tensor_float16 = tensor.type(torch.float16)
tensor_float16
tensor([10., 20., 30., 40., 50., 60., 70., 80., 90.], dtype=torch.float16)

그리고 torch.int8 텐서를 만들기 위해 비슷한 작업을 수행할 수 있습니다.

# int8 텐서 생성
tensor_int8 = tensor.type(torch.int8)
tensor_int8
tensor([10, 20, 30, 40, 50, 60, 70, 80, 90], dtype=torch.int8)

참고: 서로 다른 데이터 타입은 처음에는 혼란스러울 수 있습니다. 하지만 이렇게 생각해 보세요. 숫자(예: 32, 16, 8)가 작을수록 컴퓨터가 값을 덜 정밀하게 저장합니다. 저장 용량이 적으면 일반적으로 계산 속도가 빨라지고 전체 모델 크기가 작아집니다. 모바일 기반 신경망은 종종 8비트 정수로 작동하는데, float32 버전보다 작고 실행 속도가 빠르지만 정확도는 떨어집니다. 이에 대한 자세한 내용은 컴퓨팅 정밀도에 대해 읽어보시기 바랍니다.

과제: 지금까지 꽤 많은 텐서 메서드를 다루었지만 torch.Tensor 문서에는 훨씬 더 많은 메서드가 있습니다. 10분 정도 시간을 내어 스크롤하면서 눈에 띄는 것을 찾아보시길 권장합니다. 클릭해 본 다음 직접 코드로 작성하여 어떤 일이 일어나는지 확인해 보세요.

재구조화, 쌓기, 압축 및 압축 해제 (Reshaping, stacking, squeezing and unsqueezing)

종종 내부의 값을 실제로 변경하지 않고 텐서의 모양을 바꾸거나 차원을 변경하고 싶을 때가 있습니다.

이를 위해 인기 있는 메서드는 다음과 같습니다.

메서드 한 줄 설명
torch.reshape(input, shape) inputshape(호환되는 경우)로 재구조화함. torch.Tensor.reshape()도 사용 가능.
torch.Tensor.view(shape) 원래 텐서의 다른 shape 뷰(view)를 반환하지만 원래 텐서와 동일한 데이터를 공유함.
torch.stack(tensors, dim=0) 새로운 차원(dim)을 따라 일련의 tensors를 결합함. 모든 tensors는 크기가 같아야 함.
torch.squeeze(input) input에서 값이 1인 모든 차원을 제거(압축)함.
torch.unsqueeze(input, dim) dim 위치에 값이 1인 차원이 추가된 input을 반환함.
torch.permute(input, dims) 차원이 dims로 순열(재배열)된 원래 input를 반환함.

왜 이런 작업을 할까요?

딥러닝 모델(신경망)은 어떤 방식으로든 텐서를 조작하는 것이 전부이기 때문입니다. 그리고 행렬 곱셈의 규칙 때문에 모양이 맞지 않으면 오류가 발생합니다. 이러한 메서드는 텐서의 올바른 요소가 다른 텐서의 올바른 요소와 섞이도록 도와줍니다.

한번 시도해 봅시다.

먼저 텐서를 생성합니다.

# 텐서 생성
import torch
x = torch.arange(1., 8.)
x, x.shape
(tensor([1., 2., 3., 4., 5., 6., 7.]), torch.Size([7]))

이제 torch.reshape()를 사용하여 차원을 추가해 보겠습니다.

# 추가 차원 더하기
x_reshaped = x.reshape(1, 7)
x_reshaped, x_reshaped.shape
(tensor([[1., 2., 3., 4., 5., 6., 7.]]), torch.Size([1, 7]))

torch.view()를 사용하여 뷰를 변경할 수도 있습니다.

# 뷰 변경 (원본과 동일한 데이터를 유지하면서 뷰만 변경)
# 자세히 보기: https://stackoverflow.com/a/54507446/7900723
z = x.view(1, 7)
z, z.shape
(tensor([[1., 2., 3., 4., 5., 6., 7.]]), torch.Size([1, 7]))

torch.view()로 텐서의 뷰를 변경하는 것은 실제로는 동일한 텐서의 새로운 뷰를 생성할 뿐이라는 점을 기억하세요.

따라서 뷰를 변경하면 원본 텐서도 변경됩니다.

# z를 변경하면 x도 변경됨
z[:, 0] = 5
z, x
(tensor([[5., 2., 3., 4., 5., 6., 7.]]), tensor([5., 2., 3., 4., 5., 6., 7.]))

새로운 텐서를 자신 위에 네 번 쌓고 싶다면 torch.stack()을 사용하여 수행할 수 있습니다.

# 텐서를 서로 위에 쌓기
x_stacked = torch.stack([x, x, x, x], dim=0) # dim을 dim=1로 변경하고 어떤 일이 일어나는지 확인해 보세요
x_stacked
tensor([[5., 2., 3., 4., 5., 6., 7.],
        [5., 2., 3., 4., 5., 6., 7.],
        [5., 2., 3., 4., 5., 6., 7.],
        [5., 2., 3., 4., 5., 6., 7.]])

텐서에서 모든 단일 차원을 제거하는 것은 어떨까요?

그렇게 하려면 torch.squeeze()를 사용할 수 있습니다(저는 이를 텐서를 압축하여 1보다 큰 차원만 남게 하는 것으로 기억합니다).

print(f"이전 텐서: {x_reshaped}")
print(f"이전 모양: {x_reshaped.shape}")

# x_reshaped에서 추가 차원 제거
x_squeezed = x_reshaped.squeeze()
print(f"\n새로운 텐서: {x_squeezed}")
print(f"새로운 모양: {x_squeezed.shape}")
Previous tensor: tensor([[5., 2., 3., 4., 5., 6., 7.]])
Previous shape: torch.Size([1, 7])

New tensor: tensor([5., 2., 3., 4., 5., 6., 7.])
New shape: torch.Size([7])

그리고 torch.squeeze()의 반대 작업을 하려면 torch.unsqueeze()를 사용하여 특정 인덱스에 값이 1인 차원을 추가할 수 있습니다.

print(f"이전 텐서: {x_squeezed}")
print(f"이전 모양: {x_squeezed.shape}")

## unsqueeze로 추가 차원 더하기
x_unsqueezed = x_squeezed.unsqueeze(dim=0)
print(f"\n새로운 텐서: {x_unsqueezed}")
print(f"새로운 모양: {x_unsqueezed.shape}")
Previous tensor: tensor([5., 2., 3., 4., 5., 6., 7.])
Previous shape: torch.Size([7])

New tensor: tensor([[5., 2., 3., 4., 5., 6., 7.]])
New shape: torch.Size([1, 7])

torch.permute(input, dims)를 사용하여 축 값의 순서를 재배열할 수도 있습니다. 여기서 input은 새로운 dims를 가진 로 변환됩니다.

# 특정 모양의 텐서 생성
x_original = torch.rand(size=(224, 224, 3))

# 원본 텐서를 순열하여 축 순서 재배열
x_permuted = x_original.permute(2, 0, 1) # 축을 0->1, 1->2, 2->0으로 이동

print(f"이전 모양: {x_original.shape}")
print(f"새로운 모양: {x_permuted.shape}")
Previous shape: torch.Size([224, 224, 3])
New shape: torch.Size([3, 224, 224])

참고: 순열(permuting)은 (원본과 동일한 데이터를 공유함)를 반환하므로 순열된 텐서의 값은 원본 텐서와 동일하며 뷰에서 값을 변경하면 원본의 값도 변경됩니다.

인덱싱 (텐서에서 데이터 선택)

때로는 텐서에서 특정 데이터(예: 첫 번째 열 또는 두 번째 행만)를 선택하고 싶을 때가 있습니다.

그렇게 하려면 인덱싱을 사용할 수 있습니다.

파이썬 리스트나 NumPy 배열에서 인덱싱을 해보셨다면 PyTorch 텐서의 인덱싱도 매우 유사합니다.

# 텐서 생성
import torch
x = torch.arange(1, 10).reshape(1, 3, 3)
x, x.shape
(tensor([[[1, 2, 3],
          [4, 5, 6],
          [7, 8, 9]]]), torch.Size([1, 3, 3]))

값 인덱싱은 바깥쪽 차원에서 안쪽 차원 순서로 진행됩니다(대괄호를 확인하세요).

# 대괄호별로 인덱싱해 봅시다
print(f"첫 번째 대괄호:\n{x[0]}") 
print(f"두 번째 대괄호: {x[0][0]}") 
print(f"세 번째 대괄호: {x[0][0][0]}")
First square bracket:
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])
Second square bracket: tensor([1, 2, 3])
Third square bracket: 1

:을 사용하여 “이 차원의 모든 값”을 지정한 다음 쉼표(,)를 사용하여 다른 차원을 추가할 수도 있습니다.

# 0번째 차원의 모든 값과 1번째 차원의 0번 인덱스 가져오기
x[:, 0]
tensor([[1, 2, 3]])
# 0번째 및 1번째 차원의 모든 값을 가져오되 2번째 차원의 인덱스 1만 가져오기
x[:, :, 1]
tensor([[2, 5, 8]])
# 0차원의 모든 값을 가져오되 1차원과 2차원의 인덱스 1 값만 가져오기
x[:, 1, 1]
tensor([5])
# 0번째 및 1번째 차원의 인덱스 0과 2번째 차원의 모든 값 가져오기
x[0, 0, :] # x[0][0]과 동일
tensor([1, 2, 3])

인덱싱은 처음에는 상당히 혼란스러울 수 있으며, 특히 텐서가 커질수록 더 그렇습니다(저도 올바르게 인덱싱하기 위해 여러 번 시도하곤 합니다). 하지만 약간의 연습과 데이터 탐험가의 좌우명인 (시각화, 시각화, 시각화)를 따르다 보면 요령을 터득하기 시작할 것입니다.

PyTorch 텐서와 NumPy

NumPy는 인기 있는 파이썬 수치 컴퓨팅 라이브러리이므로 PyTorch는 이와 잘 상호 작용할 수 있는 기능을 갖추고 있습니다.

NumPy에서 PyTorch로(그리고 그 반대로) 전환하는 데 사용하려는 두 가지 주요 메서드는 다음과 같습니다. * torch.from_numpy(ndarray) - NumPy 배열 -> PyTorch 텐서. * torch.Tensor.numpy() - PyTorch 텐서 -> NumPy 배열.

한번 시도해 봅시다.

# NumPy 배열에서 텐서로
import torch
import numpy as np
array = np.arange(1.0, 8.0)
tensor = torch.from_numpy(array)
array, tensor
(array([1., 2., 3., 4., 5., 6., 7.]),
 tensor([1., 2., 3., 4., 5., 6., 7.], dtype=torch.float64))

참고: 기본적으로 NumPy 배열은 float64 데이터 타입으로 생성되며 이를 PyTorch 텐서로 변환하면 동일한 데이터 타입이 유지됩니다(위와 같이).

그러나 많은 PyTorch 계산은 기본적으로 float32를 사용합니다.

따라서 NumPy 배열(float64) -> PyTorch 텐서(float64) -> PyTorch 텐서(float32)로 변환하려면 tensor = torch.from_numpy(array).type(torch.float32)를 사용할 수 있습니다.

위에서 tensor를 재할당했으므로 텐서를 변경해도 배열은 그대로 유지됩니다.

# 배열을 변경하고 텐서는 유지하기
array = array + 1
array, tensor
(array([2., 3., 4., 5., 6., 7., 8.]),
 tensor([1., 2., 3., 4., 5., 6., 7.], dtype=torch.float64))

PyTorch 텐서에서 NumPy 배열로 가려면 tensor.numpy()를 호출하면 됩니다.

# 텐서에서 NumPy 배열로
tensor = torch.ones(7) # dtype=float32인 1로 구성된 텐서 생성
numpy_tensor = tensor.numpy() # 변경하지 않는 한 dtype=float32가 됨
tensor, numpy_tensor
(tensor([1., 1., 1., 1., 1., 1., 1.]),
 array([1., 1., 1., 1., 1., 1., 1.], dtype=float32))

그리고 위와 동일한 규칙이 적용됩니다. 원래 tensor를 변경해도 새로운 numpy_tensor는 그대로 유지됩니다.

# 텐서를 변경하고 배열은 동일하게 유지하기
tensor = tensor + 1
tensor, numpy_tensor
(tensor([2., 2., 2., 2., 2., 2., 2.]),
 array([1., 1., 1., 1., 1., 1., 1.], dtype=float32))

재현성 (무작위성 제어하기)

신경망과 머신러닝에 대해 더 배우다 보면 무작위성이 얼마나 큰 역할을 하는지 알게 될 것입니다.

음, 실제로는 의사 무작위성(pseudorandomness)입니다. 결국 설계된 대로 컴퓨터는 근본적으로 결정론적(각 단계가 예측 가능함)이므로 컴퓨터가 생성하는 무작위성은 시뮬레이션된 무작위성입니다(이에 대해서도 논란이 있지만 저는 컴퓨터 과학자가 아니므로 직접 더 알아보시기 바랍니다).

그렇다면 이것이 신경망 및 딥러닝과 어떤 관련이 있을까요?

우리는 신경망이 데이터의 패턴을 설명하기 위해 무작위 숫자로 시작하고(이 숫자는 서투른 설명입니다), 텐서 연산(및 아직 논의하지 않은 몇 가지 다른 것들)을 사용하여 해당 무작위 숫자를 개선하여 데이터의 패턴을 더 잘 설명하려고 시도한다는 것을 논의했습니다.

요약하자면:

무작위 숫자로 시작 -> 텐서 연산 -> 더 나아지도록 시도 (반복 반복 반복)

무작위성은 훌륭하고 강력하지만 때로는 무작위성이 조금 덜했으면 할 때가 있습니다.

왜일까요?

반복 가능한 실험을 수행할 수 있기 때문입니다.

예를 들어, 여러분이 성능 X를 달성할 수 있는 알고리즘을 만들었다고 가정해 봅시다.

그러면 친구가 여러분이 미치지 않았는지 확인하기 위해 시도해 봅니다.

그들은 어떻게 그런 일을 할 수 있을까요?

여기서 재현성(reproducibility)이 등장합니다.

다시 말해, 여러분이 얻은 것과 동일한 코드를 실행하여 여러분의 컴퓨터에서와 동일한(또는 매우 유사한) 결과를 내 컴퓨터에서도 얻을 수 있습니까?

PyTorch에서 재현성의 간단한 예를 들어봅시다.

먼저 두 개의 무작위 텐서를 생성해 보겠습니다. 무작위이기 때문에 서로 다를 것으로 예상하시죠?

import torch

# 두 개의 무작위 텐서 생성
random_tensor_A = torch.rand(3, 4)
random_tensor_B = torch.rand(3, 4)

print(f"텐서 A:\n{random_tensor_A}\n")
print(f"텐서 B:\n{random_tensor_B}\n")
print(f"텐서 A와 텐서 B가 같습니까? (어디든)")
random_tensor_A == random_tensor_B
Tensor A:
tensor([[0.8016, 0.3649, 0.6286, 0.9663],
        [0.7687, 0.4566, 0.5745, 0.9200],
        [0.3230, 0.8613, 0.0919, 0.3102]])

Tensor B:
tensor([[0.9536, 0.6002, 0.0351, 0.6826],
        [0.3743, 0.5220, 0.1336, 0.9666],
        [0.9754, 0.8474, 0.8988, 0.1105]])

Does Tensor A equal Tensor B? (anywhere)
tensor([[False, False, False, False],
        [False, False, False, False],
        [False, False, False, False]])

예상했던 대로 텐서는 서로 다른 값으로 나옵니다.

하지만 동일한 값을 가진 두 개의 무작위 텐서를 만들고 싶다면 어떻게 해야 할까요?

즉, 텐서에 여전히 무작위 값이 포함되어 있지만 동일한 풍미(flavour)를 갖기를 원하는 것입니다.

여기서 torch.manual_seed(seed)가 등장합니다. 여기서 seed는 무작위성에 풍미를 더하는 정수(42와 같지만 무엇이든 될 수 있음)입니다.

좀 더 풍미가 더해진 무작위 텐서를 만들어 시도해 봅시다.

import torch
import random

# 무작위 시드 설정
RANDOM_SEED=42 # 이 값을 다른 값으로 변경하고 아래 숫자에 어떤 일이 일어나는지 확인해 보세요
torch.manual_seed(seed=RANDOM_SEED) 
random_tensor_C = torch.rand(3, 4)

# 새로운 rand()가 호출될 때마다 시드를 재설정해야 함
# 이것이 없으면 tensor_D는 tensor_C와 달라짐
torch.random.manual_seed(seed=RANDOM_SEED) # 이 라인을 주석 처리하고 어떤 일이 일어나는지 확인해 보세요
random_tensor_D = torch.rand(3, 4)

print(f"텐서 C:\n{random_tensor_C}\n")
print(f"텐서 D:\n{random_tensor_D}\n")
print(f"텐서 C와 텐서 D가 같습니까? (어디든)")
random_tensor_C == random_tensor_D
Tensor C:
tensor([[0.8823, 0.9150, 0.3829, 0.9593],
        [0.3904, 0.6009, 0.2566, 0.7936],
        [0.9408, 0.1332, 0.9346, 0.5936]])

Tensor D:
tensor([[0.8823, 0.9150, 0.3829, 0.9593],
        [0.3904, 0.6009, 0.2566, 0.7936],
        [0.9408, 0.1332, 0.9346, 0.5936]])

Does Tensor C equal Tensor D? (anywhere)
tensor([[True, True, True, True],
        [True, True, True, True],
        [True, True, True, True]])

좋네요!

시드 설정이 작동한 것 같습니다.

리소스: 우리가 방금 다룬 내용은 PyTorch 재현성의 겉핥기 수준일 뿐입니다. 일반적인 재현성과 무작위 시드에 대한 자세한 내용은 다음을 확인하세요. * PyTorch 재현성 문서 (10분 동안 이 문서를 읽어보는 것이 좋은 연습이 될 것입니다. 지금 당장 이해하지 못하더라도 이를 인지하고 있는 것이 중요합니다). * Wikipedia 무작위 시드 페이지 (무작위 시드 및 의사 무작위성에 대한 좋은 개요를 제공합니다).

GPU에서 텐서 실행하기 (및 계산 가속화)

딥러닝 알고리즘은 많은 수치 연산을 필요로 합니다.

그리고 기본적으로 이러한 연산은 종종 CPU(중앙 처리 장치)에서 수행됩니다.

하지만 GPU(그래픽 처리 장치)라는 또 다른 일반적인 하드웨어가 있는데, 이는 신경망이 필요로 하는 특정 유형의 연산(행렬 곱셈)을 수행하는 데 종종 CPU보다 훨씬 빠릅니다.

여러분의 컴퓨터에도 하나 있을 수 있습니다.

그렇다면 신경망을 학습시킬 때마다 가급적 이를 사용해야 합니다. 왜냐하면 학습 시간을 비약적으로 단축할 수 있기 때문입니다.

먼저 GPU에 액세스하고 다음으로 PyTorch가 GPU를 사용하도록 하는 몇 가지 방법이 있습니다.

참고: 이 과정 전반에서 “GPU”를 언급할 때는 달리 명시되지 않는 한 CUDA가 활성화된 Nvidia GPU를 의미합니다(CUDA는 GPU를 그래픽뿐만 아니라 일반적인 용도의 컴퓨팅에 사용할 수 있도록 도와주는 컴퓨팅 플랫폼 및 API입니다).

1. GPU 확보하기

GPU라고 말할 때 무슨 일이 일어나고 있는지 이미 알고 계실 수도 있습니다. 하지만 그렇지 않다면 GPU에 액세스하는 몇 가지 방법이 있습니다.

방법 설정 난이도 장점 단점 설정 방법
Google Colab 쉬움 무료 사용 가능, 설정 거의 불필요, 링크만으로 작업 공유 가능 데이터 출력 저장 안 됨, 컴퓨팅 제한적, 타임아웃 발생 가능 Google Colab 가이드 따르기
개인 기기 사용 중간 자신의 기기에서 모든 것을 로컬로 실행 GPU는 무료가 아님, 초기 비용 필요 PyTorch 설치 지침 따르기
클라우드 컴퓨팅 (AWS, GCP, Azure) 중간-어려움 적은 초기 비용, 거의 무한한 컴퓨팅 액세스 계속 실행하면 비쌀 수 있음, 올바르게 설정하는 데 시간이 걸림 PyTorch 클라우드 설치 지침 따르기

GPU를 사용하기 위한 더 많은 옵션이 있지만 지금은 위의 세 가지면 충분합니다.

개인적으로 저는 소규모 실험(및 이 과정 제작)에는 Google Colab과 개인 컴퓨터를 혼용해서 사용하고, 더 많은 컴퓨팅 파워가 필요할 때는 클라우드 리소스를 활용합니다.

리소스: 직접 GPU를 구매하고 싶지만 무엇을 사야 할지 모르겠다면 Tim Dettmers의 훌륭한 가이드를 참조하세요.

Nvidia GPU에 액세스할 수 있는지 확인하려면 !nvidia-smi를 실행할 수 있습니다. 여기서 !(뱅이라고도 함)는 “이를 명령줄에서 실행하라”는 의미입니다.

!nvidia-smi
Thu Feb 10 02:09:18 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03    Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla P100-PCIE...  Off  | 00000000:00:04.0 Off |                    0 |
| N/A   36C    P0    28W / 250W |      0MiB / 16280MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

접근 가능한 Nvidia GPU가 없는 경우 위 명령은 다음과 같은 내용을 출력합니다.

NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver. Make sure that the latest NVIDIA driver is installed and running.

이 경우 위로 돌아가 설치 단계를 따르세요.

GPU가 있는 경우 위 라인은 다음과 같은 내용을 출력합니다.

Wed Jan 19 22:09:08 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 495.46       Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla P100-PCIE...  Off  | 00000000:00:04.0 Off |                    0 |
| N/A   35C    P0    27W / 250W |      0MiB / 16280MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

2. PyTorch가 GPU에서 실행되도록 하기

액세스할 준비가 된 GPU가 있으면 다음 단계는 PyTorch가 데이터를 저장(텐서)하고 데이터를 계산(텐서에 대한 연산 수행)하는 데 GPU를 사용하도록 하는 것입니다.

그렇게 하려면 torch.cuda 패키지를 사용할 수 있습니다.

말로 설명하기보다 직접 해봅시다.

torch.cuda.is_available()을 사용하여 PyTorch가 GPU에 액세스할 수 있는지 테스트할 수 있습니다.

# GPU 확인
import torch
torch.cuda.is_available()
True

위 출력이 True이면 PyTorch가 GPU를 보고 사용할 수 있다는 뜻이고, False이면 GPU를 볼 수 없다는 뜻이므로 이 경우 설치 단계를 다시 거쳐야 합니다.

이제 CPU에서 실행되거나 GPU를 사용할 수 있는 경우 GPU에서 실행되도록 코드를 설정하고 싶다고 가정해 보겠습니다.

그렇게 하면 여러분이나 누군가가 코드를 실행하기로 결정하더라도 사용 중인 컴퓨팅 장치에 관계없이 코드가 작동할 것입니다.

사용 가능한 장치 종류를 저장하기 위해 device 변수를 생성해 보겠습니다.

# 장치 타입 설정
device = "cuda" if torch.cuda.is_available() else "cpu"
device
'cuda'

위 출력이 "cuda"이면 사용 가능한 CUDA 장치(GPU)를 사용하도록 모든 PyTorch 코드를 설정할 수 있고, "cpu"이면 PyTorch 코드가 CPU를 그대로 사용하게 됩니다.

참고: PyTorch에서는 장치에 구애받지 않는 코드(device agnostic code)를 작성하는 것이 가장 좋은 관행입니다. 이는 CPU(항상 사용 가능) 또는 GPU(사용 가능한 경우)에서 실행될 코드를 의미합니다.

더 빠른 계산을 원하면 GPU를 사용할 수 있지만, 훨씬 더 빠른 계산을 원하면 여러 개의 GPU를 사용할 수 있습니다.

torch.cuda.device_count()를 사용하여 PyTorch가 액세스할 수 있는 GPU 수를 셀 수 있습니다.

# 장치 수 세기
torch.cuda.device_count()
1

PyTorch가 액세스할 수 있는 GPU 수를 아는 것은 하나의 GPU에서 특정 프로세스를 실행하고 다른 GPU에서 다른 프로세스를 실행하려는 경우에 유용합니다(PyTorch에는 모든 GPU에서 프로세스를 실행할 수 있게 해주는 기능도 있습니다).

3. GPU에 텐서(및 모델) 넣기

텐서(및 모델, 나중에 보게 될 것입니다)를 특정 장치에서 실행하려면 해당 텐서(또는 모델)에 대해 to(device)를 호출하면 됩니다. 여기서 device는 텐서(또는 모델)가 이동하려는 대상 장치입니다.

왜 이렇게 할까요?

GPU는 CPU보다 훨씬 빠른 수치 계산을 제공하며, GPU를 사용할 수 없는 경우 장치에 구애받지 않는 코드(위 참조) 덕분에 CPU에서 실행될 것이기 때문입니다.

참고: to(device)(예: some_tensor.to(device))를 사용하여 텐서를 GPU에 넣으면 해당 텐서의 복사본이 반환됩니다. 즉, 동일한 텐서가 CPU와 GPU에 모두 있게 됩니다. 텐서를 덮어쓰려면 다음과 같이 재할당하세요.

some_tensor = some_tensor.to(device)

텐서를 생성하고 GPU(사용 가능한 경우)에 넣어봅시다.

# 텐서 생성 (기본적으로 CPU에 있음)
tensor = torch.tensor([1, 2, 3])

# 텐서가 GPU에 있지 않음
print(tensor, tensor.device)

# 텐서를 GPU로 이동 (사용 가능한 경우)
tensor_on_gpu = tensor.to(device)
tensor_on_gpu
tensor([1, 2, 3]) cpu
tensor([1, 2, 3], device='cuda:0')

GPU를 사용할 수 있는 경우 위 코드는 다음과 같은 내용을 출력합니다.

tensor([1, 2, 3]) cpu
tensor([1, 2, 3], device='cuda:0')

두 번째 텐서에 device='cuda:0'이 있는 것을 확인하세요. 이는 사용 가능한 0번째 GPU에 저장되었음을 의미합니다(GPU는 0부터 인덱싱되며, 두 개의 GPU를 사용할 수 있는 경우 각각 'cuda:0''cuda:1'부터 'cuda:n'까지입니다).

4. 텐서를 다시 CPU로 이동하기

텐서를 다시 CPU로 이동하고 싶다면 어떻게 해야 할까요?

예를 들어, NumPy와 텐서를 상호 작용시키고 싶을 때 이 작업이 필요합니다(NumPy는 GPU를 활용하지 않기 때문입니다).

tensor_on_gpu에 대해 torch.Tensor.numpy() 메서드를 사용해 봅시다.

# 텐서가 GPU에 있으면 NumPy로 변환할 수 없음 (오류 발생)
tensor_on_gpu.numpy()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-75-53175578f49e> in <module>()
      1 # 텐서가 GPU에 있으면 NumPy로 변환할 수 없음 (오류 발생)
----> 2 tensor_on_gpu.numpy()

TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

대신 텐서를 다시 CPU로 가져와 NumPy와 함께 사용하려면 Tensor.cpu()를 사용할 수 있습니다.

이는 텐서를 CPU 메모리로 복사하여 CPU에서 사용할 수 있도록 합니다.

# 대신 텐서를 다시 cpu로 복사
tensor_back_on_cpu = tensor_on_gpu.cpu().numpy()
tensor_back_on_cpu
array([1, 2, 3])

위 명령은 GPU 텐서의 복사본을 CPU 메모리로 반환하므로 원본 텐서는 여전히 GPU에 있습니다.

tensor_on_gpu
tensor([1, 2, 3], device='cuda:0')

연습 문제

  1. 문서 읽기 - 딥러닝(및 일반적인 코딩 학습)의 큰 부분은 사용 중인 특정 프레임워크의 문서에 익숙해지는 것입니다. 이 과정의 나머지 부분에서 PyTorch 문서를 많이 사용하게 될 것입니다. 따라서 다음 내용을 10분 동안 읽어보시기 바랍니다(지금 당장 이해가 되지 않더라도 괜찮습니다. 핵심은 완전한 이해가 아니라 인지하는 것입니다).
  1. 모양이 (7, 7)인 무작위 텐서를 만드세요.
  2. 2번의 텐서에 모양이 (1, 7)인 다른 무작위 텐서와 행렬 곱셈을 수행하세요(힌트: 두 번째 텐서를 전치해야 할 수도 있습니다).
  3. 무작위 시드를 0으로 설정하고 2번과 3번을 다시 수행하세요. 출력은 다음과 같아야 합니다.
(tensor([[1.8542],
         [1.9611],
         [2.2884],
         [3.0481],
         [1.7067],
         [2.5290],
         [1.7989]]), torch.Size([7, 1]))
  1. 무작위 시드와 관련하여 torch.manual_seed()로 설정하는 방법을 보았는데 GPU에서도 동일한 방법이 있나요? (힌트: 이를 위해 torch.cuda 문서를 찾아봐야 합니다)
  • 방법이 있다면 GPU 무작위 시드를 1234로 설정하세요.
  1. 모양이 (2, 3)인 두 개의 무작위 텐서를 만들고 둘 다 GPU로 보내세요(이를 위해 GPU에 액세스해야 합니다). 텐서를 생성할 때 torch.manual_seed(1234)를 설정하세요(이것이 GPU 무작위 시드일 필요는 없습니다). 출력은 다음과 같아야 합니다.
Device: cuda
(tensor([[0.0290, 0.4019, 0.2598],
         [0.3666, 0.0583, 0.7006]], device='cuda:0'),
 tensor([[0.0518, 0.4681, 0.6738],
         [0.3315, 0.7837, 0.5631]], device='cuda:0'))
  1. 6번에서 만든 텐서에 대해 행렬 곱셈을 수행하세요(여기서도 텐서 중 하나의 모양을 조정해야 할 수도 있습니다).
  2. 7번 출력의 최대값과 최소값을 찾으세요.
  3. 7번 출력의 최대값 및 최소값 인덱스 값을 찾으세요.
  4. 모양이 (1, 1, 1, 10)인 무작위 텐서를 만든 다음 값이 1인 모든 차원이 제거된 모양이 (10)인 새로운 텐서를 만드세요. 생성할 때 시드를 7로 설정하고 첫 번째 텐서와 그 모양, 두 번째 텐서와 그 모양을 출력하세요. 출력은 다음과 같아야 합니다.
tensor([[[[0.5349, 0.1988, 0.6592, 0.6569, 0.2328, 0.4251, 0.2071, 0.6297,
           0.3653, 0.8513]]]]) torch.Size([1, 1, 1, 10])
tensor([0.5349, 0.1988, 0.6592, 0.6569, 0.2328, 0.4251, 0.2071, 0.6297, 0.3653,
        0.8513]) torch.Size([10])

리소스: 이 연습 문제를 완료하려면 과정 GitHub에서 연습 문제 노트북 템플릿 및 잠재적인 솔루션을 참조하세요.

추가 학습 자료