Changes to be committed:
modified: .gitignore deleted: Figure_1.png deleted: checkpoints/forward/best_tcn_model.pt deleted: checkpoints/forward/training_history.csv deleted: checkpoints_mlp/task1_feature_mlp/best_feature_mlp.pt deleted: checkpoints_mlp/task1_feature_mlp/training_history.csv deleted: checkpoints_rms/forward_rms/best_rms_model.pt deleted: checkpoints_rms/forward_rms/training_history.csv deleted: checkpoints_tree/task1_tr_tree/best_tr_tree.pkl deleted: checkpoints_tree/task1_tr_tree/model_selection.csv deleted: evaluation_outputs/forward/evaluation_forward_test_b0_s0.png deleted: evaluation_outputs/forward/evaluation_forward_val_b0_s0.png deleted: evaluation_outputs/forward/evaluation_forward_val_b3_s0.png deleted: evaluation_outputs/forward_rms/evaluation_test_all_samples.csv deleted: evaluation_outputs/forward_rms/evaluation_test_s0.png deleted: evaluation_outputs/forward_rms/evaluation_train_all_samples.csv deleted: evaluation_outputs/forward_rms/evaluation_val_all_samples.csv deleted: evaluation_outputs/forward_rms/evaluation_val_s0.png deleted: evaluation_outputs/forward_rms/evaluation_val_s0_waveform.png deleted: evaluation_outputs/task1_feature_mlp/evaluation_train_all_samples.csv deleted: evaluation_outputs/task1_feature_mlp/evaluation_train_curve.png deleted: evaluation_outputs/task1_feature_mlp/evaluation_val_all_samples.csv deleted: evaluation_outputs/task1_feature_mlp/evaluation_val_curve.png deleted: evaluation_outputs/task1_feature_mlp/evaluation_val_s0.png deleted: evaluation_outputs/task1_feature_mlp/harmonic_5mm_0.75Hz_prediction.png deleted: evaluation_outputs/task1_feature_mlp/harmonic_5mm_1.55Hz_prediction.png deleted: evaluation_outputs/task1_tr_tree/evaluation_train_all_samples.csv deleted: evaluation_outputs/task1_tr_tree/evaluation_train_curve.png deleted: evaluation_outputs/task1_tr_tree/evaluation_val_all_samples.csv deleted: evaluation_outputs/task1_tr_tree/evaluation_val_curve.png deleted: evaluation_outputs/task1_tr_tree/harmonic_5mm_1.55Hz_prediction.png deleted: sanity_check_alignment_forward.png new file: scripts/README.md modified: scripts/__pycache__/config.cpython-310.pyc modified: scripts/__pycache__/dataset.cpython-310.pyc deleted: scripts/__pycache__/model.cpython-310.pyc modified: scripts/config.py modified: scripts/dataset.py modified: scripts/evaluate.py deleted: scripts/model.py modified: scripts/predict_single.py deleted: scripts/train.py new file: scripts/train_final.py deleted: scripts_tree/__pycache__/config.cpython-310.pyc deleted: scripts_tree/config.py deleted: scripts_tree/evaluate.py deleted: scripts_tree/predict_single.py deleted: scripts_tree/train.py deleted: src/__pycache__/config.cpython-310.pyc deleted: src/__pycache__/config.cpython-314.pyc deleted: src/__pycache__/dataset.cpython-310.pyc deleted: src/__pycache__/dataset.cpython-314.pyc deleted: src/__pycache__/model.cpython-310.pyc deleted: src/__pycache__/model.cpython-314.pyc deleted: src/config.py deleted: src/dataset.py deleted: src/evaluate.py deleted: src/model.py deleted: src/sanity_check.py deleted: src/train.py deleted: src_new/__pycache__/config.cpython-310.pyc deleted: src_new/__pycache__/dataset.cpython-310.pyc deleted: src_new/__pycache__/evaluate.cpython-310.pyc deleted: src_new/__pycache__/model.cpython-310.pyc deleted: src_new/__pycache__/train.cpython-310.pyc deleted: src_new/config.py deleted: src_new/dataset.py deleted: src_new/evaluate.py deleted: src_new/model.py deleted: src_new/train.py deleted: src_old/__init__.py deleted: src_old/__pycache__/config.cpython-310.pyc deleted: src_old/__pycache__/config.cpython-314.pyc deleted: src_old/__pycache__/dataset.cpython-310.pyc deleted: src_old/__pycache__/dataset.cpython-314.pyc deleted: src_old/__pycache__/evaluate.cpython-314.pyc deleted: src_old/__pycache__/model.cpython-310.pyc deleted: src_old/__pycache__/model.cpython-314.pyc deleted: src_old/__pycache__/train.cpython-310.pyc deleted: src_old/__pycache__/train.cpython-314.pyc deleted: src_old/config.py deleted: src_old/dataset.py deleted: src_old/evaluate.py deleted: src_old/model.py deleted: src_old/train.py
4
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
.venv/
|
|
||||||
.venv310/
|
.venv310/
|
||||||
.vscode/
|
.vscode/
|
||||||
src/__pycache__/
|
scripts/__pycache__/
|
||||||
|
old/
|
||||||
|
|||||||
BIN
Figure_1.png
|
Before Width: | Height: | Size: 167 KiB |
@@ -1,11 +0,0 @@
|
|||||||
epoch,lr,train_loss,train_time_loss,train_weighted_time_loss,train_fft_loss,train_rms_loss,train_scale_loss,train_underestimate_loss,train_envelope_loss,train_rms_error,train_dominant_freq_error,val_loss,val_time_loss,val_weighted_time_loss,val_fft_loss,val_rms_loss,val_scale_loss,val_underestimate_loss,val_envelope_loss,val_rms_error,val_dominant_freq_error
|
|
||||||
1,0.001,25.599940884788083,1.7562742283884085,0.1756274257347269,2774.0617503040244,0.48780856886,0.4243854204157613,0.1559595560549565,0.8805500887474924,0.48780856886,0.5304876364710495,11.634879671294113,0.5565754904829222,0.05565755005026686,1030.2479747903758,0.19599251089424924,0.30123303316790484,0.07480601036262795,0.6266409343686598,0.19599251089424924,1.0759024783184585
|
|
||||||
2,0.001,18.8950198911271,1.807937761522689,0.18079377879511635,1975.3159283332104,0.3459213998801303,0.35067820830165214,0.1092575388907824,0.758364588865694,0.3459213998801303,0.4821035959354592,10.542049144876414,0.5095152341086289,0.05095152441283752,783.4861005585769,0.16758740950247336,0.2732687505154774,0.10411370940634916,0.6904992646184461,0.16758740950247336,0.9969777073497166
|
|
||||||
3,0.001,16.69826183678969,1.7823544706938401,0.17823545016207784,1672.4529568654186,0.30989001663226,0.3281119924108937,0.10324830568905147,0.7008864249823228,0.30989001663226,0.5562505104268722,11.536806665617844,0.5393576467859333,0.05393576609163449,807.3707762093379,0.21286767027501402,0.33425827504232014,0.10436723167718999,0.6995668801768072,0.21286767027501402,1.0535930765001758
|
|
||||||
4,0.001,14.293646308611024,1.7581631035174963,0.1758163128540201,1394.1521462494472,0.25787185048157313,0.2936001386282579,0.08524404234200153,0.6856944434485346,0.25787185048157313,0.4707189468993712,11.0088351677204,0.512058621850507,0.05120586244196727,716.754319289635,0.18768149871250678,0.30934940792363264,0.12457355485972145,0.7022169437901727,0.18768149871250678,0.9801404230908486
|
|
||||||
5,0.001,13.64289491581467,1.7736638503254585,0.1773663880127781,1282.9595152656987,0.2586157521549261,0.2966467724093851,0.0801986690856657,0.662726102291413,0.2586157521549261,0.4816929557024733,12.69674718791041,0.5321119869577473,0.0532112003400408,923.869591548525,0.2018765033832912,0.3501281591838804,0.1300795058286267,0.763439338782738,0.2018765033832912,0.8944807380729035
|
|
||||||
6,0.001,11.776648678869572,1.7715154886245728,0.17715155192703572,1060.149999654518,0.21744662122625225,0.2669158032480276,0.07291070573067046,0.6286382666736279,0.21744662122625225,0.49275163173347086,13.566723692006079,0.5599699513665561,0.055996995740409554,1081.8859031940328,0.23347977780062576,0.399347482827203,0.09571884981966738,0.753588840879243,0.23347977780062576,0.802507071705832
|
|
||||||
7,0.001,10.598831523139522,1.8021629632643934,0.180216298631902,959.8165948256006,0.1918528508746399,0.24521326632151064,0.06225078732197015,0.5634953882896675,0.1918528508746399,0.43498506576279067,12.335502788938324,0.5373578256574171,0.053735782938270735,897.6921434073613,0.22004679693230267,0.3851976631016567,0.09088767872288309,0.7485740184783936,0.22004679693230267,0.7111648032890617
|
|
||||||
8,0.0005,10.096835316352124,1.771101622086651,0.17711016507643573,904.1345197569649,0.18218446649470418,0.2265465495721349,0.06017633231427028,0.5831915904890816,0.18218446649470418,0.4688892364850107,12.481576097422632,0.55741053922423,0.05574105519416003,993.8351429906385,0.20113462980451255,0.37394588877414836,0.08785425563310754,0.7251736295634302,0.20113462980451255,0.7193729794563382
|
|
||||||
9,0.0005,9.301918011791301,1.8092092241881028,0.18092092534281173,872.4488234609928,0.15496070823579464,0.2050890047454609,0.05094481654078612,0.5316191723324218,0.15496070823579464,0.45952377956299667,12.33887189010094,0.529184412339638,0.052918442107480146,888.4320715542498,0.2059096264941939,0.35693827361382285,0.11748063429820768,0.7306599390917811,0.2059096264941939,0.7187415812949122
|
|
||||||
10,0.0005,9.509747361237148,1.775009681593697,0.17750097143481364,875.1525979671839,0.15211023376235422,0.21900151374767413,0.054768192924488826,0.5556492130711393,0.15211023376235422,0.48492007759302524,11.80303616359316,0.5109482181483301,0.05109482271404102,766.1427895118451,0.20308723608995305,0.33445684663180647,0.13568100256138835,0.7249893361124499,0.20308723608995305,0.7126380656820888
|
|
||||||
|
@@ -1,59 +0,0 @@
|
|||||||
epoch,lr,train_loss,train_relative_loss,train_log_rms_huber_loss,train_mae_loss,train_rms_error,val_loss,val_relative_loss,val_log_rms_huber_loss,val_mae_loss,val_rms_error
|
|
||||||
1,0.001,1.0684084097544353,0.6919674078623453,0.19923226369751823,1.5134448210398357,0.6919673813713921,0.3821044862270355,0.2511320114135742,0.033154696226119995,0.7073763012886047,0.2511320412158966
|
|
||||||
2,0.001,1.0431654651959736,0.6733311547173394,0.19394073883692423,1.49585849708981,0.673331167962816,0.3861435353755951,0.2520703375339508,0.0340907983481884,0.7233673334121704,0.2520703673362732
|
|
||||||
3,0.001,1.0058492289649115,0.6443076067500644,0.18731229503949484,1.473716417948405,0.6443075670136346,0.39013171195983887,0.252702534198761,0.03538244962692261,0.7392822504043579,0.2527025640010834
|
|
||||||
4,0.001,0.9792734649446275,0.6221125589476691,0.18307040548986858,1.4657206005520291,0.6221125887499915,0.394980788230896,0.253364622592926,0.037405483424663544,0.7570802569389343,0.253364622592926
|
|
||||||
5,0.001,0.939600666364034,0.5912809504403008,0.17598583300908408,1.4422023958630033,0.5912809636857774,0.3971262574195862,0.2521932125091553,0.03932402655482292,0.7696000933647156,0.2521932125091553
|
|
||||||
6,0.001,0.9127781920962863,0.5688717034127977,0.17321109771728516,1.4266544183095295,0.5688716835445828,0.3954552412033081,0.24867530167102814,0.040780965238809586,0.7746281623840332,0.24867533147335052
|
|
||||||
7,0.001,0.8162693844901191,0.4975863430235121,0.15291899773809645,1.3599585427178278,0.4975863430235121,0.39774268865585327,0.24631300568580627,0.044477906078100204,0.7871417999267578,0.24631300568580627
|
|
||||||
8,0.001,0.8098740312788222,0.48419485489527386,0.16087035338083902,1.3668427334891424,0.4841948416497972,0.40212470293045044,0.24344877898693085,0.05097588896751404,0.8029600381851196,0.24344877898693085
|
|
||||||
9,0.001,0.7296120656861199,0.4106071988741557,0.16139008270369637,1.3197486003239949,0.4106071756945716,0.43612974882125854,0.2614344656467438,0.060854148119688034,0.8603642582893372,0.2614344358444214
|
|
||||||
10,0.001,0.7408013741175333,0.38955933849016827,0.2076671322186788,1.3032778369055853,0.3895593351787991,0.4869456887245178,0.29191675782203674,0.07229459285736084,0.9387197494506836,0.29191672801971436
|
|
||||||
11,0.001,0.7496903538703918,0.3897787862353855,0.21981331043773228,1.3003438181347318,0.3897787994808621,0.5376651287078857,0.32053330540657043,0.0865083634853363,1.0150035619735718,0.32053327560424805
|
|
||||||
12,0.001,0.9797695477803549,0.38233999411265057,0.5365811387697855,1.29995772573683,0.3823399974240197,0.5587358474731445,0.3330889940261841,0.09165894240140915,1.0460175275802612,0.3330889642238617
|
|
||||||
13,0.001,0.7886577645937601,0.39104337162441677,0.27514297929075027,1.2750475406646729,0.39104337493578595,0.5636787414550781,0.3352062702178955,0.09356075525283813,1.055345892906189,0.3352062702178955
|
|
||||||
14,0.001,0.6367178559303284,0.34643163283665973,0.1475350492530399,1.1975661383734808,0.3464316460821364,0.5635904669761658,0.3346971571445465,0.09383875876665115,1.0567615032196045,0.3346971571445465
|
|
||||||
15,0.001,0.6623357137044271,0.36682088838683236,0.15060860332515505,1.2170556386311848,0.3668208916982015,0.5532020926475525,0.32986027002334595,0.08957388252019882,1.0410760641098022,0.32986021041870117
|
|
||||||
16,0.001,0.6297085682551066,0.36428216265307534,0.12578680531846154,1.140575302971734,0.36428215106328327,0.5544819235801697,0.33266526460647583,0.08754842728376389,1.041035532951355,0.33266523480415344
|
|
||||||
17,0.001,0.6100040011935763,0.36249567733870613,0.10765769663784239,1.111767013867696,0.36249566078186035,0.5716693997383118,0.34491467475891113,0.08935325592756271,1.0649319887161255,0.34491464495658875
|
|
||||||
18,0.001,0.5903598533736335,0.3496483862400055,0.10458873874611324,1.0817992157406278,0.34964838955137467,0.601777195930481,0.3637298047542572,0.0955144539475441,1.109410285949707,0.3637298047542572
|
|
||||||
19,0.001,0.5701847804917229,0.3414529164632161,0.09985838168197209,1.0255872938368056,0.3414529164632161,0.629698634147644,0.3823082447052002,0.10031794756650925,1.1476796865463257,0.3823082447052002
|
|
||||||
20,0.001,0.5357944567998251,0.3132438593440586,0.09814917544523875,0.9929247697194418,0.3132438593440586,0.6656612157821655,0.4028518795967102,0.11059614270925522,1.199081540107727,0.4028518795967102
|
|
||||||
21,0.001,0.5140700340270996,0.29829433891508317,0.1006597230831782,0.9352060423956977,0.2982943256696065,0.6950218081474304,0.4190041720867157,0.11983328312635422,1.2409510612487793,0.4190041720867157
|
|
||||||
22,0.001,0.5517757270071242,0.31282461020681596,0.12161111583312352,0.9849517875247531,0.31282461020681596,0.6951501965522766,0.42067644000053406,0.11815791577100754,1.2390354871749878,0.4206763803958893
|
|
||||||
23,0.001,0.5385147333145142,0.3098441825972663,0.12465377317534553,0.9012014071146647,0.3098441743188434,0.6760208606719971,0.41229119896888733,0.10967614501714706,1.209816813468933,0.41229113936424255
|
|
||||||
24,0.001,0.4948686576551861,0.2839176009098689,0.10664872804449664,0.873096740908093,0.283917604221238,0.6479407548904419,0.3981650471687317,0.0997471809387207,1.1664352416992188,0.3981650173664093
|
|
||||||
25,0.001,0.4815748400158352,0.2894285586145189,0.08536433428525925,0.8541535006629096,0.2894285437133577,0.628013014793396,0.3875465989112854,0.09387131035327911,1.133752703666687,0.3875465989112854
|
|
||||||
26,0.001,0.4902527266078525,0.3052508632342021,0.08093913561768001,0.8286499811543359,0.3052508615785175,0.6259267330169678,0.3861921727657318,0.09331636875867844,1.1316486597061157,0.3861921429634094
|
|
||||||
27,0.001,0.4992575960026847,0.3134735193517473,0.0804886631667614,0.8361171748903062,0.31347350279490155,0.6494007110595703,0.399603933095932,0.09916889667510986,1.1694673299789429,0.3996039032936096
|
|
||||||
28,0.001,0.45642075273725724,0.2935712155368593,0.07241135980519983,0.7236066361268362,0.29357120229138267,0.6795501708984375,0.4163399338722229,0.10745861381292343,1.2174421548843384,0.4163399338722229
|
|
||||||
29,0.0005,0.4504433075586955,0.27966498997476363,0.08081474165535635,0.7344483600722419,0.2796649800406562,0.712536096572876,0.4332484304904938,0.11834639310836792,1.2701857089996338,0.4332483410835266
|
|
||||||
30,0.0005,0.4445747633775075,0.2770162257883284,0.0779732180138429,0.7271908124287924,0.27701622247695923,0.7285711765289307,0.44202420115470886,0.12342425435781479,1.293191909790039,0.44202420115470886
|
|
||||||
31,0.0005,0.47021543317370945,0.29266008569134605,0.08219371032383707,0.7727337082227071,0.29266009893682265,0.7398308515548706,0.4480714797973633,0.12714092433452606,1.3093578815460205,0.4480714499950409
|
|
||||||
32,0.0005,0.4485766556527879,0.27516163720024955,0.07896825671195984,0.7612588869200813,0.27516160408655804,0.7477874755859375,0.45214587450027466,0.1301499605178833,1.3201942443847656,0.45214587450027466
|
|
||||||
33,0.0005,0.4806170066197713,0.29431821240319145,0.0897826933198505,0.7930783894326952,0.29431822564866805,0.7456773519515991,0.45039525628089905,0.13018397986888885,1.3176275491714478,0.4503951966762543
|
|
||||||
34,0.0005,0.4639296531677246,0.27904501888487077,0.09117302215761608,0.7766991588804457,0.2790450221962399,0.7344075441360474,0.4435312747955322,0.12712228298187256,1.3035639524459839,0.44353124499320984
|
|
||||||
35,0.0005,0.4217291673024495,0.25814421640502083,0.07712550130155352,0.704938703113132,0.2581442031595442,0.728592038154602,0.43990880250930786,0.12561193108558655,1.2964953184127808,0.4399087429046631
|
|
||||||
36,0.0005,0.4452095528443654,0.2780698604053921,0.07377014433344205,0.7454138000806173,0.2780698537826538,0.719640851020813,0.4346870481967926,0.12287390232086182,1.285322666168213,0.4346870481967926
|
|
||||||
37,0.0005,0.4346568849351671,0.27202920781241524,0.0730870481994417,0.7187491787804497,0.27202921443515354,0.7090620994567871,0.42957648634910583,0.11880777031183243,1.2691987752914429,0.42957648634910583
|
|
||||||
38,0.0005,0.45190709829330444,0.2777327348788579,0.07873265279663934,0.7674991223547194,0.2777327398459117,0.7021456360816956,0.42580172419548035,0.11658556759357452,1.2593647241592407,0.42580166459083557
|
|
||||||
39,0.0005,0.41501253181033665,0.25652576155132717,0.06958397726217906,0.7086585627661811,0.2565257747968038,0.6967095136642456,0.4226696193218231,0.11500495672225952,1.2519077062606812,0.42266955971717834
|
|
||||||
40,0.0005,0.426217923561732,0.2659038090043598,0.06590471830632952,0.739237109820048,0.26590381893846726,0.6904255747795105,0.4193277060985565,0.11303865909576416,1.2421258687973022,0.41932764649391174
|
|
||||||
41,0.0005,0.474410249127282,0.302842206425137,0.07200946576065487,0.7837395668029785,0.302842206425137,0.6886562705039978,0.41840869188308716,0.11255472153425217,1.2388770580291748,0.41840869188308716
|
|
||||||
42,0.0005,0.4341147674454583,0.2751454992426766,0.06902315095067024,0.7146794034375085,0.2751455108324687,0.6964045763015747,0.42251697182655334,0.11516380310058594,1.2500985860824585,0.42251691222190857
|
|
||||||
43,0.0005,0.40912215577231514,0.2595863143603007,0.058075524038738675,0.7065279748704698,0.2595863276057773,0.709182620048523,0.430009663105011,0.11880651861429214,1.2671202421188354,0.430009663105011
|
|
||||||
44,0.0005,0.44166630175378585,0.27680986291832393,0.07062854783402549,0.7459001806047227,0.2768098645740085,0.7103798389434814,0.4306167662143707,0.11920434236526489,1.2690653800964355,0.4306167662143707
|
|
||||||
45,0.0005,0.4364900390307109,0.2806501239538193,0.06813105609681872,0.6982774602042304,0.28065012726518845,0.713029146194458,0.43142566084861755,0.12053372710943222,1.274687647819519,0.43142572045326233
|
|
||||||
46,0.0005,0.42031361990504795,0.2599939935737186,0.0675199499560727,0.7311977677875094,0.2599939935737186,0.7204116582870483,0.43440350890159607,0.12379532307386398,1.287744164466858,0.43440350890159607
|
|
||||||
47,0.0005,0.41649588611390853,0.2588261928823259,0.06496965015927951,0.7262829906410642,0.2588262077834871,0.7308988571166992,0.43982744216918945,0.12745337188243866,1.3032091856002808,0.43982744216918945
|
|
||||||
48,0.0005,0.45140007469389176,0.2842165165477329,0.07611107577880223,0.734001550409529,0.2842165331045787,0.738196074962616,0.4431730806827545,0.13040359318256378,1.314801573753357,0.44317302107810974
|
|
||||||
49,0.0005,0.4124910682439804,0.25314582718743217,0.068223740077681,0.7211828695403205,0.25314583049880135,0.7486779689788818,0.4491503834724426,0.1336752474308014,1.3284742832183838,0.4491503834724426
|
|
||||||
50,0.00025,0.41983669333987766,0.2572576337390476,0.07067853129572338,0.7304676373799642,0.25725764367315507,0.7499342560768127,0.4504518508911133,0.1333393007516861,1.32985258102417,0.4504518508911133
|
|
||||||
51,0.00025,0.39338985085487366,0.24541797240575156,0.061128986792431936,0.6808343132336935,0.2454179906182819,0.7484983205795288,0.4500373899936676,0.1325150579214096,1.3271640539169312,0.4500373899936676
|
|
||||||
52,0.00025,0.4347236222691006,0.26843487554126316,0.07120031118392944,0.7525900999704996,0.2684348738855786,0.7462888956069946,0.44955143332481384,0.1310974657535553,1.3227624893188477,0.44955143332481384
|
|
||||||
53,0.00025,0.39689934915966457,0.24233278632164001,0.06171891983184549,0.721849156750573,0.24233278466595543,0.7399852871894836,0.44651278853416443,0.1285889446735382,1.3135385513305664,0.44651278853416443
|
|
||||||
54,0.00025,0.37512414654095966,0.23281626568900216,0.05609904395209418,0.6682239903344048,0.23281625906626383,0.7348796129226685,0.44354644417762756,0.12702621519565582,1.307090163230896,0.44354644417762756
|
|
||||||
55,0.00025,0.42326178153355914,0.271195156706704,0.06771800035817756,0.6751875513129764,0.2711951591902309,0.731861412525177,0.4418794810771942,0.1260313242673874,1.3030563592910767,0.4418794810771942
|
|
||||||
56,0.00025,0.40027793248494464,0.2503356287876765,0.0625538213385476,0.6868461138672299,0.25033563044336105,0.7314411997795105,0.44189128279685974,0.12566323578357697,1.3020164966583252,0.44189128279685974
|
|
||||||
57,0.00025,0.41923924618297154,0.2631028691927592,0.06492394229604138,0.7162894474135505,0.2631028923723433,0.7334963083267212,0.4432640075683594,0.12621350586414337,1.30381441116333,0.443263977766037
|
|
||||||
58,0.00025,0.4435661964946323,0.2827550404601627,0.06622585654258728,0.7409449021021525,0.2827550503942702,0.7361425757408142,0.44472166895866394,0.12714305520057678,1.3070908784866333,0.44472166895866394
|
|
||||||
|
@@ -1,30 +0,0 @@
|
|||||||
epoch,lr,train_loss,train_relative_loss,train_log_loss,train_mae_loss,train_waveform_l1,train_waveform_huber,train_rms_error,val_loss,val_relative_loss,val_log_loss,val_mae_loss,val_waveform_l1,val_waveform_huber,val_rms_error
|
|
||||||
1,0.001,0.3693518406814999,0.24889015323585933,0.05584108498361376,0.25861359967125785,0.5032549103101095,0.25580188632011414,0.24889015323585933,0.4390127956867218,0.30896639823913574,0.056898053735494614,0.3188031315803528,0.44043534994125366,0.17367032170295715,0.30896639823913574
|
|
||||||
2,0.001,0.3945225642787086,0.2697031597296397,0.06360895559191704,0.27172964480188155,0.4614827699131436,0.224760792321629,0.2697031597296397,0.36929574608802795,0.2602313160896301,0.0452926866710186,0.2584935128688812,0.4390406906604767,0.17246951162815094,0.2602313160896301
|
|
||||||
3,0.001,0.5084060496754117,0.3563646740383572,0.09457878602875604,0.32678870028919643,0.4492427276240455,0.19155073165893555,0.3563646740383572,0.30823153257369995,0.2178763449192047,0.03594436123967171,0.20229224860668182,0.4395522177219391,0.1724669635295868,0.2178763449192047
|
|
||||||
4,0.001,0.46196970012452865,0.2874237828784519,0.10073509646786584,0.3864523735311296,0.4910557005140517,0.2566720247268677,0.2874237828784519,0.2588622272014618,0.1819354146718979,0.02867879904806614,0.16302458941936493,0.44016343355178833,0.1725275069475174,0.1819354146718979
|
|
||||||
5,0.001,0.4498247702916463,0.2731182641453213,0.0863925533162223,0.3938671946525574,0.6069219443533156,0.33671536213821834,0.2731182641453213,0.3132787346839905,0.22292804718017578,0.02889658883213997,0.2158222645521164,0.44235706329345703,0.17352235317230225,0.22292804718017578
|
|
||||||
6,0.001,0.42692894405788845,0.28966741760571796,0.05804820607105891,0.3025594221221076,0.5698766128884422,0.3100252277735207,0.28966741760571796,0.4311088025569916,0.3014686703681946,0.060949672013521194,0.30897605419158936,0.4416200518608093,0.17345383763313293,0.3014686703681946
|
|
||||||
7,0.001,0.36648913555675083,0.2673240436447991,0.04492858507566982,0.2210414773888058,0.40499014324612087,0.1858146521780226,0.2673240436447991,0.3253621459007263,0.2316536009311676,0.031499434262514114,0.22455067932605743,0.43930211663246155,0.17284171283245087,0.2316536009311676
|
|
||||||
8,0.001,0.3705151147312588,0.2639704677793715,0.056356401907073125,0.23181239929464129,0.40243885583347744,0.1668036257227262,0.2639704677793715,0.2693358361721039,0.19200395047664642,0.023184901103377342,0.17564474046230316,0.43946224451065063,0.17288772761821747,0.19200395047664642
|
|
||||||
9,0.001,0.3809369206428528,0.238957146803538,0.05438671095503701,0.32686107357343036,0.5658377408981323,0.32192010349697536,0.238957146803538,0.22007109224796295,0.15654480457305908,0.01698182336986065,0.1330064833164215,0.43889322876930237,0.17233915627002716,0.15654480457305908
|
|
||||||
10,0.001,0.3033063875304328,0.20820601118935478,0.03028729951216115,0.2125823481215371,0.4863186809751723,0.244431518846088,0.20820601118935478,0.2353043407201767,0.16689710319042206,0.020381871610879898,0.14587727189064026,0.43858274817466736,0.17178986966609955,0.16689710319042206
|
|
||||||
11,0.001,0.3418840931521522,0.21859833929273817,0.041131472835938133,0.3211692141162025,0.4186662236849467,0.1973548432191213,0.21859833929273817,0.2819516360759735,0.19871805608272552,0.029886296018958092,0.1862599402666092,0.43857088685035706,0.17136642336845398,0.19871805608272552
|
|
||||||
12,0.001,0.327799528837204,0.2257505324151781,0.038094287945164576,0.24139907293849522,0.4269571900367737,0.19686741133530936,0.2257505324151781,0.2265533208847046,0.15818463265895844,0.02364269271492958,0.13944414258003235,0.43839970231056213,0.17068645358085632,0.15818463265895844
|
|
||||||
13,0.001,0.5482124156422086,0.2889212518930435,0.05876548960804939,0.7335240874025557,0.7617671754625108,0.4734871983528137,0.2889212518930435,0.13387161493301392,0.09191606938838959,0.01390319224447012,0.05341717228293419,0.4380643963813782,0.1701544225215912,0.09191606938838959
|
|
||||||
14,0.001,0.2683628300825755,0.1934465699725681,0.03215170403321584,0.1588393354581462,0.3651085015800264,0.16354632439712682,0.1934465699725681,0.11752602458000183,0.08033295720815659,0.0075136758387088776,0.0473531074821949,0.43740665912628174,0.16951507329940796,0.08033295720815659
|
|
||||||
15,0.001,0.23245521552032894,0.15845761530929142,0.021862993792941172,0.1661414752403895,0.4185062315728929,0.17951083762778175,0.15845761530929142,0.13540877401828766,0.09327995032072067,0.009294009767472744,0.06379763782024384,0.4362388551235199,0.16890482604503632,0.09327995032072067
|
|
||||||
16,0.001,0.2540251049730513,0.16881393061743843,0.02455571148958471,0.1829415543211831,0.4814679291513231,0.2550777710146374,0.16881393061743843,0.24452388286590576,0.17012111842632294,0.027872953563928604,0.15593497455120087,0.43555423617362976,0.16831809282302856,0.17012111842632294
|
|
||||||
17,0.001,0.27790621254179215,0.16291005578305987,0.02768930456497603,0.27147142092386883,0.5733835763401456,0.3216428938839171,0.16291005578305987,0.23126113414764404,0.16358613967895508,0.019583800807595253,0.14588342607021332,0.4346035122871399,0.16748279333114624,0.16358613967895508
|
|
||||||
18,0.001,0.21714572608470917,0.16336159076955584,0.02207756083872583,0.11655174030197991,0.29744883709483677,0.09367910772562027,0.16336159076955584,0.21335509419441223,0.15434198081493378,0.013381856493651867,0.12396419048309326,0.43387162685394287,0.1662997454404831,0.15434198081493378
|
|
||||||
19,0.001,0.39585938718583846,0.23502571880817413,0.03807514740361108,0.43226980169614154,0.5700339277585348,0.33255258699258167,0.23502571880817413,0.29088473320007324,0.1979036182165146,0.050344835966825485,0.18606995046138763,0.4335789680480957,0.16567720472812653,0.1979036182165146
|
|
||||||
20,0.001,0.26503710283173454,0.18777255879508126,0.03789552880658044,0.15700330336888632,0.36711519294314915,0.16105001833703783,0.18777255879508126,0.3211463689804077,0.22426843643188477,0.04961967095732689,0.20329146087169647,0.4336654543876648,0.16470572352409363,0.22426843643188477
|
|
||||||
21,0.001,0.28275859852631885,0.18599259356657663,0.043205282702628106,0.20048580318689346,0.4617711802323659,0.22377558714813656,0.18599259356657663,0.26557254791259766,0.19436083734035492,0.022131552919745445,0.15585075318813324,0.43292519450187683,0.1639096587896347,0.19436083734035492
|
|
||||||
22,0.001,0.21910542911953396,0.14179434875647226,0.016369602125551965,0.16523817347155678,0.5044849514961243,0.2536436948511336,0.14179434875647226,0.2662939727306366,0.19275937974452972,0.025155413895845413,0.15944524109363556,0.4315638244152069,0.16297321021556854,0.19275937974452972
|
|
||||||
23,0.0005,0.3775199121899075,0.2587335771984524,0.04730055698504051,0.2791730182038413,0.45998597972922856,0.23086444040139517,0.2587335771984524,0.24067305028438568,0.1702818125486374,0.027465825900435448,0.1423315852880478,0.43073099851608276,0.16307000815868378,0.1702818125486374
|
|
||||||
24,0.0005,0.12755481650431952,0.09668536235888799,0.006956045328277267,0.05454847796095742,0.30247317420111763,0.09360233859883414,0.09668536235888799,0.21739451587200165,0.15576092898845673,0.02215108834207058,0.1178358793258667,0.4309665262699127,0.16340160369873047,0.15576092898845673
|
|
||||||
25,0.0005,0.2033023022943073,0.13513341546058655,0.014085318272312483,0.12979511668284735,0.5040803915924497,0.2711007396380107,0.13513341546058655,0.22780413925647736,0.16796176135540009,0.01980750262737274,0.11519914120435715,0.43173280358314514,0.1637372523546219,0.16796176135540009
|
|
||||||
26,0.0005,0.26767876081996494,0.19527330001195273,0.029956953910489876,0.14093264937400818,0.4134024911456638,0.19583487345112693,0.19527330001195273,0.22256356477737427,0.16064496338367462,0.0197360347956419,0.12349464744329453,0.43188977241516113,0.16440483927726746,0.16064496338367462
|
|
||||||
27,0.0005,0.29266027278370327,0.21245999303128985,0.037366345110866755,0.15680206815401712,0.4156636761294471,0.19693358656432894,0.21245999303128985,0.23951780796051025,0.1713121384382248,0.026933521032333374,0.13419118523597717,0.4317554533481598,0.16476942598819733,0.1713121384382248
|
|
||||||
28,0.0005,0.2840205712450875,0.1834611776802275,0.03268412335051431,0.22491087267796198,0.502896871831682,0.25805431852738064,0.1834611776802275,0.20573638379573822,0.1483912318944931,0.018318505957722664,0.10800518840551376,0.43159055709838867,0.16473759710788727,0.1483912318944931
|
|
||||||
29,0.0005,0.18021956914001042,0.1047939153181182,0.012183074632452594,0.14656837284564972,0.5704893204900954,0.31154681907759774,0.1047939153181182,0.17089061439037323,0.12431001663208008,0.01182954479008913,0.07795087993144989,0.43144071102142334,0.16469746828079224,0.12431001663208008
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
model_name,train_mean_rms_error,train_median_rms_error,train_max_rms_error,val_mean_rms_error,val_median_rms_error,val_max_rms_error
|
|
||||||
random_forest,0.23011051198123578,0.1289880713751052,1.3614534820442807,0.2941758399359114,0.1350525270089895,0.8030963210135198
|
|
||||||
extra_trees,3.6456671909608616e-08,2.9450879227826572e-08,1.1542847206743363e-07,0.30134282357310554,0.1285832699374185,0.989878917287992
|
|
||||||
gradient_boosting,0.012023494394761644,0.005998110300540536,0.11812203696846045,0.3579803351621235,0.3397381373027084,0.9497835717961135
|
|
||||||
|
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 323 KiB |
@@ -1,3 +0,0 @@
|
|||||||
file_name,true_rms,pred_rms,relative_error_percent
|
|
||||||
Kobe_seismic_wave.csv,0.9873504638671875,0.8297591805458069,15.961029052734375
|
|
||||||
Northridge_seismic_wave.csv,0.36303532123565674,0.6096307635307312,67.92601776123047
|
|
||||||
|
|
Before Width: | Height: | Size: 172 KiB |
@@ -1,37 +0,0 @@
|
|||||||
file_name,frequency_hz,true_rms,pred_rms,relative_error_percent
|
|
||||||
harmonic_5mm_1.65Hz.csv,1.649999976158142,0.9560214281082153,0.958398163318634,0.24860689734976008
|
|
||||||
harmonic_5mm_1.7Hz.csv,1.7000000476837158,0.7670571804046631,0.7446213364601135,2.9249245711660596
|
|
||||||
harmonic_5mm_0.7Hz.csv,0.699999988079071,0.3087632358074188,0.2989788353443146,3.1689007395967814
|
|
||||||
harmonic_5mm_1.75Hz.csv,1.75,1.1417795419692993,1.1781681776046753,3.187010652915903
|
|
||||||
harmonic_5mm_1Hz.csv,1.0,0.4110860228538513,0.4256628453731537,3.545929977892899
|
|
||||||
harmonic_5mm_0.6Hz.csv,0.6000000238418579,0.22619637846946716,0.2362937331199646,4.463977150660003
|
|
||||||
harmonic_5mm_2.5Hz.csv,2.5,1.527108907699585,1.4517914056777954,4.932032132223416
|
|
||||||
harmonic_5mm_1.1Hz.csv,1.100000023841858,0.4332965314388275,0.4580433964729309,5.711300054013273
|
|
||||||
harmonic_5mm_0.8Hz.csv,0.800000011920929,0.3433072865009308,0.31963616609573364,6.895024176870468
|
|
||||||
harmonic_5mm_0.9Hz.csv,0.8999999761581421,0.39361461997032166,0.3609310984611511,8.303431796216017
|
|
||||||
harmonic_5mm_2Hz.csv,2.0,0.8881404399871826,0.8095211982727051,8.852118220808878
|
|
||||||
harmonic_5mm_0.5Hz.csv,0.5,0.185550257563591,0.20308159291744232,9.448294809207178
|
|
||||||
harmonic_5mm_2.2Hz.csv,2.200000047683716,1.4068351984024048,1.25899076461792,10.50900872770146
|
|
||||||
harmonic_5mm_1.45Hz.csv,1.4500000476837158,0.8364413976669312,0.9315659403800964,11.372529262479622
|
|
||||||
harmonic_5mm_2.25Hz.csv,2.25,2.169835090637207,1.8927007913589478,12.77213648512221
|
|
||||||
harmonic_5mm_1.95Hz.csv,1.9500000476837158,1.9055689573287964,1.6509041786193848,13.364238419710492
|
|
||||||
harmonic_5mm_1.3Hz.csv,1.2999999523162842,0.8282244205474854,0.7093077301979065,14.358027534490079
|
|
||||||
harmonic_5mm_2.4Hz.csv,2.4000000953674316,4.0633931159973145,3.3863630294799805,16.66169300361096
|
|
||||||
harmonic_5mm_1.8Hz.csv,1.7999999523162842,1.193358063697815,0.9930893182754517,16.781949317189667
|
|
||||||
harmonic_5mm_1.4Hz.csv,1.399999976158142,0.5552759170532227,0.6490811109542847,16.893438202555952
|
|
||||||
harmonic_5mm_1.15Hz.csv,1.149999976158142,0.8088750243186951,0.6577692627906799,18.680977528671942
|
|
||||||
harmonic_5mm_0.85Hz.csv,0.8500000238418579,0.590942919254303,0.47555646300315857,19.5258209366055
|
|
||||||
harmonic_5mm_1.6Hz.csv,1.600000023841858,1.2506930828094482,0.928046464920044,25.797425629366955
|
|
||||||
harmonic_5mm_1.35Hz.csv,1.350000023841858,1.2584363222122192,0.9336408972740173,25.809444562696697
|
|
||||||
harmonic_5mm_1.5Hz.csv,1.5,1.0597823858261108,0.7772730588912964,26.657295942373644
|
|
||||||
harmonic_5mm_1.9Hz.csv,1.899999976158142,0.816762387752533,1.040550708770752,27.399440078773996
|
|
||||||
harmonic_5mm_1.2Hz.csv,1.2000000476837158,0.8279687762260437,0.5912047028541565,28.595773194622048
|
|
||||||
harmonic_5mm_2.15Hz.csv,2.1500000953674316,2.85538649559021,1.9993276596069336,29.98048906182589
|
|
||||||
harmonic_5mm_1.05Hz.csv,1.0499999523162842,0.39136433601379395,0.5116328001022339,30.73056306392747
|
|
||||||
harmonic_5mm_2.3Hz.csv,2.299999952316284,3.4340105056762695,2.2803103923797607,33.59628956840676
|
|
||||||
harmonic_5mm_0.55Hz.csv,0.550000011920929,0.23804350197315216,0.33142292499542236,39.22788156292628
|
|
||||||
harmonic_5mm_0.65Hz.csv,0.6499999761581421,0.24615468084812164,0.35735413432121277,45.174624788752894
|
|
||||||
harmonic_5mm_2.45Hz.csv,2.450000047683716,4.256887435913086,2.1653497219085693,49.13302842728069
|
|
||||||
harmonic_5mm_2.1Hz.csv,2.0999999046325684,1.085241436958313,1.8744914531707764,72.72575385847348
|
|
||||||
harmonic_5mm_2.05Hz.csv,2.049999952316284,0.7474663257598877,1.5259369611740112,104.14792059330784
|
|
||||||
harmonic_5mm_2.35Hz.csv,2.3499999046325684,1.0703518390655518,2.9562220573425293,176.19161750807072
|
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
file_name,frequency_hz,true_rms,pred_rms,relative_error_percent
|
|
||||||
harmonic_5mm_1.85Hz.csv,1.850000023841858,1.2355948686599731,1.2751460075378418,3.2009795347210073
|
|
||||||
harmonic_5mm_1.55Hz.csv,1.5499999523162842,1.4854224920272827,1.5365926027297974,3.4448186275056623
|
|
||||||
harmonic_5mm_0.95Hz.csv,0.949999988079071,0.5067455172538757,0.52765291929245,4.125818843326791
|
|
||||||
harmonic_5mm_1.25Hz.csv,1.25,0.49293941259384155,0.5253125429153442,6.56736497314257
|
|
||||||
harmonic_5mm_0.75Hz.csv,0.75,0.40764957666397095,0.31443294882774353,22.866852603913454
|
|
||||||
|
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 280 KiB |
@@ -1,37 +0,0 @@
|
|||||||
file_name,frequency_hz,true_rms,pred_rms,relative_error_percent,dominant_frequency_hz,frequency_squared,inverse_frequency_hz,log_frequency_hz,input_rms,input_peak_abs,input_peak_to_peak,crest_factor,middle_length_ratio,dominant_amplitude,dominant_energy_ratio,harmonic_fit_amplitude,harmonic_fit_residual_ratio,spectral_peak_prominence,half_power_bandwidth_hz,spectral_centroid_hz,signal_mean,sampling_rate
|
|
||||||
harmonic_5mm_0.9Hz.csv,0.8999999761581421,0.3999128043651581,0.42324697971343994,5.8348157632321165,0.8994534015655518,0.8090164065361023,1.1117863655090332,-0.10596802830696106,0.1796521544456482,0.5928999781608582,1.0781500339508057,3.3002665042877197,0.2499224841594696,0.2150648981332779,0.9518226981163025,0.21449175477027893,0.5359740853309631,58.302303314208984,0.06203479692339897,0.9868842959403992,-0.00024062092415988445,50.000047683761295
|
|
||||||
harmonic_5mm_0.7Hz.csv,0.699999988079071,0.2675361931324005,0.24590837955474854,8.08407016801223,0.6991758346557617,0.4888468384742737,1.43025541305542,-0.3578530251979828,0.11656410992145538,0.33765000104904175,0.6533100008964539,2.8966891765594482,0.25,0.1183394342660904,0.8892508745193481,0.11721993237733841,0.7034800052642822,37.61368942260742,0.06349212676286697,0.9012212753295898,-0.00016539028729312122,50.000047683761295
|
|
||||||
harmonic_5mm_1.2Hz.csv,1.2000000476837158,0.7224284410476685,0.6560892462730408,9.182804967980266,1.1994376182556152,1.4386504888534546,0.8337240815162659,0.1818527728319168,0.3020855486392975,0.8440300226211548,1.5216000080108643,2.7940099239349365,0.24991999566555023,0.3333832025527954,0.9823232889175415,0.38784340023994446,0.4198954999446869,124.32585906982422,0.0960308238863945,1.2258714437484741,0.0011010364396497607,50.000047683761295
|
|
||||||
harmonic_5mm_0.6Hz.csv,0.6000000238418579,0.16537058353424072,0.18173347413539886,9.894680330356415,0.5964841246604919,0.3557933568954468,1.6764904260635376,-0.5167025923728943,0.09397729486227036,0.3058300018310547,0.5446599721908569,3.2542967796325684,0.2499224841594696,0.08657485246658325,0.8864659667015076,0.09075998514890671,0.7302541732788086,35.94514465332031,0.0930522009730339,0.8173193335533142,0.001446693786419928,50.000047683761295
|
|
||||||
harmonic_5mm_0.5Hz.csv,0.5,0.18624502420425415,0.16313482820987701,12.408490424438014,0.5023733973503113,0.25237902998924255,1.990551233291626,-0.6884115934371948,0.06567567586898804,0.22301000356674194,0.4394000172615051,3.395625352859497,0.25,0.024631651118397713,0.31016308069229126,0.026469262316823006,0.9584437608718872,16.04189109802246,0.050632961094379425,2.159400463104248,0.0017537976382300258,50.000047683761295
|
|
||||||
harmonic_5mm_0.8Hz.csv,0.800000011920929,0.3204104006290436,0.25158339738845825,21.480889230019116,0.7963964343070984,0.6342473030090332,1.2556560039520264,-0.22765816748142242,0.09064009040594101,0.43685001134872437,0.7310900092124939,4.819611549377441,0.2499212622642517,0.0584954135119915,0.7228869199752808,0.06087201461195946,0.8802145719528198,26.92901039123535,0.09451805055141449,1.1917314529418945,0.002280592219904065,50.000047683761295
|
|
||||||
harmonic_5mm_1.5Hz.csv,1.5,1.2808952331542969,0.9783489108085632,23.619911645755096,1.5013059377670288,2.2539196014404297,0.666086733341217,0.40633538365364075,0.45307496190071106,1.1585400104522705,2.150049924850464,2.5570602416992188,0.25,0.5677548050880432,0.9871437549591064,0.5882158875465393,0.3955877423286438,194.53369140625,0.08695660531520844,1.5181952714920044,0.0008904285496100783,50.000047683761295
|
|
||||||
harmonic_5mm_0.65Hz.csv,0.6499999761581421,0.2034619301557541,0.2532237470149994,24.45755666485207,0.6577304005622864,0.43260928988456726,1.5203797817230225,-0.4189601540565491,0.1112922951579094,0.35517001152038574,0.6907100081443787,3.191326141357422,0.25,0.06641923636198044,0.5974180698394775,0.06589993834495544,0.9084223508834839,14.282920837402344,0.09230778366327286,1.486210823059082,-0.00035510817542672157,50.000047683761295
|
|
||||||
harmonic_5mm_0.55Hz.csv,0.550000011920929,0.1971234530210495,0.24593766033649445,24.763267164477078,0.5490954518318176,0.30150580406188965,1.8211770057678223,-0.5994830131530762,0.109112448990345,0.40608999133110046,0.751579999923706,3.72175669670105,0.25,0.06793846935033798,0.5611140727996826,0.07492320239543915,0.874541163444519,13.549410820007324,0.08955232053995132,1.263006329536438,-0.0011556772515177727,50.000047683761295
|
|
||||||
harmonic_5mm_2.1Hz.csv,2.0999999046325684,0.9339020252227783,1.1686650514602661,25.137864561487174,2.10196590423584,4.4182610511779785,0.4757450819015503,0.7428730726242065,0.6165724992752075,1.728060007095337,3.2244300842285156,2.802687406539917,0.25,0.7569394707679749,0.9819692969322205,0.7959626913070679,0.40733572840690613,133.85641479492188,0.11111121624708176,2.100722312927246,-0.005007810425013304,50.000047683761295
|
|
||||||
harmonic_5mm_1.1Hz.csv,1.100000023841858,0.5534171462059021,0.410175621509552,25.883102046689434,1.0999548435211182,1.2099006175994873,0.9091282486915588,0.0952691063284874,0.1373506784439087,0.587689995765686,1.1190800666809082,4.2787556648254395,0.24994152784347534,0.1195499375462532,0.8312234282493591,0.11857906728982925,0.7920843362808228,37.334434509277344,0.04679461568593979,1.324162483215332,-7.055706373648718e-05,50.000047683761295
|
|
||||||
harmonic_5mm_2.2Hz.csv,2.200000047683716,1.0718663930892944,1.3539294004440308,26.315127442496312,2.201188325881958,4.845230579376221,0.45430004596710205,0.7889974117279053,0.6085917353630066,1.7638200521469116,3.3627500534057617,2.8981993198394775,0.25,0.7086807489395142,0.9761964082717896,0.7861699461936951,0.4083978533744812,121.00875854492188,0.07894744724035263,2.1985433101654053,0.0010015374282374978,50.000047683761295
|
|
||||||
harmonic_5mm_1.8Hz.csv,1.7999999523162842,1.6704485416412354,1.2214888334274292,26.876596137029036,1.8012458086013794,3.2444865703582764,0.5551713109016418,0.5884785652160645,0.5062151551246643,1.4020500183105469,2.7015299797058105,2.769672155380249,0.25,0.6510956883430481,0.9815194606781006,0.6610633730888367,0.3853244483470917,126.02283477783203,0.05555560812354088,1.819625735282898,0.0018060111906379461,50.000047683761295
|
|
||||||
harmonic_5mm_1.45Hz.csv,1.4500000476837158,1.0070611238479614,0.7258758544921875,27.921370679206674,1.451728343963623,2.107515335083008,0.6888340711593628,0.372754842042923,0.2516467273235321,0.9840899705886841,1.9117000102996826,3.9106009006500244,0.25,0.25519290566444397,0.8691993951797485,0.26430585980415344,0.6701168417930603,45.977691650390625,0.09523818641901016,1.7056236267089844,-0.00019088915723841637,50.000047683761295
|
|
||||||
harmonic_5mm_2.35Hz.csv,2.3499999046325684,0.7861254215240479,1.0398896932601929,32.280379795399135,2.351473808288574,5.529428958892822,0.42526522278785706,0.8550422787666321,0.991222620010376,2.2557199001312256,4.422339916229248,2.2756946086883545,0.2499212622642517,1.1917885541915894,0.9816745519638062,1.3294483423233032,0.31509023904800415,127.34346771240234,0.09451805055141449,2.3649213314056396,-0.004312594421207905,50.000047683761295
|
|
||||||
harmonic_5mm_1.75Hz.csv,1.75,1.59738028049469,0.9762305021286011,38.8855293852586,1.7508330345153809,3.065416097640991,0.5711566805839539,0.5600916743278503,0.35962721705436707,1.2678200006484985,2.4300899505615234,3.5253727436065674,0.25,0.39048516750335693,0.9655163288116455,0.4300072193145752,0.5336334109306335,81.74029541015625,0.08955232053995132,1.7760101556777954,-0.000693939218763262,50.000047683761295
|
|
||||||
harmonic_5mm_0.85Hz.csv,0.8500000238418579,0.5794365406036377,0.35035184025764465,39.53577040677142,0.845729649066925,0.7152586579322815,1.1824109554290771,-0.1675555258989334,0.12664538621902466,0.5095099806785583,0.9128699898719788,4.023123264312744,0.24991869926452637,0.0956917256116867,0.6594006419181824,0.09168906509876251,0.8589633107185364,18.147825241088867,0.06506186723709106,1.4032906293869019,-0.0011623100144788623,50.000047683761295
|
|
||||||
harmonic_5mm_1.3Hz.csv,1.2999999523162842,0.9550632834434509,0.5524730682373047,42.15325017569723,1.3018183708190918,1.6947312355041504,0.7681562900543213,0.26376205682754517,0.2030215710401535,0.7108299732208252,1.34552001953125,3.501253366470337,0.24993710219860077,0.20925211906433105,0.9342537522315979,0.22753745317459106,0.6098353862762451,55.399173736572266,0.07549075782299042,1.397734522819519,-0.00013690412743017077,50.000047683761295
|
|
||||||
harmonic_5mm_2.5Hz.csv,2.5,0.6538841724395752,0.9343159198760986,42.887067657603815,2.5015766620635986,6.257885932922363,0.39974790811538696,0.9169211983680725,0.5744112730026245,1.5267499685287476,2.5278899669647217,2.6579387187957764,0.2499280571937561,0.7362810969352722,0.9838377833366394,0.7437620162963867,0.4004853665828705,234.6715087890625,0.05757058039307594,2.530334234237671,-0.01246642041951418,50.000047683761295
|
|
||||||
harmonic_5mm_1.7Hz.csv,1.7000000476837158,0.6067864894866943,0.8731490969657898,43.89725415679946,1.700693964958191,2.892360210418701,0.5879952311515808,0.531036376953125,0.32135581970214844,1.1075899600982666,2.123849868774414,3.446615695953369,0.25,0.3294788897037506,0.9566228985786438,0.3822683095932007,0.5411480069160461,79.01654815673828,0.09523818641901016,1.7275936603546143,0.0009157023159787059,50.000047683761295
|
|
||||||
harmonic_5mm_1.15Hz.csv,1.149999976158142,0.9153674244880676,0.5124025344848633,44.022201273829275,1.1485493183135986,1.319165587425232,0.8706635236740112,0.13849970698356628,0.18818436563014984,0.7081000208854675,1.3734800815582275,3.7627995014190674,0.24991869926452637,0.17172867059707642,0.8639681935310364,0.183344766497612,0.7253570556640625,30.369056701660156,0.0975928083062172,1.386907696723938,-0.0019338843412697315,50.000047683761295
|
|
||||||
harmonic_5mm_1.95Hz.csv,1.9500000476837158,2.063512086868286,1.132091999053955,45.13761241049535,1.9470664262771606,3.7910678386688232,0.5135931372642517,0.6663238406181335,0.4040626883506775,1.629040002822876,2.9249401092529297,4.031651496887207,0.25,0.407725989818573,0.9061827659606934,0.44422510266304016,0.629830002784729,43.91968536376953,0.09677428752183914,2.039226531982422,-0.004885347560048103,50.000047683761295
|
|
||||||
harmonic_5mm_2.25Hz.csv,2.25,2.1223104000091553,1.1386420726776123,46.34893780510615,2.2518274784088135,5.070727348327637,0.4440837502479553,0.8117421269416809,0.45399144291877747,1.468690037727356,2.881589889526367,3.235060930252075,0.24992366135120392,0.4949429929256439,0.961538553237915,0.5433851480484009,0.5339846014976501,91.52095031738281,0.09163112193346024,2.2496755123138428,-0.001197385834529996,50.000047683761295
|
|
||||||
harmonic_5mm_1.6Hz.csv,1.600000023841858,1.1856828927993774,0.6328451633453369,46.62610321962223,1.60205078125,2.5665667057037354,0.6241999268531799,0.47128453850746155,0.22232241928577423,0.8100799918174744,1.6100399494171143,3.643717050552368,0.2499159723520279,0.20403607189655304,0.9129946231842041,0.22558331489562988,0.6967292428016663,54.344844818115234,0.10087434202432632,1.6988458633422852,0.0011404975084587932,50.000047683761295
|
|
||||||
harmonic_5mm_1Hz.csv,1.0,0.3037901520729065,0.4519365727901459,48.76603790687915,0.9989057183265686,0.9978126883506775,1.0010954141616821,-0.0010948515264317393,0.21532277762889862,0.635129988193512,1.2681899070739746,2.94966459274292,0.2499224841594696,0.2571507394313812,0.9758802056312561,0.2638331651687622,0.49583709239959717,152.86651611328125,0.0930522009730339,1.0530376434326172,-0.0009750677854754031,50.000047683761295
|
|
||||||
harmonic_5mm_1.35Hz.csv,1.350000023841858,1.256479024887085,0.6293237805366516,49.913705834189585,1.3494648933410645,1.8210554122924805,0.7410345077514648,0.29970812797546387,0.20795053243637085,0.8708500266075134,1.7264100313186646,4.187775135040283,0.24991999566555023,0.1956998109817505,0.8739701509475708,0.19486407935619354,0.7484625577926636,37.17776870727539,0.0640205442905426,1.5017311573028564,-0.005424206610769033,50.000047683761295
|
|
||||||
harmonic_5mm_2.15Hz.csv,2.1500000953674316,3.0180647373199463,1.3036904335021973,56.80376178213196,2.1465415954589844,4.607641220092773,0.4658656418323517,0.7638580203056335,0.46591538190841675,1.5632699728012085,3.0576400756835938,3.3552658557891846,0.2499212622642517,0.48979803919792175,0.9034159779548645,0.49743175506591797,0.6561670303344727,72.98995971679688,0.063012033700943,2.2847402095794678,0.002827510703355074,50.000047683761295
|
|
||||||
harmonic_5mm_1.65Hz.csv,1.649999976158142,0.5992732048034668,0.9399581551551819,56.8496885261946,1.6503766775131226,2.723743200302124,0.605922281742096,0.5010035634040833,0.38267070055007935,1.171970009803772,2.2650599479675293,3.0626070499420166,0.24991999566555023,0.4040696918964386,0.9350709319114685,0.4694216847419739,0.4976775050163269,43.57965850830078,0.0960308238863945,1.6984570026397705,-0.006530857179313898,50.000047683761295
|
|
||||||
harmonic_5mm_1.4Hz.csv,1.399999976158142,0.3850800693035126,0.6127961277961731,59.134729799058796,1.3995026350021362,1.9586076736450195,0.7145395278930664,0.3361169397830963,0.2212747484445572,0.8126099705696106,1.4906599521636963,3.6724026203155518,0.2499212622642517,0.2121341973543167,0.9263311624526978,0.2458721548318863,0.6176945567131042,50.935482025146484,0.09451805055141449,1.501712679862976,0.0024810773320496082,50.000047683761295
|
|
||||||
harmonic_5mm_2.45Hz.csv,2.450000047683716,4.167365074157715,1.4023194313049316,66.34997399193875,2.4505887031555176,6.00538444519043,0.40806522965431213,0.8963282704353333,0.5156936049461365,1.7411500215530396,3.2864298820495605,3.376326560974121,0.25,0.5547626614570618,0.9294771552085876,0.5594695210456848,0.6415255665779114,73.60872650146484,0.06451619416475296,2.469957113265991,0.0013848240487277508,50.000047683761295
|
|
||||||
harmonic_5mm_2.3Hz.csv,2.299999952316284,3.7199018001556396,1.1657079458236694,68.66293766747023,2.301363945007324,5.296276569366455,0.43452489376068115,0.8335019946098328,0.9912338852882385,1.9464600086212158,3.861459970474243,1.963673710823059,0.24992592632770538,1.217545509338379,0.9911264181137085,1.3459256887435913,0.2792012691497803,227.761474609375,0.08891531825065613,2.3008110523223877,0.008378428407013416,50.000047683761295
|
|
||||||
harmonic_5mm_2Hz.csv,2.0,0.5447156429290771,0.9262616038322449,70.04497958815654,2.0002939701080322,4.001175880432129,0.49992653727531433,0.693294107913971,0.7751342058181763,1.559309959411621,3.019509792327881,2.011664390563965,0.24990099668502808,0.8933139443397522,0.9935725927352905,1.0516541004180908,0.28168511390686035,319.8121337890625,0.11885906755924225,2.0122017860412598,0.008680449798703194,50.000047683761295
|
|
||||||
harmonic_5mm_1.05Hz.csv,1.0499999523162842,0.36426904797554016,0.6340927481651306,74.07263990425794,1.0530656576156616,1.1089472770690918,0.9496083855628967,0.051705583930015564,0.2716720998287201,0.7901600003242493,1.5045499801635742,2.908506393432617,0.24991999566555023,0.3438712954521179,0.9576663374900818,0.3379869759082794,0.4775521755218506,72.97195434570312,0.0640205442905426,1.126065731048584,-0.0008716708398424089,50.000047683761295
|
|
||||||
harmonic_5mm_2.4Hz.csv,2.4000000953674316,4.107017993927002,1.0405583381652832,74.66389629400348,2.4017505645751953,5.768405437469482,0.4163629710674286,0.876197874546051,0.9670910835266113,2.3246400356292725,4.529560089111328,2.403744697570801,0.25,1.1933718919754028,0.9901480078697205,1.3087358474731445,0.2874786853790283,216.48052978515625,0.10344837605953217,2.4013094902038574,-8.548868208890781e-05,50.000047683761295
|
|
||||||
harmonic_5mm_1.9Hz.csv,1.899999976158142,0.5868141055107117,1.1536935567855835,96.60290131940668,1.9012991189956665,3.614938497543335,0.5259561538696289,0.6425374150276184,0.5143030285835266,1.3888100385665894,2.6350998878479004,2.7003729343414307,0.25,0.6525437235832214,0.9834353923797607,0.670233428478241,0.38811808824539185,157.31686401367188,0.06250005960464478,1.9118845462799072,-0.0027283257804811,50.000047683761295
|
|
||||||
harmonic_5mm_2.05Hz.csv,2.049999952316284,0.6199118494987488,1.310292363166809,111.36752979093583,2.0498123168945312,4.201730251312256,0.48784953355789185,0.71774822473526,0.7315996885299683,1.6353000402450562,3.2010200023651123,2.235239028930664,0.24991999566555023,0.9387410283088684,0.9784685373306274,0.9522533416748047,0.3911336362361908,110.50621795654297,0.0640205442905426,2.0580592155456543,-0.004247209522873163,50.000047683761295
|
|
||||||
|
|
Before Width: | Height: | Size: 266 KiB |
@@ -1,6 +0,0 @@
|
|||||||
file_name,frequency_hz,true_rms,pred_rms,relative_error_percent,dominant_frequency_hz,frequency_squared,inverse_frequency_hz,log_frequency_hz,input_rms,input_peak_abs,input_peak_to_peak,crest_factor,middle_length_ratio,dominant_amplitude,dominant_energy_ratio,harmonic_fit_amplitude,harmonic_fit_residual_ratio,spectral_peak_prominence,half_power_bandwidth_hz,spectral_centroid_hz,signal_mean,sampling_rate
|
|
||||||
harmonic_5mm_1.85Hz.csv,1.850000023841858,1.185911774635315,1.2428719997406006,4.803074421181267,1.8481981754302979,3.4158363342285156,0.5410675406455994,0.6142112016677856,0.6145902872085571,1.3770899772644043,2.6619200706481934,2.240663528442383,0.25,0.7274389863014221,0.9514397978782654,0.789516270160675,0.42080339789390564,85.657958984375,0.09677428752183914,1.8637527227401733,-0.002481284085661173,50.000047683761295
|
|
||||||
harmonic_5mm_1.25Hz.csv,1.25,0.3802441656589508,0.4504010081291199,18.450471777414265,1.2483434677124023,1.5583614110946655,0.8010615706443787,0.22181743383407593,0.15971125662326813,0.5707299709320068,1.0413799285888672,3.5735113620758057,0.24992592632770538,0.1439778357744217,0.8661479949951172,0.14893445372581482,0.7513272762298584,56.41539764404297,0.05927687883377075,1.4980274438858032,-0.0021986484061926603,50.000047683761295
|
|
||||||
harmonic_5mm_0.95Hz.csv,0.949999988079071,0.5053804516792297,0.3541945815086365,29.915258824959956,0.9513587951660156,0.9050835967063904,1.0511281490325928,-0.04986399784684181,0.1381060928106308,0.5125100016593933,1.0134000778198242,3.7109878063201904,0.25,0.09424196928739548,0.7232213020324707,0.11362786591053009,0.8133426308631897,20.55999183654785,0.09677428752183914,1.3476505279541016,0.00015772903861943632,50.000047683761295
|
|
||||||
harmonic_5mm_0.75Hz.csv,0.75,0.39182430505752563,0.2595634460449219,33.755144156559126,0.7515671849250793,0.5648532509803772,1.3305530548095703,-0.285594642162323,0.11687792837619781,0.4074699878692627,0.8070399761199951,3.4862868785858154,0.24992701411247253,0.10824807733297348,0.8746803998947144,0.11673416197299957,0.7099294662475586,36.5,0.08761690557003021,0.8937567472457886,-0.001955104758962989,50.000047683761295
|
|
||||||
harmonic_5mm_1.55Hz.csv,1.5499999523162842,1.4177086353302002,0.9243398308753967,34.80043728025205,1.5492842197418213,2.4002816677093506,0.6454593539237976,0.4377930164337158,0.392645925283432,1.105049967765808,2.1589999198913574,2.8143677711486816,0.25,0.4746793210506439,0.9668325185775757,0.5014731884002686,0.43158969283103943,69.77510833740234,0.09836074709892273,1.584490418434143,-0.0011147483019158244,50.000047683761295
|
|
||||||
|
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 263 KiB |
@@ -1,37 +0,0 @@
|
|||||||
file_name,frequency_hz,true_rms,pred_rms,relative_error_percent,x_rms,pred_tr,true_tr,dominant_frequency_hz,frequency_squared,inverse_frequency_hz,log_frequency_hz,input_rms,input_peak_abs,input_peak_to_peak,crest_factor,middle_length_ratio,dominant_amplitude,dominant_energy_ratio,harmonic_fit_amplitude,harmonic_fit_residual_ratio,spectral_peak_prominence,half_power_bandwidth_hz,spectral_centroid_hz,signal_mean,sampling_rate
|
|
||||||
harmonic_5mm_1.95Hz.csv,1.95,2.063512086868286,2.042942868521436,0.996806293394051,0.4040626883506775,5.056004742383957,5.106910705566406,1.9470664262771606,3.7910678386688232,0.5135931372642517,0.6663238406181335,0.4040626883506775,1.629040002822876,2.9249401092529297,4.031651496887207,0.25,0.407725989818573,0.9061827659606934,0.44422510266304016,0.629830002784729,43.91968536376953,0.09677428752183914,2.039226531982422,-0.004885347560048103,50.000047683761295
|
|
||||||
harmonic_5mm_0.7Hz.csv,0.7,0.2675361931324005,0.2647816168893098,1.0296088207129102,0.11656410992145538,2.2715535430908202,2.295184850692749,0.6991758346557617,0.4888468384742737,1.43025541305542,-0.3578530251979828,0.11656410992145538,0.33765000104904175,0.6533100008964539,2.8966891765594482,0.25,0.1183394342660904,0.8892508745193481,0.11721993237733841,0.7034800052642822,37.61368942260742,0.06349212676286697,0.9012212753295898,-0.00016539028729312122,50.000047683761295
|
|
||||||
harmonic_5mm_0.8Hz.csv,0.8,0.3204104006290436,0.3104756411225298,3.1006357743098976,0.09064009040594101,3.4253677344322204,3.5349743366241455,0.7963964343070984,0.6342473030090332,1.2556560039520264,-0.22765816748142242,0.09064009040594101,0.43685001134872437,0.7310900092124939,4.819611549377441,0.2499212622642517,0.0584954135119915,0.7228869199752808,0.06087201461195946,0.8802145719528198,26.92901039123535,0.09451805055141449,1.1917314529418945,0.002280592219904065,50.000047683761295
|
|
||||||
harmonic_5mm_1.45Hz.csv,1.45,1.0070611238479614,1.0508663280991015,4.349805906890859,0.2516467273235321,4.175958651542664,4.0018839836120605,1.451728343963623,2.107515335083008,0.6888340711593628,0.372754842042923,0.2516467273235321,0.9840899705886841,1.9117000102996826,3.9106009006500244,0.25,0.25519290566444397,0.8691993951797485,0.26430585980415344,0.6701168417930603,45.977691650390625,0.09523818641901016,1.7056236267089844,-0.00019088915723841637,50.000047683761295
|
|
||||||
harmonic_5mm_1.1Hz.csv,1.1,0.5534171462059021,0.5779597429583696,4.434737326215095,0.1373506784439087,4.207913273572922,4.0292277336120605,1.0999548435211182,1.2099006175994873,0.9091282486915588,0.0952691063284874,0.1373506784439087,0.587689995765686,1.1190800666809082,4.2787556648254395,0.24994152784347534,0.1195499375462532,0.8312234282493591,0.11857906728982925,0.7920843362808228,37.334434509277344,0.04679461568593979,1.324162483215332,-7.055706373648718e-05,50.000047683761295
|
|
||||||
harmonic_5mm_1.65Hz.csv,1.65,0.5992732048034668,0.6282565440955618,4.836415020691626,0.38267070055007935,1.6417680872678757,1.566028356552124,1.6503766775131226,2.723743200302124,0.605922281742096,0.5010035634040833,0.38267070055007935,1.171970009803772,2.2650599479675293,3.0626070499420166,0.24991999566555023,0.4040696918964386,0.9350709319114685,0.4694216847419739,0.4976775050163269,43.57965850830078,0.0960308238863945,1.6984570026397705,-0.006530857179313898,50.000047683761295
|
|
||||||
harmonic_5mm_0.5Hz.csv,0.5,0.18624502420425415,0.19870795051296228,6.6916828312363865,0.06567567586898804,3.025594299316406,2.835829496383667,0.5023733973503113,0.25237902998924255,1.990551233291626,-0.6884115934371948,0.06567567586898804,0.22301000356674194,0.4394000172615051,3.395625352859497,0.25,0.024631651118397713,0.31016308069229126,0.026469262316823006,0.9584437608718872,16.04189109802246,0.050632961094379425,2.159400463104248,0.0017537976382300258,50.000047683761295
|
|
||||||
harmonic_5mm_0.85Hz.csv,0.85,0.5794365406036377,0.5367616244860894,7.364899022952703,0.12664538621902466,4.238303822278977,4.575267791748047,0.845729649066925,0.7152586579322815,1.1824109554290771,-0.1675555258989334,0.12664538621902466,0.5095099806785583,0.9128699898719788,4.023123264312744,0.24991869926452637,0.0956917256116867,0.6594006419181824,0.09168906509876251,0.8589633107185364,18.147825241088867,0.06506186723709106,1.4032906293869019,-0.0011623100144788623,50.000047683761295
|
|
||||||
harmonic_5mm_1.15Hz.csv,1.15,0.9153674244880676,0.8472856717514132,7.437642078505262,0.18818436563014984,4.502423295974731,4.864205360412598,1.1485493183135986,1.319165587425232,0.8706635236740112,0.13849970698356628,0.18818436563014984,0.7081000208854675,1.3734800815582275,3.7627995014190674,0.24991869926452637,0.17172867059707642,0.8639681935310364,0.183344766497612,0.7253570556640625,30.369056701660156,0.0975928083062172,1.386907696723938,-0.0019338843412697315,50.000047683761295
|
|
||||||
harmonic_5mm_2.1Hz.csv,2.1,0.9339020252227783,1.003448067990676,7.446824280235163,0.6165724992752075,1.6274616029262543,1.5146669149398804,2.10196590423584,4.4182610511779785,0.4757450819015503,0.7428730726242065,0.6165724992752075,1.728060007095337,3.2244300842285156,2.802687406539917,0.25,0.7569394707679749,0.9819692969322205,0.7959626913070679,0.40733572840690613,133.85641479492188,0.11111121624708176,2.100722312927246,-0.005007810425013304,50.000047683761295
|
|
||||||
harmonic_5mm_0.65Hz.csv,0.65,0.2034619301557541,0.22107336420719614,8.655886650618205,0.1112922951579094,1.986421107530594,1.8281761407852173,0.6577304005622864,0.43260928988456726,1.5203797817230225,-0.4189601540565491,0.1112922951579094,0.35517001152038574,0.6907100081443787,3.191326141357422,0.25,0.06641923636198044,0.5974180698394775,0.06589993834495544,0.9084223508834839,14.282920837402344,0.09230778366327286,1.486210823059082,-0.00035510817542672157,50.000047683761295
|
|
||||||
harmonic_5mm_1Hz.csv,1.0,0.3037901520729065,0.33260700774091306,9.485776767737626,0.21532277762889862,1.544690308213234,1.4108593463897705,0.9989057183265686,0.9978126883506775,1.0010954141616821,-0.0010948515264317393,0.21532277762889862,0.635129988193512,1.2681899070739746,2.94966459274292,0.2499224841594696,0.2571507394313812,0.9758802056312561,0.2638331651687622,0.49583709239959717,152.86651611328125,0.0930522009730339,1.0530376434326172,-0.0009750677854754031,50.000047683761295
|
|
||||||
harmonic_5mm_1.2Hz.csv,1.2,0.7224284410476685,0.6515492682358728,9.811237872778989,0.3020855486392975,2.156836933016777,2.391469717025757,1.1994376182556152,1.4386504888534546,0.8337240815162659,0.1818527728319168,0.3020855486392975,0.8440300226211548,1.5216000080108643,2.7940099239349365,0.24991999566555023,0.3333832025527954,0.9823232889175415,0.38784340023994446,0.4198954999446869,124.32585906982422,0.0960308238863945,1.2258714437484741,0.0011010364396497607,50.000047683761295
|
|
||||||
harmonic_5mm_1.35Hz.csv,1.35,1.256479024887085,1.1329108880206473,9.83447669391395,0.20795053243637085,5.447982627153396,6.042201519012451,1.3494648933410645,1.8210554122924805,0.7410345077514648,0.29970812797546387,0.20795053243637085,0.8708500266075134,1.7264100313186646,4.187775135040283,0.24991999566555023,0.1956998109817505,0.8739701509475708,0.19486407935619354,0.7484625577926636,37.17776870727539,0.0640205442905426,1.5017311573028564,-0.005424206610769033,50.000047683761295
|
|
||||||
harmonic_5mm_2.3Hz.csv,2.3,3.7199018001556396,3.330646787548538,10.464120654766079,0.9912338852882385,3.360101825594902,3.7527992725372314,2.301363945007324,5.296276569366455,0.43452489376068115,0.8335019946098328,0.9912338852882385,1.9464600086212158,3.861459970474243,1.963673710823059,0.24992592632770538,1.217545509338379,0.9911264181137085,1.3459256887435913,0.2792012691497803,227.761474609375,0.08891531825065613,2.3008110523223877,0.008378428407013416,50.000047683761295
|
|
||||||
harmonic_5mm_0.9Hz.csv,0.9,0.3999128043651581,0.4468199892673058,11.729353096510772,0.1796521544456482,2.4871396095752716,2.2260396480560303,0.8994534015655518,0.8090164065361023,1.1117863655090332,-0.10596802830696106,0.1796521544456482,0.5928999781608582,1.0781500339508057,3.3002665042877197,0.2499224841594696,0.2150648981332779,0.9518226981163025,0.21449175477027893,0.5359740853309631,58.302303314208984,0.06203479692339897,0.9868842959403992,-0.00024062092415988445,50.000047683761295
|
|
||||||
harmonic_5mm_1.75Hz.csv,1.75,1.59738028049469,1.4050853159227565,12.038145638831969,0.35962721705436707,3.907060559630394,4.441766738891602,1.7508330345153809,3.065416097640991,0.5711566805839539,0.5600916743278503,0.35962721705436707,1.2678200006484985,2.4300899505615234,3.5253727436065674,0.25,0.39048516750335693,0.9655163288116455,0.4300072193145752,0.5336334109306335,81.74029541015625,0.08955232053995132,1.7760101556777954,-0.000693939218763262,50.000047683761295
|
|
||||||
harmonic_5mm_2.15Hz.csv,2.15,3.0180647373199463,2.6414293752430624,12.479366576190065,0.46591538190841675,5.669332839846611,6.477710247039795,2.1465415954589844,4.607641220092773,0.4658656418323517,0.7638580203056335,0.46591538190841675,1.5632699728012085,3.0576400756835938,3.3552658557891846,0.2499212622642517,0.48979803919792175,0.9034159779548645,0.49743175506591797,0.6561670303344727,72.98995971679688,0.063012033700943,2.2847402095794678,0.002827510703355074,50.000047683761295
|
|
||||||
harmonic_5mm_1.5Hz.csv,1.5,1.2808952331542969,1.110302433240289,13.318247698830973,0.45307496190071106,2.450593227624893,2.827115535736084,1.5013059377670288,2.2539196014404297,0.666086733341217,0.40633538365364075,0.45307496190071106,1.1585400104522705,2.150049924850464,2.5570602416992188,0.25,0.5677548050880432,0.9871437549591064,0.5882158875465393,0.3955877423286438,194.53369140625,0.08695660531520844,1.5181952714920044,0.0008904285496100783,50.000047683761295
|
|
||||||
harmonic_5mm_1.3Hz.csv,1.3,0.9550632834434509,0.8240282575900374,13.72003595206496,0.2030215710401535,4.058821204900742,4.704245567321777,1.3018183708190918,1.6947312355041504,0.7681562900543213,0.26376205682754517,0.2030215710401535,0.7108299732208252,1.34552001953125,3.501253366470337,0.24993710219860077,0.20925211906433105,0.9342537522315979,0.22753745317459106,0.6098353862762451,55.399173736572266,0.07549075782299042,1.397734522819519,-0.00013690412743017077,50.000047683761295
|
|
||||||
harmonic_5mm_1.6Hz.csv,1.6,1.1856828927993774,1.011715705384904,14.672319932333666,0.22232241928577423,4.550668837785721,5.333168029785156,1.60205078125,2.5665667057037354,0.6241999268531799,0.47128453850746155,0.22232241928577423,0.8100799918174744,1.6100399494171143,3.643717050552368,0.2499159723520279,0.20403607189655304,0.9129946231842041,0.22558331489562988,0.6967292428016663,54.344844818115234,0.10087434202432632,1.6988458633422852,0.0011404975084587932,50.000047683761295
|
|
||||||
harmonic_5mm_2.45Hz.csv,2.45,4.167365074157715,3.5243981540885345,15.428619970357008,0.5156936049461365,6.83428710436821,8.081088066101074,2.4505887031555176,6.00538444519043,0.40806522965431213,0.8963282704353333,0.5156936049461365,1.7411500215530396,3.2864298820495605,3.376326560974121,0.25,0.5547626614570618,0.9294771552085876,0.5594695210456848,0.6415255665779114,73.60872650146484,0.06451619416475296,2.469957113265991,0.0013848240487277508,50.000047683761295
|
|
||||||
harmonic_5mm_1.05Hz.csv,1.05,0.36426904797554016,0.42230971018971875,15.933459770119113,0.2716720998287201,1.5544831819534302,1.3408409357070923,1.0530656576156616,1.1089472770690918,0.9496083855628967,0.051705583930015564,0.2716720998287201,0.7901600003242493,1.5045499801635742,2.908506393432617,0.24991999566555023,0.3438712954521179,0.9576663374900818,0.3379869759082794,0.4775521755218506,72.97195434570312,0.0640205442905426,1.126065731048584,-0.0008716708398424089,50.000047683761295
|
|
||||||
harmonic_5mm_1.8Hz.csv,1.8,1.6704485416412354,1.3841861638257655,17.136856998552837,0.5062151551246643,2.734383097410202,3.2998785972595215,1.8012458086013794,3.2444865703582764,0.5551713109016418,0.5884785652160645,0.5062151551246643,1.4020500183105469,2.7015299797058105,2.769672155380249,0.25,0.6510956883430481,0.9815194606781006,0.6610633730888367,0.3853244483470917,126.02283477783203,0.05555560812354088,1.819625735282898,0.0018060111906379461,50.000047683761295
|
|
||||||
harmonic_5mm_2.4Hz.csv,2.4,4.107017993927002,3.396024555134596,17.311670897077725,0.9670910835266113,3.5115870810747145,4.246774673461914,2.4017505645751953,5.768405437469482,0.4163629710674286,0.876197874546051,0.9670910835266113,2.3246400356292725,4.529560089111328,2.403744697570801,0.25,1.1933718919754028,0.9901480078697205,1.3087358474731445,0.2874786853790283,216.48052978515625,0.10344837605953217,2.4013094902038574,-8.548868208890781e-05,50.000047683761295
|
|
||||||
harmonic_5mm_2.2Hz.csv,2.2,1.0718663930892944,1.2945454730228163,20.774891476140446,0.6085917353630066,2.1271164194345475,1.7612240314483643,2.201188325881958,4.845230579376221,0.45430004596710205,0.7889974117279053,0.6085917353630066,1.7638200521469116,3.3627500534057617,2.8981993198394775,0.25,0.7086807489395142,0.9761964082717896,0.7861699461936951,0.4083978533744812,121.00875854492188,0.07894744724035263,2.1985433101654053,0.0010015374282374978,50.000047683761295
|
|
||||||
harmonic_5mm_0.6Hz.csv,0.6,0.16537058353424072,0.2013263053238447,21.742513705382912,0.09397729486227036,2.142286662101746,1.7596864700317383,0.5964841246604919,0.3557933568954468,1.6764904260635376,-0.5167025923728943,0.09397729486227036,0.3058300018310547,0.5446599721908569,3.2542967796325684,0.2499224841594696,0.08657485246658325,0.8864659667015076,0.09075998514890671,0.7302541732788086,35.94514465332031,0.0930522009730339,0.8173193335533142,0.001446693786419928,50.000047683761295
|
|
||||||
harmonic_5mm_2.25Hz.csv,2.25,2.1223104000091553,1.6541852357127729,22.057337338325393,0.45399144291877747,3.643648490548134,4.67478084564209,2.2518274784088135,5.070727348327637,0.4440837502479553,0.8117421269416809,0.45399144291877747,1.468690037727356,2.881589889526367,3.235060930252075,0.24992366135120392,0.4949429929256439,0.961538553237915,0.5433851480484009,0.5339846014976501,91.52095031738281,0.09163112193346024,2.2496755123138428,-0.001197385834529996,50.000047683761295
|
|
||||||
harmonic_5mm_1.9Hz.csv,1.9,0.5868141055107117,0.7590829336435948,29.356626999099728,0.5143030285835266,1.4759449030160905,1.1409889459609985,1.9012991189956665,3.614938497543335,0.5259561538696289,0.6425374150276184,0.5143030285835266,1.3888100385665894,2.6350998878479004,2.7003729343414307,0.25,0.6525437235832214,0.9834353923797607,0.670233428478241,0.38811808824539185,157.31686401367188,0.06250005960464478,1.9118845462799072,-0.0027283257804811,50.000047683761295
|
|
||||||
harmonic_5mm_2.05Hz.csv,2.05,0.6199118494987488,0.82180861364958,32.568624767228094,0.7315996885299683,1.1233036680221558,0.8473374843597412,2.0498123168945312,4.201730251312256,0.48784953355789185,0.71774822473526,0.7315996885299683,1.6353000402450562,3.2010200023651123,2.235239028930664,0.24991999566555023,0.9387410283088684,0.9784685373306274,0.9522533416748047,0.3911336362361908,110.50621795654297,0.0640205442905426,2.0580592155456543,-0.004247209522873163,50.000047683761295
|
|
||||||
harmonic_5mm_0.55Hz.csv,0.55,0.1971234530210495,0.2787700623838687,41.41902351624325,0.109112448990345,2.5548877782821657,1.8066083192825317,0.5490954518318176,0.30150580406188965,1.8211770057678223,-0.5994830131530762,0.109112448990345,0.40608999133110046,0.751579999923706,3.72175669670105,0.25,0.06793846935033798,0.5611140727996826,0.07492320239543915,0.874541163444519,13.549410820007324,0.08955232053995132,1.263006329536438,-0.0011556772515177727,50.000047683761295
|
|
||||||
harmonic_5mm_1.7Hz.csv,1.7,0.6067864894866943,0.8862511380642422,46.05650478703942,0.32135581970214844,2.7578499710559843,1.8882076740264893,1.700693964958191,2.892360210418701,0.5879952311515808,0.531036376953125,0.32135581970214844,1.1075899600982666,2.123849868774414,3.446615695953369,0.25,0.3294788897037506,0.9566228985786438,0.3822683095932007,0.5411480069160461,79.01654815673828,0.09523818641901016,1.7275936603546143,0.0009157023159787059,50.000047683761295
|
|
||||||
harmonic_5mm_1.4Hz.csv,1.4,0.3850800693035126,0.6183495861111026,60.57688657569331,0.2212747484445572,2.7944878051280977,1.740280270576477,1.3995026350021362,1.9586076736450195,0.7145395278930664,0.3361169397830963,0.2212747484445572,0.8126099705696106,1.4906599521636963,3.6724026203155518,0.2499212622642517,0.2121341973543167,0.9263311624526978,0.2458721548318863,0.6176945567131042,50.935482025146484,0.09451805055141449,1.501712679862976,0.0024810773320496082,50.000047683761295
|
|
||||||
harmonic_5mm_2.5Hz.csv,2.5,0.6538841724395752,1.1258108364036792,72.17282262749896,0.5744112730026245,1.9599386177062987,1.1383553743362427,2.5015766620635986,6.257885932922363,0.39974790811538696,0.9169211983680725,0.5744112730026245,1.5267499685287476,2.5278899669647217,2.6579387187957764,0.2499280571937561,0.7362810969352722,0.9838377833366394,0.7437620162963867,0.4004853665828705,234.6715087890625,0.05757058039307594,2.530334234237671,-0.01246642041951418,50.000047683761295
|
|
||||||
harmonic_5mm_2.35Hz.csv,2.35,0.7861254215240479,1.6179925774542805,105.81863060954142,0.991222620010376,1.6323200709819794,0.7930866479873657,2.351473808288574,5.529428958892822,0.42526522278785706,0.8550422787666321,0.991222620010376,2.2557199001312256,4.422339916229248,2.2756946086883545,0.2499212622642517,1.1917885541915894,0.9816745519638062,1.3294483423233032,0.31509023904800415,127.34346771240234,0.09451805055141449,2.3649213314056396,-0.004312594421207905,50.000047683761295
|
|
||||||
harmonic_5mm_2Hz.csv,2.0,0.5447156429290771,1.2863206517188583,136.14534820442807,0.7751342058181763,1.6594812124967575,0.7027371525764465,2.0002939701080322,4.001175880432129,0.49992653727531433,0.693294107913971,0.7751342058181763,1.559309959411621,3.019509792327881,2.011664390563965,0.24990099668502808,0.8933139443397522,0.9935725927352905,1.0516541004180908,0.28168511390686035,319.8121337890625,0.11885906755924225,2.0122017860412598,0.008680449798703194,50.000047683761295
|
|
||||||
|
|
Before Width: | Height: | Size: 165 KiB |
@@ -1,6 +0,0 @@
|
|||||||
file_name,frequency_hz,true_rms,pred_rms,relative_error_percent,x_rms,pred_tr,true_tr,dominant_frequency_hz,frequency_squared,inverse_frequency_hz,log_frequency_hz,input_rms,input_peak_abs,input_peak_to_peak,crest_factor,middle_length_ratio,dominant_amplitude,dominant_energy_ratio,harmonic_fit_amplitude,harmonic_fit_residual_ratio,spectral_peak_prominence,half_power_bandwidth_hz,spectral_centroid_hz,signal_mean,sampling_rate
|
|
||||||
harmonic_5mm_0.75Hz.csv,0.75,0.39182430505752563,0.4018011530100954,2.546255508857473,0.11687792837619781,3.4377846920490267,3.3524234294891357,0.7515671849250793,0.5648532509803772,1.3305530548095703,-0.285594642162323,0.11687792837619781,0.4074699878692627,0.8070399761199951,3.4862868785858154,0.24992701411247253,0.10824807733297348,0.8746803998947144,0.11673416197299957,0.7099294662475586,36.5,0.08761690557003021,0.8937567472457886,-0.001955104758962989,50.000047683761295
|
|
||||||
harmonic_5mm_0.95Hz.csv,0.95,0.5053804516792297,0.48249583166035853,4.528196518648947,0.1381060928106308,3.4936607201099394,3.6593642234802246,0.9513587951660156,0.9050835967063904,1.0511281490325928,-0.04986399784684181,0.1381060928106308,0.5125100016593933,1.0134000778198242,3.7109878063201904,0.25,0.09424196928739548,0.7232213020324707,0.11362786591053009,0.8133426308631897,20.55999183654785,0.09677428752183914,1.3476505279541016,0.00015772903861943632,50.000047683761295
|
|
||||||
harmonic_5mm_1.85Hz.csv,1.85,1.185911774635315,1.0257513926611004,13.50525270089895,0.6145902872085571,1.6690003308057786,1.9295974969863892,1.8481981754302979,3.4158363342285156,0.5410675406455994,0.6142112016677856,0.6145902872085571,1.3770899772644043,2.6619200706481934,2.240663528442383,0.25,0.7274389863014221,0.9514397978782654,0.789516270160675,0.42080339789390564,85.657958984375,0.09677428752183914,1.8637527227401733,-0.002481284085661173,50.000047683761295
|
|
||||||
harmonic_5mm_1.55Hz.csv,1.55,1.4177086353302002,0.7627473327797605,46.198583138198344,0.392645925283432,1.942583084821701,3.610654354095459,1.5492842197418213,2.4002816677093506,0.6454593539237976,0.4377930164337158,0.392645925283432,1.105049967765808,2.1589999198913574,2.8143677711486816,0.25,0.4746793210506439,0.9668325185775757,0.5014731884002686,0.43158969283103943,69.77510833740234,0.09836074709892273,1.584490418434143,-0.0011147483019158244,50.000047683761295
|
|
||||||
harmonic_5mm_1.25Hz.csv,1.25,0.3802441656589508,0.6856168561865096,80.30963210135198,0.15971125662326813,4.292852430582046,2.3808226585388184,1.2483434677124023,1.5583614110946655,0.8010615706443787,0.22181743383407593,0.15971125662326813,0.5707299709320068,1.0413799285888672,3.5735113620758057,0.24992592632770538,0.1439778357744217,0.8661479949951172,0.14893445372581482,0.7513272762298584,56.41539764404297,0.05927687883377075,1.4980274438858032,-0.0021986484061926603,50.000047683761295
|
|
||||||
|
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 502 KiB |
54
scripts/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 建筑结构简谐激励响应预测 (Harmonic Response Prediction)
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
本项目旨在通过数据驱动的方法,预测建筑结构在简谐波底座激振下的顶层加速度响应。工程利用机器学习算法提取核心物理特征,建立激励与响应之间的传递关系,实现对目标结构响应均方根(RMS)的快速、准确预测。
|
||||||
|
|
||||||
|
## 技术路线
|
||||||
|
本项目的核心技术链路包含数据预处理、特征工程、传递率建模与算法回归四个关键环节,逻辑清晰,具备较强的工程可解释性:
|
||||||
|
|
||||||
|
1. **稳态信号截取**
|
||||||
|
由于原始振动数据包含起振与衰减的瞬态过程,系统通过滑动窗口结合变异系数评估,自动截取最平稳的中间段数据,消除非稳态噪声对分析的干扰。
|
||||||
|
2. **物理特征工程**
|
||||||
|
系统从底座激励信号中提取 4 维极具代表性的物理特征作为模型输入:
|
||||||
|
- 频率 ($f$)
|
||||||
|
- 频率的平方 ($f^2$)
|
||||||
|
- 激励均方根 ($x_{rms}$)
|
||||||
|
- 理论加速度幅值 ($(2\pi f)^2 \cdot A$)
|
||||||
|
3. **传递率 (TR) 目标建模**
|
||||||
|
模型将预测目标设定为系统的**传递率 (Transmissibility, TR)**,即输出响应与输入激励的均方根比值 ($TR = y_{rms} / x_{rms}$)。
|
||||||
|
在最终推理阶段,通过公式 $\text{预测响应 } y_{rms} = \text{预测 } TR \times \text{实际输入 } x_{rms}$ 还原最终结果,这种无量纲化的处理极大提升了模型的泛化能力。
|
||||||
|
4. **距离权重 KNN 回归**
|
||||||
|
采用数据标准化 (StandardScaler) 结合距离加权的 K 近邻回归 (KNeighborsRegressor) 算法。通过特征空间中的距离衰减机制,对高维空间中的传递率进行平滑拟合。
|
||||||
|
|
||||||
|
## 核心模块说明
|
||||||
|
|
||||||
|
- **`config.py`**
|
||||||
|
全局配置中心。集中管理数据路径、传感器编号(基座输入与顶层输出)、信号截取比例以及模型超参数。
|
||||||
|
- **`dataset.py`**
|
||||||
|
数据处理引擎。负责读取原始 CSV 振动数据、缺失值插值对齐、稳态窗口搜索、傅里叶主频计算以及核心物理特征的打包提取。
|
||||||
|
- **`train_final.py`**
|
||||||
|
模型训练入口。读取全量简谐波数据,构建特征矩阵与目标向量完成 KNN 模型训练,将最优模型序列化保存,并输出全量数据的拟合评估曲线。
|
||||||
|
- **`evaluate.py`**
|
||||||
|
批量评估工具。加载已保存的模型权重,对全集数据进行预测评估,生成包含理论拟合曲线、散点对比与误差分布的可视化图表。
|
||||||
|
- **`predict_single.py`**
|
||||||
|
单样本预测脚本。支持输入特定的单一 CSV 数据文件,提供端到端的预测功能,并绘制包含时域对比、频域谱线及预测 RMS 对比的综合诊断图。
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
**1. 训练模型**
|
||||||
|
读取配置文件中指定的数据集,执行全量训练并保存模型权重:
|
||||||
|
```bash
|
||||||
|
python scripts/train_final.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 批量评估**
|
||||||
|
加载已训练的模型,对样本进行批量评估与统计分析:
|
||||||
|
```bash
|
||||||
|
python scripts/evaluate.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 单文件预测**
|
||||||
|
对特定的单一样本进行独立预测,并生成详细的图表报告:
|
||||||
|
```bash
|
||||||
|
python scripts/predict_single.py --file path/to/your/harmonic.csv
|
||||||
|
```
|
||||||
@@ -5,34 +5,18 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
CORE_FEATURE_NAMES: tuple[str, ...] = (
|
CORE_FEATURE_NAMES: tuple[str, ...] = (
|
||||||
"dominant_frequency_hz",
|
"frequency_hz",
|
||||||
"frequency_squared",
|
"frequency_squared",
|
||||||
"inverse_frequency_hz",
|
"x_rms",
|
||||||
"log_frequency_hz",
|
"theoretical_accel",
|
||||||
"input_rms",
|
|
||||||
"input_peak_abs",
|
|
||||||
"input_peak_to_peak",
|
|
||||||
"crest_factor",
|
|
||||||
"middle_length_ratio",
|
|
||||||
"dominant_amplitude",
|
|
||||||
"dominant_energy_ratio",
|
|
||||||
"harmonic_fit_amplitude",
|
|
||||||
"harmonic_fit_residual_ratio",
|
|
||||||
"spectral_peak_prominence",
|
|
||||||
"half_power_bandwidth_hz",
|
|
||||||
"spectral_centroid_hz",
|
|
||||||
"signal_mean",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DataConfig:
|
class DataConfig:
|
||||||
project_root: Path = field(default_factory=lambda: Path(__file__).resolve().parents[1])
|
project_root: Path = field(default_factory=lambda: Path(__file__).resolve().parents[1])
|
||||||
scenario: str = "Non_TMD"
|
data_dir: str = "downloads/Non_TMD"
|
||||||
train_split_name: str = "train"
|
harmonic_pattern: str = "harmonic*.csv"
|
||||||
val_split_name: str = "val"
|
|
||||||
test_split_name: str = "test"
|
|
||||||
csv_pattern: str = "*.csv"
|
|
||||||
|
|
||||||
code_column: str = "code"
|
code_column: str = "code"
|
||||||
time_column: str = "time"
|
time_column: str = "time"
|
||||||
@@ -49,52 +33,31 @@ class DataConfig:
|
|||||||
stability_subwindow_count: int = 4
|
stability_subwindow_count: int = 4
|
||||||
interpolation_method: str = "linear"
|
interpolation_method: str = "linear"
|
||||||
normalization_eps: float = 1e-6
|
normalization_eps: float = 1e-6
|
||||||
|
harmonic_amplitude_m: float = 0.005
|
||||||
|
|
||||||
downloads_dir: Path = field(init=False)
|
data_root: Path = field(init=False)
|
||||||
scenario_dir: Path = field(init=False)
|
|
||||||
train_dir: Path = field(init=False)
|
|
||||||
val_dir: Path = field(init=False)
|
|
||||||
test_dir: Path = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.project_root = Path(self.project_root).resolve()
|
self.project_root = Path(self.project_root).resolve()
|
||||||
self.downloads_dir = self.project_root / "downloads"
|
self.data_root = (self.project_root / self.data_dir).resolve()
|
||||||
self.scenario_dir = self.downloads_dir / self.scenario
|
|
||||||
self.train_dir = self.scenario_dir / self.train_split_name
|
|
||||||
self.val_dir = self.scenario_dir / self.val_split_name
|
|
||||||
self.test_dir = self.scenario_dir / self.test_split_name
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ModelConfig:
|
class ModelConfig:
|
||||||
input_dim: int = len(CORE_FEATURE_NAMES)
|
model_name: str = "knn_distance"
|
||||||
hidden_dims: tuple[int, ...] = (96, 64, 32)
|
n_neighbors: int = 4
|
||||||
dropout: float = 0.08
|
distance_power: int = 2
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TrainConfig:
|
class TrainConfig:
|
||||||
epochs: int = 400
|
checkpoint_dir: str = "checkpoints_final"
|
||||||
batch_size: int = 16
|
model_name: str = "task1_final_model.pkl"
|
||||||
learning_rate: float = 1e-3
|
fit_csv_name: str = "task1_final_fit_all.csv"
|
||||||
weight_decay: float = 1e-4
|
fit_figure_name: str = "task1_final_fit_curve.png"
|
||||||
seed: int = 42
|
dense_curve_csv_name: str = "task1_final_dense_curve.csv"
|
||||||
device: str = "cuda"
|
evaluation_dense_csv_name: str = "evaluation_dense_curve.csv"
|
||||||
grad_clip_norm: float = 1.0
|
dense_curve_points: int = 400
|
||||||
lr_scheduler_patience: int = 20
|
|
||||||
lr_scheduler_factor: float = 0.5
|
|
||||||
min_learning_rate: float = 1e-6
|
|
||||||
early_stop_patience: int = 50
|
|
||||||
checkpoint_dir: str = "checkpoints_mlp"
|
|
||||||
history_name: str = "training_history.csv"
|
|
||||||
best_model_name: str = "best_feature_mlp.pt"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LossConfig:
|
|
||||||
relative_rms_weight: float = 1.0
|
|
||||||
log_rms_huber_weight: float = 0.75
|
|
||||||
mae_weight: float = 0.15
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -102,8 +65,19 @@ class ExperimentConfig:
|
|||||||
data: DataConfig = field(default_factory=DataConfig)
|
data: DataConfig = field(default_factory=DataConfig)
|
||||||
model: ModelConfig = field(default_factory=ModelConfig)
|
model: ModelConfig = field(default_factory=ModelConfig)
|
||||||
train: TrainConfig = field(default_factory=TrainConfig)
|
train: TrainConfig = field(default_factory=TrainConfig)
|
||||||
loss: LossConfig = field(default_factory=LossConfig)
|
|
||||||
|
|
||||||
|
|
||||||
def make_experiment_config() -> ExperimentConfig:
|
def make_experiment_config() -> ExperimentConfig:
|
||||||
return ExperimentConfig()
|
return ExperimentConfig()
|
||||||
|
|
||||||
|
|
||||||
|
def checkpoint_dir(config: ExperimentConfig) -> Path:
|
||||||
|
path = config.data.project_root / config.train.checkpoint_dir / "task1_final"
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def evaluation_dir(config: ExperimentConfig) -> Path:
|
||||||
|
path = config.data.project_root / "evaluation_outputs" / "task1_final"
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import torch
|
|
||||||
from torch.utils.data import DataLoader, Dataset
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .config import CORE_FEATURE_NAMES, DataConfig, ExperimentConfig
|
from .config import CORE_FEATURE_NAMES, DataConfig, ExperimentConfig
|
||||||
@@ -85,7 +82,6 @@ def get_middle_segment(
|
|||||||
peak_cv = float(split_peaks.std() / max(split_peaks.mean(), config.normalization_eps))
|
peak_cv = float(split_peaks.std() / max(split_peaks.mean(), config.normalization_eps))
|
||||||
window_rms = calculate_rms(window_y)
|
window_rms = calculate_rms(window_y)
|
||||||
|
|
||||||
# Prefer windows that are stable inside the middle candidate region while keeping enough energy.
|
|
||||||
score = rms_cv + 0.35 * peak_cv - 0.05 * (window_rms / max(candidate_rms, config.normalization_eps))
|
score = rms_cv + 0.35 * peak_cv - 0.05 * (window_rms / max(candidate_rms, config.normalization_eps))
|
||||||
if score < best_score:
|
if score < best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
@@ -134,106 +130,24 @@ def get_dominant_frequency(signal: np.ndarray, sampling_rate: float) -> float:
|
|||||||
return _parabolic_peak_frequency(freqs, magnitudes, peak_index)
|
return _parabolic_peak_frequency(freqs, magnitudes, peak_index)
|
||||||
|
|
||||||
|
|
||||||
def compute_harmonic_fit_features(signal: np.ndarray, sampling_rate: float, frequency_hz: float) -> tuple[float, float]:
|
def theoretical_acceleration(frequency_hz: float, amplitude_m: float) -> float:
|
||||||
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
return float((2.0 * np.pi * frequency_hz) ** 2 * amplitude_m)
|
||||||
if signal.size < 4 or frequency_hz <= 0.0:
|
|
||||||
return 0.0, 1.0
|
|
||||||
time_axis = np.arange(signal.size, dtype=np.float64) / max(sampling_rate, 1e-12)
|
|
||||||
omega_t = 2.0 * np.pi * frequency_hz * time_axis
|
|
||||||
design = np.stack([np.sin(omega_t), np.cos(omega_t), np.ones_like(omega_t)], axis=1)
|
|
||||||
coefficients, _, _, _ = np.linalg.lstsq(design, signal, rcond=None)
|
|
||||||
fitted = design @ coefficients
|
|
||||||
harmonic_amplitude = float(np.sqrt(coefficients[0] ** 2 + coefficients[1] ** 2))
|
|
||||||
residual = signal - fitted
|
|
||||||
residual_ratio = calculate_rms(residual) / max(calculate_rms(signal), 1e-6)
|
|
||||||
return harmonic_amplitude, float(residual_ratio)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_spectral_features(signal: np.ndarray, sampling_rate: float) -> tuple[float, float, float, float, float]:
|
|
||||||
freqs, amplitudes = _compute_windowed_spectrum(signal, sampling_rate)
|
|
||||||
if amplitudes.size == 0:
|
|
||||||
return 0.0, 0.0, 0.0, 0.0, 0.0
|
|
||||||
|
|
||||||
powers = np.square(amplitudes)
|
|
||||||
amplitudes[0] = 0.0
|
|
||||||
powers[0] = 0.0
|
|
||||||
band_mask = (freqs >= 0.1) & (freqs <= 5.0)
|
|
||||||
if not np.any(band_mask):
|
|
||||||
return 0.0, 0.0, 0.0, 0.0, 0.0
|
|
||||||
|
|
||||||
band_amplitudes = np.where(band_mask, amplitudes, 0.0)
|
|
||||||
dominant_index = int(np.argmax(band_amplitudes))
|
|
||||||
dominant_amplitude = float(amplitudes[dominant_index])
|
|
||||||
dominant_freq = _parabolic_peak_frequency(freqs, amplitudes, dominant_index)
|
|
||||||
|
|
||||||
local_mask = np.abs(freqs - dominant_freq) <= 0.10
|
|
||||||
dominant_energy = float(powers[local_mask].sum())
|
|
||||||
total_band_energy = float(powers[band_mask].sum())
|
|
||||||
dominant_energy_ratio = dominant_energy / max(total_band_energy, 1e-12)
|
|
||||||
spectral_centroid = float((freqs[band_mask] * powers[band_mask]).sum() / max(total_band_energy, 1e-12))
|
|
||||||
|
|
||||||
background_mask = band_mask & (~local_mask)
|
|
||||||
background_level = float(np.median(amplitudes[background_mask])) if np.any(background_mask) else 0.0
|
|
||||||
spectral_peak_prominence = dominant_amplitude / max(background_level, 1e-6)
|
|
||||||
|
|
||||||
half_power_level = dominant_amplitude / np.sqrt(2.0)
|
|
||||||
left_index = dominant_index
|
|
||||||
right_index = dominant_index
|
|
||||||
while left_index > 0 and amplitudes[left_index] >= half_power_level:
|
|
||||||
left_index -= 1
|
|
||||||
while right_index < amplitudes.size - 1 and amplitudes[right_index] >= half_power_level:
|
|
||||||
right_index += 1
|
|
||||||
half_power_bandwidth = float(freqs[right_index] - freqs[left_index]) if right_index > left_index else 0.0
|
|
||||||
return dominant_amplitude, dominant_energy_ratio, spectral_centroid, spectral_peak_prominence, half_power_bandwidth
|
|
||||||
|
|
||||||
|
|
||||||
def extract_core_features(
|
def extract_core_features(
|
||||||
signal: np.ndarray,
|
signal: np.ndarray,
|
||||||
sampling_rate: float,
|
sampling_rate: float,
|
||||||
original_length: int,
|
|
||||||
config: DataConfig,
|
config: DataConfig,
|
||||||
|
known_frequency_hz: float | None = None,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
|
frequency_hz = known_frequency_hz if known_frequency_hz is not None else get_dominant_frequency(signal, sampling_rate)
|
||||||
x_rms = calculate_rms(signal)
|
x_rms = calculate_rms(signal)
|
||||||
x_peak = float(np.max(np.abs(signal))) if signal.size > 0 else 0.0
|
|
||||||
x_peak_to_peak = float(np.max(signal) - np.min(signal)) if signal.size > 0 else 0.0
|
|
||||||
crest_factor = x_peak / max(x_rms, config.normalization_eps)
|
|
||||||
dominant_frequency = get_dominant_frequency(signal, sampling_rate)
|
|
||||||
frequency_squared = dominant_frequency * dominant_frequency
|
|
||||||
inverse_frequency = 1.0 / max(dominant_frequency, 1e-6)
|
|
||||||
log_frequency = float(np.log(max(dominant_frequency, 1e-6)))
|
|
||||||
middle_length_ratio = float(signal.size) / float(max(original_length, 1))
|
|
||||||
(
|
|
||||||
dominant_amplitude,
|
|
||||||
dominant_energy_ratio,
|
|
||||||
spectral_centroid,
|
|
||||||
spectral_peak_prominence,
|
|
||||||
half_power_bandwidth,
|
|
||||||
) = compute_spectral_features(signal, sampling_rate)
|
|
||||||
harmonic_fit_amplitude, harmonic_fit_residual_ratio = compute_harmonic_fit_features(
|
|
||||||
signal,
|
|
||||||
sampling_rate,
|
|
||||||
dominant_frequency,
|
|
||||||
)
|
|
||||||
signal_mean = float(np.mean(signal)) if signal.size > 0 else 0.0
|
|
||||||
return np.asarray(
|
return np.asarray(
|
||||||
[
|
[
|
||||||
dominant_frequency,
|
frequency_hz,
|
||||||
frequency_squared,
|
frequency_hz * frequency_hz,
|
||||||
inverse_frequency,
|
|
||||||
log_frequency,
|
|
||||||
x_rms,
|
x_rms,
|
||||||
x_peak,
|
theoretical_acceleration(frequency_hz, config.harmonic_amplitude_m),
|
||||||
x_peak_to_peak,
|
|
||||||
crest_factor,
|
|
||||||
middle_length_ratio,
|
|
||||||
dominant_amplitude,
|
|
||||||
dominant_energy_ratio,
|
|
||||||
harmonic_fit_amplitude,
|
|
||||||
harmonic_fit_residual_ratio,
|
|
||||||
spectral_peak_prominence,
|
|
||||||
half_power_bandwidth,
|
|
||||||
spectral_centroid,
|
|
||||||
signal_mean,
|
|
||||||
],
|
],
|
||||||
dtype=np.float32,
|
dtype=np.float32,
|
||||||
)
|
)
|
||||||
@@ -277,29 +191,26 @@ def load_aligned_signals(file_path: Path, config: DataConfig) -> tuple[np.ndarra
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SampleRecord:
|
class SampleRecord:
|
||||||
file_path: Path
|
file_path: Path
|
||||||
split: str
|
|
||||||
frequency_hz: float
|
frequency_hz: float
|
||||||
features: torch.Tensor
|
features: np.ndarray
|
||||||
x_rms: torch.Tensor
|
x_rms: float
|
||||||
y_rms: torch.Tensor
|
y_rms: float
|
||||||
target_y_rms: torch.Tensor
|
target_tr: float
|
||||||
target_log_y_rms: torch.Tensor
|
time_middle: np.ndarray
|
||||||
time_middle: torch.Tensor
|
x_middle: np.ndarray
|
||||||
x_middle: torch.Tensor
|
y_middle: np.ndarray
|
||||||
y_middle: torch.Tensor
|
|
||||||
sampling_rate: float
|
sampling_rate: float
|
||||||
interpolation_count: int = 0
|
interpolation_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SplitLoadReport:
|
class LoadReport:
|
||||||
split: str
|
|
||||||
loaded_files: list[str] = field(default_factory=list)
|
loaded_files: list[str] = field(default_factory=list)
|
||||||
skipped_files: list[tuple[str, str]] = field(default_factory=list)
|
skipped_files: list[tuple[str, str]] = field(default_factory=list)
|
||||||
interpolated_files: dict[str, int] = field(default_factory=dict)
|
interpolated_files: dict[str, int] = field(default_factory=dict)
|
||||||
|
|
||||||
def to_lines(self) -> list[str]:
|
def to_lines(self) -> list[str]:
|
||||||
lines = [f"[{self.split}] loaded={len(self.loaded_files)} skipped={len(self.skipped_files)}"]
|
lines = [f"[all_data] loaded={len(self.loaded_files)} skipped={len(self.skipped_files)}"]
|
||||||
for file_name, reason in self.skipped_files:
|
for file_name, reason in self.skipped_files:
|
||||||
lines.append(f" - skipped {file_name}: {reason}")
|
lines.append(f" - skipped {file_name}: {reason}")
|
||||||
for file_name, count in self.interpolated_files.items():
|
for file_name, count in self.interpolated_files.items():
|
||||||
@@ -308,45 +219,12 @@ class SplitLoadReport:
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
def list_harmonic_files(config: DataConfig) -> list[Path]:
|
||||||
class NormalizationStats:
|
files = sorted(config.data_root.rglob(config.harmonic_pattern))
|
||||||
feature_mean: torch.Tensor
|
return [file_path for file_path in files if extract_frequency_hz(file_path.name) is not None]
|
||||||
feature_std: torch.Tensor
|
|
||||||
target_mean: torch.Tensor
|
|
||||||
target_std: torch.Tensor
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureDataset(Dataset):
|
def build_record_from_file(file_path: Path, config: DataConfig) -> SampleRecord:
|
||||||
def __init__(self, records: list[SampleRecord], normalization: NormalizationStats) -> None:
|
|
||||||
self.records = records
|
|
||||||
self.normalization = normalization
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.records)
|
|
||||||
|
|
||||||
def __getitem__(self, index: int) -> dict[str, Any]:
|
|
||||||
record = self.records[index]
|
|
||||||
feature_norm = (record.features - self.normalization.feature_mean) / self.normalization.feature_std
|
|
||||||
target_norm = (record.target_y_rms - self.normalization.target_mean) / self.normalization.target_std
|
|
||||||
return {
|
|
||||||
"x": feature_norm,
|
|
||||||
"target": target_norm,
|
|
||||||
"target_raw": record.target_y_rms,
|
|
||||||
"target_log_raw": record.target_log_y_rms,
|
|
||||||
"x_rms_raw": record.x_rms,
|
|
||||||
"y_rms_raw": record.y_rms,
|
|
||||||
"frequency_hz": torch.tensor(record.frequency_hz, dtype=torch.float32),
|
|
||||||
"features_raw": record.features,
|
|
||||||
"file_name": record.file_path.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def list_split_files(config: DataConfig, split: str) -> list[Path]:
|
|
||||||
split_dir = getattr(config, f"{split}_dir")
|
|
||||||
return sorted(split_dir.glob(config.csv_pattern))
|
|
||||||
|
|
||||||
|
|
||||||
def build_record_from_file(file_path: Path, split: str, config: DataConfig) -> SampleRecord:
|
|
||||||
time_values, x_values, y_values, interpolation_count = load_aligned_signals(file_path, config)
|
time_values, x_values, y_values, interpolation_count = load_aligned_signals(file_path, config)
|
||||||
time_middle, x_middle, y_middle = get_middle_segment(time_values, x_values, y_values, config)
|
time_middle, x_middle, y_middle = get_middle_segment(time_values, x_values, y_values, config)
|
||||||
sampling_rate = estimate_sampling_rate(time_middle)
|
sampling_rate = estimate_sampling_rate(time_middle)
|
||||||
@@ -356,34 +234,32 @@ def build_record_from_file(file_path: Path, split: str, config: DataConfig) -> S
|
|||||||
|
|
||||||
x_rms = calculate_rms(x_middle)
|
x_rms = calculate_rms(x_middle)
|
||||||
y_rms = calculate_rms(y_middle)
|
y_rms = calculate_rms(y_middle)
|
||||||
transmission_ratio = float(y_rms / max(x_rms, config.normalization_eps))
|
target_tr = float(y_rms / max(x_rms, config.normalization_eps))
|
||||||
target_y_rms = transmission_ratio
|
features = extract_core_features(x_middle, sampling_rate, config, known_frequency_hz=frequency_hz)
|
||||||
target_log_y_rms = float(np.log(max(transmission_ratio, config.normalization_eps)))
|
|
||||||
features = extract_core_features(x_middle, sampling_rate, len(x_values), config)
|
|
||||||
|
|
||||||
return SampleRecord(
|
return SampleRecord(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
split=split,
|
frequency_hz=float(frequency_hz),
|
||||||
frequency_hz=frequency_hz,
|
features=features,
|
||||||
features=torch.tensor(features, dtype=torch.float32),
|
x_rms=float(x_rms),
|
||||||
x_rms=torch.tensor([x_rms], dtype=torch.float32),
|
y_rms=float(y_rms),
|
||||||
y_rms=torch.tensor([y_rms], dtype=torch.float32),
|
target_tr=target_tr,
|
||||||
target_y_rms=torch.tensor([target_y_rms], dtype=torch.float32),
|
time_middle=np.asarray(time_middle, dtype=np.float64),
|
||||||
target_log_y_rms=torch.tensor([target_log_y_rms], dtype=torch.float32),
|
x_middle=np.asarray(x_middle, dtype=np.float32).reshape(-1),
|
||||||
time_middle=torch.tensor(time_middle, dtype=torch.float64),
|
y_middle=np.asarray(y_middle, dtype=np.float32).reshape(-1),
|
||||||
x_middle=torch.tensor(x_middle[:, None], dtype=torch.float32),
|
sampling_rate=float(sampling_rate),
|
||||||
y_middle=torch.tensor(y_middle[:, None], dtype=torch.float32),
|
|
||||||
sampling_rate=sampling_rate,
|
|
||||||
interpolation_count=interpolation_count,
|
interpolation_count=interpolation_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_split_records(config: DataConfig, split: str) -> tuple[list[SampleRecord], SplitLoadReport]:
|
def load_all_records(config: ExperimentConfig | DataConfig) -> tuple[list[SampleRecord], LoadReport]:
|
||||||
|
data_config = config.data if isinstance(config, ExperimentConfig) else config
|
||||||
records: list[SampleRecord] = []
|
records: list[SampleRecord] = []
|
||||||
report = SplitLoadReport(split=split)
|
report = LoadReport()
|
||||||
for file_path in list_split_files(config, split):
|
|
||||||
|
for file_path in list_harmonic_files(data_config):
|
||||||
try:
|
try:
|
||||||
record = build_record_from_file(file_path, split, config)
|
record = build_record_from_file(file_path, data_config)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
report.skipped_files.append((file_path.name, str(error)))
|
report.skipped_files.append((file_path.name, str(error)))
|
||||||
continue
|
continue
|
||||||
@@ -393,86 +269,26 @@ def load_split_records(config: DataConfig, split: str) -> tuple[list[SampleRecor
|
|||||||
report.interpolated_files[file_path.name] = record.interpolation_count
|
report.interpolated_files[file_path.name] = record.interpolation_count
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
raise RuntimeError(f"No usable records found for split='{split}'.")
|
raise RuntimeError("No usable harmonic records found in data_root.")
|
||||||
return records, report
|
return records, report
|
||||||
|
|
||||||
|
|
||||||
def fit_normalization(records: list[SampleRecord], config: DataConfig) -> NormalizationStats:
|
def records_to_frame(records: list[SampleRecord]) -> pd.DataFrame:
|
||||||
feature_all = torch.stack([record.features for record in records], dim=0)
|
rows = []
|
||||||
target_all = torch.cat([record.target_y_rms for record in records], dim=0)
|
for record in records:
|
||||||
|
row = {
|
||||||
feature_std = torch.clamp(feature_all.std(dim=0, unbiased=False), min=config.normalization_eps)
|
"file_name": record.file_path.name,
|
||||||
target_std = torch.clamp(target_all.std(dim=0, unbiased=False).view(1), min=config.normalization_eps)
|
"frequency_hz": record.frequency_hz,
|
||||||
|
"x_rms": record.x_rms,
|
||||||
return NormalizationStats(
|
"y_rms": record.y_rms,
|
||||||
feature_mean=feature_all.mean(dim=0),
|
"target_tr": record.target_tr,
|
||||||
feature_std=feature_std,
|
"sampling_rate": record.sampling_rate,
|
||||||
target_mean=target_all.mean(dim=0, keepdim=True),
|
|
||||||
target_std=target_std,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_datasets(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[dict[str, FeatureDataset], dict[str, list[SampleRecord]], dict[str, SplitLoadReport]]:
|
|
||||||
data_config = config.data if isinstance(config, ExperimentConfig) else config
|
|
||||||
train_records, train_report = load_split_records(data_config, "train")
|
|
||||||
normalization = fit_normalization(train_records, data_config)
|
|
||||||
val_records, val_report = load_split_records(data_config, "val")
|
|
||||||
test_records, test_report = load_split_records(data_config, "test")
|
|
||||||
|
|
||||||
raw_records = {
|
|
||||||
"train": train_records,
|
|
||||||
"val": val_records,
|
|
||||||
"test": test_records,
|
|
||||||
}
|
}
|
||||||
datasets = {
|
for feature_name, feature_value in zip(CORE_FEATURE_NAMES, record.features.tolist()):
|
||||||
split: FeatureDataset(records, normalization)
|
row[feature_name] = float(feature_value)
|
||||||
for split, records in raw_records.items()
|
rows.append(row)
|
||||||
}
|
return pd.DataFrame(rows)
|
||||||
reports = {
|
|
||||||
"train": train_report,
|
|
||||||
"val": val_report,
|
|
||||||
"test": test_report,
|
|
||||||
}
|
|
||||||
return datasets, raw_records, reports
|
|
||||||
|
|
||||||
|
|
||||||
def build_dataloaders(
|
def report_to_text(report: LoadReport) -> str:
|
||||||
config: ExperimentConfig | DataConfig,
|
return "\n".join(report.to_lines())
|
||||||
) -> tuple[dict[str, DataLoader], dict[str, FeatureDataset], dict[str, list[SampleRecord]], dict[str, SplitLoadReport]]:
|
|
||||||
experiment_config = config if isinstance(config, ExperimentConfig) else ExperimentConfig(data=config)
|
|
||||||
datasets, raw_records, reports = build_datasets(experiment_config)
|
|
||||||
batch_size = experiment_config.train.batch_size
|
|
||||||
loaders = {
|
|
||||||
"train": DataLoader(datasets["train"], batch_size=batch_size, shuffle=True),
|
|
||||||
"val": DataLoader(datasets["val"], batch_size=batch_size, shuffle=False),
|
|
||||||
"test": DataLoader(datasets["test"], batch_size=batch_size, shuffle=False),
|
|
||||||
}
|
|
||||||
return loaders, datasets, raw_records, reports
|
|
||||||
|
|
||||||
|
|
||||||
def report_to_text(reports: dict[str, SplitLoadReport]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
for split in ("train", "val", "test"):
|
|
||||||
lines.extend(reports[split].to_lines())
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def checkpoint_normalization_payload(normalization: NormalizationStats) -> dict[str, list[float]]:
|
|
||||||
return {
|
|
||||||
"feature_mean": normalization.feature_mean.tolist(),
|
|
||||||
"feature_std": normalization.feature_std.tolist(),
|
|
||||||
"target_mean": normalization.target_mean.tolist(),
|
|
||||||
"target_std": normalization.target_std.tolist(),
|
|
||||||
"feature_names": list(CORE_FEATURE_NAMES),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def normalization_from_payload(payload: dict[str, Any]) -> NormalizationStats:
|
|
||||||
return NormalizationStats(
|
|
||||||
feature_mean=torch.tensor(payload["feature_mean"], dtype=torch.float32),
|
|
||||||
feature_std=torch.tensor(payload["feature_std"], dtype=torch.float32),
|
|
||||||
target_mean=torch.tensor(payload["target_mean"], dtype=torch.float32),
|
|
||||||
target_std=torch.tensor(payload["target_std"], dtype=torch.float32),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,105 +1,76 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import pickle
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import torch
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .config import CORE_FEATURE_NAMES, ExperimentConfig, make_experiment_config
|
from .config import CORE_FEATURE_NAMES, evaluation_dir, make_experiment_config
|
||||||
from .dataset import build_dataloaders, normalization_from_payload, report_to_text
|
from .dataset import load_all_records, records_to_frame, report_to_text
|
||||||
from .model import build_model
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from config import CORE_FEATURE_NAMES, ExperimentConfig, make_experiment_config
|
from config import CORE_FEATURE_NAMES, evaluation_dir, make_experiment_config
|
||||||
from dataset import build_dataloaders, normalization_from_payload, report_to_text
|
from dataset import load_all_records, records_to_frame, report_to_text
|
||||||
from model import build_model
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Evaluate feature MLP on train/val/test splits.")
|
parser = argparse.ArgumentParser(description="Evaluate the final all-data task1 model.")
|
||||||
parser.add_argument("--split", choices=("train", "val", "test"), default="val")
|
|
||||||
parser.add_argument("--sample-index", type=int, default=0)
|
|
||||||
parser.add_argument("--all-samples", action="store_true")
|
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
parser.add_argument("--checkpoint", type=str, default=None)
|
parser.add_argument("--checkpoint", type=str, default=None)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str | None, config: ExperimentConfig) -> torch.device:
|
def resolve_checkpoint_path(project_root: Path, checkpoint_arg: str | None) -> Path:
|
||||||
requested = device_name or config.train.device
|
|
||||||
if requested.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(requested)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_path(config: ExperimentConfig, checkpoint_arg: str | None) -> Path:
|
|
||||||
if checkpoint_arg:
|
if checkpoint_arg:
|
||||||
return Path(checkpoint_arg).resolve()
|
return Path(checkpoint_arg).resolve()
|
||||||
return config.data.project_root / config.train.checkpoint_dir / "task1_feature_mlp" / config.train.best_model_name
|
return project_root / "checkpoints_final" / "task1_final" / "task1_final_model.pkl"
|
||||||
|
|
||||||
|
|
||||||
def relative_percent_error(true_value: float, pred_value: float) -> float:
|
def relative_error_percent(true_value: float, pred_value: float) -> float:
|
||||||
denominator = max(abs(true_value), 1e-12)
|
return abs(pred_value - true_value) / max(abs(true_value), 1e-12) * 100.0
|
||||||
return abs(pred_value - true_value) / denominator * 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def predict_rms(
|
def build_dense_curve(model, result_df: pd.DataFrame, config) -> pd.DataFrame:
|
||||||
model: torch.nn.Module,
|
|
||||||
feature_norm: torch.Tensor,
|
|
||||||
x_rms_raw: torch.Tensor,
|
|
||||||
target_mean: torch.Tensor,
|
|
||||||
target_std: torch.Tensor,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
pred_norm = model(feature_norm)
|
|
||||||
pred_tr = torch.clamp(pred_norm * target_std + target_mean, min=1e-6)
|
|
||||||
return pred_tr * x_rms_raw
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_sample(
|
|
||||||
model: torch.nn.Module,
|
|
||||||
dataset,
|
|
||||||
raw_record,
|
|
||||||
sample_index: int,
|
|
||||||
device: torch.device,
|
|
||||||
) -> dict[str, float | str]:
|
|
||||||
sample = dataset[sample_index]
|
|
||||||
normalization = dataset.normalization
|
|
||||||
feature_norm = sample["x"].unsqueeze(0).to(device)
|
|
||||||
x_rms_raw = sample["x_rms_raw"].unsqueeze(0).to(device)
|
|
||||||
target_mean = normalization.target_mean.to(device).view(1, 1)
|
|
||||||
target_std = normalization.target_std.to(device).view(1, 1)
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
pred_rms = predict_rms(model, feature_norm, x_rms_raw, target_mean, target_std)
|
|
||||||
|
|
||||||
true_rms = float(sample["y_rms_raw"].item())
|
|
||||||
pred_rms_value = float(pred_rms.detach().cpu().numpy().reshape(-1)[0])
|
|
||||||
row = {
|
|
||||||
"file_name": str(sample["file_name"]),
|
|
||||||
"frequency_hz": float(sample["frequency_hz"].item()),
|
|
||||||
"true_rms": true_rms,
|
|
||||||
"pred_rms": pred_rms_value,
|
|
||||||
"relative_error_percent": relative_percent_error(true_rms, pred_rms_value),
|
|
||||||
}
|
|
||||||
features_raw = sample["features_raw"].numpy().reshape(-1)
|
|
||||||
for name, value in zip(CORE_FEATURE_NAMES, features_raw):
|
|
||||||
row[name] = float(value)
|
|
||||||
row["sampling_rate"] = float(raw_record.sampling_rate)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def save_all_samples_plot(result_df: pd.DataFrame, split: str, save_dir: Path) -> Path | None:
|
|
||||||
if result_df["frequency_hz"].isna().any():
|
|
||||||
return None
|
|
||||||
plot_df = result_df.sort_values("frequency_hz").reset_index(drop=True)
|
plot_df = result_df.sort_values("frequency_hz").reset_index(drop=True)
|
||||||
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
|
dense_freq = np.linspace(
|
||||||
fig.suptitle(f"Task1 Feature-MLP Evaluation | {split}")
|
float(plot_df["frequency_hz"].min()),
|
||||||
|
float(plot_df["frequency_hz"].max()),
|
||||||
|
config.train.dense_curve_points,
|
||||||
|
)
|
||||||
|
dense_x_rms = np.interp(dense_freq, plot_df["frequency_hz"], plot_df["x_rms"])
|
||||||
|
dense_true_rms = np.interp(dense_freq, plot_df["frequency_hz"], plot_df["true_rms"])
|
||||||
|
dense_features = np.column_stack(
|
||||||
|
[
|
||||||
|
dense_freq,
|
||||||
|
dense_freq**2,
|
||||||
|
dense_x_rms,
|
||||||
|
(2.0 * np.pi * dense_freq) ** 2 * config.data.harmonic_amplitude_m,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
dense_pred_tr = model.predict(dense_features).clip(min=config.data.normalization_eps)
|
||||||
|
dense_pred_rms = dense_pred_tr * dense_x_rms
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
"frequency_hz": dense_freq,
|
||||||
|
"x_rms_interp": dense_x_rms,
|
||||||
|
"true_rms_interp": dense_true_rms,
|
||||||
|
"pred_tr": dense_pred_tr,
|
||||||
|
"pred_rms": dense_pred_rms,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
axes[0].plot(plot_df["frequency_hz"], plot_df["true_rms"], marker="o", label="True RMS")
|
|
||||||
axes[0].plot(plot_df["frequency_hz"], plot_df["pred_rms"], marker="o", label="Pred RMS")
|
def save_plot(result_df: pd.DataFrame, dense_df: pd.DataFrame, figure_path: Path) -> None:
|
||||||
|
plot_df = result_df.sort_values("frequency_hz").reset_index(drop=True)
|
||||||
|
fig, axes = plt.subplots(2, 1, figsize=(12, 9))
|
||||||
|
fig.suptitle("Task1 Final Model Evaluation | All Harmonic Data")
|
||||||
|
|
||||||
|
axes[0].plot(dense_df["frequency_hz"], dense_df["true_rms_interp"], color="tab:blue", linewidth=2.0, label="True RMS Guide Curve")
|
||||||
|
axes[0].plot(dense_df["frequency_hz"], dense_df["pred_rms"], color="tab:orange", linewidth=2.0, label="Pred RMS Dense Curve")
|
||||||
|
axes[0].scatter(plot_df["frequency_hz"], plot_df["true_rms"], color="tab:blue", s=28, zorder=3, label="True RMS Samples")
|
||||||
|
axes[0].scatter(plot_df["frequency_hz"], plot_df["pred_rms"], color="tab:orange", s=22, zorder=3, label="Pred RMS Samples")
|
||||||
axes[0].set_xlabel("Frequency (Hz)")
|
axes[0].set_xlabel("Frequency (Hz)")
|
||||||
axes[0].set_ylabel("RMS")
|
axes[0].set_ylabel("RMS")
|
||||||
axes[0].grid(True, alpha=0.3)
|
axes[0].grid(True, alpha=0.3)
|
||||||
@@ -112,89 +83,44 @@ def save_all_samples_plot(result_df: pd.DataFrame, split: str, save_dir: Path) -
|
|||||||
axes[1].tick_params(axis="x", labelrotation=45)
|
axes[1].tick_params(axis="x", labelrotation=45)
|
||||||
|
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
figure_path = save_dir / f"evaluation_{split}_curve.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def save_single_sample_plot(raw_record, result: dict[str, float | str], split: str, save_dir: Path, sample_index: int) -> Path:
|
|
||||||
time_middle = raw_record.time_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
x_middle = raw_record.x_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
y_middle = raw_record.y_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
frequency_hz = float(result["frequency_hz"])
|
|
||||||
pred_rms = float(result["pred_rms"])
|
|
||||||
true_rms = float(result["true_rms"])
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
|
|
||||||
fig.suptitle(f"Task1 Feature-MLP | {split} | {result['file_name']}")
|
|
||||||
|
|
||||||
axes[0].plot(time_middle, x_middle, color="tab:blue")
|
|
||||||
axes[0].set_title("Input Base Excitation (Middle Segment)")
|
|
||||||
axes[0].set_xlabel("Time")
|
|
||||||
axes[0].set_ylabel("Acceleration")
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
axes[1].plot(time_middle, y_middle, color="tab:green")
|
|
||||||
axes[1].set_title("True Top Response (Middle Segment)")
|
|
||||||
axes[1].set_xlabel("Time")
|
|
||||||
axes[1].set_ylabel("Acceleration")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
axes[2].bar(["True RMS", "Pred RMS"], [true_rms, pred_rms], color=["tab:green", "tab:orange"])
|
|
||||||
axes[2].set_title(
|
|
||||||
f"Freq: {frequency_hz:.4f} Hz | True RMS: {true_rms:.6f} | Pred RMS: {pred_rms:.6f} | "
|
|
||||||
f"Error: {float(result['relative_error_percent']):.2f}%"
|
|
||||||
)
|
|
||||||
axes[2].set_ylabel("RMS")
|
|
||||||
axes[2].grid(True, axis="y", alpha=0.3)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
figure_path = save_dir / f"evaluation_{split}_s{sample_index}.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
config = make_experiment_config()
|
config = make_experiment_config()
|
||||||
device = resolve_device(args.device, config)
|
ckpt_path = resolve_checkpoint_path(config.data.project_root, args.checkpoint)
|
||||||
checkpoint_path = resolve_checkpoint_path(config, args.checkpoint)
|
with ckpt_path.open("rb") as handle:
|
||||||
if not checkpoint_path.exists():
|
payload = pickle.load(handle)
|
||||||
raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
|
model = payload["model"]
|
||||||
|
|
||||||
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
records, report = load_all_records(config)
|
||||||
if "normalization" in checkpoint:
|
print(report_to_text(report))
|
||||||
normalization = normalization_from_payload(checkpoint["normalization"])
|
data_df = records_to_frame(records)
|
||||||
else:
|
feature_matrix = data_df.loc[:, CORE_FEATURE_NAMES].to_numpy()
|
||||||
normalization = None
|
pred_tr = model.predict(feature_matrix).clip(min=config.data.normalization_eps)
|
||||||
|
pred_rms = pred_tr * data_df["x_rms"].to_numpy()
|
||||||
|
true_rms = data_df["y_rms"].to_numpy()
|
||||||
|
|
||||||
loaders, datasets, raw_records, reports = build_dataloaders(config)
|
result_df = data_df.copy()
|
||||||
del loaders
|
result_df["pred_tr"] = pred_tr
|
||||||
print(report_to_text(reports))
|
result_df["pred_rms"] = pred_rms
|
||||||
|
result_df["true_rms"] = true_rms
|
||||||
dataset = datasets[args.split]
|
result_df["relative_error_percent"] = [
|
||||||
if normalization is not None:
|
relative_error_percent(true, pred) for true, pred in zip(true_rms, pred_rms)
|
||||||
dataset.normalization = normalization
|
|
||||||
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
model.load_state_dict(checkpoint["model_state_dict"])
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
save_dir = config.data.project_root / "evaluation_outputs" / "task1_feature_mlp"
|
|
||||||
if args.all_samples:
|
|
||||||
rows = [
|
|
||||||
evaluate_sample(model, dataset, raw_records[args.split][index], index, device)
|
|
||||||
for index in range(len(dataset))
|
|
||||||
]
|
]
|
||||||
result_df = pd.DataFrame(rows).sort_values(["relative_error_percent", "file_name"]).reset_index(drop=True)
|
result_df = result_df.sort_values(["frequency_hz", "file_name"]).reset_index(drop=True)
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
csv_path = save_dir / f"evaluation_{args.split}_all_samples.csv"
|
save_dir = evaluation_dir(config)
|
||||||
|
csv_path = save_dir / "evaluation_all_samples.csv"
|
||||||
|
dense_csv_path = save_dir / config.train.evaluation_dense_csv_name
|
||||||
|
figure_path = save_dir / "evaluation_all_curve.png"
|
||||||
|
dense_df = build_dense_curve(model, result_df, config)
|
||||||
result_df.to_csv(csv_path, index=False)
|
result_df.to_csv(csv_path, index=False)
|
||||||
figure_path = save_all_samples_plot(result_df, args.split, save_dir)
|
dense_df.to_csv(dense_csv_path, index=False)
|
||||||
|
save_plot(result_df, dense_df, figure_path)
|
||||||
|
|
||||||
summary = {
|
summary = {
|
||||||
"count": len(result_df),
|
"count": len(result_df),
|
||||||
"mean_error_percent": float(result_df["relative_error_percent"].mean()),
|
"mean_error_percent": float(result_df["relative_error_percent"].mean()),
|
||||||
@@ -202,22 +128,13 @@ def main() -> None:
|
|||||||
"max_error_percent": float(result_df["relative_error_percent"].max()),
|
"max_error_percent": float(result_df["relative_error_percent"].max()),
|
||||||
"min_error_percent": float(result_df["relative_error_percent"].min()),
|
"min_error_percent": float(result_df["relative_error_percent"].min()),
|
||||||
}
|
}
|
||||||
print(f"Checkpoint: {checkpoint_path}")
|
print(f"Checkpoint: {ckpt_path}")
|
||||||
|
print(f"Model: {payload['model_name']}")
|
||||||
print(f"Summary: {summary}")
|
print(f"Summary: {summary}")
|
||||||
print(f"CSV saved to: {csv_path}")
|
print(f"CSV saved to: {csv_path}")
|
||||||
if figure_path is not None:
|
print(f"Dense curve CSV saved to: {dense_csv_path}")
|
||||||
print(f"Figure saved to: {figure_path}")
|
|
||||||
print(result_df.to_string(index=False))
|
|
||||||
return
|
|
||||||
|
|
||||||
result = evaluate_sample(model, dataset, raw_records[args.split][args.sample_index], args.sample_index, device)
|
|
||||||
figure_path = save_single_sample_plot(raw_records[args.split][args.sample_index], result, args.split, save_dir, args.sample_index)
|
|
||||||
print(f"Checkpoint: {checkpoint_path}")
|
|
||||||
print(f"Sample file: {result['file_name']}")
|
|
||||||
print(f"True RMS: {float(result['true_rms']):.6f}")
|
|
||||||
print(f"Pred RMS: {float(result['pred_rms']):.6f}")
|
|
||||||
print(f"Relative RMS Error (%): {float(result['relative_error_percent']):.4f}")
|
|
||||||
print(f"Figure saved to: {figure_path}")
|
print(f"Figure saved to: {figure_path}")
|
||||||
|
print(result_df[["file_name", "frequency_hz", "true_rms", "pred_rms", "relative_error_percent"]].to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from torch import nn
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, ModelConfig
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, ModelConfig
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureMLP(nn.Module):
|
|
||||||
def __init__(self, config: ModelConfig) -> None:
|
|
||||||
super().__init__()
|
|
||||||
layers: list[nn.Module] = []
|
|
||||||
input_dim = config.input_dim
|
|
||||||
for hidden_dim in config.hidden_dims:
|
|
||||||
layers.append(nn.Linear(input_dim, hidden_dim))
|
|
||||||
layers.append(nn.GELU())
|
|
||||||
layers.append(nn.Dropout(config.dropout))
|
|
||||||
input_dim = hidden_dim
|
|
||||||
layers.append(nn.Linear(input_dim, 1))
|
|
||||||
self.network = nn.Sequential(*layers)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
return self.network(x)
|
|
||||||
|
|
||||||
|
|
||||||
def build_model(config: ExperimentConfig | ModelConfig) -> FeatureMLP:
|
|
||||||
model_config = config.model if isinstance(config, ExperimentConfig) else config
|
|
||||||
return FeatureMLP(model_config)
|
|
||||||
@@ -1,60 +1,44 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import pickle
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .config import CORE_FEATURE_NAMES, make_experiment_config
|
from .config import CORE_FEATURE_NAMES, evaluation_dir, make_experiment_config
|
||||||
from .dataset import build_record_from_file, normalization_from_payload
|
from .dataset import build_record_from_file
|
||||||
from .model import build_model
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from config import CORE_FEATURE_NAMES, make_experiment_config
|
from config import CORE_FEATURE_NAMES, evaluation_dir, make_experiment_config
|
||||||
from dataset import build_record_from_file, normalization_from_payload
|
from dataset import build_record_from_file
|
||||||
from model import build_model
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Predict task1 RMS from a single waveform CSV.")
|
parser = argparse.ArgumentParser(description="Predict task1 RMS from a single harmonic waveform CSV.")
|
||||||
parser.add_argument("--file", type=str, required=True)
|
parser.add_argument("--file", type=str, required=True)
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
parser.add_argument("--checkpoint", type=str, default=None)
|
parser.add_argument("--checkpoint", type=str, default=None)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str | None, default_device: str) -> torch.device:
|
def resolve_checkpoint_path(project_root: Path, checkpoint_arg: str | None) -> Path:
|
||||||
requested = device_name or default_device
|
|
||||||
if requested.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(requested)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_path(project_root: Path, checkpoint_dir: str, best_model_name: str, checkpoint_arg: str | None) -> Path:
|
|
||||||
if checkpoint_arg:
|
if checkpoint_arg:
|
||||||
return Path(checkpoint_arg).resolve()
|
return Path(checkpoint_arg).resolve()
|
||||||
return project_root / checkpoint_dir / "task1_feature_mlp" / best_model_name
|
return project_root / "checkpoints_final" / "task1_final" / "task1_final_model.pkl"
|
||||||
|
|
||||||
|
|
||||||
def save_prediction_figure(
|
def save_prediction_figure(file_path: Path, record, pred_rms: float, output_dir: Path) -> Path:
|
||||||
file_path: Path,
|
time_middle = record.time_middle.reshape(-1)
|
||||||
record,
|
x_middle = record.x_middle.reshape(-1)
|
||||||
pred_rms: float,
|
y_middle = record.y_middle.reshape(-1)
|
||||||
true_rms: float | None,
|
|
||||||
output_dir: Path,
|
|
||||||
) -> Path:
|
|
||||||
time_middle = record.time_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
x_middle = record.x_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
y_middle = record.y_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
sampling_rate = record.sampling_rate
|
sampling_rate = record.sampling_rate
|
||||||
fft_values = np.fft.rfft(x_middle)
|
fft_values = np.fft.rfft(x_middle - np.mean(x_middle))
|
||||||
freqs = np.fft.rfftfreq(x_middle.size, d=1.0 / sampling_rate)
|
freqs = np.fft.rfftfreq(x_middle.size, d=1.0 / sampling_rate)
|
||||||
amplitudes = np.abs(fft_values)
|
amplitudes = np.abs(fft_values)
|
||||||
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
|
fig, axes = plt.subplots(4, 1, figsize=(12, 12))
|
||||||
fig.suptitle(f"Task1 Single-File Prediction | {file_path.name}")
|
fig.suptitle(f"Task1 Final Single-File Prediction | {file_path.name}")
|
||||||
|
|
||||||
axes[0].plot(time_middle, x_middle, color="tab:blue")
|
axes[0].plot(time_middle, x_middle, color="tab:blue")
|
||||||
axes[0].set_title("Input Base Excitation (Middle Segment)")
|
axes[0].set_title("Input Base Excitation (Middle Segment)")
|
||||||
@@ -62,26 +46,28 @@ def save_prediction_figure(
|
|||||||
axes[0].set_ylabel("Acceleration")
|
axes[0].set_ylabel("Acceleration")
|
||||||
axes[0].grid(True, alpha=0.3)
|
axes[0].grid(True, alpha=0.3)
|
||||||
|
|
||||||
axes[1].plot(freqs, amplitudes, color="tab:purple")
|
axes[1].plot(time_middle, y_middle, color="tab:green")
|
||||||
axes[1].axvline(record.frequency_hz, color="tab:red", linestyle="--", label=f"Dominant freq = {record.frequency_hz:.4f} Hz")
|
axes[1].set_title("True Top Response (Middle Segment)")
|
||||||
axes[1].set_xlim(0.0, 5.0)
|
axes[1].set_xlabel("Time")
|
||||||
axes[1].set_title("Input Spectrum")
|
axes[1].set_ylabel("Acceleration")
|
||||||
axes[1].set_xlabel("Frequency (Hz)")
|
|
||||||
axes[1].set_ylabel("Amplitude")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
axes[1].grid(True, alpha=0.3)
|
||||||
axes[1].legend()
|
|
||||||
|
|
||||||
labels = ["Pred RMS"] if true_rms is None else ["True RMS", "Pred RMS"]
|
axes[2].plot(freqs, amplitudes, color="tab:purple")
|
||||||
values = [pred_rms] if true_rms is None else [true_rms, pred_rms]
|
axes[2].axvline(record.frequency_hz, color="tab:red", linestyle="--", label=f"Frequency = {record.frequency_hz:.4f} Hz")
|
||||||
colors = ["tab:orange"] if true_rms is None else ["tab:green", "tab:orange"]
|
axes[2].set_xlim(0.0, 5.0)
|
||||||
axes[2].bar(labels, values, color=colors)
|
axes[2].set_title("Input Spectrum")
|
||||||
title = f"Predicted RMS = {pred_rms:.6f}"
|
axes[2].set_xlabel("Frequency (Hz)")
|
||||||
if true_rms is not None:
|
axes[2].set_ylabel("Amplitude")
|
||||||
error_percent = abs(pred_rms - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
axes[2].grid(True, alpha=0.3)
|
||||||
title = f"True RMS = {true_rms:.6f} | Pred RMS = {pred_rms:.6f} | Error = {error_percent:.2f}%"
|
axes[2].legend()
|
||||||
axes[2].set_title(title)
|
|
||||||
axes[2].set_ylabel("RMS")
|
axes[3].bar(["True RMS", "Pred RMS"], [record.y_rms, pred_rms], color=["tab:green", "tab:orange"])
|
||||||
axes[2].grid(True, axis="y", alpha=0.3)
|
error_percent = abs(pred_rms - record.y_rms) / max(abs(record.y_rms), 1e-12) * 100.0
|
||||||
|
axes[3].set_title(
|
||||||
|
f"True RMS = {record.y_rms:.6f} | Pred RMS = {pred_rms:.6f} | Error = {error_percent:.2f}%"
|
||||||
|
)
|
||||||
|
axes[3].set_ylabel("RMS")
|
||||||
|
axes[3].grid(True, axis="y", alpha=0.3)
|
||||||
|
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -94,49 +80,29 @@ def save_prediction_figure(
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
config = make_experiment_config()
|
config = make_experiment_config()
|
||||||
device = resolve_device(args.device, config.train.device)
|
ckpt_path = resolve_checkpoint_path(config.data.project_root, args.checkpoint)
|
||||||
checkpoint_path = resolve_checkpoint_path(
|
with ckpt_path.open("rb") as handle:
|
||||||
config.data.project_root,
|
payload = pickle.load(handle)
|
||||||
config.train.checkpoint_dir,
|
model = payload["model"]
|
||||||
config.train.best_model_name,
|
|
||||||
args.checkpoint,
|
|
||||||
)
|
|
||||||
if not checkpoint_path.exists():
|
|
||||||
raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
|
|
||||||
|
|
||||||
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
|
||||||
normalization = normalization_from_payload(checkpoint["normalization"])
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
model.load_state_dict(checkpoint["model_state_dict"])
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
file_path = Path(args.file).resolve()
|
file_path = Path(args.file).resolve()
|
||||||
record = build_record_from_file(file_path, split="predict", config=config.data)
|
record = build_record_from_file(file_path, config.data)
|
||||||
feature_norm = ((record.features - normalization.feature_mean) / normalization.feature_std).unsqueeze(0).to(device)
|
pred_tr = max(float(model.predict([record.features.tolist()])[0]), config.data.normalization_eps)
|
||||||
x_rms_raw = record.x_rms.unsqueeze(0).to(device)
|
pred_rms = pred_tr * record.x_rms
|
||||||
target_mean = normalization.target_mean.to(device).view(1, 1)
|
|
||||||
target_std = normalization.target_std.to(device).view(1, 1)
|
|
||||||
|
|
||||||
with torch.no_grad():
|
out_dir = evaluation_dir(config)
|
||||||
pred_norm = model(feature_norm)
|
fig_path = save_prediction_figure(file_path, record, pred_rms, out_dir)
|
||||||
pred_tr = torch.clamp(pred_norm * target_std + target_mean, min=1e-6)
|
|
||||||
pred_rms = float((pred_tr * x_rms_raw).detach().cpu().numpy().reshape(-1)[0])
|
|
||||||
|
|
||||||
true_rms = float(record.y_rms.item()) if record.y_rms.numel() > 0 else None
|
print(f"Checkpoint: {ckpt_path}")
|
||||||
output_dir = config.data.project_root / "evaluation_outputs" / "task1_feature_mlp"
|
print(f"Model: {payload['model_name']}")
|
||||||
figure_path = save_prediction_figure(file_path, record, pred_rms, true_rms, output_dir)
|
|
||||||
|
|
||||||
print(f"Checkpoint: {checkpoint_path}")
|
|
||||||
print(f"Input file: {file_path}")
|
print(f"Input file: {file_path}")
|
||||||
print(f"Extracted frequency (Hz): {record.frequency_hz:.6f}")
|
|
||||||
for feature_name, feature_value in zip(CORE_FEATURE_NAMES, record.features.tolist()):
|
for feature_name, feature_value in zip(CORE_FEATURE_NAMES, record.features.tolist()):
|
||||||
print(f"{feature_name}: {feature_value:.6f}")
|
print(f"{feature_name}: {feature_value:.6f}")
|
||||||
|
print(f"Predicted TR: {pred_tr:.6f}")
|
||||||
print(f"Predicted RMS: {pred_rms:.6f}")
|
print(f"Predicted RMS: {pred_rms:.6f}")
|
||||||
if true_rms is not None:
|
print(f"True RMS: {record.y_rms:.6f}")
|
||||||
error_percent = abs(pred_rms - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
print(f"Relative RMS Error (%): {abs(pred_rms - record.y_rms) / max(abs(record.y_rms), 1e-12) * 100.0:.4f}")
|
||||||
print(f"True RMS: {true_rms:.6f}")
|
print(f"Figure saved to: {fig_path}")
|
||||||
print(f"Relative RMS Error (%): {error_percent:.4f}")
|
|
||||||
print(f"Figure saved to: {figure_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
274
scripts/train.py
@@ -1,274 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import random
|
|
||||||
from dataclasses import asdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
|
||||||
import torch.nn.functional as F
|
|
||||||
from torch import nn
|
|
||||||
from torch.optim import AdamW
|
|
||||||
from torch.optim.lr_scheduler import ReduceLROnPlateau
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, make_experiment_config
|
|
||||||
from .dataset import build_dataloaders, checkpoint_normalization_payload, report_to_text
|
|
||||||
from .model import build_model
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, make_experiment_config
|
|
||||||
from dataset import build_dataloaders, checkpoint_normalization_payload, report_to_text
|
|
||||||
from model import build_model
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Train feature-based MLP for task1 RMS prediction.")
|
|
||||||
parser.add_argument("--epochs", type=int, default=None)
|
|
||||||
parser.add_argument("--batch-size", type=int, default=None)
|
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def set_seed(seed: int) -> None:
|
|
||||||
random.seed(seed)
|
|
||||||
torch.manual_seed(seed)
|
|
||||||
torch.cuda.manual_seed_all(seed)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str) -> torch.device:
|
|
||||||
if device_name.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(device_name)
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_for_checkpoint(value: Any) -> Any:
|
|
||||||
if isinstance(value, Path):
|
|
||||||
return str(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {key: serialize_for_checkpoint(sub_value) for key, sub_value in value.items()}
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def denormalize_target(pred_norm: torch.Tensor, target_mean: torch.Tensor, target_std: torch.Tensor) -> torch.Tensor:
|
|
||||||
return pred_norm * target_std + target_mean
|
|
||||||
|
|
||||||
|
|
||||||
def relative_rms_error(pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
|
||||||
return torch.abs(pred - target) / torch.clamp(target.abs(), min=1e-6)
|
|
||||||
|
|
||||||
|
|
||||||
class RMSLoss(nn.Module):
|
|
||||||
def __init__(self, relative_rms_weight: float, log_rms_huber_weight: float, mae_weight: float) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.relative_rms_weight = relative_rms_weight
|
|
||||||
self.log_rms_huber_weight = log_rms_huber_weight
|
|
||||||
self.mae_weight = mae_weight
|
|
||||||
|
|
||||||
def forward(
|
|
||||||
self,
|
|
||||||
pred_tr: torch.Tensor,
|
|
||||||
target_tr: torch.Tensor,
|
|
||||||
target_log_tr: torch.Tensor,
|
|
||||||
) -> dict[str, torch.Tensor]:
|
|
||||||
relative_loss = relative_rms_error(pred_tr, target_tr).mean()
|
|
||||||
log_huber_loss = F.huber_loss(torch.log(torch.clamp(pred_tr, min=1e-6)), target_log_tr)
|
|
||||||
mae_loss = F.l1_loss(pred_tr, target_tr)
|
|
||||||
total = (
|
|
||||||
self.relative_rms_weight * relative_loss
|
|
||||||
+ self.log_rms_huber_weight * log_huber_loss
|
|
||||||
+ self.mae_weight * mae_loss
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"total": total,
|
|
||||||
"relative": relative_loss,
|
|
||||||
"log_huber": log_huber_loss,
|
|
||||||
"mae": mae_loss,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def run_epoch(
|
|
||||||
model: nn.Module,
|
|
||||||
dataloader: torch.utils.data.DataLoader,
|
|
||||||
optimizer: AdamW | None,
|
|
||||||
criterion: RMSLoss,
|
|
||||||
target_mean: torch.Tensor,
|
|
||||||
target_std: torch.Tensor,
|
|
||||||
device: torch.device,
|
|
||||||
grad_clip_norm: float,
|
|
||||||
) -> dict[str, float]:
|
|
||||||
is_train = optimizer is not None
|
|
||||||
model.train(is_train)
|
|
||||||
|
|
||||||
total_loss_sum = 0.0
|
|
||||||
relative_loss_sum = 0.0
|
|
||||||
log_huber_sum = 0.0
|
|
||||||
mae_loss_sum = 0.0
|
|
||||||
rms_error_sum = 0.0
|
|
||||||
sample_count = 0
|
|
||||||
|
|
||||||
for batch in dataloader:
|
|
||||||
x = batch["x"].to(device)
|
|
||||||
target_tr = batch["target_raw"].to(device)
|
|
||||||
target_log_tr = batch["target_log_raw"].to(device)
|
|
||||||
x_rms_raw = batch["x_rms_raw"].to(device)
|
|
||||||
y_rms_raw = batch["y_rms_raw"].to(device)
|
|
||||||
|
|
||||||
if is_train:
|
|
||||||
optimizer.zero_grad(set_to_none=True)
|
|
||||||
|
|
||||||
pred_norm = model(x)
|
|
||||||
pred_tr = torch.clamp(denormalize_target(pred_norm, target_mean, target_std), min=1e-6)
|
|
||||||
pred_rms = pred_tr * x_rms_raw
|
|
||||||
losses = criterion(pred_tr, target_tr, target_log_tr)
|
|
||||||
|
|
||||||
if is_train:
|
|
||||||
losses["total"].backward()
|
|
||||||
torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip_norm)
|
|
||||||
optimizer.step()
|
|
||||||
|
|
||||||
batch_size = x.shape[0]
|
|
||||||
total_loss_sum += losses["total"].detach().item() * batch_size
|
|
||||||
relative_loss_sum += losses["relative"].detach().item() * batch_size
|
|
||||||
log_huber_sum += losses["log_huber"].detach().item() * batch_size
|
|
||||||
mae_loss_sum += losses["mae"].detach().item() * batch_size
|
|
||||||
rms_error_sum += relative_rms_error(pred_rms.detach(), y_rms_raw.detach()).mean().item() * batch_size
|
|
||||||
sample_count += batch_size
|
|
||||||
|
|
||||||
return {
|
|
||||||
"loss": total_loss_sum / sample_count,
|
|
||||||
"relative_loss": relative_loss_sum / sample_count,
|
|
||||||
"log_huber_loss": log_huber_sum / sample_count,
|
|
||||||
"mae_loss": mae_loss_sum / sample_count,
|
|
||||||
"rms_error": rms_error_sum / sample_count,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def checkpoint_paths(config: ExperimentConfig) -> tuple[Path, Path]:
|
|
||||||
root = config.data.project_root / config.train.checkpoint_dir / "task1_feature_mlp"
|
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
|
||||||
return root / config.train.best_model_name, root / config.train.history_name
|
|
||||||
|
|
||||||
|
|
||||||
def train_model(config: ExperimentConfig) -> None:
|
|
||||||
set_seed(config.train.seed)
|
|
||||||
device = resolve_device(config.train.device)
|
|
||||||
loaders, datasets, raw_records, reports = build_dataloaders(config)
|
|
||||||
del raw_records
|
|
||||||
normalization = datasets["train"].normalization
|
|
||||||
target_mean = normalization.target_mean.to(device).view(1, 1)
|
|
||||||
target_std = normalization.target_std.to(device).view(1, 1)
|
|
||||||
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
optimizer = AdamW(model.parameters(), lr=config.train.learning_rate, weight_decay=config.train.weight_decay)
|
|
||||||
scheduler = ReduceLROnPlateau(
|
|
||||||
optimizer,
|
|
||||||
mode="min",
|
|
||||||
factor=config.train.lr_scheduler_factor,
|
|
||||||
patience=config.train.lr_scheduler_patience,
|
|
||||||
min_lr=config.train.min_learning_rate,
|
|
||||||
)
|
|
||||||
criterion = RMSLoss(
|
|
||||||
relative_rms_weight=config.loss.relative_rms_weight,
|
|
||||||
log_rms_huber_weight=config.loss.log_rms_huber_weight,
|
|
||||||
mae_weight=config.loss.mae_weight,
|
|
||||||
)
|
|
||||||
|
|
||||||
best_val_error = float("inf")
|
|
||||||
epochs_without_improvement = 0
|
|
||||||
history: list[dict[str, float]] = []
|
|
||||||
best_model_path, history_path = checkpoint_paths(config)
|
|
||||||
|
|
||||||
print(f"Device: {device}")
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
for epoch in range(1, config.train.epochs + 1):
|
|
||||||
train_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=loaders["train"],
|
|
||||||
optimizer=optimizer,
|
|
||||||
criterion=criterion,
|
|
||||||
target_mean=target_mean,
|
|
||||||
target_std=target_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
)
|
|
||||||
val_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=loaders["val"],
|
|
||||||
optimizer=None,
|
|
||||||
criterion=criterion,
|
|
||||||
target_mean=target_mean,
|
|
||||||
target_std=target_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
)
|
|
||||||
scheduler.step(val_metrics["rms_error"])
|
|
||||||
current_lr = optimizer.param_groups[0]["lr"]
|
|
||||||
|
|
||||||
history_row = {
|
|
||||||
"epoch": epoch,
|
|
||||||
"lr": current_lr,
|
|
||||||
"train_loss": train_metrics["loss"],
|
|
||||||
"train_relative_loss": train_metrics["relative_loss"],
|
|
||||||
"train_log_rms_huber_loss": train_metrics["log_huber_loss"],
|
|
||||||
"train_mae_loss": train_metrics["mae_loss"],
|
|
||||||
"train_rms_error": train_metrics["rms_error"],
|
|
||||||
"val_loss": val_metrics["loss"],
|
|
||||||
"val_relative_loss": val_metrics["relative_loss"],
|
|
||||||
"val_log_rms_huber_loss": val_metrics["log_huber_loss"],
|
|
||||||
"val_mae_loss": val_metrics["mae_loss"],
|
|
||||||
"val_rms_error": val_metrics["rms_error"],
|
|
||||||
}
|
|
||||||
history.append(history_row)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Epoch {epoch:03d} | train_loss={train_metrics['loss']:.6f} | "
|
|
||||||
f"val_loss={val_metrics['loss']:.6f} | val_rms_error={val_metrics['rms_error']:.6f} | lr={current_lr:.2e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if val_metrics["rms_error"] < best_val_error:
|
|
||||||
best_val_error = val_metrics["rms_error"]
|
|
||||||
epochs_without_improvement = 0
|
|
||||||
torch.save(
|
|
||||||
{
|
|
||||||
"epoch": epoch,
|
|
||||||
"model_state_dict": model.state_dict(),
|
|
||||||
"optimizer_state_dict": optimizer.state_dict(),
|
|
||||||
"best_val_rms_error": best_val_error,
|
|
||||||
"config": serialize_for_checkpoint(asdict(config)),
|
|
||||||
"normalization": checkpoint_normalization_payload(normalization),
|
|
||||||
},
|
|
||||||
best_model_path,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
epochs_without_improvement += 1
|
|
||||||
if epochs_without_improvement >= config.train.early_stop_patience:
|
|
||||||
print(f"Early stopping triggered after {epoch} epochs.")
|
|
||||||
break
|
|
||||||
|
|
||||||
history_df = pd.DataFrame(history)
|
|
||||||
history_df.to_csv(history_path, index=False)
|
|
||||||
print(f"Best model saved to: {best_model_path}")
|
|
||||||
print(f"Training history saved to: {history_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_experiment_config()
|
|
||||||
if args.epochs is not None:
|
|
||||||
config.train.epochs = args.epochs
|
|
||||||
if args.batch_size is not None:
|
|
||||||
config.train.batch_size = args.batch_size
|
|
||||||
if args.device is not None:
|
|
||||||
config.train.device = args.device
|
|
||||||
train_model(config)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
178
scripts/train_final.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pickle
|
||||||
|
from dataclasses import asdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from sklearn.neighbors import KNeighborsRegressor
|
||||||
|
from sklearn.pipeline import Pipeline
|
||||||
|
from sklearn.preprocessing import StandardScaler
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .config import CORE_FEATURE_NAMES, ExperimentConfig, checkpoint_dir, evaluation_dir, make_experiment_config
|
||||||
|
from .dataset import load_all_records, records_to_frame, report_to_text
|
||||||
|
except ImportError:
|
||||||
|
from config import CORE_FEATURE_NAMES, ExperimentConfig, checkpoint_dir, evaluation_dir, make_experiment_config
|
||||||
|
from dataset import load_all_records, records_to_frame, report_to_text
|
||||||
|
|
||||||
|
|
||||||
|
def build_final_model(config: ExperimentConfig) -> Pipeline:
|
||||||
|
return Pipeline(
|
||||||
|
[
|
||||||
|
("scaler", StandardScaler()),
|
||||||
|
(
|
||||||
|
"model",
|
||||||
|
KNeighborsRegressor(
|
||||||
|
n_neighbors=config.model.n_neighbors,
|
||||||
|
weights="distance",
|
||||||
|
p=config.model.distance_power,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_for_checkpoint(value: Any) -> Any:
|
||||||
|
if isinstance(value, Path):
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {key: serialize_for_checkpoint(sub_value) for key, sub_value in value.items()}
|
||||||
|
if isinstance(value, tuple):
|
||||||
|
return [serialize_for_checkpoint(item) for item in value]
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [serialize_for_checkpoint(item) for item in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def relative_error_percent(true_value: float, pred_value: float) -> float:
|
||||||
|
return abs(pred_value - true_value) / max(abs(true_value), 1e-12) * 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def build_dense_curve(model: Pipeline, result_df: pd.DataFrame, config: ExperimentConfig) -> pd.DataFrame:
|
||||||
|
plot_df = result_df.sort_values("frequency_hz").reset_index(drop=True)
|
||||||
|
dense_freq = np.linspace(
|
||||||
|
float(plot_df["frequency_hz"].min()),
|
||||||
|
float(plot_df["frequency_hz"].max()),
|
||||||
|
config.train.dense_curve_points,
|
||||||
|
)
|
||||||
|
dense_x_rms = np.interp(dense_freq, plot_df["frequency_hz"], plot_df["x_rms"])
|
||||||
|
dense_true_rms = np.interp(dense_freq, plot_df["frequency_hz"], plot_df["true_rms"])
|
||||||
|
dense_features = np.column_stack(
|
||||||
|
[
|
||||||
|
dense_freq,
|
||||||
|
dense_freq**2,
|
||||||
|
dense_x_rms,
|
||||||
|
(2.0 * np.pi * dense_freq) ** 2 * config.data.harmonic_amplitude_m,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
dense_pred_tr = model.predict(dense_features).clip(min=config.data.normalization_eps)
|
||||||
|
dense_pred_rms = dense_pred_tr * dense_x_rms
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
"frequency_hz": dense_freq,
|
||||||
|
"x_rms_interp": dense_x_rms,
|
||||||
|
"true_rms_interp": dense_true_rms,
|
||||||
|
"pred_tr": dense_pred_tr,
|
||||||
|
"pred_rms": dense_pred_rms,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_fit_plot(result_df: pd.DataFrame, dense_df: pd.DataFrame, figure_path: Path) -> None:
|
||||||
|
plot_df = result_df.sort_values("frequency_hz").reset_index(drop=True)
|
||||||
|
fig, axes = plt.subplots(2, 1, figsize=(12, 9))
|
||||||
|
fig.suptitle("Task1 Final Model | All Harmonic Data Fit")
|
||||||
|
|
||||||
|
axes[0].plot(dense_df["frequency_hz"], dense_df["true_rms_interp"], color="tab:blue", linewidth=2.0, label="True RMS Guide Curve")
|
||||||
|
axes[0].plot(dense_df["frequency_hz"], dense_df["pred_rms"], color="tab:orange", linewidth=2.0, label="Pred RMS Dense Curve")
|
||||||
|
axes[0].scatter(plot_df["frequency_hz"], plot_df["true_rms"], color="tab:blue", s=28, zorder=3, label="True RMS Samples")
|
||||||
|
axes[0].scatter(plot_df["frequency_hz"], plot_df["pred_rms"], color="tab:orange", s=22, zorder=3, label="Pred RMS Samples")
|
||||||
|
axes[0].set_xlabel("Frequency (Hz)")
|
||||||
|
axes[0].set_ylabel("RMS")
|
||||||
|
axes[0].grid(True, alpha=0.3)
|
||||||
|
axes[0].legend()
|
||||||
|
|
||||||
|
axes[1].bar(plot_df["frequency_hz"].astype(str), plot_df["relative_error_percent"], color="tab:orange")
|
||||||
|
axes[1].set_xlabel("Frequency (Hz)")
|
||||||
|
axes[1].set_ylabel("Relative Error (%)")
|
||||||
|
axes[1].grid(True, axis="y", alpha=0.3)
|
||||||
|
axes[1].tick_params(axis="x", labelrotation=45)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
|
def train_final() -> None:
|
||||||
|
config = make_experiment_config()
|
||||||
|
records, report = load_all_records(config)
|
||||||
|
print(report_to_text(report))
|
||||||
|
|
||||||
|
data_df = records_to_frame(records)
|
||||||
|
feature_matrix = data_df.loc[:, CORE_FEATURE_NAMES].to_numpy()
|
||||||
|
target_tr = data_df["target_tr"].to_numpy()
|
||||||
|
|
||||||
|
model = build_final_model(config)
|
||||||
|
model.fit(feature_matrix, target_tr)
|
||||||
|
|
||||||
|
pred_tr = model.predict(feature_matrix).clip(min=config.data.normalization_eps)
|
||||||
|
pred_rms = pred_tr * data_df["x_rms"].to_numpy()
|
||||||
|
true_rms = data_df["y_rms"].to_numpy()
|
||||||
|
|
||||||
|
result_df = data_df.copy()
|
||||||
|
result_df["pred_tr"] = pred_tr
|
||||||
|
result_df["pred_rms"] = pred_rms
|
||||||
|
result_df["true_rms"] = true_rms
|
||||||
|
result_df["relative_error_percent"] = [
|
||||||
|
relative_error_percent(true, pred) for true, pred in zip(true_rms, pred_rms)
|
||||||
|
]
|
||||||
|
result_df = result_df.sort_values(["frequency_hz", "file_name"]).reset_index(drop=True)
|
||||||
|
|
||||||
|
mean_error = float(result_df["relative_error_percent"].mean())
|
||||||
|
median_error = float(result_df["relative_error_percent"].median())
|
||||||
|
max_error = float(result_df["relative_error_percent"].max())
|
||||||
|
|
||||||
|
ckpt_dir = checkpoint_dir(config)
|
||||||
|
eval_dir = evaluation_dir(config)
|
||||||
|
csv_path = eval_dir / config.train.fit_csv_name
|
||||||
|
fig_path = eval_dir / config.train.fit_figure_name
|
||||||
|
dense_csv_path = eval_dir / config.train.dense_curve_csv_name
|
||||||
|
model_path = ckpt_dir / config.train.model_name
|
||||||
|
|
||||||
|
dense_df = build_dense_curve(model, result_df, config)
|
||||||
|
result_df.to_csv(csv_path, index=False)
|
||||||
|
dense_df.to_csv(dense_csv_path, index=False)
|
||||||
|
save_fit_plot(result_df, dense_df, fig_path)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model_name": config.model.model_name,
|
||||||
|
"model": model,
|
||||||
|
"config": serialize_for_checkpoint(asdict(config)),
|
||||||
|
"feature_names": list(CORE_FEATURE_NAMES),
|
||||||
|
"summary": {
|
||||||
|
"count": len(result_df),
|
||||||
|
"mean_error_percent": mean_error,
|
||||||
|
"median_error_percent": median_error,
|
||||||
|
"max_error_percent": max_error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with model_path.open("wb") as handle:
|
||||||
|
pickle.dump(payload, handle)
|
||||||
|
|
||||||
|
print(f"Final model saved to: {model_path}")
|
||||||
|
print(f"Fit CSV saved to: {csv_path}")
|
||||||
|
print(f"Dense curve CSV saved to: {dense_csv_path}")
|
||||||
|
print(f"Fit figure saved to: {fig_path}")
|
||||||
|
print(
|
||||||
|
f"All-data self-fit summary | count={len(result_df)} | "
|
||||||
|
f"mean={mean_error:.4f}% | median={median_error:.4f}% | max={max_error:.4f}%"
|
||||||
|
)
|
||||||
|
print(result_df[["file_name", "frequency_hz", "true_rms", "pred_rms", "relative_error_percent"]].to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
train_final()
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(PROJECT_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
|
||||||
|
|
||||||
from scripts.config import CORE_FEATURE_NAMES, DataConfig
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TreeModelConfig:
|
|
||||||
candidate_models: tuple[str, ...] = ("extra_trees", "random_forest", "gradient_boosting")
|
|
||||||
random_state: int = 42
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TrainConfig:
|
|
||||||
checkpoint_dir: str = "checkpoints_tree"
|
|
||||||
summary_name: str = "model_selection.csv"
|
|
||||||
best_model_name: str = "best_tr_tree.pkl"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ExperimentConfig:
|
|
||||||
data: DataConfig = field(default_factory=DataConfig)
|
|
||||||
model: TreeModelConfig = field(default_factory=TreeModelConfig)
|
|
||||||
train: TrainConfig = field(default_factory=TrainConfig)
|
|
||||||
|
|
||||||
|
|
||||||
def make_experiment_config() -> ExperimentConfig:
|
|
||||||
return ExperimentConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def checkpoint_dir(config: ExperimentConfig) -> Path:
|
|
||||||
path = config.data.project_root / config.train.checkpoint_dir / "task1_tr_tree"
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def evaluation_dir(config: ExperimentConfig) -> Path:
|
|
||||||
path = config.data.project_root / "evaluation_outputs" / "task1_tr_tree"
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
FEATURE_NAMES = CORE_FEATURE_NAMES
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import pickle
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(PROJECT_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
|
||||||
|
|
||||||
from scripts.dataset import build_datasets, report_to_text
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import FEATURE_NAMES, evaluation_dir, make_experiment_config
|
|
||||||
except ImportError:
|
|
||||||
from config import FEATURE_NAMES, evaluation_dir, make_experiment_config
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Evaluate tree model on train/val/test splits.")
|
|
||||||
parser.add_argument("--split", choices=("train", "val", "test"), default="val")
|
|
||||||
parser.add_argument("--sample-index", type=int, default=0)
|
|
||||||
parser.add_argument("--all-samples", action="store_true")
|
|
||||||
parser.add_argument("--checkpoint", type=str, default=None)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_path(project_root: Path, checkpoint_arg: str | None) -> Path:
|
|
||||||
if checkpoint_arg:
|
|
||||||
return Path(checkpoint_arg).resolve()
|
|
||||||
return project_root / "checkpoints_tree" / "task1_tr_tree" / "best_tr_tree.pkl"
|
|
||||||
|
|
||||||
|
|
||||||
def relative_percent_error(true_value: float, pred_value: float) -> float:
|
|
||||||
return abs(pred_value - true_value) / max(abs(true_value), 1e-12) * 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_record(model, record) -> dict[str, float | str]:
|
|
||||||
feature_matrix = [record.features.tolist()]
|
|
||||||
pred_tr = max(float(model.predict(feature_matrix)[0]), 1e-6)
|
|
||||||
x_rms = float(record.x_rms.item())
|
|
||||||
y_rms = float(record.y_rms.item())
|
|
||||||
pred_rms = pred_tr * x_rms
|
|
||||||
row = {
|
|
||||||
"file_name": record.file_path.name,
|
|
||||||
"frequency_hz": float(record.frequency_hz),
|
|
||||||
"true_rms": y_rms,
|
|
||||||
"pred_rms": pred_rms,
|
|
||||||
"relative_error_percent": relative_percent_error(y_rms, pred_rms),
|
|
||||||
"x_rms": x_rms,
|
|
||||||
"pred_tr": pred_tr,
|
|
||||||
"true_tr": float(record.target_y_rms.item()),
|
|
||||||
}
|
|
||||||
for name, value in zip(FEATURE_NAMES, record.features.tolist()):
|
|
||||||
row[name] = float(value)
|
|
||||||
row["sampling_rate"] = float(record.sampling_rate)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def save_all_samples_plot(result_df: pd.DataFrame, split: str, save_dir: Path) -> Path | None:
|
|
||||||
if result_df["frequency_hz"].isna().any():
|
|
||||||
return None
|
|
||||||
plot_df = result_df.sort_values("frequency_hz").reset_index(drop=True)
|
|
||||||
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
|
|
||||||
fig.suptitle(f"Task1 TR Tree Evaluation | {split}")
|
|
||||||
|
|
||||||
axes[0].plot(plot_df["frequency_hz"], plot_df["true_rms"], marker="o", label="True RMS")
|
|
||||||
axes[0].plot(plot_df["frequency_hz"], plot_df["pred_rms"], marker="o", label="Pred RMS")
|
|
||||||
axes[0].set_xlabel("Frequency (Hz)")
|
|
||||||
axes[0].set_ylabel("RMS")
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
axes[0].legend()
|
|
||||||
|
|
||||||
axes[1].bar(plot_df["frequency_hz"].astype(str), plot_df["relative_error_percent"], color="tab:orange")
|
|
||||||
axes[1].set_xlabel("Frequency (Hz)")
|
|
||||||
axes[1].set_ylabel("Relative Error (%)")
|
|
||||||
axes[1].grid(True, axis="y", alpha=0.3)
|
|
||||||
axes[1].tick_params(axis="x", labelrotation=45)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
figure_path = save_dir / f"evaluation_{split}_curve.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def save_single_sample_plot(record, result: dict[str, float | str], split: str, save_dir: Path, sample_index: int) -> Path:
|
|
||||||
time_middle = record.time_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
x_middle = record.x_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
y_middle = record.y_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
frequency_hz = float(result["frequency_hz"])
|
|
||||||
pred_rms = float(result["pred_rms"])
|
|
||||||
true_rms = float(result["true_rms"])
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
|
|
||||||
fig.suptitle(f"Task1 TR Tree | {split} | {result['file_name']}")
|
|
||||||
|
|
||||||
axes[0].plot(time_middle, x_middle, color="tab:blue")
|
|
||||||
axes[0].set_title("Input Base Excitation (Middle Segment)")
|
|
||||||
axes[0].set_xlabel("Time")
|
|
||||||
axes[0].set_ylabel("Acceleration")
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
axes[1].plot(time_middle, y_middle, color="tab:green")
|
|
||||||
axes[1].set_title("True Top Response (Middle Segment)")
|
|
||||||
axes[1].set_xlabel("Time")
|
|
||||||
axes[1].set_ylabel("Acceleration")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
axes[2].bar(["True RMS", "Pred RMS"], [true_rms, pred_rms], color=["tab:green", "tab:orange"])
|
|
||||||
axes[2].set_title(
|
|
||||||
f"Freq: {frequency_hz:.4f} Hz | True RMS: {true_rms:.6f} | Pred RMS: {pred_rms:.6f} | "
|
|
||||||
f"Error: {float(result['relative_error_percent']):.2f}%"
|
|
||||||
)
|
|
||||||
axes[2].set_ylabel("RMS")
|
|
||||||
axes[2].grid(True, axis="y", alpha=0.3)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
figure_path = save_dir / f"evaluation_{split}_s{sample_index}.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_experiment_config()
|
|
||||||
ckpt_path = resolve_checkpoint_path(config.data.project_root, args.checkpoint)
|
|
||||||
with ckpt_path.open("rb") as handle:
|
|
||||||
bundle = pickle.load(handle)
|
|
||||||
model = bundle["model"]
|
|
||||||
|
|
||||||
_, raw_records, reports = build_datasets(config.data)
|
|
||||||
print(report_to_text(reports))
|
|
||||||
records = raw_records[args.split]
|
|
||||||
save_dir = evaluation_dir(config)
|
|
||||||
|
|
||||||
if args.all_samples:
|
|
||||||
rows = [evaluate_record(model, record) for record in records]
|
|
||||||
result_df = pd.DataFrame(rows).sort_values(["relative_error_percent", "file_name"]).reset_index(drop=True)
|
|
||||||
csv_path = save_dir / f"evaluation_{args.split}_all_samples.csv"
|
|
||||||
result_df.to_csv(csv_path, index=False)
|
|
||||||
fig_path = save_all_samples_plot(result_df, args.split, save_dir)
|
|
||||||
summary = {
|
|
||||||
"count": len(result_df),
|
|
||||||
"mean_error_percent": float(result_df["relative_error_percent"].mean()),
|
|
||||||
"median_error_percent": float(result_df["relative_error_percent"].median()),
|
|
||||||
"max_error_percent": float(result_df["relative_error_percent"].max()),
|
|
||||||
"min_error_percent": float(result_df["relative_error_percent"].min()),
|
|
||||||
}
|
|
||||||
print(f"Checkpoint: {ckpt_path}")
|
|
||||||
print(f"Model: {bundle['model_name']}")
|
|
||||||
print(f"Summary: {summary}")
|
|
||||||
print(f"CSV saved to: {csv_path}")
|
|
||||||
if fig_path is not None:
|
|
||||||
print(f"Figure saved to: {fig_path}")
|
|
||||||
print(result_df.to_string(index=False))
|
|
||||||
return
|
|
||||||
|
|
||||||
record = records[args.sample_index]
|
|
||||||
result = evaluate_record(model, record)
|
|
||||||
fig_path = save_single_sample_plot(record, result, args.split, save_dir, args.sample_index)
|
|
||||||
print(f"Checkpoint: {ckpt_path}")
|
|
||||||
print(f"Model: {bundle['model_name']}")
|
|
||||||
print(f"Sample file: {result['file_name']}")
|
|
||||||
print(f"True RMS: {float(result['true_rms']):.6f}")
|
|
||||||
print(f"Pred RMS: {float(result['pred_rms']):.6f}")
|
|
||||||
print(f"Relative RMS Error (%): {float(result['relative_error_percent']):.4f}")
|
|
||||||
print(f"Figure saved to: {fig_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import pickle
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(PROJECT_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
|
||||||
|
|
||||||
from scripts.dataset import build_record_from_file
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import FEATURE_NAMES, evaluation_dir, make_experiment_config
|
|
||||||
except ImportError:
|
|
||||||
from config import FEATURE_NAMES, evaluation_dir, make_experiment_config
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Predict task1 RMS from a single waveform CSV with tree model.")
|
|
||||||
parser.add_argument("--file", type=str, required=True)
|
|
||||||
parser.add_argument("--checkpoint", type=str, default=None)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_path(project_root: Path, checkpoint_arg: str | None) -> Path:
|
|
||||||
if checkpoint_arg:
|
|
||||||
return Path(checkpoint_arg).resolve()
|
|
||||||
return project_root / "checkpoints_tree" / "task1_tr_tree" / "best_tr_tree.pkl"
|
|
||||||
|
|
||||||
|
|
||||||
def save_prediction_figure(file_path: Path, record, pred_rms: float, true_rms: float | None, output_dir: Path) -> Path:
|
|
||||||
time_middle = record.time_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
x_middle = record.x_middle.detach().cpu().numpy().reshape(-1)
|
|
||||||
sampling_rate = record.sampling_rate
|
|
||||||
fft_values = np.fft.rfft(x_middle)
|
|
||||||
freqs = np.fft.rfftfreq(x_middle.size, d=1.0 / sampling_rate)
|
|
||||||
amplitudes = np.abs(fft_values)
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
|
|
||||||
fig.suptitle(f"Task1 TR Tree Single-File Prediction | {file_path.name}")
|
|
||||||
|
|
||||||
axes[0].plot(time_middle, x_middle, color="tab:blue")
|
|
||||||
axes[0].set_title("Input Base Excitation (Middle Segment)")
|
|
||||||
axes[0].set_xlabel("Time")
|
|
||||||
axes[0].set_ylabel("Acceleration")
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
axes[1].plot(freqs, amplitudes, color="tab:purple")
|
|
||||||
axes[1].axvline(record.frequency_hz, color="tab:red", linestyle="--", label=f"Dominant freq = {record.frequency_hz:.4f} Hz")
|
|
||||||
axes[1].set_xlim(0.0, 5.0)
|
|
||||||
axes[1].set_title("Input Spectrum")
|
|
||||||
axes[1].set_xlabel("Frequency (Hz)")
|
|
||||||
axes[1].set_ylabel("Amplitude")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
axes[1].legend()
|
|
||||||
|
|
||||||
labels = ["Pred RMS"] if true_rms is None else ["True RMS", "Pred RMS"]
|
|
||||||
values = [pred_rms] if true_rms is None else [true_rms, pred_rms]
|
|
||||||
colors = ["tab:orange"] if true_rms is None else ["tab:green", "tab:orange"]
|
|
||||||
axes[2].bar(labels, values, color=colors)
|
|
||||||
title = f"Predicted RMS = {pred_rms:.6f}"
|
|
||||||
if true_rms is not None:
|
|
||||||
error_percent = abs(pred_rms - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
|
||||||
title = f"True RMS = {true_rms:.6f} | Pred RMS = {pred_rms:.6f} | Error = {error_percent:.2f}%"
|
|
||||||
axes[2].set_title(title)
|
|
||||||
axes[2].set_ylabel("RMS")
|
|
||||||
axes[2].grid(True, axis="y", alpha=0.3)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
figure_path = output_dir / f"{file_path.stem}_prediction.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_experiment_config()
|
|
||||||
ckpt_path = resolve_checkpoint_path(config.data.project_root, args.checkpoint)
|
|
||||||
with ckpt_path.open("rb") as handle:
|
|
||||||
bundle = pickle.load(handle)
|
|
||||||
model = bundle["model"]
|
|
||||||
|
|
||||||
file_path = Path(args.file).resolve()
|
|
||||||
record = build_record_from_file(file_path, split="predict", config=config.data)
|
|
||||||
pred_tr = max(float(model.predict([record.features.tolist()])[0]), 1e-6)
|
|
||||||
pred_rms = pred_tr * float(record.x_rms.item())
|
|
||||||
true_rms = float(record.y_rms.item()) if record.y_rms.numel() > 0 else None
|
|
||||||
|
|
||||||
out_dir = evaluation_dir(config)
|
|
||||||
fig_path = save_prediction_figure(file_path, record, pred_rms, true_rms, out_dir)
|
|
||||||
|
|
||||||
print(f"Checkpoint: {ckpt_path}")
|
|
||||||
print(f"Model: {bundle['model_name']}")
|
|
||||||
print(f"Input file: {file_path}")
|
|
||||||
print(f"Extracted frequency (Hz): {record.frequency_hz:.6f}")
|
|
||||||
for feature_name, feature_value in zip(FEATURE_NAMES, record.features.tolist()):
|
|
||||||
print(f"{feature_name}: {feature_value:.6f}")
|
|
||||||
print(f"Predicted TR: {pred_tr:.6f}")
|
|
||||||
print(f"Predicted RMS: {pred_rms:.6f}")
|
|
||||||
if true_rms is not None:
|
|
||||||
error_percent = abs(pred_rms - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
|
||||||
print(f"True RMS: {true_rms:.6f}")
|
|
||||||
print(f"Relative RMS Error (%): {error_percent:.4f}")
|
|
||||||
print(f"Figure saved to: {fig_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pickle
|
|
||||||
from dataclasses import asdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from sklearn.ensemble import ExtraTreesRegressor, GradientBoostingRegressor, RandomForestRegressor
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(PROJECT_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
|
||||||
|
|
||||||
from scripts.dataset import build_datasets, report_to_text
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import FEATURE_NAMES, ExperimentConfig, checkpoint_dir, make_experiment_config
|
|
||||||
except ImportError:
|
|
||||||
from config import FEATURE_NAMES, ExperimentConfig, checkpoint_dir, make_experiment_config
|
|
||||||
|
|
||||||
|
|
||||||
def build_regressor(model_name: str, random_state: int):
|
|
||||||
if model_name == "extra_trees":
|
|
||||||
return ExtraTreesRegressor(
|
|
||||||
n_estimators=600,
|
|
||||||
max_depth=None,
|
|
||||||
min_samples_leaf=1,
|
|
||||||
min_samples_split=2,
|
|
||||||
random_state=random_state,
|
|
||||||
)
|
|
||||||
if model_name == "random_forest":
|
|
||||||
return RandomForestRegressor(
|
|
||||||
n_estimators=500,
|
|
||||||
max_depth=None,
|
|
||||||
min_samples_leaf=1,
|
|
||||||
min_samples_split=2,
|
|
||||||
random_state=random_state,
|
|
||||||
)
|
|
||||||
if model_name == "gradient_boosting":
|
|
||||||
return GradientBoostingRegressor(
|
|
||||||
n_estimators=300,
|
|
||||||
learning_rate=0.03,
|
|
||||||
max_depth=3,
|
|
||||||
random_state=random_state,
|
|
||||||
loss="huber",
|
|
||||||
)
|
|
||||||
raise ValueError(f"Unsupported model: {model_name}")
|
|
||||||
|
|
||||||
|
|
||||||
def records_to_frame(records: list[Any]) -> pd.DataFrame:
|
|
||||||
rows = []
|
|
||||||
for record in records:
|
|
||||||
row = {
|
|
||||||
"file_name": record.file_path.name,
|
|
||||||
"frequency_hz": float(record.frequency_hz),
|
|
||||||
"x_rms": float(record.x_rms.item()),
|
|
||||||
"y_rms": float(record.y_rms.item()),
|
|
||||||
"target_tr": float(record.target_y_rms.item()),
|
|
||||||
}
|
|
||||||
for name, value in zip(FEATURE_NAMES, record.features.tolist()):
|
|
||||||
row[name] = float(value)
|
|
||||||
rows.append(row)
|
|
||||||
return pd.DataFrame(rows)
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_frame(model, frame: pd.DataFrame) -> dict[str, float]:
|
|
||||||
feature_matrix = frame.loc[:, FEATURE_NAMES].to_numpy()
|
|
||||||
pred_tr = model.predict(feature_matrix)
|
|
||||||
pred_tr = pred_tr.clip(min=1e-6)
|
|
||||||
pred_rms = pred_tr * frame["x_rms"].to_numpy()
|
|
||||||
true_rms = frame["y_rms"].to_numpy()
|
|
||||||
relative_error = abs(pred_rms - true_rms) / true_rms.clip(min=1e-6)
|
|
||||||
return {
|
|
||||||
"mean_rms_error": float(relative_error.mean()),
|
|
||||||
"median_rms_error": float(pd.Series(relative_error).median()),
|
|
||||||
"max_rms_error": float(relative_error.max()),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_for_checkpoint(value: Any) -> Any:
|
|
||||||
if isinstance(value, Path):
|
|
||||||
return str(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {key: serialize_for_checkpoint(sub_value) for key, sub_value in value.items()}
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def train() -> None:
|
|
||||||
config = make_experiment_config()
|
|
||||||
datasets, raw_records, reports = build_datasets(config.data)
|
|
||||||
del datasets
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
train_df = records_to_frame(raw_records["train"])
|
|
||||||
val_df = records_to_frame(raw_records["val"])
|
|
||||||
|
|
||||||
feature_matrix = train_df.loc[:, FEATURE_NAMES].to_numpy()
|
|
||||||
target = train_df["target_tr"].to_numpy()
|
|
||||||
|
|
||||||
results: list[dict[str, Any]] = []
|
|
||||||
best_bundle: dict[str, Any] | None = None
|
|
||||||
best_val_error = float("inf")
|
|
||||||
|
|
||||||
for model_name in config.model.candidate_models:
|
|
||||||
model = build_regressor(model_name, config.model.random_state)
|
|
||||||
model.fit(feature_matrix, target)
|
|
||||||
|
|
||||||
train_metrics = evaluate_frame(model, train_df)
|
|
||||||
val_metrics = evaluate_frame(model, val_df)
|
|
||||||
row = {
|
|
||||||
"model_name": model_name,
|
|
||||||
"train_mean_rms_error": train_metrics["mean_rms_error"],
|
|
||||||
"train_median_rms_error": train_metrics["median_rms_error"],
|
|
||||||
"train_max_rms_error": train_metrics["max_rms_error"],
|
|
||||||
"val_mean_rms_error": val_metrics["mean_rms_error"],
|
|
||||||
"val_median_rms_error": val_metrics["median_rms_error"],
|
|
||||||
"val_max_rms_error": val_metrics["max_rms_error"],
|
|
||||||
}
|
|
||||||
results.append(row)
|
|
||||||
print(
|
|
||||||
f"{model_name}: train_mean={train_metrics['mean_rms_error']:.6f} | "
|
|
||||||
f"val_mean={val_metrics['mean_rms_error']:.6f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if val_metrics["mean_rms_error"] < best_val_error:
|
|
||||||
best_val_error = val_metrics["mean_rms_error"]
|
|
||||||
best_bundle = {
|
|
||||||
"model_name": model_name,
|
|
||||||
"model": model,
|
|
||||||
"config": serialize_for_checkpoint(asdict(config)),
|
|
||||||
"feature_names": list(FEATURE_NAMES),
|
|
||||||
"val_metrics": val_metrics,
|
|
||||||
"train_metrics": train_metrics,
|
|
||||||
}
|
|
||||||
|
|
||||||
summary_df = pd.DataFrame(results).sort_values("val_mean_rms_error").reset_index(drop=True)
|
|
||||||
ckpt_dir = checkpoint_dir(config)
|
|
||||||
summary_path = ckpt_dir / config.train.summary_name
|
|
||||||
summary_df.to_csv(summary_path, index=False)
|
|
||||||
print(f"Model selection saved to: {summary_path}")
|
|
||||||
print(summary_df.to_string(index=False))
|
|
||||||
|
|
||||||
if best_bundle is None:
|
|
||||||
raise RuntimeError("No valid model trained.")
|
|
||||||
|
|
||||||
best_path = ckpt_dir / config.train.best_model_name
|
|
||||||
with best_path.open("wb") as handle:
|
|
||||||
pickle.dump(best_bundle, handle)
|
|
||||||
print(f"Best tree model saved to: {best_path}")
|
|
||||||
print(f"Best model: {best_bundle['model_name']} | val_mean_rms_error={best_val_error:.6f}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
train()
|
|
||||||
133
src/config.py
@@ -1,133 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
|
|
||||||
TaskMode = Literal["forward", "inverse"]
|
|
||||||
IncompleteFilePolicy = Literal["skip", "raise"]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DataConfig:
|
|
||||||
project_root: Path = field(default_factory=lambda: Path(__file__).resolve().parents[1])
|
|
||||||
scenario: str = "Non_TMD"
|
|
||||||
task: TaskMode = "forward"
|
|
||||||
|
|
||||||
train_split_name: str = "train"
|
|
||||||
val_split_name: str = "val"
|
|
||||||
test_split_name: str = "test"
|
|
||||||
csv_pattern: str = "*.csv"
|
|
||||||
|
|
||||||
code_column: str = "code"
|
|
||||||
time_column: str = "time"
|
|
||||||
base_sensor_code: str = "WSMS00012"
|
|
||||||
base_axis: str = "value1"
|
|
||||||
response_sensor_code: str = "WSMS00007"
|
|
||||||
response_axis: str = "value3"
|
|
||||||
|
|
||||||
window_size: int = 512
|
|
||||||
window_stride: int = 64
|
|
||||||
min_sequence_length: int = 512
|
|
||||||
|
|
||||||
batch_size: int = 32
|
|
||||||
num_workers: int = 0
|
|
||||||
pin_memory: bool = True
|
|
||||||
shuffle_train: bool = True
|
|
||||||
drop_last_train: bool = False
|
|
||||||
use_weighted_train_sampler: bool = True
|
|
||||||
train_weight_gain_power: float = 1.0
|
|
||||||
train_weight_response_rms_power: float = 0.5
|
|
||||||
train_weight_min: float = 0.5
|
|
||||||
train_weight_max: float = 4.0
|
|
||||||
|
|
||||||
interpolation_method: str = "linear"
|
|
||||||
incomplete_file_policy: IncompleteFilePolicy = "skip"
|
|
||||||
normalization_eps: float = 1e-6
|
|
||||||
|
|
||||||
downloads_dir: Path = field(init=False)
|
|
||||||
scenario_dir: Path = field(init=False)
|
|
||||||
train_dir: Path = field(init=False)
|
|
||||||
val_dir: Path = field(init=False)
|
|
||||||
test_dir: Path = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
self.project_root = Path(self.project_root).resolve()
|
|
||||||
self.downloads_dir = self.project_root / "downloads"
|
|
||||||
self.scenario_dir = self.downloads_dir / self.scenario
|
|
||||||
self.train_dir = self.scenario_dir / self.train_split_name
|
|
||||||
self.val_dir = self.scenario_dir / self.val_split_name
|
|
||||||
self.test_dir = self.scenario_dir / self.test_split_name
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ModelConfig:
|
|
||||||
input_channels: int = 1
|
|
||||||
output_channels: int = 1
|
|
||||||
tcn_channels: tuple[int, ...] = (32, 32, 64, 64, 128)
|
|
||||||
kernel_size: int = 5
|
|
||||||
dropout: float = 0.15
|
|
||||||
use_causal_conv: bool = True
|
|
||||||
dilation_base: int = 2
|
|
||||||
use_linear_skip_branch: bool = True
|
|
||||||
linear_skip_kernel_size: int = 33
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LossConfig:
|
|
||||||
time_loss: str = "huber"
|
|
||||||
huber_delta: float = 1.0
|
|
||||||
fft_loss_weight: float = 0.005
|
|
||||||
fft_loss_min_hz: float = 0.1
|
|
||||||
fft_loss_max_hz: float = 5.0
|
|
||||||
forward_time_loss_weight: float = 0.1
|
|
||||||
forward_rms_loss_weight: float = 8.0
|
|
||||||
forward_scale_loss_weight: float = 8.0
|
|
||||||
forward_underestimate_loss_weight: float = 16.0
|
|
||||||
forward_envelope_loss_weight: float = 2.0
|
|
||||||
envelope_kernel_size: int = 33
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TrainConfig:
|
|
||||||
epochs: int = 100
|
|
||||||
learning_rate: float = 1e-3
|
|
||||||
weight_decay: float = 1e-4
|
|
||||||
seed: int = 42
|
|
||||||
device: str = "cuda"
|
|
||||||
grad_clip_norm: float = 1.0
|
|
||||||
use_amp: bool = True
|
|
||||||
lr_scheduler_patience: int = 5
|
|
||||||
lr_scheduler_factor: float = 0.5
|
|
||||||
min_learning_rate: float = 1e-6
|
|
||||||
early_stop_patience: int = 8
|
|
||||||
checkpoint_dir: str = "checkpoints"
|
|
||||||
best_model_name: str = "best_tcn_model.pt"
|
|
||||||
history_name: str = "training_history.csv"
|
|
||||||
dominant_freq_min_hz: float = 0.1
|
|
||||||
dominant_freq_max_hz: float = 5.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ExperimentConfig:
|
|
||||||
data: DataConfig = field(default_factory=DataConfig)
|
|
||||||
model: ModelConfig = field(default_factory=ModelConfig)
|
|
||||||
loss: LossConfig = field(default_factory=LossConfig)
|
|
||||||
train: TrainConfig = field(default_factory=TrainConfig)
|
|
||||||
|
|
||||||
|
|
||||||
def make_forward_config() -> ExperimentConfig:
|
|
||||||
config = ExperimentConfig()
|
|
||||||
config.data.task = "forward"
|
|
||||||
config.model.input_channels = 1
|
|
||||||
config.model.output_channels = 1
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def make_inverse_config() -> ExperimentConfig:
|
|
||||||
config = ExperimentConfig()
|
|
||||||
config.data.task = "inverse"
|
|
||||||
config.model.input_channels = 1
|
|
||||||
config.model.output_channels = 1
|
|
||||||
return config
|
|
||||||
449
src/dataset.py
@@ -1,449 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
|
||||||
from torch.utils.data import DataLoader, Dataset, WeightedRandomSampler
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import DataConfig, ExperimentConfig
|
|
||||||
except ImportError:
|
|
||||||
from config import DataConfig, ExperimentConfig
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NormalizationStats:
|
|
||||||
x_mean: torch.Tensor
|
|
||||||
x_std: torch.Tensor
|
|
||||||
y_mean: torch.Tensor
|
|
||||||
y_std: torch.Tensor
|
|
||||||
|
|
||||||
def normalize_x(self, tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return (tensor - self.x_mean) / self.x_std
|
|
||||||
|
|
||||||
def denormalize_x(self, tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return tensor * self.x_std + self.x_mean
|
|
||||||
|
|
||||||
def normalize_y(self, tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return (tensor - self.y_mean) / self.y_std
|
|
||||||
|
|
||||||
def denormalize_y(self, tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return tensor * self.y_std + self.y_mean
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TensorStandardScaler:
|
|
||||||
mean: torch.Tensor
|
|
||||||
std: torch.Tensor
|
|
||||||
|
|
||||||
def transform(self, array: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray:
|
|
||||||
return self._apply(array, inverse=False)
|
|
||||||
|
|
||||||
def inverse_transform(self, array: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray:
|
|
||||||
return self._apply(array, inverse=True)
|
|
||||||
|
|
||||||
def _apply(self, array: torch.Tensor | np.ndarray, inverse: bool) -> torch.Tensor | np.ndarray:
|
|
||||||
if isinstance(array, torch.Tensor):
|
|
||||||
mean = self.mean.to(array.device, dtype=array.dtype)
|
|
||||||
std = self.std.to(array.device, dtype=array.dtype)
|
|
||||||
return array * std + mean if inverse else (array - mean) / std
|
|
||||||
|
|
||||||
np_array = np.asarray(array, dtype=np.float32)
|
|
||||||
mean_np = self.mean.detach().cpu().numpy().astype(np.float32)
|
|
||||||
std_np = self.std.detach().cpu().numpy().astype(np.float32)
|
|
||||||
return np_array * std_np + mean_np if inverse else (np_array - mean_np) / std_np
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SequenceRecord:
|
|
||||||
file_path: Path
|
|
||||||
split: str
|
|
||||||
time: torch.Tensor
|
|
||||||
x_raw: torch.Tensor
|
|
||||||
y_raw: torch.Tensor
|
|
||||||
interpolation_count: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SplitLoadReport:
|
|
||||||
split: str
|
|
||||||
loaded_files: list[str] = field(default_factory=list)
|
|
||||||
skipped_files: list[tuple[str, str]] = field(default_factory=list)
|
|
||||||
interpolated_files: dict[str, int] = field(default_factory=dict)
|
|
||||||
short_files: list[tuple[str, int]] = field(default_factory=list)
|
|
||||||
|
|
||||||
def to_lines(self) -> list[str]:
|
|
||||||
lines = [
|
|
||||||
f"[{self.split}] loaded={len(self.loaded_files)} "
|
|
||||||
f"skipped={len(self.skipped_files)} short={len(self.short_files)}"
|
|
||||||
]
|
|
||||||
if self.skipped_files:
|
|
||||||
for file_name, reason in self.skipped_files:
|
|
||||||
lines.append(f" - skipped {file_name}: {reason}")
|
|
||||||
if self.short_files:
|
|
||||||
for file_name, length in self.short_files:
|
|
||||||
lines.append(f" - short {file_name}: length={length}")
|
|
||||||
if self.interpolated_files:
|
|
||||||
for file_name, count in self.interpolated_files.items():
|
|
||||||
if count > 0:
|
|
||||||
lines.append(f" - interpolated {file_name}: missing_points={count}")
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
class WindowedTimeSeriesDataset(Dataset):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
records: list[SequenceRecord],
|
|
||||||
normalization: NormalizationStats,
|
|
||||||
config: DataConfig,
|
|
||||||
split: str,
|
|
||||||
report: SplitLoadReport | None = None,
|
|
||||||
) -> None:
|
|
||||||
self.config = config
|
|
||||||
self.split = split
|
|
||||||
self.normalization = normalization
|
|
||||||
self.report = report or SplitLoadReport(split=split)
|
|
||||||
self.sequence_store: list[dict[str, Any]] = []
|
|
||||||
self.window_index: list[tuple[int, int]] = []
|
|
||||||
self.window_weights: list[float] = []
|
|
||||||
|
|
||||||
min_length = max(config.min_sequence_length, config.window_size)
|
|
||||||
raw_window_gains: list[float] = []
|
|
||||||
raw_window_response_rms: list[float] = []
|
|
||||||
|
|
||||||
for record in records:
|
|
||||||
sequence_length = int(record.time.shape[0])
|
|
||||||
if sequence_length < min_length:
|
|
||||||
self.report.short_files.append((record.file_path.name, sequence_length))
|
|
||||||
continue
|
|
||||||
|
|
||||||
x_norm = normalization.normalize_x(record.x_raw)
|
|
||||||
y_norm = normalization.normalize_y(record.y_raw)
|
|
||||||
|
|
||||||
record_index = len(self.sequence_store)
|
|
||||||
self.sequence_store.append(
|
|
||||||
{
|
|
||||||
"file_name": record.file_path.name,
|
|
||||||
"file_path": str(record.file_path),
|
|
||||||
"time": record.time,
|
|
||||||
"x": x_norm,
|
|
||||||
"y": y_norm,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
last_start = sequence_length - config.window_size
|
|
||||||
for start in range(0, last_start + 1, config.window_stride):
|
|
||||||
self.window_index.append((record_index, start))
|
|
||||||
raw_x = record.x_raw[start : start + config.window_size]
|
|
||||||
raw_y = record.y_raw[start : start + config.window_size]
|
|
||||||
x_rms = torch.sqrt(torch.mean(raw_x.pow(2))).item()
|
|
||||||
y_rms = torch.sqrt(torch.mean(raw_y.pow(2))).item()
|
|
||||||
gain = y_rms / max(x_rms, float(config.normalization_eps))
|
|
||||||
raw_window_gains.append(float(gain))
|
|
||||||
raw_window_response_rms.append(float(y_rms))
|
|
||||||
|
|
||||||
if not self.window_index:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"No valid windows were created for split='{split}'. "
|
|
||||||
"Check file completeness, sequence length, and window configuration."
|
|
||||||
)
|
|
||||||
|
|
||||||
if split == "train":
|
|
||||||
self.window_weights = build_window_weights(
|
|
||||||
gains=raw_window_gains,
|
|
||||||
response_rms=raw_window_response_rms,
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.window_weights = [1.0 for _ in self.window_index]
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.window_index)
|
|
||||||
|
|
||||||
def __getitem__(self, index: int) -> dict[str, Any]:
|
|
||||||
record_index, start = self.window_index[index]
|
|
||||||
record = self.sequence_store[record_index]
|
|
||||||
end = start + self.config.window_size
|
|
||||||
|
|
||||||
x = record["x"][start:end]
|
|
||||||
time = record["time"][start:end]
|
|
||||||
y = record["y"][start:end]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"x": x,
|
|
||||||
"y": y,
|
|
||||||
"time": time,
|
|
||||||
"file_name": record["file_name"],
|
|
||||||
"file_path": record["file_path"],
|
|
||||||
"window_start": torch.tensor(start, dtype=torch.long),
|
|
||||||
"window_end": torch.tensor(end, dtype=torch.long),
|
|
||||||
}
|
|
||||||
|
|
||||||
def denormalize_x(self, tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return self.normalization.denormalize_x(tensor)
|
|
||||||
|
|
||||||
def denormalize_y(self, tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return self.normalization.denormalize_y(tensor)
|
|
||||||
|
|
||||||
|
|
||||||
def list_split_files(config: DataConfig, split: str) -> list[Path]:
|
|
||||||
split_dir = getattr(config, f"{split}_dir")
|
|
||||||
return sorted(split_dir.glob(config.csv_pattern))
|
|
||||||
|
|
||||||
|
|
||||||
def _value_frame(df: pd.DataFrame, config: DataConfig, sensor_code: str, value_column: str) -> pd.DataFrame:
|
|
||||||
sensor_df = df.loc[df[config.code_column] == sensor_code, [config.time_column, value_column]].copy()
|
|
||||||
sensor_df = sensor_df.sort_values(config.time_column)
|
|
||||||
sensor_df = sensor_df.drop_duplicates(subset=config.time_column, keep="first")
|
|
||||||
sensor_df[config.time_column] = sensor_df[config.time_column].astype("float64")
|
|
||||||
sensor_df[value_column] = sensor_df[value_column].astype("float32")
|
|
||||||
return sensor_df
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_incomplete_file(
|
|
||||||
file_path: Path,
|
|
||||||
split: str,
|
|
||||||
reason: str,
|
|
||||||
config: DataConfig,
|
|
||||||
report: SplitLoadReport,
|
|
||||||
) -> None:
|
|
||||||
if config.incomplete_file_policy == "raise":
|
|
||||||
raise ValueError(f"{file_path.name}: {reason}")
|
|
||||||
report.skipped_files.append((file_path.name, reason))
|
|
||||||
|
|
||||||
|
|
||||||
def align_file_to_base_timeline(
|
|
||||||
file_path: Path,
|
|
||||||
config: DataConfig,
|
|
||||||
split: str,
|
|
||||||
report: SplitLoadReport,
|
|
||||||
) -> SequenceRecord | None:
|
|
||||||
df = pd.read_csv(file_path)
|
|
||||||
|
|
||||||
base_df = _value_frame(df, config, config.base_sensor_code, config.base_axis)
|
|
||||||
if base_df.empty:
|
|
||||||
_handle_incomplete_file(
|
|
||||||
file_path=file_path,
|
|
||||||
split=split,
|
|
||||||
reason=f"missing base sensor {config.base_sensor_code}",
|
|
||||||
config=config,
|
|
||||||
report=report,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
response_df = _value_frame(df, config, config.response_sensor_code, config.response_axis)
|
|
||||||
if response_df.empty:
|
|
||||||
_handle_incomplete_file(
|
|
||||||
file_path=file_path,
|
|
||||||
split=split,
|
|
||||||
reason=f"missing response sensor {config.response_sensor_code}",
|
|
||||||
config=config,
|
|
||||||
report=report,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
aligned = base_df.rename(columns={config.base_axis: "base_signal"})
|
|
||||||
response_df = response_df.rename(columns={config.response_axis: "response_signal"})
|
|
||||||
aligned = aligned.merge(response_df, on=config.time_column, how="left")
|
|
||||||
|
|
||||||
interpolation_count = int(aligned["response_signal"].isna().sum())
|
|
||||||
aligned[["response_signal"]] = aligned[["response_signal"]].interpolate(
|
|
||||||
method=config.interpolation_method,
|
|
||||||
limit_direction="both",
|
|
||||||
)
|
|
||||||
aligned[["response_signal"]] = aligned[["response_signal"]].ffill().bfill()
|
|
||||||
|
|
||||||
if aligned["response_signal"].isna().any():
|
|
||||||
_handle_incomplete_file(
|
|
||||||
file_path=file_path,
|
|
||||||
split=split,
|
|
||||||
reason="remaining NaN values after alignment/interpolation",
|
|
||||||
config=config,
|
|
||||||
report=report,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if interpolation_count > 0:
|
|
||||||
report.interpolated_files[file_path.name] = interpolation_count
|
|
||||||
|
|
||||||
time_tensor = torch.tensor(aligned[config.time_column].to_numpy(), dtype=torch.float64)
|
|
||||||
base_tensor = torch.tensor(aligned[["base_signal"]].to_numpy(), dtype=torch.float32)
|
|
||||||
response_tensor = torch.tensor(aligned[["response_signal"]].to_numpy(), dtype=torch.float32)
|
|
||||||
|
|
||||||
if config.task == "forward":
|
|
||||||
x_raw = base_tensor
|
|
||||||
y_raw = response_tensor
|
|
||||||
elif config.task == "inverse":
|
|
||||||
x_raw = response_tensor
|
|
||||||
y_raw = base_tensor
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported task: {config.task}")
|
|
||||||
|
|
||||||
report.loaded_files.append(file_path.name)
|
|
||||||
return SequenceRecord(
|
|
||||||
file_path=file_path,
|
|
||||||
split=split,
|
|
||||||
time=time_tensor,
|
|
||||||
x_raw=x_raw,
|
|
||||||
y_raw=y_raw,
|
|
||||||
interpolation_count=interpolation_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_split_records(config: DataConfig, split: str) -> tuple[list[SequenceRecord], SplitLoadReport]:
|
|
||||||
report = SplitLoadReport(split=split)
|
|
||||||
records: list[SequenceRecord] = []
|
|
||||||
|
|
||||||
for file_path in list_split_files(config, split):
|
|
||||||
record = align_file_to_base_timeline(file_path=file_path, config=config, split=split, report=report)
|
|
||||||
if record is not None:
|
|
||||||
records.append(record)
|
|
||||||
|
|
||||||
if not records:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"No usable files found in split='{split}' under '{getattr(config, f'{split}_dir')}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
return records, report
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_feature_std(tensor: torch.Tensor, eps: float) -> torch.Tensor:
|
|
||||||
std = tensor.std(dim=0, unbiased=False)
|
|
||||||
eps_tensor = torch.full_like(std, eps)
|
|
||||||
return torch.maximum(std, eps_tensor)
|
|
||||||
|
|
||||||
|
|
||||||
def fit_normalization_stats(records: list[SequenceRecord], config: DataConfig) -> NormalizationStats:
|
|
||||||
x_all = torch.cat([record.x_raw for record in records], dim=0)
|
|
||||||
y_all = torch.cat([record.y_raw for record in records], dim=0)
|
|
||||||
x_mean = x_all.mean(dim=0)
|
|
||||||
x_std = _safe_feature_std(x_all, config.normalization_eps)
|
|
||||||
y_mean = y_all.mean(dim=0)
|
|
||||||
y_std = _safe_feature_std(y_all, config.normalization_eps)
|
|
||||||
|
|
||||||
return NormalizationStats(
|
|
||||||
x_mean=x_mean,
|
|
||||||
x_std=x_std,
|
|
||||||
y_mean=y_mean,
|
|
||||||
y_std=y_std,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_window_weights(
|
|
||||||
gains: list[float],
|
|
||||||
response_rms: list[float],
|
|
||||||
config: DataConfig,
|
|
||||||
) -> list[float]:
|
|
||||||
if not gains or not response_rms:
|
|
||||||
return []
|
|
||||||
|
|
||||||
gain_array = np.asarray(gains, dtype=np.float32)
|
|
||||||
rms_array = np.asarray(response_rms, dtype=np.float32)
|
|
||||||
gain_ref = float(np.median(gain_array[gain_array > 0.0])) if np.any(gain_array > 0.0) else 1.0
|
|
||||||
rms_ref = float(np.median(rms_array[rms_array > 0.0])) if np.any(rms_array > 0.0) else 1.0
|
|
||||||
gain_ref = max(gain_ref, float(config.normalization_eps))
|
|
||||||
rms_ref = max(rms_ref, float(config.normalization_eps))
|
|
||||||
|
|
||||||
normalized_gain = np.power(np.maximum(gain_array / gain_ref, config.normalization_eps), config.train_weight_gain_power)
|
|
||||||
normalized_rms = np.power(
|
|
||||||
np.maximum(rms_array / rms_ref, config.normalization_eps),
|
|
||||||
config.train_weight_response_rms_power,
|
|
||||||
)
|
|
||||||
weights = normalized_gain * normalized_rms
|
|
||||||
weights = np.clip(weights, config.train_weight_min, config.train_weight_max)
|
|
||||||
return weights.astype(np.float32).tolist()
|
|
||||||
|
|
||||||
|
|
||||||
def build_datasets(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[dict[str, WindowedTimeSeriesDataset], NormalizationStats, dict[str, SplitLoadReport]]:
|
|
||||||
data_config = config.data if isinstance(config, ExperimentConfig) else config
|
|
||||||
|
|
||||||
train_records, train_report = load_split_records(data_config, "train")
|
|
||||||
normalization = fit_normalization_stats(train_records, data_config)
|
|
||||||
|
|
||||||
val_records, val_report = load_split_records(data_config, "val")
|
|
||||||
test_records, test_report = load_split_records(data_config, "test")
|
|
||||||
|
|
||||||
datasets = {
|
|
||||||
"train": WindowedTimeSeriesDataset(train_records, normalization, data_config, "train", train_report),
|
|
||||||
"val": WindowedTimeSeriesDataset(val_records, normalization, data_config, "val", val_report),
|
|
||||||
"test": WindowedTimeSeriesDataset(test_records, normalization, data_config, "test", test_report),
|
|
||||||
}
|
|
||||||
reports = {"train": train_report, "val": val_report, "test": test_report}
|
|
||||||
return datasets, normalization, reports
|
|
||||||
|
|
||||||
|
|
||||||
def build_dataloaders(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[dict[str, DataLoader], dict[str, WindowedTimeSeriesDataset], dict[str, SplitLoadReport]]:
|
|
||||||
data_config = config.data if isinstance(config, ExperimentConfig) else config
|
|
||||||
datasets, _, reports = build_datasets(config)
|
|
||||||
train_sampler = None
|
|
||||||
train_shuffle = data_config.shuffle_train
|
|
||||||
|
|
||||||
if data_config.use_weighted_train_sampler:
|
|
||||||
train_weights = torch.tensor(datasets["train"].window_weights, dtype=torch.double)
|
|
||||||
train_sampler = WeightedRandomSampler(
|
|
||||||
weights=train_weights,
|
|
||||||
num_samples=len(train_weights),
|
|
||||||
replacement=True,
|
|
||||||
)
|
|
||||||
train_shuffle = False
|
|
||||||
|
|
||||||
loaders = {
|
|
||||||
"train": DataLoader(
|
|
||||||
datasets["train"],
|
|
||||||
batch_size=data_config.batch_size,
|
|
||||||
shuffle=train_shuffle,
|
|
||||||
sampler=train_sampler,
|
|
||||||
num_workers=data_config.num_workers,
|
|
||||||
pin_memory=data_config.pin_memory,
|
|
||||||
drop_last=data_config.drop_last_train,
|
|
||||||
),
|
|
||||||
"val": DataLoader(
|
|
||||||
datasets["val"],
|
|
||||||
batch_size=data_config.batch_size,
|
|
||||||
shuffle=False,
|
|
||||||
num_workers=data_config.num_workers,
|
|
||||||
pin_memory=data_config.pin_memory,
|
|
||||||
drop_last=False,
|
|
||||||
),
|
|
||||||
"test": DataLoader(
|
|
||||||
datasets["test"],
|
|
||||||
batch_size=data_config.batch_size,
|
|
||||||
shuffle=False,
|
|
||||||
num_workers=data_config.num_workers,
|
|
||||||
pin_memory=data_config.pin_memory,
|
|
||||||
drop_last=False,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return loaders, datasets, reports
|
|
||||||
|
|
||||||
|
|
||||||
def get_dataloaders(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[
|
|
||||||
dict[str, DataLoader],
|
|
||||||
dict[str, WindowedTimeSeriesDataset],
|
|
||||||
dict[str, SplitLoadReport],
|
|
||||||
TensorStandardScaler,
|
|
||||||
TensorStandardScaler,
|
|
||||||
]:
|
|
||||||
loaders, datasets, reports = build_dataloaders(config)
|
|
||||||
normalization = datasets["train"].normalization
|
|
||||||
scaler_x = TensorStandardScaler(mean=normalization.x_mean, std=normalization.x_std)
|
|
||||||
scaler_y = TensorStandardScaler(mean=normalization.y_mean, std=normalization.y_std)
|
|
||||||
return loaders, datasets, reports, scaler_x, scaler_y
|
|
||||||
|
|
||||||
|
|
||||||
def report_to_text(reports: dict[str, SplitLoadReport]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
for split in ("train", "val", "test"):
|
|
||||||
report = reports[split]
|
|
||||||
lines.extend(report.to_lines())
|
|
||||||
return "\n".join(lines)
|
|
||||||
274
src/evaluate.py
@@ -1,274 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, make_forward_config, make_inverse_config
|
|
||||||
from .dataset import get_dataloaders, report_to_text
|
|
||||||
from .model import build_model
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, make_forward_config, make_inverse_config
|
|
||||||
from dataset import get_dataloaders, report_to_text
|
|
||||||
from model import build_model
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_rms(signal: np.ndarray) -> float:
|
|
||||||
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
|
||||||
if signal.size == 0:
|
|
||||||
raise ValueError("signal must not be empty")
|
|
||||||
return float(np.sqrt(np.mean(np.square(signal))))
|
|
||||||
|
|
||||||
|
|
||||||
def get_dominant_frequency(signal: np.ndarray, sampling_rate: float) -> float:
|
|
||||||
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
|
||||||
if signal.size == 0:
|
|
||||||
raise ValueError("signal must not be empty")
|
|
||||||
fft_values = np.fft.rfft(signal)
|
|
||||||
frequencies = np.fft.rfftfreq(signal.size, d=1.0 / sampling_rate)
|
|
||||||
magnitudes = np.abs(fft_values)
|
|
||||||
if magnitudes.size > 0:
|
|
||||||
magnitudes[0] = 0.0
|
|
||||||
dominant_index = int(np.argmax(magnitudes))
|
|
||||||
return float(frequencies[dominant_index])
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Evaluate trained TCN with time-domain and frequency-domain plots.")
|
|
||||||
parser.add_argument("--task", choices=("forward", "inverse"), default="forward")
|
|
||||||
parser.add_argument("--split", choices=("val", "test"), default="test")
|
|
||||||
parser.add_argument("--batch-index", type=int, default=0)
|
|
||||||
parser.add_argument("--sample-index", type=int, default=0)
|
|
||||||
parser.add_argument("--sampling-rate", type=float, default=None)
|
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
parser.add_argument("--checkpoint", type=str, default=None)
|
|
||||||
parser.add_argument("--save-dir", type=str, default=None)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def make_config(task: str) -> ExperimentConfig:
|
|
||||||
if task == "forward":
|
|
||||||
return make_forward_config()
|
|
||||||
if task == "inverse":
|
|
||||||
return make_inverse_config()
|
|
||||||
raise ValueError(f"Unsupported task: {task}")
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str | None, config: ExperimentConfig) -> torch.device:
|
|
||||||
requested = device_name or config.train.device
|
|
||||||
if requested.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(requested)
|
|
||||||
|
|
||||||
|
|
||||||
def default_checkpoint_path(config: ExperimentConfig) -> Path:
|
|
||||||
return config.data.project_root / config.train.checkpoint_dir / config.data.task / config.train.best_model_name
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_path(config: ExperimentConfig, checkpoint_arg: str | None) -> Path:
|
|
||||||
if checkpoint_arg:
|
|
||||||
return Path(checkpoint_arg).resolve()
|
|
||||||
|
|
||||||
default_path = default_checkpoint_path(config)
|
|
||||||
if default_path.exists():
|
|
||||||
return default_path
|
|
||||||
|
|
||||||
legacy_path = config.data.project_root / "best_model.pth"
|
|
||||||
if legacy_path.exists():
|
|
||||||
return legacy_path
|
|
||||||
|
|
||||||
return default_path
|
|
||||||
|
|
||||||
|
|
||||||
def select_batch(dataloader: torch.utils.data.DataLoader, batch_index: int) -> dict[str, object]:
|
|
||||||
for current_index, batch in enumerate(dataloader):
|
|
||||||
if current_index == batch_index:
|
|
||||||
return batch
|
|
||||||
raise IndexError(f"batch_index={batch_index} is out of range")
|
|
||||||
|
|
||||||
|
|
||||||
def get_sample_1d(batch_tensor: torch.Tensor, sample_index: int) -> torch.Tensor:
|
|
||||||
sample = batch_tensor[sample_index]
|
|
||||||
if sample.ndim == 2 and sample.shape[-1] == 1:
|
|
||||||
sample = sample.squeeze(-1)
|
|
||||||
return sample
|
|
||||||
|
|
||||||
|
|
||||||
def compute_fft_curve(signal: np.ndarray, sampling_rate: float) -> tuple[np.ndarray, np.ndarray]:
|
|
||||||
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
|
||||||
fft_values = np.fft.rfft(signal)
|
|
||||||
frequencies = np.fft.rfftfreq(signal.size, d=1.0 / sampling_rate)
|
|
||||||
magnitudes = np.abs(fft_values)
|
|
||||||
return frequencies, magnitudes
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_sampling_rate_from_time(time_values: np.ndarray) -> float:
|
|
||||||
time_values = np.asarray(time_values, dtype=np.float64).reshape(-1)
|
|
||||||
if time_values.size < 2:
|
|
||||||
return 100.0
|
|
||||||
dt = np.diff(time_values)
|
|
||||||
dt = dt[np.isfinite(dt)]
|
|
||||||
dt = dt[dt > 0.0]
|
|
||||||
if dt.size == 0:
|
|
||||||
return 100.0
|
|
||||||
median_dt = float(np.median(dt))
|
|
||||||
if median_dt <= 0.0:
|
|
||||||
return 100.0
|
|
||||||
return 1.0 / median_dt
|
|
||||||
|
|
||||||
|
|
||||||
def relative_percent_error(true_value: float, pred_value: float) -> float:
|
|
||||||
denominator = max(abs(true_value), 1e-12)
|
|
||||||
return abs(pred_value - true_value) / denominator * 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def task_labels(config: ExperimentConfig) -> tuple[str, str]:
|
|
||||||
if config.data.task == "forward":
|
|
||||||
return (
|
|
||||||
f"{config.data.base_sensor_code}/{config.data.base_axis}",
|
|
||||||
f"{config.data.response_sensor_code}/{config.data.response_axis}",
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
f"{config.data.response_sensor_code}/{config.data.response_axis}",
|
|
||||||
f"{config.data.base_sensor_code}/{config.data.base_axis}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_one_batch(
|
|
||||||
config: ExperimentConfig,
|
|
||||||
split: str,
|
|
||||||
batch_index: int,
|
|
||||||
sample_index: int,
|
|
||||||
sampling_rate: float,
|
|
||||||
checkpoint_path: Path,
|
|
||||||
device: torch.device,
|
|
||||||
save_dir: Path,
|
|
||||||
) -> Path:
|
|
||||||
loaders, datasets, reports, scaler_x, scaler_y = get_dataloaders(config)
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
dataset = datasets[split]
|
|
||||||
dataloader = loaders[split]
|
|
||||||
batch = select_batch(dataloader, batch_index)
|
|
||||||
|
|
||||||
x = batch["x"].to(device)
|
|
||||||
y = batch["y"].to(device)
|
|
||||||
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
|
||||||
model.load_state_dict(checkpoint["model_state_dict"])
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
pred_norm = model(x)
|
|
||||||
|
|
||||||
scaler_output = scaler_y if config.data.task == "forward" else scaler_x
|
|
||||||
pred_physical = scaler_output.inverse_transform(pred_norm).detach().cpu()
|
|
||||||
true_physical = scaler_output.inverse_transform(y).detach().cpu()
|
|
||||||
|
|
||||||
pred_signal = get_sample_1d(pred_physical, sample_index).numpy()
|
|
||||||
true_signal = get_sample_1d(true_physical, sample_index).numpy()
|
|
||||||
time_values = batch["time"][sample_index].detach().cpu().numpy()
|
|
||||||
time_steps = np.arange(true_signal.shape[0], dtype=np.float64)
|
|
||||||
effective_sampling_rate = (
|
|
||||||
sampling_rate if sampling_rate is not None else estimate_sampling_rate_from_time(time_values)
|
|
||||||
)
|
|
||||||
|
|
||||||
true_rms = calculate_rms(true_signal)
|
|
||||||
pred_rms = calculate_rms(pred_signal)
|
|
||||||
rms_error_percent = relative_percent_error(true_rms, pred_rms)
|
|
||||||
|
|
||||||
true_freq = get_dominant_frequency(true_signal, sampling_rate=effective_sampling_rate)
|
|
||||||
pred_freq = get_dominant_frequency(pred_signal, sampling_rate=effective_sampling_rate)
|
|
||||||
|
|
||||||
true_fft_freqs, true_fft_mag = compute_fft_curve(true_signal, sampling_rate=effective_sampling_rate)
|
|
||||||
pred_fft_freqs, pred_fft_mag = compute_fft_curve(pred_signal, sampling_rate=effective_sampling_rate)
|
|
||||||
|
|
||||||
freq_mask_true = (true_fft_freqs >= 0.0) & (true_fft_freqs <= 5.0)
|
|
||||||
freq_mask_pred = (pred_fft_freqs >= 0.0) & (pred_fft_freqs <= 5.0)
|
|
||||||
|
|
||||||
input_label, output_label = task_labels(config)
|
|
||||||
file_name = batch["file_name"][sample_index]
|
|
||||||
window_start = int(batch["window_start"][sample_index].item())
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(2, 1, figsize=(14, 9))
|
|
||||||
fig.suptitle(f"{config.data.task.upper()} evaluation | {split} | {file_name} | window_start={window_start}")
|
|
||||||
|
|
||||||
axes[0].plot(time_steps, true_signal, linewidth=1.2, label="True")
|
|
||||||
axes[0].plot(time_steps, pred_signal, linewidth=1.2, label="Pred")
|
|
||||||
axes[0].set_xlabel("Time Step")
|
|
||||||
axes[0].set_ylabel("Acceleration")
|
|
||||||
axes[0].set_title(
|
|
||||||
f"{output_label} | True RMS: {true_rms:.4f}, Pred RMS: {pred_rms:.4f}, Error: {rms_error_percent:.2f}%"
|
|
||||||
)
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
axes[0].legend(loc="upper right")
|
|
||||||
|
|
||||||
axes[1].plot(true_fft_freqs[freq_mask_true], true_fft_mag[freq_mask_true], linewidth=1.2, label="True FFT")
|
|
||||||
axes[1].plot(pred_fft_freqs[freq_mask_pred], pred_fft_mag[freq_mask_pred], linewidth=1.2, label="Pred FFT")
|
|
||||||
true_peak_mag = np.interp(true_freq, true_fft_freqs, true_fft_mag)
|
|
||||||
pred_peak_mag = np.interp(pred_freq, pred_fft_freqs, pred_fft_mag)
|
|
||||||
if 0.0 <= true_freq <= 5.0:
|
|
||||||
axes[1].scatter([true_freq], [true_peak_mag], s=50, marker="o", label="True Peak")
|
|
||||||
if 0.0 <= pred_freq <= 5.0:
|
|
||||||
axes[1].scatter([pred_freq], [pred_peak_mag], s=50, marker="x", label="Pred Peak")
|
|
||||||
axes[1].set_xlim(0.0, 5.0)
|
|
||||||
axes[1].set_xlabel("Frequency (Hz)")
|
|
||||||
axes[1].set_ylabel("Amplitude")
|
|
||||||
axes[1].set_title(f"True Freq: {true_freq:.4f} Hz, Pred Freq: {pred_freq:.4f} Hz")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
axes[1].legend(loc="upper right")
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
figure_path = save_dir / f"evaluation_{config.data.task}_{split}_b{batch_index}_s{sample_index}.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
print(f"Input channel: {input_label}")
|
|
||||||
print(f"Output channel: {output_label}")
|
|
||||||
print(f"Checkpoint: {checkpoint_path}")
|
|
||||||
print(f"Sampling Rate Used: {effective_sampling_rate:.6f} Hz")
|
|
||||||
print(f"Figure saved to: {figure_path}")
|
|
||||||
print(f"True RMS: {true_rms:.6f}")
|
|
||||||
print(f"Pred RMS: {pred_rms:.6f}")
|
|
||||||
print(f"RMS Error (%): {rms_error_percent:.4f}")
|
|
||||||
print(f"True Dominant Frequency: {true_freq:.6f} Hz")
|
|
||||||
print(f"Pred Dominant Frequency: {pred_freq:.6f} Hz")
|
|
||||||
print(f"Dominant Frequency Error: {abs(pred_freq - true_freq):.6f} Hz")
|
|
||||||
|
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_config(args.task)
|
|
||||||
device = resolve_device(args.device, config)
|
|
||||||
checkpoint_path = resolve_checkpoint_path(config, args.checkpoint)
|
|
||||||
save_dir = (
|
|
||||||
Path(args.save_dir).resolve()
|
|
||||||
if args.save_dir
|
|
||||||
else config.data.project_root / "evaluation_outputs" / config.data.task
|
|
||||||
)
|
|
||||||
|
|
||||||
if not checkpoint_path.exists():
|
|
||||||
raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
|
|
||||||
|
|
||||||
evaluate_one_batch(
|
|
||||||
config=config,
|
|
||||||
split=args.split,
|
|
||||||
batch_index=args.batch_index,
|
|
||||||
sample_index=args.sample_index,
|
|
||||||
sampling_rate=args.sampling_rate,
|
|
||||||
checkpoint_path=checkpoint_path,
|
|
||||||
device=device,
|
|
||||||
save_dir=save_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
162
src/model.py
@@ -1,162 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.nn.utils import weight_norm
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, ModelConfig
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, ModelConfig
|
|
||||||
|
|
||||||
|
|
||||||
class Chomp1d(nn.Module):
|
|
||||||
def __init__(self, chomp_size: int) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.chomp_size = chomp_size
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
if self.chomp_size == 0:
|
|
||||||
return x
|
|
||||||
return x[:, :, :-self.chomp_size].contiguous()
|
|
||||||
|
|
||||||
|
|
||||||
class CausalConv1d(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
in_channels: int,
|
|
||||||
out_channels: int,
|
|
||||||
kernel_size: int,
|
|
||||||
dilation: int = 1,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
|
||||||
padding = (kernel_size - 1) * dilation
|
|
||||||
self.net = nn.Sequential(
|
|
||||||
weight_norm(
|
|
||||||
nn.Conv1d(
|
|
||||||
in_channels=in_channels,
|
|
||||||
out_channels=out_channels,
|
|
||||||
kernel_size=kernel_size,
|
|
||||||
padding=padding,
|
|
||||||
dilation=dilation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Chomp1d(padding),
|
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
return self.net(x)
|
|
||||||
|
|
||||||
|
|
||||||
class TemporalBlock(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
in_channels: int,
|
|
||||||
out_channels: int,
|
|
||||||
kernel_size: int,
|
|
||||||
dilation: int,
|
|
||||||
dropout: float,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.conv1 = CausalConv1d(in_channels, out_channels, kernel_size, dilation=dilation)
|
|
||||||
self.act1 = nn.GELU()
|
|
||||||
self.dropout1 = nn.Dropout(dropout)
|
|
||||||
|
|
||||||
self.conv2 = CausalConv1d(out_channels, out_channels, kernel_size, dilation=dilation)
|
|
||||||
self.act2 = nn.GELU()
|
|
||||||
self.dropout2 = nn.Dropout(dropout)
|
|
||||||
|
|
||||||
self.residual = nn.Conv1d(in_channels, out_channels, kernel_size=1) if in_channels != out_channels else nn.Identity()
|
|
||||||
self.final_act = nn.GELU()
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
residual = self.residual(x)
|
|
||||||
out = self.conv1(x)
|
|
||||||
out = self.act1(out)
|
|
||||||
out = self.dropout1(out)
|
|
||||||
out = self.conv2(out)
|
|
||||||
out = self.act2(out)
|
|
||||||
out = self.dropout2(out)
|
|
||||||
return self.final_act(out + residual)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReceptiveFieldInfo:
|
|
||||||
receptive_field: int
|
|
||||||
dilations: tuple[int, ...]
|
|
||||||
|
|
||||||
|
|
||||||
class TemporalConvNet(nn.Module):
|
|
||||||
def __init__(self, config: ModelConfig) -> None:
|
|
||||||
super().__init__()
|
|
||||||
blocks: list[nn.Module] = []
|
|
||||||
in_channels = config.input_channels
|
|
||||||
dilations: list[int] = []
|
|
||||||
|
|
||||||
for level, out_channels in enumerate(config.tcn_channels):
|
|
||||||
dilation = config.dilation_base ** level
|
|
||||||
dilations.append(dilation)
|
|
||||||
blocks.append(
|
|
||||||
TemporalBlock(
|
|
||||||
in_channels=in_channels,
|
|
||||||
out_channels=out_channels,
|
|
||||||
kernel_size=config.kernel_size,
|
|
||||||
dilation=dilation,
|
|
||||||
dropout=config.dropout,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
in_channels = out_channels
|
|
||||||
|
|
||||||
self.network = nn.Sequential(*blocks)
|
|
||||||
self.output_head = nn.Conv1d(in_channels, config.output_channels, kernel_size=1)
|
|
||||||
self.linear_skip = None
|
|
||||||
if config.use_linear_skip_branch:
|
|
||||||
self.linear_skip = CausalConv1d(
|
|
||||||
in_channels=config.input_channels,
|
|
||||||
out_channels=config.output_channels,
|
|
||||||
kernel_size=config.linear_skip_kernel_size,
|
|
||||||
dilation=1,
|
|
||||||
)
|
|
||||||
self.receptive_field_info = compute_receptive_field(config)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
features = self.network(x)
|
|
||||||
nonlinear_output = self.output_head(features)
|
|
||||||
if self.linear_skip is None:
|
|
||||||
return nonlinear_output
|
|
||||||
return nonlinear_output + self.linear_skip(x)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildingTCN(nn.Module):
|
|
||||||
def __init__(self, config: ModelConfig) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.config = config
|
|
||||||
self.tcn = TemporalConvNet(config)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
if x.ndim != 3:
|
|
||||||
raise ValueError(f"Expected input shape (batch, seq, channels), got {tuple(x.shape)}")
|
|
||||||
x = x.transpose(1, 2)
|
|
||||||
y = self.tcn(x)
|
|
||||||
return y.transpose(1, 2)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def receptive_field(self) -> int:
|
|
||||||
return self.tcn.receptive_field_info.receptive_field
|
|
||||||
|
|
||||||
|
|
||||||
def compute_receptive_field(config: ModelConfig) -> ReceptiveFieldInfo:
|
|
||||||
receptive_field = 1
|
|
||||||
dilations: list[int] = []
|
|
||||||
for level, _ in enumerate(config.tcn_channels):
|
|
||||||
dilation = config.dilation_base ** level
|
|
||||||
dilations.append(dilation)
|
|
||||||
receptive_field += 2 * (config.kernel_size - 1) * dilation
|
|
||||||
return ReceptiveFieldInfo(receptive_field=receptive_field, dilations=tuple(dilations))
|
|
||||||
|
|
||||||
|
|
||||||
def build_model(config: ExperimentConfig | ModelConfig) -> BuildingTCN:
|
|
||||||
model_config = config.model if isinstance(config, ExperimentConfig) else config
|
|
||||||
return BuildingTCN(model_config)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from config import make_forward_config
|
|
||||||
from dataset import build_dataloaders, report_to_text
|
|
||||||
|
|
||||||
|
|
||||||
def _to_cpu(tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
return tensor.detach().cpu()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
config = make_forward_config()
|
|
||||||
config.data.batch_size = 4
|
|
||||||
config.data.window_size = 1024
|
|
||||||
config.data.window_stride = 256
|
|
||||||
|
|
||||||
loaders, datasets, reports = build_dataloaders(config)
|
|
||||||
train_loader = loaders["train"]
|
|
||||||
train_dataset = datasets["train"]
|
|
||||||
|
|
||||||
print("Dataset loading report:")
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
batch = next(iter(train_loader))
|
|
||||||
|
|
||||||
time = _to_cpu(batch["time"][0]).numpy()
|
|
||||||
x_norm = batch["x"][0]
|
|
||||||
y_norm = batch["y"][0]
|
|
||||||
|
|
||||||
x = _to_cpu(train_dataset.denormalize_x(x_norm)).squeeze(-1).numpy()
|
|
||||||
y = _to_cpu(train_dataset.denormalize_y(y_norm)).squeeze(-1).numpy()
|
|
||||||
|
|
||||||
if config.data.task == "forward":
|
|
||||||
input_label = f"Input {config.data.base_sensor_code}/{config.data.base_axis}"
|
|
||||||
output_label = f"Output {config.data.response_sensor_code}/{config.data.response_axis}"
|
|
||||||
else:
|
|
||||||
input_label = f"Input {config.data.response_sensor_code}/{config.data.response_axis}"
|
|
||||||
output_label = f"Output {config.data.base_sensor_code}/{config.data.base_axis}"
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)
|
|
||||||
fig.suptitle(f"Alignment sanity check: {batch['file_name'][0]}")
|
|
||||||
|
|
||||||
axes[0].plot(time, x, color="black", linewidth=1.2, label=input_label)
|
|
||||||
axes[0].set_ylabel("Acceleration")
|
|
||||||
axes[0].legend(loc="upper right")
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
axes[1].plot(time, y, linewidth=1.1, label=output_label)
|
|
||||||
axes[1].set_xlabel("Time")
|
|
||||||
axes[1].set_ylabel("Acceleration")
|
|
||||||
axes[1].legend(loc="upper right")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
n_guides = min(12, len(time))
|
|
||||||
guide_indices = torch.linspace(0, len(time) - 1, steps=n_guides).round().to(torch.long).tolist()
|
|
||||||
guide_times = [time[i] for i in guide_indices]
|
|
||||||
for ax in axes:
|
|
||||||
for guide_time in guide_times:
|
|
||||||
ax.axvline(guide_time, color="gray", linestyle="--", linewidth=0.8, alpha=0.35)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
output_path = Path(__file__).resolve().parents[1] / f"sanity_check_alignment_{config.data.task}.png"
|
|
||||||
plt.savefig(output_path, dpi=180, bbox_inches="tight")
|
|
||||||
print(f"Saved figure to: {output_path}")
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
586
src/train.py
@@ -1,586 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import math
|
|
||||||
import random
|
|
||||||
from dataclasses import asdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
|
||||||
import torch.nn.functional as F
|
|
||||||
from torch import nn
|
|
||||||
from torch.optim import AdamW
|
|
||||||
from torch.optim.lr_scheduler import ReduceLROnPlateau
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, make_forward_config, make_inverse_config
|
|
||||||
from .dataset import build_dataloaders, report_to_text
|
|
||||||
from .model import build_model, compute_receptive_field
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, make_forward_config, make_inverse_config
|
|
||||||
from dataset import build_dataloaders, report_to_text
|
|
||||||
from model import build_model, compute_receptive_field
|
|
||||||
|
|
||||||
|
|
||||||
class MixedTimeFrequencyLoss(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
huber_delta: float,
|
|
||||||
fft_loss_weight: float,
|
|
||||||
fft_loss_min_hz: float,
|
|
||||||
fft_loss_max_hz: float,
|
|
||||||
forward_time_loss_weight: float,
|
|
||||||
forward_rms_loss_weight: float,
|
|
||||||
forward_scale_loss_weight: float,
|
|
||||||
forward_underestimate_loss_weight: float,
|
|
||||||
forward_envelope_loss_weight: float,
|
|
||||||
envelope_kernel_size: int,
|
|
||||||
task: str,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.huber_delta = huber_delta
|
|
||||||
self.fft_loss_weight = fft_loss_weight
|
|
||||||
self.fft_loss_min_hz = fft_loss_min_hz
|
|
||||||
self.fft_loss_max_hz = fft_loss_max_hz
|
|
||||||
self.forward_time_loss_weight = forward_time_loss_weight
|
|
||||||
self.forward_rms_loss_weight = forward_rms_loss_weight
|
|
||||||
self.forward_scale_loss_weight = forward_scale_loss_weight
|
|
||||||
self.forward_underestimate_loss_weight = forward_underestimate_loss_weight
|
|
||||||
self.forward_envelope_loss_weight = forward_envelope_loss_weight
|
|
||||||
self.envelope_kernel_size = envelope_kernel_size
|
|
||||||
self.task = task
|
|
||||||
|
|
||||||
def forward(
|
|
||||||
self,
|
|
||||||
prediction: torch.Tensor,
|
|
||||||
target: torch.Tensor,
|
|
||||||
time_tensor: torch.Tensor,
|
|
||||||
) -> dict[str, torch.Tensor]:
|
|
||||||
raw_time_loss = F.huber_loss(prediction, target, delta=self.huber_delta)
|
|
||||||
fft_loss = band_limited_fft_loss(
|
|
||||||
prediction=prediction,
|
|
||||||
target=target,
|
|
||||||
time_tensor=time_tensor,
|
|
||||||
min_hz=self.fft_loss_min_hz,
|
|
||||||
max_hz=self.fft_loss_max_hz,
|
|
||||||
)
|
|
||||||
rms_pred = sequence_rms(prediction)
|
|
||||||
rms_target = sequence_rms(target)
|
|
||||||
rms_loss = F.l1_loss(rms_pred, rms_target) if self.task == "forward" else prediction.new_tensor(0.0)
|
|
||||||
scale_loss = forward_scale_loss(prediction, target) if self.task == "forward" else prediction.new_tensor(0.0)
|
|
||||||
underestimate_loss = (
|
|
||||||
forward_underestimate_loss(prediction, target) if self.task == "forward" else prediction.new_tensor(0.0)
|
|
||||||
)
|
|
||||||
envelope_loss = (
|
|
||||||
forward_envelope_loss(prediction, target, self.envelope_kernel_size)
|
|
||||||
if self.task == "forward"
|
|
||||||
else prediction.new_tensor(0.0)
|
|
||||||
)
|
|
||||||
time_loss = (
|
|
||||||
raw_time_loss * self.forward_time_loss_weight
|
|
||||||
if self.task == "forward"
|
|
||||||
else raw_time_loss
|
|
||||||
)
|
|
||||||
total_loss = (
|
|
||||||
time_loss
|
|
||||||
+ self.fft_loss_weight * fft_loss
|
|
||||||
+ self.forward_rms_loss_weight * rms_loss
|
|
||||||
+ self.forward_scale_loss_weight * scale_loss
|
|
||||||
+ self.forward_underestimate_loss_weight * underestimate_loss
|
|
||||||
+ self.forward_envelope_loss_weight * envelope_loss
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"total": total_loss,
|
|
||||||
"time": raw_time_loss,
|
|
||||||
"weighted_time": time_loss,
|
|
||||||
"fft": fft_loss,
|
|
||||||
"rms_loss": rms_loss,
|
|
||||||
"scale_loss": scale_loss,
|
|
||||||
"underestimate_loss": underestimate_loss,
|
|
||||||
"envelope_loss": envelope_loss,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Train single-input single-output TCN for structural response modeling.")
|
|
||||||
parser.add_argument("--task", choices=("forward", "inverse"), default="forward")
|
|
||||||
parser.add_argument("--epochs", type=int, default=None)
|
|
||||||
parser.add_argument("--batch-size", type=int, default=None)
|
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def make_config(task: str) -> ExperimentConfig:
|
|
||||||
if task == "forward":
|
|
||||||
return make_forward_config()
|
|
||||||
if task == "inverse":
|
|
||||||
return make_inverse_config()
|
|
||||||
raise ValueError(f"Unsupported task: {task}")
|
|
||||||
|
|
||||||
|
|
||||||
def set_seed(seed: int) -> None:
|
|
||||||
random.seed(seed)
|
|
||||||
torch.manual_seed(seed)
|
|
||||||
torch.cuda.manual_seed_all(seed)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str) -> torch.device:
|
|
||||||
if device_name.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(device_name)
|
|
||||||
|
|
||||||
|
|
||||||
def move_batch_to_device(batch: dict[str, object], device: torch.device) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
||||||
x = batch["x"].to(device)
|
|
||||||
y = batch["y"].to(device)
|
|
||||||
time = batch["time"].to(device)
|
|
||||||
return x, y, time
|
|
||||||
|
|
||||||
|
|
||||||
def denormalize_prediction(
|
|
||||||
prediction_norm: torch.Tensor,
|
|
||||||
target_norm: torch.Tensor,
|
|
||||||
y_mean: torch.Tensor,
|
|
||||||
y_std: torch.Tensor,
|
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
||||||
prediction = prediction_norm * y_std + y_mean
|
|
||||||
target = target_norm * y_std + y_mean
|
|
||||||
return prediction, target
|
|
||||||
|
|
||||||
|
|
||||||
def sequence_rms(signal: torch.Tensor) -> torch.Tensor:
|
|
||||||
return torch.sqrt(torch.mean(signal ** 2, dim=1))
|
|
||||||
|
|
||||||
|
|
||||||
def rms_error(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
|
||||||
pred_rms = sequence_rms(prediction)
|
|
||||||
target_rms = sequence_rms(target)
|
|
||||||
return torch.abs(pred_rms - target_rms)
|
|
||||||
|
|
||||||
|
|
||||||
def forward_scale_loss(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
|
||||||
epsilon = 1e-6
|
|
||||||
pred_std = prediction.std(dim=1, unbiased=False)
|
|
||||||
target_std = target.std(dim=1, unbiased=False)
|
|
||||||
pred_peak = prediction.abs().amax(dim=1)
|
|
||||||
target_peak = target.abs().amax(dim=1)
|
|
||||||
pred_rms = sequence_rms(prediction)
|
|
||||||
target_rms = sequence_rms(target)
|
|
||||||
std_loss = torch.mean(torch.abs(pred_std - target_std) / (target_std.abs() + epsilon))
|
|
||||||
peak_loss = torch.mean(torch.abs(pred_peak - target_peak) / (target_peak.abs() + epsilon))
|
|
||||||
rms_loss = torch.mean(torch.abs(pred_rms - target_rms) / (target_rms.abs() + epsilon))
|
|
||||||
return (std_loss + peak_loss + rms_loss) / 3.0
|
|
||||||
|
|
||||||
|
|
||||||
def forward_underestimate_loss(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
|
||||||
epsilon = 1e-6
|
|
||||||
pred_rms = sequence_rms(prediction)
|
|
||||||
target_rms = sequence_rms(target)
|
|
||||||
pred_peak = prediction.abs().amax(dim=1)
|
|
||||||
target_peak = target.abs().amax(dim=1)
|
|
||||||
pred_std = prediction.std(dim=1, unbiased=False)
|
|
||||||
target_std = target.std(dim=1, unbiased=False)
|
|
||||||
|
|
||||||
rms_under = torch.relu((target_rms - pred_rms) / (target_rms.abs() + epsilon))
|
|
||||||
peak_under = torch.relu((target_peak - pred_peak) / (target_peak.abs() + epsilon))
|
|
||||||
std_under = torch.relu((target_std - pred_std) / (target_std.abs() + epsilon))
|
|
||||||
return (rms_under.mean() + peak_under.mean() + std_under.mean()) / 3.0
|
|
||||||
|
|
||||||
|
|
||||||
def local_envelope(signal: torch.Tensor, kernel_size: int) -> torch.Tensor:
|
|
||||||
signal_abs = signal.squeeze(-1).abs().unsqueeze(1)
|
|
||||||
effective_kernel = max(3, int(kernel_size))
|
|
||||||
if effective_kernel % 2 == 0:
|
|
||||||
effective_kernel += 1
|
|
||||||
padding = effective_kernel // 2
|
|
||||||
return F.max_pool1d(signal_abs, kernel_size=effective_kernel, stride=1, padding=padding).squeeze(1)
|
|
||||||
|
|
||||||
|
|
||||||
def forward_envelope_loss(prediction: torch.Tensor, target: torch.Tensor, kernel_size: int) -> torch.Tensor:
|
|
||||||
epsilon = 1e-6
|
|
||||||
pred_envelope = local_envelope(prediction, kernel_size)
|
|
||||||
target_envelope = local_envelope(target, kernel_size)
|
|
||||||
relative_error = torch.abs(pred_envelope - target_envelope) / (target_envelope.abs() + epsilon)
|
|
||||||
underestimate_error = torch.relu((target_envelope - pred_envelope) / (target_envelope.abs() + epsilon))
|
|
||||||
return relative_error.mean() + 2.0 * underestimate_error.mean()
|
|
||||||
|
|
||||||
|
|
||||||
def band_limited_fft_loss(
|
|
||||||
prediction: torch.Tensor,
|
|
||||||
target: torch.Tensor,
|
|
||||||
time_tensor: torch.Tensor,
|
|
||||||
min_hz: float,
|
|
||||||
max_hz: float,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
prediction_signal = prediction.squeeze(-1)
|
|
||||||
target_signal = target.squeeze(-1)
|
|
||||||
if prediction_signal.shape[1] < 4:
|
|
||||||
pred_fft = torch.abs(torch.fft.rfft(prediction_signal, dim=1))
|
|
||||||
target_fft = torch.abs(torch.fft.rfft(target_signal, dim=1))
|
|
||||||
return F.mse_loss(pred_fft, target_fft)
|
|
||||||
|
|
||||||
dt = torch.diff(time_tensor, dim=1).mean(dim=1)
|
|
||||||
dt = torch.clamp(dt, min=1e-6)
|
|
||||||
pred_fft = torch.abs(torch.fft.rfft(prediction_signal, dim=1))
|
|
||||||
target_fft = torch.abs(torch.fft.rfft(target_signal, dim=1))
|
|
||||||
fft_bins = pred_fft.shape[1]
|
|
||||||
n = prediction_signal.shape[1]
|
|
||||||
bin_indices = torch.arange(fft_bins, device=prediction.device, dtype=prediction.dtype).unsqueeze(0)
|
|
||||||
frequencies = bin_indices / (dt.unsqueeze(1) * n)
|
|
||||||
band_mask = (frequencies >= min_hz) & (frequencies <= max_hz)
|
|
||||||
band_mask[:, 0] = False
|
|
||||||
|
|
||||||
squared_error = (pred_fft - target_fft) ** 2
|
|
||||||
masked_error = squared_error * band_mask.to(squared_error.dtype)
|
|
||||||
valid_counts = band_mask.sum(dim=1).clamp_min(1).to(squared_error.dtype)
|
|
||||||
per_sample_loss = masked_error.sum(dim=1) / valid_counts
|
|
||||||
fallback_loss = squared_error.mean(dim=1)
|
|
||||||
has_valid_band = band_mask.any(dim=1)
|
|
||||||
per_sample_loss = torch.where(has_valid_band, per_sample_loss, fallback_loss)
|
|
||||||
return per_sample_loss.mean()
|
|
||||||
|
|
||||||
|
|
||||||
def dominant_frequency_error(
|
|
||||||
prediction: torch.Tensor,
|
|
||||||
target: torch.Tensor,
|
|
||||||
time_tensor: torch.Tensor,
|
|
||||||
min_hz: float,
|
|
||||||
max_hz: float,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
if prediction.shape[1] < 4:
|
|
||||||
return torch.tensor(0.0, device=prediction.device)
|
|
||||||
|
|
||||||
dt = torch.diff(time_tensor, dim=1).mean(dim=1)
|
|
||||||
dt = torch.clamp(dt, min=1e-6)
|
|
||||||
|
|
||||||
prediction_signal = prediction.squeeze(-1)
|
|
||||||
target_signal = target.squeeze(-1)
|
|
||||||
pred_fft = torch.abs(torch.fft.rfft(prediction_signal, dim=1))
|
|
||||||
target_fft = torch.abs(torch.fft.rfft(target_signal, dim=1))
|
|
||||||
n = prediction_signal.shape[1]
|
|
||||||
fft_bins = pred_fft.shape[1]
|
|
||||||
bin_indices = torch.arange(fft_bins, device=prediction.device, dtype=prediction.dtype).unsqueeze(0)
|
|
||||||
frequencies = bin_indices / (dt.unsqueeze(1) * n)
|
|
||||||
band_mask = (frequencies >= min_hz) & (frequencies <= max_hz)
|
|
||||||
|
|
||||||
if not band_mask.any():
|
|
||||||
return torch.tensor(0.0, device=prediction.device)
|
|
||||||
|
|
||||||
masked_pred_fft = pred_fft.masked_fill(~band_mask, float("-inf"))
|
|
||||||
masked_target_fft = target_fft.masked_fill(~band_mask, float("-inf"))
|
|
||||||
|
|
||||||
pred_index = masked_pred_fft.argmax(dim=1)
|
|
||||||
target_index = masked_target_fft.argmax(dim=1)
|
|
||||||
pred_freq = pred_index.to(prediction.dtype) / (dt * n)
|
|
||||||
target_freq = target_index.to(target.dtype) / (dt * n)
|
|
||||||
return torch.mean(torch.abs(pred_freq - target_freq))
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_for_checkpoint(value: Any) -> Any:
|
|
||||||
if isinstance(value, Path):
|
|
||||||
return str(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {key: serialize_for_checkpoint(sub_value) for key, sub_value in value.items()}
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def monitor_metric_name(config: ExperimentConfig) -> str:
|
|
||||||
if config.data.task == "forward":
|
|
||||||
return "rms_error"
|
|
||||||
return "loss"
|
|
||||||
|
|
||||||
|
|
||||||
def run_epoch(
|
|
||||||
model: nn.Module,
|
|
||||||
dataloader: torch.utils.data.DataLoader,
|
|
||||||
optimizer: AdamW | None,
|
|
||||||
criterion: MixedTimeFrequencyLoss,
|
|
||||||
y_mean: torch.Tensor,
|
|
||||||
y_std: torch.Tensor,
|
|
||||||
device: torch.device,
|
|
||||||
grad_clip_norm: float,
|
|
||||||
scaler: torch.cuda.amp.GradScaler,
|
|
||||||
amp_enabled: bool,
|
|
||||||
dominant_freq_min_hz: float,
|
|
||||||
dominant_freq_max_hz: float,
|
|
||||||
) -> dict[str, float]:
|
|
||||||
is_train = optimizer is not None
|
|
||||||
model.train(is_train)
|
|
||||||
|
|
||||||
total_loss_sum = 0.0
|
|
||||||
time_loss_sum = 0.0
|
|
||||||
weighted_time_loss_sum = 0.0
|
|
||||||
fft_loss_sum = 0.0
|
|
||||||
rms_loss_sum = 0.0
|
|
||||||
scale_loss_sum = 0.0
|
|
||||||
underestimate_loss_sum = 0.0
|
|
||||||
envelope_loss_sum = 0.0
|
|
||||||
rms_error_sum = 0.0
|
|
||||||
dominant_freq_error_sum = 0.0
|
|
||||||
sample_count = 0
|
|
||||||
|
|
||||||
for batch in dataloader:
|
|
||||||
x, y_norm, time = move_batch_to_device(batch, device)
|
|
||||||
|
|
||||||
if is_train:
|
|
||||||
optimizer.zero_grad(set_to_none=True)
|
|
||||||
|
|
||||||
with torch.amp.autocast(device_type=device.type, enabled=amp_enabled):
|
|
||||||
prediction_norm = model(x)
|
|
||||||
prediction, target = denormalize_prediction(prediction_norm, y_norm, y_mean, y_std)
|
|
||||||
losses = criterion(prediction, target, time)
|
|
||||||
|
|
||||||
if is_train:
|
|
||||||
scaler.scale(losses["total"]).backward()
|
|
||||||
scaler.unscale_(optimizer)
|
|
||||||
torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip_norm)
|
|
||||||
scaler.step(optimizer)
|
|
||||||
scaler.update()
|
|
||||||
|
|
||||||
batch_size = x.shape[0]
|
|
||||||
total_loss_sum += losses["total"].detach().item() * batch_size
|
|
||||||
time_loss_sum += losses["time"].detach().item() * batch_size
|
|
||||||
weighted_time_loss_sum += losses["weighted_time"].detach().item() * batch_size
|
|
||||||
fft_loss_sum += losses["fft"].detach().item() * batch_size
|
|
||||||
rms_loss_sum += losses["rms_loss"].detach().item() * batch_size
|
|
||||||
scale_loss_sum += losses["scale_loss"].detach().item() * batch_size
|
|
||||||
underestimate_loss_sum += losses["underestimate_loss"].detach().item() * batch_size
|
|
||||||
envelope_loss_sum += losses["envelope_loss"].detach().item() * batch_size
|
|
||||||
rms_error_sum += rms_error(prediction.detach(), target.detach()).mean().item() * batch_size
|
|
||||||
dominant_freq_error_sum += dominant_frequency_error(
|
|
||||||
prediction.detach(),
|
|
||||||
target.detach(),
|
|
||||||
time.detach(),
|
|
||||||
min_hz=dominant_freq_min_hz,
|
|
||||||
max_hz=dominant_freq_max_hz,
|
|
||||||
).item() * batch_size
|
|
||||||
sample_count += batch_size
|
|
||||||
|
|
||||||
if sample_count == 0:
|
|
||||||
raise RuntimeError("No samples were processed in the epoch.")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"loss": total_loss_sum / sample_count,
|
|
||||||
"time_loss": time_loss_sum / sample_count,
|
|
||||||
"weighted_time_loss": weighted_time_loss_sum / sample_count,
|
|
||||||
"fft_loss": fft_loss_sum / sample_count,
|
|
||||||
"rms_loss": rms_loss_sum / sample_count,
|
|
||||||
"scale_loss": scale_loss_sum / sample_count,
|
|
||||||
"underestimate_loss": underestimate_loss_sum / sample_count,
|
|
||||||
"envelope_loss": envelope_loss_sum / sample_count,
|
|
||||||
"rms_error": rms_error_sum / sample_count,
|
|
||||||
"dominant_freq_error": dominant_freq_error_sum / sample_count,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def checkpoint_paths(config: ExperimentConfig) -> tuple[Path, Path]:
|
|
||||||
root = config.data.project_root / config.train.checkpoint_dir / config.data.task
|
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
|
||||||
return root / config.train.best_model_name, root / config.train.history_name
|
|
||||||
|
|
||||||
|
|
||||||
def train_model(config: ExperimentConfig) -> None:
|
|
||||||
set_seed(config.train.seed)
|
|
||||||
device = resolve_device(config.train.device)
|
|
||||||
|
|
||||||
dataloaders, datasets, reports = build_dataloaders(config)
|
|
||||||
train_dataset = datasets["train"]
|
|
||||||
normalization = train_dataset.normalization
|
|
||||||
y_mean = normalization.y_mean.to(device).view(1, 1, -1)
|
|
||||||
y_std = normalization.y_std.to(device).view(1, 1, -1)
|
|
||||||
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
receptive_field = compute_receptive_field(config.model)
|
|
||||||
optimizer = AdamW(
|
|
||||||
model.parameters(),
|
|
||||||
lr=config.train.learning_rate,
|
|
||||||
weight_decay=config.train.weight_decay,
|
|
||||||
)
|
|
||||||
scheduler = ReduceLROnPlateau(
|
|
||||||
optimizer,
|
|
||||||
mode="min",
|
|
||||||
factor=config.train.lr_scheduler_factor,
|
|
||||||
patience=config.train.lr_scheduler_patience,
|
|
||||||
min_lr=config.train.min_learning_rate,
|
|
||||||
)
|
|
||||||
criterion = MixedTimeFrequencyLoss(
|
|
||||||
huber_delta=config.loss.huber_delta,
|
|
||||||
fft_loss_weight=config.loss.fft_loss_weight,
|
|
||||||
fft_loss_min_hz=config.loss.fft_loss_min_hz,
|
|
||||||
fft_loss_max_hz=config.loss.fft_loss_max_hz,
|
|
||||||
forward_time_loss_weight=config.loss.forward_time_loss_weight,
|
|
||||||
forward_rms_loss_weight=config.loss.forward_rms_loss_weight,
|
|
||||||
forward_scale_loss_weight=config.loss.forward_scale_loss_weight,
|
|
||||||
forward_underestimate_loss_weight=config.loss.forward_underestimate_loss_weight,
|
|
||||||
forward_envelope_loss_weight=config.loss.forward_envelope_loss_weight,
|
|
||||||
envelope_kernel_size=config.loss.envelope_kernel_size,
|
|
||||||
task=config.data.task,
|
|
||||||
)
|
|
||||||
|
|
||||||
amp_enabled = config.train.use_amp and device.type == "cuda"
|
|
||||||
scaler = torch.cuda.amp.GradScaler(enabled=amp_enabled)
|
|
||||||
best_model_path, history_path = checkpoint_paths(config)
|
|
||||||
|
|
||||||
print(f"Task: {config.data.task}")
|
|
||||||
print(f"Device: {device}")
|
|
||||||
print(
|
|
||||||
"TCN receptive field: "
|
|
||||||
f"{receptive_field.receptive_field} samples, "
|
|
||||||
f"dilations={receptive_field.dilations}"
|
|
||||||
)
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
history_rows: list[dict[str, float | int]] = []
|
|
||||||
monitor_name = monitor_metric_name(config)
|
|
||||||
best_monitor_value = math.inf
|
|
||||||
stale_epochs = 0
|
|
||||||
|
|
||||||
for epoch in range(1, config.train.epochs + 1):
|
|
||||||
train_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=dataloaders["train"],
|
|
||||||
optimizer=optimizer,
|
|
||||||
criterion=criterion,
|
|
||||||
y_mean=y_mean,
|
|
||||||
y_std=y_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
scaler=scaler,
|
|
||||||
amp_enabled=amp_enabled,
|
|
||||||
dominant_freq_min_hz=config.train.dominant_freq_min_hz,
|
|
||||||
dominant_freq_max_hz=config.train.dominant_freq_max_hz,
|
|
||||||
)
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
val_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=dataloaders["val"],
|
|
||||||
optimizer=None,
|
|
||||||
criterion=criterion,
|
|
||||||
y_mean=y_mean,
|
|
||||||
y_std=y_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
scaler=scaler,
|
|
||||||
amp_enabled=amp_enabled,
|
|
||||||
dominant_freq_min_hz=config.train.dominant_freq_min_hz,
|
|
||||||
dominant_freq_max_hz=config.train.dominant_freq_max_hz,
|
|
||||||
)
|
|
||||||
|
|
||||||
current_monitor_value = val_metrics[monitor_name]
|
|
||||||
scheduler.step(current_monitor_value)
|
|
||||||
current_lr = optimizer.param_groups[0]["lr"]
|
|
||||||
|
|
||||||
history_row: dict[str, float | int] = {
|
|
||||||
"epoch": epoch,
|
|
||||||
"lr": current_lr,
|
|
||||||
"train_loss": train_metrics["loss"],
|
|
||||||
"train_time_loss": train_metrics["time_loss"],
|
|
||||||
"train_weighted_time_loss": train_metrics["weighted_time_loss"],
|
|
||||||
"train_fft_loss": train_metrics["fft_loss"],
|
|
||||||
"train_rms_loss": train_metrics["rms_loss"],
|
|
||||||
"train_scale_loss": train_metrics["scale_loss"],
|
|
||||||
"train_underestimate_loss": train_metrics["underestimate_loss"],
|
|
||||||
"train_envelope_loss": train_metrics["envelope_loss"],
|
|
||||||
"train_rms_error": train_metrics["rms_error"],
|
|
||||||
"train_dominant_freq_error": train_metrics["dominant_freq_error"],
|
|
||||||
"val_loss": val_metrics["loss"],
|
|
||||||
"val_time_loss": val_metrics["time_loss"],
|
|
||||||
"val_weighted_time_loss": val_metrics["weighted_time_loss"],
|
|
||||||
"val_fft_loss": val_metrics["fft_loss"],
|
|
||||||
"val_rms_loss": val_metrics["rms_loss"],
|
|
||||||
"val_scale_loss": val_metrics["scale_loss"],
|
|
||||||
"val_underestimate_loss": val_metrics["underestimate_loss"],
|
|
||||||
"val_envelope_loss": val_metrics["envelope_loss"],
|
|
||||||
"val_rms_error": val_metrics["rms_error"],
|
|
||||||
"val_dominant_freq_error": val_metrics["dominant_freq_error"],
|
|
||||||
}
|
|
||||||
history_rows.append(history_row)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Epoch {epoch:03d} | "
|
|
||||||
f"train_loss={train_metrics['loss']:.6f} | "
|
|
||||||
f"val_loss={val_metrics['loss']:.6f} | "
|
|
||||||
f"val_rms={val_metrics['rms_error']:.6f} | "
|
|
||||||
f"val_dom_freq_err={val_metrics['dominant_freq_error']:.6f} Hz | "
|
|
||||||
f"monitor=val_{monitor_name}:{current_monitor_value:.6f} | "
|
|
||||||
f"lr={current_lr:.2e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_monitor_value < best_monitor_value:
|
|
||||||
best_monitor_value = current_monitor_value
|
|
||||||
stale_epochs = 0
|
|
||||||
torch.save(
|
|
||||||
{
|
|
||||||
"model_state_dict": model.state_dict(),
|
|
||||||
"optimizer_state_dict": optimizer.state_dict(),
|
|
||||||
"config": serialize_for_checkpoint(asdict(config)),
|
|
||||||
"epoch": epoch,
|
|
||||||
"best_monitor_name": monitor_name,
|
|
||||||
"best_monitor_value": best_monitor_value,
|
|
||||||
"val_metrics": val_metrics,
|
|
||||||
},
|
|
||||||
best_model_path,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stale_epochs += 1
|
|
||||||
|
|
||||||
if stale_epochs >= config.train.early_stop_patience:
|
|
||||||
print(f"Early stopping triggered after {epoch} epochs.")
|
|
||||||
break
|
|
||||||
|
|
||||||
history_df = pd.DataFrame(history_rows)
|
|
||||||
history_df.to_csv(history_path, index=False)
|
|
||||||
print(f"Best model saved to: {best_model_path}")
|
|
||||||
print(f"Training history saved to: {history_path}")
|
|
||||||
|
|
||||||
if best_model_path.exists():
|
|
||||||
checkpoint = torch.load(best_model_path, map_location=device, weights_only=False)
|
|
||||||
model.load_state_dict(checkpoint["model_state_dict"])
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
test_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=dataloaders["test"],
|
|
||||||
optimizer=None,
|
|
||||||
criterion=criterion,
|
|
||||||
y_mean=y_mean,
|
|
||||||
y_std=y_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
scaler=scaler,
|
|
||||||
amp_enabled=amp_enabled,
|
|
||||||
dominant_freq_min_hz=config.train.dominant_freq_min_hz,
|
|
||||||
dominant_freq_max_hz=config.train.dominant_freq_max_hz,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
"Test metrics | "
|
|
||||||
f"loss={test_metrics['loss']:.6f} | "
|
|
||||||
f"rms={test_metrics['rms_error']:.6f} | "
|
|
||||||
f"dominant_freq_error={test_metrics['dominant_freq_error']:.6f} Hz"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_config(args.task)
|
|
||||||
|
|
||||||
if args.epochs is not None:
|
|
||||||
config.train.epochs = args.epochs
|
|
||||||
if args.batch_size is not None:
|
|
||||||
config.data.batch_size = args.batch_size
|
|
||||||
if args.device is not None:
|
|
||||||
config.train.device = args.device
|
|
||||||
|
|
||||||
train_model(config)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DataConfig:
|
|
||||||
project_root: Path = field(default_factory=lambda: Path(__file__).resolve().parents[1])
|
|
||||||
scenario: str = "Non_TMD"
|
|
||||||
train_split_name: str = "train"
|
|
||||||
val_split_name: str = "val"
|
|
||||||
test_split_name: str = "test"
|
|
||||||
csv_pattern: str = "*.csv"
|
|
||||||
|
|
||||||
code_column: str = "code"
|
|
||||||
time_column: str = "time"
|
|
||||||
base_sensor_code: str = "WSMS00012"
|
|
||||||
base_axis: str = "value1"
|
|
||||||
response_sensor_code: str = "WSMS00007"
|
|
||||||
response_axis: str = "value3"
|
|
||||||
|
|
||||||
max_sequence_length: int = 4096
|
|
||||||
min_sequence_length: int = 512
|
|
||||||
batch_size: int = 8
|
|
||||||
num_workers: int = 0
|
|
||||||
pin_memory: bool = True
|
|
||||||
use_weighted_train_sampler: bool = True
|
|
||||||
train_weight_power: float = 1.0
|
|
||||||
train_weight_min: float = 0.5
|
|
||||||
train_weight_max: float = 8.0
|
|
||||||
low_frequency_emphasis_power: float = 1.25
|
|
||||||
low_frequency_reference_hz: float = 1.0
|
|
||||||
|
|
||||||
use_steady_state_only: bool = True
|
|
||||||
steady_state_start_ratio: float = 0.50
|
|
||||||
steady_state_min_samples: int = 256
|
|
||||||
|
|
||||||
interpolation_method: str = "linear"
|
|
||||||
normalization_eps: float = 1e-6
|
|
||||||
|
|
||||||
downloads_dir: Path = field(init=False)
|
|
||||||
scenario_dir: Path = field(init=False)
|
|
||||||
train_dir: Path = field(init=False)
|
|
||||||
val_dir: Path = field(init=False)
|
|
||||||
test_dir: Path = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
self.project_root = Path(self.project_root).resolve()
|
|
||||||
self.downloads_dir = self.project_root / "downloads"
|
|
||||||
self.scenario_dir = self.downloads_dir / self.scenario
|
|
||||||
self.train_dir = self.scenario_dir / self.train_split_name
|
|
||||||
self.val_dir = self.scenario_dir / self.val_split_name
|
|
||||||
self.test_dir = self.scenario_dir / self.test_split_name
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ModelConfig:
|
|
||||||
input_channels: int = 1
|
|
||||||
tcn_channels: tuple[int, ...] = (32, 32, 64, 64)
|
|
||||||
kernel_size: int = 7
|
|
||||||
dropout: float = 0.15
|
|
||||||
dilation_base: int = 2
|
|
||||||
pooled_feature_dim: int = 128
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TrainConfig:
|
|
||||||
epochs: int = 120
|
|
||||||
learning_rate: float = 1e-3
|
|
||||||
weight_decay: float = 1e-4
|
|
||||||
seed: int = 42
|
|
||||||
device: str = "cuda"
|
|
||||||
grad_clip_norm: float = 1.0
|
|
||||||
use_amp: bool = True
|
|
||||||
lr_scheduler_patience: int = 8
|
|
||||||
lr_scheduler_factor: float = 0.5
|
|
||||||
min_learning_rate: float = 1e-6
|
|
||||||
early_stop_patience: int = 15
|
|
||||||
checkpoint_dir: str = "checkpoints_rms"
|
|
||||||
best_model_name: str = "best_rms_model.pt"
|
|
||||||
history_name: str = "training_history.csv"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LossConfig:
|
|
||||||
relative_rms_weight: float = 1.0
|
|
||||||
log_rms_weight: float = 0.5
|
|
||||||
mae_weight: float = 0.25
|
|
||||||
waveform_l1_weight: float = 0.03
|
|
||||||
waveform_huber_weight: float = 0.05
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ExperimentConfig:
|
|
||||||
data: DataConfig = field(default_factory=DataConfig)
|
|
||||||
model: ModelConfig = field(default_factory=ModelConfig)
|
|
||||||
train: TrainConfig = field(default_factory=TrainConfig)
|
|
||||||
loss: LossConfig = field(default_factory=LossConfig)
|
|
||||||
|
|
||||||
|
|
||||||
def make_rms_forward_config() -> ExperimentConfig:
|
|
||||||
return ExperimentConfig()
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
|
||||||
import torch.nn.functional as F
|
|
||||||
from torch.utils.data import DataLoader, Dataset, WeightedRandomSampler
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import DataConfig, ExperimentConfig
|
|
||||||
except ImportError:
|
|
||||||
from config import DataConfig, ExperimentConfig
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_rms(signal: np.ndarray) -> float:
|
|
||||||
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
|
||||||
return float(np.sqrt(np.mean(np.square(signal))))
|
|
||||||
|
|
||||||
|
|
||||||
def extract_frequency_hz(file_name: str) -> float | None:
|
|
||||||
match = re.search(r"(\d+(?:\.\d+)?)Hz", file_name, flags=re.IGNORECASE)
|
|
||||||
if match is None:
|
|
||||||
return None
|
|
||||||
return float(match.group(1))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NormalizationStats:
|
|
||||||
x_mean: torch.Tensor
|
|
||||||
x_std: torch.Tensor
|
|
||||||
y_wave_mean: torch.Tensor
|
|
||||||
y_wave_std: torch.Tensor
|
|
||||||
aux_mean: torch.Tensor
|
|
||||||
aux_std: torch.Tensor
|
|
||||||
target_mean: torch.Tensor
|
|
||||||
target_std: torch.Tensor
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TensorStandardScaler:
|
|
||||||
mean: torch.Tensor
|
|
||||||
std: torch.Tensor
|
|
||||||
|
|
||||||
def transform(self, array: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray:
|
|
||||||
if isinstance(array, torch.Tensor):
|
|
||||||
mean = self.mean.to(array.device, dtype=array.dtype)
|
|
||||||
std = self.std.to(array.device, dtype=array.dtype)
|
|
||||||
return (array - mean) / std
|
|
||||||
np_array = np.asarray(array, dtype=np.float32)
|
|
||||||
return (np_array - self.mean.cpu().numpy()) / self.std.cpu().numpy()
|
|
||||||
|
|
||||||
def inverse_transform(self, array: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray:
|
|
||||||
if isinstance(array, torch.Tensor):
|
|
||||||
mean = self.mean.to(array.device, dtype=array.dtype)
|
|
||||||
std = self.std.to(array.device, dtype=array.dtype)
|
|
||||||
return array * std + mean
|
|
||||||
np_array = np.asarray(array, dtype=np.float32)
|
|
||||||
return np_array * self.std.cpu().numpy() + self.mean.cpu().numpy()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RMSRecord:
|
|
||||||
file_path: Path
|
|
||||||
split: str
|
|
||||||
time: torch.Tensor
|
|
||||||
x_full: torch.Tensor
|
|
||||||
x_model_input: torch.Tensor
|
|
||||||
y_model_target: torch.Tensor
|
|
||||||
x_rms: torch.Tensor
|
|
||||||
y_rms: torch.Tensor
|
|
||||||
target_value: torch.Tensor
|
|
||||||
aux_features: torch.Tensor
|
|
||||||
sample_weight: float
|
|
||||||
interpolation_count: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SplitLoadReport:
|
|
||||||
split: str
|
|
||||||
loaded_files: list[str] = field(default_factory=list)
|
|
||||||
skipped_files: list[tuple[str, str]] = field(default_factory=list)
|
|
||||||
interpolated_files: dict[str, int] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def to_lines(self) -> list[str]:
|
|
||||||
lines = [f"[{self.split}] loaded={len(self.loaded_files)} skipped={len(self.skipped_files)}"]
|
|
||||||
for file_name, reason in self.skipped_files:
|
|
||||||
lines.append(f" - skipped {file_name}: {reason}")
|
|
||||||
for file_name, count in self.interpolated_files.items():
|
|
||||||
if count > 0:
|
|
||||||
lines.append(f" - interpolated {file_name}: missing_points={count}")
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
class RMSRegressionDataset(Dataset):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
records: list[RMSRecord],
|
|
||||||
normalization: NormalizationStats,
|
|
||||||
max_sequence_length: int,
|
|
||||||
) -> None:
|
|
||||||
self.records = records
|
|
||||||
self.normalization = normalization
|
|
||||||
self.max_sequence_length = max_sequence_length
|
|
||||||
self.sample_weights = [record.sample_weight for record in records]
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.records)
|
|
||||||
|
|
||||||
def __getitem__(self, index: int) -> dict[str, Any]:
|
|
||||||
record = self.records[index]
|
|
||||||
x = record.x_model_input
|
|
||||||
if x.shape[0] > self.max_sequence_length:
|
|
||||||
x = x[-self.max_sequence_length :]
|
|
||||||
|
|
||||||
pad_length = self.max_sequence_length - x.shape[0]
|
|
||||||
if pad_length > 0:
|
|
||||||
x = F.pad(x.transpose(0, 1), (pad_length, 0), value=0.0).transpose(0, 1)
|
|
||||||
|
|
||||||
x_norm = (x - self.normalization.x_mean) / self.normalization.x_std
|
|
||||||
y = record.y_model_target
|
|
||||||
if y.shape[0] > self.max_sequence_length:
|
|
||||||
y = y[-self.max_sequence_length :]
|
|
||||||
aux_norm = (record.aux_features - self.normalization.aux_mean) / self.normalization.aux_std
|
|
||||||
target_norm = (record.target_value - self.normalization.target_mean) / self.normalization.target_std
|
|
||||||
if pad_length > 0:
|
|
||||||
y = F.pad(y.transpose(0, 1), (pad_length, 0), value=0.0).transpose(0, 1)
|
|
||||||
y_wave_norm = (y - self.normalization.y_wave_mean) / self.normalization.y_wave_std
|
|
||||||
|
|
||||||
valid_length = min(record.x_model_input.shape[0], self.max_sequence_length)
|
|
||||||
mask = torch.zeros(self.max_sequence_length, dtype=torch.float32)
|
|
||||||
mask[-valid_length:] = 1.0
|
|
||||||
|
|
||||||
return {
|
|
||||||
"x": x_norm,
|
|
||||||
"aux": aux_norm,
|
|
||||||
"y": target_norm,
|
|
||||||
"y_wave": y_wave_norm,
|
|
||||||
"y_raw": record.y_rms,
|
|
||||||
"x_rms_raw": record.x_rms,
|
|
||||||
"frequency_hz": torch.tensor(record.aux_features[0].item(), dtype=torch.float32),
|
|
||||||
"mask": mask,
|
|
||||||
"file_name": record.file_path.name,
|
|
||||||
"file_path": str(record.file_path),
|
|
||||||
"valid_length": torch.tensor(valid_length, dtype=torch.long),
|
|
||||||
"sample_weight": torch.tensor(record.sample_weight, dtype=torch.float32),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def list_split_files(config: DataConfig, split: str) -> list[Path]:
|
|
||||||
split_dir = getattr(config, f"{split}_dir")
|
|
||||||
return sorted(split_dir.glob(config.csv_pattern))
|
|
||||||
|
|
||||||
|
|
||||||
def _value_frame(df: pd.DataFrame, config: DataConfig, sensor_code: str, value_column: str) -> pd.DataFrame:
|
|
||||||
sensor_df = df.loc[df[config.code_column] == sensor_code, [config.time_column, value_column]].copy()
|
|
||||||
sensor_df = sensor_df.sort_values(config.time_column)
|
|
||||||
sensor_df = sensor_df.drop_duplicates(subset=config.time_column, keep="first")
|
|
||||||
sensor_df[config.time_column] = sensor_df[config.time_column].astype("float64")
|
|
||||||
sensor_df[value_column] = sensor_df[value_column].astype("float32")
|
|
||||||
return sensor_df
|
|
||||||
|
|
||||||
|
|
||||||
def select_model_segment(signal: np.ndarray, config: DataConfig) -> np.ndarray:
|
|
||||||
if not config.use_steady_state_only:
|
|
||||||
return signal
|
|
||||||
start_index = int(len(signal) * config.steady_state_start_ratio)
|
|
||||||
max_start = max(0, len(signal) - config.steady_state_min_samples)
|
|
||||||
start_index = min(start_index, max_start)
|
|
||||||
return signal[start_index:]
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_dominant_frequency(signal: np.ndarray, sampling_rate: float) -> float:
|
|
||||||
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
|
||||||
if signal.size < 4:
|
|
||||||
return 0.0
|
|
||||||
fft_values = np.fft.rfft(signal)
|
|
||||||
freqs = np.fft.rfftfreq(signal.size, d=1.0 / sampling_rate)
|
|
||||||
magnitudes = np.abs(fft_values)
|
|
||||||
magnitudes[0] = 0.0
|
|
||||||
band_mask = (freqs >= 0.1) & (freqs <= 5.0)
|
|
||||||
if not np.any(band_mask):
|
|
||||||
return 0.0
|
|
||||||
masked_magnitudes = np.where(band_mask, magnitudes, 0.0)
|
|
||||||
return float(freqs[int(np.argmax(masked_magnitudes))])
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_sampling_rate(time_values: np.ndarray) -> float:
|
|
||||||
if time_values.size < 2:
|
|
||||||
return 100.0
|
|
||||||
dt = np.diff(time_values)
|
|
||||||
dt = dt[np.isfinite(dt)]
|
|
||||||
dt = dt[dt > 0.0]
|
|
||||||
if dt.size == 0:
|
|
||||||
return 100.0
|
|
||||||
return float(1.0 / np.median(dt))
|
|
||||||
|
|
||||||
|
|
||||||
def compute_sample_weight(y_rms: float, x_rms: float, freq_hz: float, config: DataConfig) -> float:
|
|
||||||
gain = y_rms / max(x_rms, config.normalization_eps)
|
|
||||||
low_freq_factor = (config.low_frequency_reference_hz / max(freq_hz, config.normalization_eps)) ** config.low_frequency_emphasis_power
|
|
||||||
weight = (gain ** config.train_weight_power) * low_freq_factor
|
|
||||||
return float(np.clip(weight, config.train_weight_min, config.train_weight_max))
|
|
||||||
|
|
||||||
|
|
||||||
def load_split_records(config: DataConfig, split: str) -> tuple[list[RMSRecord], SplitLoadReport]:
|
|
||||||
records: list[RMSRecord] = []
|
|
||||||
report = SplitLoadReport(split=split)
|
|
||||||
|
|
||||||
for file_path in list_split_files(config, split):
|
|
||||||
df = pd.read_csv(file_path)
|
|
||||||
base_df = _value_frame(df, config, config.base_sensor_code, config.base_axis)
|
|
||||||
response_df = _value_frame(df, config, config.response_sensor_code, config.response_axis)
|
|
||||||
|
|
||||||
if base_df.empty or response_df.empty:
|
|
||||||
report.skipped_files.append((file_path.name, "missing required sensor"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
aligned = base_df.rename(columns={config.base_axis: "base_signal"})
|
|
||||||
aligned = aligned.merge(
|
|
||||||
response_df.rename(columns={config.response_axis: "response_signal"}),
|
|
||||||
on=config.time_column,
|
|
||||||
how="left",
|
|
||||||
)
|
|
||||||
interpolation_count = int(aligned["response_signal"].isna().sum())
|
|
||||||
aligned["response_signal"] = aligned["response_signal"].interpolate(
|
|
||||||
method=config.interpolation_method,
|
|
||||||
limit_direction="both",
|
|
||||||
).ffill().bfill()
|
|
||||||
|
|
||||||
if aligned["response_signal"].isna().any():
|
|
||||||
report.skipped_files.append((file_path.name, "remaining NaN after interpolation"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
time_values = aligned[config.time_column].to_numpy(dtype=np.float64)
|
|
||||||
x_values = aligned["base_signal"].to_numpy(dtype=np.float32)
|
|
||||||
y_values = aligned["response_signal"].to_numpy(dtype=np.float32)
|
|
||||||
if len(x_values) < config.min_sequence_length:
|
|
||||||
report.skipped_files.append((file_path.name, f"sequence too short: {len(x_values)}"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
x_model = select_model_segment(x_values, config)
|
|
||||||
y_model = select_model_segment(y_values, config)
|
|
||||||
time_model = select_model_segment(time_values, config)
|
|
||||||
sampling_rate = estimate_sampling_rate(time_model)
|
|
||||||
file_frequency = extract_frequency_hz(file_path.name)
|
|
||||||
x_rms = calculate_rms(x_model)
|
|
||||||
y_rms = calculate_rms(y_model)
|
|
||||||
target_value = float(np.log(max(y_rms / max(x_rms, config.normalization_eps), config.normalization_eps)))
|
|
||||||
dominant_freq = estimate_dominant_frequency(x_model, sampling_rate)
|
|
||||||
feature_frequency = file_frequency if file_frequency is not None else dominant_freq
|
|
||||||
sample_weight = compute_sample_weight(y_rms=y_rms, x_rms=x_rms, freq_hz=feature_frequency, config=config) if split == "train" else 1.0
|
|
||||||
|
|
||||||
aux_features = torch.tensor(
|
|
||||||
[feature_frequency, x_rms, float(len(x_model)) / float(config.max_sequence_length)],
|
|
||||||
dtype=torch.float32,
|
|
||||||
)
|
|
||||||
records.append(
|
|
||||||
RMSRecord(
|
|
||||||
file_path=file_path,
|
|
||||||
split=split,
|
|
||||||
time=torch.tensor(time_model, dtype=torch.float64),
|
|
||||||
x_full=torch.tensor(x_values[:, None], dtype=torch.float32),
|
|
||||||
x_model_input=torch.tensor(x_model[:, None], dtype=torch.float32),
|
|
||||||
y_model_target=torch.tensor(y_model[:, None], dtype=torch.float32),
|
|
||||||
x_rms=torch.tensor([x_rms], dtype=torch.float32),
|
|
||||||
y_rms=torch.tensor([y_rms], dtype=torch.float32),
|
|
||||||
target_value=torch.tensor([target_value], dtype=torch.float32),
|
|
||||||
aux_features=aux_features,
|
|
||||||
sample_weight=sample_weight,
|
|
||||||
interpolation_count=interpolation_count,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
report.loaded_files.append(file_path.name)
|
|
||||||
if interpolation_count > 0:
|
|
||||||
report.interpolated_files[file_path.name] = interpolation_count
|
|
||||||
|
|
||||||
if not records:
|
|
||||||
raise RuntimeError(f"No usable records found for split='{split}'.")
|
|
||||||
return records, report
|
|
||||||
|
|
||||||
|
|
||||||
def fit_normalization(records: list[RMSRecord], config: DataConfig) -> NormalizationStats:
|
|
||||||
x_all = torch.cat([record.x_model_input for record in records], dim=0)
|
|
||||||
y_all = torch.cat([record.y_model_target for record in records], dim=0)
|
|
||||||
aux_all = torch.stack([record.aux_features for record in records], dim=0)
|
|
||||||
target_all = torch.cat([record.target_value for record in records], dim=0)
|
|
||||||
|
|
||||||
def safe_std(tensor: torch.Tensor, dim: int) -> torch.Tensor:
|
|
||||||
std = tensor.std(dim=dim, unbiased=False)
|
|
||||||
return torch.clamp(std, min=config.normalization_eps)
|
|
||||||
|
|
||||||
return NormalizationStats(
|
|
||||||
x_mean=x_all.mean(dim=0),
|
|
||||||
x_std=safe_std(x_all, dim=0),
|
|
||||||
y_wave_mean=y_all.mean(dim=0),
|
|
||||||
y_wave_std=safe_std(y_all, dim=0),
|
|
||||||
aux_mean=aux_all.mean(dim=0),
|
|
||||||
aux_std=safe_std(aux_all, dim=0),
|
|
||||||
target_mean=target_all.mean(dim=0, keepdim=True),
|
|
||||||
target_std=safe_std(target_all, dim=0).view(1),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_datasets(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[dict[str, RMSRegressionDataset], NormalizationStats, dict[str, SplitLoadReport]]:
|
|
||||||
data_config = config.data if isinstance(config, ExperimentConfig) else config
|
|
||||||
train_records, train_report = load_split_records(data_config, "train")
|
|
||||||
normalization = fit_normalization(train_records, data_config)
|
|
||||||
val_records, val_report = load_split_records(data_config, "val")
|
|
||||||
test_records, test_report = load_split_records(data_config, "test")
|
|
||||||
|
|
||||||
datasets = {
|
|
||||||
"train": RMSRegressionDataset(train_records, normalization, data_config.max_sequence_length),
|
|
||||||
"val": RMSRegressionDataset(val_records, normalization, data_config.max_sequence_length),
|
|
||||||
"test": RMSRegressionDataset(test_records, normalization, data_config.max_sequence_length),
|
|
||||||
}
|
|
||||||
return datasets, normalization, {"train": train_report, "val": val_report, "test": test_report}
|
|
||||||
|
|
||||||
|
|
||||||
def build_dataloaders(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[dict[str, DataLoader], dict[str, RMSRegressionDataset], dict[str, SplitLoadReport]]:
|
|
||||||
data_config = config.data if isinstance(config, ExperimentConfig) else config
|
|
||||||
datasets, _, reports = build_datasets(config)
|
|
||||||
|
|
||||||
train_sampler = None
|
|
||||||
train_shuffle = True
|
|
||||||
if data_config.use_weighted_train_sampler:
|
|
||||||
weights = torch.tensor(datasets["train"].sample_weights, dtype=torch.double)
|
|
||||||
train_sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True)
|
|
||||||
train_shuffle = False
|
|
||||||
|
|
||||||
loaders = {
|
|
||||||
"train": DataLoader(
|
|
||||||
datasets["train"],
|
|
||||||
batch_size=data_config.batch_size,
|
|
||||||
shuffle=train_shuffle,
|
|
||||||
sampler=train_sampler,
|
|
||||||
num_workers=data_config.num_workers,
|
|
||||||
pin_memory=data_config.pin_memory,
|
|
||||||
),
|
|
||||||
"val": DataLoader(
|
|
||||||
datasets["val"],
|
|
||||||
batch_size=data_config.batch_size,
|
|
||||||
shuffle=False,
|
|
||||||
num_workers=data_config.num_workers,
|
|
||||||
pin_memory=data_config.pin_memory,
|
|
||||||
),
|
|
||||||
"test": DataLoader(
|
|
||||||
datasets["test"],
|
|
||||||
batch_size=data_config.batch_size,
|
|
||||||
shuffle=False,
|
|
||||||
num_workers=data_config.num_workers,
|
|
||||||
pin_memory=data_config.pin_memory,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return loaders, datasets, reports
|
|
||||||
|
|
||||||
|
|
||||||
def get_dataloaders(
|
|
||||||
config: ExperimentConfig | DataConfig,
|
|
||||||
) -> tuple[
|
|
||||||
dict[str, DataLoader],
|
|
||||||
dict[str, RMSRegressionDataset],
|
|
||||||
dict[str, SplitLoadReport],
|
|
||||||
TensorStandardScaler,
|
|
||||||
TensorStandardScaler,
|
|
||||||
TensorStandardScaler,
|
|
||||||
]:
|
|
||||||
loaders, datasets, reports = build_dataloaders(config)
|
|
||||||
normalization = datasets["train"].normalization
|
|
||||||
x_scaler = TensorStandardScaler(normalization.x_mean, normalization.x_std)
|
|
||||||
aux_scaler = TensorStandardScaler(normalization.aux_mean, normalization.aux_std)
|
|
||||||
y_scaler = TensorStandardScaler(normalization.target_mean, normalization.target_std)
|
|
||||||
return loaders, datasets, reports, x_scaler, aux_scaler, y_scaler
|
|
||||||
|
|
||||||
|
|
||||||
def report_to_text(reports: dict[str, SplitLoadReport]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
for split in ("train", "val", "test"):
|
|
||||||
lines.extend(reports[split].to_lines())
|
|
||||||
return "\n".join(lines)
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, make_rms_forward_config
|
|
||||||
from .dataset import get_dataloaders, report_to_text
|
|
||||||
from .model import build_model
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, make_rms_forward_config
|
|
||||||
from dataset import get_dataloaders, report_to_text
|
|
||||||
from model import build_model
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Evaluate direct RMS regression model on harmonic data.")
|
|
||||||
parser.add_argument("--split", choices=("train", "val", "test"), default="val")
|
|
||||||
parser.add_argument("--sample-index", type=int, default=0)
|
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
parser.add_argument("--checkpoint", type=str, default=None)
|
|
||||||
parser.add_argument("--all-samples", action="store_true")
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str | None, config: ExperimentConfig) -> torch.device:
|
|
||||||
requested = device_name or config.train.device
|
|
||||||
if requested.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(requested)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_checkpoint_path(config: ExperimentConfig, checkpoint_arg: str | None) -> Path:
|
|
||||||
if checkpoint_arg:
|
|
||||||
return Path(checkpoint_arg).resolve()
|
|
||||||
return config.data.project_root / config.train.checkpoint_dir / "forward_rms" / config.train.best_model_name
|
|
||||||
|
|
||||||
|
|
||||||
def relative_percent_error(true_value: float, pred_value: float) -> float:
|
|
||||||
denominator = max(abs(true_value), 1e-12)
|
|
||||||
return abs(pred_value - true_value) / denominator * 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def inverse_waveform(array: torch.Tensor, mean: torch.Tensor, std: torch.Tensor) -> torch.Tensor:
|
|
||||||
mean = mean.to(array.device, dtype=array.dtype)
|
|
||||||
std = std.to(array.device, dtype=array.dtype)
|
|
||||||
return array * std + mean
|
|
||||||
|
|
||||||
|
|
||||||
def predict_sample(
|
|
||||||
model: torch.nn.Module,
|
|
||||||
sample: dict[str, object],
|
|
||||||
device: torch.device,
|
|
||||||
y_scaler,
|
|
||||||
x_scaler,
|
|
||||||
y_wave_mean: torch.Tensor,
|
|
||||||
y_wave_std: torch.Tensor,
|
|
||||||
) -> dict[str, float | str | np.ndarray]:
|
|
||||||
x = sample["x"].unsqueeze(0).to(device)
|
|
||||||
aux = sample["aux"].unsqueeze(0).to(device)
|
|
||||||
mask = sample["mask"].unsqueeze(0).to(device)
|
|
||||||
x_rms_raw = sample["x_rms_raw"].unsqueeze(0).to(device)
|
|
||||||
y_true = float(sample["y_raw"].detach().cpu().numpy().reshape(-1)[0])
|
|
||||||
frequency_hz = float(sample["frequency_hz"].item())
|
|
||||||
file_name = str(sample["file_name"])
|
|
||||||
valid_mask = sample["mask"].detach().cpu().numpy() > 0.5
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
outputs = model(x, mask, aux)
|
|
||||||
|
|
||||||
pred_target = y_scaler.inverse_transform(outputs["rms"])
|
|
||||||
y_pred = float((torch.exp(pred_target) * x_rms_raw).detach().cpu().numpy().reshape(-1)[0])
|
|
||||||
pred_wave = inverse_waveform(outputs["waveform"], y_wave_mean, y_wave_std).detach().cpu().numpy().reshape(-1)[valid_mask]
|
|
||||||
true_wave = inverse_waveform(sample["y_wave"], y_wave_mean.cpu(), y_wave_std.cpu()).detach().cpu().numpy().reshape(-1)[valid_mask]
|
|
||||||
x_wave = x_scaler.inverse_transform(sample["x"]).reshape(-1)[valid_mask]
|
|
||||||
return {
|
|
||||||
"file_name": file_name,
|
|
||||||
"frequency_hz": frequency_hz,
|
|
||||||
"true_rms": y_true,
|
|
||||||
"pred_rms": y_pred,
|
|
||||||
"relative_error_percent": float(relative_percent_error(y_true, y_pred)),
|
|
||||||
"x_wave": x_wave.numpy().reshape(-1),
|
|
||||||
"true_wave": true_wave,
|
|
||||||
"pred_wave": pred_wave,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_sample(
|
|
||||||
model: torch.nn.Module,
|
|
||||||
sample: dict[str, object],
|
|
||||||
device: torch.device,
|
|
||||||
y_scaler,
|
|
||||||
x_scaler,
|
|
||||||
y_wave_mean: torch.Tensor,
|
|
||||||
y_wave_std: torch.Tensor,
|
|
||||||
) -> dict[str, float | str]:
|
|
||||||
result = predict_sample(model, sample, device, y_scaler, x_scaler, y_wave_mean, y_wave_std)
|
|
||||||
return {
|
|
||||||
"file_name": str(result["file_name"]),
|
|
||||||
"frequency_hz": float(result["frequency_hz"]),
|
|
||||||
"true_rms": float(result["true_rms"]),
|
|
||||||
"pred_rms": float(result["pred_rms"]),
|
|
||||||
"relative_error_percent": float(result["relative_error_percent"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def save_single_sample_figure(result: dict[str, float | str | np.ndarray], split: str, save_dir: Path, sample_index: int) -> Path:
|
|
||||||
true_rms = float(result["true_rms"])
|
|
||||||
pred_rms = float(result["pred_rms"])
|
|
||||||
error_percent = relative_percent_error(true_rms, pred_rms)
|
|
||||||
x_signal = np.asarray(result["x_wave"], dtype=np.float64).reshape(-1)
|
|
||||||
true_wave = np.asarray(result["true_wave"], dtype=np.float64).reshape(-1)
|
|
||||||
pred_wave = np.asarray(result["pred_wave"], dtype=np.float64).reshape(-1)
|
|
||||||
time_steps = np.arange(x_signal.shape[0], dtype=np.float64)
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
|
|
||||||
fig.suptitle(f"Forward RMS regression | {split} | {result['file_name']}")
|
|
||||||
axes[0].plot(time_steps, x_signal, linewidth=1.2)
|
|
||||||
axes[0].set_title("Input Base Excitation (Steady-State Segment)")
|
|
||||||
axes[0].set_xlabel("Time Step")
|
|
||||||
axes[0].set_ylabel("Acceleration")
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
axes[1].plot(time_steps, true_wave, label="True response", linewidth=1.2, color="tab:blue")
|
|
||||||
axes[1].plot(time_steps, pred_wave, label="Pred response", linewidth=1.2, color="tab:orange", alpha=0.85)
|
|
||||||
axes[1].set_title("Auxiliary Waveform Head: True vs Predicted Top Response")
|
|
||||||
axes[1].set_xlabel("Time Step")
|
|
||||||
axes[1].set_ylabel("Acceleration")
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
axes[1].legend()
|
|
||||||
axes[2].bar(["True RMS", "Pred RMS"], [true_rms, pred_rms], color=["tab:blue", "tab:orange"])
|
|
||||||
axes[2].set_title(f"True RMS: {true_rms:.4f}, Pred RMS: {pred_rms:.4f}, Error: {error_percent:.2f}%")
|
|
||||||
axes[2].set_ylabel("RMS")
|
|
||||||
axes[2].grid(True, axis="y", alpha=0.3)
|
|
||||||
plt.tight_layout()
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
figure_path = save_dir / f"evaluation_{split}_s{sample_index}_waveform.png"
|
|
||||||
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
|
|
||||||
plt.show()
|
|
||||||
plt.close(fig)
|
|
||||||
return figure_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_rms_forward_config()
|
|
||||||
device = resolve_device(args.device, config)
|
|
||||||
checkpoint_path = resolve_checkpoint_path(config, args.checkpoint)
|
|
||||||
loaders, datasets, reports, x_scaler, aux_scaler, y_scaler = get_dataloaders(config)
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
if not checkpoint_path.exists():
|
|
||||||
raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
|
|
||||||
|
|
||||||
dataset = datasets[args.split]
|
|
||||||
sample = dataset[args.sample_index]
|
|
||||||
normalization = datasets["train"].normalization
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
|
||||||
model.load_state_dict(checkpoint["model_state_dict"])
|
|
||||||
model.eval()
|
|
||||||
save_dir = config.data.project_root / "evaluation_outputs" / "forward_rms"
|
|
||||||
if args.all_samples:
|
|
||||||
rows = [
|
|
||||||
evaluate_sample(
|
|
||||||
model,
|
|
||||||
dataset[index],
|
|
||||||
device,
|
|
||||||
y_scaler,
|
|
||||||
x_scaler,
|
|
||||||
normalization.y_wave_mean,
|
|
||||||
normalization.y_wave_std,
|
|
||||||
)
|
|
||||||
for index in range(len(dataset))
|
|
||||||
]
|
|
||||||
result_df = pd.DataFrame(rows)
|
|
||||||
result_df = result_df.sort_values(["relative_error_percent", "file_name"]).reset_index(drop=True)
|
|
||||||
csv_path = save_dir / f"evaluation_{args.split}_all_samples.csv"
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
result_df.to_csv(csv_path, index=False)
|
|
||||||
summary = {
|
|
||||||
"count": len(result_df),
|
|
||||||
"mean_error_percent": float(result_df["relative_error_percent"].mean()),
|
|
||||||
"median_error_percent": float(result_df["relative_error_percent"].median()),
|
|
||||||
"max_error_percent": float(result_df["relative_error_percent"].max()),
|
|
||||||
"min_error_percent": float(result_df["relative_error_percent"].min()),
|
|
||||||
}
|
|
||||||
print(f"Checkpoint: {checkpoint_path}")
|
|
||||||
print(f"Summary: {summary}")
|
|
||||||
print(f"CSV saved to: {csv_path}")
|
|
||||||
print(result_df.to_string(index=False))
|
|
||||||
return
|
|
||||||
|
|
||||||
result = predict_sample(
|
|
||||||
model,
|
|
||||||
sample,
|
|
||||||
device,
|
|
||||||
y_scaler,
|
|
||||||
x_scaler,
|
|
||||||
normalization.y_wave_mean,
|
|
||||||
normalization.y_wave_std,
|
|
||||||
)
|
|
||||||
figure_path = save_single_sample_figure(
|
|
||||||
result=result,
|
|
||||||
split=args.split,
|
|
||||||
save_dir=save_dir,
|
|
||||||
sample_index=args.sample_index,
|
|
||||||
)
|
|
||||||
print(f"Checkpoint: {checkpoint_path}")
|
|
||||||
print(f"Sample file: {result['file_name']}")
|
|
||||||
print(f"True RMS: {result['true_rms']:.6f}")
|
|
||||||
print(f"Pred RMS: {result['pred_rms']:.6f}")
|
|
||||||
print(f"Relative RMS Error (%): {result['relative_error_percent']:.4f}")
|
|
||||||
print(f"Figure saved to: {figure_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
140
src_new/model.py
@@ -1,140 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.nn.utils import weight_norm
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, ModelConfig
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, ModelConfig
|
|
||||||
|
|
||||||
|
|
||||||
class Chomp1d(nn.Module):
|
|
||||||
def __init__(self, chomp_size: int) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.chomp_size = chomp_size
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
if self.chomp_size == 0:
|
|
||||||
return x
|
|
||||||
return x[:, :, :-self.chomp_size].contiguous()
|
|
||||||
|
|
||||||
|
|
||||||
class CausalConv1d(nn.Module):
|
|
||||||
def __init__(self, in_channels: int, out_channels: int, kernel_size: int, dilation: int = 1) -> None:
|
|
||||||
super().__init__()
|
|
||||||
padding = (kernel_size - 1) * dilation
|
|
||||||
self.net = nn.Sequential(
|
|
||||||
weight_norm(
|
|
||||||
nn.Conv1d(
|
|
||||||
in_channels=in_channels,
|
|
||||||
out_channels=out_channels,
|
|
||||||
kernel_size=kernel_size,
|
|
||||||
padding=padding,
|
|
||||||
dilation=dilation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Chomp1d(padding),
|
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
return self.net(x)
|
|
||||||
|
|
||||||
|
|
||||||
class TemporalBlock(nn.Module):
|
|
||||||
def __init__(self, in_channels: int, out_channels: int, kernel_size: int, dilation: int, dropout: float) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.conv1 = CausalConv1d(in_channels, out_channels, kernel_size, dilation=dilation)
|
|
||||||
self.act1 = nn.GELU()
|
|
||||||
self.dropout1 = nn.Dropout(dropout)
|
|
||||||
self.conv2 = CausalConv1d(out_channels, out_channels, kernel_size, dilation=dilation)
|
|
||||||
self.act2 = nn.GELU()
|
|
||||||
self.dropout2 = nn.Dropout(dropout)
|
|
||||||
self.residual = nn.Conv1d(in_channels, out_channels, kernel_size=1) if in_channels != out_channels else nn.Identity()
|
|
||||||
self.final_act = nn.GELU()
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
residual = self.residual(x)
|
|
||||||
out = self.dropout1(self.act1(self.conv1(x)))
|
|
||||||
out = self.dropout2(self.act2(self.conv2(out)))
|
|
||||||
return self.final_act(out + residual)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReceptiveFieldInfo:
|
|
||||||
receptive_field: int
|
|
||||||
dilations: tuple[int, ...]
|
|
||||||
|
|
||||||
|
|
||||||
class RMSRegressionTCN(nn.Module):
|
|
||||||
def __init__(self, config: ModelConfig) -> None:
|
|
||||||
super().__init__()
|
|
||||||
blocks: list[nn.Module] = []
|
|
||||||
in_channels = config.input_channels
|
|
||||||
dilations: list[int] = []
|
|
||||||
|
|
||||||
for level, out_channels in enumerate(config.tcn_channels):
|
|
||||||
dilation = config.dilation_base ** level
|
|
||||||
dilations.append(dilation)
|
|
||||||
blocks.append(
|
|
||||||
TemporalBlock(
|
|
||||||
in_channels=in_channels,
|
|
||||||
out_channels=out_channels,
|
|
||||||
kernel_size=config.kernel_size,
|
|
||||||
dilation=dilation,
|
|
||||||
dropout=config.dropout,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
in_channels = out_channels
|
|
||||||
|
|
||||||
self.encoder = nn.Sequential(*blocks)
|
|
||||||
self.rms_head = nn.Sequential(
|
|
||||||
nn.Linear(in_channels * 2 + 3, config.pooled_feature_dim),
|
|
||||||
nn.GELU(),
|
|
||||||
nn.Dropout(config.dropout),
|
|
||||||
nn.Linear(config.pooled_feature_dim, config.pooled_feature_dim),
|
|
||||||
nn.GELU(),
|
|
||||||
nn.Dropout(config.dropout),
|
|
||||||
nn.Linear(config.pooled_feature_dim, 1),
|
|
||||||
)
|
|
||||||
self.waveform_head = nn.Sequential(
|
|
||||||
nn.Conv1d(in_channels, in_channels, kernel_size=1),
|
|
||||||
nn.GELU(),
|
|
||||||
nn.Dropout(config.dropout),
|
|
||||||
nn.Conv1d(in_channels, 1, kernel_size=1),
|
|
||||||
)
|
|
||||||
self.receptive_field_info = compute_receptive_field(config)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor, mask: torch.Tensor, aux: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
||||||
if x.ndim != 3:
|
|
||||||
raise ValueError(f"Expected x shape (batch, seq, channels), got {tuple(x.shape)}")
|
|
||||||
features = self.encoder(x.transpose(1, 2))
|
|
||||||
mask_1d = mask.unsqueeze(1)
|
|
||||||
masked_features = features * mask_1d
|
|
||||||
valid_count = mask_1d.sum(dim=2).clamp_min(1.0)
|
|
||||||
mean_pool = masked_features.sum(dim=2) / valid_count
|
|
||||||
masked_for_max = features.masked_fill(mask_1d == 0.0, float("-inf"))
|
|
||||||
max_pool = masked_for_max.max(dim=2).values
|
|
||||||
max_pool = torch.where(torch.isfinite(max_pool), max_pool, torch.zeros_like(max_pool))
|
|
||||||
fused = torch.cat([mean_pool, max_pool, aux], dim=1)
|
|
||||||
rms_prediction = self.rms_head(fused)
|
|
||||||
waveform_prediction = self.waveform_head(features).transpose(1, 2)
|
|
||||||
return {"rms": rms_prediction, "waveform": waveform_prediction}
|
|
||||||
|
|
||||||
|
|
||||||
def compute_receptive_field(config: ModelConfig) -> ReceptiveFieldInfo:
|
|
||||||
receptive_field = 1
|
|
||||||
dilations: list[int] = []
|
|
||||||
for level, _ in enumerate(config.tcn_channels):
|
|
||||||
dilation = config.dilation_base ** level
|
|
||||||
dilations.append(dilation)
|
|
||||||
receptive_field += 2 * (config.kernel_size - 1) * dilation
|
|
||||||
return ReceptiveFieldInfo(receptive_field=receptive_field, dilations=tuple(dilations))
|
|
||||||
|
|
||||||
|
|
||||||
def build_model(config: ExperimentConfig | ModelConfig) -> RMSRegressionTCN:
|
|
||||||
model_config = config.model if isinstance(config, ExperimentConfig) else config
|
|
||||||
return RMSRegressionTCN(model_config)
|
|
||||||
327
src_new/train.py
@@ -1,327 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import random
|
|
||||||
from dataclasses import asdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
|
||||||
import torch.nn.functional as F
|
|
||||||
from torch import nn
|
|
||||||
from torch.optim import AdamW
|
|
||||||
from torch.optim.lr_scheduler import ReduceLROnPlateau
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .config import ExperimentConfig, make_rms_forward_config
|
|
||||||
from .dataset import build_dataloaders, report_to_text
|
|
||||||
from .model import build_model, compute_receptive_field
|
|
||||||
except ImportError:
|
|
||||||
from config import ExperimentConfig, make_rms_forward_config
|
|
||||||
from dataset import build_dataloaders, report_to_text
|
|
||||||
from model import build_model, compute_receptive_field
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="Train TCN for direct RMS regression.")
|
|
||||||
parser.add_argument("--epochs", type=int, default=None)
|
|
||||||
parser.add_argument("--batch-size", type=int, default=None)
|
|
||||||
parser.add_argument("--device", type=str, default=None)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def set_seed(seed: int) -> None:
|
|
||||||
random.seed(seed)
|
|
||||||
torch.manual_seed(seed)
|
|
||||||
torch.cuda.manual_seed_all(seed)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_device(device_name: str) -> torch.device:
|
|
||||||
if device_name.startswith("cuda") and not torch.cuda.is_available():
|
|
||||||
return torch.device("cpu")
|
|
||||||
return torch.device(device_name)
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_for_checkpoint(value: Any) -> Any:
|
|
||||||
if isinstance(value, Path):
|
|
||||||
return str(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {key: serialize_for_checkpoint(sub_value) for key, sub_value in value.items()}
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [serialize_for_checkpoint(item) for item in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def denormalize_target(
|
|
||||||
pred_norm: torch.Tensor,
|
|
||||||
target_norm: torch.Tensor,
|
|
||||||
target_mean: torch.Tensor,
|
|
||||||
target_std: torch.Tensor,
|
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
||||||
pred = pred_norm * target_std + target_mean
|
|
||||||
target = target_norm * target_std + target_mean
|
|
||||||
return pred, target
|
|
||||||
|
|
||||||
|
|
||||||
def relative_rms_error(pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
|
||||||
return torch.abs(pred - target) / torch.clamp(target.abs(), min=1e-6)
|
|
||||||
|
|
||||||
|
|
||||||
class RMSRegressionLoss(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
relative_rms_weight: float,
|
|
||||||
log_rms_weight: float,
|
|
||||||
mae_weight: float,
|
|
||||||
waveform_l1_weight: float,
|
|
||||||
waveform_huber_weight: float,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.relative_rms_weight = relative_rms_weight
|
|
||||||
self.log_rms_weight = log_rms_weight
|
|
||||||
self.mae_weight = mae_weight
|
|
||||||
self.waveform_l1_weight = waveform_l1_weight
|
|
||||||
self.waveform_huber_weight = waveform_huber_weight
|
|
||||||
|
|
||||||
def forward(
|
|
||||||
self,
|
|
||||||
pred: torch.Tensor,
|
|
||||||
target: torch.Tensor,
|
|
||||||
pred_wave: torch.Tensor,
|
|
||||||
target_wave: torch.Tensor,
|
|
||||||
mask: torch.Tensor,
|
|
||||||
) -> dict[str, torch.Tensor]:
|
|
||||||
rel_loss = relative_rms_error(pred, target).mean()
|
|
||||||
log_loss = F.huber_loss(torch.log(torch.clamp(pred, min=1e-6)), torch.log(torch.clamp(target, min=1e-6)))
|
|
||||||
mae_loss = F.l1_loss(pred, target)
|
|
||||||
mask_expanded = mask.unsqueeze(-1)
|
|
||||||
valid_count = mask_expanded.sum().clamp_min(1.0)
|
|
||||||
wave_residual = (pred_wave - target_wave) * mask_expanded
|
|
||||||
waveform_l1 = torch.abs(wave_residual).sum() / valid_count
|
|
||||||
waveform_huber = F.huber_loss(pred_wave * mask_expanded, target_wave * mask_expanded, reduction="sum") / valid_count
|
|
||||||
total = (
|
|
||||||
self.relative_rms_weight * rel_loss
|
|
||||||
+ self.log_rms_weight * log_loss
|
|
||||||
+ self.mae_weight * mae_loss
|
|
||||||
+ self.waveform_l1_weight * waveform_l1
|
|
||||||
+ self.waveform_huber_weight * waveform_huber
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"total": total,
|
|
||||||
"relative": rel_loss,
|
|
||||||
"log": log_loss,
|
|
||||||
"mae": mae_loss,
|
|
||||||
"waveform_l1": waveform_l1,
|
|
||||||
"waveform_huber": waveform_huber,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def run_epoch(
|
|
||||||
model: nn.Module,
|
|
||||||
dataloader: torch.utils.data.DataLoader,
|
|
||||||
optimizer: AdamW | None,
|
|
||||||
criterion: RMSRegressionLoss,
|
|
||||||
target_mean: torch.Tensor,
|
|
||||||
target_std: torch.Tensor,
|
|
||||||
device: torch.device,
|
|
||||||
grad_clip_norm: float,
|
|
||||||
scaler: torch.cuda.amp.GradScaler,
|
|
||||||
amp_enabled: bool,
|
|
||||||
) -> dict[str, float]:
|
|
||||||
is_train = optimizer is not None
|
|
||||||
model.train(is_train)
|
|
||||||
|
|
||||||
total_loss_sum = 0.0
|
|
||||||
relative_loss_sum = 0.0
|
|
||||||
log_loss_sum = 0.0
|
|
||||||
mae_loss_sum = 0.0
|
|
||||||
waveform_l1_sum = 0.0
|
|
||||||
waveform_huber_sum = 0.0
|
|
||||||
rms_error_sum = 0.0
|
|
||||||
sample_count = 0
|
|
||||||
|
|
||||||
for batch in dataloader:
|
|
||||||
x = batch["x"].to(device)
|
|
||||||
aux = batch["aux"].to(device)
|
|
||||||
mask = batch["mask"].to(device)
|
|
||||||
y_norm = batch["y"].to(device)
|
|
||||||
y_wave = batch["y_wave"].to(device)
|
|
||||||
x_rms_raw = batch["x_rms_raw"].to(device)
|
|
||||||
y_rms_raw = batch["y_raw"].to(device)
|
|
||||||
|
|
||||||
if is_train:
|
|
||||||
optimizer.zero_grad(set_to_none=True)
|
|
||||||
|
|
||||||
with torch.amp.autocast(device_type=device.type, enabled=amp_enabled):
|
|
||||||
outputs = model(x, mask, aux)
|
|
||||||
pred_target, _ = denormalize_target(outputs["rms"], y_norm, target_mean, target_std)
|
|
||||||
pred = torch.exp(pred_target) * x_rms_raw
|
|
||||||
target = y_rms_raw
|
|
||||||
losses = criterion(pred, target, outputs["waveform"], y_wave, mask)
|
|
||||||
|
|
||||||
if is_train:
|
|
||||||
scaler.scale(losses["total"]).backward()
|
|
||||||
scaler.unscale_(optimizer)
|
|
||||||
torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip_norm)
|
|
||||||
scaler.step(optimizer)
|
|
||||||
scaler.update()
|
|
||||||
|
|
||||||
batch_size = x.shape[0]
|
|
||||||
total_loss_sum += losses["total"].detach().item() * batch_size
|
|
||||||
relative_loss_sum += losses["relative"].detach().item() * batch_size
|
|
||||||
log_loss_sum += losses["log"].detach().item() * batch_size
|
|
||||||
mae_loss_sum += losses["mae"].detach().item() * batch_size
|
|
||||||
waveform_l1_sum += losses["waveform_l1"].detach().item() * batch_size
|
|
||||||
waveform_huber_sum += losses["waveform_huber"].detach().item() * batch_size
|
|
||||||
rms_error_sum += relative_rms_error(pred.detach(), target.detach()).mean().item() * batch_size
|
|
||||||
sample_count += batch_size
|
|
||||||
|
|
||||||
return {
|
|
||||||
"loss": total_loss_sum / sample_count,
|
|
||||||
"relative_loss": relative_loss_sum / sample_count,
|
|
||||||
"log_loss": log_loss_sum / sample_count,
|
|
||||||
"mae_loss": mae_loss_sum / sample_count,
|
|
||||||
"waveform_l1": waveform_l1_sum / sample_count,
|
|
||||||
"waveform_huber": waveform_huber_sum / sample_count,
|
|
||||||
"rms_error": rms_error_sum / sample_count,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def checkpoint_paths(config: ExperimentConfig) -> tuple[Path, Path]:
|
|
||||||
root = config.data.project_root / config.train.checkpoint_dir / "forward_rms"
|
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
|
||||||
return root / config.train.best_model_name, root / config.train.history_name
|
|
||||||
|
|
||||||
def train_model(config: ExperimentConfig) -> None:
|
|
||||||
set_seed(config.train.seed)
|
|
||||||
device = resolve_device(config.train.device)
|
|
||||||
loaders, datasets, reports = build_dataloaders(config)
|
|
||||||
train_dataset = datasets["train"]
|
|
||||||
normalization = train_dataset.normalization
|
|
||||||
target_mean = normalization.target_mean.to(device).view(1, 1)
|
|
||||||
target_std = normalization.target_std.to(device).view(1, 1)
|
|
||||||
|
|
||||||
model = build_model(config).to(device)
|
|
||||||
receptive_field = compute_receptive_field(config.model)
|
|
||||||
optimizer = AdamW(model.parameters(), lr=config.train.learning_rate, weight_decay=config.train.weight_decay)
|
|
||||||
scheduler = ReduceLROnPlateau(
|
|
||||||
optimizer,
|
|
||||||
mode="min",
|
|
||||||
factor=config.train.lr_scheduler_factor,
|
|
||||||
patience=config.train.lr_scheduler_patience,
|
|
||||||
min_lr=config.train.min_learning_rate,
|
|
||||||
)
|
|
||||||
criterion = RMSRegressionLoss(
|
|
||||||
relative_rms_weight=config.loss.relative_rms_weight,
|
|
||||||
log_rms_weight=config.loss.log_rms_weight,
|
|
||||||
mae_weight=config.loss.mae_weight,
|
|
||||||
waveform_l1_weight=config.loss.waveform_l1_weight,
|
|
||||||
waveform_huber_weight=config.loss.waveform_huber_weight,
|
|
||||||
)
|
|
||||||
amp_enabled = config.train.use_amp and device.type == "cuda"
|
|
||||||
scaler = torch.cuda.amp.GradScaler(enabled=amp_enabled)
|
|
||||||
|
|
||||||
best_val_error = float("inf")
|
|
||||||
epochs_without_improvement = 0
|
|
||||||
history: list[dict[str, float]] = []
|
|
||||||
best_model_path, history_path = checkpoint_paths(config)
|
|
||||||
|
|
||||||
print(f"Device: {device}")
|
|
||||||
print(f"TCN receptive field: {receptive_field.receptive_field} samples, dilations={receptive_field.dilations}")
|
|
||||||
print(report_to_text(reports))
|
|
||||||
|
|
||||||
for epoch in range(1, config.train.epochs + 1):
|
|
||||||
train_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=loaders["train"],
|
|
||||||
optimizer=optimizer,
|
|
||||||
criterion=criterion,
|
|
||||||
target_mean=target_mean,
|
|
||||||
target_std=target_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
scaler=scaler,
|
|
||||||
amp_enabled=amp_enabled,
|
|
||||||
)
|
|
||||||
val_metrics = run_epoch(
|
|
||||||
model=model,
|
|
||||||
dataloader=loaders["val"],
|
|
||||||
optimizer=None,
|
|
||||||
criterion=criterion,
|
|
||||||
target_mean=target_mean,
|
|
||||||
target_std=target_std,
|
|
||||||
device=device,
|
|
||||||
grad_clip_norm=config.train.grad_clip_norm,
|
|
||||||
scaler=scaler,
|
|
||||||
amp_enabled=amp_enabled,
|
|
||||||
)
|
|
||||||
scheduler.step(val_metrics["rms_error"])
|
|
||||||
current_lr = optimizer.param_groups[0]["lr"]
|
|
||||||
|
|
||||||
history_row = {
|
|
||||||
"epoch": epoch,
|
|
||||||
"lr": current_lr,
|
|
||||||
"train_loss": train_metrics["loss"],
|
|
||||||
"train_relative_loss": train_metrics["relative_loss"],
|
|
||||||
"train_log_loss": train_metrics["log_loss"],
|
|
||||||
"train_mae_loss": train_metrics["mae_loss"],
|
|
||||||
"train_waveform_l1": train_metrics["waveform_l1"],
|
|
||||||
"train_waveform_huber": train_metrics["waveform_huber"],
|
|
||||||
"train_rms_error": train_metrics["rms_error"],
|
|
||||||
"val_loss": val_metrics["loss"],
|
|
||||||
"val_relative_loss": val_metrics["relative_loss"],
|
|
||||||
"val_log_loss": val_metrics["log_loss"],
|
|
||||||
"val_mae_loss": val_metrics["mae_loss"],
|
|
||||||
"val_waveform_l1": val_metrics["waveform_l1"],
|
|
||||||
"val_waveform_huber": val_metrics["waveform_huber"],
|
|
||||||
"val_rms_error": val_metrics["rms_error"],
|
|
||||||
}
|
|
||||||
history.append(history_row)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Epoch {epoch:03d} | train_loss={train_metrics['loss']:.6f} | "
|
|
||||||
f"val_loss={val_metrics['loss']:.6f} | val_rms_error={val_metrics['rms_error']:.6f} | lr={current_lr:.2e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if val_metrics["rms_error"] < best_val_error:
|
|
||||||
best_val_error = val_metrics["rms_error"]
|
|
||||||
epochs_without_improvement = 0
|
|
||||||
torch.save(
|
|
||||||
{
|
|
||||||
"epoch": epoch,
|
|
||||||
"model_state_dict": model.state_dict(),
|
|
||||||
"optimizer_state_dict": optimizer.state_dict(),
|
|
||||||
"best_val_rms_error": best_val_error,
|
|
||||||
"config": serialize_for_checkpoint(asdict(config)),
|
|
||||||
},
|
|
||||||
best_model_path,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
epochs_without_improvement += 1
|
|
||||||
if epochs_without_improvement >= config.train.early_stop_patience:
|
|
||||||
print(f"Early stopping triggered after {epoch} epochs.")
|
|
||||||
break
|
|
||||||
|
|
||||||
history_df = pd.DataFrame(history)
|
|
||||||
history_df.to_csv(history_path, index=False)
|
|
||||||
print(f"Best model saved to: {best_model_path}")
|
|
||||||
print(f"Training history saved to: {history_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
config = make_rms_forward_config()
|
|
||||||
if args.epochs is not None:
|
|
||||||
config.train.epochs = args.epochs
|
|
||||||
if args.batch_size is not None:
|
|
||||||
config.data.batch_size = args.batch_size
|
|
||||||
if args.device is not None:
|
|
||||||
config.train.device = args.device
|
|
||||||
train_model(config)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
# Data Configuration
|
|
||||||
# 使用基于当前文件的绝对路径拼接,以防止你在不同目录下运行报错
|
|
||||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'downloads')
|
|
||||||
SEQ_LEN = 512 # 滑动窗口的长度 (时间序列步数)
|
|
||||||
STEP_SIZE = 20 # 滑动窗口的步长
|
|
||||||
BATCH_SIZE = 128
|
|
||||||
|
|
||||||
# Features Configuration
|
|
||||||
INPUT_SENSOR = 'WSMS00012'
|
|
||||||
OUTPUT_SENSORS = ['WSMS00007', 'WSMS00008', 'WSMS00009', 'WSMS00010', 'WSMS00011']
|
|
||||||
INPUT_AXIS = 'value1' # 底部传感器输入轴(课程要求:X轴)
|
|
||||||
OUTPUT_AXIS = 'value3' # 目标传感器输出轴
|
|
||||||
|
|
||||||
# Model Configuration
|
|
||||||
CHANNELS = [64, 64, 128, 128, 256, 256] # TCN 各层通道数
|
|
||||||
KERNEL_SIZE = 5
|
|
||||||
DROPOUT = 0.1
|
|
||||||
|
|
||||||
# Training Configuration
|
|
||||||
LEARNING_RATE = 3e-4
|
|
||||||
EPOCHS = 50
|
|
||||||
WEIGHT_DECAY = 1e-4
|
|
||||||
ENABLE_EARLY_STOP = False
|
|
||||||
EARLY_STOP_PATIENCE = 15
|
|
||||||
SPECTRAL_LOSS_WEIGHT = 0.25
|
|
||||||
CORR_LOSS_WEIGHT = 0.05
|
|
||||||
MSE_LOSS_WEIGHT = 0.5
|
|
||||||
STD_LOSS_WEIGHT = 0.3
|
|
||||||
PEAK_LOSS_WEIGHT = 0.2
|
|
||||||
CORR_LOSS_EPS = 1e-8
|
|
||||||
|
|
||||||
# Device
|
|
||||||
import torch
|
|
||||||
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import os
|
|
||||||
import glob
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
from torch.utils.data import Dataset, DataLoader
|
|
||||||
from sklearn.preprocessing import StandardScaler
|
|
||||||
from config import *
|
|
||||||
|
|
||||||
class MultiOutputStandardizer:
|
|
||||||
"""Per-channel standardization that ignores missing labels via masks."""
|
|
||||||
def __init__(self, n_outputs):
|
|
||||||
self.n_outputs = n_outputs
|
|
||||||
self.mean_ = np.zeros(n_outputs, dtype=np.float32)
|
|
||||||
self.scale_ = np.ones(n_outputs, dtype=np.float32)
|
|
||||||
self.fitted = False
|
|
||||||
|
|
||||||
def fit(self, y_sequences, mask_sequences):
|
|
||||||
means = []
|
|
||||||
scales = []
|
|
||||||
for c in range(self.n_outputs):
|
|
||||||
valid_values = []
|
|
||||||
for y_seq, m_seq in zip(y_sequences, mask_sequences):
|
|
||||||
valid = m_seq[:, c] > 0.5
|
|
||||||
if np.any(valid):
|
|
||||||
valid_values.append(y_seq[valid, c])
|
|
||||||
if len(valid_values) == 0:
|
|
||||||
means.append(0.0)
|
|
||||||
scales.append(1.0)
|
|
||||||
continue
|
|
||||||
vals = np.concatenate(valid_values, axis=0)
|
|
||||||
mean = float(np.mean(vals))
|
|
||||||
std = float(np.std(vals))
|
|
||||||
if std < 1e-6:
|
|
||||||
std = 1.0
|
|
||||||
means.append(mean)
|
|
||||||
scales.append(std)
|
|
||||||
|
|
||||||
self.mean_ = np.asarray(means, dtype=np.float32)
|
|
||||||
self.scale_ = np.asarray(scales, dtype=np.float32)
|
|
||||||
self.fitted = True
|
|
||||||
|
|
||||||
def transform(self, y):
|
|
||||||
if not self.fitted:
|
|
||||||
raise RuntimeError("MultiOutputStandardizer must be fitted before transform.")
|
|
||||||
return (y - self.mean_) / self.scale_
|
|
||||||
|
|
||||||
def inverse_transform(self, y):
|
|
||||||
if not self.fitted:
|
|
||||||
raise RuntimeError("MultiOutputStandardizer must be fitted before inverse_transform.")
|
|
||||||
return y * self.scale_ + self.mean_
|
|
||||||
|
|
||||||
|
|
||||||
class BuildingDataset(Dataset):
|
|
||||||
def __init__(self, file_paths, seq_len, step_size, scaler_X=None, scaler_Y=None, fit_scaler=False):
|
|
||||||
self.seq_len = seq_len
|
|
||||||
self.X_data = []
|
|
||||||
self.Y_data = []
|
|
||||||
self.M_data = []
|
|
||||||
self.scaler_X = scaler_X if scaler_X is not None else StandardScaler()
|
|
||||||
self.scaler_Y = scaler_Y if scaler_Y is not None else MultiOutputStandardizer(len(OUTPUT_SENSORS))
|
|
||||||
|
|
||||||
raw_X = []
|
|
||||||
raw_Y = []
|
|
||||||
raw_M = []
|
|
||||||
|
|
||||||
for f in file_paths:
|
|
||||||
# 读取数据
|
|
||||||
df = pd.read_csv(f)
|
|
||||||
# 使用长表格式: code, type, time, value1, value2, value3
|
|
||||||
|
|
||||||
# 提取 012 的输入轴作为基准
|
|
||||||
df_in = df[df['code'] == INPUT_SENSOR][['time', INPUT_AXIS]].rename(columns={INPUT_AXIS: 'input_signal'})
|
|
||||||
if len(df_in) == 0:
|
|
||||||
# 若无输入传感器(如自由衰减数据),则补零
|
|
||||||
df_in = pd.DataFrame({'time': df['time'].unique()})
|
|
||||||
df_in['input_signal'] = 0.0
|
|
||||||
|
|
||||||
# 提取 007~011 的 Z 轴并按时间戳逐步合并 (使用 left join 确保以 df_in 的时间为基准)
|
|
||||||
df_merged = df_in
|
|
||||||
for sens in OUTPUT_SENSORS:
|
|
||||||
df_out_sens = df[df['code'] == sens][['time', OUTPUT_AXIS]].rename(columns={OUTPUT_AXIS: f'out_{sens}'})
|
|
||||||
df_merged = pd.merge(df_merged, df_out_sens, on='time', how='left')
|
|
||||||
|
|
||||||
df_merged = df_merged.sort_values('time').reset_index(drop=True)
|
|
||||||
|
|
||||||
if len(df_merged) == 0:
|
|
||||||
print(f"Warning: Skipping file {f} due to no overlapping timestamps across required sensors.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
x_seq = df_merged['input_signal'].values.reshape(-1, 1).astype(np.float32)
|
|
||||||
|
|
||||||
# 提取所有 target 传感器列与可用性掩码
|
|
||||||
out_cols = [f'out_{sens}' for sens in OUTPUT_SENSORS]
|
|
||||||
y_seq = np.zeros((len(df_merged), len(OUTPUT_SENSORS)), dtype=np.float32)
|
|
||||||
m_seq = np.zeros((len(df_merged), len(OUTPUT_SENSORS)), dtype=np.float32)
|
|
||||||
for c, col in enumerate(out_cols):
|
|
||||||
series = df_merged[col]
|
|
||||||
observed = ~series.isna()
|
|
||||||
m_seq[:, c] = observed.astype(np.float32)
|
|
||||||
if observed.any():
|
|
||||||
filled = series.interpolate(method='linear').bfill().ffill()
|
|
||||||
y_seq[:, c] = filled.fillna(0.0).values.astype(np.float32)
|
|
||||||
else:
|
|
||||||
y_seq[:, c] = 0.0
|
|
||||||
|
|
||||||
raw_X.append(x_seq)
|
|
||||||
raw_Y.append(y_seq)
|
|
||||||
raw_M.append(m_seq)
|
|
||||||
|
|
||||||
if len(raw_X) == 0:
|
|
||||||
raise ValueError("未能从文件中构造出有效序列,请检查数据路径与传感器编码配置。")
|
|
||||||
|
|
||||||
# 拼接所有文件数据进行 fit
|
|
||||||
X_all = np.vstack(raw_X)
|
|
||||||
|
|
||||||
if fit_scaler:
|
|
||||||
self.scaler_X.fit(X_all)
|
|
||||||
self.scaler_Y.fit(raw_Y, raw_M)
|
|
||||||
|
|
||||||
# 切分窗口
|
|
||||||
for x_seq, y_seq, m_seq in zip(raw_X, raw_Y, raw_M):
|
|
||||||
x_seq_scaled = self.scaler_X.transform(x_seq)
|
|
||||||
y_seq_scaled = self.scaler_Y.transform(y_seq)
|
|
||||||
y_seq_scaled = np.where(m_seq > 0.5, y_seq_scaled, 0.0).astype(np.float32)
|
|
||||||
|
|
||||||
for i in range(0, len(x_seq_scaled) - seq_len + 1, step_size):
|
|
||||||
x_win = x_seq_scaled[i:i+seq_len]
|
|
||||||
y_win = y_seq_scaled[i:i+seq_len]
|
|
||||||
m_win = m_seq[i:i+seq_len]
|
|
||||||
if np.sum(m_win) <= 0:
|
|
||||||
continue
|
|
||||||
self.X_data.append(x_win)
|
|
||||||
self.Y_data.append(y_win)
|
|
||||||
self.M_data.append(m_win)
|
|
||||||
|
|
||||||
self.X_data = np.array(self.X_data)
|
|
||||||
self.Y_data = np.array(self.Y_data)
|
|
||||||
self.M_data = np.array(self.M_data)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.X_data)
|
|
||||||
|
|
||||||
def __getitem__(self, idx):
|
|
||||||
return (
|
|
||||||
torch.tensor(self.X_data[idx], dtype=torch.float32),
|
|
||||||
torch.tensor(self.Y_data[idx], dtype=torch.float32),
|
|
||||||
torch.tensor(self.M_data[idx], dtype=torch.float32),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_dataloaders(condition='Non_TMD', include_free_vib=False):
|
|
||||||
"""
|
|
||||||
condition: 'Non_TMD' 或者是 'TMD'
|
|
||||||
include_free_vib: 是否在训练集中加入自由振动与自由衰减数据
|
|
||||||
"""
|
|
||||||
base_dir = os.path.join(DATA_DIR, condition)
|
|
||||||
|
|
||||||
# 手动区分的子目录
|
|
||||||
train_files = glob.glob(os.path.join(base_dir, 'train', '*.csv'))
|
|
||||||
if include_free_vib:
|
|
||||||
train_files += glob.glob(os.path.join(base_dir, 'free_vib', '*.csv'))
|
|
||||||
|
|
||||||
val_files = glob.glob(os.path.join(base_dir, 'val', '*.csv'))
|
|
||||||
test_files = glob.glob(os.path.join(base_dir, 'test', '*.csv'))
|
|
||||||
|
|
||||||
print(f"[{condition}] Train files: {len(train_files)}, Val files: {len(val_files)}, Test files: {len(test_files)}")
|
|
||||||
|
|
||||||
if len(train_files) == 0:
|
|
||||||
raise ValueError(f"错误: 在 {base_dir}/train 目录下未找到训练文件!请检查路径是否正确。")
|
|
||||||
if len(val_files) == 0:
|
|
||||||
raise ValueError(f"错误: 在 {base_dir}/val 目录下未找到验证文件!请检查路径是否正确。")
|
|
||||||
|
|
||||||
train_dataset = BuildingDataset(train_files, SEQ_LEN, STEP_SIZE, fit_scaler=True)
|
|
||||||
val_dataset = BuildingDataset(val_files, SEQ_LEN, STEP_SIZE,
|
|
||||||
scaler_X=train_dataset.scaler_X,
|
|
||||||
scaler_Y=train_dataset.scaler_Y, fit_scaler=False)
|
|
||||||
# 若某条件(如 TMD)下没有 test 数据,可以处理一下防止报错
|
|
||||||
test_loader = None
|
|
||||||
if len(test_files) > 0:
|
|
||||||
test_dataset = BuildingDataset(test_files, SEQ_LEN, STEP_SIZE,
|
|
||||||
scaler_X=train_dataset.scaler_X,
|
|
||||||
scaler_Y=train_dataset.scaler_Y, fit_scaler=False)
|
|
||||||
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
|
|
||||||
|
|
||||||
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
|
|
||||||
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)
|
|
||||||
|
|
||||||
return train_loader, val_loader, test_loader, train_dataset.scaler_X, train_dataset.scaler_Y
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import torch
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from config import *
|
|
||||||
from dataset import get_dataloaders
|
|
||||||
from model import BuildingTCN
|
|
||||||
|
|
||||||
def evaluate_model():
|
|
||||||
_, _, test_loader, scaler_X, scaler_Y = get_dataloaders()
|
|
||||||
if test_loader is None:
|
|
||||||
raise ValueError("当前数据配置下没有可用的测试集。")
|
|
||||||
|
|
||||||
model = BuildingTCN(input_size=1, output_size=5, num_channels=CHANNELS,
|
|
||||||
kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE)
|
|
||||||
model.load_state_dict(torch.load('best_model.pth', map_location=DEVICE))
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
all_preds = []
|
|
||||||
all_targets = []
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
for inputs, targets, masks in test_loader:
|
|
||||||
inputs = inputs.to(DEVICE)
|
|
||||||
outputs = model(inputs)
|
|
||||||
|
|
||||||
all_preds.append(outputs.cpu().numpy())
|
|
||||||
all_targets.append(targets.numpy())
|
|
||||||
|
|
||||||
# Concatenate results
|
|
||||||
all_preds = np.concatenate(all_preds, axis=0)
|
|
||||||
all_targets = np.concatenate(all_targets, axis=0)
|
|
||||||
|
|
||||||
# Inverse transform to physical scale for metrics
|
|
||||||
all_preds_inv = scaler_Y.inverse_transform(all_preds.reshape(-1, len(OUTPUT_SENSORS))).reshape(all_preds.shape)
|
|
||||||
all_targets_inv = scaler_Y.inverse_transform(all_targets.reshape(-1, len(OUTPUT_SENSORS))).reshape(all_targets.shape)
|
|
||||||
|
|
||||||
print("\n=== Test Metrics (All Windows) ===")
|
|
||||||
for i, sens in enumerate(OUTPUT_SENSORS):
|
|
||||||
pred_i = all_preds_inv[:, :, i].reshape(-1)
|
|
||||||
target_i = all_targets_inv[:, :, i].reshape(-1)
|
|
||||||
mae = float(np.mean(np.abs(pred_i - target_i)))
|
|
||||||
pred_std = float(np.std(pred_i))
|
|
||||||
target_std = float(np.std(target_i))
|
|
||||||
amp_ratio = pred_std / (target_std + 1e-12)
|
|
||||||
pred_peak = float(np.max(np.abs(pred_i)))
|
|
||||||
target_peak = float(np.max(np.abs(target_i)))
|
|
||||||
peak_ratio = pred_peak / (target_peak + 1e-12)
|
|
||||||
corr = float(np.corrcoef(pred_i, target_i)[0, 1]) if len(pred_i) > 1 else float("nan")
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"{sens}: MAE={mae:.4f}, Corr={corr:.4f}, "
|
|
||||||
f"AmpRatio(std_pred/std_true)={amp_ratio:.4f}, "
|
|
||||||
f"PeakRatio(max|pred|/max|true|)={peak_ratio:.4f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 取一个 batch 的第一条序列进行可视化
|
|
||||||
sample_pred = all_preds_inv[0]
|
|
||||||
sample_target = all_targets_inv[0]
|
|
||||||
|
|
||||||
plt.figure(figsize=(15, 10))
|
|
||||||
for i, sens in enumerate(OUTPUT_SENSORS):
|
|
||||||
plt.subplot(5, 1, i+1)
|
|
||||||
plt.plot(sample_target[:, i], label='True', alpha=0.7)
|
|
||||||
plt.plot(sample_pred[:, i], label='Pred', alpha=0.7, linestyle='--')
|
|
||||||
plt.title(f'Sensor {sens} Z-axis Response (Test: Earthquake)')
|
|
||||||
plt.legend()
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig('test_results.png')
|
|
||||||
print("Evaluation done. Result saved to test_results.png")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
evaluate_model()
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.nn.utils import weight_norm
|
|
||||||
|
|
||||||
class Chomp1d(nn.Module):
|
|
||||||
def __init__(self, chomp_size):
|
|
||||||
super(Chomp1d, self).__init__()
|
|
||||||
self.chomp_size = chomp_size
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
return x[:, :, :-self.chomp_size].contiguous()
|
|
||||||
|
|
||||||
class TemporalBlock(nn.Module):
|
|
||||||
def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2):
|
|
||||||
super(TemporalBlock, self).__init__()
|
|
||||||
self.conv1 = weight_norm(nn.Conv1d(n_inputs, n_outputs, kernel_size,
|
|
||||||
stride=stride, padding=padding, dilation=dilation))
|
|
||||||
self.chomp1 = Chomp1d(padding)
|
|
||||||
self.relu1 = nn.ReLU()
|
|
||||||
self.dropout1 = nn.Dropout(dropout)
|
|
||||||
|
|
||||||
self.conv2 = weight_norm(nn.Conv1d(n_outputs, n_outputs, kernel_size,
|
|
||||||
stride=stride, padding=padding, dilation=dilation))
|
|
||||||
self.chomp2 = Chomp1d(padding)
|
|
||||||
self.relu2 = nn.ReLU()
|
|
||||||
self.dropout2 = nn.Dropout(dropout)
|
|
||||||
|
|
||||||
self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1,
|
|
||||||
self.conv2, self.chomp2, self.relu2, self.dropout2)
|
|
||||||
self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None
|
|
||||||
self.relu = nn.ReLU()
|
|
||||||
self.init_weights()
|
|
||||||
|
|
||||||
def init_weights(self):
|
|
||||||
self.conv1.weight.data.normal_(0, 0.01)
|
|
||||||
self.conv2.weight.data.normal_(0, 0.01)
|
|
||||||
if self.downsample is not None:
|
|
||||||
self.downsample.weight.data.normal_(0, 0.01)
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
out = self.net(x)
|
|
||||||
res = x if self.downsample is None else self.downsample(x)
|
|
||||||
return self.relu(out + res)
|
|
||||||
|
|
||||||
class TemporalConvNet(nn.Module):
|
|
||||||
def __init__(self, num_inputs, num_channels, kernel_size=2, dropout=0.2):
|
|
||||||
super(TemporalConvNet, self).__init__()
|
|
||||||
layers = []
|
|
||||||
num_levels = len(num_channels)
|
|
||||||
for i in range(num_levels):
|
|
||||||
dilation_size = 2 ** i
|
|
||||||
in_channels = num_inputs if i == 0 else num_channels[i-1]
|
|
||||||
out_channels = num_channels[i]
|
|
||||||
layers += [TemporalBlock(in_channels, out_channels, kernel_size, stride=1, dilation=dilation_size,
|
|
||||||
padding=(kernel_size-1) * dilation_size, dropout=dropout)]
|
|
||||||
|
|
||||||
self.network = nn.Sequential(*layers)
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
return self.network(x)
|
|
||||||
|
|
||||||
class BuildingTCN(nn.Module):
|
|
||||||
def __init__(self, input_size, output_size, num_channels, kernel_size=3, dropout=0.2):
|
|
||||||
super(BuildingTCN, self).__init__()
|
|
||||||
self.tcn = TemporalConvNet(input_size, num_channels, kernel_size, dropout=dropout)
|
|
||||||
self.linear = nn.Linear(num_channels[-1], output_size)
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
# x shape: (batch, seq_len, input_size)
|
|
||||||
# TCN needs shape: (batch, input_size, seq_len)
|
|
||||||
x = x.transpose(1, 2)
|
|
||||||
y = self.tcn(x)
|
|
||||||
# y shape: (batch, num_channels, seq_len)
|
|
||||||
# linear needs shape: (batch, seq_len, num_channels)
|
|
||||||
y = y.transpose(1, 2)
|
|
||||||
return self.linear(y)
|
|
||||||
241
src_old/train.py
@@ -1,241 +0,0 @@
|
|||||||
import torch
|
|
||||||
import torch.optim as optim
|
|
||||||
from config import *
|
|
||||||
from dataset import get_dataloaders
|
|
||||||
from model import BuildingTCN
|
|
||||||
|
|
||||||
def masked_l1_loss(pred, target, mask):
|
|
||||||
diff = torch.abs(pred - target) * mask
|
|
||||||
denom = mask.sum().clamp(min=1.0)
|
|
||||||
return diff.sum() / denom
|
|
||||||
|
|
||||||
def masked_mse_loss(pred, target, mask):
|
|
||||||
sq = ((pred - target) ** 2) * mask
|
|
||||||
denom = mask.sum().clamp(min=1.0)
|
|
||||||
return sq.sum() / denom
|
|
||||||
|
|
||||||
def masked_spectral_mag_loss(pred, target, mask):
|
|
||||||
# Apply mask in time domain first so missing labels do not pollute spectrum.
|
|
||||||
pred_masked = pred * mask
|
|
||||||
target_masked = target * mask
|
|
||||||
|
|
||||||
pred_fft = torch.fft.rfft(pred_masked, dim=1)
|
|
||||||
target_fft = torch.fft.rfft(target_masked, dim=1)
|
|
||||||
pred_mag = torch.abs(pred_fft)
|
|
||||||
target_mag = torch.abs(target_fft)
|
|
||||||
pred_mag = torch.nan_to_num(pred_mag, nan=0.0, posinf=1e6, neginf=0.0)
|
|
||||||
target_mag = torch.nan_to_num(target_mag, nan=0.0, posinf=1e6, neginf=0.0)
|
|
||||||
|
|
||||||
# Weight each sample/channel by its valid-label ratio.
|
|
||||||
valid_ratio = (mask.sum(dim=1) / mask.shape[1]).clamp(min=0.0, max=1.0)
|
|
||||||
freq_weight = valid_ratio.unsqueeze(1).expand_as(pred_mag)
|
|
||||||
|
|
||||||
diff = torch.abs(pred_mag - target_mag) * freq_weight
|
|
||||||
denom = freq_weight.sum().clamp(min=1.0)
|
|
||||||
return diff.sum() / denom
|
|
||||||
|
|
||||||
def masked_corr_loss(pred, target, mask, eps=1e-8):
|
|
||||||
# Compute per-sample/per-channel Pearson correlation with masked timesteps.
|
|
||||||
count = mask.sum(dim=1) # (batch, channels)
|
|
||||||
valid = count > 1.0
|
|
||||||
count_safe = count.clamp(min=1.0)
|
|
||||||
|
|
||||||
pred_mean = (pred * mask).sum(dim=1) / count_safe
|
|
||||||
target_mean = (target * mask).sum(dim=1) / count_safe
|
|
||||||
|
|
||||||
pred_centered = (pred - pred_mean.unsqueeze(1)) * mask
|
|
||||||
target_centered = (target - target_mean.unsqueeze(1)) * mask
|
|
||||||
|
|
||||||
cov = (pred_centered * target_centered).sum(dim=1)
|
|
||||||
pred_var = (pred_centered ** 2).sum(dim=1)
|
|
||||||
target_var = (target_centered ** 2).sum(dim=1)
|
|
||||||
|
|
||||||
# Put eps inside sqrt to avoid infinite gradients around zero variance.
|
|
||||||
denom = torch.sqrt((pred_var * target_var).clamp(min=0.0) + eps)
|
|
||||||
corr = cov / denom
|
|
||||||
corr = torch.nan_to_num(corr, nan=0.0, posinf=0.0, neginf=0.0)
|
|
||||||
corr = torch.where(valid, corr, torch.zeros_like(corr))
|
|
||||||
|
|
||||||
valid_float = valid.float()
|
|
||||||
denom = valid_float.sum().clamp(min=1.0)
|
|
||||||
return ((1.0 - corr) * valid_float).sum() / denom
|
|
||||||
|
|
||||||
def masked_std_loss(pred, target, mask):
|
|
||||||
count = mask.sum(dim=1).clamp(min=1.0)
|
|
||||||
pred_mean = (pred * mask).sum(dim=1) / count
|
|
||||||
target_mean = (target * mask).sum(dim=1) / count
|
|
||||||
|
|
||||||
pred_centered = (pred - pred_mean.unsqueeze(1)) * mask
|
|
||||||
target_centered = (target - target_mean.unsqueeze(1)) * mask
|
|
||||||
|
|
||||||
pred_std = torch.sqrt((pred_centered ** 2).sum(dim=1) / count + 1e-8)
|
|
||||||
target_std = torch.sqrt((target_centered ** 2).sum(dim=1) / count + 1e-8)
|
|
||||||
|
|
||||||
valid = mask.sum(dim=1) > 1.0
|
|
||||||
valid_float = valid.float()
|
|
||||||
denom = valid_float.sum().clamp(min=1.0)
|
|
||||||
return (torch.abs(pred_std - target_std) * valid_float).sum() / denom
|
|
||||||
|
|
||||||
def masked_peak_loss(pred, target, mask):
|
|
||||||
abs_pred = torch.abs(pred) * mask
|
|
||||||
abs_target = torch.abs(target) * mask
|
|
||||||
pred_peak = abs_pred.max(dim=1).values
|
|
||||||
target_peak = abs_target.max(dim=1).values
|
|
||||||
|
|
||||||
valid = mask.sum(dim=1) > 0.0
|
|
||||||
valid_float = valid.float()
|
|
||||||
denom = valid_float.sum().clamp(min=1.0)
|
|
||||||
return (torch.abs(pred_peak - target_peak) * valid_float).sum() / denom
|
|
||||||
|
|
||||||
def train_model():
|
|
||||||
train_loader, val_loader, _, _, _ = get_dataloaders()
|
|
||||||
|
|
||||||
# 初始化前向模型 (1 -> 5)
|
|
||||||
model = BuildingTCN(input_size=1, output_size=5, num_channels=CHANNELS,
|
|
||||||
kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE)
|
|
||||||
|
|
||||||
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)
|
|
||||||
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=8, factor=0.5)
|
|
||||||
|
|
||||||
best_val_loss = float('inf')
|
|
||||||
no_improve_epochs = 0
|
|
||||||
early_stop_patience = EARLY_STOP_PATIENCE
|
|
||||||
|
|
||||||
for epoch in range(EPOCHS):
|
|
||||||
model.train()
|
|
||||||
train_loss = 0.0
|
|
||||||
train_time_loss = 0.0
|
|
||||||
train_spec_loss = 0.0
|
|
||||||
train_corr_loss = 0.0
|
|
||||||
train_mse_loss = 0.0
|
|
||||||
train_std_loss = 0.0
|
|
||||||
train_peak_loss = 0.0
|
|
||||||
train_batches = 0
|
|
||||||
|
|
||||||
for inputs, targets, masks in train_loader:
|
|
||||||
inputs = inputs.to(DEVICE)
|
|
||||||
targets = targets.to(DEVICE)
|
|
||||||
masks = masks.to(DEVICE)
|
|
||||||
|
|
||||||
optimizer.zero_grad()
|
|
||||||
outputs = model(inputs)
|
|
||||||
time_loss = masked_l1_loss(outputs, targets, masks)
|
|
||||||
mse_loss = masked_mse_loss(outputs, targets, masks)
|
|
||||||
spec_loss = masked_spectral_mag_loss(outputs, targets, masks)
|
|
||||||
corr_loss = masked_corr_loss(outputs, targets, masks, eps=CORR_LOSS_EPS)
|
|
||||||
std_loss = masked_std_loss(outputs, targets, masks)
|
|
||||||
peak_loss = masked_peak_loss(outputs, targets, masks)
|
|
||||||
loss = (
|
|
||||||
time_loss
|
|
||||||
+ MSE_LOSS_WEIGHT * mse_loss
|
|
||||||
+ SPECTRAL_LOSS_WEIGHT * spec_loss
|
|
||||||
+ CORR_LOSS_WEIGHT * corr_loss
|
|
||||||
+ STD_LOSS_WEIGHT * std_loss
|
|
||||||
+ PEAK_LOSS_WEIGHT * peak_loss
|
|
||||||
)
|
|
||||||
|
|
||||||
if not torch.isfinite(loss):
|
|
||||||
print(" [Warn] Non-finite train loss encountered. Skip this batch.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
loss.backward()
|
|
||||||
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
|
|
||||||
if not torch.isfinite(grad_norm):
|
|
||||||
print(" [Warn] Non-finite gradient norm encountered. Skip optimizer step for this batch.")
|
|
||||||
optimizer.zero_grad(set_to_none=True)
|
|
||||||
continue
|
|
||||||
|
|
||||||
optimizer.step()
|
|
||||||
|
|
||||||
train_loss += loss.item()
|
|
||||||
train_time_loss += time_loss.item()
|
|
||||||
train_mse_loss += mse_loss.item()
|
|
||||||
train_spec_loss += spec_loss.item()
|
|
||||||
train_corr_loss += corr_loss.item()
|
|
||||||
train_std_loss += std_loss.item()
|
|
||||||
train_peak_loss += peak_loss.item()
|
|
||||||
train_batches += 1
|
|
||||||
|
|
||||||
# 验证
|
|
||||||
model.eval()
|
|
||||||
val_loss = 0.0
|
|
||||||
val_time_loss = 0.0
|
|
||||||
val_spec_loss = 0.0
|
|
||||||
val_corr_loss = 0.0
|
|
||||||
val_mse_loss = 0.0
|
|
||||||
val_std_loss = 0.0
|
|
||||||
val_peak_loss = 0.0
|
|
||||||
val_batches = 0
|
|
||||||
with torch.no_grad():
|
|
||||||
for inputs, targets, masks in val_loader:
|
|
||||||
inputs = inputs.to(DEVICE)
|
|
||||||
targets = targets.to(DEVICE)
|
|
||||||
masks = masks.to(DEVICE)
|
|
||||||
outputs = model(inputs)
|
|
||||||
time_loss = masked_l1_loss(outputs, targets, masks)
|
|
||||||
mse_loss = masked_mse_loss(outputs, targets, masks)
|
|
||||||
spec_loss = masked_spectral_mag_loss(outputs, targets, masks)
|
|
||||||
corr_loss = masked_corr_loss(outputs, targets, masks, eps=CORR_LOSS_EPS)
|
|
||||||
std_loss = masked_std_loss(outputs, targets, masks)
|
|
||||||
peak_loss = masked_peak_loss(outputs, targets, masks)
|
|
||||||
loss = (
|
|
||||||
time_loss
|
|
||||||
+ MSE_LOSS_WEIGHT * mse_loss
|
|
||||||
+ SPECTRAL_LOSS_WEIGHT * spec_loss
|
|
||||||
+ CORR_LOSS_WEIGHT * corr_loss
|
|
||||||
+ STD_LOSS_WEIGHT * std_loss
|
|
||||||
+ PEAK_LOSS_WEIGHT * peak_loss
|
|
||||||
)
|
|
||||||
if not torch.isfinite(loss):
|
|
||||||
continue
|
|
||||||
val_loss += loss.item()
|
|
||||||
val_time_loss += time_loss.item()
|
|
||||||
val_mse_loss += mse_loss.item()
|
|
||||||
val_spec_loss += spec_loss.item()
|
|
||||||
val_corr_loss += corr_loss.item()
|
|
||||||
val_std_loss += std_loss.item()
|
|
||||||
val_peak_loss += peak_loss.item()
|
|
||||||
val_batches += 1
|
|
||||||
|
|
||||||
train_den = max(train_batches, 1)
|
|
||||||
val_den = max(val_batches, 1)
|
|
||||||
train_loss /= train_den
|
|
||||||
train_time_loss /= train_den
|
|
||||||
train_mse_loss /= train_den
|
|
||||||
train_spec_loss /= train_den
|
|
||||||
train_corr_loss /= train_den
|
|
||||||
train_std_loss /= train_den
|
|
||||||
train_peak_loss /= train_den
|
|
||||||
val_loss /= val_den
|
|
||||||
val_time_loss /= val_den
|
|
||||||
val_mse_loss /= val_den
|
|
||||||
val_spec_loss /= val_den
|
|
||||||
val_corr_loss /= val_den
|
|
||||||
val_std_loss /= val_den
|
|
||||||
val_peak_loss /= val_den
|
|
||||||
|
|
||||||
scheduler.step(val_loss)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Epoch {epoch+1}/{EPOCHS} | "
|
|
||||||
f"Train Loss: {train_loss:.4f} "
|
|
||||||
f"(L1={train_time_loss:.4f}, MSE={train_mse_loss:.4f}, Spec={train_spec_loss:.4f}, "
|
|
||||||
f"Corr={train_corr_loss:.4f}, Std={train_std_loss:.4f}, Peak={train_peak_loss:.4f}) | "
|
|
||||||
f"Val Loss: {val_loss:.4f} "
|
|
||||||
f"(L1={val_time_loss:.4f}, MSE={val_mse_loss:.4f}, Spec={val_spec_loss:.4f}, "
|
|
||||||
f"Corr={val_corr_loss:.4f}, Std={val_std_loss:.4f}, Peak={val_peak_loss:.4f})"
|
|
||||||
)
|
|
||||||
|
|
||||||
if val_loss < best_val_loss:
|
|
||||||
best_val_loss = val_loss
|
|
||||||
torch.save(model.state_dict(), 'best_model.pth')
|
|
||||||
print(" --> Saved Best Model")
|
|
||||||
no_improve_epochs = 0
|
|
||||||
else:
|
|
||||||
no_improve_epochs += 1
|
|
||||||
if ENABLE_EARLY_STOP and no_improve_epochs >= early_stop_patience:
|
|
||||||
print(f"Early stopping at epoch {epoch+1}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
train_model()
|
|
||||||