mitian@16
|
1 #!/usr/bin/env python
|
mitian@16
|
2 # encoding: utf-8
|
mitian@16
|
3 """
|
mitian@16
|
4 snmf.py
|
mitian@16
|
5 -- A modified version of C-NMF by Nieto. Input is no longer feature matrix but SSM.
|
mitian@16
|
6
|
mitian@16
|
7 C-NMF method for segmentation, modified from here:
|
mitian@16
|
8
|
mitian@16
|
9 Nieto, O., Jehan, T., Convex Non-negative Matrix Factorization For Automatic
|
mitian@16
|
10 Music Structure Identification. Proc. of the 38th IEEE International Conference
|
mitian@16
|
11 on Acoustics, Speech, and Signal Processing (ICASSP). Vancouver, Canada, 2013.
|
mitian@16
|
12
|
mitian@16
|
13 """
|
mitian@16
|
14
|
mitian@16
|
15 import sys
|
mitian@16
|
16 import os
|
mitian@16
|
17 import pymf
|
mitian@16
|
18 from utils import SegUtil
|
mitian@16
|
19 from utils.SegUtil import median_filter
|
mitian@16
|
20 import numpy as np
|
mitian@16
|
21
|
mitian@16
|
22 # Algorithm params
|
mitian@16
|
23 h = 8 # Size of median filter for SSMs in C-NMF
|
mitian@16
|
24 R = 16 # Size of the median filter for the activation matrix C-NMF
|
mitian@16
|
25 rank = 3 # Rank of decomposition for the boundaries
|
mitian@16
|
26 rank_labels = 16 # Rank of decomposition for the labels
|
mitian@16
|
27 R_labels = 4 # Size of the median filter for the labels
|
mitian@16
|
28
|
mitian@16
|
29
|
mitian@16
|
30 def cnmf(S, rank, niter=500):
|
mitian@16
|
31 """(Convex) Non-Negative Matrix Factorization.
|
mitian@16
|
32
|
mitian@16
|
33 Parameters
|
mitian@16
|
34 ----------
|
mitian@16
|
35 S: np.array(N, N)
|
mitian@16
|
36 SSM.
|
mitian@16
|
37 rank: int
|
mitian@16
|
38 Rank of decomposition
|
mitian@16
|
39 niter: int
|
mitian@16
|
40 Number of iterations to be used
|
mitian@16
|
41
|
mitian@16
|
42 Returns
|
mitian@16
|
43 -------
|
mitian@16
|
44 F: np.array
|
mitian@16
|
45 Cluster matrix (decomposed matrix)
|
mitian@16
|
46 G: np.array
|
mitian@16
|
47 Activation matrix (decomposed matrix)
|
mitian@16
|
48 (s.t. S ~= F * G)
|
mitian@16
|
49 """
|
mitian@16
|
50 nmf_mdl = pymf.CNMF(S, num_bases=rank)
|
mitian@16
|
51 nmf_mdl.factorize(niter=niter)
|
mitian@16
|
52 F = np.asarray(nmf_mdl.W)
|
mitian@16
|
53 G = np.asarray(nmf_mdl.H)
|
mitian@16
|
54 return F, G
|
mitian@16
|
55
|
mitian@16
|
56 def nmf(S, rank, nither=500):
|
mitian@16
|
57 nmf_mdl = pymf.NMF(S, num_bases=rank, niter=nither)
|
mitian@16
|
58 nmf_mdl.factorize()
|
mitian@16
|
59 F = np.asarray(nmf_mdl.W)
|
mitian@16
|
60 G = np.asarray(nmf_mdl.H)
|
mitian@16
|
61 return F, G
|
mitian@16
|
62
|
mitian@16
|
63
|
mitian@16
|
64 def most_frequent(x):
|
mitian@16
|
65 """Returns the most frequent value in x."""
|
mitian@16
|
66 return np.argmax(np.bincount(x))
|
mitian@16
|
67
|
mitian@16
|
68
|
mitian@16
|
69 def compute_labels(X, rank, R, bound_idxs, niter=300):
|
mitian@16
|
70 """Computes the labels using the bounds."""
|
mitian@16
|
71
|
mitian@16
|
72 X = X.T
|
mitian@16
|
73 try:
|
mitian@16
|
74 F, G = cnmf(X, rank, niter=niter)
|
mitian@16
|
75 except:
|
mitian@16
|
76 return [1]
|
mitian@16
|
77
|
mitian@16
|
78 label_frames = filter_activation_matrix(G.T, R)
|
mitian@16
|
79 label_frames = np.asarray(label_frames, dtype=int)
|
mitian@16
|
80
|
mitian@16
|
81 # Get labels from the label frames
|
mitian@16
|
82 labels = []
|
mitian@16
|
83 bound_inters = zip(bound_idxs[:-1], bound_idxs[1:])
|
mitian@16
|
84 for bound_inter in bound_inters:
|
mitian@16
|
85 if bound_inter[1] - bound_inter[0] <= 0:
|
mitian@16
|
86 labels.append(np.max(label_frames) + 1)
|
mitian@16
|
87 else:
|
mitian@16
|
88 labels.append(most_frequent(
|
mitian@16
|
89 label_frames[bound_inter[0]:bound_inter[1]]))
|
mitian@16
|
90
|
mitian@16
|
91 return labels
|
mitian@16
|
92
|
mitian@16
|
93
|
mitian@16
|
94 def filter_activation_matrix(G, R):
|
mitian@16
|
95 """Filters the activation matrix G, and returns a flattened copy."""
|
mitian@16
|
96 idx = np.argmax(G, axis=1)
|
mitian@16
|
97 max_idx = np.arange(G.shape[0])
|
mitian@16
|
98 max_idx = (max_idx, idx.flatten())
|
mitian@16
|
99 G[:, :] = 0
|
mitian@16
|
100 G[max_idx] = idx + 1
|
mitian@16
|
101 G = np.sum(G, axis=1)
|
mitian@16
|
102 G = SegUtil.median_filter(G[:, np.newaxis], R)
|
mitian@16
|
103 return G.flatten()
|
mitian@16
|
104
|
mitian@16
|
105
|
mitian@16
|
106 def segmentation(X, rank=4, R=15, h=8, niter=300, CNMF=True):
|
mitian@16
|
107 """
|
mitian@16
|
108 Gets the segmentation (boundaries and labels) from the factorization
|
mitian@16
|
109 matrices.
|
mitian@16
|
110
|
mitian@16
|
111 Parameters
|
mitian@16
|
112 ----------
|
mitian@16
|
113 X: np.array()
|
mitian@16
|
114 Features matrix (e.g. chromagram)
|
mitian@16
|
115 rank: int
|
mitian@16
|
116 Rank of decomposition
|
mitian@16
|
117 R: int
|
mitian@16
|
118 Size of the median filter for activation matrix
|
mitian@16
|
119 niter: int
|
mitian@16
|
120 Number of iterations for k-means
|
mitian@16
|
121 bound_idxs : list
|
mitian@16
|
122 Use previously found boundaries (None to detect them)
|
mitian@16
|
123 CNMF : bool
|
mitian@16
|
124 If True, use CNMF; otherwise use NMF
|
mitian@16
|
125
|
mitian@16
|
126 Returns
|
mitian@16
|
127 -------
|
mitian@16
|
128 bounds_idx: np.array
|
mitian@16
|
129 Bound indeces found
|
mitian@16
|
130 labels: np.array
|
mitian@16
|
131 Indeces of the labels representing the similarity between segments.
|
mitian@16
|
132 """
|
mitian@16
|
133
|
mitian@16
|
134 # Filter
|
mitian@16
|
135 X = median_filter(X, M=h)
|
mitian@16
|
136 X = X.T
|
mitian@16
|
137
|
mitian@16
|
138 # Find non filtered boundaries
|
mitian@16
|
139 bound_idxs = None
|
mitian@16
|
140 while True:
|
mitian@16
|
141 if bound_idxs is None:
|
mitian@16
|
142 try:
|
mitian@16
|
143 if CNMF: F, G = cnmf(X, rank, niter=niter)
|
mitian@16
|
144 else: F, G = nmf(X, rank, niter=niter)
|
mitian@16
|
145 except:
|
mitian@16
|
146 return np.empty(0), [1]
|
mitian@16
|
147
|
mitian@16
|
148 # Filter G
|
mitian@16
|
149 G = filter_activation_matrix(G.T, R)
|
mitian@16
|
150 if bound_idxs is None:
|
mitian@16
|
151 bound_idxs = np.where(np.diff(G) != 0)[0] + 1
|
mitian@16
|
152
|
mitian@16
|
153 if len(np.unique(bound_idxs)) <= 2:
|
mitian@16
|
154 rank += 1
|
mitian@16
|
155 bound_idxs = None
|
mitian@16
|
156 else:
|
mitian@16
|
157 break
|
mitian@16
|
158
|
mitian@16
|
159 return G, bound_idxs
|