이 포스트의 내용은 Qiskit Textbook | Quantum States and Qubits - The Atoms of Computation을 통해 공부한 흔적임을 밝힙니다.
양자 컴퓨팅을 하려면 무엇을 만들어야 하는가,
양자 프로그램이란 무엇인가,
아니 애초에 양자 컴퓨터란 무엇인가아ㅏㅏㅏㅏㅏ
그런데 생각해보면 고전적인 컴퓨터도 그 내부 구조를 다 이해하지 않고 개발하는 개발자도 많다.
전공자들이나 컴퓨터회로, 컴퓨터구조, 시스템 소프트웨어 같은 걸 배우지... 아무튼.
여기서부턴 슬슬 Jupyter Notebook이 필요하다.
IBM Quantum의 Quantum lab을 사용하거나 textbook에서 직접 실행할 수 있다.
Qiskit 코드를 실행하기 위해서는 다음과 같은 패키지를 import해야 한다.
from qiskit import QuantumCircuit, execute, Aer from qiskit.visualization import plot_histogram
컴퓨팅을 이해하기 위해 비트(bit)에 대해 알 필요가 있다.
비트는 아주 간단한 알파벳을 가진다―오직 0과 1만을 알파벳으로 가진다.
그리고 이것만으로 모든 정보를 표현한다.
사전에 정의된 규칙에 따라 숫자, 문자, 그림, 소리, 그리고 더 복잡한 정보도 표현할 수 있다.
고전적인 컴퓨터 즉, 우리가 사용하는 일반적인 컴퓨터에서 그렇듯이
양자 컴퓨터에서도 이 개념은 크게 다르지 않다.
단지 비트가 아니라 큐비트일 뿐이다.
큐비트에 대한 건 여기선 깊이 다루지 않겠다.
비트든 큐비트든 입력값을 조작하여 출력값을 만들어야 한다.
일련의 비트 조작 과정은 회로도Circuit Diagram를 통해 나타낼 수 있다.
회로도는 어떤 입력값이 일련의 연산 과정을 거쳐 출력값을 내보내는 것을 보여주는데,
여기에 사용되는 연산을 게이트Gate라고 한다.
다음은 회로도의 한 예시다.
는 와 가 부호가 같을 때 , 다를 때 을 출력하며
는 와 가 모두 일 때 , 그렇지 않을 때 을 출력한다는 건 여담.
양자 컴퓨터의 경우에도 게이트가 포함된 회로도를 사용하지만
고전적인 컴퓨터의 회로도와는 조금 다른 규칙을 가진다.
다음은 앞서 살펴본 회로도와 동일하게 작동하는 양자 회로도다.
는 와 가 부호가 같을 때 , 다를 때 을 출력하며
는 와 가 모두 일 때 , 그렇지 않을 때 을 출력한다는 건 여담.
회로의 작업은 크게 세 단계로 구분할 수 있다.
마지막 작업인 결과값 추출에 집중하여 첫번째 양자 회로를 작성해보자.
입력값으로 사용될 8개의 큐비트와 결과값을 추출하여 저장할 8개의 비트를 생성하는 걸로 시작하겠다.
n = 8 n_q = n n_b = n qc_output = QuantumCircuit(n_q, n_b)
qiskit
패키지의 QuantumCircuit
는
첫번째 인자로 입력 큐비트의 개수를, 두번째 인자로 출력 비트의 개수를 받는다.
인자를 하나만 전달할 경우 그것은 입력 큐비트의 개수로 사용되며 출력 비트는 설정되지 않는다.
양자 회로의 결과값은 측정measure 연산을 통해 추출된다.
측정 연산은 특정 큐비트의 값을 측정하여 특정 비트에 그 결과값을 기록한다.
측정 연산은 QuantumCircuit
객체에서 measure
메서드를 통해 수행한다.
즉, 다음과 같은 연산으로 n=8
개의 큐비트를 측정하여 n=8
개의 비트에 기록할 수 있다.
for j in range(n): qc_output.measure(j, j)
여기서 첫번째 인자는 몇번째 큐비트를 측정할 것인지를,
두번째 인자는 몇번째 큐비트에 기록할 것인지를 나타낸다.
구성한 회로를 확인하기 위해서는 QuantumCircuit
객체의 draw
메서드를 사용한다.
qc_output.draw()
큐비트는 따로 조작하지 않으면 0으로 초기화되므로
위 회로를 지금 상태 그대로 측정하면 모두 0이 측정될 것이다.
이를 확인하기 위해 회로를 여러 번 실행하여 그 결과를 히스토그램으로 확인할 수 있다.
counts = execute(qc_output, Aer.get_backend('qasm_simulator')).result().get_counts() plot_histogram(counts)
고전적인 컴퓨터는 단 한 번의 측정으로 결과값을 얻을 수 있지만
양자 컴퓨터는 무작위성을 가지고 있기에 여러 번의 실행 후 그 통계를 활용한다.
따로 설정하지 않으면 이것은 1024번의 측정을 수행한다.
주의할 점은, 이것이 양자 컴퓨터에서 실행된 것이 아니라
양자 컴퓨터가 하는 일을 고전적인 컴퓨터에서 할 수 있도록 한 양자 시뮬레이터에서 실행되었다는 것이다.
이 시뮬레이션은 최대 30개의 적은 큐비트만 사용할 수 있다.
Aer.get_backend
의 인자값을 원하는 기기로 변경함으로써 실제 기기를 실행할 수 있다.
이제 그저 값을 측정하는 게 아니라 회로로서 기능하는 회로를 만들어보자.
먼저, 비트 스트링을 입력값으로 인코딩하는 방법을 알아보자.
이를 위해 NOT 게이트가 무엇인지 알아야 한다.
이산수학을 공부했다면 알겠지만 NOT 게이트는 0을 1로, 1을 0으로 뒤집는다.
양자회로는 NOT 게이트 대신 파울리 X 게이트를 사용하며
코드 상에서는 QuantumCircuit
의 메서드 x
를 사용한다.
이 메서드는 몇 번째 큐비트에 대한 연산인지를 인자로 받는다.
qc_encode = QuantumCircuit(n) qc_encode.x(7) qc_encode.draw()
이 회로의 결과값은 이전 회로를 통해서 추출할 수 있다.
회로에 직접 측정 코드를 덧붙여도 되지만 기존에 만들어진 측정 회로를 덧붙일 수도 있는 것이다.
qc = qc_encode + qc_output qc.draw()
그리고 앞서 첫번째 회로에서 했던 것과 같이 결과값을 추출할 수 있다.
counts = execute(qc, Aer.get_backend('qasm_simulator')).result().get_counts() plot_histogram(counts)
맨 앞의 비트만 로 변경되었음을 알 수 있는데
비트 스트링의 인덱스는 bits[7:0]
과 같이 내림차순으로 작성하는 것이 관례이기 때문에
7번째 큐비트에 파울리 X 게이트를 설정한 것이 이와 같이 나타난 것이다.
이와 같이 작성하면 번째 비트가 를 나타낸다는 것을 직관적으로 알 수 있다.
이 예제는 8쌍의 큐비트&비트를 가지고 있으므로
까지의 음이 아닌 정수를 나타낼 수 있다.
예를 들어 백금의 원자번호인 78를 나타내고자 한다면
78은 이진수로 이므로 다음과 같이 작성할 수 있다.
qc_encode = QuantumCircuit(n) qc_encode.x(1) qc_encode.x(2) qc_encode.x(3) qc_encode.x(6) qc_encode.draw()
덧셈 회로를 통해 입력값의 덧셈을 수행하여 출력하기 위해서는 덧셈이라는 문제를 해결해야 한다.
그리고 문제를 해결하기 위해서는 그 문제를 작고 단순한 단계로 나누어 생각하는 게 좋다.
컴퓨터에서 어떤 수의 덧셈은 이진수 상태에서 이루어진다.
비트 스트링의 덧셈은 그 길이가 같은 녀석들끼리만 가능하므로
길이가 다르다면 계산하기 전에 짧은 녀석은 좌측에 여분의 을 덧붙여준다.
덧셈 연산은 최하위 비트부터 계산된다.
0과 1을 더하면 1이 된다는 것에는 설명이 필요하지 않으리라.
이진수 과 을 더하면 이 된다.
따라서 다음 연산에는 받아올림이 존재한다.
그 다음에는 연산을 하게 되는데 3개의 숫자의 덧셈을 피하기 위해
2개의 숫자의 덧셈을 두 번 수행하는 방법이 있다.
먼저 받아올림을 고려하지 않고 덧셈을 한 뒤,
받아올림에 해당하는 값을 더해주는 것이다.
그러면 이번 자리에 이 오고, 또 1이 받아올림 된다.
이와 같은 방식으로 최상위 비트까지 덧셈을 이어나갈 수 있다.
결국 비트 스트링의 덧셈은 두 개의 비트의 덧셈의 연속으로 이해할 수 있으며
두 개의 비트의 덧셈은 다음과 같은 네 가지 경우의 수를 가진다.
받아올림을 고려한 덧셈을 하기 위해서는 이와 같은 덧셈 연산이 두 번씩 이루어져야 하며
그렇기에 이것을 반가산기a half adder라고 한다.
우리가 처음에 봤던, 고전적인 회로와 양자 회로의 비교에서 사용된 회로가 바로 반가산기다.
위에서 언급했던 회로를 보지 않고! Qiskit을 이용한 반가산기 회로를 직접 작성해보자.
반가산기 회로는 덧셈 연산을 할 두 개의 큐비트 입력값을 인코딩해야 하고
덧셈 알고리즘을 수행한 뒤, 그것을 측정하여 두 개의 비트 출력값으로 기록해야 한다.
여기서 덧셈 알고리즘의 구현을 제외하고는 배운 내용으로 쉽게 구성할 수 있다.
덧셈의 대상이 되는 녀석들이 q[0]
와 q[1]
에 인코딩된다.
위 이미지에서는 둘 모두 을 전달했다.
즉, 연산을 하겠다는 것이다.
연산 결과는 q[2]
와 q[3]
에 존재하며 이를 측정함으로써 결과값을 얻을 수 있다.
세로로 그어진 대시는 회로의 다른 부분을 구분하기 위한 것이다.
"여기서부터 여기까지가 하나의 맥락이다"라는 식의 부가적인 표시라고 볼 수 있다.
다른 용도로도 사용될 수 있지만 일단 영역 구분용으로 이해하고 넘어가자.
코드 상에서는 barrier
명령어를 통해 생성할 수 있다.
반가산기의 연산은 다음과 같이 이루어진다고 했다.
연산 결과는 2-bit로 나타나는데 하나씩 차례대로 회로를 구성해보자.
먼저 오른쪽 비트를 확인해보면,
과 는 로, , 는 로 출력되는 것을 볼 수 있는데
이것은 XOR 게이트의 연산과 일치한다.
Input 1 | Input 2 | XOR Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
양자 컴퓨터에서는 XOR 게이트 대신 제어된 NOTcontrolled-NOT 게이트,
줄여서 CNOT 게이트를 사용한다.
CNOT 게이트는 컨트롤 큐비트와 타겟 큐비트를 가지며,
타겟 큐비트의 값이 컨트롤 큐비트의 값과 같다면 타겟 큐비트의 값이 이 되고
다르다면 타겟 큐비트의 값이 이 되는 방식이다.
컨트롤 큐비트가 일 때만 NOT 연산이 수행된다고 이해할 수도 있다.
CNOT 게이트는 코드 상에서는 QuantumCircuit
객체에서 cx
메서드를 통해 사용할 수 있다.
cx
는 첫번째 인자로 컨트롤 큐비트의 인덱스를, 두번째 인자로 타겟 큐비트의 인덱스를 가진다.
qc_cnot = QuantumCircuit(2) qc_cnot.cx(0, 1) qc_cnot.draw()
CNOT 게이트를 적용하면 게이트의 입력값 중 하나는 XOR 결과로 덮어씌워지는데
아직 출력 큐비트 두 개 중 하나에 대한 연산 밖에 하지 않았으므로
회로의 입력값을 덮어씌우면 문제가 될 수 있다.
따라서 결과값을 저장하기 위한 큐비트를 추가적으로 따로 준비한다.
둘 사이의 덧셈임에도 큐비트를 네 개나 사용하는 것이 바로 이 때문이다.
큐비트 q[3:2]
를 측정하여 비트 c[1:0]
에 기록할 것이므로
q[2]
에 CNOT 게이트의 결과값이 존재하도록 한다.
q[2]
의 초기값을 으로 둔 채
q[0]
과 q[1]
을 컨트롤 큐비트로서 각각 q[2]
에 CNOT 게이트를 설정하면
q[0]
과 q[1]
의 XOR 결과값을 q[2]
에 기록할 수 있다.
이를 코드로 나타내면 다음과 같다.
qc_ha = QuantumCircuit(4, 2) qc_ha.x(0) qc_ha.x(1) qc_ha.barrier() # q[2] := q[0] XOR q[1] qc_ha.cx(0, 2) qc_ha.cx(1, 2) qc_ha.barrier() qc_ha.measure(2, 0) qc_ha.measure(3, 1) qc_ha.draw()
이제 q[3]
에 대한 연산도 생각해보자.
반가산기의 연산을 다시 살펴보면
왼쪽 비트는 입력값이 모두 일 때만 이고 나머진 이라는 것을 확인할 수 있다.
즉, 이것은 AND 연산과 같다.
두 개의 큐비트가 모두 일 때만 다른 큐비트를 로 만드는 연산이 필요하다.
이는 컨트롤 큐비트가 일 때 타겟 큐비트를 로 만드는 CNOT 게이트와 유사한데,
이를 위해 두 개의 컨트롤 큐비트가 모두 일 때만 타겟 큐비트를 로 만드는 게이트가 존재한다.
그것은 토폴리toffoli 게이트, 혹은 CCNOT 게이트라고 부르며
코드 상에서는 QuantumCircuit
의 메서드 ccx
를 사용한다.
반가산기 코드에 q[0]
, q[1]
를 컨트롤, q[3]
를 타겟으로 하는 CCNOT 게이트를 추가하면
qc_ha = QuantumCircuit(4, 2) qc_ha.x(0) qc_ha.x(1) qc_ha.barrier() # q[2] := q[0] XOR q[1] qc_ha.cx(0, 2) qc_ha.cx(1, 2) # q[3] := q[0] AND q[1] qc_ha.ccx(0, 1, 3) qc_ha.barrier() qc_ha.measure(2, 0) qc_ha.measure(3, 1) qc_ha.draw()
이제 이 양자회로의 값을 측정해보면 가 수행되는 것을 확인할 수 있다.
counts = execute(qc_ha, Aer.get_backend('qasm_simulator')).result().get_counts() plot_histogram(counts)
물론 q[0]
, q[1]
에 대한 파울리 X 게이트 조작으로
다른 비트의 덧셈도 계산할 수 있다.
아무튼 우리는 덧셈을 원자성을 띈 연산으로 나누어 반가산기 회로를 완성했다.