나의 작은 valley

[딥러닝] Tensor Manipulation 본문

Computer Science/[인공지능]

[딥러닝] Tensor Manipulation

붕옥 아이젠 2024. 3. 15. 16:37
728x90

Tensor(텐서)란

1차원의 배열을 List라고 부르고 2차원의 배열을 Matrix라고 부른다. 그리고 3차원 이상의 배열을 Tensor라고 한다.

 

텐서의 값을 무작위로 생성하는 방법들

- rand

# 0부터 1 사이의 값을 랜덤하게 NxM 텐서로 반환
torch.rand(2, 3) # torch.rand(NxM) NxM은 텐서의 크기를 말합니다.

- randn

# 가우시안 분포에서 렌덤하게 값을 추출 후, NxM 텐서로 반환
torch.randn(2, 3) # torch.randn(NxM) NxM은 텐서의 크기를 말합니다.

- randint

# 범위 내의 정수를 N x M 텐서로 반환
torch.randint(1, 10, (5, 5)) # 생성 가능한 최솟값 : 1, 최댓값 : 9, (5x5) Tensor 크기

 

텐서의 값을 지정하여 생성

- zeros: 모든 요소가 0인 텐서

torch.zeros(3, 3) # torch.zeros(*size) 여기서 size 는 ","로 구분하며 차원을 여러개로 늘릴 수 있습니다.

- full: 모든 요소가 지정된 값인 텐서

torch.full((2, 3), 5) # torch.full((size),value) => 괄호로 텐서의 크기 (2,3) 를 입력하고, 지정한 값 value (5) 로 모든 요소가 설정됩니다.

- eye: 단위 행렬 반환

torch.eye(3) # torch.eye(n) (nxn) 크기를 가지는 단위 행렬 반환, 단위행렬 특성 상 정사각행렬 (square matrix)만 가능

 

 

텐서로 변환하기

- tensor.tensor

: 원본 데이터가 바뀌어도 메모리 공유를 하지 않아서 반영되지 않음.

# list, tuple, numpy array를 텐서로 바꾸기
ls = [[1, 2, 3, 4, 5],[6, 7, 8, 9, 10]] # sample list 생성
tup = tuple([1, 2, 3]) # sample tuple 생성
arr = np.array([[[1, 2, 3],[4, 5, 6]],[[7, 8, 9],[10, 11, 12]]]) # sample numpy array 생성

print(torch.tensor(ls))
print(torch.tensor(tup))
print(torch.tensor(arr))

 

cf) from_numpy, as_tensor는 메모리 공유를 하는 함수이다.

 

- Tensor : 텐서의 각 원소를 float로 변환

tensor2 = torch.Tensor(data) # list 에서 Tensor 변환, 각 원소가 float32

 

 

텐서 인덱싱

print(tmp_3dim[:,:,0]) # 전체 채널과 전체 행에서 0번째 열만 추출
print(tmp_3dim[0,:,1])  # 0번째 채널의 전체 행에서 1번째 열만 추출

 

- index 변수로 인덱싱

# index_select
tmp_2dim = torch.tensor([[i for i in range(10)],[i for i in range(10, 20)]])
print(tmp_2dim)
"""
tensor([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]])
"""
print('\n')

my_index = torch.tensor([0, 2]) # 선택하고자 하는 index 는 텐서 형태이어야 함.
torch.index_select(tmp_2dim, dim=1, index=my_index) # 열을 기준으로 0열과 2열을 추출
"""
tensor([[ 0,  2],
        [10, 12]])
"""

dim에 대해서 할 이야기가 있는데, 새로운 차원이 추가되면 그 차원이 최댓값이다. 그러니까 지금 tmp_2dim이라는 텐서는 2차원이다. 행이 처음 있고 열이 생겼다. 그래서 dim = 1이 열을 가르키는 것이다.

 

열을 기준으로 0,2번째 인덱스인 값들을 고른 것이다.

 

-mask indexing

# mask 를 이용한 텐서 Indexing (조건에 맞는 값만 추출)
mask = tmp_2dim >= 5 # 5보다 큰 텐서만 추출
tmp_2dim[mask] # 1차원 Tensor 로 반환

- take

텐서를 일차원으로 늘렸을 때를 기준으로 인덱싱

my_index = torch.tensor([0, 15])
torch.take(tmp_2dim, index = my_index) # Tensor가 1차원으로 늘려졌을 때 기준으로 index 번호로 접근

-gather

: 주워진 차원에서 인덱스에 해당하는 요소들을 선택하여 새로운 텐서로 반환

# 0번째 값, 1번 째 값을 0번째 행으로 설정하고, 9번째 값, 8번째 값을 1번째 행으로 설정한다.
recon_index =  torch.tensor([[0 ,1],[9, 8]]) 
# dim =1 이므로 열 기준, 0행 0열, 0행 1열 선택, 1행 9열, 1행 8열
torch.gather(tmp_2dim, dim = 1, index = recon_index)

 

Tensor 모양 바꾸기

- .shape

: 텐서의 차원 크기 확인 가능

a.shape # a.size() 와 동일

 

- .reshape()

텐서의 모양을 반환한다. 단 메모리 공유를 하지는 않는다.

# 모양 변경
a = torch.randn(2, 3, 5) 
# (2,3,5) 크기를 가지는 텐서 생성
reshape_a = a.reshape(5, 6) 
# 3차원 텐서를 2차원 텐서로 크기 변경 (2,3,5) -> (5,6)

cf) 바꾸는 값을 다 넣고 나머지 하나에 -1를 넣으면 -1에는 자동으로 모양이 맞춰진 값이 들어간다. 당연히 불가능한 조합으로 값을 넣은 상태에서 -1을 넣으면 RuntimeError 발생

 

- .view()

reshape와 동일하게 크기 변경

view_a = a.view(5, 6) # reshape 과 동일하게 (2,3,5) 크기를 (5,6) 크기로 변경

 

- .transpose()

: 2개의 차원을 입력받아 두 차원을 서로 전치

# (3,2,5) 를 (3,5,2) 의 크기로 변경
trans_a = tensor_a.transpose(1, 2) # 행과 열을 서로 전치, 서로 전치할 차원 2개를 지정

 

- permute

: 차원의 순서를 재배열

permute_a = tensor_a.permute(0, 2, 1) # (3,2,5)의 모양을 (3,5,2)의 모양으로 변경

 

 

텐서의 차원을 추가 및 제거

- unsqueeze : 특정 차원에 크기가 1인 차원 추가

unsqu_a = tensor_a.unsqueeze(0) # 0번째 차원 하나 추가 (5,2) => (1,5,2)
unsqu_a2 = tensor_a.unsqueeze(-1) # 마지막번째에 차원 하나 추가 (5,2) => (5,2,1)

- squeeze : 차원의 크기가 1인 차원 전부 제거

squ = unsqu_a.squeeze() # 차원이 1인 차원을 제거
# 차원 지정
print("Shape (squeeze()) :", x.squeeze().size()) # 차원이 1인 차원이 여러개일 때, 모든 차원이 1인 차원 제거
print("Shape (squeeze(0)) :", x.squeeze(0).size()) # 0번째 차원은 차원의 크기가 1이 아니므로, 변화 없음
print("Shape (squeeze(1)) :", x.squeeze(1).size()) # 1번째 차원은 차원의 크기가 1이므로 제거
print("Shape (squeeze(0,1,3)) :", x.squeeze((0, 1, 3)).size()) # 여러 차원 제거 가능 (0번째 차원은 차원의 크기가 1이 아니기 때문에 무시)

 

-expand

: 텐서의 값을 반복하며 크기를 확장한다.단 텐서가 2차원 이상인 경우 크기가 1인 차원에 대해서만 적용가능함.

expand_tensor = tensor_1dim.expand(3, 4) # (,4) 를 (3,4) 의 크기로 확장 (값을 반복)
"""
tensor([1, 2, 3, 4])
Shape :  torch.Size([4])

tensor([[1, 2, 3, 4],
        [1, 2, 3, 4],
        [1, 2, 3, 4]])
Shape :  torch.Size([3, 4])
"""
expand_tensor = tensor_2dim.expand(4,4) # (2,4) 를 (4,8) 의 크기로 확장 (값을 반복)
-> Error: The expanded size of the tensor (4) must match the existing size (2) at non-singleton dimension 0.  Target sizes: [4, 4].  Tensor sizes: [2, 4]

 

-repeat

: 텐서를 반복하여 크기를 확장한다.

repeat_tensor = tensor_1dim.repeat(3, 4) # tensor_1dim 자체를 행으로 3번 반복, 열로 4번 반복

 

 

-flatten

: 다차원 텐서를 1차원 텐서로 변경

flat_tensor = t.flatten() # (2, 5, 2) 의 Tensor를 (20,)로 모양 변경, 1차원으로 변경
flat_tensor2 = t.flatten(start_dim=1) # flatten을 시작할 차원을 지정할 수 있음. 2차원으로 flatten

 

-reval

: 다차원 텐서를 1차원 텐서로 변경

ravel_tensor = t.ravel() # flatten 과 동일하게 (2,5,2) 의 텐서를 (20,)로 모양 변경, 1차원으로 변경
t.ravel(1) # 에러 발생, 얘는 그거 안됨.

 

view vs reshape vs unsqueeze

: view 는 contiguous 하지 않은 텐서에 대해서 동작하지 않는다

: reshape 는 contiguous 하지 않은 텐서를 contiguous 하게 만들어주고, 크기를 변경한다.

: unsqueeze 는 차원의 크기가 1인 차원을 추가하지만, 차원의 크기가 1이 아니면 차원의 모양을 변경할 수 없다.

 

cf) contiguous의 뜻은 데이터가 하드웨어적(메모리 상)으로 연속적인 배치를 가지는 것을 의미한다.

 

transpose vs permute

transpose_tensor = tensor_a.transpose(2, 1) # 행과 열을 전치
permute_tensor = tensor_a.permute(0, 2, 1) # 행과 열을 바꿈.

 

expand vs repeat

- expand : 원본 데이터와 메모리 공유

- repeat : 원본 데이터와 메모리 공유 x

 

텐서 합치기

- cat : 지정한 차원에 따라 텐서를 연결함. (지정한 차원 외의 다른 차원의 크기는 같아야한다.)

tensor_a = torch.randint(1, 10, (2, 3)) # 1부터 9까지의 무작위 정수가 있는 (2,3) Tensor
tensor_b = torch.rand(5, 3) # 0부터 1까지의 균등분포를 따르는 (5,3) Tensor
# dim = 0 (행), Tensor A 와 Tensor B 를 행 기준으로 합친다.
a_cat_b_row = torch.cat((tensor_a, tensor_b), dim=0) 
# torch.Size([7, 3])

 

 - stack

: 지정한 차원을 새로운 차원으로 추가하여 텐서를 쌓는다. (합쳐진 차원은 크기가 같아야 한다.)

tensor_a = torch.randint(1, 10, (3, 2))  # 1부터 9까지의 무작위 정수가 있는 (3,2) Tensor
tensor_b = torch.rand(3, 2)  # 0부터 1까지의 균등분포를 따르는 (3,2) Tensor
stack_tensor_row = torch.stack([tensor_a, tensor_b], dim=0)  # dim = 0, 행을 기준으로 Tensor A 에 Tensor B 를 쌓기
#torch.Size([2, 3, 2])

 

텐서 나누기

- chunk

: 나누고자 하는 텐서의 개수를 지정하여 원래 텐서를 개수에 맞게 분리한다.

tensor_a = torch.randint(1, 10, (6, 4))  # (6,4) 텐서
chunk_num = 3
chunk_tensor = torch.chunk(tensor_a, chunks = chunk_num, dim=0)  # dim = 0 (행), 6개의 행이 3개로 나누어 떨어지므로 3개의 텐서로 분리
for idx,a in enumerate(chunk_tensor):
    print(f'{idx} 번째 Tensor \n{a}')
    print(f'{idx} 번째 Tensor 크기', a.size())
Original :  tensor([[3, 3, 5, 5],
        [3, 8, 7, 7],
        [6, 5, 4, 4],
        [8, 4, 2, 2],
        [9, 6, 3, 1],
        [5, 6, 7, 3]])

0 번째 Tensor 
tensor([[3, 3, 5, 5],
        [3, 8, 7, 7]])
0 번째 Tensor 크기 torch.Size([2, 4])
------------------------------
1 번째 Tensor 
tensor([[6, 5, 4, 4],
        [8, 4, 2, 2]])
1 번째 Tensor 크기 torch.Size([2, 4])
------------------------------
2 번째 Tensor 
tensor([[9, 6, 3, 1],
        [5, 6, 7, 3]])
2 번째 Tensor 크기 torch.Size([2, 4])

 

- split

: 입력한 크기로 여러 개의 작은 텐서로 나눈다.

tensor_a = torch.randint(1, 10, (6, 4))  # (6,4) 텐서
split_size = 2
split_tensor = torch.split(tensor_a , split_size_or_sections = split_size, dim=0)  # dim = 0 (행), 텐서 A 를 행의 길이가 2 (split_size)인 텐서로 나눔

for idx,a in enumerate(split_tensor):
    print(f'{idx} 번째 Tensor \n{a}')
    print(f'{idx} 번째 Tensor 크기', a.size())
    print('---'*10)

출력은 chunk와 동일.

 

텐서 간의 사측연산

그냥 기호를 쓰자 ㅎㅎ

통계치

- sum

print("dim = 0 일 때 : ", torch.sum(tensor_a, dim=0))  # 행을 기준 (행 인덱스 변화)으로 합함 (0행 0열 + 1행 0열, 0행 1열 + 1행 1열)
print("dim = 1 일 때 : ", torch.sum(tensor_a, dim=1)) # 열을 기준 (열 인덱스 변화)으로 합함 (0행 0열 + 0행 1열, 1행 0열 + 1행 1열)

 

- mean

print("dim = 0 일 때 : ", torch.mean(tensor_a, dim=0))  # 행을 기준 (행 인덱스 변화)으로 평균 구함 ((0행 0열 + 1행 0열)/2, (0행 1열 + 1행 1열)/2)
print("dim = 1 일 때 : ", torch.mean(tensor_a, dim=1)) # 열을 기준 (열 인덱스 변화)으로 평균 구함 ((0행 0열 + 0행 1열)/2, (1행 0열 + 1행 1열)/2)

 

- min, max

print("dimension 지정 안했을 때 : ", torch.max(tensor_a))  # 모든 원소 중 최댓값 반환
print("dim = 0 일 때 : ", torch.max(tensor_a, dim=0).values)  # 행을 기준 (행 인덱스 변화)으로 max 비교 (max(0행 0열 , 1행 0열), max(0행 1열 , 1행 1열))
print("dim = 1 일 때 : ", torch.max(tensor_a, dim=1).values) # 열을 기준 (열 인덱스 변화)으로 max 비교 (max(0행 0열 , 0행 1열), max(1행 0열 , 1행 1열))

print("dimension 지정 안했을 때 : ", torch.min(tensor_a))  # 모든 원소의 최솟값 반환 함
print("dim = 0 일 때 : ", torch.min(tensor_a, dim=0).values)  # 행을 기준 (행 인덱스 변화)으로 min 비교 (min(0행 0열 , 1행 0열), min(0행 1열 , 1행 1열))
print("dim = 1 일 때 : ", torch.min(tensor_a, dim=1).values) # 열을 기준 (열 인덱스 변화)으로 min 비교 (min(0행 0열 , 0행 1열), min(1행 0열 , 1행 1열))

 

- argmin, argmax : -> return index

print("dimension 지정 안했을 때 : ", torch.argmax(tensor_a))  # 모든 원소 중 최댓값 위치 반환함
print("dim = 0 일 때 : ", torch.argmax(tensor_a, dim=0))  # 행을 기준 (행 인덱스 변화)으로 max 비교 (max(0행 0열 , 1행 0열), max(0행 1열 , 1행 1열)) => 위치 반환
print("dim = 1 일 때 : ", torch.argmax(tensor_a, dim=1)) # 열을 기준 (열 인덱스 변화)으로 max 비교 (max(0행 0열 , 0행 1열), max(1행 0열 , 1행 1열)) => 위치 반환

print("dimension 지정 안했을 때 : ", torch.argmin(tensor_a))  # 모든 원소의 최솟값 위치 반환 함
print("dim = 0 일 때 : ", torch.argmin(tensor_a, dim=0))  # 행을 기준 (행 인덱스 변화)으로 min 비교 (min(0행 0열 , 1행 0열), min(0행 1열 , 1행 1열)) => 위치 반환
print("dim = 1 일 때 : ", torch.argmin(tensor_a, dim=1)) # 열을 기준 (열 인덱스 변화)으로 min 비교 (min(0행 0열 , 0행 1열), min(1행 0열 , 1행 1열)) => 위치 반환

dimension 지정 안했을 때는 텐서를 1차원으로 바꾼 후에 1차원 텐서일 경우에 대한 인덱스를 반환함. 

 

 

행렬 간 연산

- dot (inner product)

print("v1.dot(u1) : ", v1.dot(u1)) # v1 과 u1 내적 (torch.tensor 에도 dot 함수 존재)
print("torch.dot(v1, u1) : ", torch.dot(v1, u1)) # v1 과 u1 내적

 

- 행렬 곱

A = torch.tensor([[1, 2], [3, 4]])  # (2,2) Tensor
B = torch.tensor([[-1, 2], [1, 0]])  # (2,2) Tensor

print("AB : ", torch.matmul(A, B)) # A에서 B를 행렬곱
print("BA : ", B.matmul(A))  # B에서 A를 행렬곱

 

 

Broadcasting

Ex1)

# 0 행의 모든 열을 10 으로 변경하기
tensor_a[0, :] = 10 # 0행의 모든 열에 broadcasting 을 통한 scalar 값 대입

 

Ex2)

tensor_a = torch.randn(3, 2)
## 모든 값을 tensor [0,1]로 변경하기
tensor_a[:, :] = torch.tensor([0, 1]) # 모든 값에 접근하여 [0,1] 로 변경

Ex3)

tensor_a = torch.eye(3)
tensor_b = torch.tensor([1, 2, 3])
# tensor_b = torch.tensor([1, 2, 3]).reshape(3, 1) # 행 벡터로 형식으로 변환
print('A + B : \n', tensor_a + tensor_b) # broadcasting을 통해 (3,) 인 B가 (3,3)으로 변환되어 계산 (행의 확장)

 

B가 1차원인 경우는 예외적으로 Ex3 같은 형태의 broadcasting이 가능했다. 그러나 2차원 이상인 경우 차원을 늘려주어야 한다. (unsqueeze 사용 권장)

 

 

Sparse Tensor

: 0이 많은 행렬들의 경우 값이 0이 아닌 값들의 정보만을 가지고 있는게 메모리 상의 이익이 존재한다. 그렇기에 희소행렬이라는 개념이 생겨났다.

 

Sparse Tensor를 구현하는 방법은 일반적으로 2가지가 존재한다. 

 

1) Sparse Coo Tensor:

는 3개의 1차원 텐서로 이루워져있고 각각은 순서대로 row_idx, col_idx, value를 의미한다.

 

-Coo 변환

a = torch.tensor([[0, 2.], [3, 0]])
a.to_sparse() # COO Sparse tensor 로 변환
"""
tensor(indices=tensor([[0, 1],
                       [1, 0]]),
       values=tensor([2., 3.]),
       size=(2, 2), nnz=2, layout=torch.sparse_coo)
"""

 

-Coo 생성

indices = torch.tensor([[0, 1, 1],[2, 0, 1]]) # 0이 아닌 값의 (index, column) pairs
values = torch.tensor([4, 5, 6]) # 0 이 아닌 값의 values, values의 사이
sparse_tensor = torch.sparse_coo_tensor(indices = indices, values = values, size=(2, 3)) # (2,3)의 sparse tensor

 

 

2)Sparse CSR Tensor:

COO 보다는 덜 직관적인 희소 행렬이지만 메모리 측면에서 더 효율적이다. 

0이 아닌 행의 위치,

0이 아닌 열의 위치,

0이 아닌 값 으로 이루워져있다. 

 

그러니깐 index_pointer 배열을 보면 0 다음에 숫자가 2이다. 이게 0~1 범위 + 1~2 범위에 0이 아닌 값이 존재한다는 뜻이다. 3~3, 3~3 부분은 숫자가 바뀌지 않으니깐 저 범위에는 존재하지 않는 것이다. 구현은 다음과 같은 방식으로 된다.

 

crow_indices = torch.tensor([0, 2, 2]) # 0이 아닌 행의 위치 (첫번쨰는 무조건 0), 즉 row_pointer
col_indices = torch.tensor([0, 1]) # 0이 아닌 열의 위치
values = torch.tensor([1, 2]) # 0이 아닌 값
csr = torch.sparse_csr_tensor(crow_indices = crow_indices, col_indices = col_indices, values = values)

 

 

 

Sparse 연산

- 필요성: 아주 큰 matrix는 연산 시 메모리 아웃이 발생한다.

 

- 사측 연산

print(torch.add(a, b).to_dense() == torch.add(sparse_a, sparse_b).to_dense()) #덧셈
print(torch.mul(a, b).to_dense() == torch.mul(sparse_a, sparse_b).to_dense()) #곱셈
print(torch.matmul(a, b).to_dense() == torch.matmul(sparse_a, sparse_b).to_dense()) #행렬곱

cf) 2차원 이상에서는 행렬곱이 불가능하다.

cf_2) dense matrix(일반 행렬)과의 사측 연산도 자유롭게 가능하다.

 

-인덱싱

일반 텐서와 동일하게 indexing이 가능하지만 slicing(':')은 불가능하다.

 

 

마치며

텐서 조작에 대해 자세히 알아보는 시간을 가져보았다. 

728x90

'Computer Science > [인공지능]' 카테고리의 다른 글

[ComputerVision] ch 1 ~ ch 4  (0) 2024.04.05
[딥러닝] DNN 구현  (0) 2024.03.15
[딥러닝] 성능 고도화  (0) 2024.03.12
[딥러닝] 모델 학습법  (0) 2024.03.06
[Ai] 클러스터링  (0) 2024.02.20
Comments