File size: 10,360 Bytes
2c6366f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
from torch import optim, nn, mean
import torch
import lightning as L
from typing import Any
from pytorch_lightning.utilities.types import STEP_OUTPUT, OptimizerLRScheduler
from src.config import NUM_FRETS, NUM_STRINGS, PITCH_CLASSES, PLAYABILITY_THRESHOLD
import src.config as C
from torchmetrics import Accuracy, Precision, Recall, F1Score
from src.data.dataset import TorchDataset
from src.model.losses import pitch_class_loss, completeness_metric, open_chord_metric, playability_metric
import src.model.losses as Lo
import music21 as m21

class FingeringPredictor(L.LightningModule):
    def __init__(self, span_loss_coeff: float = 0.0, with_mute: bool = False, 
            learning_rate: float = 1e-3,
            *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.save_hyperparameters()
        self.with_mute = with_mute
        self.num_frets = NUM_FRETS + 1 if with_mute else NUM_FRETS
        self.model = nn.Linear((self.num_frets*NUM_STRINGS) + 2*PITCH_CLASSES,
                out_features=(NUM_STRINGS*self.num_frets))
        self.activation = nn.Sigmoid()
        self.pc_loss = pitch_class_loss
        self.loss = nn.BCELoss()
        self.span_loss = Lo.hand_span_loss
        self.oc_metric = open_chord_metric
        self.acc = Accuracy(task="binary")
        self.prec = Precision(task="binary")
        self.rec = Recall(task="binary")
        self.sf_prec = Lo.stringfret_precision
        self.sf_rec = Lo.stringfret_recall
        self.sf_f1 = Lo.stringfret_f1
        self.lr = learning_rate
        self.f1 = F1Score(task="binary")
        self.sf_exactness = Lo.stringwise_exactness
        self.playability = playability_metric
        self.completeness = completeness_metric
        self.span_loss_coeff = span_loss_coeff

    def forward(self, x) -> Any:
        z = self.model(x)
        y_hat = self.activation(z) # [B, NUM_FRETS*NUM_STRINGS]
        if not self.with_mute:
            return y_hat
        if x.ndim == 2:
            y_hat = nn.functional.normalize(y_hat.view((x.size()[0], NUM_STRINGS, self.num_frets)),
                    p=1, dim=2).flatten(start_dim=1)
        else:
            y_hat = nn.functional.normalize(y_hat.view((NUM_STRINGS, self.num_frets)),
                    p=1, dim=1).flatten()
        return y_hat

    def training_step(self, batch, batch_idx, *args: Any, **kwargs: Any) -> STEP_OUTPUT:
        x, y = batch
        y_hat = self(x)
        loss = self.loss(y_hat, y)
        if self.span_loss_coeff > 0 :
            span_loss = self.span_loss(torch.unflatten(y_hat, dim=1, sizes=(NUM_STRINGS, self.num_frets)),
                    with_mute=self.with_mute)
            loss = loss + self.span_loss_coeff*span_loss
            self.log("Train/span-loss", span_loss, prog_bar=True)
        self.log("Train/loss", loss, prog_bar=True)
        return loss

    def validation_step(self, batch, batch_idx, *args: Any, **kwargs: Any) -> STEP_OUTPUT:
        x, y = batch
        y_hat = self(x)
        loss = self.loss(y_hat, y)
        if self.span_loss_coeff > 0 :
            span_loss = self.span_loss(torch.unflatten(y_hat, dim=1, sizes=(NUM_STRINGS, self.num_frets)),
                    with_mute=self.with_mute)
            loss = loss + self.span_loss_coeff*span_loss
            self.log("Val/span-loss", span_loss, prog_bar=True)
        self.log("Val/loss", loss, prog_bar=True)
        self.log("val_loss", loss, prog_bar=False)
        return loss

    def binarize(self, tensor) -> torch.Tensor:
        if self.with_mute:
            max_tensor = torch.max(torch.unflatten(tensor, 1, 
                (-1, self.num_frets)), dim=-1).values
            pred_bin = torch.where(torch.unflatten(tensor, 1, 
                (-1, self.num_frets)) == max_tensor[:, :, None], 1, 0)
        else:
            pred_bin = tensor > 0.5
            pred_bin = torch.unflatten(pred_bin, 1, (-1, self.num_frets))
        return pred_bin

    def test_step(self, batch, batch_idx, *args: Any, **kwargs: Any) -> STEP_OUTPUT:
        x, y = batch
        batch_size = len(y)
        y_hat = self(x)
        loss = self.loss(y_hat, y)
        acc = self.acc(y_hat, y)
        rec = self.rec(y_hat, y)
        prec = self.prec(y_hat, y)
        f1 = self.f1(y_hat, y)
        pred_bin = self.binarize(y_hat)
        fingerings = pred_bin.view((y_hat.size()[0], NUM_STRINGS, self.num_frets))
        expected = y.view((y_hat.size()[0], NUM_STRINGS, self.num_frets))
        expected_notes = Lo._midi_notes_from_fingering_batchwise(expected[:, :,:-1])
        expected_notes = [[m21.note.Note(a).name for a in expected_notes[i] if a !=0] for i in range(len(expected_notes))]
        pred_notes = Lo._midi_notes_from_fingering_batchwise(pred_bin[:, :, :-1])
        pred_notes = [[m21.note.Note(a).name for a in pred_notes[i] if a !=0] for i in range(len(pred_notes))]
        pc_prec = torch.Tensor([Lo.pc_precision(expected_notes[i], pred_notes[i]) for i in range(len(expected_notes))])
        pc_rec = torch.Tensor([Lo.pc_recall(expected_notes[i], pred_notes[i]) for i in range(len(expected_notes))])
        oc = self.oc_metric(fingerings, expected)
        sw_exact = self.sf_exactness(fingerings, expected)
        sf_prec = self.sf_prec(fingerings, expected, with_mute=self.with_mute)
        sf_rec = self.sf_rec(fingerings, expected, with_mute=self.with_mute)
        sf_f1 = self.sf_f1(fingerings, expected, with_mute=self.with_mute)
        pc_f1 = torch.div(2*pc_prec*pc_rec, pc_prec+pc_rec)
        pc_f1 = torch.nan_to_num(pc_f1, nan=0)
        diagrams = [TorchDataset.fingering_from_target_tensor(pred_bin.view((batch_size, NUM_STRINGS, self.num_frets))[i], with_mute=self.with_mute) for i in range(batch_size)]
        diagrams_previous = [TorchDataset.fingering_from_source_tensor(x[i], with_mute=self.with_mute) for i in range(batch_size)]
        playability = torch.Tensor([Lo.anatomical_score(diagrams[i])[0] for i in range(len(diagrams))])
        unplayable = playability < PLAYABILITY_THRESHOLD
        best_fingerings = [Lo.anatomical_score(diagrams[i])[1] for i in range(len(diagrams))]
        best_fingerings_previous = [Lo.anatomical_score(diagrams_previous[i])[1] for i in range(batch_size)]
        transition_costs = torch.Tensor([Lo.transition_cost(best_fingerings_previous[i],
                                                            best_fingerings[i]) for i in range(batch_size)])
        c_r_muted = [Lo.ratio_muted_strings(current_diag) for current_diag in diagrams_previous]
        c_r_open = [Lo.ratio_open_strings(current_diag) for current_diag in diagrams_previous]
        c_num_strings = [Lo.num_strings_played(current_diag) for current_diag in diagrams_previous]
        c_centroid = [Lo.string_centroid(current_diag) for current_diag in diagrams_previous]
        c_r_unique = [Lo.ratio_unique_notes(current_diag) for current_diag in diagrams_previous]
        n_r_muted = [Lo.ratio_muted_strings(current_diag) for current_diag in diagrams]
        n_r_open = [Lo.ratio_open_strings(current_diag) for current_diag in diagrams]
        n_num_strings = [Lo.num_strings_played(current_diag) for current_diag in diagrams]
        n_centroid = [Lo.string_centroid(current_diag) for current_diag in diagrams]
        n_r_unique = [Lo.ratio_unique_notes(current_diag) for current_diag in diagrams]
        r_muted_diff = [abs(c_r_muted[i] - n_r_muted[i]) for i in range(batch_size)]
        r_open_diff = [abs(c_r_open[i] - n_r_open[i]) for i in range(batch_size)]
        num_strings_diff = [abs(c_num_strings[i] - n_num_strings[i]) for i in range(batch_size)]
        centroid_diff = [abs(c_centroid[i] - n_centroid[i]) for i in range(batch_size)]
        r_unique_diff = [abs(c_r_unique[i] - n_r_unique[i]) for i in range(batch_size)]
        self.log("Test/loss", loss, prog_bar=False)
        self.log("Test/acc", acc)
        self.log("Test/rec", rec)
        self.log("Test/prec", prec)
        self.log("Test/SF rec", sf_rec.mean())
        self.log("Test/SF prec", sf_prec.mean())
        self.log("Test/SF F1", sf_f1.mean())
        self.log("Test/PC F1", pc_f1.mean())
        self.log("Test/F1", f1)
        self.log("Test/exactness", self.exact_acc(pred_bin, y))
        self.log("Test/StringExactness", sw_exact.mean())
        self.log("Test/PC-Recall", pc_rec.mean())
        self.log("Test/playability", playability.mean())
        self.log("Test/unplayable", unplayable.mean(dtype=torch.float32))
        self.log("Test/open-closed", oc.mean(dtype=torch.float32))
        self.log("Test/PC-Precision", pc_prec.mean())
        self.log('Test/transition_cost', transition_costs.mean())
        self.log('Test/ratio_muted', torch.Tensor(n_r_muted).mean())
        self.log('Test/ratio_open', torch.Tensor(n_r_open).mean())
        self.log('Test/num_strings', torch.Tensor(n_num_strings).mean())
        self.log('Test/centroid', torch.Tensor(n_centroid).mean())
        self.log('Test/ratio_unique_notes', torch.Tensor(n_r_unique).mean())
        self.log('Test/ratio_muted_diff', torch.Tensor(r_muted_diff).mean())
        self.log('Test/ratio_open_diff', torch.Tensor(r_open_diff).mean())
        self.log('Test/num_strings_diff', torch.Tensor(num_strings_diff).mean())
        self.log('Test/centroid_diff', torch.Tensor(centroid_diff).mean())
        self.log('Test/ratio_unique_notes_diff', torch.Tensor(r_unique_diff).mean())
        return loss

    def exact_acc(self, pred, target, threshold: float = 0.5):
        tmp = torch.eq(torch.flatten(pred,start_dim=1), target)
        return torch.mean(torch.all(tmp, dim=-1), dtype=torch.float16)
        
    def configure_optimizers(self) -> OptimizerLRScheduler:
        optimizer = optim.Adam(self.parameters(), lr=self.lr)
        return optimizer
        

class FingeringPredictorBaseline(FingeringPredictor):
    def __init__(self, learning_rate: float = 0.001, *args: Any, **kwargs: Any) -> None:
        super().__init__(learning_rate=learning_rate, **kwargs)
        self.model = nn.Linear(2*PITCH_CLASSES, self.num_frets*NUM_STRINGS)

    def forward(self, x) -> Any:
        # drop chord fingering info (first dim is batch size)
        if x.dim() > 1:
            x = x[:, -2*PITCH_CLASSES:]
        else:
            x = x[-2*PITCH_CLASSES:]
        z = self.model(x)
        y_hat = self.activation(z)
        return y_hat