Developer's Development

3.2.16 [딥러닝] 인공신경망 본문

데이터 분석과 머신러닝, 딥러닝/딥러닝

3.2.16 [딥러닝] 인공신경망

mylee 2025. 8. 3. 18:56
퍼셉트론

 

생물학적 뉴런을 모방한 간단한 인공지능 모델이다.

퍼셉트론은 논리 게이트를 모방할 수 있는 인공 신경망의 한 종류이다.

가중치와 편향을 통해 논리 게이트와 같은 동작을 한다.

 

  • AND/NAND/OR 게이트

AND, NAND, OR 게이트는 퍼셉트론의 기본적인 논리 게이트를 구현한다. 각각의 게이트는 입력값에 대해 일정한 규칙을 적용하여 특정 조건을 만족할 때만 출력을 반환한다.

- AND 게이트 : 두 입력이 모두 1일 때만 출력이 1이 된다. 퍼셉트론 모델에서는 가중치와 편향 값을 설정하여 이를 구현할 수 있다.

- NAND 게이트 : AND 게이트의 출력을 뒤집은 형태로, 두 입력이 모두 1일 때만 출력이 0이 된다.

- OR 게이트 : 두 입력 중 하나라도 1이면 출력이 1이 된다.

import numpy as np
import matplotlib.pyplot as plt

class Perceptron:
    def __init__(self, weights, bias):
        self.weights = weights
        self.bias = bias

	# perceptron 연산
    def activate(self, x):
        return 1 if np.dot(self.weights, x) + self.bias > 0 else 0
        
test_case = [(0, 0), (0, 1), (1, 0), (1, 1)]

AND_gate = Perceptron(weights=[0.5, 0.5], bias=-0.7)
for test in test_case:
    print(f"input: {test} | output: {AND_gate.activate(test)}")
    
OR_gate = Perceptron(weights=[0.5, 0.5], bias=-0.2)
for test in test_case:
    print(f"input: {test} | output: {OR_gate.activate(test)}")
    
NAND_gate = Perceptron(weights=[-0.5, -0.5], bias=0.7)
for test in test_case:
    print(f"input: {test} | output: {NAND_gate.activate(test)}")
    
# 시각화
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
gates = {
    "AND": AND_gate,
    "OR": OR_gate,
    "NAND": NAND_gate
}

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for idx, (gate_name, perceptron) in enumerate(gates.items()):
    ax = axes[idx]
    outputs = np.array([perceptron.activate(x) for x in inputs])

	# 각 데이터포인트 시각화
    for (x1, x2), y in zip(inputs, outputs):
        ax.scatter(x1, x2, c='red' if y==1 else 'blue', s=100, edgecolor='black')

	# 결정경계 시각화
    x_vals = np.linspace(-0.1, 1.1, 100)
    y_vals = (-perceptron.weights[0] * x_vals - perceptron.bias) / perceptron.weights[1]
    ax.plot(x_vals, y_vals, 'k--')

    ax.set_xlim(-0.1, 1.1)
    ax.set_ylim(-0.1, 1.1)
    ax.set_xticks([0, 1])
    ax.set_yticks([0, 1])
    ax.set_title(f'{gate_name} Gate')
    ax.set_xlabel('x1')
    ax.set_ylabel('x2')

plt.show()

 

  • XOR Gate
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])

plt.figure(figsize=(4, 4))
plt.scatter(X[:, 0], X[:, 1], c=y, s=200, edgecolors='black', cmap='coolwarm')
plt.show()

 

  • 가중치와 편향 구현

가중치 : 입력에 대해 얼마나 중요한지 결정하는 값

편향 : 출력 값을 조정하는 역할 (이를 통해 퍼셉트론의 결정을 유연하게 만듦)

 

  • 선형과 비선형 모델

퍼셉트론은 선형 모델이다. (입력값과 가중치의 선형 결합에 의해 출력 결정)

→ 단층 퍼셉트론만으로는 XOR과 같은 비선형 문제를 해결할 수 없다.

▶ 이를 극복하기 위해 비선형 활성화 함수와 다층 구조가 필요하다.

 

  • 다층 퍼셉트론 (Multi-Layer Preceptron, MLP)

여러 개의 퍼셉트론을 연결하여 비선형 문제를 해결한다. 입력층, 은닉층, 출력층을 가지고 있으며 은닉층에서 비선형 활성화 함수를 사용하여 복잡한 패턴을 학습할 수 있다.

각 층은 활성화 함수를 사용하여 출력을 계산한다.

# 여러 개 퍼셉트론 조합으로 XOR 문제 해결해보기
def XOR_gate(x1, x2):
    or_out = OR_gate.activate((x1, x2))
    nand_out = NAND_gate.activate((x1, x2))
    return AND_gate.activate((nand_out, or_out))

for test in test_case:
    print(f"input: {test} | output: {XOR_gate(test[0], test[1])}")
from sklearn.neural_network import MLPClassifier

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])

mlp = MLPClassifier(
    hidden_layer_sizes=(4, 4),  # 은닉층 크기
    activation='relu',          # 활성화 함수
    solver='adam',              # 가중치 업데이트 방식(최적화 알고리즘)
    max_iter=1000,              # 모델의 학습 반복 횟수(epoch)
    random_state=42             # 가중치 초기화 값 고정
)

mlp.fit(X, y)
pred = mlp.predict(X)

for i in range(4):
    print(f"XOR({X[i][0]}, {X[i][1]}) = {pred[i]}")
# 은닉층 크기에 따른 결과 확인
hidden_layers = [(1,), (2,), (4,), (8,)]

for config in hidden_layers:
    mlp = MLPClassifier(
        hidden_layer_sizes=config,  # 은닉층 크기
        activation='relu',          # 활성화 함수
        solver='adam',              # 가중치 업데이트 방식(최적화 알고리즘)
        max_iter=1000,              # 모델의 학습 반복 횟수(epoch)
        random_state=42             # 가중치 초기화 값 고정
    )

    mlp.fit(X, y)
    pred = mlp.predict(X)

    print(f"은닉층 구조 {config} -> XOR 예측 결과: {pred}")
    
"""
은닉층 구조 (1,) -> XOR 예측 결과: [0 1 0 1]
은닉층 구조 (2,) -> XOR 예측 결과: [1 1 1 1]
은닉층 구조 (4,) -> XOR 예측 결과: [0 0 0 0]
은닉층 구조 (8,) -> XOR 예측 결과: [0 1 1 0]
"""

 

 

 

활성화 함수

 

import numpy as np
import matplotlib.pyplot as plt

# 시각화를 위한 X 값
X = np.linspace(-5, 5, 100)

 

  • 시그모이드 함수

출력값을 0과 1 사이로 압축하는 비선형 함수이다. 주로 이진 분류에서 사용된다.

단, 시그모이드 함수는 경사하강법을 사용하여 학습할 때 기울기가 매우 작은 값으로 변할 수 있어, 학습이 느려지는 기울기 소실 (Vanishing Gradient) 문제가 발생할 수 있다.

매끄러운 S자 곡선 형태로 확률 해석이 가능하다.

def sigmoid(x):
    return 1 / (1 + np.exp(-x))
    
plt.figure(figsize=(5, 3))
plt.plot(X, sigmoid(X))
plt.title('Sigmoid Function')
plt.grid()
plt.ylim(-0.1, 1.1)
plt.show()

 

  • 계단 함수

입력값이 0(일정 임계값)보다 크면 1, 작거나 같으면 0을 반환하는 함수이다. 퍼셉트론에서 출력층으로 사용된다.

비선형 문제를 해결할 수 있지만, 미분이 불가능하여 신경망 학습에 사용하기 어렵다.

작은 변화에도 값이 급격히 변하여 역전파 학습에 부적합하다.

def step_function(x):
    return np.where(x >= 0, 1, 0)
    
plt.figure(figsize=(5, 3))
plt.plot(X, step_function(X))
plt.title('Step Function')
plt.grid()
plt.ylim(-0.1, 1.1)
plt.show()

 

  • ReLU 함수

ReLU(Rectified Linear Unit)는 입력값이 음수나 0이면 0으로 변환하고, 양수는 그대로 출력한다. 비선형성을 제공하면서도 계산이 간단하여 딥러닝에서 자주 사용된다.

시그모이드에서 발생할 수 있는 기울기 소실 문제를 해결하고 학습 속도를 빠르게 만든다.

음수 입력에 대해 0을 출력하여 계산이 간단하고 학습이 빠르지만, 죽은 뉴런(Dying ReLU) 문제가 발생할 수 있다.

def relu(x):
    return np.maximum(0, x)
    
plt.figure(figsize=(5, 3))
plt.plot(X, relu(X))
plt.title('ReLU Function')
plt.grid()
plt.show()

 

  • Leaky ReLU (Leaky Rectified Linear Unit)

ReLU의 변형된 형태로, ReLU의 단점 죽은 뉴런 문제를 해결하기 위해 고안되었다.

음수 입력에 작은 기울기 alpha를 적용한다. (일반적으로 0.01을 사용)

def leaky_relu(x, alpha=0.01):
    return np.where(x >= 0, x, alpha * x)

plt.figure(figsize=(5, 3))
plt.plot(X, leaky_relu(X, alpha=0.1))
plt.title('Leaky ReLU Function')
plt.grid()
plt.show()

 

 

  • Tanh (Hyperbolic Tangent)

시그모이드(Sigmoid) 함수의 변형된 형태로, 출력 범위가 -1에서 1까지 확장된 함수이다.

시그모이드보다 중심이 0에 가까워 더 빠른 학습 진행이 가능하고, 기울기 소실 문제가 발생할 수 있다.

def tanh(x):
    # return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
    return np.tanh(x)
    
plt.figure(figsize=(5, 3))
plt.plot(X, tanh(X))
plt.title('Tanh Function')
plt.grid()
plt.show()

 

import pandas as pd

np.random.seed(42)
X = np.random.uniform(-5, 5, size=10)

df = pd.DataFrame({
    "input (X)": X,
    "Step Function": step_function(X),
    "Sigmoid": sigmoid(X),
    "Tanh": tanh(X),
    "ReLU": relu(X),
    "Leaky ReLU": leaky_relu(X)
})

plt.figure(figsize=(8, 6))

plt.plot(df["input (X)"], df["Step Function"], marker="o", label="step function")
plt.plot(df["input (X)"], df["Sigmoid"], marker="*", label="Sigmoid")
plt.plot(df["input (X)"], df["Tanh"], marker="^", label="Tanh")
plt.plot(df["input (X)"], df["ReLU"], marker="d", label="ReLU")
plt.plot(df["input (X)"], df["Leaky ReLU"], marker="p", label="Leaky ReLU")

plt.xlabel('input (X)')
plt.ylabel('Activation output')
plt.legend()
plt.grid()
plt.show()

 

함수 출력 범위 비선형성 미분 기능 장점 단점 주요 사용처
Sigmoid (0, 1) O O 이진 분류 출력층 기울기 소실 출력층 (Binary)
Tanh (-1, 1) O O 중심화, 학습 빠름 기울기 소실 RNN 등 시계열
ReLU (0, ∞) O O 계산 단순, 학습 빠름 Dying ReLU CNN, DNN 중간층
Leaky ReLU (- ∞, ∞) O O Dying ReLU 보완 α 설정 필요 ReLU 대체용
Step (0, 1) O X 개념적 단순성 학습 불가 이론적 개념 (X)

 

 

다차원 배열의 계산

 

  • 다차원 배열

여러 개의 차원을 가지는 배열로, 2차원 이상의 배열을 의미한다. 인공지능에서는 주로 행렬(Matrix) 또는 텐서(Tensor)라는 용어를 사용한다. 파이썬에서는 numpy 라이브러리를 활용하여 다차원 배열을 쉽게 다룰 수 있다.

딥러닝에서 자주 다루는 이미지 처리 문제와 관련할 때도 보통 4차원 텐서로 이미지 데이터를 표현한다.

 

  • 다차원 배열의 계산 복습
import numpy as np

A = np.array([[1, 2, 3],
             [4, 5, 6]])
B = np.array([[7, 8, 9],
             [10, 11, 12]])

# 단순 합
print(A + B)    # (2, 3)

# 브로드캐스팅 연산
vec = np.array([1, 2, 3])
print(A + vec)  # (2, 3)

# 점곱(내적) with 전치
print(A.dot(B.T))   # (2, 2)

 

  • 다층 퍼셉트론
X = np.random.randn(6, 10)

# 은닉층: 10개의 입력 -> 8개의 뉴런
W1 = np.random.randn(10, 8)  # 가중치
b1 = np.random.randn(1, 8)   # 편향

# 출력층: 8개의 입력 -> 4개의 클래스
W2 = np.random.randn(8, 4)
b2 = np.random.randn(1, 4)

# 은닉층 계산 (선형 계산 + tanh 활성화 함수)
z1 = np.dot(X, W1) + b1
result1 = np.tanh(z1)

# 출력층 계산 (선형 계산 + softmax 활성화 함수)
z2 = np.dot(result1, W2) + b2
exp_z = np.exp(z2 - np.max(z2, axis=1, keepdims=True))
result2 = exp_z / np.sum(exp_z, axis=1, keepdims=True)

print(result2)

 

  • 이미지 데이터 예시
import matplotlib.pyplot as plt

# 이미지 생성
batch_size = 5
channels = 3 # 3=RGB, 4=PNG(투명도까지)
height = 32
width = 32

images = np.random.rand(batch_size, channels, height, width)

bright_images = np.clip(images + 0.2, 0, 1)

gray_images = np.mean(images, axis=1)

images_for_display = images[0].transpose(1, 2, 0)
bright_images_for_display = bright_images[0].transpose(1, 2, 0)

plt.subplot(1, 3, 1)
plt.imshow(images_for_display)
plt.axis('off')

plt.subplot(1, 3, 2)
plt.imshow(bright_images_for_display)
plt.axis('off')

plt.subplot(1, 3, 3)
plt.imshow(gray_images[0], cmap='gray')
plt.axis('off')

plt.show()

 

 

출력층 설계

 

  • 출력층 활성화 함수

주로 항등 함수, 시그모이드 함수, 소프트맥스 함수의 세 가지 활성화 함수가 사용된다.

 

👉🏻 세 가지 함수를 주로 사용하는 이유

1. 문제 유형에 최적화

- 항등 함수 (Identify Function) : 회귀 문제에서, 연속적인 값을 그대로 출력할 때 적합하다.

- 시그모이드 함수 (Sigmoid Function) : 이진 분류나 다중 레이블 분류에서, 각 클래스의 존재 확률을 0과 1 사이로 표현할 때 유용하다.

- 소프트맥스 함수 (Softmax Function) : 다중 클래스 분류에서, 각 클래스에 대한 예측 확률 분포를 생성하기 위해 사용된다.

 

2. 미분 가능성 및 학습의 안정성

- 세 함수 모두 미분 가능하여 역전파(backpropagation) 알고리즘에 적합하다.

- 문제 유형에 따라 적절한 함수 선택이 학습의 수렴과 안정성을 보장한다.

 

3. 수학적, 실무적 검증

- 각 함수는 해당 문제에 대해 오랜 연구와 실제 적용에서 최적의 성능을 보임으로써 표준으로 자리 잡았다.

 

  • 항등 함수

입력 값을 그대로 출력하는 함수이다.

신경망의 회귀 문제에서는 출력층에서 항등 함수를 사용하여 연속적인 값을 그대로 출력한다.

 

  • 시그모이드 함수

이진 분류나 다중 레이블 분류 문제에서 주로 샤용되는 활성화 함수이다.

이진 분류 문제에서 출력층에 시그모이드 함수를 적용하면, 각 뉴런의 출력이 특정 클래스에 속할 확률로 해석되며, 다중 레이블 분류에서도 각 레이블에 대한 존재 여부를 예측할 수 있다.

 

  • 소프트맥스 함수

분류 문제에서 출력증에서 사용되는 활성화 함수이다.

입력 값을 정규화하여 각 클래스에 대한 확률 분포를 생성한다.

 

  • 출력층의 뉴런 수

출력층의 뉴런 수는 해결하려는 문제에 따라 결정된다.

- 회귀 문제 : 1개 (연속된 값 출력)

- 이진 분류 : 1개 또는 2개 (시그모이드 사용 시 1개, 소프트맥스 사용 시 2개)

- 다중 클래스 분류 : 클래스 개수와 동일한 뉴런 개수 (소프트맥스 사용)

 

  • torch 사용을 위한 가상환경 생성

👉🏻 아래 사이트에서 PC 환경에 맞게 Compute Platform 선택(CPU) 후 Run this Command 부분 복사

https://pytorch.org/

 

PyTorch

PyTorch Foundation is the deep learning community home for the open source PyTorch framework and ecosystem.

pytorch.org

conda create -n torch_env python=3.12
conda activate torch_env
pip install jupyter notebook ipykernel numpy pandas matplotlib seaborn
python -m ipykernel install --user --name torch_env --display-name torch_env
pip3 install torch torchvision torchaudio

 

 

  • 소프트맥스 오버플로우 방지
import numpy as np

def softmax(z):
    exp_z = np.exp(z)
    return exp_z / np.sum(exp_z)

def stable_softmax(z):
    exp_z = np.exp(z - np.max(z))
    return exp_z / np.sum(exp_z)

x = np.array([1000, 1001, 1002])
print(softmax(x))  # [nan nan nan]
print(stable_softmax(x))  # [0.09003057 0.24472847 0.66524096]

 

👉🏻 pytorch 라이브러리 사용

import torch
import torch.nn.functional as F

x = torch.tensor([1000, 1001, 1002], dtype=torch.float32)

softmax_output = F.softmax(x)
print(softmax_output)  # tensor([0.0900, 0.2447, 0.6652])

sigmoid_output = torch.sigmoid(x)
print(sigmoid_output)  # tensor([1., 1., 1.])

 

  • 손실 함수와 연계
import torch
import torch.nn as nn
import torch.optim as optim

# 간단한 다중 클래스 분류 모델 정의
class SimpleMultiClassModel(nn.Module):
    def __init__(self):
        super(SimpleMultiClassModel, self).__init__()
        self.fc = nn.Linear(5, 3)

    def forward(self, x):
        return self.fc(x)
   
# 모델, 손실함수, 최적화함수 설정
model = SimpleMultiClassModel()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

# 데이터 생성
inputs = torch.randn(4, 5)
labels = torch.tensor([0, 2, 1, 0])

# 학습
for _ in range(10):
    preds = model(inputs)           # 순전파
    loss = criterion(preds, labels) # 손실 계산
    print(loss.item())

    optimizer.zero_grad()   # 기울기 초기화 (이전 단계에서 계산된 기울기를 0으로 초기화)
    loss.backward()         # 역전파 (손실에 대한 역전파 수행 - 파라미터에 대한 기울기 계산)
    optimizer.step()        # 가중치 업데이트 (계산된 기울기를 사용하여 옵티마이저가 모델 파라미터 갱신)