Skip to content

Commit fd8d164

Browse files
authored
Merge pull request #59 from liebharc/feature/next_conv
ConvNext and data augmentation
2 parents 46ab9cf + 8433695 commit fd8d164

29 files changed

Lines changed: 2611 additions & 969 deletions

Training.md

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,74 @@ This validation provides a **more representative indication of overall system pe
5555

5656
Implementation: `rate_validation_result.py`
5757

58+
## Run 334
59+
60+
Commit: 6d996c3d118c1e183f8412832383168e52630ce8
61+
Day: 17 Feb 2026
62+
Transformer Smoke Test: SER 6%
63+
System Level: 10.2 diffs, SER: 13.6%
64+
65+
convnextv2_base, https://github.com/liebharc/homr/pull/59
66+
67+
## Run 333
68+
69+
Commit: bb0ced2ff0cacdbd8ee33db4533a04c9e77f0ca8
70+
Day: 17 Feb 2026
71+
Transformer Smoke Test: SER 18%
72+
System Level: 14.4 diffs, SER: 14.5%
73+
74+
convnextv2_tiny, https://github.com/liebharc/homr/pull/59
75+
76+
## Run 331
77+
78+
Commit: e10346542968cc71fbcce0c0696f3ac963f11ae1
79+
Day: 17 Feb 2026
80+
Transformer Smoke Test: SER 6%
81+
System Level: 5.3 diffs, SER: 3.4%
82+
83+
Scheduled Sampling, https://github.com/liebharc/homr/pull/59
84+
85+
## Run 328 Epch 21
86+
87+
Commit: a78209527e2b8a4fb866fba9b2ef8540f4b8dad9
88+
Day: 14 Feb 2026
89+
Transformer Smoke Test: SER 10%
90+
System Level: 16.0 diffs, SER: 9.3%
91+
92+
Harder augmentation, https://github.com/liebharc/homr/pull/59
93+
94+
## Run 326 Epoch 13
95+
96+
Commit: 290d4e79aa377681523ca676b984b9cee3eb16ce
97+
Day: 13 Feb 2026
98+
Transformer Smoke Test: SER 13%
99+
System Level: 42.8 diffs, SER: 32.8%
100+
101+
Backtracking, removed 90deg rotation and sinusoidal bias, https://github.com/liebharc/homr/pull/59
102+
103+
## Run 322 Epoch 14
104+
105+
Commit: fd3d66d7d989003ec4cadd1d594ca2e820ece941
106+
Day: 12 Feb 2026
107+
Transformer Smoke Test: SER 12%
108+
System Level: 18.1 diffs, SER: 22.4%
109+
110+
Improved pitch accuracy, https://github.com/liebharc/homr/pull/59
111+
112+
## Run 317
113+
114+
Commit: 6f72a0bc2577907503e7ec84ac9850a5a972ded0
115+
Day: 4 Feb 2026
116+
Transformer Smoke Test: SER 15%
117+
System Level: 25.9 diffs, SER: 15.5%
118+
119+
ConvNext, https://github.com/liebharc/homr/pull/59
120+
58121
## Run 286 - after segnet update
59122

60123
Commit: 87d30ed79a81b4f07a38a8f6419334c59633709a
61124
Day: 30 Jan 2026
62-
Transformer Smoke Test: SER 26%
125+
Transformer Smoke Test: SER 14% (the SER reported for the previous runs was too large due to an unreasonable large temperature setting during the smoke test)
63126
System Level: 6.9 diffs, SER: 5.7%
64127

65128
Updated segnet model for staff detection.
@@ -68,8 +131,8 @@ Updated segnet model for staff detection.
68131

69132
Commit: 0daf75fea21e6ea6a865405e03a4bc7e73e9aa14
70133
Day: 4 Jan 2026
71-
Transformer Smoke Test: SER 26%
72-
System levle: 7.4 diffs
134+
Transformer Smoke Test: SER 26% (higher, due to an error in the smoke test)
135+
System Level: 7.4 diffs
73136

74137
After fixing an issue with accidentals during the conversion of the PrIMuS dataset Run 242 (a00be6debbedf617acdf39558c89ba6113c06af3)
75138
was used as basis of a 15 epoch run which only trained the lift decoder.
@@ -78,16 +141,16 @@ was used as basis of a 15 epoch run which only trained the lift decoder.
78141

79142
Commit: a00be6debbedf617acdf39558c89ba6113c06af3
80143
Day: 9 Dec 2025
81-
Transformer Smoke Test: SER 23% after fixing some errors in the SER calculation itself, before 36%
82-
System levle: 7.2 diffs after fixing an error in the validation result calculation itself, before 8.1
144+
Transformer Smoke Test: SER 23% (some errors in the smoke test fixed, still higher as some issue remained)
145+
System Level: 7.2 diffs after fixing an error in the validation result calculation itself, before 8.1
83146

84147
Singe staff images now use the full resolution.
85148

86149
## Run 236
87150

88151
Commit: 922ad08f8895f6d9c0ae61954cd78a021ff950a7
89152
Day: 26 Oct 2025
90-
Transformer Smoke Test: SER 37%
153+
Transformer Smoke Test: SER 37% (higher, due to an error in the smoke test)
91154
System Level: 9.8 diffs, 8.6 after some tweaks to the staff image preparation
92155

93156
Volta brackets, bf16
@@ -96,10 +159,10 @@ Volta brackets, bf16
96159

97160
Commit:ea96f0150ec74388df8cb0bb78ee2c36782a00d9
98161
Day: 01 Oct 2025
99-
Transformer Smoke Test: SER 39%
162+
Transformer Smoke Test: SER 39% (higher, due to an error in the smoke test)
100163
System Level: 9.8 diffs
101164

102-
Grandstaff support. With the grandstaff support the SER numbers can't be directly compared to the previous runs anymore. Validations results still can be used for comparison.
165+
Grandstaff support.
103166

104167
Some notes about other experiments which have been performed on the lieder dataset with only 15 epochs:
105168

homr/staff_dewarping.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,14 @@ def dewarp_staff_image(image: NDArray, staff: Staff, index: int, debug: Debug) -
373373

374374
def warp_image_randomly(image: PIL.Image.Image) -> PIL.Image.Image:
375375
array = np.array(image)
376-
result = warp_image_array_randomly(array)
376+
377+
# Randomly choose between two different warping algorithms
378+
# to add more variety to the dewarping augmentation.
379+
warp_algo = np.random.randint(0, 2)
380+
if warp_algo == 0:
381+
result = warp_image_array_randomly(array)
382+
else:
383+
result = warp_image_array_randomly2(array)
377384
return PIL.Image.fromarray(result)
378385

379386

@@ -393,8 +400,56 @@ def warp_image_array_randomly(image: NDArray) -> NDArray:
393400
]
394401
result = calculate_dewarp_transformation(
395402
image, [upper, source, lower], [upper, destination, lower]
396-
).dewarp(image, order=3)
397-
return (255 * result).astype(np.uint8)
403+
).dewarp(image, fill_color=255, order=3)
404+
405+
return result.astype(np.uint8)
406+
407+
408+
def warp_image_array_randomly2(image: NDArray) -> NDArray:
409+
"""
410+
Apply a smooth random warp to the image to simulate paper folding/bending.
411+
Uses cv2.remap with a 1D vertical displacement pattern (tiled horizontally)
412+
to create a realistic wavy look without "hard cuts" or 2D stretching.
413+
"""
414+
height, width = image.shape[:2]
415+
416+
# Use a small grid: 5 columns, 2 identical rows for vertical-only wave
417+
grid_w = 5
418+
grid_h = 2
419+
420+
# Generate random vertical displacement (same for both rows)
421+
# alpha controls the maximum displacement in pixels
422+
alpha = np.random.uniform(5, 25)
423+
dy_row = np.random.uniform(-1, 1, (1, grid_w)).astype(np.float32)
424+
425+
# Anchor left and right edges to zero displacement
426+
dy_row[0, 0] = 0
427+
dy_row[0, -1] = 0
428+
429+
# Tile vertically to create a consistent wave across the entire image height
430+
dy_small = np.tile(dy_row, (grid_h, 1))
431+
dx_small = np.zeros((grid_h, grid_w), dtype=np.float32)
432+
433+
# Upscale to full image size with cubic interpolation for smoothness
434+
dx = cv2.resize(dx_small, (width, height), interpolation=cv2.INTER_CUBIC) * alpha
435+
dy = cv2.resize(dy_small, (width, height), interpolation=cv2.INTER_CUBIC) * alpha
436+
437+
# Create coordinate maps for cv2.remap
438+
x, y = np.meshgrid(np.arange(width), np.arange(height))
439+
map_x = (x + dx).astype(np.float32)
440+
map_y = (y + dy).astype(np.float32)
441+
442+
# Perform the warping
443+
result = cv2.remap(
444+
image,
445+
map_x,
446+
map_y,
447+
interpolation=cv2.INTER_LINEAR,
448+
borderMode=cv2.BORDER_CONSTANT,
449+
borderValue=(255, 255, 255),
450+
)
451+
452+
return result.astype(np.uint8)
398453

399454

400455
if __name__ == "__main__":

homr/transformer/configs.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class FilePaths:
1212
def __init__(self) -> None:
13-
model_name = "pytorch_model_286-0daf75fea21e6ea6a865405e03a4bc7e73e9aa14"
13+
model_name = "pytorch_model_331-e10346542968cc71fbcce0c0696f3ac963f11ae1"
1414
self.encoder_path = os.path.join(
1515
workspace,
1616
f"encoder_{model_name}.onnx",
@@ -62,6 +62,9 @@ def __init__(self) -> None:
6262
self.ff_glu = True
6363
self.rel_pos_bias = False
6464
self.use_scalenorm = False
65+
self.attn_dropout = 0.1
66+
self.ff_dropout = 0.1
67+
self.layer_dropout = 0.1
6568

6669
def to_dict(self) -> dict[str, Any]:
6770
return {
@@ -70,6 +73,9 @@ def to_dict(self) -> dict[str, Any]:
7073
"ff_glu": self.ff_glu,
7174
"rel_pos_bias": self.rel_pos_bias,
7275
"use_scalenorm": self.use_scalenorm,
76+
"attn_dropout": self.attn_dropout,
77+
"ff_dropout": self.ff_dropout,
78+
"layer_dropout": self.layer_dropout,
7379
}
7480

7581
def to_json_string(self) -> str:
@@ -94,15 +100,17 @@ def __init__(self) -> None:
94100
self.num_lift_tokens = len(self.vocab.lift)
95101
self.num_articulation_tokens = len(self.vocab.articulation)
96102
self.num_position_tokens = len(self.vocab.position)
97-
self.encoder_structure = "hybrid"
103+
self.encoder_structure = "convnext"
98104
self.encoder_depth = 8
99105
self.backbone_layers = [3, 4, 6, 3]
100-
self.encoder_dim = 312
106+
self.encoder_dim = 512
107+
# encoder_h_dim balances how many dimensions the
108+
# horizontal vs vertical embeddings get
109+
self.encoder_h_dim = self.encoder_dim // 3
101110
self.encoder_heads = 8
102-
self.decoder_dim = 312
111+
self.decoder_dim = self.encoder_dim
103112
self.decoder_depth = 8
104113
self.decoder_heads = 8
105-
self.temperature = 0.01
106114
self.decoder_args = DecoderArgs()
107115
self.lift_vocab = self.vocab.lift
108116
self.pitch_vocab = self.vocab.pitch
@@ -111,6 +119,11 @@ def __init__(self) -> None:
111119
self.position_vocab = self.vocab.position
112120
self.use_gpu_inference = True
113121

122+
# Scheduled Sampling parameters
123+
self.scheduled_sampling_start_prob = 1.0
124+
self.scheduled_sampling_end_prob = 0.7
125+
self.scheduled_sampling_decay_steps = 20000
126+
114127
def to_dict(self) -> dict[str, Any]:
115128
return {
116129
"filepaths": self.filepaths.to_dict(),
@@ -132,7 +145,6 @@ def to_dict(self) -> dict[str, Any]:
132145
"decoder_dim": self.decoder_dim,
133146
"decoder_depth": self.decoder_depth,
134147
"decoder_heads": self.decoder_heads,
135-
"temperature": self.temperature,
136148
"decoder_args": self.decoder_args.to_dict(),
137149
}
138150

homr/transformer/decoder_inference.py

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from math import ceil
21
from typing import Any
32

43
import numpy as np
54
import onnxruntime as ort
65

76
from homr.simple_logging import eprint
87
from homr.transformer.configs import Config
9-
from homr.transformer.utils import softmax
108
from homr.transformer.vocabulary import EncodedSymbol
119
from homr.type_definitions import NDArray
1210

@@ -22,6 +20,7 @@ def __init__(
2220
):
2321
super().__init__()
2422
self.ignore_index = ignore_index
23+
self.config = config
2524
self.net = transformer
2625
self.io_binding = self.net.io_binding()
2726
self.max_seq_len = config.max_seq_len
@@ -49,8 +48,6 @@ def generate(
4948
self,
5049
start_tokens: NDArray,
5150
nonote_tokens: NDArray,
52-
temperature: float = 1.0,
53-
filter_thres: float = 0.7,
5451
**kwargs: Any,
5552
) -> list[EncodedSymbol]:
5653
num_dims = len(start_tokens.shape)
@@ -103,32 +100,19 @@ def generate(
103100
outputs = self.io_binding.get_outputs()
104101
cache = outputs[6:]
105102

106-
# And convert the outputs to np arrays...
103+
# Greedy decoding: pick the highest logit directly for each output
107104
rhythmsp = outputs[0].numpy()
108105
pitchsp = outputs[1].numpy()
109106
liftsp = outputs[2].numpy()
110107
positionsp = outputs[3].numpy()
111108
articulationsp = outputs[4].numpy()
112109
attention = outputs[5].numpy()
113110

114-
# ...To continue with the normal CPU based processing
115-
filtered_lift_logits = top_k(liftsp[:, -1, :], thres=filter_thres)
116-
filtered_pitch_logits = top_k(pitchsp[:, -1, :], thres=filter_thres)
117-
filtered_rhythm_logits = top_k(rhythmsp[:, -1, :], thres=filter_thres)
118-
filtered_articulations_logits = top_k(articulationsp[:, -1, :], thres=filter_thres)
119-
filtered_positions_logits = top_k(positionsp[:, -1, :], thres=filter_thres)
120-
121-
lift_probs = softmax(filtered_lift_logits / temperature, dim=-1)
122-
pitch_probs = softmax(filtered_pitch_logits / temperature, dim=-1)
123-
rhythm_probs = softmax(filtered_rhythm_logits / temperature, dim=-1)
124-
articulation_probs = softmax(filtered_articulations_logits / temperature, dim=-1)
125-
positions_probs = softmax(filtered_positions_logits / temperature, dim=-1)
126-
127-
lift_sample = np.array([[lift_probs.argmax()]])
128-
pitch_sample = np.array([[pitch_probs.argmax()]])
129-
rhythm_sample = np.array([[rhythm_probs.argmax()]])
130-
articulation_sample = np.array([[articulation_probs.argmax()]])
131-
position_sample = np.array([[positions_probs.argmax()]])
111+
rhythm_sample = np.array([[rhythmsp[:, -1, :].argmax()]])
112+
pitch_sample = np.array([[pitchsp[:, -1, :].argmax()]])
113+
lift_sample = np.array([[liftsp[:, -1, :].argmax()]])
114+
articulation_sample = np.array([[articulationsp[:, -1, :].argmax()]])
115+
position_sample = np.array([[positionsp[:, -1, :].argmax()]])
132116

133117
lift_token = detokenize(lift_sample, self.inv_lift_vocab)
134118
pitch_token = detokenize(pitch_sample, self.inv_pitch_vocab)
@@ -160,19 +144,21 @@ def init_cache(self, cache_len: int = 0) -> tuple[list[NDArray], list[str], list
160144
cache = []
161145
input_names = []
162146
output_names = []
163-
for i in range(32):
147+
heads = self.config.decoder_heads
148+
head_dim = self.config.decoder_dim // heads
149+
for i in range(self.config.decoder_depth * 4):
164150
if self.fp16: # the cache needs to be fp16 as well
165151
cache.append(
166152
ort.OrtValue.ortvalue_from_numpy(
167-
np.zeros((1, 8, cache_len, 64), dtype=np.float16),
153+
np.zeros((1, heads, cache_len, head_dim), dtype=np.float16),
168154
"cuda" if self.use_gpu else "cpu",
169155
self.device_id,
170156
)
171157
)
172158
else:
173159
cache.append(
174160
ort.OrtValue.ortvalue_from_numpy(
175-
np.zeros((1, 8, cache_len, 64), dtype=np.float32),
161+
np.zeros((1, heads, cache_len, head_dim), dtype=np.float32),
176162
"cuda" if self.use_gpu else "cpu",
177163
self.device_id,
178164
)
@@ -182,30 +168,6 @@ def init_cache(self, cache_len: int = 0) -> tuple[list[NDArray], list[str], list
182168
return cache, input_names, output_names
183169

184170

185-
def top_k(logits: NDArray, thres: float = 0.9) -> NDArray:
186-
"""Numpy implementation matching torch's top_k behavior"""
187-
k = ceil((1 - thres) * logits.shape[-1])
188-
189-
# Get top k elements
190-
flat_logits = logits.ravel()
191-
indices = np.argpartition(flat_logits, -k)[-k:] # Get indices of top k elements
192-
indices = indices[np.argsort(-flat_logits[indices])] # Sort them in descending order
193-
values = flat_logits[indices] # Get the corresponding values
194-
195-
# Create output array with -inf
196-
output = np.full_like(logits, -np.inf)
197-
198-
# Scatter the topk values back into the output array
199-
# For multi-dimensional arrays, we need to convert flat indices to multi-indices
200-
if logits.ndim > 1:
201-
multi_indices = np.unravel_index(indices, logits.shape)
202-
output[multi_indices] = values
203-
else:
204-
output[indices] = values
205-
206-
return output
207-
208-
209171
def detokenize(tokens: NDArray, vocab: dict[int, str]) -> list[str]:
210172
toks = [vocab[tok.item()] for tok in tokens]
211173
toks = [t for t in toks if t not in ("[BOS]", "[EOS]", "[PAD]")]

0 commit comments

Comments
 (0)