728x90
반응형

훈련 세트 - fit() 메서드 전달 모델 훈련 사용  - epoch사용해 반복 훈련

테스트 세트 - score() 메서드 전달 모델 성능 평가 - 정확도 

 

 

테스트 세트로 모델 튜닝

  • SGDClassifier클래스 이용해 회귀 문제 경사 하강법 적용
  • loss 매개변수의 값을 log로 지정해 로지스틱 손실 함수를 손실 함수로 지정

 

로지스틱 회귀로 모델 훈련하고 평가

# 로지스틱 회귀로 모델 훈련하고 평가하기
# cancer 데이터 세트 읽어서 훈련 세트와 테스트 세트로 나누기

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

cancer = load_breast_cancer()
x = cancer.data
y = cancer.target

x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify=y,test_size=0.2, random_state=42)
# 훈련 데이터 전부, 테스트 , 훈련 데이터 전부, 테스트  
# split분할 data, target데이터        
                                  

 

SGDClassifier 클래스 이용해 로지스틱 회귀 모델 훈련

# 위의 데이터 SGDClassifier클래스 이용해 로지스틱 회귀 모델 훈련
# fit () - 반복 훈련
# score () - 정확도 성능

from sklearn.linear_model import SGDClassifier

sgd = SGDClassifier(loss='log', random_state=42)
sgd.fit(x_train_all, y_train_all)
sgd.score(x_test, y_test) 
# 0.83333~~~

[그림1] SGDClassifier 클래스 로지스틱 회귀 모델 훈련

 

loss와 같은 매개변수의 값은 가중치나 절편처럼 알아서 학습되는 것이 아니다. (사용자가 직접 선택)

이러한 값을 하이퍼파라미터(hyperparameter)

 

하이퍼파라미터(hyperparameter)

  • 기계 학습에서 하이퍼 파라미터란 학습 프로세스를 제어하는 데 사용되는 값을 갖는 매개 변수

 

서포트 벡터 머신으로 모델 훈련하고 평가하기

  • loss 매개변수를 log에서 hinge로 바꾸면 서포트 벡터 머신(Support Vector Machine: SVM)문제를 푸는 모델이 만들어진다. 
  • SVM
    • 훈련 데이터의 클래스를 구분하는 경계선을 찾는 작업
  • hinge loss
    • 학습 데이터 각각의 범주를 구분하면서 데이터와의 거리가 가장 먼 결정경계(decision boundary)를 찾기 위해 고안된 손실 함수의 한 부류
    • 데이터와 경계 사이의 마진(margin)이 최대화된다. KSVMs이 - hinge loss 손실 함수 사용
  • 결정 경계
    • 두 개의 계층을 가지고 있는 통계적인 분류 문제에서 기본 벡터공간을 각 클래스에 대하여 하나씩 두 개의 집합으로 나누는 초표면

[그림1] 결정 경계 출처 : https://m.blog.naver.com/PostView.nhn?blogId=msnayana&logNo=220018903916&proxyReferer=https:%2F%2Fwww.google.com%2F

# 다른 손실 함수
# loss 같은 매개변수의 값은 가중치나 절편처럼 알아서 학습되는 것이 아니다.

# 즉 사용자가 '직접' 선택해야 한다. - 이런 값을 하이퍼파라미터(hypterparameter)
# loss 값 변경시 좋아지는지 확인
# SGDClassifier 클래스 loss 매개변수 log에서 hinge로 바꾸면 선형 서포트 벡터 머신(Support Vector Machine:SVM) 문제를 푸는 모델 만들어짐
# SVM - 훈련 데이터의 클래스를 구분하는 경계선을 찾는 작업
# hinge loss - 학습 데이터 각각의 범주를 구분하면서 데이터와의 거리가 가장 먼 결정경계(decision boundary)를 찾기 위해 고안된 손실 함수의 한 부류 
# 데이터와 경계 사이의 마진(margin)이 최대화된다. KSVMs이 - hinge loss 손실 함수 사용
# 결정 경계 - 두 개의 계층을 가지고 있는 통계적인 분류 문제에서 기본 벡터공간을 각 클래스에 대하여 하나씩 두 개의 집합으로 나누는 초표면

from sklearn.linear_model import SGDClassifier
sgd = SGDClassifier(loss='hinge', random_state=42)
sgd.fit(x_train_all, y_train_all)
sgd.score(x_test, y_test)
 # 0.93 = 93% 
 # SGDClassifier 클래스의 다른 매개변수들을 바꿔보는것을 - '모델을 튜닝한다.' 표현
 # 하지만 이러한 모델은 실전에서 좋은 성능을 내지 못할 확률이 높다.

[그림2] hinge loss 사용한 정확도

SGD클래스의 다른 매개변수들을 바꾸는 작업 - 모델을 튜닝한다.

모델은 실전에서 좋은 성능을 내지 못한 확률이 높다.

 

테스트 세트로 모델을 튜닝시 '테스트 세트에 대해서만 좋은 성능을 보여주는 모델' (자격증 시험)?

 

테스트 세트로 모델을 튜닝하면 테스트 세트의 정보가 모델에 새어 나가므로 모델의 일반화 성능(generalization performance)이 왜곡된다.

 

일반화 성능(generalization performance)

  • 우리가 학습시킨 데이터로 모델의 성능을 측정하면 완벽히 예측할 수 있기 때문에 의미가 없다.
  • 따라서 학습에 사용하지 않는 데이터를 가지고 모델의 성능을 측정해야 하고, 그렇게 측정한 성능을 일반화 성능

 

 

검증 세트 준비

  • 모델 튜닝시 테스트 세트를 사용하지 않으면 된다.
  • 모델 튜닝시 성능 점수가 필요
  • 테스트 세트는 모델 튜닝 모두 마치고 실전에 투입하기 전 딱 한 번만 사용하는 것이 좋다. (권장, 표준)
  • 모델 튜닝을 위한 세트 따로 준비 필수
  • 모델 튜닝 용도의 세트는 검증 세트(validation set)라고 하며 훈련 세트를 조금 떼어 만든다.

[그림3] 훈련 세트 비중

 

1. 데이터 세트 준비하기

  • 위스콘신 유방암 데이터 사용
# 1. 데이터 세트 준비

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

cancer = load_breast_cancer()
x = cancer.data
y = cancer.target

x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify=y,test_size=0.2, random_state=42)

 

2, 검증 세트 분할

  • 훈련, 검증, 테스트 세트는 6:2:2정도로 분할한다.
  • 하지만 실제 분할 작업은 전체 데이터 세트를 8:2 순련 세트와 테스트 세트를 만들고 다시 훈련 세트를 8:2로 나누어 훈련 세트와 검증 세트 만든다.
  • 전체 데이터 100 64:20:16
# 2. 검증 세트 분할
# 6:2:2정도로 분할 
# 실제로는 전체 데이터 세트를 8:2로 나누어 훈련 세트와 테스트 세트 만든 후 다시 훈련 세트를 8:2로 나누어 훈련 세트와 검증 세트 만든다.
# 100기준 64 : 20 : 16

x_train, x_val, y_train, y_val = train_test_split(x_train_all, y_train_all, stratify=y_train_all, test_size=0.2, random_state=42)
print(len(x_train), len(x_val))
# 364 91
# 455개 훈련 세트 8:2 비율 나누어져 훈련 세트(x_train)364, 검증 세트(x_val) 91

 

3. 검증 세트 사용해 모델 평가

  • 데이터 양이 적을 경우 검증 세트의 비율이나 random_state 매개변수의 값을 조금만 조절해도 성능 평가 점수가 크게 변한다.
  • 데이터 양이 너무 적은 경우 검증 세트를 나누지 않고 교차 검증(cross validation)이라는 방법도 사용
  • 일반적 10만개 정도 8:1:1, 딥러닝 100만개 이상 98:1:1 비율로 샘플을 나눈다.
# 3. 검증 세트 사용해 모델 평가

sgd = SGDClassifier(loss='log', random_state=42)
sgd.fit(x_train, y_train)
sgd.score(x_val, y_val)
# 69% 낮다.
# 변동이 심한 이유는 데이터의 양이 적으니 검증 세트 비율을 약간만 수정해도 성능 평가 점수가 크게 변한다.
# 그래서 데이터 양이 너무 적은 경우 검증 세트를 나누지 않고 교차 검증(cross validation)이라는 방법도 사용
# 일반적 10만개 데이터 8:1:1 분할
# 딥러닝은 더 많은 데이터를 사용하는 경우가 많아 100만개 이상 98:1:1

[그림4]  검증 세트 사용해 모델 평가

교차 검증(cross validation)

  • 일반적으로 train set으로 모델 훈련, test set 모델 검증하지만 
  • 고정된 testset을 통해 모델을 검증하고 수정하는 과정 반복시 결국 내가 만든 모델은 test set에만 잘 동작하는 모델
  • test_set에 과적합(overfit)하게 되므로 다른 실제 데이터를 가지고 예측 수행시 이상한 결과가 나와버린다.
  • 교차 검증은 train_set을 train_set + validation_set으로 분리한 뒤 validation_set 사용해 검증하는 방식
  • validation- 검증, 확인 

데이터 전처리와 특성 스케일

  • 패키지에 준비된 대부분 데이터는 실습을 위한 것이라 잘 가공되어 있다.
  • 실전 수집 데이터는 누락된 값이 있을 수도 있고, 데이터 형태가 균일하지 않을 수도 있다.
  • 이런 경우 데이터 적절히 가공하는 '데이터 전처리(data preprocessing)'과정이 필요하다.

 

특성의 스케일은 알고리즘에 영향을 준다.

  • 잘 정리된 데이터 전처리시 - 특성의 스케일(scale)이 다른 경우
  • 특성의 스케일 - 어떤 특성이 가지고 있는 값의 범위 

예)

  당도 무게
사과1 4 540
사과2 8 700
사과3 2 480
  • 당도 1~10 사과 무게 500~1,000 '두 특성의 스케일 차이가 크다'라고 말한다.
  • 경사 하강법은 스케일에 민감한 알고리즘 - 특성의 스케일을 맞추는등의 전처리 필수적.

 

 

 

 

스케일 조정하지 않고 모델 훈련

 

1. 훈련 데이터 준비하고 스케일 비교

  • mean perimeter, mean area특성
# 1. 훈련 데이터 준비하고 스케일 비교
import numpy as np
import matplotlib.pyplot as plt

print(cancer.feature_names[[2,3]])
plt.boxplot(x_train[:, 2:4])
plt.xlabel('feature')
plt.ylabel('value')
plt.show() # mean perimeter, mean area
# mean perimeter - 100~200
# mean area - 200 ~2,000 (특이값 2500?)

[그림5] 훈련 데이터 스케일,특성 비교

2. 가중치를 기록할 변수와 학습률 파라미터 추가

# 2. 가중치를 기록할 변수와 학습률 파라미터 추가
# SingleLayer 클래스에 인스턴스 변수 추가해 에포크마다 가중치 값 저장해 가중치의 변화를 관찰
# 학습률 개념 도입

def __init__(self, learning_rate = 0.1):
    self.w = None
    self.b = None
    self.losses = []
    self.w_history = [] # 가중치
    self.lr = learning_rate # 학습률 - 하이퍼파라미터 가중치 업데이트 양 조절

# 만약 가중치 큰 폭 업데이트시 손실 함수가 최소가 될 수 있는 지점 '전역 최솟값' 지나칠 시 
#최적의 해(최적의 가중치와 절편) 구할 수 없다. 전역 최솟값 놓치지 않게 가중치 업데이트 양 조절해야 한다.


  • learning_rate는 하이퍼파라미터 - '학습률'의미 
  • 이 값으로 업데이트 양 조절
  • 일반적으로 손실 함수는 복잡한 굴곡을 가진 다차원 공간의 초평면(hyperplane)이다.
  • 만약 가중치 큰 폭 업데이트시 손실 함수가 최소가 될 수 있는 지점 '전역 최솟값' 지나칠 시 최적의 해(최적의 가중치와 절편) 구할 수 없다. 전역 최솟값 놓치지 않게 가중치 업데이트 양 조절해야 한다. 

[그림6]  학습률 파라미터 

 

 

3. 가중치 기록하고 업데이트 양 조절

# 3. 가중치 기록하고 업데이트 양 조절

# fit() 메서드 가중치 바뀔 시  w_history에 가중치 기록

def fit(self, x, y, epochs=100):
    self.w = np.ones(x.shape[1]) # 가중치 초기화 0 
    self.b = 0 # 절편 초기화
    self.w_histroy.append(self.w.copy()) # 가중치 기록
    np.random.seed(42) # 무작위 시드 지정
    for i in range(epochs): 
        loss = 0
        indexes = np.random.permutation(np.arange(len(x)))# 인덱스 섞기
        for i in indexes :  # 모든 샘플 반복
            z = self.forpass(x[i]) # 정방향 계산
            a = self.activation(z) # 활성화 함수 적용
            err = -(y[i] - a) # 오차 계산
            w_grad, b_grad = self.backprop(x[i], err) # 역방향 계산
            self.w -= self.lr * w_grad # 가중치 업데이트(학습률 적용))
            self. b -= b_grad # 절편 업데이트
            self.w_history.append(self.w.copy()) # 가중치 기록
            a = np.clip(a, 1e-10, 1-1e-10) # 안전한 로그 게산 위해 클리핑 후 손실 누적
            loss += -(y[i]*np.log(a) + (1-y[i])*np.log(1-a)) 
        self.losses.append(loss/len(y)) # 에포크마다 손실 저장

 

# 3. 전체 코드
class SingleLayer:
    
    def __init__(self, learning_rate=0.1, l1=0, l2=0):
        self.w = None
        self.b = None
        self.losses = []
        self.val_losses = []
        self.w_history = []
        self.lr = learning_rate
        self.l1 = l1
        self.l2 = l2

    def forpass(self, x):
        z = np.sum(x * self.w) + self.b    # 직선 방정식을 계산합니다
        return z

    def backprop(self, x, err):
        w_grad = x * err          # 가중치에 대한 그래디언트를 계산합니다
        b_grad = 1 * err    # 절편에 대한 그래디언트를 계산합니다
        return w_grad, b_grad

    def activation(self, z):
        z = np.clip(z, -100, None) # 안전한 np.exp() 계산을 위해
        a = 1 / (1 + np.exp(-z))  # 시그모이드 계산
        return a
        
    def fit(self, x, y, epochs=100, x_val=None, y_val=None):
        self.w = np.ones(x.shape[1])               # 가중치를 초기화합니다.
        self.b = 0                                 # 절편을 초기화합니다.
        self.w_history.append(self.w.copy())       # 가중치를 기록합니다.
        np.random.seed(42)                         # 랜덤 시드를 지정합니다.
        for i in range(epochs):                    # epochs만큼 반복합니다.
            loss = 0
            # 인덱스를 섞습니다
            indexes = np.random.permutation(np.arange(len(x)))
            for i in indexes:                      # 모든 샘플에 대해 반복합니다
                z = self.forpass(x[i])             # 정방향 계산
                a = self.activation(z)             # 활성화 함수 적용
                err = -(y[i] - a)                  # 오차 계산
                w_grad, b_grad = self.backprop(x[i], err) # 역방향 계산
                # 그래디언트에서 페널티 항의 미분 값을 더합니다
                w_grad += self.l1 * np.sign(self.w) + self.l2 * self.w
                self.w -= self.lr * w_grad         # 가중치 업데이트
                self.b -= b_grad                   # 절편 업데이트
                # 가중치를 기록합니다.
                self.w_history.append(self.w.copy())
                # 안전한 로그 계산을 위해 클리핑한 후 손실을 누적합니다
                a = np.clip(a, 1e-10, 1-1e-10)
                loss += -(y[i]*np.log(a)+(1-y[i])*np.log(1-a))
            # 에포크마다 평균 손실을 저장합니다
            self.losses.append(loss/len(y) + self.reg_loss())
            # 검증 세트에 대한 손실을 계산합니다
            self.update_val_loss(x_val, y_val)
    
    def predict(self, x):
        z = [self.forpass(x_i) for x_i in x]     # 정방향 계산
        return np.array(z) >= 0                   # 스텝 함수 적용
    
    def score(self, x, y):
        return np.mean(self.predict(x) == y)
    
    def reg_loss(self):
        return self.l1 * np.sum(np.abs(self.w)) + self.l2 / 2 * np.sum(self.w**2)
    
    def update_val_loss(self, x_val, y_val):
        if x_val is None:
            return
        val_loss = 0
        for i in range(len(x_val)):
            z = self.forpass(x_val[i])     # 정방향 계산
            a = self.activation(z)         # 활성화 함수 적용
            a = np.clip(a, 1e-10, 1-1e-10)
            val_loss += -(y_val[i]*np.log(a)+(1-y_val[i])*np.log(1-a))
        self.val_losses.append(val_loss/len(y_val) + self.reg_loss())

 

4. 모델 훈련하고 평가

# 4. 모델 훈련하고 평가
layer1 = SingleLayer()
layer1.fit(x_train, y_train)
layer1.score(x_val, y_val)
# 91%

 

5. layer1 객체 100번의 에포크 변경 가중치 모두 기록

# 5. layer1 객체 변수 그래프

w2 = []
w3 = []
for w in layer1.w_history:
    w2.append(w[2])
    w3.append(w[3])

plt.plot(w2, w3)
plt.plot(w2[-1],w3[-1],'ro') # 최종 결정 가중치
plt.xlabel('w[2]')
plt.ylabel('w[3]')
plt.show()

[그림7] 스케일 조정하지 않고 그래프 그리기

빨간 점이 최종 가중치

mean perimeter에 비해 mean area의 스케일이 크므로 w3의 값이 학습 과정에서 큰 폭으로 흔들리며 변화

w2의 값은 0부터 시작해 조금씩 최적값에 가까워진다. 

'w3에 대한 기울기가 크기 대문에 w3 축을 따라 가중치가 크게 요동치고 있다.'

이런 현상 줄이는게 스케일 조정

 

 

스케일 조정해 모델 훈련

  • 스케일 조정 방법 중 하나 표준화(standardization)
  • 표준화
    • 특성값에서 평균 빼고 표준 편차로 나누기
    • 평균이 0이고 분산이 1인 특성 만들어진다.

[그림8] 표준화 공식
[그림9] 표준 편차 공식

표준 편차(standard deviation)

  • 데이터가 얼마나 흩어져 있는지 나타내는 지표, 분산(variance)의 제곱근
  • m은 샘플 수 y?는 평균

 

1. 넘파이로 표준화 구현

# 1. 넘파이로 표준화 구현
# mean(), std() 함수 사용해 평균과 표준 편차 계산
# axis 매개변수 0 지정시 2차원 배열의 열을 기준으로 통계치 계산해 하나의 행 벡터로 반환
# 넘파이 브로드캐스팅으로 모든 원소에 산술 연산 적용

train_mean = np.mean(x_train, axis = 0) # 평균
train_std = np.std(x_train, axis = 0) # 표준 편차
x_train_scaled = (x_train - train_mean) /train_std

 

2. 모델 훈련하기

# 2. 모델 훈련하기

layer2 = SingleLayer()
layer2.fit(x_train_scaled, y_train)
w2 = []
w3 = []
for w in layer2.w_history:
    w2.append(w[2])
    w3.append(w[3])

plt.plot(w2, w3)
plt.plot(w2[-1], w3[-1], 'ro')
plt.xlabel('w[2]')
plt.ylabel('w[3]')
plt.show()
# w2와 w3의 변화 비율이 비슷해 대각선 방향으로 가중치 이동
# 최적값에 빠르게 근접

[그림10] 스케일 조정 후 그래프

 

3. 모델 성능 평가

# 3. 모델 성능 평가하기

layer2.score(x_val, y_val) #37% 성능 나쁨

[그림11] 모델 평가

 

4. 성능이 나쁜 이유

  • 훈련 세트와 검증 세트의 스케일이 비슷하다고 예상했다. 
  • 하지만 검증 세트의 스케일이 비슷하지 않아 검증 세트의 스케일을 바꾸지 않았기 때문에 성능이 좋지 않다.
#4. 성능이 나쁜 이유
# 훈련 세트와 검증 세트의 스케일이 비슷하다고 예상했다.
# 하지만 섬증 세트의 스케일이 비슷하지 않아 검증 세트의 스케일을 바꾸지 않았기 때문에 성능이 좋지 않다.

val_mean = np.mean(x_val, axis = 0)
val_std = np.std(x_val, axis = 0)
x_val_scaled = (x_val - val_mean) / val_std
layer2.score(x_val_scaled, y_val) # 96%

[그림12] 검증 세트 스케일 조정

 

스케일 조정 후 실수하기 쉬운 함정 

 

1, 원본 훈련 세트와 검증 세트 산점도 그리기

# 1. 원본 훈련 세트와 검증 세트 산점도 그리기

plt.plot(x_train[:50, 0], x_train[:50, 1], 'bo') # 파란색 훈련세트
plt.plot(x_val[:50, 0], x_val[:50, 1],'ro') # 빨간색 검증 세트
plt.xlabel('fature 1')
plt.ylabel('fature 2')
plt.legend(['train set', 'val.set'])
plt.show()

[그림13] 원본 훈련 세트 검증 세트 산점도 그리기

 

2. 전처리한 훈련 세트와 검증 세트로 산점도 그리기

# 2. 전처리한 훈련 세트와 검증 세트로 산점드 그리기

plt.plot(x_train_scaled[:50, 0], x_train_scaled[:50, 1], 'bo')
plt.plot(x_val_scaled[:50, 0], x_val_scaled[:50, 1], 'ro')
plt.xlabel('fature1')
plt.ylabel('fature2')
plt.legend(['train set','val.set'])
plt.show()
# 훈련 세트와 검증 세트가 각각 다른 비율로 변환
# 원본 훈련 세트와 검증 세트의 점과 점 사이의 거리가 변환된 이후 그대로 유지되지 않았다.
# 제대로 전처리(스케일 조정)했다면 훈련 세트와 검증 세트읙 거리가 그대로 유지
# 점과 점 사이 거리가 달라진 이유 훈련 세트와 검증 세트 각각 다른 비율로 전처리


 

  • 훈련 세트와 검증 세트가 각각 다른 비율로 변환
  • 원본 훈련 세트와 검증 세트의 점과 점 사이의 거리가 변환된 이후 그대로 유지되지 않았다.
  • 제대로 전처리(스케일 조정)했다면 훈련 세트와 검증 세트읙 거리가 그대로 유지
  • 점과 점 사이 거리가 달라진 이유 훈련 세트와 검증 세트 각각 다른 비율로 전처리

[그림14] 전처리 훈련 세트 검증 세트 산점도

 

3. 올바르게 검증 세트 전처리

  • 검증 세트의 스케일이 훈련 세트의 스케일과 다른 비율로 조정되면 모델에 적용된 알고리즘들이 검증 세트의 샘플 데이터를 잘못 인식한다.
  • 검증 세트를 훈련 세트와 같은 비율로 전처리
  • 실전에서는 샘플 하나에 대한 예측값을 만들기 때문에 전처리를 위해 평균이나 표준 편차 계산 불가능
  • 같은 비율로 검증 세트 변환 - 훈련 세트의 평균, 표준 편차 사용해 검증 세트 변환
# 3. 올바르게 검증 세트 전처리하기
# 검증 세트의 스케일이 훈련 세트의 스케일과 다른 비율로 조정되면 모델에 적용된 알고리즘들이 검증 세트의 샘플 데이터 잘못 인식한다.
# 검증 세트를 훈련 세트와 같은 비율로 전처리

# 실전에서는 샘플 하나에 대한 예측값을 만들기 때문에 전처리를 위해 평균이나 표준 편차 계산 불가능
# 같은 비율로 검증 세트 변환 - 훈련 세트의 평균, 표준 편차 사용해 검증 세트 변환

x_val_scaled = (x_val - train_mean) / train_std

plt.plot(x_train_scaled[:50, 0], x_train_scaled[:50, 1], 'bo')
plt.plot(x_val_scaled[:50, 0], x_val_scaled[:50, 1],'ro')
plt.xlabel('fature1')
plt.ylabel('fature2')
plt.legend(['train set', 'val.set'])
plt.show()

# 원본 데이터 산점도와 스케일 조정 이후 산점도 같아짐 - 검증 세트와 훈련 세트가 동일한 비율로 변환

[그림15] 올바르게 검증 세트 전처리

 

 

4. 모델 평가

# 4. 모델 평가

layer2.score(x_val_scaled, y_val) # 96%
#  데이터 세트가 크지 않기 대문에 검증 세트 전처리 전과 후의 성능이 동일하다.
# 동일한 개수 샘플 올바르게 예측

[그림16] 모델 평가

 

728x90
반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기