[{"data":1,"prerenderedAt":705},["ShallowReactive",2],{"article:identifying-the-raga-from-the-notes":3,"\u002Fblog\u002Fidentifying-the-raga-from-the-notes-surround":694},{"id":4,"title":5,"archived":6,"author":7,"body":8,"date":678,"description":679,"extension":680,"image":681,"meta":682,"minRead":683,"navigation":684,"path":685,"published":684,"seo":686,"sitemap":687,"stem":688,"tags":689,"__hash__":693},"blog\u002Fblog\u002Fidentifying-the-raga-from-the-notes.md","Identifying the Raga from the Notes",false,"Baljeet Singh",{"type":9,"value":10,"toc":669},"minimark",[11,15,18,21,26,29,70,78,83,86,90,93,100,206,213,221,231,234,314,325,328,332,335,428,431,441,448,461,467,473,477,484,487,490,494,497,503,510,516,519,523,526,533,540,639,642,645,649,655,662,665],[12,13,14],"p",{},"A raga isn't a scale. It's a set of permitted notes, plus a hierarchy among them, plus characteristic phrases, plus rules about how you ascend and descend. Two ragas can share every note and still be different ragas.",[12,16,17],{},"In my notation app, users label their compositions with a raga. Many don't, or pick the wrong one. So: given the notes, can you tell which raga this is?",[12,19,20],{},"This is a classification problem with about 31 classes in my data, roughly 55 labelled examples, and nine classes with exactly one example. Those numbers are the whole story, and I will come back to them.",[22,23,25],"h2",{"id":24},"six-signals-weighted","Six signals, weighted",[12,27,28],{},"The first version has no learning in it. It scores every candidate raga against the notation on six signals:",[30,31,32,40,46,52,58,64],"ol",{},[33,34,35,39],"li",{},[36,37,38],"strong",{},"Note set"," — frequency-weighted precision, set recall, and a penalty for notes the raga forbids",[33,41,42,45],{},[36,43,44],{},"Vadi and samvadi"," — does the raga's dominant and sub-dominant note actually dominate, by rank and by frequency mass",[33,47,48,51],{},[36,49,50],{},"Variant usage"," — komal and tivra versus shuddh, frequency-weighted, because that is often the only difference between two ragas",[33,53,54,57],{},[36,55,56],{},"Pakad matching"," — bigrams and trigrams from the raga's characteristic phrase, IDF-weighted so a phrase shared by many ragas counts for less",[33,59,60,63],{},[36,61,62],{},"Phrase boundaries"," — do the notes landing on beat boundaries match vadi and samvadi",[33,65,66,69],{},[36,67,68],{},"Movement"," — ascending bigrams checked against aroh, descending against avroh",[12,71,72,73,77],{},"All the blend factors sit in one exported ",[74,75,76],"code",{},"DEFAULT_TUNING"," object rather than scattered through the scoring, so calibrating is editing one place.",[12,79,80],{},[36,81,82],{},"Leave-one-out top-1: 67.3%. Top-3: 94.5%. Top-5: 98.2%.",[12,84,85],{},"The gap between top-1 and top-3 is the interesting part. The heuristic almost always knows the answer is in a small set; it is the final discrimination between near-identical ragas that it gets wrong.",[22,87,89],{"id":88},"the-rerank","The rerank",[12,91,92],{},"If the heuristic narrows to three and fumbles the last step, the obvious help is examples. Other compositions in this raga exist. Do they look like this one?",[12,94,95,96,99],{},"Each composition gets a ",[36,97,98],{},"fingerprint",": three blocks — note unigrams, bigrams and trigrams — each normalised to sum to 1.",[101,102,107],"pre",{"className":103,"code":104,"language":105,"meta":106,"style":106},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","addBlock('u', Object.entries(data.freq));\naddBlock('b', data.bigrams);\naddBlock('t', data.trigrams);\n","ts","",[74,108,109,155,181],{"__ignoreMap":106},[110,111,114,118,122,126,130,132,135,138,141,144,147,149,152],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"s2Zo4","addBlock",[110,119,121],{"class":120},"sTEyZ","(",[110,123,125],{"class":124},"sMK4o","'",[110,127,129],{"class":128},"sfazB","u",[110,131,125],{"class":124},[110,133,134],{"class":124},",",[110,136,137],{"class":120}," Object",[110,139,140],{"class":124},".",[110,142,143],{"class":116},"entries",[110,145,146],{"class":120},"(data",[110,148,140],{"class":124},[110,150,151],{"class":120},"freq))",[110,153,154],{"class":124},";\n",[110,156,158,160,162,164,167,169,171,174,176,179],{"class":112,"line":157},2,[110,159,117],{"class":116},[110,161,121],{"class":120},[110,163,125],{"class":124},[110,165,166],{"class":128},"b",[110,168,125],{"class":124},[110,170,134],{"class":124},[110,172,173],{"class":120}," data",[110,175,140],{"class":124},[110,177,178],{"class":120},"bigrams)",[110,180,154],{"class":124},[110,182,184,186,188,190,193,195,197,199,201,204],{"class":112,"line":183},3,[110,185,117],{"class":116},[110,187,121],{"class":120},[110,189,125],{"class":124},[110,191,192],{"class":128},"t",[110,194,125],{"class":124},[110,196,134],{"class":124},[110,198,173],{"class":120},[110,200,140],{"class":124},[110,202,203],{"class":120},"trigrams)",[110,205,154],{"class":124},[12,207,208,209,212],{},"Similarity is the ",[36,210,211],{},"Bhattacharyya coefficient",", Σ√(pᵢqᵢ), computed per block and averaged:",[101,214,219],{"className":215,"code":217,"language":218},[216],"language-text","\u002F**\n * Per block so the dozen unigrams don't drown the phrase blocks;\n * square-rooted so a few dominant n-grams don't drown the rest.\n *\u002F\n","text",[74,220,217],{"__ignoreMap":106},[12,222,223,224,227,228,230],{},"Both halves of that comment are load-bearing. Averaging per block stops twelve unigrams outvoting hundreds of trigrams. Square-rooting the distributions stops the handful of very frequent notes — every composition is full of ",[74,225,226],{},"s"," and ",[74,229,12],{}," — from dominating a measure that is supposed to be about phrasing.",[12,232,233],{},"Then the corpus vote is blended with the heuristic score, with three calibrated knobs:",[101,235,237],{"className":103,"code":236,"language":105,"meta":106,"style":106},"export const DEFAULT_CORPUS_RERANK = {\n  alpha: 0.12,    \u002F\u002F weight of the corpus vote against the heuristic\n  tau: 0.05,      \u002F\u002F softmax temperature; lower → closer to a 1-NN vote\n  nullSim: 0.75,  \u002F\u002F similarity of an implicit \"no match\" class\n};\n",[74,238,239,258,277,292,308],{"__ignoreMap":106},[110,240,241,245,249,252,255],{"class":112,"line":113},[110,242,244],{"class":243},"s7zQu","export",[110,246,248],{"class":247},"spNyl"," const",[110,250,251],{"class":120}," DEFAULT_CORPUS_RERANK ",[110,253,254],{"class":124},"=",[110,256,257],{"class":124}," {\n",[110,259,260,264,267,271,273],{"class":112,"line":157},[110,261,263],{"class":262},"swJcz","  alpha",[110,265,266],{"class":124},":",[110,268,270],{"class":269},"sbssI"," 0.12",[110,272,134],{"class":124},[110,274,276],{"class":275},"sHwdD","    \u002F\u002F weight of the corpus vote against the heuristic\n",[110,278,279,282,284,287,289],{"class":112,"line":183},[110,280,281],{"class":262},"  tau",[110,283,266],{"class":124},[110,285,286],{"class":269}," 0.05",[110,288,134],{"class":124},[110,290,291],{"class":275},"      \u002F\u002F softmax temperature; lower → closer to a 1-NN vote\n",[110,293,295,298,300,303,305],{"class":112,"line":294},4,[110,296,297],{"class":262},"  nullSim",[110,299,266],{"class":124},[110,301,302],{"class":269}," 0.75",[110,304,134],{"class":124},[110,306,307],{"class":275},"  \u002F\u002F similarity of an implicit \"no match\" class\n",[110,309,311],{"class":112,"line":310},5,[110,312,313],{"class":124},"};\n",[12,315,316,319,320,324],{},[74,317,318],{},"nullSim"," is the one I would point at. With a small corpus, most ragas have no labelled example at all, and without a null class every composition gets dragged toward whichever few ragas the corpus happens to hold. Giving \"none of these\" a fixed similarity — set at roughly the typical best ",[321,322,323],"em",{},"wrong"," raga similarity — means a raga only wins the vote if its nearest labelled composition is genuinely closer than chance.",[12,326,327],{},"A small corpus that knows when to abstain beats a small corpus that always votes.",[22,329,331],{"id":330},"the-numbers-all-of-them","The numbers, all of them",[12,333,334],{},"Leave-one-out over 55 compositions, 31 ragas, 9 of them with a single example:",[336,337,338,360],"table",{},[339,340,341],"thead",{},[342,343,344,348,351,354,357],"tr",{},[345,346,347],"th",{},"Configuration",[345,349,350],{},"top-1",[345,352,353],{},"top-3",[345,355,356],{},"top-5",[345,358,359],{},"raga present in corpus",[361,362,363,381,398,415],"tbody",{},[342,364,365,369,372,375,378],{},[366,367,368],"td",{},"Heuristic only",[366,370,371],{},"67.3%",[366,373,374],{},"94.5%",[366,376,377],{},"98.2%",[366,379,380],{},"—",[342,382,383,386,391,393,395],{},[366,384,385],{},"+ library, public only, cap 10",[366,387,388],{},[36,389,390],{},"72.7%",[366,392,374],{},[366,394,377],{},[366,396,397],{},"52.7%",[342,399,400,403,408,410,413],{},[366,401,402],{},"+ library, all labelled, cap 10",[366,404,405],{},[36,406,407],{},"83.6%",[366,409,374],{},[366,411,412],{},"100%",[366,414,407],{},[342,416,417,420,422,424,426],{},[366,418,419],{},"+ library, all labelled, no cap",[366,421,407],{},[366,423,374],{},[366,425,412],{},[366,427,407],{},[12,429,430],{},"Four things worth reading off that table, including the ones I would rather not write.",[12,432,433,436,437,440],{},[36,434,435],{},"The last column bounds what reranking can add, not what the system can score."," Reranking can't find a raga the corpus has never seen — but the reranker blends its score with the heuristic's rather than replacing it, so coverage caps the rerank's ",[321,438,439],{},"contribution",", not the final number. The shippable row is the proof: 52.7% coverage, 72.7% top-1. The heuristic alone scores 67.3% with no corpus at all, and that floor doesn't go away when the corpus is thin.",[12,442,443,444,447],{},"Worth resisting a reading I nearly published. With every labelled composition available, coverage is 83.6% and top-1 is 83.6%, which looks like the rerank converting all of its opportunities. It isn't — those are two equal numbers, not a conversion rate. 83.6% coverage is the 46 non-singleton compositions and 83.6% top-1 is 46 correct out of 55, but nothing says they are the ",[321,445,446],{},"same"," 46. For that reading to hold, every covered composition would have to be right and all 9 singletons wrong, which against a 37\u002F55 heuristic baseline would mean reranking broke singletons the heuristic already had. Equal percentages are a coincidence worth checking, not a finding.",[12,449,450,453,454,456,457,460],{},[36,451,452],{},"But I can only ship the public-only version."," Reranking against every labelled composition means shipping fingerprints built from private ones, and I won't do that (below). The corpus I can actually ship contains the right raga for 52.7% of compositions, and delivers ",[36,455,390],{},". The 83.6% is real, measured, and unshippable — an upper bound on what a larger ",[321,458,459],{},"public"," corpus would buy, not a number I get to quote.",[12,462,463,466],{},[36,464,465],{},"Capping does nothing here."," Cap 10, cap 5, no cap are identical, because almost no raga has more than a handful of examples. That knob is waiting for a dataset that doesn't exist yet.",[12,468,469,472],{},[36,470,471],{},"Top-3 never moves."," 94.5% across every configuration. The rerank reorders within the top few; it doesn't rescue the compositions the heuristic never shortlisted. Those are a different failure and need a different fix.",[22,474,476],{"id":475},"the-sample-is-small-and-i-should-say-so","The sample is small and I should say so",[12,478,479,480,483],{},"55 compositions. 31 ragas. ",[36,481,482],{},"Nine ragas with exactly one example",", which under leave-one-out means those nine are unsolvable by the corpus path by construction — remove the one example and the raga has no representation at all.",[12,485,486],{},"83.6% of 55 is 46 compositions. The 95% Wilson interval on 46\u002F55 runs from roughly 71% to 91%. So \"84%\" is really \"somewhere in the seventies to low nineties, probably\", and the honest way to write it is with the interval attached.",[12,488,489],{},"Any of these numbers moves by almost two percentage points if a single composition flips. Treat them as directional.",[22,491,493],{"id":492},"why-the-corpus-is-built-only-from-published-compositions","Why the corpus is built only from published compositions",[12,495,496],{},"There's a privacy constraint here that took me a while to see clearly, and it changed the design.",[12,498,499,502],{},[36,500,501],{},"A fingerprint can be inverted back into the melody."," It's a normalised distribution over note unigrams, bigrams and trigrams. Given the trigram distribution of a composition, reconstructing a recognisable melody isn't hard — n-gram models are generative, that is what they are for. A fingerprint is not an anonymised summary. It is a lossy but musically informative encoding of someone's unpublished work.",[12,504,505,506,509],{},"So the shipped corpus is built ",[36,507,508],{},"only from published compositions"," — material the author has already made public. The private ones improve the eval and never ship.",[12,511,512,513,140],{},"It costs real accuracy: 83.6% down to 72.7%, eleven points. I would rather pay eleven points than ship a file that leaks subscribers' unpublished melodies to anyone who runs ",[74,514,515],{},"npm view",[12,517,518],{},"The corpus is also loaded lazily, so the data only reaches users who actually click Identify.",[22,520,522],{"id":521},"the-eval-is-the-artifact","The eval is the artifact",[12,524,525],{},"The harness matters more than any single figure.",[12,527,528,529,532],{},"It runs leave-one-out with the ",[36,530,531],{},"production functions"," and the same library builder the generator script uses — not a reimplementation. An eval that quietly diverges from the shipped code measures something nobody ships.",[12,534,535,536,539],{},"It excludes the composition being scored, and also any ",[36,537,538],{},"identical twin"," saved under a different id:",[101,541,543],{"className":103,"code":542,"language":105,"meta":106,"style":106},"const corpus = lib.filter(\n  (e) => e.id !== p.r.id && fingerprintSimilarity(p.fp, e.fingerprint) \u003C 0.9999\n);\n",[74,544,545,566,633],{"__ignoreMap":106},[110,546,547,550,553,555,558,560,563],{"class":112,"line":113},[110,548,549],{"class":247},"const",[110,551,552],{"class":120}," corpus ",[110,554,254],{"class":124},[110,556,557],{"class":120}," lib",[110,559,140],{"class":124},[110,561,562],{"class":116},"filter",[110,564,565],{"class":120},"(\n",[110,567,568,571,575,578,581,584,586,589,592,595,597,600,602,604,607,610,613,615,618,620,622,624,627,630],{"class":112,"line":157},[110,569,570],{"class":124},"  (",[110,572,574],{"class":573},"sHdIc","e",[110,576,577],{"class":124},")",[110,579,580],{"class":247}," =>",[110,582,583],{"class":120}," e",[110,585,140],{"class":124},[110,587,588],{"class":120},"id ",[110,590,591],{"class":124},"!==",[110,593,594],{"class":120}," p",[110,596,140],{"class":124},[110,598,599],{"class":120},"r",[110,601,140],{"class":124},[110,603,588],{"class":120},[110,605,606],{"class":124},"&&",[110,608,609],{"class":116}," fingerprintSimilarity",[110,611,612],{"class":120},"(p",[110,614,140],{"class":124},[110,616,617],{"class":120},"fp",[110,619,134],{"class":124},[110,621,583],{"class":120},[110,623,140],{"class":124},[110,625,626],{"class":120},"fingerprint) ",[110,628,629],{"class":124},"\u003C",[110,631,632],{"class":269}," 0.9999\n",[110,634,635,637],{"class":112,"line":183},[110,636,577],{"class":120},[110,638,154],{"class":124},[12,640,641],{},"Without that filter, a composition duplicated under two ids scores itself against its own copy and the accuracy is a lie. I found that because a number looked too good.",[12,643,644],{},"It prints every configuration on every run, so the comparison is always in front of you rather than reconstructed from memory of a good run.",[22,646,648],{"id":647},"a-note-on-a-number-i-have-been-quoting","A note on a number I have been quoting",[12,650,651,652,654],{},"My own CV says this work lifted top-1 accuracy \"from 59.5% to 72.7%\". Re-running the eval today, the heuristic-only baseline is ",[36,653,371],{},", not 59.5%.",[12,656,657,658,661],{},"Both were true when measured. The heuristic improved after the rerank landed, and the dataset has grown since. But the honest version today is ",[36,659,660],{},"67.3% → 72.7% shipped, 83.6% with everything labelled"," — a smaller delta for the rerank than the one I have been quoting, because the baseline it is measured against got better.",[12,663,664],{},"That is the argument for running the eval before you cite it, rather than citing the run you remember.",[666,667,668],"style",{},"html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":106,"searchDepth":157,"depth":157,"links":670},[671,672,673,674,675,676,677],{"id":24,"depth":157,"text":25},{"id":88,"depth":157,"text":89},{"id":330,"depth":157,"text":331},{"id":475,"depth":157,"text":476},{"id":492,"depth":157,"text":493},{"id":521,"depth":157,"text":522},{"id":647,"depth":157,"text":648},"2026-09-08","A weighted heuristic gets the raga right 67.3% of the time. Reranking against a corpus of published compositions takes it to 72.7%, which is the version I can actually ship — and on a 55-composition eval that number carries an interval wide enough to be worth writing down. Here is the method and the number, including the unflattering parts.","md","\u002Fimg\u002Fraga-identification.jpg",{},7,true,"\u002Fblog\u002Fidentifying-the-raga-from-the-notes",{"title":5,"description":679},{"loc":685},"blog\u002Fidentifying-the-raga-from-the-notes",[690,691,692],"Machine Learning","Evaluation","TypeScript","pRX-MrwFfRDJQiak1KZS5OaNgnceDZfec8Uh-LM9XJk",[695,700],{"title":696,"path":697,"stem":698,"description":699,"children":-1},"How I Built a Personal Knowledge Wiki with Claude Code","\u002Fblog\u002Fhow-i-built-a-personal-knowledge-wiki-with-claude-code","blog\u002Fhow-i-built-a-personal-knowledge-wiki-with-claude-code","How I turned 14,000+ scattered files — YouTube transcripts, 13 years of RSS feeds, Google Takeout data, Notion exports — into a 50-page cross-referenced knowledge wiki using Claude Code and flat markdown files.",{"title":701,"path":702,"stem":703,"description":704,"children":-1},"Ionic Snippets – Sublime Plugin","\u002Fblog\u002Fionic-snippets-sublime-plugin-301","blog\u002Fionic-snippets-sublime-plugin-301","In this article we will take a look at Ionic Snippets – Sublime Plugin",1790075468914]