본문 바로가기
목차
Python

[DL] Tensor: Indexing <Simple, Slicing, Fancy, Boolean Mask>

by ds31x 2024. 3. 18.
728x90
반응형

NumPy나 PyTorch, Tensorflow의 텐서들도

파이썬의 list 또는 tubple 에서의 indexingslicing이 거의 그대 사용됨.

2023.07.12 - [Python] - [Python] list (sequence type) : summary

 

[Python] list (sequence type) : summary

list는 ordered mutable collection으로, collection을 위한 python data type들 중 가장 많이 사용된다. C에서의 array와 같이 가장 기본적인 collection임. 단, heterogeneous item을 가질 수 있으며, 여러 methods를 가지는

ds31x.tistory.com

 

단, multi-dimension에서 square bracket(대가로) 안에서 comma로 구분하여 각 축에 대한 indexing과 slicing을 할 수 있다는 차이가 있음.

  • square bracket안에서 comma로 구분되는 객체들은 index로 사용되며,
  • 해당 객체들의 갯수는 indexing과 slicing의 대상이 되는 source tensor객체의 ndim과 같거나 작아야 함.


1. Simple Indexing and Slicing

1-1. NumPy의 ndarray

  • 가장 기본이 되는 indexing 을 지원함.
  • arr[x,y,z]arr[x][y][z] 둘 다 사용가능함.
  • 후자를 index chaining이라고 부르기도 함. NumPy등에선 전자의 사용을 권함.
a = np.arange(0,12).reshape(3,4)

print(a)
print('==================')
print(f'a[0] is "{a[0]}"')       # 첫번째 행.
print(f'a[0,2] is "{a[0,2]}"')   # 2
print(f'a[0][2] is "{a[0][2]}"') # 2
print('------------------')
print(f'a[1,2:] is "{a[1,2:]}"') # slicing의 활용. 
print(f'a[1,::2] is "{a[1,::2]}"')
print(f'a[1,::-2] is "{a[1,::-2]}"')
print(f'a[1,::-1] is "{a[1,::-1]}"')
print(f'a[1,3:0:-1] is "{a[1,3:0:-1]}"')

1-2. PyTorch의 tensor

  • negative step이 동작하지 않음.
a = np.arange(0,12).reshape(3,4)
a_torch = torch.tensor(a)

print(a_torch)
print(f'a_torch[0] is "{a_torch[0]}"')       # 첫번째 행.
print(f'a_torch[0,2] is "{a_torch[0,2]}"')   # 2
print(f'a_torch[0][2] is "{a_torch[0][2]}"') # 2

print(f'a_torch[1,2:] is "{a_torch[1,2:]}"')
print(f'a_torch[1,::2] is "{a_torch[1,::2]}"')
# print(f'a_torch[1,::-2] is "{a_torch[1,::-2]}"')
# print(f'a_torch[1,::-1] is "{a_torch[1,::-1]}"')
# print(f'a_torch[1,3:0:-1] is "{a_torch[1,3:0:-1]}"')
  • 아래의 3개 statement는 동작하지 않음: negative step

negative step 대신에 역순 처리flip 메서드 또는 torch.flip 함수를 이용하면 됨: 새로운 복사본을 만들고 특정 축만 지정하여 사용가능함.


1-3. Tensorflow의 constant (텐서)

  • 텐서 인스턴스가 immutable이라는 것 이외에는 numpy와 유사함.
a = np.arange(0,12).reshape(3,4)
a_tf = tf.constant(a)

print(a_tf)
print(f'a_tf[0] is "{a_tf[0]}"')       # 첫번째 행.
print(f'a_tf[0,2] is "{a_tf[0,2]}"')   # 2
print(f'a_tf[0][2] is "{a_tf[0][2]}"') # 2

print(f'a_tf[1,2:] is "{a_tf[1,2:]}"')
print(f'a_tf[1,::2] is "{a_tf[1,::2]}"')
# print(f'a_tf[1,::-2] is "{a_tf[1,::-2]}"')
# print(f'a_tf[1,::-1] is "{a_tf[1,::-1]}"')
# print(f'a_tf[1,3:0:-1] is "{a_tf[1,3:0:-1]}"')

2. Fancy Indexing ***

단순한 scalar index나 slicing가 달리,

Fancy indexing

  • index들의 tensor(index tensor)들 을 square bracket 내에 기재하여,
  • 여러 elements를 한번에 선택함.

다음 URL을 자세히 읽어볼 것: https://dsaint31.tistory.com/374

 

[NumPy] Fancy Indexing & Combined Indexing

IndexingNumPy에서 indexing은 4+1 가지 방식을 따름.scalar를 이용한 indexing ( simple indexing ) : array[0]slicingboolean mask : array[array > 1]fancy indexing : vectorized indexing. index들을 element로 가지는 array를 넘겨줌.combined

dsaint31.tistory.com

 

PyTorch나 TensorFlow에선
Advanced Indexing 또는
Tensor based Indexing 이라는 용어로 불림.
Fancy Indexing은 NumPy에서 주로 사용되는 용어임.

Fancy Indexing에서  

index array는 행  or 열 (정확히는 각 axis에서의) index를 나타내는 integer 로 이루어진  "index tensor 인스턴스를 사용"함.

제한점은 square bracket 안에 주어지는 index tensor 객체의 수
source가 되는 tensor객체의 ndim과 같거나 작은 수 여야 함.

 

NumPy의 경우 np.array 가 아닌 list를 사용해도 똑같은 동작을 보장하나,

PyTorch에서는 list를 사용할 경우 동작이 차이가 있다.

반드시,
각자의 tensor를 나타내는 클래스의 객체
index tensor로 사용하길 권한다.

 

즉,

  • NumPy에서는 ndarray 객체를 index로 직접 사용한다
    • 유일하게 NumPy만이 Python의 list 객체를 사용해도 완벽하게 동일하게 동작함.
    • Fancy Indexing의 경우 index 객체의 shape로 결과 객체의 shape가 결정됨.
    • 즉, source가 1d-array라고해도, index로 넘겨진 객체가 2d-array이면, 결과 객체도 index로 넘겨진 객체와 동일한 shape의 2d-array가 됨.
  • PyTorch에서는 tensor 객체를 index로 직접 사용하는게 좋다
    • NumPy의 ndarray객체도 잘 동작하나,
    • Python의 list객체를 사용할 경우, nested list 객체에서 동작이 다르다.
    • integer index 값만으로 구성된 list (= list의 item이 int로 구성됨,1차원)는 같은 값을 가지는 tensor 객체의 경우와 같은 동작을 보이지만, nested list는 그렇지 않음 (axis-0가 unpacking되어 들어가는  동작을 PyTorch에선 보임).
tensorflow의 경우,
tf.gather 와 tf.gather_nd 를 통해
fancy indexing 와 유사한 기능을 제공하나
직접적으로 fancy indexing을 지원하지 않음.

 

2-1. 제한사항과 동작 방식

  • 사용되는 index tensor의 개수는 원본 tensor의 차원 수(number of dimensions, ndim)보다 많으면 동작하지 않음.
    • 예시: "3차원 원본 tensor"에 "index tensor 2개를 사용"하는 경우
      • index tensor들이 2개이므로 원본 텐서의 첫 2 차원(dimension-0, dimension-1)을 결정함
      • 남은 차원(dimension-2)은 결과 텐서에 그대로 유지됨.
  •  구체적 예: 원본 텐서가 2×3×4이고 크기가 3×2인 인덱스 텐서 2개를 사용할 경우
    • index tensor가 2개이므로 원본의 첫 2개 차원을 결정.
    • 각 위치에 원본의 세 번째 차원에 해당하는 길이 4의 1차원 텐서가 배치됨.
    • 결과적으로 3×2×4 크기의 텐서가 생성됨 (2-2를 참고).

예제 코드: 3×5×6인 원본텐서에 대해 2×3×4인 인덱스 텐서 2개를 사용.

import torch

# 원본 텐서: 3 x 5 x 6
x = torch.arange(3 * 5 * 6).reshape(3, 5, 6)

# Fancy indexing에 사용할 index 텐서 2개: shape = (2, 3, 4)
# 예시로 일부 값만 임의 생성
i = torch.tensor([
    [[0, 0, 0, 0],
     [0, 0, 0, 0],
     [0, 0, 0, 0]],

    [[1, 1, 1, 1],
     [1, 1, 1, 1],
     [2, 2, 2, 2]]
])

j = torch.tensor([
    [[0, 0, 0, 0],
     [0, 0, 0, 0],
     [0, 0, 0, 0]],

    [[1, 1, 1, 1],
     [1, 1, 1, 1],
     [2, 2, 2, 2]]
])

# Fancy indexing: i, j의 shape = (2,3,4)가 leading axis가 됨
out = x[i, j]

print(out.shape)   # torch.Size([2, 3, 4, 6])
print(out)         # Fancy indexing 결과

 

개별 접근한 결과는 다음을 참고:

 

두번째 에 대한 설명은 다음과 같음:

  • out[1,2,1,1] 조회 위치 지정
    • 첫 번째 차원 인덱스 1, 두 번째 차원 인덱스 2, 세 번째 차원 인덱스 1, 마지막 차원 인덱스 1 선택 의미
  • 해당 값이 원래 x 텐서에서 유도되는 구조
    • 사용되는 인덱스가
      • i[1,2,1]의 값 2,
      • j[1,2,1]의 값 2인 상황
    • 원본 텐서에서 참조되는 위치가 x[2, 2, 1]인 구조
      • x[2, 2, 1] 계산식이 2 * (5 * 6) + 2 * 6 + 1인 구성
      • 계산 결과가 60 + 12 + 1 = 73인 도출
  • 최종적으로 out[1,2,1,1] = 73인 결론

 


2-2. index tensor와 결과 텐서의 형태(shape)

index tensor는 많은 경우 1차원으로 사용하나, 원하는 출력 텐서 형태로 구성하는 경우도 많음.

  • 기본적으로 모든 index tensor는 동일한 shape를 가지며,
    이 형태가 결과 텐서의 shape의 앞에 위치하는 axis들 (leadng axes)의 크기를 결정함.
  • index tensor들의 형태가 다를 경우, 이들 간에 브로드캐스팅(broadcasting)이 이루어져 최종 결과 텐서의 형태가 결정
  • broadcasting이 안되는 경우엔 동작하지 않음.

https://dsaint31.tistory.com/359

 

[NumPy] Broadcasting

0. Broadcasting이란?tensor와 scalar를 연산시킬 때 scalar를 상대 tensor와 같은 shape이면서 해당 scalar의 값을 가진 tensor로 변경시키고나서 이 scalar로부터 만들어진 tensor와 상대 tensor를 동작시키는 방식

dsaint31.tistory.com


2-3. index tensor 대신 Python list 를 사용할 경우의 차이점

  • 스칼라(scalar)로 구성된 리스트만 이와 같은 값의 indexing tensor와 완전히 동일하게 동작함: non-nested list를 의미.
  • 중첩 리스트(nested list) l을 사용할 경우 src_tensor[l]src_tensor[l[0], l[1], ...]과 같이 동작함: 일종의 unpacking! src_tensor[*l]
  • 때문에, 첫 번째 차원의 크기가 원본 텐서의 차원 수보다 많으면 동작하지 않음: indexing tensor의 경우엔 첫 번째 차원의 크기가 커도 상관없음.

다음의 예제 코드로 반드시 이해할 것: https://gist.github.com/dsaint31x/a5c5b11fc3e1d75e5a0e4fa51ef03842

 

dl_fancyindexing_list.ipynb

dl_fancyindexing_list.ipynb. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com


다음 예는 1d tensor에서 fancy indexing을 사용하는 것을 보여줌.

x = np.array([10.,20.,30.,40.,50.])
x_torch = torch.tensor([10.,20.,30.,40.,50.])
x_tf = tf.constant([10.,20.,30.,40.,50.])

f_indices = [3, 4, 1]

print('original:')
print(x)
print('----------')
print('numpy:')
print(x[f_indices])
print('----------')
print('torch:')
print(x_torch[f_indices])
print('----------')
print('tensorflow:')
print(tf.gather(x_tf,f_indices)) #1D 에선 gahter, 2D 이상시 gather_nd
print(tf.gather_nd(x_tf, [ i for i in zip(f_indices,)])) # 굳이 쓴다면, 다음과 같이.
  • tensorflow의 경우, fancy indexing을 직접적으로 지원하지 않으며
  • tf.gathertf.gather_nd 함수를 통해 같은 동작을 수행할 수 있음.
  • f_indices가 source tensor인 x와 같은 ndim을 가지고 있는 간단한 예제임.
  • f_indices를 numpy.array 객체나 torch.tensor 객체로 바꾸면, 다른 ndim으로 해도 동작함: 실제 구현에서는 많이 사용되는 방식.

다음 예는 2d tensor에서 fancy indexing을 사용하는 것을 보여줌.

x = np.arange(5*5).reshape(5,5) * 10
x_torch = torch.arange(5*5).view(size=(5,5)) * 10
x_tf = tf.constant(x)

indices_0 = [0, 1, 2]
indices_1 = [0, 1, 2]

print('original:')
print(x)
print('----------')
print('numpy:')
b = x[indices_0, indices_1]
print('b.shape =',b.shape)
print(b)
print('----------')
print('torch:')
c = x_torch[indices_0, indices_1]
print('c.shape =',c.shape)
print(c)
print('----------')
print('tensorflow:')
d = tf.gather_nd(x_tf, [ i for i in zip(indices_0, indices_1)])
print('d.shape =',d.shape)
print(d)
  • tensorflow의 경우, fancy indexing을 직접적으로 지원하지 않으며
  • tf.gather_nd 함수를 통해 같은 동작을 수행할 수 있음.

다음 예는 3d tensor에서 fancy indexing을 사용하는 것을 보여준다.

각각의 축에서 index array를 접근하려는 elements 에 맞게 설정함

x = np.arange(5*5*5).reshape(5,5,5) * 10
x_torch = torch.arange(5*5*5).view(size=(5,5,5)) * 10
x_tf = tf.constant(x)

indices_0 = [0, 1] # x
indices_1 = [1, 2] # y
indices_2 = [2, 0] # z

print('original:')
print(x)
print('----------')
print('numpy:')
b = x[indices_0, indices_1, indices_2]
print('b.shape=',b.shape)
print(b)
print('----------')
print('torch:')
c = x_torch[indices_0, indices_1, indices_2]
print('c.shape=',c.shape)
print(c)
print('----------')
print('tensorflow')
d = tf.gather_nd(x_tf, [ i for i in zip(indices_0, indices_1, indices_2)]) # multi-dim 에선 gater_nd 임.
print('d.shape=',d.shape)
print(d)
  • tensorflow의 경우, fancy indexing을 직접적으로 지원하지 않으며
  • tf.gather_nd 함수를 통해 같은 동작을 수행할 수 있음.

3. Boolean Mask **

대상이 되는 tensor와 같은 shape를 가지는 boolean mask의 tensor를 통해 복수의 특정 element를 추출할 수 있음.

  • 텐서 인스턴스에 비교 연산자를 적용하여 boolean mask를 얻을 수 있음 (다음 예에서 b가 boolean mask인 tensor 인스턴스임)
  • 해당 텐서 인스턴스에 관계(relational, 비교)연산자으로 구성된 expression(=condition이라고 불림)
    "index가 기재되는 square bracket 안에 넣는 방식"으로의 활용이 많음.

아래 예는 numpy의 ndarray 인스턴스에서의 활용을 보여줌.

x = np.arange(3*3*3).reshape(3,3,3) * 10

print('original:')
print(x)
print('----------')
print('boolean mask:')
b = x <= 270/2
print(b.shape)
print(b)
print('----------')
print('x <= 135')
print(x[b])
print('----------')
print(x[x<=270/2])
print('----------')
print('----------')
print('x <= 135 | x>= 200')
b1 = b | (x >= 200)
print('----------')
print('boolean mask')
print(b1)
print('----------')
print(x[b1])
print('----------')
print(x[ (x<=270/2) | (x>=200)])
  • boolean mask를 사용하는 방법은 조건을 만족하는 elements를 찾는데 주로 이용됨..
  • 비교 연산자를 텐서에 사용할 경우, 대상 텐서와 같은 shape의 boolean mask 를 얻게 됨.
  • boolean mask를 변수에 할당한 후 처리하는 것보다 조건을 square bracket 안에 넣어주는 경우가 더 많음.
  • condition을 여러 개 사용할 경우 and, or, not이 아닌 &, |, ~ 를 사용해야함.

 

위의 경우에서 135 이하 또는 200 이상인 elements를 선택하는 동작을

torch와 tensorflow의 tensor 인스턴스로 수행하는 것을 아래의 예에서 보여줌.

x_torch = torch.arange(3*3*3).view(size=(3,3,3)) * 10
print(x_torch[ (x_torch<=270/2) | (x_torch>=200)])

print('--------------')

x_tf = tf.constant(x)
print(x_tf[ (x_tf<= tf.cast(270/2, tf.int64)) | (x_tf>=200)])

 

3-1. 특정 조건에 맞는 element의 index 자체를 얻기: np.where

위의 예에서는 조건에 맞는 value에 접근하는 방법을 보여줌.

 

만약 해당 조건에 맞는 value들이 있는 index를 얻고자 한다면, np.where 를 사용하면 된다.

2024.03.19 - [Python] - [ML] where: numpy 의 idx찾기

 

[ML] where: numpy 의 idx찾기

np.where 함수numpy에서 ndarray 인스턴스에서 특정 조건을 만족하는 elements의 위치(index, idx)를 찾는 기능을numpy 모듈의 where 함수가 제공해줌.사실 where 함수는 특정 조건에 따라 값을 바꾸어주는 기능

ds31x.tistory.com


위의 예제 코드들을 수행해 본 ipynb 임. (combined indexing을 포함함)

https://gist.github.com/dsaint31x/8dae0cde657c7ffc8bd768f35591cedd

 

dl_tensor_indexing.ipynb

dl_tensor_indexing.ipynb. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

728x90