AI/PyTorch

[PyTorch] 파이토치(Pytorch)

caramel-bottle 2024. 1. 6.

1. 파이토치란?

Tensorflow와 함께 머신러닝, 딥러닝에서 가장 널리 사용되는 프레임워크입니다.

 

초기에는 Torch라는 이름으로 Lua언어 기반으로 만들어졌으나, 파이썬 기반으로 변경한 것이 Pytorch입니다.

 

뉴욕대학교와 페이스북(메타)이 공동으로 개발하였고, 현재 가장 대중적인 머신러닝, 딥러닝 프레임워크 라고 하네요.

 

파이토치의 공식 홈페이지에서 관련된 다양한 정보를 얻으실 수 있습니다.

https://pytorch.org/

 

PyTorch

 

pytorch.org


2. 파이토치(Pytorch)

파이토치를 사용하기 위해서는 설치를 해야합니다.

!pip3 install torch torchvision

 

코랩에는 기본적으로 설치가 되어있기 때문에 저와 같이 코랩을 사용하신다면 따로 설치하지 않으셔도 됩니다.

 

torch를 import하고 버전을 확인해보았습니다.

import torch
print(torch.__version__)

output>>

2.1.0+cu121

"PyTorch 2.1.0"과 CUDA Toolkit 버전 "cu121"을 지원한다는 의미입니다.

 

CUDA는 PyTorch 연산에 GPU를 사용하도록 하기 위한 툴이라고 생각하시면 될 것 같습니다.

 

더 자세한 내용은 차차 올려보도록 하겠습니다.


2-1. 스칼라(Scalar)

스칼라(scalar): 방향이 없고 크기만 나타내는 요소

 

즉, 스칼라는 하나의 상수라고 할 수 있습니다.

 

파이토치에서 Tensor객체에 스칼라값을 넣을 수 있습니다.

 

* Tensor란?

배열이나 행렬과 매우 유사한 특수한 자료구조로, GPU나 다른 연산 가속을 위한 특수한 하드웨어에서 실행할 수 있다는 차이점이 있습니다.

 

tensor타입의 스칼라를 만들어봅시다!

var1 = torch.tensor([1])

print(f'data: {var1}\ntype: {type(var1)}\ndtype: {var1.dtype}')

output>>

data: tensor([1])
type: <class 'torch.Tensor'>
dtype: torch.int64

 

data는 tensor자료구조이고 요소의 dtype은 torch.int64임을 확인할 수 있습니다.

 

dtype의 경우 부동 소수점 타입, 정수 타입, 부호 없는 정수 타입, 복소수 타입이 있습니다.

 

부동 소수점 타입을 확인하기 위해 실수값을 넣어봅시다.

var2 = torch.tensor([10.4])

print(f'data: {var2}\ntype: {type(var2)}\ndtype: {var2.dtype}')

output>>

data: tensor([10.4000])
type: <class 'torch.Tensor'>
dtype: torch.float32

 

스칼라는 사칙연산이 가능합니다. 아주아주 간단하게 파이썬 사칙연산을 그대로 적용하면 됩니다.

# 두 스칼라의 사칙연산
print(var1 + var2)
print(var1 - var2)
print(var1 * var2)
print(var1 / var2)

print(f'연산 결과 dtype: {(var1 + var2).dtype}')

output>>

tensor([11.4000])
tensor([-9.4000])
tensor([10.4000])
tensor([0.0962])
연산 결과 dtype: torch.float32

 

결과를 보니 dtype이 달라도 연산이 가능하고, 타입도 자동으로 변경되는 것을 알 수 있습니다.


2-2. 벡터(Vector)

벡터(vector): 방향과 크기를 나타내는 요소

 

상수가 두 개 이상 나열된 경우 벡터라고 할 수 있습니다.

 

우리가 아는 리스트도 벡터라고 할 수 있죠.

 

그럼 tensor타입의 벡터를 만들어봅시다.

vec1 = torch.tensor([1, 2, 3])

print(f'data: {vec1}\ntype: {type(vec1)}\ndtype: {vec1.dtype}')

output>>

data: tensor([1, 2, 3])
type: <class 'torch.Tensor'>
dtype: torch.int64

 

벡터도 마찬가지로 쉽게 사칙 연산이 가능합니다. 실수형 벡터를 만들고 사칙연산을 해보죠.

vec2 = torch.tensor([1.5, 2.4, 3.3])

# 두 벡터의 사칙연산
print(vec1 + vec2)
print(vec1 - vec2)
print(vec1 * vec2)
print(vec1 / vec2)

print(f'연산 결과 dtype: {(vec1 + vec2).dtype}')

output>>

tensor([2.5000, 4.4000, 6.3000])
tensor([-0.5000, -0.4000, -0.3000])
tensor([1.5000, 4.8000, 9.9000])
tensor([0.6667, 0.8333, 0.9091])
연산 결과 dtype: torch.float32

 

벡터끼리의 사칙연산은 서로 크기가 같아야 가능합니다. 


2-3. 행렬(matrix)

2개 이상의 벡터로 이루어져있는 것을 행렬이라고 합니다.

 

바로 tensor타입의 행렬을 만들어봅시다.

mat1 = torch.tensor([[1, 2], [3, 4]])

print(mat1)

output>>

tensor([[1, 2],
        [3, 4]])

 

 

행렬도 마찬가지로 사칙연산이 가능합니다. 새로운 행렬 하나를 생성하여 사칙연산을 해보고 특성을 알아봅시다.

mat2 = torch.tensor([[7, 8], [9, 10]])

# 두 행렬의 사칙 연산
print(mat1 + mat2)
print(mat1 - mat2)
print(mat1 * mat2)
print(mat1 / mat2)

output>>

tensor([[ 8, 10],
        [12, 14]])
tensor([[-6, -6],
        [-6, -6]])
tensor([[ 7, 16],
        [27, 40]])
tensor([[0.1429, 0.2500],
        [0.3333, 0.4000]])

 

행렬도 크기가 같아야 사칙연산이 가능합니다.

 

사칙연산 외에도 파이토치에서 제공하는 여러가지 연산 메서드가 있습니다.

 

아래에서 확인해봅시다.


2-4. 텐서(Tensor)

선형대수학 Tensor

다차원 배열로, 벡터와 행렬을 일반화한 개념.

Scalar, Vector, Matrics을 포함하는 다차원 배열.

 

자료구조 Tensor

Pytorch, TenserFlow 등 딥러닝 라이브러리에서의 자료구조인 Tensor는 다차원 배열을 나타낸다.

딥러닝 모델의 입력데이터, 가중치, 출력 등을 저장하고 연산하는데 사용된다.

 

다차원 배열로서의 tensor를 파이토치 tensor객체로 생성해보고 특징을 알아봅시다.

 

1차원 배열인 벡터를 여러개 만들면 2차원 행렬이 되는 것을 알고 있습니다.

 

그렇다면 2차원 행렬을 여러개 만들면 tensor가 될까요?

tensor1 = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])

print(tensor1)

output>>

tensor([[[1, 2],
         [3, 4]],

        [[5, 6],
         [7, 8]]])

 

행렬 [[1, 2], [3, 4]]와 [[5, 6], [7, 8]]를 포함하는 3차원 텐서가 생성이 되었습니다.

 

텐서의 차원과 크기를 확인해보겠습니다.

print('tensor1의 크기: ', tensor1.shape)
print('tensor1의 차원: ', tensor1.dim())

output>>

tensor1의 크기:  torch.Size([2, 2, 2])
tensor1의 차원:  3

 

0차원, 1차원, 2차원 텐서인 스칼라, 벡터, 행렬 모두 사칙연산이 가능했듯이 3차원 텐서 혹은 그 이상의 텐서도 사칙연산이 가능합니다.

tensor1 = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
tensor2 = torch.tensor([[[9, 10], [11, 12]], [[13, 14], [15, 16]]])

# 두 텐서의 사칙 연산
print(tensor1 + tensor2)
print(tensor1 - tensor2)
print(tensor1 * tensor2)
print(tensor1 / tensor2)

output>>

tensor([[[10, 12],
         [14, 16]],

        [[18, 20],
         [22, 24]]])
tensor([[[-8, -8],
         [-8, -8]],

        [[-8, -8],
         [-8, -8]]])
tensor([[[  9,  20],
         [ 33,  48]],

        [[ 65,  84],
         [105, 128]]])
tensor([[[0.1111, 0.2000],
         [0.2727, 0.3333]],

        [[0.3846, 0.4286],
         [0.4667, 0.5000]]])

 

각 요소별로 연산이 되는 것을 알 수 있습니다.

 

파이토치에선 사칙연산 뿐만 아니라 여러가지 수학적 연산을 메서드로 제공합니다.

 

아래는 그 일부입니다.

사칙연산  
torch.add() 덧셈
torch.sub() 뺄셈
torch.mul() 곱셈
torch.div() 나눗셈
수학 함수  
torch.sqrt() 제곱근
torch.exp() 지수 함수 값
torch.log() 자연 로그 값
torch.sin(), cos(), tan() 삼각 함수 값
torch.abs() 절댓값
torch.ceil() 소수점 이하 올림
torch.floor() 소수점 이하 내림
torch.round() 반올림
논리연산  
torch.logical_and() 논리 AND
torch.logical_not() 논리 NOT
torch.logical_or() 논리 OR
torch.logical_xor() 논리 XOR
행렬 연산  
torch.mm() 행렬곱
torch.transpose() 전치행렬

 

이 외에도 비교연산, 히스토그램, 평균 등등 많은 메서드를 공식 홈페이지에서 확인할 수 있습니다.

https://pytorch.org/docs/stable/torch.html#math-operations

 

torch — PyTorch 2.1 documentation

Shortcuts

pytorch.org

 

다음은 수학적 연산 메서드의 간단한 사용 예시입니다.

print(torch.add(tensor1, tensor2))

print(torch.matmul(tensor1, tensor2)) # 행렬곱

output>>

tensor([[[10, 12],
         [14, 16]],

        [[18, 20],
         [22, 24]]])
         
tensor([[[ 31,  34],
         [ 71,  78]],

        [[155, 166],
         [211, 226]]])

 

* tensor객체 연산 메서드

tensor 객체 자체에도 연산 메서드가 있는데요.

 

inplace연산까지 한번에 할 수 있으니 유용하게 사용될 것 같습니다.

print(tensor1.add_(tensor2))
# inplace 연산, 모든 사칙연산 가능

print(tensor1)

output>>

tensor([[[10, 12],
         [14, 16]],

        [[18, 20],
         [22, 24]]])
         
tensor([[[10, 12],
         [14, 16]],

        [[18, 20],
         [22, 24]]])

 

add_()에서 언더바(_)를 빼면 inplace연산을 하지 않는 메서드를 사용할 수 있습니다.

print(tensor1.add(tensor2))
# inplace x

print(tensor1)

output>>

tensor([[[19, 22],
         [25, 28]],

        [[31, 34],
         [37, 40]]])
         
tensor([[[10, 12],
         [14, 16]],

        [[18, 20],
         [22, 24]]])

2. 텐서의 변환

2-1. tensor <-> ndarray

torch.tensor에서 numpy.ndarray로 혹은 그 반대로 변환이 가능합니다. 

 

우선 텐서 하나를 만들어줍니다. ( numpy도 필요하니 import해줍시다. )

import numpy as np

data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
print(x_data)

output>>

tensor([[1, 2],
        [3, 4]])

 

tensor 객체를 ndarray로 변환하기 위해선 numpy의 array메서드를 사용합니다.

np_array = np.array(x_data)

print(np_array)
print('크기: ', np_array.shape)
print('차원: ', np_array.ndim)

output>>

[[1 2]
 [3 4]]
크기:  (2, 2)
차원:  2

 

반대로 ndarray를 tensor로 변환하려면 지금까지 tensor객체를 만들어온 방법 그대로 하면 됩니다.

x_np_1 = torch.tensor(np_array)
print(x_np_1)

output>>

tensor([[1, 2],
        [3, 4]])

2-2. 메모리 주소 공유

위의 x_np_1과 np_array는 서로 다른 메모리 주소에 있습니다.

 

따라서 x_np_1의 값을 변경하여도 np_array에는 아무런 영향이 없죠.

 

np_array가 참조하는 메모리를 tensor객체도 함께 공유하고 싶다면 torch의 as_tensor()를 사용하면 됩니다.

# as_tensor: ndarray와 동일한 메모리 주소를 가리키는 뷰를 만듦
# 타입을 다양하게 받을 수 있음
x_np_2 = torch.as_tensor(np_array)

print('변경전 x_np_2:\n', x_np_2)
print('변경전 np_array:\n', np_array)

print()
x_np_2[0, 0] = 200
print('변경후 x_np_2:\n', x_np_2)
print('변경후 np_array:\n', np_array)

print(f'x_np_2가 참조하는 주소: {x_np_2.data_ptr()}\nnp_array가 참조하는 주소: {np_array.ctypes.data}')

output>>

변경전 x_np_2:
 tensor([[100,   2],
        [  3,   4]])
변경전 np_array:
 [[100   2]
 [  3   4]]

변경후 x_np_2:
 tensor([[200,   2],
        [  3,   4]])
변경후 np_array:
 [[200   2]
 [  3   4]]
x_np_2가 참조하는 주소: 94399778496256
np_array가 참조하는 주소: 94399778496256

 

as_tensor()는 리스트, ndarray, tensor, range등 다양한 객체들을 사용할 수 있습니다.

 

반면에 같은 기능을 하는 torch의 from_numpy()는 입력 변수로 ndarray만 사용할 수 있습니다.

 

사용방법은 같으니 생략하도록 하겠습니다.

 

 

* 추가로 tensor객체가 참조하는 메모리를 공유하는 ndarray도 만들 수 있습니다.

np_again = x_np_1.numpy()
print(np_again, type(np_again))

x_np_1[0, 0] = 300
print(np_again)

output>>

[[100   2]
 [  3   4]] <class 'numpy.ndarray'>
[[300   2]
 [  3   4]]

3. 파이토치 주요 함수

수학적 연산 메서드에 이어서 자주 사용되는 메서드를 알아봅시다.

# 1로 채우기
a = torch.ones(2, 3)

# 0으로 채우기
b = torch.zeros(2, 3)

# 지정한 숫자로 채우기
c = torch.full((2, 3), 10)

# 쓰레기값으로 채우기
d = torch.empty(2, 3)

# 단위행렬
e = torch.eye(5)

# 숫자열로 채우기
f = torch.arange(10)

# 무작위로 채우기
g = torch.rand(2, 3)

# 정규분포의 무작위 숫자로 채우기
h = torch.randn(2, 3)

# 모양을 변경, 차원 수 조정
i = torch.arange(16).reshape(2, 2, 4)

# 차원 순서를 변경
j = i.permute((2, 0, 1)) # 2, 2, 4 -> 4, 2, 2


print(f'\n<a>\n{a}')
print(f'\n<b>\n{b}')
print(f'\n<c>\n{c}')
print(f'\n<d>\n{d}')
print(f'\n<e>\n{e}')
print(f'\n<f>\n{f}')
print(f'\n<g>\n{g}')
print(f'\n<h>\n{h}')
print(f'\n<i>\n{i}\n<i.shape>\n{i.shape}')
print(f'\n<i>\n{j}\n<j.shape>\n{j.shape}')

output>>

<a>
tensor([[1., 1., 1.],
        [1., 1., 1.]])

<b>
tensor([[0., 0., 0.],
        [0., 0., 0.]])

<c>
tensor([[10, 10, 10],
        [10, 10, 10]])

<d>
tensor([[1.6407e-13, 3.0799e-41, 1.6327e-13],
        [3.0799e-41, 0.0000e+00, 0.0000e+00]])

<e>
tensor([[1., 0., 0., 0., 0.],
        [0., 1., 0., 0., 0.],
        [0., 0., 1., 0., 0.],
        [0., 0., 0., 1., 0.],
        [0., 0., 0., 0., 1.]])

<f>
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

<g>
tensor([[0.3345, 0.0396, 0.0012],
        [0.2932, 0.0500, 0.8019]])

<h>
tensor([[ 1.3651,  0.6868, -0.9428],
        [ 1.0809,  1.7929, -1.1394]])

<i>
tensor([[[ 0,  1,  2,  3],
         [ 4,  5,  6,  7]],

        [[ 8,  9, 10, 11],
         [12, 13, 14, 15]]])
<i.shape>
torch.Size([2, 2, 4])

<i>
tensor([[[ 0,  4],
         [ 8, 12]],

        [[ 1,  5],
         [ 9, 13]],

        [[ 2,  6],
         [10, 14]],

        [[ 3,  7],
         [11, 15]]])
<j.shape>
torch.Size([4, 2, 2])

4. 텐서의 인덱싱과 슬라이싱

텐서는 파이썬의 리스트나 문자열과 같이 인덱싱과 슬라이싱이 가능합니다.

 

예시를 위한 텐서를 하나 만들고 인덱싱과 슬라이싱을 해봅시다.

a = torch.arange(1, 13).reshape(3, 4)

print(f'\n<a>\n{a}')
print(f'\n<인덱싱1>\n{a[1]}')
print(f'\n<인덱싱2>\n{a[0, -1]}')
print(f'\n<슬라이싱1>\n{a[1:-1]}')
print(f'\n<슬라이싱2>\n{a[:2, 2:]}')

output>>

<a>
tensor([[ 1,  2,  3,  4],
        [ 5,  6,  7,  8],
        [ 9, 10, 11, 12]])

<인덱싱1>
tensor([5, 6, 7, 8])

<인덱싱2>
4

<슬라이싱1>
tensor([[5, 6, 7, 8]])

<슬라이싱2>
tensor([[3, 4],
        [7, 8]])

 

인덱싱1과 슬라이싱1은 결과가 같은게 아닌가 싶지만

 

자세히 보면 차원이 다릅니다.

 

이처럼 인덱싱은 차원이 낮아지게 되고 슬라이싱은 기존 차원을 유지한다는 차이점이 있습니다.


끝내며

PyTorch Tensor의 아주 기본적인 사용법을 다뤄봤습니다.

 

다음 포스팅부터는 torch.nn, torch.optim 등등을 사용하여 모델을 직접 만들어보도록 하겠습니다.

 

긴 글 봐주셔서 감사합니다!

 

 

 

댓글