5장 인공지능 학습
- “부록3 매트플롯립 입문”에서 한글 폰트를 올바르게 출력하기 위한 설치 방법을 설명했다. 설치 방법은 다음과 같다.
!sudo apt-get install -y fonts-nanum* | tail -n 1
!sudo fc-cache -fv
!rm -rf ~/.cache/matplotlibdebconf: unable to initialize frontend: Dialog
debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 78, <> line 4.)
debconf: falling back to frontend: Readline
debconf: unable to initialize frontend: Readline
debconf: (This frontend requires a controlling tty.)
debconf: falling back to frontend: Teletype
dpkg-preconfigure: unable to re-open stdin:
Processing triggers for fontconfig (2.13.1-4.2ubuntu5) ...
/usr/share/fonts: caching, new cache contents: 0 fonts, 1 dirs
/usr/share/fonts/truetype: caching, new cache contents: 0 fonts, 3 dirs
/usr/share/fonts/truetype/humor-sans: caching, new cache contents: 1 fonts, 0 dirs
/usr/share/fonts/truetype/liberation: caching, new cache contents: 16 fonts, 0 dirs
/usr/share/fonts/truetype/nanum: caching, new cache contents: 39 fonts, 0 dirs
/usr/local/share/fonts: caching, new cache contents: 0 fonts, 0 dirs
/root/.local/share/fonts: skipping, no such directory
/root/.fonts: skipping, no such directory
/usr/share/fonts/truetype: skipping, looped directory detected
/usr/share/fonts/truetype/humor-sans: skipping, looped directory detected
/usr/share/fonts/truetype/liberation: skipping, looped directory detected
/usr/share/fonts/truetype/nanum: skipping, looped directory detected
/var/cache/fontconfig: cleaning cache directory
/root/.cache/fontconfig: not cleaning non-existent cache directory
/root/.fontconfig: not cleaning non-existent cache directory
fc-cache: succeeded
# 필요 라이브러리 설치
!pip install torchviz | tail -n 1Successfully installed nvidia-cublas-cu12-12.4.5.8 nvidia-cuda-cupti-cu12-12.4.127 nvidia-cuda-nvrtc-cu12-12.4.127 nvidia-cuda-runtime-cu12-12.4.127 nvidia-cudnn-cu12-9.1.0.70 nvidia-cufft-cu12-11.2.1.3 nvidia-curand-cu12-10.3.5.147 nvidia-cusolver-cu12-11.6.1.9 nvidia-cusparse-cu12-12.3.1.170 nvidia-nvjitlink-cu12-12.4.127 torchviz-0.0.3
- 모든 설치가 끝나면 한글 폰트를 바르게 출력하기 위해 [런타임] -> **[런타임 다시시작]**을 클릭한 다음, 아래 셀부터 코드를 실행해 주십시오.
# 라이브러리 임포트
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import display
# 폰트 관련 용도
import matplotlib.font_manager as fm
# Colab, Linux
# 나눔 고딕 폰트의 경로 명시
# path = '/usr/share/fonts/truetype/nanum/NanumGothic.ttf'
# font_name = fm.FontProperties(fname=path, size=10).get_name()
# Window
font_name = "NanumBarunGothic"
# Mac
# font_name = "AppleGothic"# 파이토치 관련 라이브러리
import torch
from torchviz import make_dot# 기본 폰트 설정
plt.rcParams['font.family'] = font_name
# 기본 폰트 사이즈 변경
plt.rcParams['font.size'] = 14
# 기본 그래프 사이즈 변경
plt.rcParams['figure.figsize'] = (6,6)
# 기본 그리드 표시
# 필요에 따라 설정할 때는, plt.grid()
plt.rcParams['axes.grid'] = True
plt.rcParams["grid.linestyle"] = ":"
# 마이너스 기호 정상 출력
plt.rcParams['axes.unicode_minus'] = False
# 넘파이 부동소수점 자릿수 표시
np.set_printoptions(suppress=True, precision=4)# warning 표시 끄기
import warnings
warnings.simplefilter('ignore')경사 하강법 (Gradient descent, GD) 구현
GD 3D plot
def L(u, v):
return 3 * u**2 + 3 * v**2 - u*v + 7*u - 7*v + 10
def Lu(u, v):
return 6* u - v + 7
def Lv(u, v):
return 6* v - u - 7
u = np.linspace(-5, 5, 501)
v = np.linspace(-5, 5, 501)
U, V = np.meshgrid(u, v)
Z = L(U, V)# 경사 하강법 시뮬레이션
W = np.array([4.0, 4.0])
W1 = [W[0]]
W2 = [W[1]]
N = 21
alpha = 0.05
for i in range(N):
W = W - alpha *np.array([Lu(W[0], W[1]), Lv(W[0], W[1])])
W1.append(W[0])
W2.append(W[1])n_loop=11
WW1 = np.array(W1[:n_loop])
WW2 = np.array(W2[:n_loop])
ZZ = L(WW1, WW2)
fig = plt.figure(figsize=(8,8))
ax = plt.axes(projection='3d')
ax.set_zlim(0,250)
ax.set_xlabel('W')
ax.set_ylabel('B')
ax.set_zlabel('loss')
ax.view_init(30, 240)
ax.xaxis._axinfo["grid"]['linewidth'] = 2.
ax.yaxis._axinfo["grid"]['linewidth'] = 2.
ax.zaxis._axinfo["grid"]['linewidth'] = 2.
ax.contour3D(U, V, Z, 100, cmap='Blues', alpha=0.7)
ax.plot3D(WW1, WW2, ZZ, 'o-', c='r', alpha=1, markersize=7)
plt.show()
데이터 전처리
다섯명의 신장과 체중 데이터를 사용한다.
1차 함수를 사용해 신장으로 체중을 예측하는 경우, 최적 직선을 구하는 것이 목적이다.
# 샘플 데이터 선언
sampleData1 = np.array([
[166, 58.7],
[176.0, 75.7],
[171.0, 62.1],
[173.0, 70.4],
[169.0,60.1]
])
print(sampleData1)[[166. 58.7]
[176. 75.7]
[171. 62.1]
[173. 70.4]
[169. 60.1]]
# 머신러닝 모델에서 사용하기 위해, 신장을 변수 x로,
# 체중을 변수 y로 함
x = sampleData1[:,0]
y = sampleData1[:,1]# 산포도 출력 확인
plt.scatter(x, y, c='k', s=50)
# plt.plot([166, 176], [60, 75], 'r:')
plt.xlabel('$x$: 신장 (cm) ')
plt.ylabel('$y$: 체중 (kg)')
plt.title('신장과 체중의 관계')
plt.show()
데이터 변환
머신러닝 모델에서 데이터는 0에 가까운 값을 갖는 것이 바람직하다. 따라서, x, y 모두 평균값이 0이 되도록 평행이동시켜서 새로운 좌표계를 X, Y로 한다.
X = x - x.mean()
Y = y - y.mean()# 산포도를 통해 결과 확인
plt.scatter(X, Y, c='k', s=50)
plt.xlabel('$X$')
plt.ylabel('$Y$')
plt.title('데이터 가공 후 신장과 체중의 관계')
plt.show()
예측 계산
# X와 Y를 텐서 변수로 변환
X = torch.tensor(X).float()
Y = torch.tensor(Y).float()
# 결과 확인
print(X)
print(Y)tensor([-5., 5., 0., 2., -2.])
tensor([-6.7000, 10.3000, -3.3000, 5.0000, -5.3000])
# 파라미터 정의
# W와 B는 경사 계산을 위해, requires_grad=True 로 설정함
W = torch.tensor(1.0, requires_grad=True).float()
B = torch.tensor(1.0, requires_grad=True).float()# 예측 함수는 1차 함수
def pred(X):
return W * X + B# 예측 값 계산
Yp = pred(X)
# 결과 확인
print(Yp)tensor([-4., 6., 1., 3., -1.], grad_fn=<AddBackward0>)
# 예측 값의 계산 그래프 표시
params = {'W': W, 'B': B}
g = make_dot(Yp, params=params)
display(g)손실 계산
# 평균 제곱 오차 손실함수
def mse(Yp, Y):
loss = ((Yp - Y) ** 2).mean()
return loss# 손실 계산
loss = mse(Yp, Y)
# 결과 표시
print(loss)tensor(13.3520, grad_fn=<MeanBackward0>)
# 손실 계산 그래프 출력
params = {'W': W, 'B': B}
g = make_dot(loss, params=params)
display(g)경사 계산
# 경사 계산
loss.backward()
# loss.backward(retain_graph=True)
# 경삿값 확인
print(W.grad)
print(B.grad)tensor(-19.0400)
tensor(2.0000)
파라미터 수정
# 학습률 정의
lr = 0.001W와 B는 한번 계산이 끝났기 때문에, 이 상태로 값의 갱신은 불가능하다. 따라서, 다음과 같이 수정해야 한다.
# 경사를 기반으로 파라미터 수정
# W -= lr * W.grad
# B -= lr * B.grad ## Error
with torch.no_grad():
W -= lr * W.grad
B -= lr * B.grad
# 계산이 끝난 경삿값을 초기화함
W.grad.zero_()
B.grad.zero_()
# 파라미터 경삿값 확인
print(W)
print(B)
print(W.grad)
print(B.grad)tensor(1.0190, requires_grad=True)
tensor(0.9980, requires_grad=True)
tensor(0.)
tensor(0.)
반복 계산
# 초기화
# W와 B를 변수로 사용
W = torch.tensor(1.0, requires_grad=True).float()
B = torch.tensor(1.0, requires_grad=True).float()
# 반복 횟수
num_epochs = 500
# 학습률
lr = 0.001
# history 기록을 위한 배열 초기화
history = np.zeros((0, 2))# 루프 처리
for epoch in range(num_epochs):
# 예측 계산
Yp = pred(X)
# 손실 계산
loss = mse(Yp, Y)
# 경사 계산
loss.backward()
with torch.no_grad():
# 파라미터 수정
W -= lr * W.grad
B -= lr * B.grad
# 경삿값 초기화
W.grad.zero_()
B.grad.zero_()
# 손실 기록
if (epoch %10 == 0):
item = np.array([epoch, loss.item()])
history = np.vstack((history, item))
print(f'epoch = {epoch} loss = {loss:.4f}')epoch = 0 loss = 13.3520
epoch = 10 loss = 10.3855
epoch = 20 loss = 8.5173
epoch = 30 loss = 7.3364
epoch = 40 loss = 6.5858
epoch = 50 loss = 6.1047
epoch = 60 loss = 5.7927
epoch = 70 loss = 5.5868
epoch = 80 loss = 5.4476
epoch = 90 loss = 5.3507
epoch = 100 loss = 5.2805
epoch = 110 loss = 5.2275
epoch = 120 loss = 5.1855
epoch = 130 loss = 5.1507
epoch = 140 loss = 5.1208
epoch = 150 loss = 5.0943
epoch = 160 loss = 5.0703
epoch = 170 loss = 5.0480
epoch = 180 loss = 5.0271
epoch = 190 loss = 5.0074
epoch = 200 loss = 4.9887
epoch = 210 loss = 4.9708
epoch = 220 loss = 4.9537
epoch = 230 loss = 4.9373
epoch = 240 loss = 4.9217
epoch = 250 loss = 4.9066
epoch = 260 loss = 4.8922
epoch = 270 loss = 4.8783
epoch = 280 loss = 4.8650
epoch = 290 loss = 4.8522
epoch = 300 loss = 4.8399
epoch = 310 loss = 4.8281
epoch = 320 loss = 4.8167
epoch = 330 loss = 4.8058
epoch = 340 loss = 4.7953
epoch = 350 loss = 4.7853
epoch = 360 loss = 4.7756
epoch = 370 loss = 4.7663
epoch = 380 loss = 4.7574
epoch = 390 loss = 4.7488
epoch = 400 loss = 4.7406
epoch = 410 loss = 4.7327
epoch = 420 loss = 4.7251
epoch = 430 loss = 4.7178
epoch = 440 loss = 4.7108
epoch = 450 loss = 4.7040
epoch = 460 loss = 4.6976
epoch = 470 loss = 4.6913
epoch = 480 loss = 4.6854
epoch = 490 loss = 4.6796
결과 평가
# 최종 파라미터 값
print('W = ', W.data.numpy())
print('B = ', B.data.numpy())
# 손실 확인
print(f'초기상태 : 손실:{history[0,1]:.4f}')
print(f'최종상태 : 손실:{history[-1,1]:.4f}')W = 1.820683
B = 0.3675114
초기상태 : 손실:13.3520
최종상태 : 손실:4.6796
# 학습 곡선 출력(손실)
plt.plot(history[:,0], history[:,1], 'b')
plt.xlabel('반복 횟수')
plt.ylabel('손실')
plt.title('학습 곡선(손실)')
plt.show()
산포도에 회귀 직선을 동시에 출력함
# x의 범위를 구함(Xrange)
X_max = X.max()
X_min = X.min()
X_range = np.array((X_min, X_max))
X_range = torch.from_numpy(X_range).float()
print(X_range)
# 이와 대응하는 예측값 y를 구함
Y_range = pred(X_range)
print(Y_range.data)tensor([-5., 5.])
tensor([-8.7359, 9.4709])
# 그래프 출력
plt.scatter(X, Y, c='k', s=50)
plt.xlabel('$X$')
plt.ylabel('$Y$')
plt.plot(X_range.data, Y_range.data, lw=2, c='b')
plt.title('신장과 체중의 상관 직선(가공 후)')
plt.show()
가공 전 데이터로 회귀 직선 출력
# y좌표와 x좌표 값 계산
x_range = X_range + x.mean()
yp_range = Y_range + y.mean()# 그래프 출력
plt.scatter(x, y, c='k', s=50)
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.plot(x_range, yp_range.data, lw=2, c='b')
plt.title('신장과 체중의 상관 직선(가공 전)')
plt.show()
최적화 함수와 step 함수 이용하기
# 초기화
# W와 B를 변수로 사용
W = torch.tensor(1.0, requires_grad=True).float()
B = torch.tensor(1.0, requires_grad=True).float()
# 반복 횟수
num_epochs = 500
# 학습률
lr = 0.001
# optimizer 로 SGD(확률적 경사 하강법)을 사용
import torch.optim as optim
optimizer = optim.SGD([W, B], lr=lr)
# history 기록을 위한 배열 초기화
history = np.zeros((0, 2))# 루프 처리
for epoch in range(num_epochs):
# 예측 계산
Yp = pred(X)
# 손실 계산
loss = mse(Yp, Y)
# 경삿값 초기화
optimizer.zero_grad()
# 경사 계산
loss.backward()
# 파라미터 수정
optimizer.step()
# 손실 기록
if (epoch %10 == 0):
item = np.array([epoch, loss.item()])
history = np.vstack((history, item))
print(f'epoch = {epoch} loss = {loss:.4f}')epoch = 0 loss = 13.3520
epoch = 10 loss = 10.3855
epoch = 20 loss = 8.5173
epoch = 30 loss = 7.3364
epoch = 40 loss = 6.5858
epoch = 50 loss = 6.1047
epoch = 60 loss = 5.7927
epoch = 70 loss = 5.5868
epoch = 80 loss = 5.4476
epoch = 90 loss = 5.3507
epoch = 100 loss = 5.2805
epoch = 110 loss = 5.2275
epoch = 120 loss = 5.1855
epoch = 130 loss = 5.1507
epoch = 140 loss = 5.1208
epoch = 150 loss = 5.0943
epoch = 160 loss = 5.0703
epoch = 170 loss = 5.0480
epoch = 180 loss = 5.0271
epoch = 190 loss = 5.0074
epoch = 200 loss = 4.9887
epoch = 210 loss = 4.9708
epoch = 220 loss = 4.9537
epoch = 230 loss = 4.9373
epoch = 240 loss = 4.9217
epoch = 250 loss = 4.9066
epoch = 260 loss = 4.8922
epoch = 270 loss = 4.8783
epoch = 280 loss = 4.8650
epoch = 290 loss = 4.8522
epoch = 300 loss = 4.8399
epoch = 310 loss = 4.8281
epoch = 320 loss = 4.8167
epoch = 330 loss = 4.8058
epoch = 340 loss = 4.7953
epoch = 350 loss = 4.7853
epoch = 360 loss = 4.7756
epoch = 370 loss = 4.7663
epoch = 380 loss = 4.7574
epoch = 390 loss = 4.7488
epoch = 400 loss = 4.7406
epoch = 410 loss = 4.7327
epoch = 420 loss = 4.7251
epoch = 430 loss = 4.7178
epoch = 440 loss = 4.7108
epoch = 450 loss = 4.7040
epoch = 460 loss = 4.6976
epoch = 470 loss = 4.6913
epoch = 480 loss = 4.6854
epoch = 490 loss = 4.6796
# 최종 파라미터 값
print('W = ', W.data.numpy())
print('B = ', B.data.numpy())
# 손실 확인
print(f'초기상태 : 손실:{history[0,1]:.4f}')
print(f'최종상태 : 손실:{history[-1,1]:.4f}')W = 1.820683
B = 0.3675114
초기상태 : 손실:13.3520
최종상태 : 손실:4.6796
# 학습 곡선 출력(손실)
plt.plot(history[:,0], history[:,1], 'b')
plt.xlabel('반복 횟수')
plt.ylabel('손실')
plt.title('학습 곡선(손실)')
plt.show()
3.7의 결과와 비교해보면 동일한 것을 알 수 있다. 따라서, step 함수는 다음의 코드와 같은 로직을 수행하고 있다.
with torch.no_grad():
# 파라미터 수정
# 프레임워크를 사용하는 경우는 step 함수가 이를 대신함
W -= lr * W.grad
B -= lr * B.grad최적화 함수 튜닝
# 초기화
# W와 B를 변수로 사용
W = torch.tensor(1.0, requires_grad=True).float()
B = torch.tensor(1.0, requires_grad=True).float()
# 반복 횟수
num_epochs = 500
# 학습률
lr = 0.001
# optimizer로 SGD(확률적 경사 하강법)을 사용
import torch.optim as optim
optimizer = optim.SGD([W, B], lr=lr, momentum=0.9)
# history 기록을 위한 배열 초기화
history2 = np.zeros((0, 2))# 루프 처리
for epoch in range(num_epochs):
# 예측 계산
Yp = pred(X)
# 손실 계산
loss = mse(Yp, Y)
# 경사 계산
loss.backward()
# 파라미터 수정
optimizer.step()
# 경삿값 초기화
optimizer.zero_grad()
# 손실 기록
if (epoch %10 == 0):
item = np.array([epoch, loss.item()])
history2 = np.vstack((history2, item))
print(f'epoch = {epoch} loss = {loss:.4f}')epoch = 0 loss = 13.3520
epoch = 10 loss = 5.7585
epoch = 20 loss = 5.9541
epoch = 30 loss = 5.0276
epoch = 40 loss = 4.8578
epoch = 50 loss = 4.7052
epoch = 60 loss = 4.6327
epoch = 70 loss = 4.5940
epoch = 80 loss = 4.5698
epoch = 90 loss = 4.5574
epoch = 100 loss = 4.5495
epoch = 110 loss = 4.5452
epoch = 120 loss = 4.5426
epoch = 130 loss = 4.5411
epoch = 140 loss = 4.5403
epoch = 150 loss = 4.5398
epoch = 160 loss = 4.5395
epoch = 170 loss = 4.5393
epoch = 180 loss = 4.5392
epoch = 190 loss = 4.5391
epoch = 200 loss = 4.5391
epoch = 210 loss = 4.5391
epoch = 220 loss = 4.5391
epoch = 230 loss = 4.5390
epoch = 240 loss = 4.5390
epoch = 250 loss = 4.5390
epoch = 260 loss = 4.5390
epoch = 270 loss = 4.5390
epoch = 280 loss = 4.5390
epoch = 290 loss = 4.5390
epoch = 300 loss = 4.5390
epoch = 310 loss = 4.5390
epoch = 320 loss = 4.5390
epoch = 330 loss = 4.5390
epoch = 340 loss = 4.5390
epoch = 350 loss = 4.5390
epoch = 360 loss = 4.5390
epoch = 370 loss = 4.5390
epoch = 380 loss = 4.5390
epoch = 390 loss = 4.5390
epoch = 400 loss = 4.5390
epoch = 410 loss = 4.5390
epoch = 420 loss = 4.5390
epoch = 430 loss = 4.5390
epoch = 440 loss = 4.5390
epoch = 450 loss = 4.5390
epoch = 460 loss = 4.5390
epoch = 470 loss = 4.5390
epoch = 480 loss = 4.5390
epoch = 490 loss = 4.5390
# 학습 곡선(손실) 출력
plt.plot(history[:,0], history[:,1], 'b', label='기본값 설정')
plt.plot(history2[:,0], history2[:,1], 'k', label='momentum=0.9')
plt.xlabel('반복 횟수')
plt.ylabel('손실')
plt.legend()
plt.title('학습 곡선(손실)')
plt.show()
y = x**2 함수의 경사하강 (GD)
x = np.arange(-4, 4.1, 0.1)
y = x**2
plt.figure(figsize = (6, 4))
plt.grid(linestyle = ":")
plt.plot(x, y)
## 초기값 (initial value)
x_ = 4
y_ = 16
lr = 0.1
iter = 10
# for _ in range(iter):
# plt.scatter(x_, y_, s = 80, c = "r")
# dy_dx = 2*x_
# x_ = x_ - lr*dy_dx
# y_ = x_**2
plt.show()
Regression GD
## X, Y data
x = np.arange(2, 10, step = 2, dtype=np.float16)
y = np.array([81, 93, 91, 97], dtype=np.float16)
plt.scatter(x, y, s = 80)
plt.show()
## 최소제곱법을 이용한 회귀계수 추정
a = 0
b = 0
## learning rate
lr = 0.02
iter = 501
fig, ax = plt.subplots(1, 6, figsize = (12,4))
x_fit = np.arange(2, 10)
j = 0
for i in range(iter):
y_hat = a*x + b
error = (y - y_hat)
a_diff = -(2/len(x))*sum(error*x)
b_diff = -(2/len(x))*sum(error)
a = a - lr*a_diff
b = b - lr*b_diff
if i % 100 == 0:
print(f'iter = {i}, slope = {a:.4f}, intercept = {b:.4f}')
ax[j].scatter(x, y)
ax[j].plot(x_fit, b + a*x_fit, 'r')
ax[j].set_title(f'iteration = {i}')
ax[j].set_ylim(70, 100)
j += 1
plt.show()

iter = 0, slope = 18.5600, intercept = 3.6200
iter = 100, slope = 8.9456, intercept = 39.3669
iter = 200, slope = 5.7656, intercept = 58.3238
iter = 300, slope = 4.1119, intercept = 68.2131
iter = 400, slope = 3.2519, intercept = 73.3725
iter = 500, slope = 2.7869, intercept = 76.0619

복잡한 함수의 GD
# From calculation, we expect that the local minimum occurs at x=9/4
x_old = 0
x_new = 6 # The algorithm starts at x=6
eps = 0.01 # step size
precision = 0.00001
def f(x):
return x**4 + 3*x**3 + 1
def f_prime(x):
return 4 * x**3 - 9 * x**2
while abs(x_new - x_old) > precision:
x_old = x_new
x_new = x_old - eps * f_prime(x_old)
print(f"Local minimum occurs at: {x_new}")
x = np.arange(-4, 2, 0.1)
plt.plot(x, f(x))
plt.grid(linestyle = ":")
plt.show()Local minimum occurs at: 2.2499646074278457
