본문 바로가기

카테고리 없음

Wafer Map Defect Pattern Classification Using CNN 실습 (1)

 

안녕하세요. 톡발이입니다!

 

오늘은 제가 지난 글에서 리뷰한 CNN을 활용해 wafer map의 이상을 감지하고 분류하는 논문의 방법대로 실제로 dataset도 생성해 보고 CNN을 이용해 분류해 보는 실습을 진행해 보려고 합니다.

지난 글의 내용은 아래 링크를 통해 확인해 보실 수 있습니다.

2023.10.17 - [분류 전체 보기] - CNN으로 Wafer Map Defect Pattern 분류하기 (논문 리뷰)

 

CNN으로 Wafer Map Defect Pattern 분류하기 (논문 리뷰)

안녕하세요. 이 글에서는 CNN으로 Wafer Map Defect Pattern을 분류하는 방법을 제안하는 논문을 하나 리뷰해 보려고 합니다. "Wafer Map Defect Pattern Classification and Image Retrieval Using Convolution Neural Network"이라

aiegg-travel.tistory.com

2023.10.17 - [분류 전체 보기] - Homogeneous Poisson Process 생성

 

Homogeneous Poisson Process 생성

안녕하세요. 톡발이입니다. 제가 CNN을 이용해 wafer map defect 분류하는 논문을 리뷰한 적이 있었는데요. Wafer에 대한 정보는 회사에서 공개를 하지 않기 때문에 직접 시뮬레이션으로 dataset을 만들

aiegg-travel.tistory.com

 

실습은 google colab에서 진행되었고 pytorch를 이용했습니다.

Generating Wafer Map Defect Pattern

보통 Wafer map에 대한 정보는 회사 내에서는 비밀로 하여 외부로 공개하지 않습니다. 따라서 저희가 직접 wafer map dataset을 만들어줘야 하는데요. 어떤 wafer map defect pattern을 만들 것인지는 자유이기 때문에 정해진 정답은 없습니다. 저는 논문에서 나온 dataset과 최대한 유사하게 구현해 보았습니다.

함수 정의

먼저 코드를 짜기 위해서 필요한 module을 import 해야겠죠?

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs

numpy와 matplotlib과 달리 make_blobs는 조금 생소하실 수도 있는데요, 이 모듈을 이용해서 점들이 모여 있는 cluster를 만들 수 있습니다. 자세한 사용 방법은 밑에 dataset을 만들면서 설명드리도록 하겠습니다.

 

이제 본격적으로 defect pattern을 만들기 전에 기본적인 wafer 틀을 하나 만들어줍니다.

def wafer():
    plt.figure(figsize=(5,5))
    plt.axis('off')

    radius = 10
    margin = 1

    x = np.linspace(-radius - margin, radius + margin, 100)
    y = np.linspace(-radius - margin, radius + margin, 100)

    X, Y = np.meshgrid(x, y)
    F = X ** 2 + Y ** 2 - radius ** 2

    plt.contour(X, Y, F, [0])

반지름이 10인 Wafer 틀

그리고 dataset을 만들 때 사용할 유용한 함수들도 몇 가지 정의해 보겠습니다.

 

def f(x, y):
  return x ** 2 + y ** 2 - 10 ** 2

함수 f는 wafer의 원 함수를 나타내는 식입니다. 미리 말하자면 나중에 f(x, y) > 0인 점들은 그림에 나타나지 않도록 필터링을 해줄 것입니다.

 

def poisson(start=0, end=1):
    lam = np.random.uniform(start, end)
    while True:
      n = np.random.poisson(np.pi * radius**2 * lam)
      if n != 0: break
    return n

지난 글에서 다루었던 Homogeneous Poisson Process에서 점을 생성하는 알고리즘 중 Algorithm 3에 나온 방법대로 n 값을 구하는 코드를 구현해 보았습니다. $ \lambda $값은 사용자가 정의해서 무작위로 나올 수 있도록 해줍니다.

 

def Random_xy(start=0, end=1):
    n = poisson(start, end)

    u_list = np.random.uniform(0, 1, n)

    R_list = radius * np.sqrt(u_list)
    R_list.sort()

    u_list2 = np.random.uniform(0, 1, n)

    theta_list = 2 * np.pi * u_list2

    x = R_list * np.cos(theta_list)
    y = R_list * np.sin(theta_list)

    return x, y

Algorithm 3을 코드로 구현한 것입니다. 구간은 사용자가 임의로 지정할 수 있게 해서 적절한 n값을 뽑을 수 있게 만들었습니다. 최종 반환 값은 극좌표를 직교 좌표로 바꾼 것의 x좌표와 y좌표입니다.

 

def dense_map(x, y):
    # Density Map 생성
    density_map, _, _ = np.histogram2d(x, y, bins=100, range=[[-radius, radius], [-radius, radius]])
    max_density = np.max(density_map)

    # 정규화 및 시각화
    plt.imshow(np.where(F <= 0, density_map.T / max_density, np.nan), 
    	       extent=[-radius - margin, radius + margin, -radius - margin, radius + margin], 
               origin='lower', 
               cmap='Reds', 
               alpha=0.5)
    plt.axis('off')

마지막으로 좌표값을 입력받아서 density map으로 만들어주는 함수까지 만들어 주었습니다.

 

이제부터는 결함 점들을 어느 곳에 적절히 생성시킬지 정해주어서 non-random pattern을 만들고 그 위에 random pattern을 합해주면 됩니다.

Shape Class 만들기

논문에서는 총 22가지의 shape class가 있었죠. 저도 똑같이 22개 class를 각각 C1, C2,..., C22라고 이름 지어서 함수로 정의해 보겠습니다. 22개 모두 다루면 너무 많고 겹치는 부분도 있어서 몇 가지만 다루어 보도록 하겠습니다.

Random

def C1():
    x, y = Random_xy(0.1, 0.2)

    dense_map(x, y)

Random Class (C1)

구간을 적절히 조절해서 random 한 점들을 생성하고 density map으로 결과를 시각화했습니다. 위의 사진이 1번 class Random의 결과입니다.

Edge

def C2():
    n = poisson(3, 4)
    R = np.random.uniform(8, 10, n)
    R.sort()
    u_list = np.random.uniform(0, 1, n)
    T = 2 * np.pi * u_list

    x = R * np.cos(T)
    y = R * np.sin(T)

    x0, y0 = Random_xy(0.5, 1)
    x = np.concatenate((x, x0))
    y = np.concatenate((y, y0))
  
    dense_map(x, y)

Edge class (C2)

Edge class는 wafer 원 경계 쪽에 점들이 모여 있어야 하므로 중심에서 8에서 10만큼 떨어지고 0에서 $2\pi$의 값을 범위로 갖는 점들을 무작위로 생성하게 해 주었습니다. 또한 거기에 Random_xy함수를 이용해서 random 한 패턴을 포함시켰습니다.

 

Right side edge와 Left side edge는 여기에 구간 $ [0, 2\pi] $를 적절히 수정해 주면 됩니다.

Line scratch

def C5():
    n = poisson(1, 1.1)
    R = np.random.uniform(-7, 7, n)
    R.sort()
    
    # 직선이므로 각도가 거의 일정해야 한다 
    t = np.random.uniform(0, 1)
    u_list = np.random.uniform(t - 0.001, t + 0.001, n)
    
    T = 2 * np.pi * u_list
    
    # 직선 평행 이동
    x = R * np.cos(T) + np.random.uniform(-5, 5)
    y = R * np.sin(T) + np.random.uniform(-5, 5)

    x0, y0 = Random_xy(6, 7)
    x = np.concatenate((x, x0))
    y = np.concatenate((y, y0))

    # 만약 직선이 원 밖으로 나간다면 그건 그림에 나타내지 않는다
    dense_map(x[f(x, y) <= 0], y[f(x, y) <= 0])​

Line class (C5)

반지름의 길이가 10이므로 직선의 길이를 14 이내가 되도록 범위를 잡았습니다. 또한 직선을 극좌표로 나타내면 각도는 일정해야 하므로 무작위로 나올 수 있는 각도의 범위를 최소한으로 줄였습니다. 그리고 그 직선을 랜덤 한 위치로 평행이동 하게 만들어서 무작위성을 높였습니다. 마지막으로 만약 직선이 원 밖을 나간다면 그 값은 무시하도록 해주었습니다.

Curved scratch

def C6():
    n = poisson(1, 1.2)
    
    # 또 다른 원을 하나 만들어준다
    r = np.random.uniform(0, 10)
    R = np.random.uniform(r, r + 0.1, n)
    R.sort()
    # 임의로 원을 자른다 (특정 각도만 나오도록 조정)
    t1 = np.random.uniform(0, 0.5)
    t2 = np.random.uniform(0.5, 1)
    u_list = np.random.uniform(t1, t2, n)
    T = 2 * np.pi * u_list

    # 평행 이동
    x = R * np.cos(T) + np.random.uniform(-5, 5)
    y = R * np.sin(T) + np.random.uniform(-5, 5)

    x0, y0 = Random_xy(6, 7)
    x = np.concatenate((x, x0))
    y = np.concatenate((y, y0))
  
    dense_map(x[f(x, y) <= 0], y[f(x, y) <= 0])

Curve class (C6)

저 같은 경우 curved class를 만들 때, 랜덤 한 크기를 가진 원을 하나 만들고 그 원에서 랜덤 하게 구간을 잘라서 평행이동을 시키는 방법으로 만들어 주었습니다.

물론 이 방법 말고도 이차 곡선으로 만들어주거나 곡률을 바꿔주는 등의 방법을 사용할 수도 있겠죠?

Non-random Cluster

def C7():
    # cluster를 이루는 점의 개수
    n_cluster = int(np.random.uniform(150, 300))
    # cluster 개수
    cluster = 1
    # cluster의 분산
    cluster_std = np.random.uniform(0.1, 2)

    # cluster 생성
    coord, _ = make_blobs(n_samples=n_cluster, n_features=2, centers=cluster, cluster_std=cluster_std, center_box=(4, 5))

    x0, y0 = Random_xy(0.7, 1)
    x = np.concatenate((coord[:, 0], x0))
    y = np.concatenate((coord[:, 1], y0))

    dense_map(x[f(x, y) <= 0], y[f(x, y) <= 0])

Cluster at 1st quadrant class (C7)

make_blobs 함수를 이용해서 cluster를 생성했습니다. make_blobs는 cluster를 만들어주는 함수로 인자들은 다음과 같습니다.

  • n_samples : 표본 데이터의 수
  • default: 100 n_features : 차원. 만약 n_features=2라고 하면 2차원에서 나타납니다
  • centers : 생성할 cluster의 수 혹은 중심, [n_centers, n_features] 크기의 배열. default: 3
  • cluster_std: cluster의 표준 편차, default: 1.0
  • center_box: 생성할 cluster의 중심의 범위를 나타냅니다. default: (-10.0, 10.0))

반환값은 다음과 같습니다.

  • X : [n_samples, n_features] 크기의 배열 (독립 변수)
  • y : [n_samples] 크기의 배열 (종속 변수)

여기서는 1 사분면에 cluster가 나타나야 하므로 center_box를 (4, 5)로 해주었습니다. 이렇게 하면 각 차원에 대해서, 여기서는 x좌표와 y 좌표가 각가 (4, 5) 범위 내에서 cluster의 중심이 제한됩니다.

 

또한 centers 인자를 조절해서 cluster의 위치를 조절할 수 있는데요, [n_centers, n_features] 크기의 배열을 입력하면 그 위치에 cluster가 생기게 됩니다. 예를 들어 Cluster at top (C12) class의 경우 centers=[(0, 5)]로 해주면 cluster가 원 위쪽에 생성되게 됩니다.

 

나머지 cluster class의 경우 make_blobs의 인자 값을 잘 조절해서 만들어주면 됩니다.

Gross

def C16():
    x, y = Random_xy(3, 4)
    x0, y0 = Random_xy(7, 9)
    x = np.concatenate((x, x0))
    y = np.concatenate((y, y0))
  
    dense_map(x[f(x, y) <= 0], y[f(x, y) <= 0])

Gross class (C16)

Gross class의 경우 단순히 n값(결함 점)을 증가시켜서 얻을 수 있습니다.

 

Gross at left half와 right half class 역시 Gross class에서 점들을 필터링해 주어서 얻을 수 있습니다.

Line scratch with Non-random Cluster

def C19():
    C5()
    C7()

Line scratch와 Cluster가 같이 나오는 class는 단순히 두 함수를 그냥 호출하기만 하면 됩니다. 간다 하죠?

 

이렇게 해서 22개의 defect class를 모두 만들었습니다! 결과물을 한번 보고 가시죠.

 

 

자 그럼 이렇게 만들어진 이미지를 저장을 해야 dataset으로 쓸 수 있겠죠? 논문에서는 각 class 당 1300장의 이미지를 생성했습니다. 한 번 구현해 보겠습니다.

 

먼저 각 이미지를 저장할 폴더를 만들어 보겠습니다. 각각의 폴더는 class 이름인 C1부터 C22로 하였습니다.

import os

for i in range(1, 23):
    # 폴더의 경로는 사용자에게 맞게 지정
    os.makedirs(f"C{i}")

 

그런 다음 각 폴더 class에 맞는 이미지를 각각 1300장씩 생성하겠습니다.

def save_img(n):
    folder_list = [f'C{i}' for i in range(1, 23)]

    for cls, folder_name in enumerate(folder_list):
        for i in range(1, n + 1):
            function_name = f'C{cls + 1}'
            func = globals()[function_name]
            func();
            plt.savefig(f"{folder_name}/{i + n * cls:04d}.jpg")
            plt.clf() # 그래프 초기화 안해주면 기존 그래프 위에 그려짐

save_img(1300) # 각 폴더 당 1300장의 이미지 생성

globals()를 이용해서 C1부터 C22까지의 함수를 불러와주고 savefig로 이미지를 저장합니다.

제가 처음에는 plt.clf()를 하지 않고 그냥 코드를 돌렸는데 생각보다 너무 오래 걸리더라고요. 알고 봤더니 이미지를 초기화시켜주지 않으면 이미지 위에 이미지가 덮혀져서 저장이 되는 것 같았습니다. 그래서 시간이 오래 걸린 거였고요. 그래서 이미지를 저장할 때는 plt.clf()를 꼭 해주셔야 합니다!

 

우선 여기까지 dataset을 생성하는 법을 작성해 보았고 다음에는 이 dataset으로 직접 CNN으로 training 해보는 시간을 가져보도록 하겠습니다.

 

긴 글 읽어주셔서 감사합니다!