# Tensor 인덱싱과 슬라이싱

PyTorch에서 텐서의 인덱싱과 슬라이싱은 텐서의 특정 요소나 하위 텐서에 접근할 수 있는 중요한 기능이다. 이를 통해 복잡한 데이터 구조에서 원하는 데이터만 추출하거나, 텐서의 일부를 변경할 수 있다. 파이썬의 `numpy` 라이브러리를 사용해본 경험이 있다면, PyTorch의 인덱싱과 슬라이싱도 매우 유사하게 사용할 수 있을 것이다.

### 기본 인덱싱

텐서의 기본 인덱싱은 텐서의 특정 위치에 있는 요소에 접근하는 방법이다. 일반적으로 정수 인덱스를 사용하여 특정 위치에 있는 값을 가져오거나 수정할 수 있다. 예를 들어, 다음과 같은 1차원 텐서가 있다고 하자:

```python
import torch

x = torch.tensor([1, 2, 3, 4, 5])
```

이때, `x[0]`은 텐서의 첫 번째 요소인 `1`을 반환하고, `x[2]`는 세 번째 요소인 `3`을 반환한다. 인덱스는 0부터 시작한다는 점을 유의해야 한다.

### 다차원 텐서 인덱싱

2차원 이상의 텐서에서도 동일한 방식으로 인덱싱을 사용할 수 있다. 예를 들어, 다음과 같은 2차원 텐서를 고려하자:

```python
y = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
```

이 텐서에서 첫 번째 행의 두 번째 요소에 접근하려면 `y[0, 1]`을 사용한다. 이 경우, 결과는 `2`가 된다. 수학적으로 표현하면, 행렬 (\mathbf{Y})의 요소는 다음과 같이 나타낼 수 있다:

\[ \mathbf{Y} = \begin{bmatrix} 1 & 2 & 3 \ 4 & 5 & 6 \ 7 & 8 & 9 \end{bmatrix} ]

이때, (\mathbf{Y}\_{0,1})은 행렬의 첫 번째 행, 두 번째 열의 요소를 의미하며, 값은 `2`이다.

### 슬라이싱

슬라이싱은 텐서의 일부를 추출하는 방법으로, 특정 범위의 요소를 선택할 수 있다. 슬라이싱의 일반적인 형식은 다음과 같다:

```python
tensor[start:stop:step]
```

#### 1차원 텐서 슬라이싱

1차원 텐서에서 슬라이싱을 사용하는 경우를 살펴보자:

```python
z = torch.tensor([10, 20, 30, 40, 50])
sub_z = z[1:4]  # [20, 30, 40]
```

위 코드에서 `z[1:4]`는 텐서 `z`의 두 번째 요소부터 네 번째 요소까지 선택하여 새로운 텐서를 반환한다. 이와 같은 방법으로 텐서의 특정 부분을 쉽게 추출할 수 있다.

#### 다차원 텐서 슬라이싱

다차원 텐서에서도 유사하게 슬라이싱을 사용할 수 있다. 예를 들어, 2차원 텐서의 경우는 다음과 같다:

```python
a = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
sub_a = a[0:2, 1:3]  # [[2, 3], [5, 6]]
```

위 코드에서 `a[0:2, 1:3]`은 첫 번째와 두 번째 행, 그리고 두 번째와 세 번째 열을 포함하는 하위 텐서를 추출한다. 수학적으로 표현하면, 슬라이싱된 하위 텐서는 다음과 같다:

\[ \mathbf{A}\_{\text{sub}} = \begin{bmatrix} 2 & 3 \ 5 & 6 \end{bmatrix} ]

### 특정 차원에 대한 인덱싱 및 슬라이싱

PyTorch에서는 다차원 텐서에서 특정 차원에 대해 인덱싱과 슬라이싱을 개별적으로 적용할 수 있다. 예를 들어, 3차원 텐서에서 특정 "채널"에 있는 데이터를 추출하거나, 특정 "축"을 기준으로 슬라이싱할 수 있다. 이러한 작업은 특히 이미지 데이터 처리에서 유용하다.

예를 들어, 크기가 (3 \times 4 \times 5)인 3차원 텐서를 고려하자. 이 텐서를 다음과 같이 생성할 수 있다:

```python
b = torch.arange(60).reshape(3, 4, 5)
```

이 텐서는 3개의 행렬을 가지고 있으며, 각 행렬은 (4 \times 5) 크기를 가진다. 이제 첫 번째 차원(채널)에서 첫 번째 행렬을 선택해 보자:

```python
first_channel = b[0]
```

수학적으로 이는 (\mathbf{B}\_{0,:,:})와 같으며, 첫 번째 채널의 모든 행과 열을 선택하는 것이다.

#### 축별 슬라이싱

특정 차원을 기준으로 슬라이싱할 때는 슬라이싱을 해당 차원에서만 적용할 수 있다. 예를 들어, 텐서 (\mathbf{B})의 두 번째 채널에서 두 번째와 세 번째 행만 선택하는 경우를 살펴보자:

```python
sub_b = b[1, 1:3, :]
```

이 경우 `b[1, 1:3, :]`은 두 번째 채널에서 두 번째와 세 번째 행 전체를 선택하여 새로운 하위 텐서를 반환한다. 수학적으로 표현하면, 이 하위 텐서는 다음과 같다:

\[ \mathbf{B}\_{\text{sub}} = \begin{bmatrix} 26 & 27 & 28 & 29 & 30 \ 31 & 32 & 33 & 34 & 35 \end{bmatrix} ]

### 논리적 인덱싱

논리적 인덱싱(Logical Indexing)은 조건문을 사용하여 특정 조건을 만족하는 요소를 선택하는 방법이다. 이를 통해 텐서의 값들을 필터링하거나 원하는 조건의 값만 변경할 수 있다.

#### 예제

아래의 코드를 살펴보자:

```python
x = torch.tensor([10, 20, 30, 40, 50])
mask = x > 25
filtered_x = x[mask]  # [30, 40, 50]
```

위 예제에서 `x > 25`는 텐서의 각 요소가 25보다 큰지 아닌지를 판단하여 불리언 텐서 `mask`를 생성한다. 이 마스크를 사용하여 25보다 큰 값들만 선택할 수 있다. 수학적으로 이는 다음과 같다:

\[ \mathbf{x} = \begin{bmatrix} 10 & 20 & 30 & 40 & 50 \end{bmatrix}, \quad \mathbf{x}\_{\text{filtered}} = \begin{bmatrix} 30 & 40 & 50 \end{bmatrix} ]

### 인덱싱을 사용한 텐서 값 변경

인덱싱과 슬라이싱은 단순히 데이터를 추출하는 데 그치지 않고, 선택된 부분의 값을 변경하는 데에도 사용할 수 있다. 예를 들어, 특정 위치의 값을 변경하거나, 슬라이싱을 통해 추출된 부분을 수정할 수 있다.

#### 예제

다음 예제에서 텐서의 특정 요소와 부분을 변경해 보자:

```python
x = torch.tensor([1, 2, 3, 4, 5])
x[1] = 10  # [1, 10, 3, 4, 5]
x[2:4] = torch.tensor([20, 30])  # [1, 10, 20, 30, 5]
```

위 코드에서는 `x[1]`을 10으로 변경하고, `x[2:4]`을 `[20, 30]`으로 변경하였다. 이를 통해 텐서의 일부 요소를 쉽게 수정할 수 있다. 수학적으로 이를 나타내면 다음과 같다:

\[ \mathbf{x} = \begin{bmatrix} 1 & 2 & 3 & 4 & 5 \end{bmatrix} \rightarrow \begin{bmatrix} 1 & 10 & 20 & 30 & 5 \end{bmatrix} ]

### 고급 인덱싱

고급 인덱싱(Advanced Indexing)은 기본적인 정수 인덱싱이나 슬라이싱을 넘어서, 특정 패턴의 인덱스를 통해 텐서의 요소에 접근하는 방법이다. 고급 인덱싱은 브로드캐스팅, 정수 배열 인덱싱, 그리고 혼합 인덱싱을 포함한다.

#### 정수 배열 인덱싱

정수 배열 인덱싱은 정수 배열을 사용하여 여러 위치의 요소를 동시에 선택하는 방법이다. 이를 통해 특정 위치에 있는 여러 요소를 한 번에 선택할 수 있다. 예를 들어, 다음과 같은 텐서를 생각해 보자:

```python
x = torch.tensor([10, 20, 30, 40, 50])
indices = torch.tensor([0, 2, 4])
selected = x[indices]  # [10, 30, 50]
```

위 코드에서 `indices`는 선택하고자 하는 위치를 나타내는 정수 배열이다. 이를 통해 텐서 `x`에서 첫 번째, 세 번째, 다섯 번째 요소를 선택할 수 있다. 수학적으로 표현하면 다음과 같다:

\[ \mathbf{x} = \begin{bmatrix} 10 & 20 & 30 & 40 & 50 \end{bmatrix}, \quad \mathbf{x}\_{\text{selected}} = \begin{bmatrix} 10 & 30 & 50 \end{bmatrix} ]

#### 브로드캐스팅 인덱싱

PyTorch에서 고급 인덱싱은 브로드캐스팅 규칙을 따르므로, 다양한 크기의 인덱스 배열을 결합하여 더 복잡한 인덱싱을 수행할 수 있다. 예를 들어, 다음과 같은 2차원 텐서가 있을 때:

```python
y = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
rows = torch.tensor([0, 1])
cols = torch.tensor([1, 2])
selected_elements = y[rows, cols]  # [2, 6]
```

위 코드에서 `rows`와 `cols`는 각각 행과 열의 인덱스를 나타내는 정수 배열이다. `y[rows, cols]`는 `(0,1)`과 `(1,2)` 위치에 있는 값을 선택하여 `[2, 6]`을 반환한다. 수학적으로 표현하면 다음과 같다:

\[ \mathbf{Y} = \begin{bmatrix} 1 & 2 & 3 \ 4 & 5 & 6 \ 7 & 8 & 9 \end{bmatrix}, \quad \mathbf{Y}\_{\text{selected}} = \begin{bmatrix} 2 & 6 \end{bmatrix} ]

#### 혼합 인덱싱

PyTorch에서는 기본 인덱싱과 고급 인덱싱을 혼합하여 사용할 수 있다. 이를 통해 더 유연한 인덱싱이 가능해진다. 예를 들어, 다음과 같은 3차원 텐서에서 특정 패턴으로 요소를 선택할 수 있다:

```python
z = torch.arange(24).reshape(2, 3, 4)
selected = z[1, :, [1, 3]]  # [[17, 19], [21, 23], [25, 27]]
```

위 코드에서 `z[1, :, [1, 3]]`은 두 번째 채널에서 모든 행의 두 번째와 네 번째 열을 선택하여 새로운 텐서를 반환한다. 이 경우 각 행에서 선택된 요소를 포함한 하위 텐서를 얻을 수 있다. 수학적으로 이를 나타내면 다음과 같다:

\[ \mathbf{Z} = \text{3D 텐서}, \quad \mathbf{Z}\_{\text{selected}} = \begin{bmatrix} 17 & 19 \ 21 & 23 \ 25 & 27 \end{bmatrix} ]

### 슬라이싱의 확장: `...` 연산자

PyTorch에서는 `...` 연산자를 사용하여 다차원 텐서에서 복잡한 슬라이싱을 간단하게 표현할 수 있다. 이 연산자는 모든 차원을 포함하는 축약 표현으로, 고차원 텐서에서 특정 차원만 슬라이싱할 때 유용하다.

예를 들어, 다음과 같은 4차원 텐서가 있을 때:

```python
w = torch.rand(2, 3, 4, 5)
```

첫 번째와 두 번째 차원 전체에서 특정 슬라이싱을 수행하고 싶을 때, `...`을 사용할 수 있다:

```python
slice_w = w[..., 2:4]  # 마지막 두 차원에 슬라이싱 적용
```

이 코드는 첫 번째와 두 번째 차원을 유지하면서, 세 번째 차원에서 `2:4` 범위의 슬라이싱을 수행한다.

### Boolean Masking

Boolean Masking은 특정 조건에 따라 텐서의 값을 필터링하거나 선택할 수 있는 강력한 방법이다. 이를 통해 텐서 내에서 조건에 맞는 요소들만 골라내거나, 그 값을 변경할 수 있다. 이는 특히 데이터 전처리 과정에서 유용하다.

#### 예제: Boolean Masking으로 값 선택

예를 들어, 다음과 같은 텐서가 있다고 하자:

```python
a = torch.tensor([5, 10, 15, 20, 25, 30])
mask = a > 15
filtered_a = a[mask]  # [20, 25, 30]
```

위의 코드에서 `a > 15`는 각 요소가 `15`보다 큰지 여부를 판단하는 불리언 텐서를 생성한다. 이 불리언 텐서 `mask`를 이용해 `15`보다 큰 값들만 선택하여 새로운 텐서를 반환한다. 수학적으로 표현하면 다음과 같다:

\[ \mathbf{a} = \begin{bmatrix} 5 & 10 & 15 & 20 & 25 & 30 \end{bmatrix}, \quad \mathbf{a}\_{\text{filtered}} = \begin{bmatrix} 20 & 25 & 30 \end{bmatrix} ]

#### 예제: Boolean Masking으로 값 변경

Boolean Masking을 사용하여 조건에 맞는 요소의 값을 변경할 수도 있다. 다음 코드를 살펴보자:

```python
b = torch.tensor([1, 2, 3, 4, 5])
b[b < 3] = 0  # [0, 0, 3, 4, 5]
```

위 예제에서 `b[b < 3] = 0`은 텐서 `b`의 요소 중 `3`보다 작은 모든 값을 `0`으로 변경한다. 수학적으로 표현하면:

\[ \mathbf{b} = \begin{bmatrix} 1 & 2 & 3 & 4 & 5 \end{bmatrix} \rightarrow \begin{bmatrix} 0 & 0 & 3 & 4 & 5 \end{bmatrix} ]

### 인덱스 텐서를 사용한 값 할당

PyTorch에서는 인덱싱을 사용하여 단순히 값을 가져오는 것뿐 아니라, 선택된 요소에 새로운 값을 할당할 수 있다. 이때, 인덱스 텐서를 사용하면 특정 위치에 다양한 값을 효율적으로 할당할 수 있다.

#### 정수 배열을 통한 값 할당

예를 들어, 다음과 같은 텐서에서 인덱스를 사용하여 값을 할당해보자:

```python
c = torch.tensor([10, 20, 30, 40, 50])
indices = torch.tensor([1, 3])
c[indices] = torch.tensor([99, 88])  # [10, 99, 30, 88, 50]
```

위 코드에서 `indices`는 `c`의 두 번째와 네 번째 위치를 가리키며, 이 위치의 값들이 각각 `99`와 `88`로 변경된다. 수학적으로 표현하면:

\[ \mathbf{c} = \begin{bmatrix} 10 & 20 & 30 & 40 & 50 \end{bmatrix} \rightarrow \begin{bmatrix} 10 & 99 & 30 & 88 & 50 \end{bmatrix} ]

### 인덱싱을 활용한 다양한 활용 예시

인덱싱과 슬라이싱의 응용은 매우 다양하다. 이를 통해 텐서의 데이터를 효율적으로 다룰 수 있으며, 다양한 형태의 연산을 빠르고 간결하게 처리할 수 있다.

#### 예제: 특정 축을 따라 평균 계산하기

다음과 같은 2차원 텐서가 있다고 하자:

```python
d = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
column_means = d.mean(dim=0)  # [4.0, 5.0, 6.0]
```

위의 코드에서 `d.mean(dim=0)`은 각 열의 평균을 계산하여 새로운 1차원 텐서를 반환한다. 수학적으로 표현하면:

\[ \mathbf{d} = \begin{bmatrix} 1 & 2 & 3 \ 4 & 5 & 6 \ 7 & 8 & 9 \end{bmatrix}, \quad \text{Mean}\_{\text{columns}} = \begin{bmatrix} 4.0 & 5.0 & 6.0 \end{bmatrix} ]

#### 예제: 특정 축을 따라 최대값 위치 찾기

PyTorch의 `argmax` 함수를 이용하여 특정 축에서 최대값의 위치를 찾을 수도 있다:

```python
e = torch.tensor([[4, 9, 1], [7, 3, 2], [5, 6, 8]])
max_indices = e.argmax(dim=1)  # [1, 0, 2]
```

위 코드에서 `e.argmax(dim=1)`은 각 행에서 가장 큰 값의 인덱스를 반환한다. 첫 번째 행에서는 `9`, 두 번째 행에서는 `7`, 세 번째 행에서는 `8`이 가장 크므로 인덱스는 각각 `1`, `0`, `2`가 된다.

### 텐서의 차원 축소와 유지

슬라이싱과 인덱싱을 할 때 차원이 줄어들거나 유지되는 것이 중요하다. PyTorch에서는 슬라이싱을 통해 차원이 감소하지만, `None`이나 `unsqueeze()` 메서드를 사용하여 차원을 유지하거나 추가할 수 있다. 예를 들어:

```python
f = torch.tensor([[10, 20], [30, 40], [50, 60]])
row_vector = f[1, :]  # [30, 40], 1차원 텐서
column_vector = f[:, 1].unsqueeze(1)  # [[20], [40], [60]], 2차원 텐서
```

첫 번째 경우는 1차원 텐서 `[30, 40]`을 반환하고, 두 번째 경우는 `unsqueeze(1)`을 사용하여 2차원으로 변형된 텐서를 반환한다.
