- 이전글
[tensorflow, numpy] Tensor 데이터 구조 - 1
내 기술 스택에 있어서 크게 변화가 있었던 건 3학년 1학기의 중간고사 전쯤이었을 것이다. 아... 그렇게 보니 정말 오래되긴 했구나. 그때 학부연구생으로 지원을 했다. 거기서 머신러닝과 관련
passingprogram.tistory.com
이전 글에 바로 이어서 적어보겠다. 앞에서도 간단한 행렬의 더하기 연산에 대해서 살펴보았지만 당연히 그것보다는 훨씬 더 많은 연산이 존재하며, 그중에서 자주 쓰이는 연산이 무엇인가 간단하게 보도록 하겠다.
1. 행렬곱
흔히들 말하는 dot product라는 물건이다. 행렬곱이 무엇인지에 대해서는 여기서 설명하진 않을 것이다. 다만 주의할 점은 그냥 *을 써서 진행하는 원소별 곱과는 다른 것이라는 것을 생각하면 된다. 연산자는 @를 이용한다.
A = np.array([
[1, 2],
[3, 4],
]) # shape (2, 2)
B = np.array([
[5, 6],
[7, 8],
]) # shape (2, 2)
C = A @ B
print(C)
print(C.shape) # (2, 2)
1D x 1D를 해서 내적을 하는 예시도 있다.
a = np.array([1, 2, 3]) # shape (3,)
b = np.array([4, 5, 6]) # shape (3,)
c = a @ b # 또는 np.matmul(a, b)
print(c) # 32
print(c.ndim) # 0 그니까 스칼라가 나옴
3D 이상의 tensor에 대한 dot product도 지원하는데, 뒤의 2축을 행렬로보고 나머지는 broadcast하는 방식이다. 그래서 나머지 차원의 경우 아예 축이 같거나(A.shape=(10, 2, 3), B=(10,3,4)와 같은 경우), 아님 1이라서 broadcast 가능하거나(아래의 경우) 해야 한다.
A = np.random.randn(10, 2, 3) # batch=10
B = np.random.randn(1, 3, 4) # batch=1
Z = A @ B
2. reshape
이건 tensor를 원소의 갯수가 같지만, 다른 모양의 tensor로 바꾸는 것이다. 곱해서 원소 수가 나오는 그 어떠한 dimension이든 설정이 가능하다. 극단적으로는 :
import numpy as np
x = np.arange(12)
x.reshape(3, 4) # 가능
x.reshape(2, 6) # 가능
x.reshape(12, ) # 가능
x.reshape(1, 12) # 가능
x.reshape(3, 2, 2) # 가능 (3*2*2 = 12)

이렇게 까지 바꿀 건 없지만 전부 가능하다는 것이다. 다만 대부분은 나중에 배울 keras의 "Flatten"을 하게 될 것이긴 하다. 보통은 Dense층에 넣기 위해서 납작~하게 1D로 바꾸거나, Convolution에 활용하기 위해서 4D텐서로 바꿔놓거나 하는 경우가 대부분이다.
3. 코사인 유사도
이전까지는 rank-2 텐서, 행렬과 관련한 연산을 보았다. 여기서는 rank-1인 텐서, 벡터로 취급을 해보자. 벡터의 장점은 기하학적인 해석이 용이하다는 것이다. 여러분도 물리학에 관심이 있었다면, 3차원 벡터를 활용해서 우리가 사는 공간상의 물체의 움직임을 계산할 수 있다는 것을 알 것이다.
머신러닝 세계에서는 벡터의 특성에 따른 유사도에 더 주목한다. 실제로 자주 사용되는 개념 중 하나가 cosine 유사도이다. 두 벡터를 내적하고 그것을 두 벡터의 크기의 곱으로 나눈다. 그럼 그 두 벡터가 이루는 공간상의 "각도"를 비슷하게 구할 수 있다. 머신러닝을 할 때는 굳이 cos의 역함수를 써서 실제 각도까지 구하진 않고(컴퓨팅 파워 낭비다), 그냥 cos(theta) 값을 구하는 것으로 만족한다. 이 값의 범위는 [-1, 1]로, -1에 가까우면 강한 역상관관계, 1에 가까우면 강한 상관관계가 있으리라고, 두 벡터가 "유사"하다고 판단하는 것이다.

코드를 보면 이렇다
# 1D 벡터 두 개
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 내적
dot = a @ b
# 노름(norm)
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
# 코사인 유사도
cos_sim = dot / (norm_a * norm_b)
print("dot =", dot)
print("norm_a =", norm_a)
print("norm_b =", norm_b)
print("cosine similarity =", cos_sim)

실제로 공간상에 벡터(1,2,3), (4,5,6)은 이래저래 비슷한 각도를 가지므로 비슷하다고 할 수 있겠다. 혹여나도 "하지만 두 개 scale차이가 큰데?"라고 할 수도 있다. 다행히, 머신러닝에서는 대부분의 input을 [-1, 1] 안으로 욱여넣으려고 하기 때문에(100% 그렇다는 건 아니다), 대충 향하는 방향만을 보려는 우리의 시도는 대부분 맞아 들어간다. 게다가 이렇게 해도 결과는 같다.
# 1D 벡터 두 개
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 노름(norm) - 크기구하기
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
# 내적
dot = (a / norm_a) @ (b / norm_b)
# 코사인 유사도
cos_sim = dot #/ (norm_a * norm_b)
print("dot =", dot)
print("norm_a =", norm_a)
print("norm_b =", norm_b)
print("cosine similarity =", cos_sim)
4. translation, rotation, scaling, skewing
이건 affine을 설명하기 위해서 설명하는 것이다. 우선 [x, y]를 2차원 상의 벡터라고 생각해보자.

4-1. translation(이동)
제일 간단한 연산이다. 아래처럼 더하기를 수행하면 된다. 그럼 $x_2$ 만큼 $x$축으로, $y_2$만큼 $y$축으로 이동한 벡터가 만들어진다.
$\begin{bmatrix}
x_2 \\
y_2
\end{bmatrix} + \begin{bmatrix}
x\\
y
\end{bmatrix}$

4-2. rotation(회전)
이건 다소간 복잡할 수도 있다. 회전하고 싶은 각도인 $\theta$만큼 아래의 dot product를 계산한다.
$\begin{bmatrix}
cos(\theta) & -sin(\theta) \\
sin(\theta) & cos(\theta)
\end{bmatrix} \cdot \begin{bmatrix}
x\\
y
\end{bmatrix}$
import math
from math import sin, cos
v = np.array([1, 2])
theta = 90 / 360 * 2 * math.pi
rot = np.array([
[cos(theta), -sin(theta)],
[sin(theta), cos(theta)]
])
rv = rot @ v
o = np.array([0, 0])
fig, ax = plt.subplots(figsize=(5, 5))
ax.quiver(*o, *v, angles='xy', scale_units='xy', scale=1, color='blue')
ax.quiver(*o, *rv, angles='xy', scale_units='xy', scale=1, color='red')
sq = 3.0
ax.set_aspect('auto')
ax.set_xlim([-sq, sq])
ax.set_xbound(lower=-sq, upper=sq)
ax.set_ylim([-sq, sq])
ax.set_ybound(lower=-sq, upper=sq)
ax.grid(True)
plt.show()

4-3. scaling(크기 조절)
$\begin{bmatrix}
sc_x & 0\\
0 & sc_y
\end{bmatrix} \cdot \begin{bmatrix}
x\\
y
\end{bmatrix}$
import math
from math import sin, cos
v = np.array([1, 2])
sca = np.array([
[-1, 0],
[0, 0.5]
])
rv = sca @ v
o = np.array([0, 0])
fig, ax = plt.subplots(figsize=(5, 5))
ax.quiver(*o, *v, angles='xy', scale_units='xy', scale=1, color='blue')
ax.quiver(*o, *rv, angles='xy', scale_units='xy', scale=1, color='red')
sq = 3.0
ax.set_aspect('auto')
ax.set_xlim([-sq, sq])
ax.set_xbound(lower=-sq, upper=sq)
ax.set_ylim([-sq, sq])
ax.set_ybound(lower=-sq, upper=sq)
ax.grid(True)
plt.show()

똑같이 dot product를 이용하는데 내용물이 전에 비해서 훨씬 간단하다. 원하는 축 - scale이 대각으로 늘어서있는 행렬을 곱한다.
4-4. skewing 은 생략
5. affine 연산
이게 제일 일반적인 경우의 변환이라고도 할 수 있을 것이다. 원칙적으로는 affine 연산은 선형변환 + 평행이동으로 이루어진 모든 연산을 의미한다. 앞에서 살펴본 모든 연산의 조합이 가능하다고 할 수 있다. $y = W \cdot x + b$ 에서 임의의 W값이 있어도 전부 이 안에 포함된다고 할 수 있다. 만약 Dense층(keras의, 머신러닝의 제일 기본적인 전연결 신경망 층)에 activation function을 적용하지 않으면 기본적으로 affine연산이 된다.

활성화 함수라는 개념이 사실 등장하는 이유도 어파인 연산의 특성 때문이다. 당연히 어파인 연산도 선형 연산이기 때문에 여러 번 반복하여 실행한다 한들, 심지어는 구체적인 $W$, $b$값이 다르다 한들 어떤 특정한 어파인 연산을 한번 한 것과 다르지 않기 때문이다.
$y = W_2 \cdot (W_1 \cdot x + b_1) + b_2 = W_s \cdot x + b_s$
와 같다는 의미이다.
6. 활성화 함수의 등장
앞과 같은 이유로, 신경망 층에 적당한 비선형성을 주기 위해서 활성화 함수라는 걸 사용한다. 참고로, 뇌에서 뉴런이 발화활 확률과 닮았다고 믿은 sigmoid 함수를 처음에는 썼지만 나중에는 비용과 효율을 따져서 relu라는 함수를 더 많이 쓰게 되었다. relu는 그냥 max(0, x)이기 때문에 계산 비용도 적다.

'ML > Tensorflow' 카테고리의 다른 글
| [tensorflow, numpy] Tensor 데이터 구조 - 1 (2) | 2025.12.06 |
|---|