머신러닝과 딥러닝에서의 선형대수학
ML과 DL에서 선형대수학은 데이터를 표현하고 계산하는 언어이다.
복잡하고 방대한 데이터를 컴퓨터가 이해하고 아주 빠르게 처리할 수 있도록 만들어주는 가장 핵심적인 수학적 기반.
1. ML/DL에서 선형대수가 필수적인 이유
데이터의 구조화: 현실의 데이터(이미지, 텍스트, 음성 등)를 컴퓨터가 다룰 수 있는 숫자들의 배열(벡터, 행렬, 텐서)로 변환해 준다.
계산의 효율성: 수만 개의 수식을 하나하나 계산하는 대신, 행렬 연산을 통해 한 번에 병렬로 처리 할 수 있다. GPU가 딥러닝에 강력한 이유도 바로 이 행렬 연산에 최적화 되어 있기 때문.
2. ML/DL에 적용되는 핵심 선형대수 개념
1) 벡터(Vector)와 행렬(Matrix), 텐서(Tensor) 데이터를 담는 그릇의 차원을 의미
벡터(1차원):
하나의 데이터 포인트.
(예: 집의 평수, 방 개수, 가격을 담은 1차원 배열)
행렬(2차원):
여러 개의 데이터 포인트(행)와 각 데이터의 특징(열)을 담은 형태의 데이터 세트.
텐서(3차원 이상):
컬러 이미지(가로, 세로, RGB 채널)나 비디오 데이터 등을 표현할 때 사용.
딥러닝 프레임워크인 'TensorFlow(텐서플로우)'의 이름도 여기서 유래.
2) 행렬 곱 (Matrix Multiplication) : 딥러닝의 인공신경망(Neural Network) 연산의 90% 이상은 행렬 곱셈.
3) 노름 (Norm)
벡터의 크기나 길이를 측정하는 방법.
ML/DL에서는 모델이 학습데이터에만 너무 과도하게 맞춰지는 '과적합(Overfitting)을 방지하는 규제로 주로 쓰인다.
4) 고윳값과 고유벡터
행렬을 곱해도 방향이 변하지 않고 크기만 변하는 벡터를 고유벡터, 그때 변화하는 크기를 고윳값이라고 한다.
차원 축소(PCA): 데이터의 특성이 너무 많을 때, 정보 손실을 최소화 하면서 차원을 줄이는 주성분(PCA) 알고리즘의 핵심 원리.
데이터의 분산이 가장 큰 방향(가장 중요한 특징)을 찾는데 사용.
선형대수의 기초적인 수학적 계산 원리(행렬의 곱셈 방법 등)
선형대수의 가장 기본이 되면서도 머신러닝/딥러닝 연산의 뼈대를 이루는 벡터의 내적과 행렬의 곱셈, 그리고 몇 가지 필수 연산 규칙
# 벡터의 내적
# 함수를 만들어 벡터의 내적 구하기
def dot(v,w):
return sum(v_i*w_i for v_i, w_i in zip(v,w))
A=[4,3]
B=[6,0]
dot(A,B)
직접 내적 함수를 만드는 방식. v,w의 요소끼리 곱한 값들을 모두 더해 반환한다. (출력결과 : 24)
# 넘파이로 벡터의 내적 구하기
a=np.array([1,2,3,4,5])
b=np.array([2,4,6,8,10])
print(a*b)
print(sum(a*b))
print(np.sum(a*b))
넘파이를 딸깍하면 요소의 개수만 맞춰주면 a*b만으로도 각 요소의 곱 array를 만들 수 있다.
sum함수만 취해주면내적 완성!
#넘파이 브로드캐스팅으로 벡터의 내적 구하기
np_Hong1 = np.array([80,90,10,9])
np_Jung1 = np.array([70,80,9,8])
v=np_Hong1+np_Jung1
w=[0.5,0.5,0.5,0.5]
[i*j for i,j in zip(v,w)]
비교를 위해 위 코드는 넘파이 어레이로 구한 홍원과 정원의평균이다.
#넘파이 브로드캐스팅 실습
######벡터와 스칼라는 관례적으로 1-벡터를 사용하여 스칼라를 벡터로 변환하는 연산을 허용하는데, 이를 브로드캐스팅이라고 부른다!!
avg=[75.0,85.0,9.5,8.5]
avg1=np.array(avg)
W1=np.array([0.4,0.4,1,1])
print(avg1*W1)
#내적
Final=avg1*W1
print(Final.sum())
위 코드가 바로 브로드캐스팅을 이용한 내적이다.
가중치 array W1과, 정원이와 홍원이의 평균점수 array avg1을 요소끼리 곱한 Final arry의 합계를 구하면 내적을 구할 수 있다.
출력 결과: 82.0
#벡터의 길이
#넘파이 함수로 벡터의 길이 구하기
a1 = np.array([1,2])
np.linalg.norm(a1) # 넘파이의 벡터의 길이를 구하는 함수
2차원 평면에서 벡터 (1,2)의 길이는 루트(1^2+2^2) 인데, 넘파이의 linalg.norm 함수를 사용하면 벡터의 길이를 그냥 딸~깍 해서 구할 수 있다.
출력 결과: np.float64(2.23606797749979)
위 코드를 넘파이 없이 파이썬 함수만으로 구현하면 다음과 같다.
def dot(v,w):
return sum(v_i*w_i for v_i, w_i in zip(v,w))
def sum_of_squares(v):
return dot(v,v)
v=[1,2]
import math # 수학의 함수들을 사용할떄
def magnitude(v):
return math.sqrt(sum_of_squares(v))
v=[1,2]
mag=magnitude(v)
print(mag)
어우~복잡해. 넘파이를 쓰도록 하자.
출력 결과: 2.23606797749979
#벡터 간의 거리(유클리드 거리) 구하기
#유클리드 거리는 두 벡터가 가리키는 점 사이의 거리.
#유클리드 거리는 벡터의 놈의 정의와 차의 정의를 사용하여 구함
#math 라이브러리의 sqrt함수를 사용하여 계산 할 수도 있음
def vector_subtract(v,w):
return [v_i-w_i for v_i, w_i in zip(v,w)]
def squared_distance(v,w):
return sum_of_squares(vector_subtract(v,w))
v=[1,2]
w=[3,4]
sd=squared_distance(v,w)
def distance(v,w):
return math.sqrt(squared_distance(v,w))
def distance1(v,w):
return magnitude(vector_subtract(v,w))
v=[1,2]
w=[3,4]
print(distance(v,w))
print(distance1(v,w))
유클리드 거리라 함은 공간에서의 두 벡터의 시작점을 원점으로 통일 시키고 벡터 끝이 가리키는점 사이의 거리라고 볼 수 있다.
위 코드의 distance함수는 넘파이 없이 파이썬만으로 유클리드 거리를 구한 것이다.
vector_subtract 함수는 리스트의 각 요소의 차를 저장한 리스트를 반환, squared_distance함수는 v,w의 요소의 차를 제곱한 값을 담은 리스트를 반환한다.distance 함수는 math라이브러리의 sqrt(제곱근 함수)와 squared_distance함수를 사용하여 유클리드 거리를구하는 함수이다.
distance1함수는 magnitude함수와 vector_subtract함수를 사용해서 유클리드 거리를 구한다.
출력 결과:
2.8284271247461903
2.8284271247461903
# 벡터를 이용한 평균 제곱 오차와 평균 제곱근 오차
### 평균 제곱 오차
#예측값이 실제값에 얼마나 근접한지 나타냄
y=[1,1,0,1,0,1,0,0]#실제값
t=[0,0,1,0,0,1,0,0]#예측값
import numpy as np
def mean_squared_error(y,t):
return 0.5*np.sum((y-t)**2)
print(mean_squared_error(np.array(y),np.array(t)))
출력 결과: 2.0
평균인데 왜 0.5를 곱할까? (y-t)**2의 어레이의 크기는 8인데?? 라는 궁금증에 대한 클로드의 답변:
이건 사실 딥러닝(신경망) 학습에서 자주 쓰이는 손실 함수(loss function) 공식이에요. 유명한 딥러닝 교재(사이토 고키의 "밑바닥부터 시작하는 딥러닝" 등)에서 이 형태를 "평균제곱오차"라는 이름으로 소개하는 경우가 있는데, 엄밀히 통계학적 정의의 "평균"과는 다른 형태예요.
#평균 제곱근 오차
c=np.array([1,2,3,4,5])
d=np.array([2,4,6,8,10])
print((sum((c-d)**2)/len(c))**(1/2))
여기는 또 어레이 크기로 나누네..
행렬
import numpy as np
A=[[1,2,3],
[4,5,6]]
B= [[1,2],
[3,4],
[5,6]]
A1=np.array(A)
A1.shape
np.shape(A)
A와 B는 각각 2행3열, 3행 2열의 리스트. A1은A를 행렬로 만든것.
출력 결과:
A1.shape
>>(2, 3)
np.shape(A)
>>(2, 3)
#행렬의 덧셈, 뺄셈, 곱셈
x=np.array([10,11,12,13,14])
y=np.array([0,1,2,3,4])
x+y
x-y
#행렬의 곱셈
x= np.array([[1],[2],[3]])
y= np.array([[4],[5],[6]])
x*y
합의 경우 그냥 리스트는 문자열끼리 이어주는데 행렬은 요소끼리의 연산.
#전치 행렬과 브로드캐스팅
x.shape, y.shape # (3, 1), (3,1)
x.T
x.T.shape
x.T*y
x,y는 3행 1열 인데, x에 전치를 함으로써 x는 1행 3열, y는 3행 1열.
둘을 곱해주면 형태가 안맞기 때문에 브로드캐스팅이 일어난다.
1 2 3
1 2 3
1 2 3
과
4 4 4
5 5 5
6 6 6
의 벡터곱을 한다는 의미.
결과
array([[ 4, 8, 12],
[ 5, 10, 15],
[ 6, 12, 18]])
#행렬곱
x=np.array([[1,2,3]])
y=np.array([[4,5,6]])
print(x.T@y)
print(x@y.T)
행렬곱을 하려면 @연산기호를 사용해야 한다.
각 어레이에 전치를 해서 행렬곱을 해주면 결과는 3x3, 1x1이 나오게 된다.
결과
[[ 4 5 6]
[ 8 10 12]
[12 15 18]]
[[32]]
손실함수와 활성화함수
'''
손실 함수 (Loss Function)
MSE : 모델읠 출력층에서 선형회귀, 주택 가격 등 수치 예측
MAE : 모델의 출력층에서 이상치가 많은 환경에서의 수치 예측
BCE : 분류류기의 최종 출력층, 주요모델은 불량 탐지, GAN의 판별자
CCE : 다중 분류기의 최종출력층, 이미지분류, 텍스트 카테고리 분류
회귀 : MSE
이진 : BCE
다중 : CCE
활성화 함수(Activation Function)
이전 층으로 부터 전달받은
Sigmoid : 이진분류 모델의 최종 출력층
Tanh : RNN의 은닉층에서
ReLU : 대부분의 은닉층에서
Softmax : 다중 분류의 최종 출력층에서
은닉층 : ReLU
출력층 : Softmax,Sigmoid
'''
# 손실 함수의 예
import tensorflow as tf
# 1. MSE (Mean Squared Error) 실례 - 수치 예측
mse_loss_fn = tf.keras.losses.MeanSquaredError()
# 예: 모델의 집값 예측이 10.0, 실제 정답(집값)이 12.0인 경우
predictions = tf.constant([[10.0]]) # 예측값
targets = tf.constant([[12.0]]) # 정답
# TensorFlow에서는 관례상 (정답, 예측값) 순서로 입력합니다.
loss_mse = mse_loss_fn(targets, predictions)
print(f"MSE Loss: {loss_mse.numpy()}") # 결과: 4.0 ((12-10)^2)
# 2. BCE (Binary Cross Entropy) 실례 - 이진 분류 (합격/불합격)
bce_loss_fn = tf.keras.losses.BinaryCrossentropy() #- 위치: 다중
#분류기의 최종
#출력층 - 주요 모델: 이미지
#분류, 텍스트
#카테고리 분류
# 예: 모델이 '합격(1)'일 확률을 0.8로 예측했고, 실제 정답도 '합격(1.0)'인 경우
pred_probs = tf.constant([[0.8]])
target_binary = tf.constant([[1.0]])
loss_bce = bce_loss_fn(target_binary, pred_probs)
print(f"BCE Loss: {loss_bce.numpy()}") # 예측이 맞았으므로 매우 낮은 손실값 출력
# 3. CCE (Categorical Cross Entropy) 실례 - 다중 분류 (개/고양이/새)
cce_loss_fn = tf.keras.losses.CategoricalCrossentropy()
# 정답이 원-핫 인코딩 되어있다고 가정 (고양이: [0, 1, 0])
target_class = tf.constant([[0.0, 1.0, 0.0]])
# 모델이 예측한 각 클래스별 속할 확률 (합이 1.0)
pred_probs_multi = tf.constant([[0.1, 0.8, 0.1]])
loss_cce = cce_loss_fn(target_class, pred_probs_multi)
print(f"CCE Loss: {loss_cce.numpy()}")
아마 우리 프로젝트에선 이걸 사용하지 않을까...
#활성함수의 예
import tensorflow as tf
# 임의의 1차원 데이터 생성
input_data = tf.constant([-2.0, 0.0, 2.0])
# 1. Sigmoid 실례
out_sigmoid = tf.keras.activations.sigmoid(input_data)
print(f"Sigmoid Out: {out_sigmoid.numpy()}")
# 결과: 음수는 0에 가깝게, 양수는 1에 가깝게 변환됨 (0과 1 사이)
좀 더 자세히 말하면 Sigmoid(x)는 1/e^x+1이다. 자연상수 e의 지수함수를 사용함.
x가 무한히 커지면 Sigmoid(x)는 0에 수렴,
x가 무한히 작아지면 Sigmoid(x)는 1에 수렴.
# 2. ReLU 실례
out_relu = tf.keras.activations.relu(input_data)
print(f"ReLU Out: {out_relu.numpy()}")
# 결과: [0.0, 0.0, 2.0] -> 0 미만의 값은 모두 0으로 깎아버림
# 3. Leaky ReLU 실례 (tf.nn 모듈 사용)
out_leaky = tf.nn.leaky_relu(input_data, alpha=0.1) # 음수일 때 곱할 작은 기울기(alpha) 설정
print(f"Leaky ReLU Out: {out_leaky.numpy()}")
# 결과: [-0.2, 0.0, 2.0] -> 음수값이 완전히 0이 되지 않고 약간의 값을 유지 (Dying ReLU 방지)
# 4. Softmax 실례
# 텐서플로우의 Softmax는 다차원 텐서를 기대하므로 2차원 형태로 만들어 계산
input_data_2d = tf.constant([[-2.0, 0.0, 2.0]])
out_softmax = tf.keras.activations.softmax(input_data_2d)
print(f"Softmax Out: {out_softmax.numpy()}")
# 결과: 가장 큰 값인 2.0이 가장 높은 확률을 가지며, 값들의 합이 무조건 1.0이 됨
print(f"Softmax Sum: {tf.reduce_sum(out_softmax).numpy()}") # 합: 1.0
보통 Softmax와 sigmoid가 가장 많이 쓰인다고 함ㅇㅇ
순전파와 역전파
#지방과 소금 성분에 따른 치즈 선호도
# 독립변수 : Fat, Salt
# 종속변수 : Acceptance (like, dislike)
# 3개 계층으로 이루어지며 왼쪽부터 입력층, 은닉층, 출력층 구조이다.
#시그모이드 함수 시각화
import numpy as np
import matplotlib.pylab as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
X=np.arange(-5.0, 5.0, 0.1)
Y=sigmoid(X)
plt.plot(X, Y)
plt.ylim(-0.1, 1.1)
plt.show()
맷플롯립을 사용해서 시그모이드 함수를 시각화하는 코드이다.

시그모이드 함수는 '기울기 소실'이라는 한계점이 있기 때문에 이진분류의 출력층에만 제한적으로 주로 쓰인다.
시그모이드 함수의 그래프를 보면, 양 끝 곡선이 거의 평평한데, 이때 기울기는 거의 0에 가깝다.
경사하강법에 따르면, 모델 학습은 기울기를 따라 가중치를 조금씩 업데이트 하는것인데, 기울기가 0에 가깝기 때문에 가중치가 거의 업데이트 되지 않아서, 학습이 멈춘것 처럼 느려진다.
이런한계 때문에, 오늘날신경망의 은닉층에서는 시그모이드 대신 ReLU같은 함수를 훨씬 많이 쓴다.
# 2*3*1 구조의 간단한 순전파 신경망 구현
input=[0.2, 0.9] # 입력층의 2개의 값
weight1=[[0.05, 0.01], [-0.01, 0.03], [0.02, -0.01]] # 입력층에서 은닉층으로 가는 가중치
bias1=[-0.3, 0.2, 0.05] # 은닉층으로 가는 바이어스
weight2=[0.01,0.05,0.015] # 은닉층에서 출력층으로 가는 가중치
bias2=[-0.015] # 출력층으로 가는 바이어스
우선 입력층의 2개의 값(지방과 소금의 양)과, 입력층에서 은닉층으로 가는 가중치 weight1, 은닉층으로 가는 바이어스 bias1, 은닉층에서 출력층으로 가는 가중치 weight2, 출력층으로 가는 바이어스 bias2를 정의한다.
입력값을 제외한 가중치와 편향은 임의로 정해진다.
# 입력층의 첫 번째 노드에서 은닉층의 첫 번째 노드로 신호 전달
def dot(v, w):
return sum(i * j for i, j in zip(v, w))
dot(input,weight1[0])+bias1[0] #0.2*0.05 +0.9*0.01 + (-0.3) # =-0.28099999999999997
input 벡터 (0.2,0.9) 와 weight1[0] 벡터 (0.05,0.01)을 내적하고 bias1[0]을 더해서 A3을 구한다.
위와 같은 방식으로, A3,A4,A5를 구한다.
# 입력층에서 은닉층 노드로 보내는 신호값
A3=dot(input,weight1[0])+bias1[0]
A4=dot(input,weight1[1])+bias1[1]
A5=dot(input,weight1[2])+bias1[2]
A3, A4, A5 #(-0.28099999999999997, 0.225, 0.045)
ㅇㅇ
#시그모이드 함수를 이용해 A3, A4, A5 노드에서 출력하는 값
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x)) # np.exp(x)는 자연상수 e의 x 승 값을 리턴함
outputA3=sigmoid(A3)
outputA4=sigmoid(A4)
outputA5=sigmoid(A5)
outputA3, outputA4, outputA5 #(0.4302086298074603, 0.5560138905446199, 0.5112481019468548)
#은닉층에서 출력층으로 신호 전달
Y1=[outputA3, outputA4, outputA5]
A6=dot(Y1, weight2)+bias2
outputA6=sigmoid(A6)
outputA6 #array([0.50619256])
ㅇㅇ
여기까지 2*3*1 구조의 간단한 순전파 신경망을 구현해보았다.
이제 넘파이를 이용해서 계산해보자.
# 넘파이를 활용한 순전파 신경망 계산
'''
지금까지의 계산을 넘파이를 이용하면 매우 편리하게 진행할 수 있다. 입력층에서 은닉층으로 전달되는 값
A1을 구하고 시그모이드 함수를 이용하여 출력층으로 전달하기 위한 값 Y1을 구한다.
'''
# 입력층에서 첫 번째 은닉층으로 신호 전달 방법
import numpy as np
input=np.array([0.2, 0.9]) # 1*2 행렬
weight1=np.array([[0.05,0.01], [-0.01,0.03], [0.02,-0.01]]) # 3*2 행렬
bias1=np.array([-0.3,0.2,0.05])
A1=np.dot(input, weight1.T) + bias1 # weight1.T는 weight의 전치 행렬
print(A1)
Y1=sigmoid(A1)
Y1 #array([0.43020863, 0.55601389, 0.5112481])
'''
다음은 은닉층의 값 Y1에 가중치와 바이어스 값을 적용해 출력층으로 전달하는 과정이다. 이 값은 A2에
저장하고 시그모이드 함수를 이용해 출력층에서 최종 산출하는 값을 Y2에 저장한다.
'''
weight2=np.array([0.01, 0.05, 0.015])
bias2=np.array([-0.015])
A2=np.dot(Y1, weight2.T)+bias2
Y2=sigmoid(A2)
Y2 #array([0.50619256])
'''
보통 마지막 출력층에서는 시그모이드 함수 대신 항등 함수를 사용하지만 이 책에서는 편의상 최종 출력
값으로 시그모이드 함수를 사용한 값을 이용하기로 한다. 항등 함수를 사용할 때 출력층의 값은 다음과 같
다.
'''
넘파이를 사용하면 행렬곱 연산이 용이해서 훨씬 쉽게 구현 할 수 있다.
#항등함수를 이용해서 출력층 구하기
def identity_function(x):
return x
Y=identity_function(A2) # 혹은 Y=A2
Y #array([0.0247715])
#순전파 신경망을 함수로 정의 한다면?
def init_network():
network={}
network['W1']=np.array([[0.05,0.01], [-0.01,0.03], [0.02,-0.01]])
network['b1']=np.array([-0.3,0.2,0.05])
network['W2']=np.array([0.01, 0.05, 0.015])
network['b2']=np.array([-0.015])
return network
def forward(network, x):
W1, W2=network['W1'], network['W2']
b1, b2=network['b1'], network['b2']
a1=np.dot(x, W1.T) + b1
y1=sigmoid(a1)
a2=np.dot(y1, W2.T) + b2
y2=sigmoid(a2) # 시그모이드 함수 사용
# z2=identity_function(a2) # 항등 함수 사용
return y2
network=init_network()
input=np.array([0.2, 0.9])
Y=forward(network, input)
Y #array([0.50619256])
'파이썬' 카테고리의 다른 글
| 0713 ML/DL (0) | 2026.07.13 |
|---|---|
| 0710 Numpy (0) | 2026.07.10 |
| 0709 파이썬 기초 문법 복습 (0) | 2026.07.09 |
| 0708 데이터 분석 및 시각화 (0) | 2026.07.09 |
| 0707 Pandas (0) | 2026.07.09 |