# 간단 실습 예제 소개

웨이블릿 변환을 실습으로 체험해 보려 할 때 가장 먼저 떠오르는 접근 방법은 구체적인 예제 신호를 준비하여 실제로 변환을 수행해 보는 것이다. 간단한 예시로는 1차원 시계열 신호가 자주 활용된다. 미리 생성된 임의 데이터나 간단한 함수(예: 사인파, 사각파)를 사용해도 무방하고, 직접 수집한 센서 데이터나 오디오 신호를 이용해도 좋다. 예시 신호를 준비한 뒤에 어떤 종류의 웨이블릿을 사용해 변환할지 선택하는 과정이 뒤따른다.

연산 과정의 구체적 이해를 위해서는 웨이블릿 변환의 기본 정의를 살펴보고, 이를 실제로 적용해 보는 단계를 거치는 것이 중요하다. 연속 웨이블릿 변환과 이산 웨이블릿 변환 중에서 특히 초심자에게는 이산 웨이블릿 변환이 간단 실습에 적합하다고 할 수 있다. 이산 웨이블릿 변환은 디지털 신호에 적합하도록 샘플링된 데이터에 기반하며, 다중 해상도 분석 구조를 통해 단계적으로 신호를 분해한다.

Haar 웨이블릿을 가장 기본적인 예시로 들 수 있다. Haar 웨이블릿은 정의가 단순하며, 신호 분할 과정과 에너지가 어떻게 분산되는지 쉽게 파악할 수 있게 해준다. 일반적으로 크기가 2의 거듭제곱 형태인 신호 길이에 대해 적용하면 수식 전개가 간단해진다. 신호 길이가 그렇지 않은 경우에도 적절한 보간이나 zero-padding 과정을 거친 다음 변환을 적용할 수 있다.

데이터 벡터를 $\mathbf{x}$라고 할 때, 이산 웨이블릿 변환은 필터 계수를 통해 부분대역 신호를 산출한다. 예를 들어 Haar 웨이블릿 변환에서는 스케일링 필터와 웨이블릿 필터가 간단히 정의되며, 이를 $h\_n, g\_n$으로 나타낼 수 있다. 신호 $\mathbf{x} = \[x\_0, x\_1, x\_2, \dots, x\_{N-1}]^\top$에 대하여 레벨 1 변환으로 스케일링 계수 $\mathbf{a}^{(1)}$와 디테일 계수 $\mathbf{d}^{(1)}$를 얻으려면 다음과 같은 방식으로 연산이 이루어진다.

$$
a\_k^{(1)} = \sum\_{n} x\_{2k+n} h\_n
$$

$$
d\_k^{(1)} = \sum\_{n} x\_{2k+n} g\_n
$$

특정 웨이블릿에 따라 $h\_n$과 $g\_n$의 값이 달라진다. Haar 웨이블릿인 경우 스케일링 계수의 필터는 $\frac{1}{\sqrt{2}}\[1, 1]$로, 웨이블릿 계수의 필터는 $\frac{1}{\sqrt{2}}\[1, -1]$로 정의된다. 직접 이 연산을 수행하여 결과를 확인하면, 원신호가 가장 기본적인 평균 성분(스케일링 계수)과 고주파 성분(디테일 계수)으로 분해됨을 명확히 알 수 있다.

실제 코드 실습을 위해서는 Python의 PyWavelets 라이브러리를 사용할 수 있다. 아래 예시는 간단한 파이썬 코드로, Haar 웨이블릿을 이용해 1차원 신호를 변환하는 과정을 보여준다.

```python
import pywt
import numpy as np

# 예시 신호 생성
t = np.linspace(0, 1, 16, endpoint=False)
x = np.sin(2 * np.pi * 5 * t)  # 5Hz 사인파

# 이산 웨이블릿 변환 수행
coeffs = pywt.wavedec(x, 'haar', level=None)

# 결과 확인
approx = coeffs[0]
detail_levels = coeffs[1:]
print("Approximation Coefficients:", approx)
print("Detail Coefficients:", detail_levels)
```

이 예시에서는 wavedec 함수가 여러 레벨의 웨이블릿 변환 계수를 한 번에 산출해 준다. coeffs\[0]은 가장 낮은 주파수 대역(스케일링 계수)이고, 그 뒤의 원소들은 점점 더 높은 주파수 대역의 디테일 계수가 된다. 레벨 개수를 직접 지정하지 않으면 신호가 가능한 수준까지 자동으로 분해된다.

신호를 재구성하기 위해서는 pywt.waverec 함수를 이용한다. wavedec으로 얻은 coeffs를 다시 waverec에 입력하면, 원래의 신호 혹은 특정 대역의 신호만을 재구성할 수 있다. 예를 들어 디테일 계수를 모두 제거하고 스케일링 계수만 inverse transform에 사용하면 저주파 성분만 남은 신호를 확인할 수 있다.

직접 각 레벨의 신호를 그래프로 확인해 보면, 시간-주파수 특성이 어떻게 변하는지 더 잘 감을 잡을 수 있다. 예를 들어 디테일 계수 그래프에는 신호의 급격한 변동(에지나 짧은 펄스 등)이 어느 위치에서 주로 발생했는지 나타나게 된다.

계속해서 실습 예제를 좀 더 확장해 보면, 다른 종류의 웨이블릿을 적용하는 방안을 고려해 볼 수 있다. Haar 웨이블릿은 계산이 간단하고 직관적인 장점이 있지만, 부드럽지 않은 특성 때문에 신호나 이미지 경계에서 특이하게 보일 수 있다. 보다 부드러운 형태의 웨이블릿을 사용하고 싶다면 Daubechies, Coiflet, Symlet 등 다양한 계열을 선택할 수 있다. PyWavelets에서는 각 웨이블릿에 대응하는 문자열 이름을 지정하면 쉽게 적용할 수 있다.

Daubechies 웨이블릿의 예시로 db2, db3, db4 등이 자주 활용된다. 숫자가 커질수록 웨이블릿의 지지 길이가 길어지며, 결과적으로 더 많은 필터 계수를 사용하게 된다. 가령 db4를 활용하면 필터 계수 $h\_n, g\_n$가 각각 4개의 기본 요소로 구성되므로, Haar 웨이블릿에 비해 좀 더 부드럽게 신호를 분해한다. 결과적으로, 특정 신호를 구성하는 세부 구조가 더 매끄럽게 표현되는 경향이 있다.

Python에서의 적용 예시는 다음과 같이 매우 간단하다.

```python
import pywt
import numpy as np

t = np.linspace(0, 1, 32, endpoint=False)
x = np.sin(2*np.pi*3*t) + 0.5*np.cos(2*np.pi*10*t)

coeffs_db4 = pywt.wavedec(x, 'db4')
```

이 코드를 통해 여러 레벨의 계수가 한 번에 계산되며, coeffs\_db4에 저장된다. wavedec 함수를 사용할 때 level 파라미터를 직접 지정함으로써 원하는 수준만큼만 변환할 수도 있다. 예를 들어 level=2로 설정하면 2번 단계까지만 변환이 이루어진다. Daubechies 웨이블릿의 필터 계수는 Haar 대비 길이가 길기 때문에, 신호 경계 처리 방식에 대해 추가적인 고려가 필요할 수 있다. PyWavelets는 이를 위해 다양한 경계(extension) 모드를 제공하며, 기본값은 신호 끝부분을 반사(reflect)하는 방식이다.

각 레벨별 디테일 계수를 살펴보면, 신호 내의 다른 스케일에서 관찰되는 특성들이 어떻게 분해되는지 구체적으로 확인할 수 있다. 필요한 경우에는 특정 레벨의 디테일 계수만 0으로 만들고 inverse transform을 수행하여, 고주파 노이즈를 제거하거나 저주파 성분만 추출하는 식의 실습도 가능하다. 이러한 실험 과정을 통해 웨이블릿 변환이 단순한 필터링 이상의 의미를 가진다는 점을 체감할 수 있다.

가령 오디오 신호에 대해 웨이블릿 변환을 수행하는 경우에는, 인간의 청각 범위에서 중요하게 여겨지는 특정 대역만 강조하거나 잡음을 제거하기 위해 디테일 계수를 조절할 수 있다. 반면 센서 데이터를 분석할 때는 갑작스러운 외란이 발생하는 구간을 디테일 계수를 통해 찾고, 나머지 구간과 구분하여 처리하는 실습을 해볼 수 있다.

실제 프로젝트 환경에서 웨이블릿 변환이 동작하는 과정을 더 명확히 이해하기 위해, 간단한 실습용 GUI 툴을 만들어 보거나 Jupyter Notebook 상에서 interactive widget을 활용하는 것도 유용하다. 슬라이더를 통해 웨이블릿 종류나 분해 레벨을 조절하면서 실시간으로 결과 그래프를 확인하면, 웨이블릿이 시간 축과 주파수 축에서 어떤 정보를 포착하는지 한눈에 파악할 수 있다.

#### 2차원 웨이블릿 변환 예제

간단 실습 예제를 2차원 데이터로 확장하면, 이미지나 행렬 형태의 데이터를 대상으로 웨이블릿 변환을 수행하는 과정을 체험할 수 있다. 2차원 웨이블릿 변환은 일반적으로 행과 열에 대해 각각 1차원 웨이블릿 변환을 적용하는 방식으로 구현된다. 예를 들어 이미지 행렬을 $\mathbf{I}$라고 하면, 먼저 모든 행에 대해 1차원 이산 웨이블릿 변환을 수행하고, 이어서 변환된 결과에 대해 다시 열 방향으로 1차원 변환을 수행하는 식이다. 그 결과 한 레벨의 변환에서는 저주파 성분인 LL 서브밴드와 고주파 영역을 대표하는 LH, HL, HH 서브밴드가 동시에 구해진다.

PyWavelets에서는 2차원 변환을 수행하기 위해 dwt2 함수를 이용할 수 있다. Haar 웨이블릿 예시 코드는 다음과 같다.

```python
import pywt
import numpy as np
import matplotlib.pyplot as plt

# 8x8 예시 행렬(또는 이미지를 대신 사용 가능)
I = np.arange(64).reshape(8, 8)

# 2차원 이산 웨이블릿 변환 수행
coeffs2 = pywt.dwt2(I, 'haar')
LL, (LH, HL, HH) = coeffs2

# 결과 확인
fig, axes = plt.subplots(1, 4, figsize=(12, 3))
axes[0].imshow(I, cmap='gray')
axes[0].set_title('Original')
axes[1].imshow(LL, cmap='gray')
axes[1].set_title('LL')
axes[2].imshow(LH, cmap='gray')
axes[2].set_title('LH')
axes[3].imshow(HH, cmap='gray')
axes[3].set_title('HH')
plt.show()
```

이 예시에서 dwt2 함수는 한 번의 호출로 2차원 변환을 수행하며, 결과물인 coeffs2에서 저주파(LL)와 세 종류의 고주파(LH, HL, HH) 서브밴드를 얻을 수 있다. 각 서브밴드의 크기는 원본 행렬과 동일하거나 절반으로 줄어들 수 있는데, PyWavelets에서는 디폴트로 decimation을 수행하여 서브밴드 크기가 원본의 절반씩 축소된다. 구성된 서브밴드들을 다시 합성할 때는 idwt2 함수를 이용한다.

2차원 웨이블릿 변환을 여러 레벨로 진행하면, 단계별로 더 낮은 주파수 성분에 대한 LL 서브밴드를 지속적으로 분할하고, 나머지 LH, HL, HH 서브밴드는 그 시점에서의 최종 결과로 저장된다. 이미지에 대해 여러 레벨을 설정해 변환하면, 이미지의 대략적인 윤곽선 정보와 디테일한 엣지 성분들이 어떻게 분해되어 나타나는지 명확히 볼 수 있다.

이미지를 직접 불러와서 실습할 경우, matplotlib의 imread 함수를 통해 간단히 처리할 수 있다. PyWavelets에서 dwt2, idwt2 함수를 이용하여 몇 레벨에 걸친 변환을 수행한 뒤, 역변환을 통해 특정 부분만 재구성하는 식으로 잡음을 줄이거나 특징을 강조하는 실험을 해볼 수 있다.

#### 다중 레벨 변환과 이미지 분해 구조

이미지에 대한 다중 레벨 2차원 웨이블릿 변환을 수행하면, 가장 낮은 주파수 성분을 나타내는 LL 서브밴드를 계속해서 재귀적으로 분해해 나가게 된다. 각 레벨에서 LL을 다시 Haar나 다른 웨이블릿으로 변환하면, 새로운 LL, LH, HL, HH 서브밴드가 추가적으로 구해지고, 그렇게 누적되어 전체 이미지가 점차 세부 주파수 대역으로 나뉘게 된다.

파이썬에서는 wavedec2 함수를 이용해 여러 레벨의 2차원 변환을 한 번에 계산할 수 있다. 예시는 다음과 같이 작성할 수 있다.

```python
import pywt
import numpy as np
import matplotlib.pyplot as plt

I = np.random.rand(128, 128)  # 128x128 가상의 예시 이미지
coeffs2 = pywt.wavedec2(I, 'haar', level=2)

# coeffs2[0]은 최종 레벨의 LL 서브밴드
# coeffs2[1:]은 각 레벨별 (LH, HL, HH) 서브밴드가 튜플로 저장
print("Low-pass subband at final level (LL):")
print(coeffs2[0].shape)
print("Detail subbands at each level:")
for detail_subbands in coeffs2[1:]:
    for sb in detail_subbands:
        print(np.array(sb).shape)
```

이 코드를 실행하면, 여러 레벨에 걸쳐 나누어진 서브밴드들의 크기와 형태가 계층적으로 구성되어 있음을 확인할 수 있다. coeffs2의 인덱싱 구조는 파이썬 튜플 형식으로, 맨 앞의 coeffs2\[0]이 가장 낮은 주파수 성분에 해당하며, 나머지 원소들은 (LH, HL, HH) 순서로 각 레벨에서 분해된 고주파 계수들이 순차적으로 저장된다.

다중 레벨 변환 과정을 그림으로 표현하면 다음과 같은 흐름이 된다.

{% @mermaid/diagram content="flowchart LR
A(Original Image) --> B(2D Wavelet\nDecomposition\nLevel 1)
B --> C(LL1)
B --> D(LH1)
B --> E(HL1)
B --> F(HH1)
C --> G(2D Wavelet\nDecomposition\nLevel 2)
G --> H(LL2)
G --> I(LH2)
G --> J(HL2)
G --> K(HH2)" %}

이미지 노이즈 제거를 목적으로 실습을 진행할 때는, 여러 레벨로 분해된 계수 중에서 특정 임계값(threshold) 이하의 계수를 0으로 만들어 버린 뒤에 역변환을 적용하는 방식으로 소위 웨이블릿 임계값 필터링을 시도해 볼 수 있다. 신호 처리 관점에서는 이러한 방식이 특정 대역의 노이즈를 효과적으로 억제하는 결과로 이어질 때가 많다. PyWavelets 라이브러리에서는 denoise\_wavelet 함수 등 편리한 인터페이스도 제공하지만, 직접 wavedec2, idwt2를 통해 계수를 조작해 보는 것이 변환 구조를 이해하는 좋은 실습이 될 수 있다.

#### 웨이블릿 패킷 변환 예제

웨이블릿 변환은 저주파와 고주파 대역으로 분할을 계속하며 신호를 계층적으로 표현해 준다. 그러나 기본적인 이산 웨이블릿 변환(DWT)에서는 저주파 부분(LL)만 재귀적으로 분할하고 고주파 부분은 더 이상 세분화하지 않는다. 웨이블릿 패킷 변환(Wavelet Packet Transform)은 저주파뿐 아니라 고주파 성분도 재귀적으로 분할하여 더 풍부한 시간-주파수 해상도를 얻을 수 있도록 확장된 기법이다.

PyWavelets에서는 WaveletPacket 클래스를 이용해 웨이블릿 패킷 변환을 수행할 수 있다. 1차원 예시 코드로 살펴보면 다음과 같은 형태가 된다.

```python
import pywt
import numpy as np

# 예시 신호
t = np.linspace(0, 1, 16, endpoint=False)
x = np.sin(2*np.pi*5*t) + 0.3*np.random.randn(16)

# 웨이블릿 패킷 객체 생성
wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric', maxlevel=3)

# 모든 노드에 대한 접근
nodes = wp.get_level(level=3, order='natural')

# 노드별로 데이터 확인
for node in nodes:
    print(node.path, node.data)
```

이 코드는 db1(=Haar) 웨이블릿을 사용하여 최대 3단계까지 패킷 분해를 수행한다. get\_level 메서드는 특정 레벨에 존재하는 모든 노드를 가져오며, order 파라미터를 통해 순회 방식을 지정할 수 있다. 이 때 node.path는 어떤 서브밴드(노드)에 해당하는지 경로를 나타낸다. 예를 들어 ‘aaa’는 레벨 3까지 세 번 모두 저주파 대역으로 내려갔다는 의미이고, ‘aad’라면 레벨 2까지 저주파, 마지막 단계에서 고주파 방향으로 분할했다는 뜻이다.

웨이블릿 패킷 변환을 통해 신호를 재구성할 때는, WaveletPacket 객체에서 특정 노드를 선택하고 그 노드들만 합성하도록 설정하면 된다. 즉, 노드별로 데이터를 확인한 뒤 필요한 경로만 남겨 두고 나머지를 비워 두는 식으로 구성한 후에 reconstruct 함수를 호출하면, 특정 대역만 포함한 신호를 재구성하거나 노이즈나 불필요한 성분이 담긴 경로를 제거할 수 있다.

2차원 데이터(예: 이미지)에 대해서도 WaveletPacket2D 클래스를 사용해 유사한 과정을 진행할 수 있다. PyWavelets는 현재 2차원 웨이블릿 패킷 변환에 대한 별도의 클래스를 공식적으로 제공하지 않으므로, 예전 버전의 구현 사례나 직접 2차원에 확장한 서드파티 패키지를 참고해야 하는 경우가 있다. 다만, 1차원 웨이블릿 패킷 변환을 행과 열에 각각 적용해 간접적으로 2차원 패킷 분해를 구현할 수도 있다.

웨이블릿 패킷 변환 과정에서 노이즈 제거, 특정 주파수 대역 강조 등의 작업은 매우 직관적으로 진행된다. 왜냐하면 단순 웨이블릿 변환보다 훨씬 세밀한 대역 분할이 가능하기 때문이다. 예컨대, 기존의 DWT 구조에서 LL 밴드만 레벨을 올렸던 제약이 사라지고, LH, HL, HH 등 모든 대역을 원하는 만큼 세분화할 수 있다. 이 점이 웨이블릿 패킷 변환의 가장 큰 장점으로 꼽힌다.

추가로, 웨이블릿 패킷 분해 결과를 트리 구조로 시각화해 보면, 어떤 노드가 어느 스케일/대역을 담당하는지 한눈에 파악하기 쉽다. PyWavelets 자체가 해당 기능을 내장하지는 않았지만, node.path 정보를 이용하면 간단히 트리 형태의 시각화를 구성할 수 있다. 예시로 mermaid나 네트워크 그래프 라이브러리를 활용하면 다음과 같이 구조도를 나타낼 수 있다.

{% @mermaid/diagram content="flowchart TB
A(Original Signal) --> B(db1 level 1)
B --> B1(Node a)
B --> B2(Node d)
B1 --> C(level 2)
B2 --> D(level 2)
C --> C1(Node aa)
C --> C2(Node ad)
D --> D1(Node da)
D --> D2(Node dd)
C1 --> E(level 3)
C2 --> F(level 3)
D1 --> G(level 3)
D2 --> H(level 3)" %}

이와 같이 웨이블릿 패킷 변환을 통해 간단 실습을 진행하면, 다양한 주파수 대역을 어떻게 선택적으로 분해하거나 재구성할 수 있는지 체감할 수 있다.

#### 웨이블릿 기반 압축 실습 예제

이미지나 신호를 압축할 때, 웨이블릿 변환이 자주 활용된다. 대표적인 예로 JPEG2000 이미지 압축 표준이 웨이블릿 변환을 기반으로 하며, 변환 후 계수에 적절한 양자화와 엔트로피 부호화를 적용해 고압축율을 달성한다. 간단 실습 관점에서 접근한다면, 웨이블릿 변환을 통해 얻은 디테일 계수 중 상대적으로 작은 값을 일정 기준 이하로 잘라내거나 0으로 만드는 방법부터 시도해 볼 수 있다. 이는 소위 ‘하드 쓰레숄딩(hard thresholding)’ 기법에 해당한다.

웨이블릿 변환을 이용한 간단 압축 실험은 다음과 같이 진행할 수 있다. 2차원 이미지를 입력받아 dwt2(또는 wavedec2)로 다단계 분해한 뒤, 특정 임계값 이상인 계수만 남기고 나머지는 0으로 만든다. 이후 idwt2(또는 waverec2)를 통해 이미지를 재구성하면, 일부 세부 정보가 사라진 대신 데이터 용량이 줄어들게 된다. PyWavelets에서는 이 과정을 직접 수행하는 예시 코드를 아래처럼 작성할 수 있다.

```python
import pywt
import numpy as np
import matplotlib.pyplot as plt

def wavelet_compress(I, wavelet='haar', level=2, threshold=0.1):
    coeffs2 = pywt.wavedec2(I, wavelet=wavelet, level=level)
    cA = coeffs2[0]
    cD = coeffs2[1:]

    # 임계값 적용
    cA_thresh = pywt.threshold(cA, threshold, mode='hard')
    cD_thresh = []
    for detail_level in cD:
        detail_level_thresh = []
        for subband in detail_level:
            sub_thresh = pywt.threshold(subband, threshold, mode='hard')
            detail_level_thresh.append(sub_thresh)
        cD_thresh.append(tuple(detail_level_thresh))

    new_coeffs2 = (cA_thresh, cD_thresh)
    I_compressed = pywt.waverec2(new_coeffs2, wavelet=wavelet)
    return I_compressed

# 가상의 2차원 이미지 생성
np.random.seed(0)
I_orig = np.random.rand(128, 128)

# 압축 수행
I_comp = wavelet_compress(I_orig, wavelet='db2', level=3, threshold=0.05)

# 결과 비교
fig, axes = plt.subplots(1, 2, figsize=(6, 3))
axes[0].imshow(I_orig, cmap='gray')
axes[0].set_title('Original')
axes[1].imshow(I_comp, cmap='gray')
axes[1].set_title('Compressed')
plt.show()
```

이 코드에서 wavelet\_compress 함수는 지정된 웨이블릿과 레벨로 이미지를 변환한 뒤, 각 서브밴드 계수에 대해 threshold 함수를 적용해 특정 임계값 이하를 0으로 처리하고, 다시 역변환하는 과정을 수행한다. threshold 함수에는 여러 모드가 있는데, ‘hard’는 절댓값이 임계값 미만인 계수를 직접 0으로 만들고, ‘soft’는 계수 크기를 임계값만큼 줄이는 방식으로 처리한다. 적절한 임계값 설정에 따라 압축 효과와 화질(또는 신호 품질) 사이의 균형점을 찾을 수 있다.

결과로 재구성된 이미지를 살펴보면, 원본 이미지 대비 어느 정도 해상도 손실이 발생하지만, 잡음이나 세밀한 부분이 줄어들어 공간 차원을 줄이는 효과가 얻어진다. 간단 실습으로는, 다양한 threshold 값을 바꿔 가며 화질과 압축 효과가 어떻게 달라지는지 관찰해 볼 수 있다. 또한 wavelet 파라미터를 Haar, db2, db4 등으로 바꾸면서 압축 성능에 어떤 차이가 생기는지 비교할 수도 있다.

소비자용 이미지 압축 표준인 JPEG2000은 웨이블릿 변환 뒤에 양자화(quantization)와 엔트로피 부호화를 추가적으로 적용해 더욱 높은 압축률을 달성한다. 간단 실습 수준에서는 양자화 대신 threshold만으로도 어느 정도 유사한 원리를 체험할 수 있으며, 엔트로피 부호화는 별도의 라이브러리를 통해 실행할 수 있다. Python 환경에서는 PIL(Python Imaging Library)이나 OpenCV 등을 활용해 이미지 입출력을 처리하고, PyWavelets로 변환과 역변환을 담당하게 만드는 식으로 쉽게 구성할 수 있다.

#### 웨이블릿 기반 특징 추출 실습 예제

웨이블릿 변환은 신호나 이미지 내에 존재하는 시간-주파수 특성을 다양한 스케일에서 분석할 수 있게 해주므로, 특징(feature)을 추출하는 강력한 도구로 활용된다. 예를 들어 단순한 합성곱 신경망(CNN) 기반 방법을 사용하기에 앞서, 웨이블릿 변환을 통해 중요한 주파수 성분이나 에지 성분을 미리 추려낸 뒤에 분류나 회귀 문제에 적용하는 사례가 많다. 이는 웨이블릿 변환으로 신호의 노이즈를 줄이거나, 특정 구간에서 발생하는 특이점이나 급격 변화를 명확히 포착할 수 있기 때문이다.

실습적으로 접근할 때는 먼저 간단한 1차원 시계열을 대상으로 웨이블릿 변환을 수행하고, 변환 계수(스케일링 계수와 디테일 계수)의 통계값이나 에너지 분포를 추출해 보는 방법이 흔히 쓰인다. 예를 들어 어떤 센서 데이터를 분석하고자 할 때, 다양한 레벨의 디테일 계수에 대해 평균값이나 분산, 최대값, 절댓값 평균 등을 구해 특징 벡터를 만든 뒤, 이를 머신러닝 분류기의 입력으로 활용할 수 있다.

직접 코드를 작성해 보면 다음과 같은 형태가 될 수 있다.

```python
import pywt
import numpy as np
from sklearn.ensemble import RandomForestClassifier

# 1차원 시계열 가상의 예시: 두 개의 다른 패턴을 생성
np.random.seed(42)
x1 = np.sin(2*np.pi*5*np.linspace(0,1,128)) + 0.1*np.random.randn(128)  # 패턴 A
x2 = np.sign(np.sin(2*np.pi*5*np.linspace(0,1,128))) + 0.1*np.random.randn(128)  # 패턴 B

# 라벨링
label1 = 0
label2 = 1

# 간단히 여러 샘플 생성
X_data = []
Y_data = []
for _ in range(50):
    # 패턴 A 변형
    noise_a = 0.1*np.random.randn(128)
    sample_a = x1 + noise_a
    # 패턴 B 변형
    noise_b = 0.1*np.random.randn(128)
    sample_b = x2 + noise_b

    X_data.append(sample_a)
    Y_data.append(label1)
    X_data.append(sample_b)
    Y_data.append(label2)

X_data = np.array(X_data)
Y_data = np.array(Y_data)

# 웨이블릿 변환을 이용해 특징 추출
def extract_wavelet_features(signal, wavelet='db2', level=3):
    coeffs = pywt.wavedec(signal, wavelet, level=level)
    features = []
    for c in coeffs:
        features.append(np.mean(c))
        features.append(np.std(c))
        features.append(np.max(c))
        features.append(np.min(c))
        features.append(np.mean(np.abs(c)))
    return np.array(features)

# 모든 시계열에 대해 특징 벡터 생성
feature_list = []
for s in X_data:
    feat = extract_wavelet_features(s, wavelet='db2', level=3)
    feature_list.append(feat)

feature_list = np.array(feature_list)

# 분류기 학습
clf = RandomForestClassifier(n_estimators=50, random_state=42)
clf.fit(feature_list, Y_data)

# 간단 테스트
test_signal = x1 + 0.05*np.random.randn(128)  # 패턴 A 유사
test_feat = extract_wavelet_features(test_signal)
pred = clf.predict(test_feat.reshape(1, -1))
print("Predicted label:", pred)
```

이 코드는 두 가지 유형의 1차원 시계열 패턴(A, B)을 임의로 생성하고, 이에 여러 번 노이즈를 추가해 데이터셋을 만든 뒤, 웨이블릿 변환 계수의 통계값을 특징으로 삼아 랜덤 포레스트 분류기를 학습한다. extract\_wavelet\_features 함수는 주어진 신호에 대해 wavedec으로 분해한 계수들의 평균, 표준편차, 최대값, 최소값, 절댓값 평균 등을 추출하여 하나의 특징 벡터로 만든다.

실제로는 계수의 에너지 분포, 엔트로피(entropy), 주요 피크(peak)의 위치나 크기, 계수의 상관관계 등 더 복잡한 특징들을 추출하기도 한다. 적용 문제에 따라 어떤 통계량을 사용하는 것이 좋은지 달라지므로, 다양한 방법을 시도해 보는 것이 좋다.

이미지에서도 유사한 방법으로 2차원 웨이블릿 변환을 적용한 뒤, 각 레벨의 LL, LH, HL, HH 계수에 대해 통계값 등을 구해 특징 벡터를 구성할 수 있다. 다만 이미지의 경우에는 계수 차원이 매우 커질 수 있으므로, PCA(주성분 분석)나 다른 차원 축소 기법과 병행해 효율적인 특징 표현을 도모하기도 한다.

웨이블릿 기반 특징 추출의 주요 장점은, 시간적 혹은 공간적 위치 정보를 놓치지 않으면서도 주파수 대역별 정보를 함께 고려할 수 있다는 점이다. 기존의 푸리에 변환 기반 접근이 전체 신호에 대한 전역 주파수 스펙트럼만 제공한다면, 웨이블릿 변환은 구간별로 어떻게 주파수 특성이 변화하는지까지 살펴볼 수 있다. 즉, 에지나 급격 변동 구간에서 강한 계수가 나오며, 그 외 안정적인 구간에서는 상대적으로 계수가 작게 나타나는 식이다.

실습에서는 머신러닝 기법이나 딥러닝 모델과 결합해 더 복합적인 문제에 도전할 수 있다. 예를 들어, 시계열 이상 감지(anomaly detection) 문제에서 특정 주파수 대역에서만 발생하는 이상 징후가 있다면, 해당 스케일의 디테일 계수가 이상적으로 높은 값을 보일 수 있다. 이 계수를 특징으로 삼아 간단한 임계값 기반 분류를 해 볼 수도 있고, 지도학습 기법을 적용할 수도 있다.

특히 산업용 센서 데이터나 의료 신호(EEG, ECG 등)를 다룰 때, 웨이블릿 변환 기반 특징 추출은 상당히 널리 쓰이는 방법이다. 구현도 비교적 간단하며, pywt 라이브러리를 활용해 다양한 파라미터(웨이블릿 종류, 변환 레벨, 경계 조건 등)를 빠르게 실험할 수 있으므로, 간단 실습 예제로도 적합하고 실제 현업 환경에서도 그대로 적용 가능하다는 장점이 있다.

#### 웨이블릿 기반 에지 검출 예제

이미지에서 에지를 검출하는 전통적인 방법으로는 Sobel, Canny 등의 선형 필터 기반 기법이 널리 알려져 있다. 웨이블릿 변환을 이용하면, 각 스케일별로 이미지에 존재하는 에지나 세부 구조가 어떤 계수 분포를 보이는지 살펴보고, 이를 바탕으로 에지를 추출하는 방식으로 접근할 수 있다. 2차원 이산 웨이블릿 변환 결과에서 LH, HL, HH 서브밴드는 각각 수평, 수직, 대각선 방향의 급격한 변화(에지)에 민감하게 반응하므로, 특정 레벨의 고주파 서브밴드에서 계수가 일정 기준 이상인 위치를 에지로 판별할 수 있다. 일반적으로 레벨 1 또는 2의 디테일 계수가 가장 날카로운 에지를 잘 포착하는데, 이는 고주파 영역일수록 에지 성분이 강하게 반영되기 때문이다.

직접 실습해 보려면, 이미지에 대해 2차원 웨이블릿 변환을 수행하고 디테일 서브밴드를 확인한 뒤, 특정 임계값을 설정해 에지라고 볼 만한 픽셀 좌표들을 추출할 수 있다. 아래 파이썬 예시는 PyWavelets와 OpenCV(또는 matplotlib)을 활용해 이미지를 불러온 뒤, 웨이블릿 변환을 통해 에지를 간단히 검출하는 과정을 보여준다.

```python
import pywt
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 이미지 불러오기 (흑백으로 로드)
I = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)
I = I.astype(np.float32) / 255.0

# 2차원 웨이블릿 변환 (Haar 웨이블릿, 1레벨)
coeffs2 = pywt.dwt2(I, 'haar')
LL, (LH, HL, HH) = coeffs2

# 에지 지도(edge map) 생성 예시
# 간단히 LH, HL, HH를 절댓값 취해 더한 뒤 임계값을 적용
edge_map = np.abs(LH) + np.abs(HL) + np.abs(HH)
threshold = 0.2  # 임계값은 이미지 특성에 따라 조정
edge_binary = (edge_map > threshold).astype(np.uint8)

# 시각화
fig, axes = plt.subplots(1, 3, figsize=(9, 3))
axes[0].imshow(I, cmap='gray')
axes[0].set_title('Original')
axes[1].imshow(edge_map, cmap='gray')
axes[1].set_title('Edge Map (LH+HL+HH)')
axes[2].imshow(edge_binary, cmap='gray')
axes[2].set_title('Binary Edge')
plt.show()
```

이 코드를 실행하면, 원본 이미지에서 에지로 추정되는 부위가 edge\_map에 반영되며, threshold 값에 따라 에지 검출 결과가 달라진다. 웨이블릿 변환의 레벨을 늘려 가며 분석하면, 더 넓은 스케일에서 나타나는 윤곽선 중심의 에지를 탐지하거나, 더 작은 스케일에서 세밀한 에지를 추적하는 식으로 시도할 수 있다. 결과적으로, 수평·수직·대각선 방향 계수를 통해 다양한 방향의 에지를 효율적으로 얻을 수 있다. 또한 단순 임계값 대신에 더욱 정교한 히스테리시스(hysteresis) 기법이나 누적 필터를 적용해 에지를 다듬을 수도 있다.

#### 연속 웨이블릿 변환 예시

웨이블릿 변환을 활용해 시간-주파수 해석을 좀 더 부드럽고 연속적으로 표현하고 싶다면, 연속 웨이블릿 변환(Continuous Wavelet Transform, CWT)을 고려해 볼 수 있다. CWT는 스케일(Scale)과 시프트(Shift)를 실수 영역에서 연속적으로 변화시키면서 신호와 웨이블릿 기저의 내적을 구하는 방식이다. 이산 웨이블릿 변환(DWT)처럼 다운샘플링과 업샘플링이 포함되지 않으며, 그 대신에 스케일을 좀 더 미세하게 선택할 수 있어 매우 세밀한 시간-주파수 해상도를 얻을 수 있다.

파이썬 PyWavelets 라이브러리를 이용하면, 아래처럼 cwt 함수를 통해 간단하게 연속 웨이블릿 변환을 수행할 수 있다.

```python
import pywt
import numpy as np
import matplotlib.pyplot as plt

# 예시 신호 생성: 주파수가 천천히 변하는 사인파
fs = 100
t = np.linspace(0, 2, 2*fs, endpoint=False)
signal = np.sin(2*np.pi*3*t) + 0.5*np.sin(2*np.pi*10*(t**2))

# CWT를 위해 스케일 범위 설정
scales = np.arange(1, 64)

# 'mexh'(멕시컨 햇) 웨이블릿 사용
cwtmatr, freqs = pywt.cwt(signal, scales, 'mexh', 1.0/fs)

# 시간-주파수(스케일) 결과 시각화
plt.figure(figsize=(8, 4))
plt.imshow(np.abs(cwtmatr), extent=[0, t[-1], scales[-1], scales[0]],
           interpolation='bilinear', aspect='auto', cmap='jet')
plt.colorbar(label='Magnitude')
plt.title('Continuous Wavelet Transform (Mexican Hat)')
plt.xlabel('Time [sec]')
plt.ylabel('Scale')
plt.show()
```

이 예시에서 scales는 1부터 63까지 설정되어 있으며, Mexican Hat(Mexh) 웨이블릿을 사용해 변환을 수행한다. 일반적으로 스케일이 클수록 저주파 영역, 스케일이 작을수록 고주파 영역을 의미하며, cwt 함수는 각 시점(t)에 대해 지정된 스케일 각각에서 웨이블릿 계수를 계산한다. 그 결과 cwtmatr가 생성되는데, 행(스케일)과 열(시간)에 걸쳐 웨이블릿 계수가 대응된다. 이를 시각화하면, 시간에 따른 주파수 성분(또는 스케일)이 어떻게 변동하는지 한눈에 확인할 수 있다.

CWT의 장점은 신호가 가진 비정상성(Non-stationary)이나 특정 구간에서 급격히 변하는 고주파 성분을 훨씬 매끄럽게 관찰할 수 있다는 점이다. 다만 데이터 크기가 커질수록 연산량이 매우 커질 수 있고, 이산 변환에 비해 계수 중복이 많이 발생한다. 실무에서는 무작정 모든 스케일을 세밀하게 지정하기보다는, 관심 있는 주파수 대역을 중심으로 합리적인 스케일 범위를 지정한 뒤에 분석하는 경우가 많다.

연속 웨이블릿 변환에서 획득한 시간-주파수 분포를 스펙트로그램과 유사한 방식으로 이해할 수도 있는데, 스펙트로그램은 윈도우 푸리에 변환(STFT)을 적용한 결과물이므로 일정한 창(Window) 크기를 사용한다. 반면 웨이블릿 변환은 스케일이 낮을수록(고주파) 시간 해상도가 좋아지고, 스케일이 높을수록(저주파) 주파수 해상도가 좋아지는 형태를 취한다. 이러한 특성 덕분에 국부적 이벤트를 더욱 선명하게 포착할 수 있는 장점이 있다.

여기서 사용된 Mexican Hat 외에도 Morlet, Gaussian, Shannon 등 다양한 모함수(Mother Wavelet)를 선택할 수 있다. 신호 특성에 맞추어 웨이블릿 기저를 잘 고르면, 시간적 변동과 주파수 구조를 보다 명확히 드러낼 수 있다. 예컨대 진동이나 회전과 관련된 물리적 신호 분석에는 Morlet 웨이블릿이 자주 사용되고, 임펄스적인 급격 변화가 많은 신호에는 Gaussian, Paul 웨이블릿 등이 고려되기도 한다.

실습 관점에서는, 여러 모함수와 스케일 범위를 변경해 가며 결과를 시각화해 보는 것만으로도 유용한 경험을 쌓을 수 있다. 실제로는 신호의 길이, 샘플링 주파수, 예상되는 주파수 대역 등을 고려하여, 너무 넓거나 세밀하게 스케일을 잡지 않도록 주의해야 한다. CWT의 결과 계수를 분석해 특정 시간 구간에서만 강하게 나타나는 주파수 성분을 골라낼 수도 있으며, 나아가 이를 임계값 기반으로 필터링하거나 이상 징후 탐지(Anomaly Detection)에 활용하기도 한다.

#### 웨이블릿 코히어런스 실습 예시

두 개 이상의 시계열 신호가 있을 때, 시간에 따라 변화하는 상관관계를 파악하고 싶으면 웨이블릿 코히어런스(Wavelet Coherence)를 시도할 수 있다. 연속 웨이블릿 변환(CWT)을 각각의 신호에 적용한 뒤, 두 신호 간에 에너지가 동시에 크게 나타나는 대역과 시점을 측정하여 시각화하는 기법이다. 시간-주파수 영역에서 상관도가 높게 나타나는 부분을 찾을 수 있기 때문에, 공진 현상이나 동조 현상을 포착하기에 유용하다.

pywt 라이브러리 자체로는 웨이블릿 코히어런스를 직접 계산하는 기능이 부족하므로, PyCWT(pycwt) 등의 패키지를 사용하면 편리하다. 예시 코드는 다음과 같다.

```python
import numpy as np
import matplotlib.pyplot as plt
import pycwt as wavelet

fs = 100
t = np.linspace(0, 10, fs*10, endpoint=False)
x1 = np.sin(2*np.pi*5*t) + 0.5*np.random.randn(len(t))
x2 = np.sin(2*np.pi*5*t + np.pi/4) + 0.5*np.random.randn(len(t))

mother = wavelet.Morlet(6)
alpha, _, _ = wavelet.ar1(x1)
beta, _, _ = wavelet.ar1(x2)

wave1, scales1, freqs1, _, _ = wavelet.cwt(x1, 1/fs, 1, 128, mother)
wave2, scales2, freqs2, _, _ = wavelet.cwt(x2, 1/fs, 1, 128, mother)

W12, cross_coherence, _, _ = wavelet.xwt(x1, x2, 1/fs, significance_level=0.95,
                                         wavelet=mother, normalize=True)
WCT, _, _ = wavelet.wct(x1, x2, 1/fs, significance_level=0.95,
                        wavelet=mother, normalize=True)

plt.figure(figsize=(8, 6))
plt.imshow(np.abs(WCT), extent=[t[0], t[-1], freqs2[-1], freqs2[0]],
           aspect='auto', cmap='jet', interpolation='bilinear')
plt.colorbar(label='Wavelet Coherence')
plt.xlabel('Time [sec]')
plt.ylabel('Frequency [Hz]')
plt.title('Wavelet Coherence between x1 and x2')
plt.show()
```

이 예시에서는 두 신호 $x\_1, x\_2$에 대해 각각 Morlet 웨이블릿 변환을 수행하고, 교차 스펙트럼 $W\_{12}$와 웨이블릿 코히어런스(WCT)를 계산한다. wct 함수는 시간과 주파수(스케일) 축에 대해 두 신호가 얼마나 동조되어 있는지를 계수로 나타내며, 0에서 1 사이 값을 취한다. 계수가 1에 가까울수록 두 신호의 해당 구간·스케일에서 상관관계가 높다는 의미다. 이미지로 시각화했을 때 특정 시점과 주파수 대역에서 코히어런스가 특히 높다면, 그 구간에서 두 신호가 유사한 주기나 위상을 지녔음을 유추할 수 있다.

파라미터로 사용하는 모함수(mother wavelet)는 Morlet 외에도 Mexican Hat, Paul 등이 있으며, 신호 특성에 맞추어 선택 가능하다. 스케일 범위 역시 신호 길이와 샘플링 주파수를 고려하여 합리적으로 설정해야 한다. 실제로는 파라미터 튜닝에 따라 결과 해석이 달라질 수 있으므로, 실습 단계에서 다양한 값을 적용해 보고 어떤 주파수 대역이 민감하게 포착되는지 관찰해 보는 것이 좋다.

#### 다중해상도 분석과 스케일 선택

웨이블릿 변환을 통해 신호나 이미지를 다중해상도로 분석한다는 것은, 서로 다른 스케일에서 얻어진 정보를 종합적으로 활용한다는 의미이다. 작은 스케일(고주파)로 갈수록 급격한 변화나 디테일한 패턴을 잘 포착할 수 있고, 큰 스케일(저주파)은 전반적인 추세나 전체 윤곽을 부드럽게 보여준다. 이러한 특성 덕분에 웨이블릿 변환은 다단계 필터 뱅크 구조로 이해될 수 있으며, 신호 처리 전반에 광범위하게 응용된다.

스케일 선택은 변환 결과 해석에서 매우 중요한 부분이다. 스케일이 지나치게 작으면, 노이즈 성분까지 세밀하게 잡아내려 하므로 의미 있는 특징을 놓칠 수 있고, 반대로 지나치게 크면 국소 구조나 에지를 제대로 포착하지 못할 수 있다. 이산 웨이블릿 변환(DWT)에서는 스케일을 2의 거듭제곱 형태로 고정하므로 선택의 여지가 제한되지만, 연속 웨이블릿 변환(CWT)에서는 사용자가 원하는 범위와 간격으로 스케일을 설정할 수 있다.

다중해상도 분석에서 중요한 점은, 신호가 어느 정도 비정상성(Non-stationary)을 갖고 있는지, 그리고 주파수 대역별로 어떤 물리적 의미가 있는지에 따라 스케일 범위를 어떻게 설정하느냐가 달라진다는 것이다. 예컨대, 지진파나 음향 신호와 같은 데이터에서는 극저주파부터 초음파 대역까지 넓은 범위를 조사할 수 있고, ECG 같은 생체 신호에서는 특정 주파수 대역만 중점적으로 보는 경우가 많다.

다중해상도 분석 구조를 수식으로 나타내면, 이산 웨이블릿 변환 계수 $\mathbf{a}^{(j)}$, $\mathbf{d}^{(j)}$를 통해 레벨 $j$에서의 대역 정보를 얻을 수 있다. 레벨 $J$까지 분해했다고 했을 때, 원 신호 $\mathbf{x}$는 아래와 같이 표현할 수 있다.

$$
\mathbf{x} = \mathbf{a}^{(J)} + \sum\_{j=1}^{J} \mathbf{d}^{(j)}
$$

여기서 $\mathbf{a}^{(J)}$는 가장 저주파 대역(스케일링 계수)이고, 각 $\mathbf{d}^{(j)}$는 레벨 $j$의 디테일 계수에 해당한다. 분석 목적에 따라 일부 레벨의 $\mathbf{d}^{(j)}$만 취하거나, 특정 구간의 계수를 제거하는 식으로 재구성할 수 있다.

실제 응용에서 다중해상도 분석의 가치는, 신호나 이미지가 가지고 있는 다양한 스케일의 특징을 한 번에 통합적으로 다룰 수 있다는 점이다. 전통적인 방법으로는 각각의 스케일별 필터링을 개별적으로 수행해야 했을 테지만, 웨이블릿 변환을 통해 계층적인 구조로 빠르게 분해할 수 있게 된다.

#### 웨이블릿 변환 구현 시 주의점

실습 예제를 통해 웨이블릿 변환이 다양한 방식으로 활용될 수 있음을 확인했지만, 실제 환경에서 구현할 때는 몇 가지 주의할 점이 있다. 특히 실시간성(real-time)이 요구되는 애플리케이션에서는 필터 계수의 길이가 긴 웨이블릿을 사용하면 지연(latency)이 커질 수 있으므로, 필요한 해상도와 계산 복잡도를 균형 있게 고려해야 한다. 또한 신호나 이미지의 경계 처리(boundary handling) 방식에 따라 결과가 달라질 수 있음을 명심해야 한다. PyWavelets나 다른 라이브러리들은 대개 여러 경계 모드를 지원하므로, 분석 목적에 맞는 모드를 적절히 선택하는 것이 중요하다.

정밀한 분석을 위해 연속 웨이블릿 변환(CWT)을 사용할 때는 스케일 범위와 간격에 대한 설정값이 적절해야만, 시간-주파수 지도(time-frequency map)가 의미 있는 해석을 담을 수 있다. 스케일을 너무 조밀하게 두면 불필요한 중복 정보로 인해 계산 비용이 크게 증가하고, 메모리 사용량도 급증한다. 반면 스케일을 지나치게 넓은 간격으로 설정하면, 놓치고 지나가는 국소 이벤트가 생길 수 있다.

결과 해석 단계에서는 각 스케일(또는 레벨)별 계수가 갖는 물리적 의미를 반드시 염두에 두어야 한다. 단순히 수학적으로 변환 계수를 구했다고 해서, 그 계수가 모두 동일한 중요도를 갖는 것은 아니다. 분석 대상이 물리적 신호라면, 해당 주파수 대역이 어떤 의미를 갖는지, 에너지가 집중된 구간이 시스템이나 현상의 어떤 상태를 반영하는지 종합적으로 검토해야 한다.

대규모 데이터를 실시간에 가깝게 처리하는 빅데이터 환경이나 스트리밍 분석에서는, 빠른 변환 알고리즘이 필요하다. 이 경우 Fast Wavelet Transform(FWT)을 직접 최적화하거나, GPU 연산을 지원하는 라이브러리를 모색할 수 있다. 최근에는 딥러닝 프레임워크와 결합하여 웨이블릿 변환을 커스텀 레이어로 구현하는 시도도 늘고 있다. 이러한 방법들을 통해 대용량 데이터에서도 웨이블릿의 다중해상도 분석 능력을 적극 활용할 수 있다.
