JINWOOJUNG

[EECS 498] Assignment 1. PyTorch 101...(1) 본문

딥러닝/Michigan EECS 498

[EECS 498] Assignment 1. PyTorch 101...(1)

Jinu_01 2024. 12. 22. 16:22
728x90
반응형

본 포스팅은 Michigan Univ.의 EECS 498 강의를 수강하면서 공부한 내용을 정리하는 포스팅입니다.


0. 개발 환경

OS : Ubuntu 20.04

GPU : GeForce RTX 3070

cuda  version: 12.1

torch version : 2.3.0+cu121

 

1. Tensor Basics

def create_sample_tensor() -> Tensor:
    x = torch.tensor([[0, 10],[100, 0],[0,0]])
    return x

 

Tensor 객체는 torch.tensor를 통해 생성할 수 있다. 

 

x = mytorch.create_sample_tensor()
print('Here is the sample tensor:')
print(x)
print(type(x))
print(x.dim())
print(x.shape)

# Here is the sample tensor:
# tensor([[  0,  10],
#         [100,   0],
#         [  0,   0]])
# <class 'torch.Tensor'>
# 2
# torch.Size([3, 2])

 

Tensor 객체는 tensor(Data) 형태로 표현되며, Data type은 torch.Tensor Class를 가진다. 

Tensor 객체는 Rank, Shape 개념을 가진다.

  • Rank
    • 차원의 수
    • Tensor.dim()을 통해 획득 가능
  • Shape
    • 크기
    • Tensor.shape 를 통해 획득 가능

create_sample_tensor()를 통해 생성한 Tensor 객체는 2 Dimension을 가지며, 크기는 3x2 임을 확인할 수 있다. 

 

def mutate_tensor(
    x: Tensor, indices: List[Tuple[int, int]], values: List[float]
) -> Tensor:
        
    # enumerate를 통해 index를 동시에 접근해서 변경할 Value를 가져옴
    for idx, (i, j)  in enumerate(indices):
        x[i,j] = values[idx]

    return x

 

Tensor 객체의 값을 변경하기 위해서는, 배열에 접근하는 것과 같이 Tensor 객체에 대하여 Index로 접근할 수 있다. mutate_tensor는 변경할 Tensor 객체의 원소의 Index가 담긴 indices List와 변경할 값을 가지는 values List를 받아와 해당 Tensor 객체의 값을 변경하는 함수이다.

 

# Mutate the tensor by setting a few elements
indices = [(0, 0), (1, 0), (1, 1)]
values = [4, 5, 6]
mytorch.mutate_tensor(x, indices, values)
print('\nAfter mutating:')
print(x)

# After mutating:
# tensor([[ 4, 10],
#         [ 5,  6],
#         [ 0,  0]])

 

Tensor 객체 x의 0,0 즉 1열 1행의 원소가 0에서 4로 바뀜을 확인할 수 있다. 

 

def count_tensor_elements(x: Tensor) -> int:
    
    num_elements = 1
    
    # 각 차원의 크기를 누적 곱
    for dim in x.shape:
        num_elements *= dim

    return num_elements

 

Tensor 객체의 원소 수를 계산하는 함수이다. 이는 Tensor.shape를 통해 각 차원의 크기를 구한 뒤, 누적 곱으로 쉽게 계산할 수 있다. 

 

num = mytorch.count_tensor_elements(x)
print('\nNumber of elements in x: ', num)

# Number of elements in x:  6

 

Tensor 객체 x는 3x2의 크기를 가지므로, 원소의 총 개수는 6개가 된다. 

 

2. Tensor constructors

PyTorch는 Tensor를 생성하는 몇가지 편리한 Method를 제공한다.

torch.zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

a = torch.zeros(2, 3)
print('tensor of zeros:')
print(a)

# tensor of zeros:
# tensor([[0., 0., 0.],
#         [0., 0., 0.]])

 

torch.zeros는 입력받은 shape를 가지고, 모든 원소가 0인 Tensor 객체를 생성한다. 

 

torch.ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

b = torch.ones(1, 2)
print('\ntensor of ones:')
print(b)

# tensor of ones:
# tensor([[1., 1.]])

 

torch.ones는 입력받은 shape를 가지고, 모든 원소가 1인 Tensor 객체를 생성한다. 

하나 주의할 점은, 위와 같이 생성된 Tensor 객체 b는 1차원이 아닌 2차원임을 주의하자.

 

torch.eye(n, m=None, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

c = torch.eye(3)
print('\nidentity matrix:')
print(c)

# identity matrix:
# tensor([[1., 0., 0.],
#         [0., 1., 0.],
#         [0., 0., 1.]])

 

torch.eye는 nxn shape를 가지고, 주대각선상 원소가 1이고, 나머진 0인 Identity Matrix 형태의 Tensor 객체를 생성한다.

 

torch.rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) → Tensor

d = torch.rand(4, 5)
print('\nrandom tensor:')
print(d)

# random tensor:
# tensor([[0.7247, 0.9109, 0.9399, 0.8177, 0.9258],
#         [0.7217, 0.2551, 0.1919, 0.9802, 0.0872],
#         [0.1974, 0.7558, 0.2224, 0.4291, 0.6515],
#         [0.6502, 0.5123, 0.4780, 0.1624, 0.2765]])

 

torch.rand는 [0,1) 범위의 Random Value를 원소로 가지는 입력받은 shape의 Tensor 객체를 생성한다. 

 

torch.full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

def create_tensor_of_pi(M: int, N: int) -> Tensor:

    # 입력받은 Value로 모두 채워진 MxN shape를 가지는 Tensor 객체 반환
    x = torch.full((M,N), 3.14)

    return x
    
x = mytorch.create_tensor_of_pi(4, 5)

# tensor([[3.1400, 3.1400, 3.1400, 3.1400, 3.1400],
#         [3.1400, 3.1400, 3.1400, 3.1400, 3.1400],
#         [3.1400, 3.1400, 3.1400, 3.1400, 3.1400],
#         [3.1400, 3.1400, 3.1400, 3.1400, 3.1400]])

 

torch.full은 모든 원소를 fill_value로 가지고, 입력받은 shape의 Tensor 객체를 생성한다.

create_tensor_of_pi는 Tensor 객체의 Shape(M,N)을 입력받아서, 모든 원소를 3.14로 가지는 Tensor 객체를 반환하는 함수이다.

 

3. Datatypes

x0 = torch.tensor([1, 2])   # List of integers
x1 = torch.tensor([1., 2.]) # List of floats
x2 = torch.tensor([1., 2])  # Mixed list
print('dtype when torch chooses for us:')
print('List of integers:', x0.dtype)
print('List of floats:', x1.dtype)
print('Mixed list:', x2.dtype)

# dtype when torch chooses for us:
# List of integers: torch.int64
# List of floats: torch.float32
# Mixed list: torch.float32

 

Torch 객체의 Datatype은 torch.dtype으로 확인할 수 있다. 입력한 원소의 Datatype에 따라 Tensor 객체의 Datatype이 결정된다.

y0 = torch.tensor([1, 2], dtype=torch.float32)  # 32-bit float
y1 = torch.tensor([1, 2], dtype=torch.int32)    # 32-bit (signed) integer
y2 = torch.tensor([1, 2], dtype=torch.int64)    # 64-bit (signed) integer
print('\ndtype when we force a datatype:')
print('32-bit float: ', y0.dtype)
print('32-bit integer: ', y1.dtype)
print('64-bit integer: ', y2.dtype)

# dtype when we force a datatype:
# 32-bit float:  torch.float32
# 32-bit integer:  torch.int32
# 64-bit integer:  torch.int64

 

혹은, 생성 시 dtype으로 Datatype을 특정지을 수 있다. 

 

x0 = torch.eye(3, dtype=torch.int64)
x1 = x0.float()  # Cast to 32-bit float
x2 = x0.double() # Cast to 64-bit float
x3 = x0.to(torch.float32) # Alternate way to cast to 32-bit float
x4 = x0.to(torch.float64) # Alternate way to cast to 64-bit float
print('x0:', x0.dtype)
print('x1:', x1.dtype)
print('x2:', x2.dtype)
print('x3:', x3.dtype)
print('x4:', x4.dtype)

# x0: torch.int64
# x1: torch.float32
# x2: torch.float64
# x3: torch.float32
# x4: torch.float64

 

만약 기존에 존재하는 Tensor 객체의 Datatype을 변경하기 위해서는 torch.datatype 혹은 .datatype()으로 변경할 수 있다. 

 

torch.zeros_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor

x0 = torch.eye(3, dtype=torch.float64)  # Shape (3, 3), dtype torch.float64
x1 = torch.zeros_like(x0)               # Shape (3, 3), dtype torch.float64

# x0 shape is torch.Size([3, 3]), dtype is torch.float64
# x1 shape is torch.Size([3, 3]), dtype is torch.float64

 

특정 Tensor 객체와 동일한 shape를 가지지만, 모든 원소가 0인 Tensor 객체를 생성하려면 torch.zeros_like를 사용한다.

 

Tensor.new_zeros(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) → Tensor

x0 = torch.eye(3, dtype=torch.float64)  # Shape (3, 3), dtype torch.float64
x2 = x0.new_zeros(4, 5)                 # Shape (4, 5), dtype torch.float64

# x0 shape is torch.Size([3, 3]), dtype is torch.float64
# x2 shape is torch.Size([4, 5]), dtype is torch.float64

 

tensor.new_zeros를 통해 입력한 shape를 가지고 모든 원소가 0인 Tensor 객체를 생성할 수 있다. 

 

기존에 특정 shape를 가지고 모든 원소가 0인 Tensor 객체를 생성하는 Method인 torch.zeros를 배웠다. 그러면 이와 차이는 무엇일까?

 

x0 = torch.eye(3, dtype=torch.float64)
x1 = x0.cuda()
x2 = torch.zeros(4,5)  
x3 = x1.new_zeros(4,5)
print("x0's device = ", x0.device)
print("x1's device = ", x1.device)
print("x2's device = ", x2.device)
print("x3's device = ", x3.device)

# x0's device =  cpu
# x1's device =  cuda:0
# x2's device =  cpu
# x3's device =  cuda:0

 

torch.new_zeros를 사용하면, 기존 torch 객체와 동일한 device를 갖게된다. 추후 병렬처리를 통한 빠른 연산을 위해 GPU로 옮겨서 동작시키게 될 것인데, 이때 유용하게 활용할 수 있다.

 

torch.arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

def multiples_of_ten(start: int, stop: int) -> Tensor:
    assert start <= stop
    x = None

    # start와 가장 가까운 10의 배수 계산
    start_value  = (start + 9) // 10 * 10

    if start_value > stop:
        x = torch.zeros((0,), dtype = torch.float64)
    else:
        x = torch.arange(start_value, stop + 1, 10, dtype = torch.float64)

    return x
    
start = 5
stop = 25
x = mytorch.multiples_of_ten(start, stop)
print('Correct dtype: ', x.dtype == torch.float64)
print('Correct shape: ', x.shape == (2,))
print('Correct values: ', x.tolist() == [10, 20])

# Correct dtype:  True
# Correct shape:  True
# Correct values:  True

 

mulstiples_of_tem은 입력받은 start~end 범위의 숫자 중 10의 배수인 값만 원소로 가지는 Tensor 객체를 생성하는 함수이다. 만약 없으면 shape가 (0,)인 빈 객체를 반환한다. 

 

먼저 start와 가장 가까운 10의 배수를 계산한다. start에 9를 더하고 10으로 나눈 몫에 10을 곱하면 가장 가까운 10의 배수를 구할 수 있다. 만약 start_value가 stop보다 크면, start~end 범위에는 10의 배수가 없으므로 shape가 (0,)인 빈 객체를 반환한다. 아니면, torch.arrange를 이용해서 start_value부터 stop+1까지 10씩 증가하는 값들을 원소로 하는 Tensor 객체를 생성하여 반환한다. 

 

4. Slice Indexing

Tensor 객체는 Index를 기반으로 Slicing 할 수 있다. 방법은 크게 가지로, start:stop / start:stop:step 이 있다. 이때, stop은 항상 포함되지 않음을 주의하자. 

 

a = torch.tensor([0, 11, 22, 33, 44, 55, 66])
print(0, a)        # torch([0, 11, 22, 33, 44, 55, 66])
print(1, a[2:5])   # torch([22, 33, 44])
print(2, a[2:])    # torch([22, 33, 44, 55, 66])
print(3, a[:5])    # torch([0, 11, 22, 33, 44])
print(4, a[:])     # torch([0, 11, 22, 33, 44, 55, 66])
print(5, a[1:5:2]) # torch([11, 33])
print(6, a[:-1])   # torch([0, 11, 22, 33, 44, 55])
print(7, a[-4::2]) # torch([33, 55])

 

start 혹은 stop이 비어있는 경우 끝까지라고 생각하면 된다. 또한 -Index의 경우 마지막 원소가 -1이다. step이 존재하면, step 단위로 증가한다. 주의할 점은 stop은 항상 포함되지 않는다.

 

a = torch.tensor([[1,2,3,4], [5,6,7,8], [9,10,11,12]])

print(a)
print('shape: ', a.shape)                   # shape:  torch.Size([3, 4])
print(a[1, :])                              # tensor([5, 6, 7, 8])
print(a[1])                                 # tensor([5, 6, 7, 8])
print('shape: ', a[1].shape)                # shape:  torch.Size([4])
print(a[:, 1])                              # tensor([ 2,  6, 10])
print('shape: ', a[:, 1].shape)             # shape:  torch.Size([3])
print(a[:2, -3:])
print('shape: ', a[:2, -3:].shape)
# tensor([[2, 3, 4],
#         [6, 7, 8]])
# shape:  torch.Size([2, 3])
print(a[::2, 1:3])
print('shape: ', a[::2, 1:3].shape)
# tensor([[ 2,  3],
#         [10, 11]])
# shape:  torch.Size([2, 2])

 

다차원 Tesnor 객체에 대한 Index Slicing도 동일하다. 각각의 차원에 대해 Slice 할 수 있으며, " : "의 경우 즉, 전체에 대해서는 생략해도 동일한 결과가 나온다. 

a[::2,  1:3]을 살펴보자면, 처음 행부터 2씩 증가시키고, [1,3)의 열만 가져오므로 2x2의 shape를 가짐을 예측할 수 있다.

 

a = torch.tensor([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
print('Original tensor')
print(a)

row_r1 = a[1, :]    # Rank 1 view of the second row of a  
row_r2 = a[1:2, :]  # Rank 2 view of the second row of a
print('\nTwo ways of accessing a single row:')
print(row_r1, row_r1.shape)
print(row_r2, row_r2.shape)

# We can make the same distinction when accessing columns:
col_r1 = a[:, 1]
col_r2 = a[:, 1:2]
print('\nTwo ways of accessing a single column:')
print(col_r1, col_r1.shape)
print(col_r2, col_r2.shape)

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

# Two ways of accessing a single row:
# tensor([5, 6, 7, 8]) torch.Size([4])
# tensor([[5, 6, 7, 8]]) torch.Size([1, 4])

# Two ways of accessing a single column:
# tensor([ 2,  6, 10]) torch.Size([3])
# tensor([[ 2],
#         [ 6],
#         [10]]) torch.Size([3, 1])

 

조금 주의깊게 봐야하는 것은, Slicing 방법에 따라서 반환되는 객체의 Shape가 달라진다는 점이다. row_r1과 같이 Slicing 하면, Rank가 1인 Tensor가 반환된다. 하지만, row_r2와 같아 생성하게 되는 경우 Rank가 2가된다. 따라서 각 상황에 맞춰 적절히 연산을 수행해야 한다.

 

torch.clone(input, *, memory_format=torch.preserve_format) → Tensor

a = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]])
b = a[0, 1:]
c = a[0, 1:].clone()
print('Before mutating:')
print(a)
print(b)
print(c)
# Before mutating:
# tensor([[1, 2, 3, 4],
#         [5, 6, 7, 8]])
# tensor([2, 3, 4])
# tensor([2, 3, 4])

a[0, 1] = 20  
b[1] = 30    
c[2] = 40     
print('\nAfter mutating:')
print(a)
print(b)
print(c)
# tensor([[ 1, 20, 30,  4],
#         [ 5,  6,  7,  8]])
# tensor([20, 30,  4])
# tensor([ 2,  3, 40])

 

Slicing을 통해 반환되는 객체는 동일한 Tensor 객체를 가르키고 있다. 따라서 값을 변경하게 되면, Slicing 이전의 Tensor 객체에도 영향을 준다. 

이를 방지하기 위해서는, Slicing 된 Tensor 객체에 대하여 torch.clone을 통해 복사본을 생성하면 영향을 주지 않는다.

 

def slice_indexing_practice(x: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor]:
    """
    Given a two-dimensional tensor x, extract and return several subtensors to
    practice with slice indexing. Each tensor should be created using a single
    slice indexing operation.

    The input tensor should not be modified.

    Args:
        x: Tensor of shape (M, N) -- M rows, N columns with M >= 3 and N >= 5.

    Returns:
        A tuple of:
        - last_row: Tensor of shape (N,) giving the last row of x. It should be
          a one-dimensional tensor.
        - third_col: Tensor of shape (M, 1) giving the third column of x. It
          should be a two-dimensional tensor.
        - first_two_rows_three_cols: Tensor of shape (2, 3) giving the data in
          the first two rows and first three columns of x.
        - even_rows_odd_cols: Two-dimensional tensor containing the elements in
          the even-valued rows and odd-valued columns of x.
    """
    assert x.shape[0] >= 3
    assert x.shape[1] >= 5

    last_row = x[-1,:]
    third_col = x[:,2:3]
    first_two_rows_three_cols = x[:2,:3]
    even_rows_odd_cols = x[::2,1::2]

    out = (
        last_row,
        third_col,
        first_two_rows_three_cols,
        even_rows_odd_cols,
    )
    return out

 

slice_indexing_practice는 각각의 조건에 맞는 Index Slicing 된 Tensor 객체를 반환하는 함수이다. 

third_col의 경우 tow-dimensional tensor를 반환해야 하기에 x[:2]가 아닌 x[:,2:3]으로 구현하였다.

even_rows_odd_cols는 step을 2로 설정하고 시작 Index를 잘 설정 해 주면 쉽게 구현할 수 있다.

 

a = torch.zeros(2, 4, dtype=torch.int64)
a[:, :2] = 1
a[:, 2:] = torch.tensor([[2, 3], [4, 5]])
print(a)

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

 

Slice Indexing 한 Tensor 객체에 대하여 값을 할당할 수 있다. 또한, 동일한 shape를 가지는 Tensor 객체를 할당할 수도 있다.

728x90
반응형