59doit

다중분류 신경망(1) 본문

인공지능

다중분류 신경망(1)

yul_S2 2023. 1. 12. 10:07
반응형

다중분류의 경사하강법은 이진분류의 경사하강법과 원리는 같고
소프트맥스 함수가 추가된 점만 다름

 

 

 

소프트맥스 함수 추가

 

# 필요한 모듈 및 임포트 

import numpy as np

 

 

# MultiClassNetwork 뉴런생성

class MultiClassNetwork:
 
    def __init__(self, units=10, batch_size=32, learning_rate=0.1, l1=0, l2=0):
        self.units = units  # 은닉층의 뉴런 개수
        self.batch_size = batch_size  # 배치 크기
        self.w1 = None  # 은닉층의 가중치
        self.b1 = None  # 은닉층의 절편
        self.w2 = None  # 출력층의 가중치
        self.b2 = None  # 출력층의 절편
        self.a1 = None  # 은닉층의 활성화 출력
        self.losses = []  # 훈련 손실
        self.val_losses = []  # 검증 손실
        self.lr = learning_rate  # 학습률
        self.l1 = l1  # L1 손실 하이퍼파라미터
        self.l2 = l2  # L2 손실 하이퍼파라미터

 

 

# 정방향계산하기

def forpass(self, x):
        z1 = np.dot(x, self.w1) + self.b1  # 첫 번째 층의 선형 식을 계산합니다
        self.a1 = self.sigmoid(z1)  # 활성화 함수를 적용합니다
        z2 = np.dot(self.a1, self.w2) + self.b2  # 두 번째 층의 선형 식을 계산합니다.
        return z2

▶activation 메서드 이름을 sigmoid 로 바꿨으니 self.a1 = self.sigmoid 로 바꿔준다

 

 

 

def sigmoid(self, z):
        z = np.clip(z, -100, None)  # 안전한 np.exp() 계산을 위해
        a = 1 / (1 + np.exp(-z))  # 시그모이드 계산
        return a

▶ 다중분류에서는 마지막 출력층에 소프트맥스 함수를 사용해야하므로
    은닉층과 출력층에 다른 함수를 적용해야 한다
    activation -> sigmoid 함수로 변경

 

 

# 소프트맥스 함수 추가

 def softmax(self, z):
        # 소프트맥스 함수
        z = np.clip(z, -100, None)  # 안전한 np.exp() 계산을 위해
        exp_z = np.exp(z)
        return exp_z / np.sum(exp_z, axis=1).reshape(-1, 1)

▶ z의 각 행의 합을 계산 
    형태는 행 벡터이므로 형태를 열벡터로 바꿔야 나눗셈에 적용할 수 있다.

 

 

# 가중치추가하기

def init_weights(self, n_features, n_classes):
        self.w1 = np.random.normal(0, 1, (n_features, self.units))  # (특성 개수, 은닉층의 크기)
        self.b1 = np.zeros(self.units)  # 은닉층의 크기
        self.w2 = np.random.normal(0, 1, (self.units, n_classes))  # (은닉층의 크기, 클래스 개수)
        self.b2 = np.zeros(n_classes)

▶ 출력층의 뉴런이 2개이상이므로 가중치 w2의 크기는 (은닉층의 크기, 클래스 개수) 가 된다
     random.normal 함수를 이용하여 배열의 각 원소의 값을
     정규분포를 따르는 무작위 수로 초기화한다.

 b2의 크기는 클래스 개수에 따라 지정한다
     b2는 0으로 초기화한다.

 

 

# 훈련의 진행상황을 살펴볼 수 있도록 에포크마다'.' 출력되도록한다.

# epochs만큼 반복합니다.
        for i in range(epochs):
            loss = 0
            print('.', end='')

 

 

# training메서드

def training(self, x, y):
        m = len(x)  # 샘플 개수를 저장합니다.
        z = self.forpass(x)  # 정방향 계산을 수행합니다.
        a = self.softmax(z)  # 활성화 함수를 적용합니다.
        err = -(y - a)  # 오차를 계산합니다.
        # 오차를 역전파하여 그래디언트를 계산합니다.
        w1_grad, b1_grad, w2_grad, b2_grad = self.backprop(x, err)
        # 그래디언트에서 페널티 항의 미분 값을 뺍니다
        w1_grad += (self.l1 * np.sign(self.w1) + self.l2 * self.w1) / m
        w2_grad += (self.l1 * np.sign(self.w2) + self.l2 * self.w2) / m
        # 은닉층의 가중치와 절편을 업데이트합니다.
        self.w1 -= self.lr * w1_grad
        self.b1 -= self.lr * b1_grad
        # 출력층의 가중치와 절편을 업데이트합니다.
        self.w2 -= self.lr * w2_grad
        self.b2 -= self.lr * b2_grad
        return a

▶activation -> softmax 함수로 변경

 

 

 

# predict메서드

def predict(self, x):
        z = self.forpass(x)  # 정방향 계산을 수행합니다.
        return np.argmax(z, axis=1)  # 가장  값의 인덱스를 반환합니다.

▶ 정방향 계산에서 얻은 출력 중 가장 큰 값의 인덱스를 구한다. 이값이 예측 클래스가 된다.
    클래스를 예측 할 때는 활성화 함수, 소프트 맥스 함수를 거칠 필요가 없다.
    출력층에서 계산된 선형 계산만으로 클래스를 예측 할 수 있다.

 

 

# sore메서드

    def score(self, x, y):
        # 예측과 타깃  벡터를 비교하여 True 비율을 반환합니다.
        return np.mean(self.predict(x) == np.argmax(y, axis=1))
 
    def reg_loss(self):
        # 은닉층과 출력층의 가중치에 규제를 적용합니다.
        return self.l1 * (np.sum(np.abs(self.w1)) + np.sum(np.abs(self.w2))) + \
               self.l2 / 2 * (np.sum(self.w1 ** 2) + np.sum(self.w2 ** 2))

▶ predict결과와 타킷 y의 클래스를 비교한다. 
    배열 y의 행을 따라 가장 큰 값의 인덱스를 구해 사용한다.

 

 

# 검증 손실 계산하기

 def update_val_loss(self, x_val, y_val):
        z = self.forpass(x_val)  # 정방향 계산을 수행합니다.
        a = self.softmax(z)  # 활성화 함수를 적용합니다.
        a = np.clip(a, 1e-10, 1 - 1e-10)  # 출력 값을 클리핑합니다.
        # 크로스 엔트로피 손실과 규제 손실을 더하여 리스트에 추가합니다.
        val_loss = np.sum(-y_val * np.log(a))
        self.val_losses.append((val_loss + self.reg_loss()) / len(y_val))

▶ 사용하는 활성화 함수를 softmax로 바꾼다
    로지스틱 손실 계산을 크로스 엔트로피 손실 계산으로 바꾼다.

 

 

# 최종코드

class MultiClassNetwork:
 
    def __init__(self, units=10, batch_size=32, learning_rate=0.1, l1=0, l2=0):
        self.units = units  # 은닉층의 뉴런 개수
        self.batch_size = batch_size  # 배치 크기
        self.w1 = None  # 은닉층의 가중치
        self.b1 = None  # 은닉층의 절편
        self.w2 = None  # 출력층의 가중치
        self.b2 = None  # 출력층의 절편
        self.a1 = None  # 은닉층의 활성화 출력
        self.losses = []  # 훈련 손실
        self.val_losses = []  # 검증 손실
        self.lr = learning_rate  # 학습률
        self.l1 = l1  # L1 손실 하이퍼파라미터
        self.l2 = l2  # L2 손실 하이퍼파라미터
    """
    <정방향 계산하기>
    activation 메서드 이름을 sigmoid 로 바꿨으니
    self.a1 = self.sigmoid 로 바꿔준다
    """
    def forpass(self, x):
        z1 = np.dot(x, self.w1) + self.b1  # 첫 번째 층의 선형 식을 계산합니다
        self.a1 = self.sigmoid(z1)  # 활성화 함수를 적용합니다
        z2 = np.dot(self.a1, self.w2) + self.b2  # 두 번째 층의 선형 식을 계산합니다.
        return z2
 
    def backprop(self, x, err):
        m = len(x)  # 샘플 개수
        # 출력층의 가중치와 절편에 대한 그래디언트를 계산합니다.
        w2_grad = np.dot(self.a1.T, err) / m
        b2_grad = np.sum(err) / m
        # 시그모이드 함수까지 그래디언트를 계산합니다.
        err_to_hidden = np.dot(err, self.w2.T) * self.a1 * (1 - self.a1)
        # 은닉층의 가중치와 절편에 대한 그래디언트를 계산합니다.
        w1_grad = np.dot(x.T, err_to_hidden) / m
        b1_grad = np.sum(err_to_hidden, axis=0) / m
        return w1_grad, b1_grad, w2_grad, b2_grad
    """
    다중분류에서는 마지막 출력층에 소프트맥스 함수를 사용해야하므로
    은닉층과 출력층에 다른 함수를 적용해야 한다
    activation -> sigmoid 함수로 변경
    """
    def sigmoid(self, z):
        z = np.clip(z, -100, None)  # 안전한 np.exp() 계산을 위해
        a = 1 / (1 + np.exp(-z))  # 시그모이드 계산
        return a
    """
    <소트트맥스 함수 추가하기>
    softmax 추가
    """
    def softmax(self, z):
        # 소프트맥스 함수
        z = np.clip(z, -100, None)  # 안전한 np.exp() 계산을 위해
        exp_z = np.exp(z)
        """
        z의 각 행의 합을 계산
        형태는 행 벡터이므로 형태를 열벡터로 바꿔야 나눗셈에 적용할 수 있다.
        """
        return exp_z / np.sum(exp_z, axis=1).reshape(-1, 1)
 
    """
    <가중치 초기화하기>
    """
    def init_weights(self, n_features, n_classes):
        self.w1 = np.random.normal(0, 1, (n_features, self.units))  # (특성 개수, 은닉층의 크기)
        self.b1 = np.zeros(self.units)  # 은닉층의 크기
        """
        출력층의 뉴런이 2개이상이므로 가중치 w2의 크기는 (은닉층의 크기, 클래스 개수) 가 된다
        random.normal 함수를 이용하여 배열의 각 원소의 값을
        정규분포를 따르는 무작위 수로 초기화한다.
        """
        self.w2 = np.random.normal(0, 1, (self.units, n_classes))  # (은닉층의 크기, 클래스 개수)
        """
        b2의 크기는 클래스 개수에 따라 지정한다
        b2 0으로 초기화한다.
        """
        self.b2 = np.zeros(n_classes)
 
    def fit(self, x, y, epochs=100, x_val=None, y_val=None):
        np.random.seed(42)
        self.init_weights(x.shape[1], y.shape[1])  # 은닉층과 출력층의 가중치를 초기화합니다.
        # epochs만큼 반복합니다.
        for i in range(epochs):
            loss = 0
            """
            훈련의 진행 상황을 살펴복 수 있도록 에포크마다 '.'이 출력되도록한다
            """
            print('.', end='')
            # 제너레이터 함수에서 반환한 미니배치를 순환합니다.
            for x_batch, y_batch in self.gen_batch(x, y):
                a = self.training(x_batch, y_batch)
                # 안전한 로그 계산을 위해 클리핑합니다.
                a = np.clip(a, 1e-10, 1 - 1e-10)
                # 로그 손실과 규제 손실을 더하여 리스트에 추가합니다.
                loss += np.sum(-y_batch * np.log(a))
            self.losses.append((loss + self.reg_loss()) / len(x))
            # 검증 세트에 대한 손실을 계산합니다.
            self.update_val_loss(x_val, y_val)
 
    # 미니배치 제너레이터 함수
    def gen_batch(self, x, y):
        length = len(x)
        bins = length // self.batch_size  # 미니배치 횟수
        if length % self.batch_size:
            bins += 1  # 나누어 떨어지지 않을 때
        indexes = np.random.permutation(np.arange(len(x)))  # 인덱스를 섞습니다.
        x = x[indexes]
        y = y[indexes]
        for i in range(bins):
            start = self.batch_size * i
            end = self.batch_size * (i + 1)
            yield x[start:end], y[start:end]  # batch_size만큼 슬라이싱하여 반환합니다.
 
    def training(self, x, y):
        m = len(x)  # 샘플 개수를 저장합니다.
        z = self.forpass(x)  # 정방향 계산을 수행합니다.
        """
        <training메서드>
        activation -> softmax 함수로 변경
        """
        a = self.softmax(z)  # 활성화 함수를 적용합니다.
        err = -(y - a)  # 오차를 계산합니다.
        # 오차를 역전파하여 그래디언트를 계산합니다.
        w1_grad, b1_grad, w2_grad, b2_grad = self.backprop(x, err)
        # 그래디언트에서 페널티 항의 미분 값을 뺍니다
        w1_grad += (self.l1 * np.sign(self.w1) + self.l2 * self.w1) / m
        w2_grad += (self.l1 * np.sign(self.w2) + self.l2 * self.w2) / m
        # 은닉층의 가중치와 절편을 업데이트합니다.
        self.w1 -= self.lr * w1_grad
        self.b1 -= self.lr * b1_grad
        # 출력층의 가중치와 절편을 업데이트합니다.
        self.w2 -= self.lr * w2_grad
        self.b2 -= self.lr * b2_grad
        return a
    """
    <predict메서드>
    정방향 계산에서 얻은 출력 중 가장 큰 값의 인덱스를 구한다. 이값이 예측 클래스가 된다.
    클래스를 예측 할 때는 활성화 함수, 소프트 맥스 함수를 거칠 필요가 없다.
    출력층에서 계산된 선형 계산만으로 클래스를 예측 할 수 있다.
    """
    def predict(self, x):
        z = self.forpass(x)  # 정방향 계산을 수행합니다.
        return np.argmax(z, axis=1)  # 가장 큰 값의 인덱스를 반환합니다.
    """
    <sore메서드>
    predict결과와 타킷 y의 클래스를 비교한다.
    배열 y의 행을 따라 가장 큰 값의 인덱스를 구해 사용한다.
    """
    def score(self, x, y):
        # 예측과 타깃 열 벡터를 비교하여 True의 비율을 반환합니다.
        return np.mean(self.predict(x) == np.argmax(y, axis=1))
 
    def reg_loss(self):
        # 은닉층과 출력층의 가중치에 규제를 적용합니다.
        return self.l1 * (np.sum(np.abs(self.w1)) + np.sum(np.abs(self.w2))) + \
               self.l2 / 2 * (np.sum(self.w1 ** 2) + np.sum(self.w2 ** 2))
    """
    <검증 손실 계산하기>
    사용하는 활성화 함수를 softmax로 바꾼다
    로지스틱 손실 계산을 크로스 엔트로피 손실 계산으로 바꾼다.
    """
    def update_val_loss(self, x_val, y_val):
        z = self.forpass(x_val)  # 정방향 계산을 수행합니다.
        a = self.softmax(z)  # 활성화 함수를 적용합니다.
        a = np.clip(a, 1e-10, 1 - 1e-10)  # 출력 값을 클리핑합니다.
        # 크로스 엔트로피 손실과 규제 손실을 더하여 리스트에 추가합니다.
        val_loss = np.sum(-y_val * np.log(a))
        self.val_losses.append((val_loss + self.reg_loss()) / len(y_val))

 

 

반응형

'인공지능' 카테고리의 다른 글

다중분류 신경망(3)  (8) 2023.01.13
다중분류 신경망(2)  (0) 2023.01.12
다층신경망(2) 미니배치  (7) 2023.01.11
다층신경망(1)  (3) 2023.01.11
신경망 알고리즘  (7) 2023.01.10
Comments