퍼셉트론
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt11943 MCP 뉴런¶
생물학적 뉴런의 수학적 모델
1.1두뇌에서 출발한 인공지능¶
지능은 개별 관측치에 반응하는 데 그치지 않고, 그 관측치를 지배하는 원인을 통찰해 모형을 형성하는 능력입니다. 사과가 떨어지는 것을 보고 어째서 떨어지는지 모형을 세우고, 그 모형이 다른 경우에도 설명력이 있는지 확인하는 과정은 지능적입니다. 이런 모형을 형성하는 기관이 바로 두뇌입니다.
19세기 말, 현미경의 발전으로 두뇌의 기본 단위가 뉴런임이 밝혀졌고, 뉴런이 전달하는 것이 전기 신호임이 드러났습니다. 생각과 감정이 전기 신호로 구체화되면, 그것은 전자회로로 재현할 수 있음을 뜻합니다. 의사 맥컬럭(McCulloch)과 논리학자 피츠(Pitts)는 뉴런에서 생물학적 요소를 걷어내고 연산할 수 있는 수학적 틀을 제시하고자 했습니다. 수학으로 표현될 수 있어야 전자회로로 만들 수 있고, 전자회로로 만들 수 있는 것은 잠재적으로 컴퓨터 프로그램으로 구현할 수 있습니다.
그 결과가 1943년의 MCP 뉴런이며, 우리의 맥락에서는 인공 뉴런 또는 그냥 뉴런이라고 부릅니다. MCP 뉴런은 입력을 받아 가중치를 부여하고, 이를 단일한 값으로 취합한 뒤, 일정한 기준에 따라 판단해 출력을 발생합니다. 입력 는 여러 특징으로 구성된 벡터이며, 정보는 통째로가 아니라 여러 특징으로 나뉘어 처리됩니다. 최종 출력은 1 또는 -1의 값입니다.
MCP 뉴런의 동작 흐름: 입력을 받아 가중치를 부여하고, 단일 값으로 취합한 뒤 활성화로 출력을 결정합니다.
입력 특징 벡터
x = np.array([0.1, 0.2, 0.3])
w = np.array([0.1, 10., 0])입력에 가중치 부여
입력을 차등해 처리하는 능력이 곧 지능적인 동작입니다. 모든 입력을 있는 그대로 받아들이는 것은 지능적이라고 하기 어렵습니다. 지능적인 존재는 상황에 따라 중요한 정보와 그렇지 않은 정보를 나누고, 중요한 것을 우선해서 처리합니다.
이 차등 처리를 수학적으로 표현한 것이 가중치이며, 입력에 곱해지는 값입니다. 입력 값을 얼마나 부각시키는지를 표현하는 데에는 곱하기가 적당하기 때문입니다. 예를 들어 입력이 0.1일 때 가중치가 0.1이면 결과는 0.01이지만, 같은 입력에 가중치가 10이면 결과는 1.0이 됩니다. 가중치가 클수록 그 입력의 효과를 더 강조하는 것입니다. 반대로 가중치가 0이면 어떤 입력이 들어와도 출력에 기여하는 바가 없으므로, 처음부터 들어오지 않은 것과 마찬가지입니다. 즉 그 입력은 중요하지 않다고 평가한 셈입니다.
#z = np.sum(x * w)
z = np.dot(x, w)
z가중치가 부여된 입력은 단일한 값으로 합쳐집니다. 취합하는 방법은 간단합니다. 모두 더하면 됩니다. 이때는 곱하기가 아니라 더하기를 써야 합니다. 곱하기를 쓰면 어느 하나가 0일 때 전체 출력이 0이 되어 버리기 때문입니다. 취합한 결과는 입력 , 출력 와 겹치지 않도록 로 표기합니다.
이 연산은 두 벡터 와 의 내적과 같습니다.
입력이 하나의 벡터일 때 는 스칼라가 됩니다. 실제 데이터에서는 입력이 개의 특징으로 표현된 표본 개로 이루어진 2차원의 디자인 행렬 이며, 가중치는 특징과 일대일로 대응하므로 여전히 길이 의 벡터입니다. 같은 내적 연산으로 입력이 벡터일 때나 행렬일 때나 를 구할 수 있고, 입력이 행렬이면 는 벡터가 됩니다.
여러 값을 하나로 취합하는 것은 금융시장의 주가지수에 비유할 수 있습니다. 어떤 종목은 오르고 어떤 종목은 내릴 때, 개별 등락만 나열해서는 시장 전체가 좋은지 나쁜지 알기 어렵습니다. 그래서 이를 단일한 지표로 취합합니다. 가중치가 부여된 입력을 하나의 값으로 모으는 것도 같은 이유에서 효율적입니다.
가중치가 부여된 입력의 취합은 두 벡터의 내적과 같으며, 결과 z는 스칼라가 됩니다.
활성화 (Activation)
취합된 는 음과 양의 무한대까지 이르는 실수입니다. 이를 어떤 임계점 를 기준으로 평가해 최종 출력을 결정하며, 이 과정을 활성화라고 합니다. 임계점 는 편향(bias)이라 부릅니다. 가 를 넘으면 1, 그렇지 않으면 -1을 출력합니다.
투자에 비유하면 는 정보가 취합된 종합 지표이고, 는 사거나 파는 행동입니다. 같은 지표라도 어떤 임계점을 기준으로 삼는가에 따라 행동은 완전히 달라집니다. 누군가는 지표가 높이 올라야 사고, 누군가는 조금만 올라도 팝니다. 그래서 가중치와 더불어 편향도 지능적 동작에 기여하는 중요한 요인입니다.
출력을 1과 -1로 두는 것은 전기 신호의 관점에서 비롯합니다. 아무 입력이 없으면 출력은 0이므로, 두 값을 구별할 때 하나를 1로 둔다면 다른 하나는 0이 아니라 -1로 두는 편이 자연스럽습니다. 다만 컴퓨터의 논리 세계에서는 '없는 값’도 하나의 값이어서 0과 구별되므로, 프로그램으로 구현할 때는 -1 대신 0을 써도 무방합니다.
b = 1.0
y = 1 if z > b else -1
y를 임계점 와 비교하는 대신, 편향을 안으로 옮기면 식이 한결 깔끔해집니다. 로 두면, 활성화는 단순히 를 0과 비교하는 것으로 충분합니다. 가중치 와 편향 는 모두 뉴런의 동작을 결정하는 매개변수입니다.
더 나아가 편향을 으로, 항상 1인 가짜 입력을 으로 두면 편향까지 가중치에 흡수해 표기할 수 있습니다.
은 언제나 1이므로 곱셈에서 아무 역할을 하지 않지만, 전체 식의 모양을 가중치와 입력의 곱으로 통일해 줍니다. 이렇게 보면 출력 는 입력 의 함수이며, 그 함수의 형태는 매개변수 (가중치와 편향)의 설정에 따라 달라집니다. 따라서 특별한 이유가 없으면 매개변수와 가중치를 거의 같은 의미로 사용하고, 편향이 필요한 경우에만 따로 명시합니다.
뉴런 모형: 편향을 가중치에 흡수해 z를 구하고, 활성화 함수 A(계단함수)를 거쳐 출력 y를 냅니다.
def MCP뉴런(x, w, b):
z = x @ w + b
y = np.where(z > 0, 1, -1)
return y
Xs = np.array([(0, 0), (0, 1), (1, 0), (1, 1)])
# AND
w_and = np.array([0.5, 0.5])
b_and = -0.7
y_and = MCP뉴런(Xs, w_and, b_and)
assert all(y_and == [-1, -1, -1, 1])
# NAND
w_nand = -1 * w_and
b_nand = -1 * b_and
y_nand = MCP뉴런(Xs, w_nand, b_nand)
assert all(y_nand == [1, 1, 1, -1])
# OR
w_or = np.array([0.5, 0.5])
b_or = -0.2
y_or = MCP뉴런(Xs, w_or, b_or)
assert all(y_or == [-1, 1, 1, 1])
outputs = np.stack([y_and, y_nand, y_or], axis=1)
outputs = np.where(outputs > 0, 1, 0)
Xy = np.hstack([Xs, outputs])
pd.DataFrame(Xy, columns=['x1', 'x2', 'AND', 'NAND', 'OR'])MCP 뉴런의 동작은 가중치와 편향, 곧 매개변수가 결정합니다. 같은 구조라도 매개변수를 어떻게 두는가에 따라 하나의 뉴런이 AND, OR, NAND 논리회로가 될 수 있습니다. 논리회로의 특성은 두 입력에 대한 출력을 정리한 진리표로 표현됩니다.
이 점이 중요한 까닭은, 컴퓨터가 결국 논리회로의 조합이기 때문입니다. NAND 게이트의 조합만으로 임의의 프로그램을 실행하는 컴퓨터를 구성할 수 있다는 사실이 알려져 있습니다. 무언가가 지능적이고 '스마트’하다는 것은 특정 기능에 특화된 특수성의 반대, 곧 여러 상황에 일반적으로 적용할 수 있음을 뜻합니다. MCP 뉴런으로 논리회로를 만들 수 있다는 것은, 뉴런의 조합이 잠재적으로 어떤 연산이든 수행할 수 있는 일반성을 지닐 수 있음을 시사합니다.
2결정 경계¶
Xs = np.array([(0, 0), (0, 1), (1, 0), (1, 1)])
labels = np.array([0, 1, 1, 1])
plt.scatter(Xs[:, 0], Xs[:, 1], c=labels, cmap='bwr')
plt.axline(xy1=(0.0, 0.8), xy2=(0.8, 0.0), color='gray', linestyle='--', linewidth=2, label='결정경계')
plt.title('기울기=-1.0, 절편=0.8')
plt.grid()
plt.legend()
plt.savefig('linear_decision_boundary.png', dpi=300)
plt.show()def MCP뉴런(x, w, b):
z = x @ w + b
return np.where(z > 0, 1, -1)
y = {}; params = {}
y['AND'] = np.array([0, 0, 0, 1])
params['AND'] = {'w': np.array([0.5, 0.5]),'b': -0.7}
y['OR'] = np.array([0, 1, 1, 1])
params['OR'] = {'w': np.array([1.0, 1.0]),'b': -0.5}
Xs = np.array([(0, 0), (0, 1), (1, 0), (1, 1)])
# outputs = np.array([MCP뉴런(x, w_and, b_and) for x in Xs])
outputs = {}
z0 = lambda x, w, b: (w[0] * x + b) / -w[1]
plt.figure(figsize=(6, 3))
for i, key in enumerate(y.keys()):
# 각 논리 게이트에 대한 출력 계산
outputs[key] = MCP뉴런(Xs, **params[key])
# 출력이 기대값과 일치하는지 확인
assert np.array_equal(np.where(outputs[key] > 0, 1, 0), y[key])
# 출력별 영역 설정
plt.subplot(1, len(y), i+1)
plt.scatter(Xs[:, 0], Xs[:, 1], c=outputs[key], cmap='bwr')
plt.title(key)
# 결정 경계 시각화
xs = np.array([0.0, 1.1]) # 결정 경계를 그리기 위한 x1 좌표
plt.plot(xs, z0(xs, **params[key]), color='black', linestyle='--' , label=key)
# 그래프 표시 범위 설정
plt.xlim(-0.1, 1.1); plt.ylim(-0.1, 1.1)
plt.xlabel('x1'); plt.ylabel('x2')
plt.grid()
plt.tight_layout()
plt.show()31958 퍼셉트론¶
MCP 뉴런이 매개변수를 스스로 학습하기까지는 1950년대 후반을 기다려야 했습니다. 1958년 프랭크 로젠블랫(Frank Rosenblatt)이 MCP 뉴런에 학습 능력을 부여한 퍼셉트론을 발명합니다. 세포 수준이긴 하지만, 이때 비로소 인공지능이 시작되었다고 할 수 있습니다. 신경망의 조상인 로젠블랫의 퍼셉트론은 광전 전지와 케이블을 결합한 장치로 알파벳 문자를 인식했습니다.
퍼셉트론은 MCP 뉴런의 틀을 그대로 유지하면서, 정답과 예측값의 차이인 오차를 기반으로 가중치를 스스로 조절하는 학습 알고리즘을 더한 것입니다. 출력은 가중치와 편향으로 이루어진 매개변수의 설정에 따라 달라지므로, 이 매개변수를 데이터로부터 스스로 갱신해 정답을 향해 갈 수 있어야 비로소 학습 능력이 있다고 말합니다.
class 퍼셉트론:
def __init__(self, w=None, b=None):
self.w = w
self.b = b
def __call__(self, x):
z = x @ self.w + self.b
return np.where(z > 0, 1, -1)
def predict(self, x):
return self.__call__(x)
def fit(self, data, target, 학습횟수, 학습률=1.0):
# 매개변수 초기화
표본수, 특성수 = data.shape # (s, n)
self.w = np.zeros(특성수)
self.b = 0.0
매개변수변화 = []
for epoch in range(학습횟수):
# 매개변수 기록
매개변수변화.append(np.append(self.w, self.b))
# 각 데이터 포인트별
for xi, yi in zip(data, target):
# 예측
output = self(xi)
# 정답과 비교
# 오분류: 정답과 출력의 부호가 반대
delta = max(0, -yi * output)
# 오분류 시, 매개변수를 x 방향으로 업데이트
self.w += 학습률 * delta * yi * xi
self.b += 학습률 * delta * yi
return np.array(매개변수변화)퍼셉트론의 출력은 1 또는 -1의 이진 분류입니다. 정답과 예측의 조합은 네 가지인데, 부호가 같으면 정답을 맞춘 것이어서 오류가 없고, 부호가 반대이면 오류가 발생합니다. 오류가 없으면 가중치가 바람직한 경우이고, 오류가 있으면 가중치를 조절해야 한다는 뜻입니다.
갱신의 크기는 학습률로 조절합니다. 한 번에 너무 크게 갱신하면 정답을 사이에 두고 지나쳐 버릴 수 있고, 너무 작게 갱신하면 주어진 학습 횟수 안에 정답에 이르지 못할 수 있습니다. 적절한 학습률을 두면 정답을 향해 점차 좁혀 갑니다. 학습률은 학습 환경에 해당하며, 모델이 학습하는 대상인 매개변수와는 구별됩니다.
또한 갱신값에 각 특징의 값을 곱해 줍니다. 만약 모든 가중치를 같은 값으로 갱신하면 모든 입력을 똑같이 취급하게 되어, 정보를 차등 처리하는 지능적 동작을 할 수 없습니다. 갱신값에 특징값을 곱하면 특징의 크기에 따라 각 가중치가 서로 다르게 갱신되므로, 입력마다 알맞은 의미를 부여할 수 있습니다.
Xs = np.array([(0, 0), (0, 1), (1, 0), (1, 1)])
y = {}
y['AND'] = np.array([0, 0, 0, 1])
y['OR'] = np.array([0, 1, 1, 1])
AND = 퍼셉트론()
매개변수변화 = AND.fit(data=Xs, target=np.where(y['AND'] > 0, 1, -1), 학습횟수=10)
# display(pd.DataFrame(매개변수변화, columns=['w1', 'w2', 'b']))
# 학습된 매개변수의 출력이 정답과 같은지 확인
assert np.array_equal(np.where(AND(Xs) > 0, 1, 0), y['AND'])
OR = 퍼셉트론()
매개변수변화 = OR.fit(data=Xs, target=np.where(y['OR'] > 0, 1, -1), 학습횟수=10)
display(pd.DataFrame(매개변수변화, columns=['w1', 'w2', 'b']))
# 학습된 매개변수의 출력이 정답과 같은지 확인
assert np.array_equal(np.where(OR(Xs) > 0, 1, 0), y['OR'])3.1학습 시각화¶
Xs = np.array([(0.1, 0.4), (0.1, 0.3), (0.2, 0.3), (0.2, 0.2)])
labels = [1, -1, 1, -1]
model = 퍼셉트론()
매개변수변화 = model.fit(data=Xs, target=labels, 학습횟수=60)
display(pd.DataFrame(매개변수변화, columns=['w1', 'w2', 'b']).tail())
assert np.array_equal(model(Xs), labels), model(Xs)4종양 분류¶
4.1종양 데이터¶
import sklearn.datasets
cancer = sklearn.datasets.load_breast_cancer()
print(cancer.target_names, '=', '[악성, 양성]')
print(cancer.data.shape)
cancer.frame = pd.DataFrame(cancer.data, columns=cancer.feature_names)
display(cancer.frame.sample(5).round(3))4.2종양 분류¶
종양분류기 = 퍼셉트론()
target = np.where(cancer.target == 0, -1, 1) # 악성:-1, 양성:1
print(pd.Series(target).value_counts())
특성별최대값 = cancer.data.max(axis=0)
특성별최소값 = cancer.data.min(axis=0)
정규화데이터 = (cancer.data - 특성별최소값) / (특성별최대값 - 특성별최소값)
assert np.allclose(정규화데이터.min(axis=0), 0.0) and np.allclose(정규화데이터.max(axis=0), 1.0)
매개변수변화 = 종양분류기.fit(data=정규화데이터, target=target, 학습횟수=100, 학습률=0.1)
예측 = 종양분류기.predict(정규화데이터)
채점 = 예측 == target
print(f'정확도: {채점.mean():.1%}')
display(pd.DataFrame(매개변수변화, columns=[f'w{k + 1}' for k in range(len(종양분류기.w))] + ['b']).round(3).tail())5다중 퍼셉트론¶
복수 개의 퍼셉트론이 같은 입력을 받아 동시 다발적으로 출력을 발생하는 구성
퍼셉트론에서는 입력과 가중치가 일대일로 대응했습니다. 반면 다중 퍼셉트론에서는 여러 개의 입력이 여러 개의 뉴런에 모두 연결되므로, 입력과 가중치가 일대다의 관계로 바뀝니다. 즉 하나의 입력에 대해 출력 뉴런의 수만큼 가중치가 생깁니다. 그래서 각 층의 학습 매개변수는 1차원의 벡터가 아니라 행렬 형상의 텐서가 됩니다. 어느 뉴런의 가중치인지를 구별하기 위해, 출력 뉴런 1번에는 , 2번에는 와 같이 첨자로 표기합니다. 편향은 입력 개수와 무관하게 뉴런마다 하나씩 있으므로 처럼 순차적으로 둡니다.
class 다중퍼셉트론:
def __init__(self, 입력수, 출력수, 활성화=None):
self.W = np.random.randn(입력수, 출력수)
self.b = np.random.randn(출력수)
self.활성화 = 활성화
def __call__(self, x):
z = x @ self.W + self.b
return self.활성화(z) if self.활성화 else zlogic_gates = 다중퍼셉트론(입력수=2, 출력수=3, 활성화=lambda z: np.where(z > 0, 1, 0))
params = np.stack([
[1.0, 1.0, -1.1], # AND
[-1.0, -1.0, 1.1], # NAND
[1.0, 1.0, -0.5] # OR
])
logic_gates.W = params[:, :2].T
logic_gates.b = params[:, 2]
Xs = np.array([(0, 0), (0, 1), (1, 0), (1, 1)])
outputs = logic_gates(Xs)
assert np.array_equal(
outputs,
np.array([
[0, 0, 0, 1],
[1, 1, 1, 0],
[0, 1, 1, 1]]).T)
pd.concat([
pd.DataFrame(Xs, columns=['x1', 'x2']),
pd.DataFrame(outputs, columns=['AND', 'NAND', 'OR'])
], axis=1)6붓꽃 분류¶
import sklearn
import sklearn.datasets
iris = sklearn.datasets.load_iris()
perceptron = sklearn.linear_model.Perceptron(
max_iter=100, shuffle=True, random_state=3)
perceptron.fit(iris.data, iris.target)
outputs = perceptron.decision_function(iris.data)
f = 다중퍼셉트론(입력수=iris.data.shape[1], 출력수=3)
f.W = perceptron.coef_.T
f.b = perceptron.intercept_
assert np.allclose(outputs, f(iris.data))
예측 = np.argmax(outputs, axis=1)
채점 = 예측 == iris.target
print(f'정확도: {채점.mean():.1%}')
(pd.DataFrame(outputs)
.assign(예측=np.argmax(outputs, axis=1))
.assign(실제=iris.target)
.sample(5, random_state=0).round(3))71969 XOR¶
Xs = np.array([(0, 0), (0, 1), (1, 0), (1, 1)])
y = {}
y['OR'] = np.array([-1, 1, 1, 1])
y['XOR'] = np.array([-1, 1, 1, -1])
params = {}
OR = 퍼셉트론(w=np.array([1.0, 1.0]), b=-0.8)
# OR.fit(data=Xs, target=y['OR'], 학습횟수=5)
# params['OR'] = {'w': OR.w, 'b': OR.b}
params['OR'] = {'w': OR.w, 'b': OR.b}
assert np.array_equal(OR(Xs), y['OR'])
XOR = 퍼셉트론()
XOR.fit(data=Xs, target=y['XOR'], 학습횟수=100)
params['XOR'] = {'w': XOR.w, 'b': XOR.b}
try:
assert np.array_equal(XOR(Xs), y['XOR']), f'불일치: {XOR(Xs)} != {y["XOR"]}'
except Exception as e:
print(e)
print('선형 결정경계로는 해결 불가')
plt.figure(figsize=(6, 3))
x1 = np.array([0.0, 1.0])
결정경계 = lambda x, w, b: (w[0] * x + b) / (-w[1] + 1e-7)
for i, key in enumerate(y.keys()):
plt.subplot(1, 2, i+1)
plt.scatter(Xs[:, 0], Xs[:, 1], c=y[key], cmap='bwr')
plt.plot(x1, 결정경계(x1, **params[key]), 'k-')
plt.xlim(-0.1, 1.1); plt.ylim(-0.1, 1.1); plt.grid()
plt.title(key)
단일 퍼셉트론은 하나의 선형 결정 경계만 그릴 수 있어 XOR처럼 선형으로 나뉘지 않는 데이터는 분류하지 못합니다. 그러나 여러 퍼셉트론을 겹겹이 쌓아 적절히 구성하면 이 한계를 넘을 수 있습니다. 각각 NAND, OR, AND로 동작하는 퍼셉트론을 연결해 NAND와 OR의 출력을 다시 AND의 입력으로 넣으면, 최종적으로 XOR 출력을 얻습니다. 이런 까닭에 이미 1960년대부터 다층 퍼셉트론, 곧 인공 신경망이 나아갈 방향이라는 점은 공감되고 있었습니다.
인공 신경망은 흔히 신경망이라 줄여 부르며, 다층 퍼셉트론과 같은 말입니다. 신경망은 입력과 출력 사이에 여러 층을 두어 둘을 간접적으로 연결하며, 이 사이에 삽입되는 층을 은닉층이라 합니다. 은닉층은 한 개 이상의 뉴런으로 구성되고, 데이터가 복잡할수록 뉴런 수와 층의 깊이가 늘어나야 효과적일 가능성이 높습니다. 층이 깊어진 구성을 딥러닝이라 합니다. 다만 신경망과 딥러닝이 완전한 동의어는 아닙니다. 신경망으로 딥러닝을 구성하지만 모든 신경망이 딥러닝은 아니며, 신경망으로 딥러닝을 수행하기까지는 21세기의 여러 발전이 필요했습니다.
다층 퍼셉트론으로 XOR 구성: NAND와 OR의 출력을 다시 AND의 입력으로 넣어 최종 XOR 출력을 얻습니다.