시작하며
오늘부터 딥러닝 파트가 시작되었다. 딥러닝이 무엇인지에 대해 간단하게 배우고 딥러닝 데이터 처리를 위한 PyTorch를 배웠다.
딥러닝의 이해
머신러닝의 한계
- 데이터에 내재된 패턴을 학습
- 사람이 직접 특성을 설계해야 한다는 한계가 존재
- 문제 해결에 어떤 특성이 필요한지 스스로 판단 못함
- 특성 설계에 크게 의존
머신러닝의 도전과 해결책
- 수학적 규칙을 기반으로 인간이 해결하기 어려운 형식적인 문제를 효과적으로 해결할 수 있도록 발전
- 수학적 규칙으로 명확하게 서술하는 것이 어려운 문제에 한계가 있음
- 한계 극복을 위해 개념 간의 연결 관계를 활용하여 데이터를 학습 → 스스로 개념을 학습
- 여러 개의 층으로 구성된 구조를 형성
딥러닝의 출현
- 딥러닝의 핵심 모델은 순방향 심층 신경망 또는 다층 퍼셉트론
- 다층 퍼셉트론은 인간 뇌의 뉴런에서 영감을 받은 구조로 입력층과 출력층 사이에 은닉층을 포함
- 각 층은 가중치 행렬과 편향을 사용하여 입력에 대해 선형 결합을 수행
- 선형 결합으로 계산한 출력값은 활성화 함수를 거쳐 비선형 변환
- 이 과정을 여러 층에 걸쳐 반복
- 원시 데이터로부터 특징 집합을 추출 → 비정형 데이터 관련 문제를 효과적으로 해결
딥러닝의 핵심 발상
- 사람이 특징을 정의하지 않고 모델이 스스로 특징을 학습
- 원본 데이터로부터 시작 → 낮은 수준의 패턴에서 점점 추상적인 개념으로 나아가는 계층적 표현을 학습
- 이미지 데이터 : 선과 모서리같은 단순한 패턴에서 점차 형태와 객체를 인식하는 구조가 만들어짐
- 텍스트 데이터 : 문자 또는 토큰 단위의 패턴을 바탕으로 단어 의미와 문장 구조를 학습 → 문맥과 의도를 이해하는 추상적인 표현을 형성
- 은닉층이 깊어질수록 모델은 단순한 패턴에서 복잡한 개념으로 점진적으로 조합
딥러닝 학습의 기본 원리
- 순전파(Forward)
- 입력과 가중치의 선형 결합
- 활성화 함수(비선형 변환)
- 예측값 출력(실수, 확률)
- 역전파(Backward)
- 손실 계산
- 손실 함수의 기울기 계산
- 가중치 업데이트
머신러닝과 딥러닝의 역할 분담 차이
| 구분 | 머신러닝 | 딥러닝 |
|---|
| 특징 생성 | 사람이 직접 설계 | 모델이 자동 학습 |
| 데이터 요구량 | 비교적 적음 | 매우 많음 |
| 해석 가능성 | 상대적으로 높음 | 매우 낮음 |
| 강점 영역 | 정형 데이터 | 비정형 데이터 |
파이토치 기초
딥러닝 프레임워크의 역할
- 딥러닝은 선형대수 기반의 대규모 수치 연산을 반복 수행
- 순전파 : Forward propagation
- 손실 계산 : Loss computation
- 역전파 : Backpropagation
- 가중치 업데이트 : Optimization
- 해당 과정을 효율적으로 수행하기 위해 자동 미분(역전파)과 GPU 연산을 진원하는 전용 도구 필요 → 딥러닝 프레임워크(대표적으로 TensorFlow, PyTorch)
TensorFlow
- Google Brain 팀에서 개발한 딥러닝 프레임워크
- 대규모 분산 학습과 산업 환경 배포를 목표로 설계
- 초기에는 정적 계산 그래프(Static Graph) 방식을 사용
- Keras 통합 이후 즉시 실행을 기본으로 지원
- 고수준 API를 통해 모델 구현을 단순화 등 연구 및 교육 환경에서도 사용성이 개선
- 기본 자료형 : Tensor
- 다차원 배열 구조를 가지며, CPU와 GPU 연산을 모두 지원
- 대규모 서비스 환경을 고려한 생태계를 갖추고 있음
PyTorch
- Facebook AI Research가 Lua 기반 Torch 라이브러리의 설계 철학을 계승
- Python 환경에 맞게 개발한 딥러닝 프레임워크
- 초기에는 NumPy와 유사
- GPU(CUDA) 지원과 신경망 모듈이 추가되면서 딥러닝 연구에 널리 활용
TensorFlow vs PyTorch
| 구분 | TensorFlow | PyTorch |
|---|
| 개발 주체 | Google Brain | Meta (FAIR) |
| 초기 계산 방식 | 정적 그래프 | 동적 그래프 |
| 현재 실행 방식 | 2.x부터 즉시 실행 기본 지원 | 동적 그래프 기본 |
| 코드 흐름 | 구조 정의 후 실행 | Python 실행 흐름과 동일 |
| 산업 배포 | Serving, Lite 등 강점 | 점점 강화 중 |
| 연구 활용 | 가능 | 연구 환경에서 매우 활발 |
| 학습 곡선 | 체계적 구조 이해 필요 | 비교적 직관적 |
- Tensor
- 3차원 텐서의 기본 형태 : (Channel, Height, Width)
- 4차원 텐서의 형태 : (N, C, H, W)
NumPy 배열과 PyTorch 텐서의 차이점
- 모두 다차원 수치 데이터를 표현
- 계산 그래프 : 수치 연산의 순서와 의존 관계를 노드와 엣지로 표현한 그래프 구조를 의미
| 구분 | 다차원 구조 | 수치 연산 | GPU 사용 | 자동 미분 | 계산 그래프 | 학습 대상 |
|---|
| 배열 | 가능 | 가능 | 불가능 | 불가능 | 없음 | 불가능 |
| 텐서 | 가능 | 가능 | 가능 | 가능 | 있음 | 가능 |
텐서의 자동 미분 예시
import torch
requires_grad : True 지정 시 연산 기록이 계산 그래프로 저장
x = torch.tensor(data=[2.0], requires_grad=True)
y = x ** 2
y.backward()
x.grad
NumPy 배열과 PyTorch 텐서의 주요 코드 차이
| 구분 | NumPy 배열 | PyTorch 텐서 |
|---|
| 차원 수 | ar.ndim | ts.ndim |
| 형태(구조) | ar.shape | ts.shape / ts.size() |
| 전체 원소 개수 | ar.size | ts.numel() |
| 자료형 확인 | ar.dtype | ts.dtype |
| 자료형 변환 | ar.astype(dtype) | ts.to(dtype) / ts.float() / ts.long() |
| 차원 추가(축) | np.expand_dims(a=ar, axis=0) | ts.unsqueeze(dim=0) |
| 차원 제거 | np.squeeze(a=ar) | ts.squeeze() |
| 형태 변경 | ar.reshape() | ts.reshape() / ts.view() |
| 평탄화 | ar.flatten() / ar.ravel() | ts.flatten() / ts.ravel() |
| 차원 변경 | - | ts.permute() / ts.transpose() |
0차원 텐서 생성
a = 1.0
ts0 = torch.tensor(a)
type(ts0)
ts0.ndim
ts0.shape
ts0.numel()
ts0.dtype
1차원 텐서 생성
li1 = [1, 2, 3]
li1 = [1, 2.0, 3]
li1 = [1, 2.0, '3']
ar1 = np.array(li1)
ts1 = torch.tensor(li1)
type(ts1)
ts1.ndim
ts1.shape
ts1.numel()
ts1.dtype
PyTorch의 자료형을 지정하는 dtype의 종류
| 구분 | 자료형 | 메서드 | 설명 | 비고 |
|---|
| 정수형 | torch.int16 / torch.short | ts.short() | 16비트 정수 | 거의 사용 안 함 (제한된 지원) |
| torch.int32 / torch.int | ts.int() | 32비트 정수 | 일부 CPU 연산에 사용 (단순 카운팅, 인덱스 계산) |
| torch.int64 / torch.long | ts.long() | 64비트 정수 | PyTorch 기본 정수형 (라벨, 인덱스에 사용) |
| 실수형 | torch.float16 / torch.half | ts.half() | 16비트 실수 (half precision) | 빠른 계산은 float16, 정확한 계산은 float32 |
| torch.float32 / torch.float | ts.float() | 32비트 실수 (single precision) | PyTorch 기본 실수형 (모델의 입력, 가중치에 사용) |
| torch.float64 / torch.double | ts.double() | 64비트 실수 (double precision) | 거의 사용 안 함 (느리고 메모리 낭비) |
1차원 텐서 원소의 자료형 변환
ts1.to(torch.float)
ts1.to(torch.float).dtype
ts1 = ts1.float()
ts1.dtype
ts1.to(torch.long)
ts1.to(torch.long).dtype
ts1 = ts1.long()
ts1.dtype
2차원 텐서 생성
li2 = [[1, 3, 5], [7, 9, 11]]
ts2 = torch.tensor(li2)
type(ts2)
ts2.ndim
ts2.shape
ts2.numel()
ts2.dtype
텐서의 재구조화
ts1 = torch.arange(1, 12, 2)
ts2 = ts1.reshape(2, 3)
- 1차원 텐서로 변환
flatten
- 항상 새로운 텐서를 생성
- 전체 원소를 1차원으로 평탄화 → 배치 크기를 고려하지 못함
ts2.flatten()
- 배치 크기를 유지한 상태로 평탄화하려면
reshape 메서드 사용
img_batch.shape
img_batch.flatten().shape
img_batch.reshape(128, -1).shape
무작위 값을 갖는 텐서 생성
torch.manual_seed(1)
- 0~1 범위의 무작위 실수를 원소로 갖는 텐서 생성
torch.rand(3, 2)
- 지정한 범위의 무작위 정수를 원소로 갖는 텐서 생성
torch.randint(0, 5, (3, 2))
- 표준정규분포를 따른는 무작위 실수를 원소로 갖는 텐서 생성
torch.randn(1, 28, 28)
텐서의 브로드캐스팅
ts1 = torch.tensor([0, 1, 2])
ts1 + 1
ts2 = ts1.reshape(-1, 1)
ts2 * 2
차원 추가 및 제거
ts1.unsqueeze(dim=0)
ts1.unsqueeze(dim=0).shape
ts2.squeeze(dim=1)
ts2.squeeze(dim=1).shape
텐서의 결합
x1 = torch.tensor([0, 1, 2])
x2 = torch.tensor([3, 4, 5])
torch.stack(tensors=[x1, x2], dim=0)
torch.stack(tensors=[x1, x2], dim=1)
gray_img_1 = torch.randn(1, 28, 28)
gray_img_2 = torch.randn(1, 28, 28)
img_batch_1 = torch.stack(tensors=[gray_img_1, gray_img_2], dim=0)
img_batch_1.shape
텐서 입력 형태 변환
ts1
ts1 = ts1.unsqueeze(dim=0).unsqueeze(dim=0)
ts1.shape
- 축 배치 변경 :
permute
- 차원 축을 일괄로 변경
np.transpose() 가 동일한 기능을 수행
ts1.permute(0, 2, 1).shape
ts1.permute(2, 0, 1).shape
- 축 교환 :
transpose
- 2개의 차원을 서로 교환
np.swapaxes() 가 동일한 기능을 수행
ts1.transpose(1, 2).shape
ts1.transpose(0, 2).shape
딥러닝을 위한 기초 수학
딥러닝에서 수학의 필요성
- 딥러닝에서는 매우 단순한 1차식(선형 결합)을 반복적으로 사용
- 입력값에 가중치를 곱하고 편향을 더하는 산술 계산의 연속
- 입력값을 순방향으로 전달하며 선형 결합과 활성화 함수를 통과
- 손실을 최소화하기 위해 다시 역방향으로 미분하여 가중치를 반복적으로 최신화
- 함수의 합성, 미분, 편미분 등 기초 수학 개념에 대한 이해가 필수적
함수의 합성
- 딥러닝 모델은 하나의 함수가 아니라 함수의 합성으로 표현
- f(x)=f2(f1(x))
f1 = lambda x: 2 * x + 1
f2 = lambda x: 3 * x - 2
x = 3
f2(f1(x))
f1 = lambda x1, x2, x3: 2 * x1 + 1.5 * x2 + (-1.2) * x3 + 0.5
f2 = lambda x1: 3 * x1 - 1.2
x1, x2, x3 = 1, 1, 0
f2(f1(x1, x2, x3))
미분 / 변화율과 민감도
- 딥러닝에서 미분은 기울기 계산 이상의 의미를 갖음
- dxdy = 입력이 조금 변할 때 출력이 얼마나 변하는가
def f(x):
return x ** 2
x, h = 3, 1e-5
(f(x + h) - f(x)) / h
다양한 함수의 미분
| 구분 | 함수 | 미분 공식 |
|---|
| 상수함수 | f(x)=c | dxdf(x)=0 |
| 다항함수 | f(x)=xn | dxdf(x)=nxn−1 |
| 로그함수 | f(x)=lnx f(x)=logax | dxdf(x)=x1 dxdf(x)=xlna1 |
| 지수함수 | f(x)=ex f(x)=ax | dxdf(x)=ex dxdf(x)=axlna |
편미분의 직관적 이해
- 편미분은 하나의 가중치만 변화시켰을 때, 출력값이 얼마나 빠르게 변하는지를 나타냄
- z=w1x1+w2x2+b → ∂w1∂z=x1 , ∂w2∂z=x2
- w1을 증가시키면 출력값 z는 그 증가량에 x1을 곱한 크기만큼 증가
x1, x2 = 2.0, 3.0
w1, w2 = 0.5, -1.0
w1 * x1 + w2 * x2
(w1 + 0.1) * x1 + w2 * x2
점곱
- 가중치와 입력값의 선형 결합은 점곱 연산의 결과
- w⋅x=∑i=1nwixi
- 배열의 점곱
import numpy as np
w = np.array([0.5, -1.0])
x = np.array([2.0, 3.0])
np.dot(w, x).item()
import torch
w = torch.tensor([0.5, -1.0])
x = torch.tensor([2.0, 3.0])
torch.dot(w, x).item()
연쇄 법칙의 필연성
- 딥러닝 모델은 합성 함수를 사용 → 미분의 연쇄 법칙을 사용
- dxdy=dzdy⋅dxdz
- 이 계산이 층을 거슬러 반복적으로 저용되는 것이 역전파
x = 2.0
z = 2 * x + 1
y = 3 * z - 2
dy_dz = 3
dz_dx = 2
dy_dz * dz_dx
활성화 함수의 수학적 역할
- 활성화 함수는 대부분의 구간에서 미분 가능하며 기울기를 전달하는 역할을 수행
def relu(x):
return np.maximum(0, x)
x = np.array([-2.0, 1.0, 3.0])
relu(x)
소프트맥스
- 다중 분류 문제에서 각 클래스별 점수(로짓)를 확률로 변환할 때 필요
- Pik=∑j=1Kexp(zij)exp(zik)
- Pik는 i번째 관측값이 k번째 클래스에 속할 확률
- zik는 마지막 은닉층의 출력이 출력층 가중치와 선형 결합되어 계산된 값
- 지수 함수를 사용하면 로짓의 차이를 로그 확률 비로 표현 가능
z = np.array([2.0, 1.0, -0.1])
z = z - np.max(z)
exp_z = np.exp(z)
softmax = exp_z / np.sum(exp_z)
softmax
np.sum(softmax)
손실 함수
- 딥러닝에서 학습 목표는 손실 함수를 최소화하는 방향으로 가중치를 조정하는 것
- L=n1∑i=1n(yi−yi^)2
크로스 엔트로피
- 다중 분류 문제에서 손실 함수는 크로스 에늩로피를 주로 사용
- Li=−∑k=1Kyiklog(pik)
y_true = np.array([1.0, 0.0, 0.0])
y_prob = np.array([0.6710, 0.2468, 0.0822])
- 확률은 0과 1사이의 실수라 로그를 씌우면 항상 음수가 됨
np.log(y_prob)
- 손실 함수는 값이 작을수록 좋으므로 로그 확률의 부호를 바꿔야 함
-np.dot(y_true, np.log(y_prob))
마치며
수업 중에 계속해서 수식을 접하니까 이제 간단한 수식들은 보면 바로 이해가 되는 것 같다. 물론 수식을 적용해서 문제를 풀거나 하지는 못하겠지만 수식을 통해 해당 개념을 더 잘 이해할 수 있게 되는 것 같아서 도움이 되는 것 같다. 내일부터는 이미지 데이터를 다루는 작업을 시작하는데 상당히 재밌을 것 같아서 기대가 된다.