独立成分分析を使ったブラインド音源分離

# はじめに
前回の「[グラフで理解する独立成分分析](https://qiita.com/Miyabi1456/items/3fdc5a1603269db197d5)」の続きです。
独立成分分析を使って混ざった信号を分離します。
scikit-learnに全く同じことをやったチュートリアルがありますが、
ここではscikit-learnなしでnumpyとscipyだけでも実装します。

# 参考

scikit-learnを使ったチュートリアルです。
実際のデータに使うときはライブラリに頼ったほうが簡単そうです。
[scikit-learn Blind source separation using FastICA](https://scikit-learn.org/stable/auto_examples/decomposition/plot_ica_blind_source_separation.html)

[データ化学工学研究室 独立成分分析 (Independent Component Analysis, ICA) ～PCAの無相関より強力な ”独立” な成分を抽出～](https://datachemeng.com/independentcomponentanalysis/)

[独立成分解析の信号処理への応用](https://www.ism.ac.jp/~shiro/papers/etc/sice1999Jul.pdf)

# ブラインド音源分離の流れ

今回は2つの信号を混ぜることを考えます。
2つの信号が非ガウス分布で独立であるなら、独立成分分析を行えば分離できます。

## 1. 信号の混ざり方を決める混合行列

混ぜる前の信号を$S$とします。

```math
S = \left[
\begin{matrix} s_1 & s_2 \end{matrix}
\right]
```

2つの信号をどんな割合で混ぜるかを決める行列を混合行列と言い、ここでは$M$とします。

```math
M = \left[
\begin{matrix}
    m_{11} & m_{12} \\
    m_{21} & m_{22}
\end{matrix}
  \right]
```

混ざった後の信号$X$は

```math
X = SM
```

一般的に$X$が測定データです。

$M$の逆行列が求まれば元の信号がわかることになります。


```math
S = XM^{-1}
```

## 2. 独立成分分析

独立成分分析では、

1. 無相関化
2. 分散の正規化 
3. 非ガウス性が最大になるように直交変換

の順に行います。

イメージを掴むために、以下のデータに操作を加えて可視化します。

![ICA_平均0分散1に正規化したデータ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/26cc6c04-44c8-bab3-647f-3c4a3185ada4.png)


### 2-1. 無相関化

主成分分析を使って無相関化します。データの平均値は予め0にしてあるとします。
手順は、分散共分散行列を計算、その固有値分解を行い、データに固有ベクトルをかけます。

```math
cov = {\rm COV} (X) \\
UWU^T = {\rm SVD} (cov) \\
X_{2-1} = XU
```
$X$の添字は節と同じ数字にしておきます。

![ICA_PCAしたあと.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/97f4fd7b-6a75-0939-7107-7848de1385c6.png)

### 2-2. 分散の正規化
分散を正規化します。
固有値$W$は対角行列なので、逆行列は対角要素の逆数が並ぶ対角行列です。
それから対角成分の平方根を取ってかけます。

```math
X_{2-2} = X_{2-1} W^{-1/2}
```

![ICA_PCAして正規化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/5d2678fb-81ec-56f5-b83e-089e98f28b45.png)



### 2-3. 非ガウス性が最大になるように直交変換

まだ回転方向に自由度があります。
非ガウス性が最大になるような直交変換を行う回転行列$Z$を探します。
どれくらいガウス分布から離れたかの指標には尖度などが用いられます。

```math
S = X_{2-2}Z
```

これで信号の分離ができました。

![2020_0722_ICA_rotation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/d9e7d65b-dc5f-c565-3dfc-beb0fbd1719d.gif)


## 3. 混合行列を計算する

測定データの中に分離できた信号がどんな割合で含まれているのか知りたくなります。
それは混合行列がわかればいいことになります。

先程までの操作をまとめます。

```math
S = XUW^{-1/2}Z
```
測定データは混合行列$M$によってこのように生まれました。

```math
S = XM^{-1}
```

両者を比較して、

```math
M^{-1} = UW^{-1/2}Z \\
\therefore M = \left( UW^{-1/2}Z \right) ^{-1}
```

これで混合行列が求まりました。


# ブラインド音源分離の実装

## NumpyとScipyを使った実装

先程の式に従って実装します。

### 0. 使用するモジュールと関数

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from sklearn.decomposition import FastICA
```
円を描くときに使います。

```python
def circle(n,r=1):
    rad = np.linspace(0.0,2.0*np.pi,n)
    return np.asarray([np.cos(rad),np.sin(rad)]).T
```

データを回転させるときに使います。

```python
def rotation_matrix(rad):
    return np.array([[np.cos(rad),-np.sin(rad)],[np.sin(rad),np.cos(rad)]])
```

### 1. 混ざった信号を作る

2つの信号を作ります。最終的にこれが得られれば良いです。

```python
t = np.linspace(0.0,1.0,5000)
s1 = np.sin(2.0*np.pi*3.0*t)
s2 = np.sign(np.sin(2.0*np.pi*4.0*t))
S = np.array([s1,s2]).T
S += 0.05 * np.random.normal(size=S.shape)
```

![moto_data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/a167e7da-5b22-19c4-35ef-50e5ef00a57d.png)

散布図はこのようになります。x軸とy軸が独立であることがわかります。
![moto_scatter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/021b29b1-12c1-565a-2000-209b230d8afd.png)


上の信号の混ざり具合の異なる2つの信号を作ります。

```python
Mix = np.array([[0.2,0.4],[0.8,0.6]]) #混合行列
X = S.dot(Mix)
```
![kongou_singou.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/29350fb2-4b7c-0011-2bfe-690686dc8eee.png)

散布図はこのようになります。相関があるように見えます。

![scatter_kongou.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/89245bc7-8701-843a-0c56-f3d39ded6bc2.png)

### 2. 無相関化

主成分分析を使って無相関化します。

```python
cov = np.cov(X, rowvar=False) #分散共分散行列
U,W,_ = np.linalg.svd(cov) #固有値分解 Uが固有ベクトル Wが固有値
pca_X = X.dot(U)

#確率長円もどきを描きます
c = circle(200,r=1.0)
c = c*np.sqrt(W)
```

![pca_ato.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/ec78ea4c-66eb-b875-46e7-e0e851979774.png)

### 3. 分散の正規化
分散共分散行列を単位行列にします。

```python
norm_pca_X = pca_X/np.sqrt(W)
c /= np.sqrt(W)
```

![ICA_PCA_normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/359a7a2b-cbdd-8115-e2ad-9dc4568539b6.png)

### 4. 非ガウス性の最大化
ここではガウス分布にどの程度従うかの指標に尖度を使い、総当たりで回転行列を探します。

```python
rad_array = np.linspace(0.0,np.pi/2.0,1000)

kurt_array = []
for rad in rad_array:
    rot_data = norm_pca_X.dot(rotation_matrix(rad))
    kurt = stats.kurtosis(rot_data)
    kurt_array.append(kurt)
kurt_array = np.asarray(kurt_array)

fig,axes = plt.subplots()
axes.plot(rad_array,kurt_array.T[0])
axes.plot(rad_array,kurt_array.T[1])
axes.set_xlabel("Rotation angle rad")
axes.set_ylabel("kurtosis")
```

![ICA_kurt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/9c360dc6-320a-a426-1a7f-345b76b4f1a4.png)

上の2つのプロットを足していいのか知りませんが、足してうまくいくので良しとします。

```python
Z = rotation_matrix(rad_array[np.argmax(np.abs(np.sum(kurt_array,axis=1)))])

S = norm_pca_X.dot(Z)
```

![ICA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/51f22820-ce5c-3aa3-f8c8-2a9b424e5451.png)

分離された信号$S$が求まりました。

```python
fig,axes = plt.subplots()
axes.plot(t,S.T[0])
axes.plot(t,S.T[1])
```

![ICA_s1_s2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/0243d537-6041-bc52-c2e8-d48bbc35e848.png)

### 5. 混合行列の計算

```python
M = np.linalg.inv((U/np.sqrt(W)).dot(Z))
```

分離できた信号とこの混合行列を使って、計測データが再構成できるか試します。

```python
np.allclose(X, S.dot(M))

>>> True
````

無事に再構成できました。

## scikit-learnを使った実装

公式チュートリアル通りです。

```python
ica = FastICA(n_components=2)
S_ = ica.fit_transform(X)
M_ = ica.mixing_
```

たった3行で済みます。

```python
fig,axes = plt.subplots(figsize=(6,6))
axes.scatter(S_.T[0],S_.T[1],alpha=0.1)
axes.set_aspect("equal")
```

![sklearn_ICA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/48713de1-1291-99b5-d36a-971a926c9d4b.png)

![sklearn_ICA_s1_s2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/a615c835-df9d-b2d4-5bb9-a5ea12591b1f.png)

```python
np.allclose(X, np.dot(S_,M_.T))

>>> True
```

こちらも同様の結果になります。

# まとめ
独立成分分析を使ったブラインド音源分離を順を追って実装しました。
混ざる前の信号が非ガウス分布で独立であるとき、混合後の信号から分離が可能であることが示せました。
実際のマイクを使って試してみたいと思います。