데이터 인코딩
소개 및 표기법
양자 알고리즘을 사용하려면 고전 데이터를 어떤 방식으로든 양자 Circuit에 가져와야 합니다. 이를 보통 데이터 인코딩이라고 하며, 데이터 로딩이라고도 부릅니다. 이전 단원에서 다룬 특징 매핑(feature mapping) 개념을 떠올려 보세요. 특징 매핑은 데이터의 특징을 한 공간에서 다른 공간으로 매핑하는 것입니다. 고전 데이터를 양자 컴퓨터로 단순히 옮기는 것도 일종의 매핑이며, 이를 특징 매핑이라고 부를 수 있습니다. 실제로 Qiskit에 내장된 특징 매핑(예: z_feature_map이나 ZZFeatureMap)은 대개 회전 레이어와 얽힘 레이어를 포함하여 상태를 힐베르트 공간의 여러 차원으로 확장합니다. 이 인코딩 과정은 양자 머신러닝 알고리즘의 핵심 부분으로, 알고리즘의 계산 능력에 직접적인 영향을 미칩니다.
아래의 일부 인코딩 기법은 고전적으로도 효율적으로 시뮬레이션할 수 있습니다. 이는 특히 곱 상태(product state)를 생성하는 인코딩 방법(즉, Qubit를 얽히게 하지 않는 방법)에서 쉽게 확인할 수 있습니다. 그리고 양자 유용성(quantum utility)은 데이터셋의 양자적 복잡성이 인코딩 방법과 잘 맞아떨어질 때 가장 발현될 가능성이 높습니다. 따라서 직접 인코딩 Circuit을 작성해야 하는 경우가 많을 것입니다. 여기서는 다양한 인코딩 전략을 비교·대조하고 어떤 것이 가능한지 살펴볼 수 있도록 폭넓은 전략을 소개합니다. 인코딩 기법의 유용성에 대해 몇 가지 일반적인 진술을 할 수 있습니다. 예를 들어, 완전 얽힘 방식의 efficient_su2(아래 참고)는 곱 상태를 생성하는 방법(z_feature_map 등)보다 데이터의 양자적 특징을 포착할 가능성이 훨씬 높습니다. 그렇다고 efficient_su2가 충분하거나 여러분의 데이터셋에 충분히 잘 맞아 양자 속도 향상을 보장한다는 의미는 아닙니다. 이는 모델링하거나 분류하려는 데이터 구조를 신중하게 고려해야 합니다. 또한 Circuit 깊이와의 균형도 중요합니다. Circuit의 Qubit를 완전히 얽히게 하는 특징 매핑은 매우 깊은 Circuit을 만드는 경우가 많아, 현재 양자 컴퓨터에서 유용한 결과를 얻기 어려울 수 있습니다.
표기법
데이터셋은 개의 데이터 벡터 집합입니다: . 각 벡터는 차원, 즉 입니다. 이는 복소 데이터 특징으로도 확장할 수 있습니다. 이 단원에서는 전체 집합 과 특정 원소 등의 표기법을 가끔 사용합니다. 하지만 대부분의 경우 데이터셋에서 한 번에 하나의 벡터를 불러오는 것을 다루며, 개의 특징을 가진 단일 벡터를 단순히 로 표기하는 경우가 많습니다.
또한 는 데이터 벡터 의 특징 매핑 를 나타내는 데 사용하는 것이 일반적입니다. 양자 컴퓨팅에서는 특히 라는 표기를 사용하는 경우가 많은데, 이는 이러한 연산의 유니타리 특성 을 강조하는 표기법입니다. 두 표기 모두 올바르게 사용할 수 있으며, 둘 다 특징 매핑입니다. 이 과정에서는 다음과 같이 사용합니다:
- 머신러닝에서 특징 매핑을 일반적으로 논의할 때는 를,
- 특징 매핑의 Circuit 구현을 논의할 때는 를 사용합니다.
정규화와 정보 손실
고전 머신 러닝에서 훈련 데이터의 특징은 종종 "정규화"되거나 재조정되며, 이는 대개 모델 성능을 향상시킵니다. 일반적인 방법 중 하나는 최솟값-최댓값 정규화(min-max normalization) 또는 표준화(standardization)를 사용하는 것입니다. 최솟값-최댓값 정규화에서는 데이터 행렬 의 특징 열(예: 특징 )이 다음과 같이 정규화됩니다:
여기서 min과 max는 데이터셋 의 개 데이터 벡터에 걸쳐 특징 의 최솟값과 최댓값을 의미합니다. 이렇게 하면 모든 특징 값이 단위 구간 안에 들어오게 됩니다: 모든 , 에 대해 .
정규화는 양자역학과 양자 컴퓨팅에서도 근본적인 개념이지만, 최솟값-최댓값 정규화와는 약간 다릅니다. 양자역학에서의 정규화는 상태 벡터 의 길이(양자 컴퓨팅 맥락에서는 2-노름)가 1이어야 한다는 조건을 의미합니다: . 이는 측정 확률의 합이 1임을 보장합니다. 상태는 2-노름으로 나누어 정규화됩니다. 즉, 다음과 같이 재조정합니다:
양자 컴퓨팅과 양자역학에서 이는 사람들이 데이터에 인위적으로 부과하는 정규화가 아니라, 양자 상태의 근본적인 성질입니다. 인코딩 방식에 따라 이 제약이 데이터 재조정 방식에 영향을 줄 수 있습니다. 예를 들어, 진폭 인코딩(아래 참고)에서는 양자역학의 요구에 따라 데이터 벡터가 로 정규화되며, 이는 인코딩되는 데이터의 스케일링에 영향을 미칩니다. 위상 인코딩에서는 특징 값을 로 재조정하도록 권장하는데, 이는 Qubit 위상 각도로 인코딩할 때 발생하는 모듈로 효과로 인한 정보 손실을 방지하기 위해서입니다[1,2].
인코딩 방법
다음 몇 개의 절에서는 개의 데이터 벡터로 구성된 소규모 고전 데이터셋 예시 를 참고합니다. 각 벡터는 개의 특징을 가집니다:
위에서 소개한 표기법으로 예를 들면, 집합 의 번째 데이터 벡터의 번째 특징은 라고 할 수 있습니다.
기저 인코딩
기저 인코딩은 고전 -비트 문자열을 -Qubit 시스템의 계산 기저 상태로 인코딩합니다. 예를 들어 을 생각해봅시다. 이는 -비트 문자열 로 표현할 수 있으며, -Qubit 시스템에서 양자 상태 로 나타냅니다. 일반적으로 -비트 문자열에 대해: 이면, 대응하는 -Qubit 상태는 이며, 에 대해 입니다. 이는 단일 특징에 대한 것임을 유의하세요.
양자 컴퓨팅에서 기저 인코딩은 각 고전 비트를 별도의 Qubit로 나타내며, 데이터의 이진 표현을 계산 기저의 양자 상태로 직접 매핑합니다. 여러 특징을 인코딩해야 할 경우, 각 특징을 먼저 이진 형식으로 변환한 다음 별개의 Qubit 그룹에 할당합니다. 각 특징마다 하나의 그룹이 배정되며, 각 Qubit는 해당 특징의 이진 표현에서 하나의 비트를 반영합니다.
예시로 벡터 (5, 7, 0)을 인코딩해 봅시다.
모든 특징이 4비트로 저장된다고 가정합니다(필요한 것보다 많지만, 십진수 한 자리 정수를 모두 표현하기에 충분합니다):
5 → binary 0101
7 → binary 0111
0 → binary 0000
이 비트 문자열들은 세 개의 4-Qubit 집합에 할당되므로, 전체 12-Qubit 기저 상태는:
여기서 처음 네 Qubit는 첫 번째 특징을, 다음 네 Qubit는 두 번째 특징을, 마지막 네 Qubit는 세 번째 특징을 나타 냅니다. 아래 코드는 데이터 벡터 (5,7,0)을 양자 상태로 변환하며, 다른 한 자리 특징에도 일반화하여 적용할 수 있습니다.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit
from qiskit import QuantumCircuit
# Data point to encode
x = 5 # binary: 0101
y = 7 # binary: 0111
z = 0 # binary: 0000
# Convert each to 4-bit binary list
x_bits = [int(b) for b in format(x, "04b")] # [0,1,0,1]
y_bits = [int(b) for b in format(y, "04b")] # [0,1,1,1]
z_bits = [int(b) for b in format(z, "04b")] # [0,0,0,0]
# Combine all bits
all_bits = x_bits + y_bits + z_bits # [0,1,0,1,0,1,1,1,0,0,0,0]
# Initialize a 12-qubit quantum circuit
qc = QuantumCircuit(12)
# Apply x-gates where the bit is 1
for idx, bit in enumerate(all_bits):
if bit == 1:
qc.x(idx)
qc.draw("mpl")

이해도 확인
아래 질문을 읽고 답을 생각한 후, 삼각형을 클릭하여 정답을 확인하세요.
예시 데이터셋 의 첫 번째 벡터:
를 기저 인코딩을 사용하여 인코딩하는 코드를 작성하세요.
정답:
import math
from qiskit import QuantumCircuit
# Data point to encode
x = 4 # binary: 0100
y = 8 # binary: 1000
z = 5 # binary: 0101
# Convert each to 4-bit binary list
x_bits = [int(b) for b in format(x, '04b')] # [0,1,0,0]
y_bits = [int(b) for b in format(y, '04b')] # [1,0,0,0]
z_bits = [int(b) for b in format(z, '04b')] # [0,1,0,1]
# Combine all bits
all_bits = x_bits + y_bits + z_bits # [0,1,0,0,1,0,0,0,0,1,0,1]
# Initialize a 12-qubit quantum circuit
qc = QuantumCircuit(12)
# Apply x-gates where the bit is 1
for idx, bit in enumerate(all_bits):
if bit == 1:
qc.x(idx)
qc.draw('mpl')
진폭 인코딩
진폭 인코딩은 데이터를 양자 상태의 진폭(amplitude)으로 인코딩합니다. 정규화된 고전적인 차원 데이터 벡터 를 -Qubit 양자 상태 의 진폭으로 표현합니다:
여기서 은 앞서와 동일한 데이터 벡터의 차원, 는 의 번째 원소, 은 번째 계산 기저 상태입니다. 는 인코딩할 데이터로부터 결정되는 정규화 상수이며, 이는 양자역학에서 부과되는 정규화 조건입니다:
일반적으로 이 조건은 모든 데이터 벡터에 걸쳐 각 특성에 적용되는 최솟값/최댓값 정규화와는 다른 조건입니다. 이를 어떻게 처리할지는 풀고자 하는 문제에 따라 달라지지만, 위의 양자역학적 정규화 조건을 피할 방법은 없습니다.
진폭 인코딩에서는 데이터 벡터의 각 특성이 서로 다른 양자 상태의 진폭으로 저장됩니다. -Qubit 시스템은 개의 진폭을 제공하므로, 개의 특성을 진폭 인코딩하려면 개의 Qubit이 필요합니다.
예시로, 예제 데이터셋 의 첫 번째 벡터인 를 진폭 인코딩으로 인코딩해 보겠습니다. 결과 벡터를 정규화하면 다음과 같습니다:
그리고 이에 대응하는 2-Qubit 양자 상태는 다음과 같습니다:
위 예시에서 벡터의 특성 수 은 2의 거듭제곱이 아닙니다. 이 2의 거듭제곱이 아닌 경우, 을 만족하는 Qubit 수 을 선택하고 진폭 벡터를 정보가 없는 상수(여기서는 0)로 패딩합니다.
기저 인코딩과 마찬가지로, 데이터셋을 인코딩할 상태를 계산한 후 Qiskit에서 initialize 함수를 사용해 해당 상태를 준비할 수 있습니다:
import math
desired_state = [
1 / math.sqrt(105) * 4,
1 / math.sqrt(105) * 8,
1 / math.sqrt(105) * 5,
1 / math.sqrt(105) * 0,
]
qc = QuantumCircuit(2)
qc.initialize(desired_state, [0, 1])
qc.decompose(reps=5).draw(output="mpl")
진폭 인코딩의 장점은 앞서 언급한 것처럼 인코딩에 개의 Qubit만 필요하다는 점입니다. 그러나 후속 알고리즘이 양자 상태의 진폭 위에서 동작해야 하며, 양자 상태를 준비하고 측정하는 방법이 효율적이지 않은 경향이 있습니다.
이해도 확인
아래 질문을 읽고 답을 생각해 본 후, 삼각형을 클릭하여 해답을 확인하세요.
다음 벡터(예제 데이터셋의 두 벡터를 합친 것)를 진폭 인코딩으로 표현할 때의 정규화된 상태를 써보세요:
정답:
6개의 숫자를 인코딩하려면 진폭으로 인코딩할 수 있는 상태가 최소 6개 이상 필요합니다. 이를 위해 3개의 Qubit이 필요합니다. 미지의 정규화 인수 를 사용하면 다음과 같이 쓸 수 있습니다:
주목하세요.
따라서 최종 결과는,
같은 데이터 벡터 에 대해 진폭 인코딩으로 데이터 특성을 로드하는 Circuit을 생성하는 코드를 작성하세요.
정답:
desired_state = [
9 / math.sqrt(270),
8 / math.sqrt(270),
6 / math.sqrt(270),
2 / math.sqrt(270),
9 / math.sqrt(270),
2 / math.sqrt(270),
0,
0,
]
print(desired_state)
qc = QuantumCircuit(3)
qc.initialize(desired_state, [0, 1, 2])
qc.decompose(reps=8).draw(output="mpl")
[0.5477225575051662, 0.48686449556014766, 0.36514837167011077, 0.12171612389003691, 0.5477225575051662, 0.12171612389003691, 0, 0]
매우 큰 데이터 벡터를 다루어야 할 수도 있습니다. 다음 벡터를 고려하세요:
정규화를 자동화하고 진폭 인코딩을 위한 양자 Circuit을 생성하는 코드를 작성하세요.
정답:
정답은 여러 가지가 있을 수 있습니다. 아래는 중간 단계를 출력하는 코드 예시입니다:
import numpy as np
from math import sqrt
init_list = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5]
qubits = round(np.log(len(init_list)) / np.log(2) + 0.4999999999)
need_length = 2**qubits
pad = need_length - len(init_list)
for i in range(0, pad):
init_list.append(0)
init_array = np.array(init_list) # Unnormalized data vector
length = sqrt(
sum(init_array[i] ** 2 for i in range(0, len(init_array)))
) # Vector length
norm_array = init_array / length # Normalized array
print("Normalized array:")
print(norm_array)
print()
qubit_numbers = []
for i in range(0, qubits):
qubit_numbers.append(i)
print(qubit_numbers)
qc = QuantumCircuit(qubits)
qc.initialize(norm_array, qubit_numbers)
qc.decompose(reps=7).draw(output="mpl")
Normalized array: [0.17342199 0.34684399 0.21677749 0.39019949 0.34684399 0.26013299 0.086711 0.39019949 0.086711 0.21677749 0.30348849 0. 0.1300665 0.30348849 0.21677749 0. ]
[0, 1, 2, 3]

기저 인코딩에 비해 진폭 인코딩이 가지는 장점이 있다고 생각하시나요? 있다면 설명해 보세요.
정답:
여러 가지 답이 있을 수 있습니다. 한 가지 답은, 기저 상태의 순서가 고정되어 있으므로 이 진폭 인코딩은 인코딩된 숫자들의 순서를 보존한다는 점입니다. 또한 대부분의 경우 더 조밀하게 인코딩됩니다.
진폭 인코딩의 장점은 차원(-특성) 데이터 벡터 를 인코딩하는 데 개의 Qubit만 필요하다는 것입니다. 그러나 진폭 인코딩은 일반적으로 비효율적인 절차로, CNOT Gate 수가 지수적으로 증가하는 임의 상태 준비가 필요합니다. 달리 말하면, 상태 준비의 런타임 복잡도는 차원 수 기준으로 (여기서 , 은 Qubit 수)의 다항 복잡도를 가집니다. 진폭 인코딩은 "공간에서의 지수적 절약을 시간에서의 지수적 증가라는 대가로 제공합니다"[3]. 단, 특정 경우에는 런타임을 으로 개선할 수 있습니다[4]. 종단 간 양자 속도 향상을 위해서는 데이터 로딩 런타임 복잡도를 함께 고려해야 합니다.
각도 인코딩
각도 인코딩은 양자 지지 벡터 머신(QSVM), 변분 양자 회로(VQC) 등 Pauli 피처 맵을 사용하는 많은 QML 모델에서 활용됩니다. 각도 인코딩은 아래에서 소개할 위상 인코딩 및 밀집 각도 인코딩과 밀접하게 관련되어 있습니다. 여기서는 "각도 인코딩"이라는 용어를 방향의 회전, 즉 Gate 또는 Gate 등을 사용하여 축에서 벗어나는 회전을 의미하는 것으로 사용하겠습니다[1,3]. 실제로는 임의의 회전 또는 회전의 조합으로 데이터를 인코딩할 수 있습니다. 그러나 는 문헌에서 널리 사용되므로 여기서는 이를 중심으로 설명합니다.
단일 Qubit에 적용할 경우, 각도 인코딩은 데이터 값에 비례하는 Y축 회전을 부여합니다. 데이터셋의 번째 데이터 벡터에서 단일 (번째) 피처 를 인코딩하는 과정을 살펴보겠습니다.
또는 Gate를 사용하여 각도 인코딩을 수행할 수도 있습니다. 다만 이 경우 인코딩된 상태는 에 비해 복소수 상대 위상을 갖게 됩니다.
각도 인코딩은 앞서 설명한 두 방법과 여러 면에서 다릅니다. 각도 인코딩의 특징은 다음과 같습니다.
- 각 피처 값이 대응하는 Qubit에 매핑되어 , Qubit들은 곱 상태(product state)로 남습니다.
- 데이터 포인트의 전체 피처 집합이 아닌 하나의 수치 값을 한 번에 인코딩합니다.
- 개의 데이터 피처를 인코딩하는 데 개의 Qubit이 필요하며, 입니다. 보통은 등호가 성립합니다. 이 가능한 경우는 다음 몇 섹션에서 살펴보겠습니다.
- 결과 Circuit은 일정한 깊이를 가집니다(보통 트랜스파일 전 깊이는 1입니다).
일정한 깊이의 양자 Circuit은 현재 양자 하드웨어에서 구현하기 특히 유리합니다. 로 데이터를 인코딩하는 방식(특히 Y축 각도 인코딩 선택)의 추가적인 특징은 특정 응용에서 유용할 수 있는 실수값 양자 상태를 생성한다는 점입니다. Y축 회전의 경우, 데이터는 실수값 각도 에 대한 Y축 회전 Gate 로 매핑됩니다(Qiskit RYGate). 위상 인코딩(아래 참조)과 마찬가지로, 정보 손실과 기타 원하지 않는 효과를 방지하기 위해 가 되도록 데이터를 재조정할 것을 권장합니다.
아래 Qiskit 코드는 단일 Qubit을 초기 상태 에서 데이터 값 를 인코딩하도록 회전시킵니다.
from qiskit.quantum_info import Statevector
from math import pi
qc = QuantumCircuit(1)
state1 = Statevector.from_instruction(qc)
qc.ry(pi / 2, 0) # Phase gate rotates by an angle pi/2
state2 = Statevector.from_instruction(qc)
states = state1, state2
상태 벡터의 동작을 시각화하는 함수를 정의해 보겠습니다. 함수 정의의 세부 사항은 중요하지 않지만, 상태 벡터와 그 변화를 시각화할 수 있는 능력이 중요합니다.
import numpy as np
from qiskit.visualization.bloch import Bloch
from qiskit.visualization.state_visualization import _bloch_multivector_data
def plot_Nstates(states, axis, plot_trace_points=True):
"""This function plots N states to 1 Bloch sphere"""
bloch_vecs = [_bloch_multivector_data(s)[0] for s in states]
if axis is None:
bloch_plot = Bloch()
else:
bloch_plot = Bloch(axes=axis)
bloch_plot.add_vectors(bloch_vecs)
if len(states) > 1:
def rgba_map(x, num):
g = (0.95 - 0.05) / (num - 1)
i = 0.95 - g * num
y = g * x + i
return (0.0, y, 0.0, 0.7)
num = len(states)
bloch_plot.vector_color = [rgba_map(x, num) for x in range(1, num + 1)]
bloch_plot.vector_width = 3
bloch_plot.vector_style = "simple"
if plot_trace_points:
def trace_points(bloch_vec1, bloch_vec2):
# bloch_vec = (x,y,z)
n_points = 15
thetas = np.arccos([bloch_vec1[2], bloch_vec2[2]])
phis = np.arctan2(
[bloch_vec1[1], bloch_vec2[1]], [bloch_vec1[0], bloch_vec2[0]]
)
if phis[1] < 0:
phis[1] = phis[1] + 2 * pi
angles0 = np.linspace(phis[0], phis[1], n_points)
angles1 = np.linspace(thetas[0], thetas[1], n_points)
xp = np.cos(angles0) * np.sin(angles1)
yp = np.sin(angles0) * np.sin(angles1)
zp = np.cos(angles1)
pnts = [xp, yp, zp]
bloch_plot.add_points(pnts)
bloch_plot.point_color = "k"
bloch_plot.point_size = [4] * len(bloch_plot.points)
bloch_plot.point_marker = ["o"]
for i in range(len(bloch_vecs) - 1):
trace_points(bloch_vecs[i], bloch_vecs[i + 1])
bloch_plot.sphere_alpha = 0.05
bloch_plot.frame_alpha = 0.15
bloch_plot.figsize = [4, 4]
bloch_plot.render()
plot_Nstates(states, axis=None, plot_trace_points=True)
위 예시는 단일 데이터 벡터의 단일 피처에 해당합니다. 개의 피처를 개의 Qubit의 회전 각도로 인코딩할 때, 예를 들어 번째 데이터 벡터 에 대해 인코딩된 곱 상태는 다음과 같습니다.
이는 다음과 동일합니다.
이해도 확인
아래 질문을 읽고 답을 생각해 본 후, 삼각형을 클릭하여 정답을 확인하세요.
위에서 설명한 각도 인코딩을 사용하여 데이터 벡터 를 인코딩하세요.
정답:
qc = QuantumCircuit(3)
qc.ry(0, 0)
qc.ry(2 * math.pi / 4, 1)
qc.ry(2 * math.pi / 2, 2)
qc.draw(output="mpl")
위에서 설명한 각도 인코딩을 사용하여 5개의 피처를 인코딩하려면 몇 개의 Qubit이 필요한가요?
정답: 5
위상 인코딩
위상 인코딩은 위에서 설명한 각도 인코딩과 매우 유사합니다. Qubit의 위상 각도는 + 축으로부터 축을 중심으로 한 실수값 각도 입니다. 데이터는 위상 회전 로 매핑되며, 여기서 입니다(자세한 내용은 Qiskit PhaseGate 참조). 정보 손실과 기타 원하지 않는 효과를 방지하기 위해 가 되도록 데이터를 재조정할 것을 권장합니다[1,2].
Qubit은 보통 위상 회전 연산자의 고유 상태인 상태로 초기화됩니다. 따라서 위상 인코딩을 구현하려면 먼저 Qubit 상태를 회전시켜야 합니다. 그러므로 Hadamard Gate로 상태를 초기화하는 것이 자연스럽습니다: