728x90

ref : towardsdatascience.com/lines-detection-with-hough-transform-84020b3b1549

 

허프 변환을 이용한 직선 검출 알고리즘

 

0. 에지 영상 준비

 

1. rho와 theta의 범위 지정

- theta는 -90 ~ 90도

- rho는 -d ~ d. d는 이미지 대각선 길이.

- theta와 rho를 계산할 수 있도록 이산 값이 되게 해야함

 

2. 허프 공간을 나타낼 2차원 누적 배열 생성

- shape : n_rho * n_theta

- 0으로 초기화

- 가능한 모든 경우의 rho, theta들을 누적할 예정

 

3. 에지 영상의 모든 픽셀 확인. 에지 픽셀에 대해서 가능한 모든 theta와 rho를 구하여 허프 행렬에 해당 인덱스 증감

 

4. 지정한 임계치를 넘는 값의 인덱스(rho, theta)가 검출된 직선

 

 

 

 

 

 

알고리즘 구현 자체는 

 

여러 링크에 있는 내용들을 참고하다보니

 

대강 형태는 금방 만들었지만

 

시각화 할때가 많이 복잡했다.

 

 

이전 글에서 일반적인 xy 좌표계 상에서 rho, theta를 놓으면 아래 그림과 같이 그릴수 있지만

 

 

 

이미지 영역의 경우 y가 반대로 되어있는데

 

이때 rho, theta를 어떻게 보는 지가 문제였다.

 

제대로 이해하지 않은 상태에서 하다보니

 

rho와 theta를 xy 좌표계상에서 라인 플로팅을 할때,

 

rho와 theta로 그린 라인이 내가 생각했던 대로가 아니거나, 실제 에지와 일치 하지 않게 계속 나오더라

 

 

 

 

 

일단 사용한 기본 영상은 그림판에다가 일직선을 그려서 만들어봤다.

 

 

가우시안 블러링과 캐니 에지를 만들어주고

 

 

아무 생각없이(안해도 되지만) 모폴로지 닫힘도 해주었다.

 

 

 

이제 이 에지 영상에서 부터 시작하면 된다.

 

hough 공간의 누적 행렬을 만드는 함수를 구현하였다.

 

여기서 누적을 하는 이유는 많이 사용되는(임계치를 넘는) rho와 theta를 찾아 직선을 그려주기 위함인데,

 

 

하나의 에지 픽셀에 대해서 모든 방향 360도 -> 직선으로 나타낼태니 180도를 1도 간격(해상도)으로 나누어

 

각 에지의 180도 rho를 구하고

 

해당 rho값 자체(rho)와 theta의 인덱스(theta_idx)를

 

누적 행렬의 행과 열 인덱스로 사용하고 1씩 증감을 시켰다.

accumulator[rho, theta_idx] += 1

 

def hough_acc(edge_img, rho_resolution= 1, theta_resolution = 1):
    rows, cols = edge_img.shape    
    d = np.round(np.sqrt(rows**2 + cols**2))
    thetas = np.arange(-90, 90, theta_resolution)
    rhos = np.arange(-d, d + 1, rho_resolution)
    
    #all cos, sin thetas 0 from 180 degree
    cos_thetas = np.cos(np.deg2rad(thetas))
    sin_thetas = np.sin(np.deg2rad(thetas))
    accumulator = np.zeros((len(rhos), len(thetas)))
    cnt = 0
    for row in range(rows):
        for col in range(cols):
            # if seletecd point is edge
            if edge_img[row, col] != 0:
                cnt += 1
                # check all possible thetas
                for theta_idx in range(len(thetas)):
                    # rho = (x_dist * cos(theta)) + (y_dist * sin(theta))
                    rho =int(d + (col* cos_thetas[theta_idx])+(row *sin_thetas[theta_idx]))
                    accumulator[rho, theta_idx] += 1
    return accumulator, rhos, thetas    

 

 

위 함수로부터 acc와 rhos, theta를 전달받아 임계치를 넘는

 

rho와 theta들을 모아 반환해주는 함수를 구현하였다.

 

예를 들자면 아래와 같이 점 3개가 있는데

 

각 점에서 모든 방향으로 누적을 시키게 된다.

 

하지만 세점이 이어진 직선이 세 번 겹치므로 accumulator에서 가장 큰 값을 가지게 된다.

 

이 때, accumulator[rho, theta_idx] = 겹친 횟수이며

 

이 겹친 횟수가 임계치를 넘으면 직선으로 포함시킨다.

 

def find_peak(accumulator, rhos, thetas, threhsold=60):
    
    lines = []
    for y in range(accumulator.shape[0]):
        rho = rhos[y]
        for x in range(accumulator.shape[1]):
            if accumulator[y][x] > threhsold:
                theta = np.deg2rad(thetas[x])
                lines.append([rho, theta])
    
    return np.array(lines)

 

 

 

마지막으로 원본 이미지와

 

위 함수에서 반환한 직선으로 

 

라인을 플로팅 시키는 함수

 

 

rho와 theta로 부터

 

직선을 구하는데 

 

x0, y0가 (그리고자 하는) 법선과 만나는 점이고,

 

x1,y1 x2,y2는 이미지 밖에서 부터 그려주기 위해 구한 좌표가 된다.

def draw_hough_lines(img, lines):
    res = img.copy()
    for i, line in enumerate(lines):
        rho = line[0]
        theta = line[1]
        # reverse engineer lines from rhos and thetas
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a*rho
        y0 = b*rho
        
        # these are then scaled so that the lines go off the edges of the image
        x1 = int(x0 + 1000*(-b))
        y1 = int(y0 + 1000*(a))
        x2 = int(x0 - 1000*(-b))
        y2 = int(y0 - 1000*(a))
        cv2.circle(res, (int(x0), int(y0)),2,(0,0,255),1)
        cv2.line(res, (x1, y1), (x2, y2), (0, 255, 0), 1)
        plt.imshow(res,cmap="gray")

 

 

 

위 이미지로 부터 에지를 구할수가 있었다.

 

acc, rhos, thetas = hough_acc(morph)
lines = find_peak(acc, rhos,thetas,threhsold=40)
print(lines[:5])
draw_hough_lines(img, lines)

 

 

그런데 구한 rho와 theta 값이 이해되지 않았다.

 

rho = 38, theta= -0.7853인데

 

theta는 라디안이므로 육십분법으로 변환하면

 

- 44가 되더라

 

 

 

어딜 기준으로 -44도로 38만큼 떨어진 지점에서 직선을 그렷길래 찾았나 했더니

 

이미지 평면의 원점이 기준이더라.

 

일반 적인 xy평면에서

 

y 양의 방향으로 theta가 +, y 음의 방향으로 theta가 -가 됬었는데

 

y방향이 반대로 되면서 theta가 +인 경우 아래로, theta가 -인 경우 위로 가는거더라

 

이부분 때문에 좀 많이 햇갈렸다.

 

 

 

 

 

 

 

원래는 일반적인 사진에서 직선 검출을 하려고 했지만

 

픽셀 단위로 연산하고, 모든 픽셀들을 다루다보니 상당히 시간이 오래 걸리더라

 

이 부분을 보니 아에 행렬, 벡터 단위로 처리하는 코드도 있더라

 

그렇게 수정하면 조금 나아 지지않을까 싶다.

 

 

 

대신 위 이미지에 라인을 몇개 더 그려서 검출 해봤다.

 

 

 

임계치를 40으로 주었을때 아래와 같은 결과를 얻을수 있었다.

 

여기서 lines의 앞 다섯개 요소를 보니

 

rho가 -65, theta가 -1.38 정도 되더라

 

 

 

이게 어느직선인가 보기 위해서

 

호도법을 육십분법으로 바꾸고 원점에서 찾아보았다.

 

rho = 65, theta= - 80 정도로 대강 저 에지에서 검출한 직선인것 같다.

 

 

300x250
728x90

선형 회귀 모델 Linear Regression Model

- 입력 벡터(독립 변수) x와 계수 벡터(가중치 벡터) w의 선형 결합으로 종속 변수 y를 예측하는 모델

- x = [0, x1, x2, x3 ..., x_p]

- w = [w0, w1, w2, ..., x_p]

- y = w.dot(x) = w0 + w1 x1 + . . . + w_p x_p

 

 

 

Linear Regression

- MSE를 최소화 하는 계수 벡터로 구한 선형 회귀 모델

- 독립 변수 간의 상관 관계가 높을 수록 분산, 오류가 커짐 : 다중 공선성 multi collinearity 문제

 => 독립적이고, 중요한 변수, 피처 위주로 남기거나 규제 or PCA 수행

 

 

 

회귀 모델 평가 지표

- MAE Mean Absolute Error : sum(|y - y_hat|) -> 일반 오차 합

- MSE Mean Sqaured Error : sum( (y - y_hat)^2 ) -> 오차가 클수록 크게 반영됨

- RMSE Root Mean Squared Error : root(MSE) -> MSE가 과하게 커지는것을 방지

- R2 : var(y_hat) / var(y) = 예측 분산/ 실제 분산 -> 1에 가까울수록 잘 예측

 

 

 

회귀 모델에서 평가 지표 사용시 유의할 점

- 회귀 평가 지표들은 분류 평가 지표들과는 달리 오차의 합이므로 값이 작을 수록 오차가 작아 더 좋은 모델임

 -> -1하여 음수로 만듬

- 회귀 모델 스코어링 사용 시 "neg_mean_squared_error"와 같이 앞에 "neg_"를 붙여서 명시 해야 함.

 

 

 

 

 

1. 라이브러리 임포트

 

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from scipy import stats
from sklearn.datasets import load_boston

 

 

 

 

2. 데이터 로드 

boston = load_boston()
print(boston.feature_names)
print(boston.DESCR)

 

 

 

3. 데이터 프레임 변환, 훑어보기

'
df = pd.DataFrame(data=boston.data, columns=boston.feature_names)
df.head()

df.describe()

df["PRICE"] = boston.target
df.info()

 

 

4. 변수간 상관관계 보기

- sns.pairplot

- pairplot은 너무 오래 걸림

 -> 각 독립변수와 종속변수의 상관관계를 보자

 

 

 

5. 각 독립변수와 종속변수 PRICE 사이 상관관계 보기

- sns.regplot() : 산점도와 회귀 선을 그려줌.

- 직선을 보면 RM은 PRICE와 강한 양의 상관관계

- LSAT은 PRICE와 강한 음의 상관관계를 가짐

 

# ZN ~ LSTAT까지 각 변수들과 PRICE의 선형 관계를 sns.regplot으로 살펴보자
fix, axs = plt.subplots(figsize=(16, 12), ncols=4, nrows=3)
features = df.columns[1:-1]

for i, feature in enumerate(features):
    row = int(i/4)
    col = i%4
    sns.regplot(x=feature, y="PRICE", data=df, ax=axs[row][col])

 

 

 

 

6. 학습 및 평가 지표 살펴보기

 

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

y = df["PRICE"]
X = df.iloc[:,:-1]

X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.2)

lr = LinearRegression()
lr.fit(X_train, y_train)

y_pred = lr.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print("MSE : {0:.3f}".format(mse))
print("RMSE : {0:.3f}".format(np.sqrt(mse)))
print("r2 : {0:.3f}".format(r2))
print("intercept : {}".format(lr.intercept_))
print("coeff : {}".format(np.round(lr.coef_,1)))

 

 

7. 피처 별 회귀 계수 살펴보기

- lr.coef_를 시리즈로 만들어 정렬 후 출력

- sns.regplot에서 본대로 RM은 강한 양의 상관 관계, LSTAT은 음의 상관관계를 가짐

coef = pd.Series(data=np.round(lr.coef_, 1), index=X.columns)
coef.sort_values(ascending=False)

 

 

8. k fold 교차 검증 후 성능 살펴보기

 

from sklearn.model_selection import cross_val_score

lr = LinearRegression()

neg_mse_scores = cross_val_score(lr, X, y, scoring="neg_mean_squared_error", cv=5)
rmse = np.sqrt(-1 * neg_mse_scores)
rmse_avg = np.mean(rmse)

print("neg mse : {}".format(np.round(neg_mse_scores,1)))
print("rmse : {}".format(np.round(rmse,1)))
print("rmse avg : {0:.3f}".format(rmse_avg))
300x250
728x90

머신 러닝의 대표적인 문제

- 분류와 회귀 문제가 있음.

- 분류 : 주어진 데이터가 있으면 이 데이터가 어떤 카테고리에 속하는가에 대한 문제

- 회귀 : 데이터가 주어지면 이 데이터로 연속적인 값을 추정하는 문제

 

 

선형 회귀

- 독립 변수들과 하나의 종속 변수가 주어지고, 이에 대한 데이터들이 있을때 종속 변수를 가장 잘 추정하는 직선을 구하는 문제

- 새로운 데이터가 주어지면 학습 과정에서 구한 회귀선으로 연속적인 값을 추정할 수 있음.

- 선형 회귀 모델은 계수 벡터 beta = [beta0, . . ., beta_p], 독립 변수 벡터 x = [0, x1, ...., x_p]의 선형 결합의 형태가 됨.

- e는 오차 모형으로 기본적으로 평균 0, 분산이 1인 정규 분포를 따르고 있음.

 -> 회귀 선과 추정한 y 사이의 오차를 의미함.

 

- 실제 종속 변수를 y, 주어진 독립변수를 통해 예측한(추정한) y를 y hat로 표기

- 오차 e는 y - y_hat으로 아래와 같은 관계를 가지고 있음.

 

 

 

 

단순 선형 회귀 모형

- 독립 변수가 1개인 선형 회귀 모형

- 아래의 좌측과 같이 2차원 공간 상에 점들이 주어질때, 우측과 같은 선형 회귀 모델을 구할 수 있음

- 기존의 회귀 계수 beta를 여기선 가중치의 의미로 w로 표기함.

- 오차 = 실제 값 - 추정 값의 관계를 가짐.

 

 

 

 

최소 제곱 오차 MSE : Mean Sqaured Error

- 실제 결과(y_i) - 추정 결과(y_i hat)의 제곱한 것을 모든 데이터에 대해 합한 후 데이터 갯수(N) 만큼 나누어 구한 오차

- 위의 산점도 데이터가 주어질떄 MSE를 최소가 되게하는 회귀 계수들로 선형 회귀 모델을 만듬

 

회귀 계수 구하기 기초

- MSE를 최소로 만드는 회귀 계수는 편미분을 통해 구할 수 있음.

- y = x^2라는 그래프가 주어질때, argmin(y)를 구하는 x를 얻으려면, y를 x로 편미분하여 0이 나오게하는 x를 구하면됨.

- d/dx y = 2x = 0     => x = 0일때 기울기는 0으로 y는 최소 값을 가짐

- 단순 선형 회귀 모델의 회귀 계수 구하는 방법 : MSE를 w0, w1에 편미분 한 후 0이 되도록 하는 w0, w1를 구함. 

 

 

회귀 계수 구하기

- MSE를 w0, w1로 편미분하여 0이되는 w0, w1을 구하자

 

- n제곱 다항식의 미분에 대한 공식을 이용하여 w0, w1에 대해 편미분을 하면 아래와 같이 정리할 수 있음.

 

w0 회귀 계수 구하기

- 회귀 계수는 초기에 특정 값(주로 1)로 초기화 후 오차의 크기에 따라 갱신 값을 빼 조금씩 조정됨

- MSE를 w0로 편미분(갱신 값)하여 0이 되게 만드는 w0는 아래와 같이 구할 수 있음.

 => 기존의 회귀 계수(w0) - 기존 회귀 계수의 오차(갱신 값, d MSE/ d w0) = 새 회귀 계수(w0)

- 갱신 값이 너무 큰 경우, 올바른 회귀 모델을 구하기 힘들어짐

 -> 학습률 learning rate를 통해 조금씩 오차를 조정해 나감.

 

 

 

 

 

 

2차원 데이터로 부터 단순 선형 회귀 모델 구하기

 

 

1. 데이터 준비

import numpy as np
import matplotlib.pyplot as plt

# y = w0 + w1 * x 단순 선형 회귀 식 
# y = 4x + 6(w0 = 6, w1= 4)에 대한 선형 근사를 위해 값 준비
x = 2 * np.random.rand(100, 1) # 0 ~ 2까지 100개 임의의 점
y = 6 + 4 * x + np.random.randn(100, 1) # 4x + 6 + 정규 분포를 따르는 노이즈

plt.scatter(x, y)

 

 

 

 

2. 주어진 데이터에 적합한 회귀 모델 구하기

- 가중치(회귀 계수) 1로 초기화 -> 가중치 갱신 과정 수행(get_weight_update)

- x에 대한 추정한 y를 구함 -> 실제 y - 추정 y로 오차 diff 구함.

- 위에서 구한 가중치 갱신 공식에 따라 w0, w1의 갱신 값을 구함(w0/w1_update)

- 가중치 - 가중치 갱신 값. 이 연산을 지정한 횟수 만큼 수행

=> 회귀 계수는 일정한 값으로 수렴함 : 선형 회귀 모델의 회귀 계수.

def get_weight_update(w1, w0, x, y, learning_rate=0.01):
    # y는 길이가 100인 벡터, 길이 가져옴
    N = len(y)
    
    # 계수 w0, w1 갱신 값을 계수 w0, w1 동일한 형태로 초기화
    w1_update = np.zeros_like(w1)
    w0_update = np.zeros_like(w0)
    
    # 주어진 선형 회귀 식을 통한 값 추정
    y_pred = np.dot(x, w1.T) + w0
    # 잔차 y - hat_y
    diff = y - y_pred
    
    # (100, 1) 형태의 [[1, 1, ..., 1]] 행렬 생성, diff와 내적을 구하기 위함.
    w0_factors = np.ones((N, 1))
    
    # 우측의 식은 MSE를 w1과 w0에 대해 편미분을 하여 구함.
    # d mse/d w0 = 0 이 되게하는 w0이 mse의 최소로 함
    # d mse/d w1 = 0 이 되게하는 w1이 mse를 최소로 함
    # 급격한 w0, w1 변화를 방지 하기 위해 학습률 learning_rate 사용
    
    w1_update = -(2/N) * learning_rate * (np.dot(x.T, diff))
    w0_update = -(2/N) * learning_rate * (np.dot(w0_factors.T, diff))
    
    return w1_update, w0_update

def gradient_descent_steps(X, y, iters= 10000):
    
    w0 = np.zeros((1, 1))
    w1 = np.zeros((1, 1))
    
    for idx in range(0, iters):

        w1_update, w0_update = get_weight_update(w1, w0, X, y, learning_rate=0.01)
        # 갱신 값으로 기존의 w1, w0을 조정해 나감
        w1 = w1 - w1_update
        w0 = w0 - w0_update
    
    return w1, w0

def get_cost(y, y_pred):
    N = len(y)
    cost = np.sum(np.square(y- y_pred))/N
    return cost

w1, w0 = gradient_descent_steps(x, y, iters=1000)
print("w1:{0:.4f}, w0:{0:.4f}".format(w1[0, 0], w0[0, 0]))
y_pred = w1[0, 0] * x + w0
print("gradient descent total cost : {0:.4f}".format(get_cost(y, y_pred)))

 

 

 

 

 

 

확률적 경사 하강법 stochastic gradient descent

- 위의 경사 하강법을 이용한 계수 추정 과정에서 주어진 모든 데이터를 사용함

 -> 반복 횟수, 변수, 데이터 량이 많을 수록 느려짐

- 전체 데이터가 아닌 임의로 추출한 일부 데이터만 사용

- 아래의 경우 위(전체 데이터 100개)와 달리 10개만 추출하여 회귀 계수를 구함

def stochastic_gradient_descent_steps(x, y, batch_size=10, iters=1000):
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    prev_cost = 1000000
    iter_idx = 0
    
    for idx in range(iters):
        # x의 크기만큼 임의의 인덱스 추출
        stochastic_random_idx = np.random.permutation(x.shape[0])
        # 임의의 인덱스의 x, y를 배치 사이즈만큼 샘플링
        sample_x = x[stochastic_random_idx[0:batch_size]]
        sample_y = y[stochastic_random_idx[0:batch_size]]
        
        w1_update, w0_update = get_weight_update(w1, w0, sample_x, sample_y,learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
    return w1, w0

w1, w0 = stochastic_gradient_descent_steps(x, y, iters=1000)
print("w1:{0:.4f}, w0:{0:.4f}".format(w1[0, 0], w0[0, 0]))
y_pred = w1[0, 0] * x + w0
print("gradient descent total cost : {0:.4f}".format(get_cost(y, y_pred)))
300x250
728x90

요즘 시험 준비랑 몇 가지 일로 블로그 관리를 제대로 못하고 있다.

 

빅데이터 분석기사 시험 관련해서 이론적인 내용은 충분히 정리한 것 같아

 

더 정리 하기 보다는 그동안 배운 내용들을 반복해서 빠르게 훑어보고 있고,

 

 

 

얼마전에 잠깐 간단한 캐글 대회(타이타닉 생존 여부)에 제대로 참가하게 되서

 

그거 하느라 신경을 더 쓰지를 못하고 있었다.

 

 

 

내 목표는

 

1. 다양한 분류기들을 성능 비교

2. 하이퍼 파라미터 튜닝 전후 비교

3. 전처리 과정 추가 후 성능 비교 등을 하려고 했으나

4. 피드백, 재조정

 

여러 분류기 모델을 다루다 보니

 

이걸 다하기에는 너무 지쳐서 2번까지만 하고 말았다

 

데이터를 제대로 처리 하지 않아서인지 생각보다 성능이 잘 나오지는 않더라

 

 

 

그래도 캐글 대회에 (제대로는) 처음 참여하면서

 

노트북 작성 방법, 제출 방법 등 캐글을 어떻게 참여하는지는 알게 되었고,

 

노트북을 잘 정리한것 같아 뿌듯하긴 하다.

 

 

링크 : www.kaggle.com/dojeongchan/jdo-s-titanic

 

 

 

 

300x250

'그외 > 로그' 카테고리의 다른 글

2021.01.08 daily english study log  (0) 2021.01.08
2021.01.07 daily english study log  (0) 2021.01.07
컴퓨터 비전 알고리즘 구현 - 1. 시작  (0) 2020.11.26
논문 읽기와 구현  (0) 2020.11.23
과제 마무리  (0) 2020.11.19
728x90

ref : towardsdatascience.com/lines-detection-with-hough-transform-84020b3b1549

 

 

 

 

허프 변환 Hough Transform

- 파울 V.C 허프가 1962년 사진에 존재하는 직선들을 찾아내기 위해 제안한 알고리즘.

- 허프 변환은 컴퓨터 비전 분야에서 직선이나 원 같은 특징을 검출할때 주로 사용.

- 에지 영상(주로 케니 에지)에서 허프 변환을 통해 허프 공간(a, b 공간 표현법: 아래에서 후술)상의 교점을 구함.

- 허프 공간의 교점은 xy 공간에서 직선으로 표현됨

 

기본 허프 변환

- 기존의 이미지는 x-y 평면 상에서 표현됨

- 위에서 말한 a, b 공간은 a(기울기), b(y 절편)로 이루어진 공간을 말함.

- x, y 공간에서 한 점은, ab 공간(허프 공간)에서 한 직선으로 표현할 수 있음.

- xy 공간 상에 점이 하나 더 생기면 a-b 공간상의 교점이 생긴다.

 

- a-b 공간 상의 교점이 두 점이 지나는 직선의 기울기와 y 절편 값이 된다.

 => 이렇게 하여 ab 표현을 이용한 직선 검출이 수행.

 

 

ab 공간의 문제

- 아래의 좌측과 같은 에지 영상이 주어지면, 이들은 대부분 ab 공간 상에서 하나의 교점에서 만날것임

- 하지만 에지가 수직인 경우, ab 공간에서 b가 무한대가 되버리는 문제 발생

 -> 대안으로 rho, phi를 이용한 법선 normal line 표현법을 사용함.

 * 법선 normal line : 곡선의 한 점을 지나며, 접선에 수직인 직선

 

 

법선을 이용한 허프 변환

- 두 점을 지나는 법선은 원점까지 수직인 거리가 rho, 이때 각을 phi로 표현

- 수직 거리 rho, 각도 phi의 관계 : rho = x cos(phi) + y sin(phi)

- 하단의 우측 그림처럼 이전과 마찬가지로 rho, phi 공간 상에서 교점이 xy 공간상에서 직선을 의미함.

- ab 공간떄와는 달리 에지가 수직 혹은 수평의 형태인 경우에도 rho나 phi가 무한대로 발산하지 않음.

 

정리한 내용은 얼마 안되는데 시간이 너무 늦어졌다...

 

구현은 내일로

300x250
728x90

이미지 그라디언트를 마치고 허프 변환을 어떻게 구현할까 생각해보고 있었는데,

 

opencv를보니 에지 영상에서 허프 변환을 수행했었다.

 

그런데 내가 만든 이미지 그라디언트는 커널 사이즈도 3 x 3으로 고정되어 있고,

 

그렇게 좋은 이미지들을 만드는것 같지는 않아보였다.

 

 

그래서

 

이미지 그라디언트 구현을 공부하면서 조금 연습했으니

 

이번에는 opencv 문서를 보면서 캐니 에지 검출기를 구현해 보려고 한다.

 

 

ref 

docs.opencv.org/master/da/d22/tutorial_py_canny.html

 

 

캐니 에지 검출기 canny edge detector

- 존 F 캐니가 만든 많이 쓰이는 에지 검출기

- 아래의 단계로 구성

 

 

캐니 에지 검출기 구성 단계

1. 노이즈 제거

- 에지 검출기는 노이즈에 강인해야 한다. 그러니 우선  5 x 5 가우시안 필터로 노이즈를 제거하자.

 

2. 이미지 그라디언트 강도, 방향 구하기

- 이미지를 가우시안 필터로 블러링 해주고 나서, 소벨 커널로 수평, 수직 방향으로 필터링해서 그라디언트 구하기

- 이미지 그라디언트를 연산해서 에지 그라디언트 강도와 방향을 구해주자.

* 그라디언트 방향은 에지에 수직.

* 에지 방향 각도는 아래의 식으로 구한 후 반올림을 하여 (0, 45, 90, 135도)  중 하나가 되도록 한다.

 

 

3. 비최대 억제 Non-maximu suppression

- 그라디언트 크기와 방향을 구했다면, 필요없는 픽셀들을 없앨 준비가 되었다.

- 그러려먼 우선 모든 픽셀들에 대해 에지 그라디언트 방향 부근의 픽셀들이 지역 최대치인지 확인하여야 한다.

- 아래 그림을 보면 점 A는 에지이고, 그라디언트 방향은 에지에 수직이며, 점 B와 C는 그라디언트 방향에 있다.

- 점 B와 C를 보고 A가 지역 최대점인지 판단한다. A가 지역 최대가 아니라면, 억제되어 0이 된다.

 

4. 이력 현상 임계화 hystersis thresholding(이력 현상 : 이전의 상태가 현재에 영향을 주는 현상)

- 이제 모든 에지들이 정말로 에지인지 아닌지 판단하기 위해서 두 임계값 minVal, maxVal이 필요하다.

- 그라디언트 강도가 max val보다 크면 에지이고, min val보다 작으면 에지가 아니라고 판단해서 버린다.

- 만약 임계치에 있는 경우에는 연결성을 고려해서 에지 여부를 판단한다.

- 아래의 에지 A는 maxVal보다 크니 확실히 에지로 보지만, 에지 C는 에지 A에 연결되었으니 에지로본다.

- 하지만 에지 B는 minVal보다 크고, 에지 C랑 같은 곳에 있지만 A랑 연결이 안되있으므로 에지가 아닌것으로 본다.

 

 

 

 

 

2020. 12. 1  1:50

- 위 이론 내용 정리 후 자료 찾아가면서 구현하다보니 늦어졌다.

- 코드 일부분, 시각화 결과를 우선 보인 후 전체 코드를 맨 뒤에다가 배치.

 

 

전체 구성

1. 기본 이미지 시각화

2. 가우시안 필터링 적용

3. 이미지 그라디언트 방향 영상, 그라디언트 크기 영상 시각화

4. 비최대 억제 결과 시각화

5. 이력 현상 임계화 추가 -> 캐니 에지 검출기 완성

6. 캐니 에지 검출 과정 모든 이미지 시각화

7. 전체 코드

 

 

후기

- 기존의 opencv에서 제공하는 함수에 비하면 원하는 결과도 잘 나오지 않고, 속도도 매우 느린 편이다.

- 하지만 직접 수식, 개념들을 완전히는 아니더라도 조금씩 이해해 가면서 구현하는 과정이 좋았다.

- 이걸 하는 중간에 보니 내가 연결성 부분이나 범람 채움 알고리즘 같은 내용들을 넘어갔던것 같다. 내일 한번 봐야할듯.

 

 

 

 

1. 기본 이미지 시각화

- opencv 예제에서 제공하는 기본 messi5.jpg

import numpy as np
import matplotlib.pyplot as plt
from utils import padding, gaussian_filtering
from PIL import Image
import cv2

plt.figure(figsize=(8,12))
img = Image.open("./res/messi5.jpg").convert("L")
img = np.asarray(img)
plt.imshow(img, cmap="gray")

 

2. 가우시안 필터링 적용

- opencv doc에서 kernel size가 5인 가우시안 커널을 사용한다고 한다. 시그마는 0.8로 지정

plt.figure(figsize=(8,12))
img_blur = gaussian_filtering(img,k_size=5, sigma=0.8)
plt.imshow(img_blur,cmap="gray")

 

 

 

 

 

3. 이미지 그라디언트 방향 영상, 그라디언트 크기 영상 시각화

 

grad_intensity = get_gradient_intensity(img_blur)
plt.title("gradient direction image")
plt.imshow(grad_intensity[:,:,0])

 

 

plt.title("gradient magnitude image")
plt.imshow(grad_intensity[:,:,1])

 

 

 

 

 

4. 비최대 억제 결과 시각화

- 그라디언드 방향에 존재하는 픽셀 세 개 비교.

- 중앙 픽셀이 가장 크면 나두고, 같은 방향 다른 픽셀이 크면 억제하여 0

 

supressed_img = non_maximum_suppression(grad_intensity)
plt.imshow(supressed_img)

 

 

 

 

 

5. 이력 현상 임계화 추가 -> 캐니 에지 검출기 완성

 

 

 

img = Image.open("./res/messi5.jpg").convert("L")
img = np.asarray(img)
img_blur = gaussian_filtering(img,k_size=5, sigma=1)
grad_intensity = get_gradient_intensity(img_blur)
suppressed_img = non_maximum_suppression(grad_intensity)
canny_edge = hysteresis_threshold(suppressed_img=suppressed_img,min_th=100, max_th=200)


plt.figure(figsize=(64,32))
plt.subplot(6, 1, 1)
plt.title("original")
plt.imshow(img)
plt.subplot(6, 1, 2)
plt.title("gaussian blur")
plt.imshow(img_blur)
plt.subplot(6, 1, 3)
plt.title("gradient direction")
plt.imshow(grad_intensity[:, :,0])
plt.subplot(6, 1, 4)
plt.title("gradient magnitude")
plt.imshow(grad_intensity[:, :,1])
plt.subplot(6, 1, 5)
plt.title("non maximum suppression ")
plt.imshow(suppressed_img)
plt.subplot(6, 1, 6)
plt.title("canny edge image")
plt.imshow(canny_edge)

 

 

 

 

 

6. 캐니 에지 검출 과정 모든 이미지 시각화

 

def canny_edge_visualize(img, min_th=100, max_th=200):
    """
    canny_edge_visualize
    - visualize all images from original to canny edge
    
    parameter
    - img : original image
    - min_th : minimal threshold value
    - max_th : maximum threshold value
    """
    img_blur = gaussian_filtering(img,k_size=5, sigma=1)
    grad_intensity = get_gradient_intensity(img_blur)
    suppressed_img = non_maximum_suppression(grad_intensity)
    canny_edge = hysteresis_threshold(suppressed_img=suppressed_img,min_th=100, max_th=200)

    plt.figure(figsize=(64,32))
    plt.subplot(6, 1, 1)
    plt.title("original")
    plt.imshow(img)
    plt.subplot(6, 1, 2)
    plt.title("gaussian blur")
    plt.imshow(img_blur)
    plt.subplot(6, 1, 3)
    plt.title("gradient magnitude")
    plt.imshow(grad_intensity[:, :,0])
    plt.subplot(6, 1, 4)
    plt.title("gradient direction")
    plt.imshow(grad_intensity[:, :,1])
    plt.subplot(6, 1, 5)
    plt.title("non maximum suppression ")
    plt.imshow(suppressed_img)
    plt.subplot(6, 1, 6)
    plt.title("canny edge image")
    plt.imshow(canny_edge)
   
img = Image.open("./res/kid.jpg").convert("L")
img = np.asarray(img)
canny_edge_visualize(img)

 

 

 

 

 

 

7. 전체 코드

"""
Canny Edge Detector
- 2020. 12.1 01:50

"""


"""
ref
- https://en.wikipedia.org/wiki/Canny_edge_detector
"""
def sobel_kerenl():
    kernel_x = np.array([
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ])
    kernel_y = np.array([
        [1, 2, 1],
        [0, 0, 0],
        [-1, -2, -1]
    ])
    return kernel_x, kernel_y

def get_rounded_gradient_angle(ang):
    """
    get_rounded_gradient_angle
    - gradient direction angle round to one of four angles
    
    return : one of four angles
            representing vertical, horizontal and two diagonal direction
    
    parameteres
    ---------------
    ang : gradient direction angle
    """
    vals = [0, np.pi*1/4, np.pi*2/4, np.pi*3/4, np.pi*4/4]
    interval = [np.pi*1/8, np.pi*3/8, np.pi * 5/8, np.pi* 7/8]

    if ang < interval[0] and ang >= interval[-1]:
        ang = vals[0]
    elif ang < interval[1] and ang >= interval[0]:
        ang = vals[1]
    elif ang < interval[2] and ang >= interval[1]:
        ang = vals[2]
    elif ang < interval[3] and ang >= interval[2]:
        ang = vals[3]
    else:
        ang = vals[4]
    return ang
        

def get_gradient_intensity(img):
    """
    get gradient_intensity
    - calculate gradient direction and magnitude
    
    return (rows, cols, 2) shape of image about grad direction and mag
    
    parameteres
    ------------
    img : blured image
    """
    k_size = 3
    rows, cols = img.shape
    kernel_x, kernel_y = sobel_kerenl()
    pad_img = padding(img, k_size=k_size)
    res = np.zeros((rows, cols, 2))
    sx, sy = 0, 0

    for i in range(0, rows):
        for j in range(0, cols):
            boundary = pad_img[i:i+k_size, j:j+k_size]
            sx = np.sum(kernel_x * boundary)
            sy = np.sum(kernel_y * boundary)            
            ang = np.arctan2(sy, sx)
            mag = abs(sx) + abs(sy)
            if ang < 0:
                ang = ang + np.pi
            ang = get_rounded_gradient_angle(ang)
            res[i, j, 0] = ang
            res[i, j, 1] = mag
    return res



def check_local_maximum(direction=None, boundary=None):
    """
    check_local_maximum
    - check if center value is local maximum depend on gradient direction
    return True if center valus is local maximum
    
    parameter
    ------------
    direction : gradient direction
    boundary : region of image for finding local maximum
    """
    if direction == 0: # 0 degree, east and west direction
        kernel = np.array([
            [0, 0, 0],
            [1, 1, 1],
            [0, 0, 0]
        ])

    elif direction == 1: #45 degree, north east and south west direction
        kernel = np.array([
            [0, 0, 1],
            [0, 1, 0],
            [1, 0, 0]
        ])
    elif direction == 2: #90 degree, north & south direction
        kernel = np.array([
            [0, 1, 0],
            [0, 1, 0],
            [0, 1, 0]
        ])
    else : #135 degree, north west & south east direction
        kernel = np.array([
            [1, 0, 0],
            [0, 1, 0],
            [0, 0, 1]
        ])
    
    max_val = np.max(kernel * boundary)
    if boundary[1,1] == max_val: #local maximum
        return True
    else: # not local maximu
        return False

    
    
def non_maximum_suppression(grad_intensity=grad_intensity):
    """
    non_maximum_suppression
    - check a pixel wiht it's neighbors in the direction of gradient
    - if a pixel is not local maximum, it is suppressed(means to be zero)
    """
    directions = [0, np.pi*1/4, np.pi*2/4, np.pi*3/4, np.pi*4/4]
    
    k_size = 3
    grad_direction = grad_intensity[:, :, 0]
    grad_magnitude = grad_intensity[:, :, 1]
    
    rows, cols = grad_magnitude.shape
    pad_img = padding(grad_magnitude, k_size=k_size)
    res_img = np.zeros((rows,cols))
    sx, sy = 0, 0
    
    
    for i in range(0, rows):
        for j in range(0, cols):
            direction = directions.index(grad_direction[i,j])
            boundary = pad_img[i:i+k_size, j:j+k_size]
            if check_local_maximum(direction, boundary) == True:
                res_img[i, j] = grad_magnitude[i, j]
            else:
                res_img[i, j] = 0
    return res_img



def hysteresis_threshold(suppressed_img=None, min_th=None, max_th=None):
    """
    hysterisis_threshold
    - check edge is connected to strong edge using eight connectivity
    
    parameter
    ---------------------
    - suppressed img : non maximum suppressed image
    - min_th : minimal threshold value
    - max_th : maximum threshold value
    """
    k_size = 3
    rows, cols = supressed_img.shape
    pad_img = padding(suppressed_img, k_size=k_size)
    res_img = np.zeros((rows,cols))

    eight_connectivity = np.array([
        [1, 1, 1],
        [1, 0, 1],
        [1, 1, 1]
    ])    
    
    for i in range(0, rows):
        for j in range(0, cols):
            if pad_img[i+1, j+1] < min_th:
                res_img[i, j] = 0
            else:
                boundary = pad_img[i:i+k_size, j:j+k_size]
                max_magnitude = np.max(boundary * eight_connectivity)
                if max_magnitude >= max_th : # pixel is connected to real edge
                    res_img[i, j] = 255
                else:
                    res_img[i, j] = 0
    return res_img


def canny_edge_detector(img=None, min_th=None, max_th=None):
    """
    canny_edge_detector
    - detect canny edge from original image
    
    parameter
    - img : original image
    - min_th : minimal threshold value
    - max_th : maximum threshold value
    """
    img_blur = gaussian_filtering(img,k_size=5, sigma=1)
    grad_intensity = get_gradient_intensity(img_blur)
    suppressed_img = non_maximum_suppression(grad_intensity)
    canny_edge = hysteresis_threshold(suppressed_img=suppressed_img,
                                      min_th=min_th, max_th=max_th)
    return canny_edge


def canny_edge_visualize(img, min_th=100, max_th=200):
    """
    canny_edge_visualize
    - visualize all images from original to canny edge
    
    parameter
    - min_th : minimal threshold value
    - max_th : maximum threshold value
    """
    img_blur = gaussian_filtering(img,k_size=5, sigma=1)
    grad_intensity = get_gradient_intensity(img_blur)
    suppressed_img = non_maximum_suppression(grad_intensity)
    canny_edge = hysteresis_threshold(suppressed_img=suppressed_img,min_th=100, max_th=200)

    plt.figure(figsize=(64,32))
    plt.subplot(6, 1, 1)
    plt.title("original")
    plt.imshow(img)
    plt.subplot(6, 1, 2)
    plt.title("gaussian blur")
    plt.imshow(img_blur)
    plt.subplot(6, 1, 3)
    plt.title("gradient magnitude")
    plt.imshow(grad_intensity[:, :,0])
    plt.subplot(6, 1, 4)
    plt.title("gradient direction")
    plt.imshow(grad_intensity[:, :,1])
    plt.subplot(6, 1, 5)
    plt.title("non maximum suppression ")
    plt.imshow(suppressed_img)
    plt.subplot(6, 1, 6)
    plt.title("canny edge image")
    plt.imshow(canny_edge)

    
300x250
728x90

이번에 뭘 다뤄야 하나 생각하다가

 

opencv document를 보니

 

이번에는 이미지 그라디언트를 볼 차래더라

 

 

 

이미지 그라디언트는 이미지의 기울기를 구하는 것을 말한다.

 

이 기울기는 영상내 급격히 변하는 곳으로 영상 내 물체의 윤곽선(에지)가 된다.

 

이에 대한 설명은 다음 링크와 cmu 슬라이드에서 잘 설명해주고 있다.

 

ref 

iskim3068.tistory.com/49

www.cs.cmu.edu/~16385/s17/Slides/4.0_Image_Gradients_and_Gradient_Filtering.pdf

 

 

그러면 이미지 그라디언트 코드를 구현해보자.

 

우선 소벨 필터를 이용한 이미지 그라디언트

 

"""
ref
- https://en.wikipedia.org/wiki/Edge_detection
- http://www.cs.cmu.edu/~16385/s17/Slides/4.0_Image_Gradients_and_Gradient_Filtering.pdf
- https://iskim3068.tistory.com/49
"""
def sobel_kerenl():
    kernel_x = np.array([
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ])
    kernel_y = np.array([
        [1, 2, 1],
        [0, 0, 0],
        [-1, -2, -1]
    ])
    return kernel_x, kernel_y

def sobel(img, method=None):
    """
    get image gradient using sobel operater
    
    parameteres
    ------------
    img : input image applying sobel filter
    method : 1(x direction), 2(y dicrection), 3(x + y direction)
    """
    k_size = 3
    rows, cols = img.shape
    kernel_x, kernel_y = sobel_kerenl()
    pad_img = padding(img, k_size=k_size)
    res_img = np.zeros((rows,cols))
    sx, sy = 0, 0

    for i in range(0, rows):
        for j in range(0, cols):
            boundary = pad_img[i:i+k_size, j:j+k_size]
            if method == 1:
                sx = np.sum(kernel_x * boundary)
            elif method == 2:
                sy = np.sum(kernel_y * boundary)
            else:
                sx = np.sum(kernel_x * boundary)
                sy = np.sum(kernel_y * boundary)
            res_img[i,j] = np.sqrt(sx**2 + sy**2)
    
    return res_img

 

 

x 방향, y 방향, x+y방향 영상 시각화 결과

 

plt.figure(figsize=(12,12))
for i in range(1, 4):
    plt.subplot(2, 2, i)
    sobel_img = sobel(img, method=i)
    plt.imshow(sobel_img,cmap="gray")

 

 

 

라플라시안 그라디언트 영상

def laplacian_filter():
    kernel_x = np.array([
        [0, 1, 0],
        [1, -4, 1],
        [0, 1, 0]
    ])
    kernel_y = np.array([
        [0, -1, 0],
        [-1, 4, -1],
        [0, -1, 0]
    ])
    return kernel_x, kernel_y


def laplacian(img):
    """
    get image gradient using laplacian filter
    
    parameteres
    ------------
    img : input image applying laplacian filter
    """
    k_size = 3
    rows, cols = img.shape
    kernel_x, kernel_y = laplacian_filter()
    pad_img = padding(img, k_size=k_size)
    res_img = np.zeros((rows,cols))
    sx, sy = 0, 0

    for i in range(0, rows):
        for j in range(0, cols):
            boundary = pad_img[i:i+k_size, j:j+k_size]
            sx = np.sum(kernel_x * boundary)
            sy = np.sum(kernel_y * boundary)
            res_img[i,j] = np.sqrt(sx**2 + sy**2)    
    return res_img
 
laplacian_img = laplacian(img)
plt.imshow(laplacian_img,cmap="gray")

 

 

 

 

300x250
728x90

모폴로지 연산

- 컴퓨터 비전의 영역 연산 중 한 종류

- 이미지를 깍거나 팽창시켜준다. -> 침식, 팽창 연산

- 침식 연산과 팽창 연산을 이용하여 열림 연산과 닫힘 연산이 가능.

- 열림 연산 : 침식 -> 팽창. 이미지를 깍아 노이즈를 제거, 깍여진 이미지를 팽창하여 원래 형태로 복원

- 닫힘 연산 : 팽창 ->침식. 이미지를 팽창시켜, 내부 구멍들을 채움, 팽창된 이미지를 침식하여 원래 형태로

 

 

 

 

1. 이미지 로드. 기본 이미지 시각화

 

import numpy as np
import matplotlib.pyplot as plt
from utils import Histogram, Threshold, padding
from PIL import Image


img = Image.open("./res/j.png").convert("L")
img = np.asarray(img)

plt.imshow(img, cmap="gray")

 

 

 

 

 

 

2. 모폴로지 연산 구현

def erosion(boundary=None, kernel=None):
    """
    erosion operation
    - a pixel element is 0 at least under kernel is 0
    -> return 0
    """
    boundary = boundary * kernel
    if (np.min(boundary) == 0):
        return 0
    else:
        return 255

def dilation(boundary=None, kernel=None):
    """
    erosion operation
    - a pixel element is not 0 at least under kernel is not 0
    -> return 255
    """
    boundary = boundary * kernel
    if (np.max(boundary) != 0):
        return 255
    else:
        return 0

def openning(img=None, k_size=None):
    """
    openning operation
    - erosion followed by dilation
    - it removes noise
    """
    erosion_img = morphology(img=img, method=1, k_size=k_size)
    opened_img = morphology(img=erosion_img, method=2, k_size=k_size)
    return opened_img

def closing(img=None, k_size=None):
    """
    closing operation
    - dilation follwed by erosion
    - it can close small holes inside the objects. 
    """
    dilation_img = morphology(img=img, method=2, k_size=k_size)
    closed_img = morphology(img=dilation_img, method=1, k_size=k_size)
    return closed_img


def morphology(img=None, method=None, k_size=None):
    """
    input
    img : input image
    method : 1(erosion), 2(dilation), 3(openning), 4(closing)
    k_size : kernel size
    
    output
    res_img : morphology operation image
    """
    rows, cols = img.shape
    pad_img = padding(img, k_size)
    kernel = np.ones((k_size,k_size))
    res_img = img.copy()
    
    if method == 1 or method == 2:
        for i in range(0, rows):
            for j in range(0, cols):
                if method == 1: #erosion operation
                    res_img[i, j] = erosion(pad_img[i:i+k_size, j:j+k_size], kernel)
                elif method == 2: #
                    res_img[i, j] = dilation(pad_img[i:i+k_size, j:j+k_size], kernel)
    if method == 3:
        res_img = openning(img, k_size=k_size)
    elif method == 4:
        res_img = closing(img, k_size=k_size)

    return res_img

 

 

 

3. 침식 연산 결과

res = morphology(img, method=1, k_size=5)
plt.title("erosion operation with k_size = 5")
plt.imshow(res, cmap="gray")

 

 

 

4. 팽창 연산 결과

res = morphology(img, method=2, k_size=5)
plt.title("dilation operation with k_size = 5")
plt.imshow(res, cmap="gray")

 

 

5. 열기 연산

5.1 열기 연산 이전. 노이즈 이미지

 

noised_j = Image.open("./res/noised_j.PNG").convert("L")
noised_j = np.asarray(noised_j)
plt.title("befor opening operation")
plt.imshow(noised_j, cmap="gray")

 

5.2 열기 연산 수행

res = morphology(noised_j, method=3, k_size=7)
plt.title("after opening operation with k_size :7")
plt.imshow(res, cmap="gray")

 

 

 

6. 닫기 연산

6.1 닫기 연산 전. 이미지

 

small_hole = Image.open("./res/j_small_hole.PNG").convert("L")
small_hole = np.asarray(small_hole)
plt.title("before closing operation")
plt.imshow(small_hole, cmap="gray")

 

 

6.2 닫기 연산 후

res = morphology(small_hole, method=4, k_size=5)
plt.title("after closing operation with k_size :7")
plt.imshow(res, cmap="gray")
300x250
728x90

스태킹 Stacking

- 여러 모델들을 결합하여 결과를 도출하는 앙상블 기법 중 하나.

- 각 모델들의 예측 결과를 학습하여 최종 예측결과를 도출

 

 

1. 라이브러리 임포트

- 기본 모델로 knn, random forest, adaboost, decisiont tree 4가지

- 마지막 모델로 logistic regression

import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from utils.common import show_metrics

 

 

2. 데이터 로드 및 조회

- 행 569, 열 30개 데이터

- 악성과 양성사이 큰 비율 차이는 없음

data = load_breast_cancer()
X = data.data
y = data.target
print(data.DESCR)

import pandas as pd
print(pd.Series(y).value_counts())

 

 

 

3. 각 분류기 학습, 성능 확인

- 각 분류기 학습 및 성능 출력, 예측 데이터 쌓기

- lr_final에 학습 용 데이터를 만들기 위해 예측 데이터 shape가 (4, 114) 인것을 전치. 114행 4열 데이터로 변환

 

def get_model_train_eval(model, ftr_train=None, ftr_test=None,
                        tgt_train=None, tgt_test=None):
    model.fit(ftr_train, tgt_train)
    y_pred = model.predict(ftr_test)
    show_metrics(y_test, y_pred)
    return model, y_pred

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                   random_state=10)
knn = KNeighborsClassifier()
rf = RandomForestClassifier(random_state=10)
dt = DecisionTreeClassifier()
ada = AdaBoostClassifier()
lr_final = LogisticRegression()

clfs = [knn, rf, dt, ada]
y_preds = []
for idx, clf in enumerate(clfs):
    print("\n", clf.__class__.__name__)
    clfs[idx], y_pred = get_model_train_eval(clf, ftr_train=X_train, ftr_test=X_test,
                        tgt_train=y_train, tgt_test=y_test)
    y_preds.append(y_pred)

y_preds = np.array(y_preds)
print(y_preds.shape)
y_preds = y_preds.T

 

 

4. 최종 스태킹 모델 학습 결과

- 랜덤 포레스트 하나만 사용한 경우 보다는 성능이 저하되었으나, 타 분류기들보다는 개선된 결과를 보임

- 실제 사용시 하이퍼 파라미터를 최적으로 튜닝한 후에 사용하여야 함.

 

final, y_pred = get_model_train_eval(lr_final, ftr_train=y_preds, ftr_test=y_preds,
                    tgt_train=y_test, tgt_test=y_test)

 

300x250
728x90

캐글 신용사기 검출 대회

- 2013년 9월 유럽 신용카드 트랜잭션 데이터

- 2일간 284,807 트랜잭션중 492건이 사기로 전체중 0.172%뿐으로 데이터가 매우 불균형함

ref : www.kaggle.com/mlg-ulb/creditcardfraud

 

 

 

The datasets contains transactions made by credit cards in September 2013 by european cardholders.
This dataset presents transactions that occurred in two days, where we have 492 frauds out of 284,807 transactions. The dataset is highly unbalanced, the positive class (frauds) account for 0.172% of all transactions.

 

 

 

 

언더 샘플링, 오버샘플링

- 불균형 레이블 분포를 적절한 학습데이터로 만드는 방법, 오버 샘플링이 유리

- 언더 샘플링 : 클래스가 많은 데이터를 클래스가 적은 데이터 만큼 축소

 ex. 정상 10,000건, 비정상 100건시 정상을 100건으로 줄임. *너무 많은 정상 데이터를 제거

- 오버 샘플링 : 클래스가 적은 데이터를 클래스가 많은 데이터 만큼 증가

 * 단순 증강 시 오버피팅이 발생. 원본 데이터 피처를 약간씩 변형하여 증감

 ex. SMOTE(Syntheic Minority Over sampling techinuqe : knn으로 적은 클래스 데이터 간의 차이로 새 데이터 생성

 * imbalanced-learning 사용

 

 

 

 

 

 

 

1. 데이터로드

 

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

path = "./res/credit card fraud detection/creditcard.csv"
df = pd.read_csv(path)
df.head()

 

2. 전처리 및 훈련, 테스트 데이터 셋 분리 정의

 

from sklearn.model_selection import train_test_split

def get_preprocessed_df(df=None):
    """
    input
    df : before preprocessing

    output
    res : after dropping time columns
    """
    res = df.copy()
    res.drop("Time", axis=1, inplace=True)
    return res

def get_train_test_datasets(df=None):
    df_copy = get_preprocessed_df(df)
    X_features = df_copy.iloc[:,:-1]
    y_target= df_copy.iloc[:,-1]
    
    X_train, X_test, y_train, y_test = train_test_split(X_features, y_target,
                                                       test_size=0.2,
                                                       stratify=y_target,
                                                       random_state=100)
    return X_train, X_test, y_train, y_test
    

 

 

 

3. 로지스틱 회귀 모델로 성능 확인

- 정확도는 좋으나 재현률과 F1 스코어가 크게 떨어짐

 

X_train, X_test, y_train, y_test = get_train_test_datasets(df)

from sklearn.linear_model import LogisticRegression
from utils.common import show_metrics


lr = LogisticRegression()
lr.fit(X_train, y_train)
y_pred = lr.predict(X_test)
show_metrics(y_test, y_pred)

 

 

 

4. 모델, 데이터를 줄때 학습 및 성능을 출력하는 함수 정의

 

def get_model_train_eval(model, ftr_train=None, ftr_test=None,
                        tgt_train=None, tgt_test=None):
    model.fit(ftr_train, tgt_train)
    y_pred = model.predict(ftr_test)
    show_metrics(y_test, y_pred)
    return model

 

 

 

5. LightGBM 성능 확인

- LR 보다 평가 지표들이 좋은 결과를 보임

 

from lightgbm import LGBMClassifier

lgbm = LGBMClassifier(n_estimators=1000, num_leaves=32,
                       n_jobs=-1, boost_from_average=False)
lgbm = get_model_train_eval(lgbm, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test = y_test)

 

 

6. 거래 금액 시각화

 

- df의 time, amount를 제외한 컬럼들은 PCA를 통해 얻은 주성분 요소들

- 로지스틱 회귀는 정규분포를 따르는 데이터를 사용하는것이 좋음 -> 표준화 정규기 사용

sns.distplot(df["Amount"])

 

 

 

7. amount 정규화 후 성능 비교

- 로지스틱 분류기나 LGBM이나 큰 성능 차이가 생기지는 않음

from sklearn.preprocessing import StandardScaler

def get_preprocessed_df(df=None):
    res = df.copy()
    scaler = StandardScaler()
    #res["Amount_Scaled"] = scaler.fit_transform(df["Amount"].values.reshape(-1,1))
    amount_scaled = scaler.fit_transform(df["Amount"].values.reshape(-1,1))
    res.insert(0, "Amount_Scaled", amount_scaled)
    res.drop(["Time","Amount"], axis=1, inplace=True)
    return res
    
X_train, X_test, y_train, y_test = get_train_test_datasets(df)
lr = LogisticRegression()
print("logistic regression classification evaluation")
get_model_train_eval(model=lr, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test=y_test)

lgbm = LGBMClassifier(n_estimators=1000, num_leaves=64,
                      n_jobs=-1,boost_from_average=False)
print("\nLGBM classification evaluation")
get_model_train_eval(model=lgbm, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test=y_test)

 

 

 

8. 로그 변환후 성능 비교

- 라벨 분포가 심하게 왜곡된 경우 사용

- log 연산을 통해 매우 큰값도 작은 값으로 변환

 -> 큰 변화는 생기지 않아보임. 교차 검증 필요

 

 

def get_preprocessed_df(df=None):
    res = df.copy()
    amount_scaled = np.log1p(res["Amount"])
    res.insert(0, "Amount_Scaled", amount_scaled)
    res.drop(["Time","Amount"], axis=1, inplace=True)
    return res


X_train, X_test, y_train, y_test = get_train_test_datasets(df)
lr = LogisticRegression()
print("logistic regression classification evaluation")
get_model_train_eval(model=lr, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test=y_test)

lgbm = LGBMClassifier(n_estimators=1000, num_leaves=64,
                      n_jobs=-1, boost_from_average=False)
print("\nLGBM classification evaluation")
get_model_train_eval(model=lgbm, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test=y_test)

 

 

9. 이상치 제거를 위한 히트맵 시각화

- 클래스와 가장 강한 음의 상관 관계를 갖는 변수로 V17이 있는걸 확인할수 있음.

card_df = get_preprocessed_df(df)
plt.figure(figsize=(12,12))
corr = card_df.corr()
sns.heatmap(corr, cmap="RdBu")

 

 

10. 이상치 제거 후 성능 비교

- 25% - 1.5 * IQR보다 작거나 75% + 1.5 * IQR보다 큰 경우 아웃라이어판단.

- V17의 아웃라이어들 제거 후 성능 평가.

- LGBM의 평가 지표들이 약간 개선됨

def get_outlier(df=None, column=None, weight=1.5):
    fraud = df[df["Class"] == 1][column]
    quatile_25 = np.percentile(fraud.values, 25)
    quatile_75 = np.percentile(fraud.values, 75)
    
    iqr = quatile_75 - quatile_25
    iqr_weight = iqr * weight
    lowest_val = quatile_25 - iqr_weight
    highest_val = quatile_75 + iqr_weight
    outlier_idx = fraud[(fraud < lowest_val) | (fraud > highest_val)].index
    return outlier_idx


def get_preprocessed_df(df=None):
    res = df.copy()
    amount_scaled = np.log1p(res["Amount"])
    res.insert(0, "Amount_Scaled", amount_scaled)
    res.drop(["Time","Amount"], axis=1, inplace=True)
    
    outlier_index = get_outlier(df=res, column = "V14", weight=1.5)
    res.drop(outlier_index, axis=0, inplace=True)
    return res
    
X_train, X_test, y_train, y_test = get_train_test_datasets(df)
lr = LogisticRegression()
print("logistic regression classification evaluation")
get_model_train_eval(model=lr, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test=y_test)

lgbm = LGBMClassifier(n_estimators=1000, num_leaves=64,
                      n_jobs=-1, boost_from_average=False)
print("\nLGBM classification evaluation")
get_model_train_eval(model=lgbm, ftr_train=X_train, ftr_test=X_test,
                    tgt_train=y_train, tgt_test=y_test)

 

 

 

11. SMOTE 사용하기 위한 imblanced-learn 설치. cannot import six 에러 해결(버전 매칭)

원랜 아래와 같이 하면되나

pip install imbalanced-learn

pypi.org/project/imbalanced-learn/#description

 

최신 imbalanced-learn이 scikit-learn 0.23 이상 버전을 요구

-> 자동 업그레이드 중 문제 발생했는지 cannot import six 에러로 사용불가

 

pip install -U imbalanced-learn==0.6.2

pip install -U scikit-learn==0.22.2

로 다운그레이드 하여 해결

 

 

12. over sampling

- SMOTE oversampling을 통해 작은 라밸을 큰 라밸과 크기를 맞춤

- lr의 경우 재현율은 좋아졌으나 정밀도와 f1 score는 크게저하

- lgbm은 성능 지표들이 대채로 저하

 

from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=100)
X_train_over, y_train_over = smote.fit_sample(X_train, y_train)
print("berfore smote : ",X_train.shape, y_train.shape)
print("after smote : ", X_train_over.shape, y_train_over.shape)
print("label distribution after smote \n", pd.Series(y_train_over).value_counts())

 

 

 

 

 

 

13. 성능 지표 정리

 

 

300x250

+ Recent posts