Noise2Average: An iterative residual learning strategy for image denoising without clean data.
The 2 matches
- [1] § Experiments › Neural network implementation ↔ Code/s_N2A_cnnTrainN2A.py, lines 407 to 451 · score 0.65 · Adam optimizer, squared error, loss, epoch, CNNs, training · Report a mismatch
- [2] § Experiments › Data denoising › Noise2Average denoising for ultrahigh resolution data ↔ Code/s_N2A_cnnTrainN2A.py, lines 552 to 632 · score 0.52 · fine tuning, image block, predict, CNN, training, epochs · Report a mismatch
Paper
Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 667 lines · 29 KB · no license · 2 matches
- # s_DtiNet_cnnTrain.py
- #
- # QT 2019
- # %% 在所有导入之前设置环境变量来解决内存碎片化
- import os
- import sys
- import time
- # 设置CUDA内存分配器为异步模式,解决内存碎片化
- os.environ['TF_GPU_ALLOCATOR'] = 'cuda_malloc_async'
- os.environ['TF_ENABLE_GPU_GARBAGE_COLLECTION'] = '1'
- os.environ['TF_GPU_THREAD_MODE'] = 'gpu_private'
- os.environ['TF_CPP_MIN_LOG_LEVEL'] = '1'
- # 禁用TensorFlow的JIT编译,减少内存使用
- os.environ['TF_XLA_FLAGS'] = '--tf_xla_auto_jit=0'
- # %% load modules with delay
- import time
- import argparse
- import gc
- import scipy.io as sio
- import numpy as np
- import nibabel as nb
- import glob
- import tensorflow
- print(f"🚀 TensorFlow version: {tensorflow.__version__}")
- print("🔧 GPU memory optimization enabled: TF_GPU_ALLOCATOR=cuda_malloc_async")
- import munet_res
- from tensorflow.keras.optimizers import Adam
- from tensorflow.keras.callbacks import ModelCheckpoint
- from tensorflow.keras.models import load_model
- import qtlib as qtlib
- import tensorflow as tf
- from tensorflow.keras import backend as K
- # %% 添加命令行参数解析
- parser = argparse.ArgumentParser(description='Train Noise2Average model')
- parser.add_argument('--subject_idx', type=int, default=0,
- help='Subject index to train (0-9, default: 0)')
- parser.add_argument('--gpu_id', type=str, default='0',
- help='GPU ID to use (default: 0)')
- parser.add_argument('--gpu_memory_limit', type=int, default=16000,
- help='GPU memory limit in MB (default: None for growth mode)')
- args = parser.parse_args()
- # 设置GPU
- os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu_id
- # %% 配置TensorFlow GPU显存使用 (适配TensorFlow 2.15.0)
- gpus = tf.config.list_physical_devices('GPU')
- if gpus:
- try:
- gpu = gpus[0]
- if args.gpu_memory_limit:
- # 方法1: 设置显存限制
- tf.config.experimental.set_memory_growth(gpu, False)
- tf.config.experimental.set_virtual_device_configuration(
- gpu,
- [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=args.gpu_memory_limit)]
- )
- print(f"✅ GPU memory limit set to {args.gpu_memory_limit} MB ({args.gpu_memory_limit/1024:.1f} GB)")
- else:
- # 方法2: 启用内存增长 (按需分配,推荐)
- tf.config.experimental.set_memory_growth(gpu, True)
- print("✅ GPU memory growth enabled - will allocate as needed")
- except RuntimeError as e:
- print(f"❌ GPU configuration error: {e}")
- print("Note: GPU configuration must be set before any operations")
- except Exception as e:
- print(f"⚠️ GPU configuration warning: {e}")
- print("Continuing with default GPU settings...")
- def print_gpu_status():
- """详细的GPU状态打印"""
- try:
- import subprocess
- result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.used,memory.total,memory.free,utilization.gpu,temperature.gpu',
- '--format=csv,nounits,noheader'],
- capture_output=True, text=True)
- if result.returncode == 0:
- lines = result.stdout.strip().split('\n')
- for i, line in enumerate(lines):
- values = [v.strip() for v in line.split(',')]
- if len(values) >= 6:
- name, mem_used, mem_total, mem_free, gpu_util, temp = values[:6]
- mem_used_int = int(mem_used)
- mem_total_int = int(mem_total)
- mem_free_int = int(mem_free)
- usage_percent = (mem_used_int/mem_total_int)*100
- print(f"🖥️ GPU {i}: {name}")
- print(f" 💾 Memory: {mem_used}/{mem_total}MB ({usage_percent:.1f}%) | Free: {mem_free}MB")
- print(f" ⚡ Util: {gpu_util}% | 🌡️ Temp: {temp}°C")
- # 内存碎片化和警告检测
- if mem_free_int < 2000 and usage_percent < 85:
- print(f" ⚠️ Possible memory fragmentation detected!")
- elif mem_free_int < 1000:
- print(f" 🚨 Low memory warning: {mem_free_int}MB free")
- except Exception as e:
- print(f"Could not get GPU status: {e}")
- def aggressive_memory_cleanup():
- """积极的内存清理策略"""
- print("🧹 Performing aggressive memory cleanup...")
- # 1. 清理Keras/TensorFlow缓存
- K.clear_session()
- tf.keras.backend.clear_session()
- # 2. Python垃圾回收
- collected = gc.collect()
- print(f" 🗑️ Garbage collected: {collected} objects")
- # 3. GPU内存统计重置
- if gpus:
- try:
- tf.config.experimental.reset_memory_stats(gpus[0])
- print(" 📊 GPU memory stats reset")
- except:
- pass
- # 4. 创建临时tensor触发内存重新分配(碎片整理)
- try:
- temp_tensor = tf.random.normal([200, 200])
- del temp_tensor
- gc.collect()
- print(" 🔧 Memory defragmentation performed")
- except:
- pass
- print(" ✅ Aggressive cleanup completed")
- def smart_memory_management(current_epoch, total_epochs):
- """智能内存管理策略"""
- try:
- # 获取当前内存使用情况
- import subprocess
- result = subprocess.run(['nvidia-smi', '--query-gpu=memory.used,memory.free,memory.total',
- '--format=csv,nounits,noheader'],
- capture_output=True, text=True)
- if result.returncode == 0:
- mem_used, mem_free, mem_total = result.stdout.strip().split(', ')
- mem_used_int = int(mem_used)
- mem_free_int = int(mem_free)
- mem_total_int = int(mem_total)
- usage_percent = (mem_used_int / mem_total_int) * 100
- print(f"💾 Memory Status: {mem_used_int}MB used / {mem_total_int}MB total ({usage_percent:.1f}%)")
- print(f" 🆓 Available: {mem_free_int}MB")
- # 策略1: 如果可用内存少于2GB,执行清理
- if mem_free_int < 2048:
- print(f"⚠️ Low free memory ({mem_free_int}MB), performing cleanup...")
- aggressive_memory_cleanup()
- return True
- # 策略2: 如果使用率超过85%,执行清理
- elif usage_percent > 85:
- print(f"⚠️ High memory usage ({usage_percent:.1f}%), performing cleanup...")
- aggressive_memory_cleanup()
- return True
- # 策略3: 每5个epoch进行预防性清理
- elif current_epoch > 0 and current_epoch % 5 == 0:
- print(f"🔧 Preventive memory cleanup at epoch {current_epoch}...")
- aggressive_memory_cleanup()
- return True
- return False
- except Exception as e:
- print(f"Memory management error: {e}")
- return False
- def emergency_memory_recovery():
- """紧急内存恢复策略"""
- print("🚨 Emergency memory recovery initiated...")
- # 1. 最激进的清理
- K.clear_session()
- tf.keras.backend.clear_session()
- # 2. 强制垃圾回收多次
- for i in range(3):
- collected = gc.collect()
- print(f" 🗑️ GC round {i+1}: {collected} objects")
- # 3. 重置GPU内存
- if gpus:
- try:
- tf.config.experimental.reset_memory_stats(gpus[0])
- # 尝试创建小tensor来触发内存重组
- for size in [50, 100, 150]:
- try:
- temp = tf.random.normal([size, size])
- del temp
- except:
- break
- gc.collect()
- print(" 🔧 Emergency memory defragmentation completed")
- except Exception as e:
- print(f" ⚠️ GPU reset failed: {e}")
- print(" ✅ Emergency recovery completed")
- # %% 优化的显存清理函数
- def clear_memory():
- """标准内存清理"""
- K.clear_session()
- tf.keras.backend.clear_session()
- gc.collect()
- # 如果使用GPU,清理GPU缓存
- if gpus:
- try:
- tf.config.experimental.reset_memory_stats(gpus[0])
- except:
- pass
- print(" 📝 Standard memory cleared")
- # %% 日志记录函数
- def write_log(log_file, message):
- """写入日志信息"""
- timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
- log_message = f"[{timestamp}] {message}\n"
- with open(log_file, 'a', encoding='utf-8') as f:
- f.write(log_message)
- print(log_message.strip())
- # %% set up path
- dpRoot = '../Data'
- os.chdir(dpRoot)
- # %% subjects
- subjects = [f'sub_001']
- # 检查subject索引是否有效
- if args.subject_idx >= len(subjects) or args.subject_idx < 0:
- raise ValueError(f"Subject index {args.subject_idx} is out of range. Valid range: 0-{len(subjects)-1}")
- print(f"🎯 Training subject {args.subject_idx}: {subjects[args.subject_idx]}")
- nbatch = 1
- nepoch = 20
- niter = 2
- shouldtrain = True
- expname = f'N2A-mem{args.gpu_memory_limit or "auto"}MB'
- # 显示初始GPU状态
- print("\n" + "="*60)
- print("🚀 INITIAL GPU STATUS")
- print("="*60)
- print_gpu_status()
- # %% load data with memory optimization
- if shouldtrain:
- for ch in range(0,1):
- # 只训练指定的subject
- ii = args.subject_idx
- sj = os.path.basename(subjects[ii])
- print(f"\n🔄 Processing subject: {sj}")
- # 创建日志文件路径
- dpSub = os.path.join(dpRoot, sj)
- dpCnn = os.path.join(dpSub, expname)
- if not os.path.exists(dpCnn):
- os.makedirs(dpCnn)
- print('📁 Created directory')
- log_file = os.path.join(dpCnn, f'training_log_ch{ch}.txt')
- # 初始化日志
- write_log(log_file, f"=== Training Started for {sj} Channel {ch} ===")
- write_log(log_file, f"GPU ID: {args.gpu_id}")
- write_log(log_file, f"GPU Memory Limit: {args.gpu_memory_limit or 'Auto-growth'}")
- write_log(log_file, f"TensorFlow Version: {tensorflow.__version__}")
- write_log(log_file, f"Memory Optimization: cuda_malloc_async enabled")
- write_log(log_file, f"Block Size: 80x80x80 (Large blocks - high memory usage expected)")
- write_log(log_file, f"Batch Size: {nbatch}, Epochs per Iteration: {nepoch}, Total Iterations: {niter}")
- # 数据文件路径
- fpImg1 = os.path.join(dpSub,'T1_noise1.nii.nii.gz')
- fpImg2 = os.path.join(dpSub,'T1_noise2.nii.nii.gz')
- fpMask = os.path.join(dpSub,'T1_mask.nii.gz')
- print("📂 Loading images...")
- write_log(log_file, "Loading data files...")
- # 监控内存使用
- print("💾 Before loading images:")
- print_gpu_status()
- img1 = nb.load(fpImg1).get_fdata()
- img2 = nb.load(fpImg2).get_fdata()
- mask = nb.load(fpMask).get_fdata()
- print("💾 After loading images:")
- print_gpu_status()
- if len(mask.shape)==3:
- mask = np.expand_dims(mask, 3)
- if len(img1.shape)==3:
- img1 = np.expand_dims(img1, 3)
- if len(img2.shape)==3:
- img2 = np.expand_dims(img2, 3)
- write_log(log_file, f"Image shapes: img1={img1.shape}, img2={img2.shape}, mask={mask.shape}")
- print(f"📐 Image shapes: img1={img1.shape}, img2={img2.shape}, mask={mask.shape}")
- # 使用80x80x80的块大小 - 内存使用量大
- ind_block, ind_brain = qtlib.block_ind(mask, sz_block=80, sz_pad=0)
- write_log(log_file, f"Block extraction: {len(ind_block)} blocks of size 80x80x80")
- print(f"🧮 Block extraction: {len(ind_block)} blocks of size 80x80x80")
- # get stats
- img1_mean = np.mean(img1[mask > 0.5])
- img1_std = np.std(img1[mask > 0.5])
- img2_mean = np.mean(img2[mask > 0.5])
- img2_std = np.std(img2[mask > 0.5])
- write_log(log_file, f"Image statistics - img1: mean={img1_mean:.4f}, std={img1_std:.4f}")
- write_log(log_file, f"Image statistics - img2: mean={img2_mean:.4f}, std={img2_std:.4f}")
- img1_norm = (img1 - img1_mean) / img1_std
- img2_norm = (img2 - img2_mean) / img2_std
- print("🧮 Extracting blocks...")
- img1_norm_block = qtlib.extract_block(img1_norm * mask, ind_block)
- img2_norm_block = qtlib.extract_block(img2_norm * mask, ind_block)
- mask_block = qtlib.extract_block(mask, ind_block)
- print("💾 After block extraction:")
- print_gpu_status()
- imgavg = (img1 + img2) / 2.0
- for jj in np.arange(0, niter):
- iter_start_time = time.time()
- print(f"\n🔄 Starting Iteration {jj}")
- write_log(log_file, f"=== Starting Iteration {jj} ===")
- # 在每次迭代开始时清理显存
- print(f"🧹 Before cleanup (Iteration {jj}):")
- print_gpu_status()
- aggressive_memory_cleanup()
- print(f"✅ After cleanup (Iteration {jj}):")
- print_gpu_status()
- imgavg_block = qtlib.extract_block(imgavg, ind_block)
- imgavg1_norm_block = (imgavg_block - img1_mean) / img1_std
- imgavg2_norm_block = (imgavg_block - img2_mean) / img2_std
- imgres1_block = (imgavg1_norm_block - img1_norm_block)
- imgres2_block = (imgavg2_norm_block - img2_norm_block)
- imgres1_block = np.concatenate((imgres1_block, mask_block), axis=-1)
- imgres2_block = np.concatenate((imgres2_block, mask_block), axis=-1)
- # 明确清理临时变量
- del imgavg_block, imgavg1_norm_block, imgavg2_norm_block
- gc.collect()
- img_block_all = 0 # clear
- imgres_block_all = 0
- mask_block_all = 0
- # get block for image1
- img_block_all = np.concatenate((img1_norm_block, img2_norm_block), axis=0)
- imgres_block_all = np.concatenate((imgres1_block, imgres2_block), axis=0)
- mask_block_all = np.concatenate((mask_block, mask_block), axis=0)
- # 清理临时变量
- del imgres1_block, imgres2_block
- gc.collect()
- # %% flip left right to augment data
- print("🔄 Applying data augmentation...")
- tmp = np.flip(img_block_all, 1)
- img_block_all = np.concatenate((img_block_all, tmp), axis=0)
- del tmp
- gc.collect()
- tmp = np.flip(imgres_block_all, 1)
- imgres_block_all = np.concatenate((imgres_block_all, tmp), axis=0)
- del tmp
- gc.collect()
- tmp = np.flip(mask_block_all, 1)
- mask_block_all = np.concatenate((mask_block_all, tmp), axis=0)
- del tmp
- gc.collect()
- write_log(log_file, f"Data preparation completed. Training blocks: {img_block_all.shape[0]}")
- print(f"📊 Training data prepared: {img_block_all.shape[0]} blocks")
- print("💾 After data augmentation:")
- print_gpu_status()
- # %% set up model with memory monitoring
- dtnet = 0 # clear
- fpCp = os.path.join('../PretrainModel', 'superv_lr1e4_ep20.h5')
- model_load_start = time.time()
- write_log(log_file, 'Loading pretrained model...')
- print("🏗️ Before model loading:")
- print_gpu_status()
- try:
- dtnet = load_model(fpCp, custom_objects={'mean_squared_error_weighted': qtlib.mean_squared_error_weighted})
- model_load_time = time.time() - model_load_start
- write_log(log_file, f'Model loaded successfully in {model_load_time:.2f} seconds')
- print(f"✅ Model loaded in {model_load_time:.2f}s")
- print("💾 After model loading:")
- print_gpu_status()
- except Exception as model_error:
- write_log(log_file, f'❌ Model loading failed: {model_error}')
- print(f"❌ Model loading failed: {model_error}")
- raise model_error
- # %% set up adam optimizer
- adam_opt = Adam(lr=0.00005, beta_1=0.9, beta_2=0.999, epsilon=1e-08)
- dtnet.compile(loss = qtlib.mean_squared_error_weighted, optimizer = adam_opt)
- # train dtnet with enhanced memory management
- for mm in np.arange(0, nepoch):
- starttime = time.time()
- print(f"📚 Starting Epoch {mm}/{nepoch-1} (Iteration {jj})")
- write_log(log_file, f'--- Starting Epoch {mm} of Iteration {jj} ---')
- # 智能内存管理
- memory_cleaned = smart_memory_management(mm, nepoch)
- if memory_cleaned:
- print(" 🔧 Memory management performed")
- # 每5个epoch显示详细GPU状态
- if mm % 5 == 0 or mm == 0:
- print(f"📊 Detailed GPU status at epoch {mm}:")
- print_gpu_status()
- fnCp = 'n2a_iter' + str(jj) + '_lr1e5_ep' + str(mm) + f'_ch{ch}'
- fpCp = os.path.join(dpCnn, fnCp + '.h5')
- # 使用更节省内存的checkpoint策略
- checkpoint = ModelCheckpoint(
- fpCp,
- monitor='val_loss',
- save_best_only=True,
- save_weights_only=False, # 保存完整模型以便后续加载
- verbose=0
- )
- write_log(log_file, f'Training data shape: {img_block_all.shape}')
- try:
- # 训练前小清理
- if mm > 0:
- gc.collect()
- history = dtnet.fit(x = [img_block_all, mask_block_all],
- y = imgres_block_all,
- batch_size = nbatch,
- validation_split=0.2,
- epochs = 1,
- callbacks = [checkpoint],
- verbose = 1,
- shuffle = True)
- epochtime = time.time() - starttime
- # 记录训练时间和损失
- train_loss = history.history['loss'][0] if history.history['loss'] else 'N/A'
- val_loss = history.history['val_loss'][0] if history.history['val_loss'] else 'N/A'
- log_message = f"Iter {jj}, Epoch {mm} completed in {epochtime:.2f}s - Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}"
- write_log(log_file, log_message)
- print(f"✅ {log_message}")
- # save loss
- fpLoss = os.path.join(dpCnn, fnCp + '.mat')
- sio.savemat(fpLoss, {
- 'loss_train':history.history['loss'],
- 'loss_val':history.history['val_loss'],
- 'memory_optimization': 'cuda_malloc_async',
- 'block_size': 80,
- 'gpu_memory_limit': args.gpu_memory_limit
- })
- # 训练后立即清理
- del history
- gc.collect()
- except tf.errors.ResourceExhaustedError as e:
- write_log(log_file, f'❌ OOM Error at epoch {mm}: {str(e)}')
- print(f"❌ GPU OOM at epoch {mm} - attempting recovery...")
- # 紧急内存恢复
- emergency_memory_recovery()
- print("💾 After emergency recovery:")
- print_gpu_status()
- # 尝试重新训练该epoch
- try:
- print("🔄 Retrying training with emergency cleanup...")
- history = dtnet.fit(x = [img_block_all, mask_block_all],
- y = imgres_block_all,
- batch_size = nbatch,
- validation_split=0.2,
- epochs = 1,
- callbacks = [checkpoint],
- verbose = 1,
- shuffle = True)
- print("✅ Emergency retry successful!")
- write_log(log_file, f"Emergency retry successful for epoch {mm}")
- epochtime = time.time() - starttime
- train_loss = history.history['loss'][0] if history.history['loss'] else 'N/A'
- val_loss = history.history['val_loss'][0] if history.history['val_loss'] else 'N/A'
- log_message = f"Iter {jj}, Epoch {mm} (RETRY) completed in {epochtime:.2f}s - Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}"
- write_log(log_file, log_message)
- # save loss
- fpLoss = os.path.join(dpCnn, fnCp + '.mat')
- sio.savemat(fpLoss, {'loss_train':history.history['loss'], 'loss_val':history.history['val_loss']})
- del history
- gc.collect()
- except Exception as retry_error:
- write_log(log_file, f'❌ Emergency retry also failed: {retry_error}')
- print(f"❌ Emergency retry failed, skipping epoch {mm}")
- print("💡 Consider:")
- print(" 1. Reducing block size from 80 to 64 or 48")
- print(" 2. Increasing GPU memory limit")
- print(" 3. Using gradient accumulation")
- continue
- except Exception as e:
- write_log(log_file, f'❌ Training error at epoch {mm}: {e}')
- print(f"❌ Training error at epoch {mm}: {e}")
- raise e
- # 删除旧checkpoint文件
- if mm >= 1:
- fnCp_old = 'n2a_iter' + str(jj) + '_lr1e5_ep' + str(mm - 1) + f'_ch{ch}'
- fpCp_old = os.path.join(dpCnn, fnCp_old + '.h5')
- if os.path.exists(fpCp_old):
- os.remove(fpCp_old)
- write_log(log_file, f"🗑️ Removed old checkpoint: {fnCp_old}.h5")
- # %% apply fine tuned munet for evaluation
- if (mm==(nepoch-1)) or mm % 5 == 0:
- eval_start_time = time.time()
- print(f"📊 Starting evaluation for Epoch {mm}...")
- write_log(log_file, f"Starting evaluation for Epoch {mm}...")
- print(f"💾 Before evaluation (Epoch {mm}):")
- print_gpu_status()
- dpPred = os.path.join(dpSub, expname,'eval')
- if not os.path.exists(dpPred):
- os.makedirs(dpPred)
- write_log(log_file, '📁 Created evaluation directory')
- try:
- # Evaluation with memory management - predict in small batches
- print(" 🔍 Evaluating image1...")
- img_tmp = img1_norm_block
- mask_tmp = mask_block
- pred_tmp = np.zeros(img_tmp.shape)
- num_blocks = img_tmp.shape[0]
- for kk in range(num_blocks):
- if kk % 5 == 0: # Progress indicator every 5 blocks
- print(f" 📊 Block {kk+1}/{num_blocks} ({(kk+1)/num_blocks*100:.1f}%)")
- tmp = dtnet.predict([img_tmp[kk:kk+1, :, :, :, :], mask_tmp[kk:kk+1, :, :, :, :]], verbose=0)
- pred_tmp[kk:kk+1, :, :, :, :] = tmp[:, :, :, :, 0:1] + img_tmp[kk:kk+1, :, :, :, :]
- img1_block_pred = pred_tmp
- print(" 🔍 Evaluating image2...")
- img_tmp = img2_norm_block
- mask_tmp = mask_block
- pred_tmp = np.zeros(img_tmp.shape)
- for kk in range(num_blocks):
- tmp = dtnet.predict([img_tmp[kk:kk+1, :, :, :, :], mask_tmp[kk:kk+1, :, :, :, :]], verbose=0)
- pred_tmp[kk:kk+1, :, :, :, :] = tmp[:, :, :, :, 0:1] + img_tmp[kk:kk+1, :, :, :, :]
- img2_block_pred = pred_tmp
- print(" 🔄 Evaluating flipped image1...")
- img_tmp = np.flip(img1_norm_block, 1)
- mask_tmp = np.flip(mask_block, 1)
- pred_tmp = np.zeros(img_tmp.shape)
- for kk in range(num_blocks):
- tmp = dtnet.predict([img_tmp[kk:kk+1, :, :, :, :], mask_tmp[kk:kk+1, :, :, :, :]], verbose=0)
- pred_tmp[kk:kk+1, :, :, :, :] = tmp[:, :, :, :, 0:1] + img_tmp[kk:kk+1, :, :, :, :]
- img1_block_pred_flip = np.flip(pred_tmp, 1)
- print(" 🔄 Evaluating flipped image2...")
- img_tmp = np.flip(img2_norm_block, 1)
- mask_tmp = np.flip(mask_block, 1)
- pred_tmp = np.zeros(img_tmp.shape)
- for kk in range(num_blocks):
- tmp = dtnet.predict([img_tmp[kk:kk+1, :, :, :, :], mask_tmp[kk:kk+1, :, :, :, :]], verbose=0)
- pred_tmp[kk:kk+1, :, :, :, :] = tmp[:, :, :, :, 0:1] + img_tmp[kk:kk+1, :, :, :, :]
- img2_block_pred_flip = np.flip(pred_tmp, 1)
- print(" 🧮 Combining results...")
- img1_block_pred = (img1_block_pred + img1_block_pred_flip) / 2.0
- img2_block_pred = (img2_block_pred + img2_block_pred_flip) / 2.0
- img1_block_pred = (img1_block_pred * img1_std + img1_mean) * mask_block
- img2_block_pred = (img2_block_pred * img2_std + img2_mean) * mask_block
- img_block_pred = (img1_block_pred + img2_block_pred) / 2.0
- imgavg, vol_count = qtlib.block2brain(img_block_pred, ind_block, mask)
- fpPred = os.path.join(dpPred, 'n2a_iter' + str(jj) + '_lr1e5_ep' + str(mm) + f'_img_ch{ch}.nii.gz')
- qtlib.save_nii(fpPred, imgavg, fpImg1)
- eval_time = time.time() - eval_start_time
- write_log(log_file, f"✅ Evaluation completed in {eval_time:.2f}s, saved to: {fpPred}")
- print(f"✅ Evaluation completed in {eval_time:.2f}s")
- # 清理评估变量
- del img1_block_pred, img2_block_pred, img1_block_pred_flip, img2_block_pred_flip
- del img_block_pred
- gc.collect()
- except Exception as eval_error:
- write_log(log_file, f"❌ Evaluation failed: {eval_error}")
- print(f"❌ Evaluation failed: {eval_error}")
- if "OOM" in str(eval_error) or "out of memory" in str(eval_error).lower():
- print("💡 Evaluation OOM - consider reducing block size or batch evaluation")
- print(f"💾 After evaluation (Epoch {mm}):")
- print_gpu_status()
- # 记录迭代完成时间
- iter_time = time.time() - iter_start_time
- write_log(log_file, f"=== Iteration {jj} completed in {iter_time:.2f}s ===")
- # 清理模型和显存
- print("🧹 Cleaning up after iteration...")
- del dtnet
- aggressive_memory_cleanup()
- write_log(log_file, f"Memory cleared after iteration {jj}")
- print(f"💾 Final GPU status after Iteration {jj}:")
- print_gpu_status()
- write_log(log_file, f"=== Training completed for subject {sj} ===")
- print(f"\n🎉 Training completed for subject {args.subject_idx}: {subjects[args.subject_idx]}")
- print("="*60)
- print("🏁 FINAL GPU STATUS")
- print("="*60)
- print_gpu_status()
s_N2A_cnnTrainN2A.py at commit 93206bc, no license · at the source
Overview
- School of Biomedical Engineering, Tsinghua University, Beijing, P.R. China
- Oxford Centre for Integrative Neuroimaging, FMRIB, Nuffield Department of Clinical Neurosciences, University of Oxford, Oxford, United Kingdom
- Athinoula A. Martinos Center for Biomedical Imaging, Department of Radiology, Massachusetts General Hospital, Charlestown, MA, United States
- Department of Radiology, Harvard Medical School, Boston, MA, United States
- Harvard-MIT Program in Health Sciences and Technology, Massachusetts Institute of Technology, Cambridge, MA, United States
- Department of Engineering Physics, Tsinghua University, Beijing, P.R. China
Abstract
Magnetic resonance imaging (MRI) is a widely adopted non-invasive imaging tool for both clinical diagnosis and neuroscientific research. Nonetheless, the quality of MRI is often hampered by noise. Supervised deep learning-based denoising has proven to outperform conventional methods but requires high-signal-to-noise ratio (SNR) reference data for supervising the training, which considerably reduces its practical feasibility. To address this challenge, we propose a new iterative residual learning strategy entitled “Noise2Average” for denoising MRI data with multiple repetitions, which can be combined with transfer learning for subject-specific self-supervised training. Noise2Average learns to map each noisy repetition to the average of all noisy repetitions by fine-tuning parameters of a pre-trained convolutional neural network (CNN) and recovers higher SNR by averaging all denoised results at the first iteration, and performs this supervised residual learning-based denoising process repeatedly with the denoising results from the previous iteration as the training target for several iterations. The efficacy of Noise2Average is systematically and comprehensively demonstrated on four types of commonly acquired MRI data, including two or more consecutively acquired highly accelerated T1-weighted (T1w) image volumes, two T1w image volumes acquired with different echo times, two diffusion-weighted image (DWI) volumes acquired with opposite phase encoding directions, and two DWI volumes synthesized using different sets of DWI volumes from a diffusion tensor imaging (DTI) scan. Quantitative evaluations show that Noise2Average preserves more image sharpness and textural details and produces more accurate quantitative microstructural metrics from DTI signal modeling than the classic Noise2Noise method and conventional benchmark denoising methods BM4D and AONLM, with denoising performance slightly inferior to that of supervised learning-based denoising method. By reducing the requirement for training data and scan time, Noise2Average substantially increases the feasibility and accessibility of deep learning-based denoising methods for MRI and potentially benefits a wider range of clinical and neuroscientific studies.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repositories
Its files are read in the Code ↔ Paper reader above, with 2 matches between paragraphs and lines of code.
webpages.tuni.fi/foi/gcf-bm3d
Availability: 1 check, the latest on 30 September 2026: the link answers (HTTP 200)
- 30 September 2026: the link answers (HTTP 200)
birthlab/Noise2Average
93206bcdd77309f7b592daf2f700fb5ec27c1bfb, 22 November 2025Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
5 files
- Code/
munet_res.py , Python, 61 lines - Code/
munet_res_dti.py , Python, 57 lines - Code/
qtlib.py , Python, 119 lines - Code/
s_N2A_cnnTrainN2A.py , Python, 667 lines, 2 matches - README.md, Text, 40 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 4 scripts, each with its path and the digest of its content;
- 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
Datasets cited
- doi:10.5061/
dryad.nzs7h44q2 , at Dryad; found in “Data and Code Availability” - doi:10.5061/
dryad.rjdfn2z8g , at Dryad; found in “Data and Code Availability” - figshare:16910290, at figshare; found in “Data and Code Availability”
- humanconnectome.org/
study/ , at Human Connectome Project; found in “Data and Code Availability”hcp-lifespan-aging
Data and Code Availability
T1w and diffusion MRI data from the Human Connectome Project, WU-Minn-Ox Consortium (sections 3.1.4 and 3.2.1) and MGH-USC Consortium (section 3.2.2), are publicly available (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 30 September 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 9 authors, 4 keywords, 5 funders, 86 references.
Cite
This paper
Li, Z., Li, Z., Bilgic, B., Ying, K., Salat, D. H., Polimeni, J. R., Liao, H., Huang, S. Y., & Tian, Q. (2026). Noise2Average: An iterative residual learning strategy for image denoising without clean data. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1163. https://
BibTeX
@article{li2026noise2ave
author = {Li, Zihan and Li, Ziyu and Bilgic, Berkin and Ying, Kui and Salat, David H. and Polimeni, Jonathan R. and Liao, Hongen and Huang, Susie Y. and Tian, Qiyuan},
title = {{Noise2Average: An iterative residual learning strategy for image denoising without clean data}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = mar,
volume = {4},
pages = {IMAG.a.1163},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/
url = {https://
pmid = {41890245},
pmcid = {PMC13015446}
}
RIS
TY - JOUR
AU - Li, Zihan
AU - Li, Ziyu
AU - Bilgic, Berkin
AU - Ying, Kui
AU - Salat, David H.
AU - Polimeni, Jonathan R.
AU - Liao, Hongen
AU - Huang, Susie Y.
AU - Tian, Qiyuan
TI - Noise2Average: An iterative residual learning strategy for image denoising without clean data
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/
VL - 4
SP - IMAG.a.1163
SN - 2837-6056
PB - MIT Press
DO - 10.1162/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1162/
"type": "article-journal",
"title": "Noise2Average: An iterative residual learning strategy for image denoising without clean data",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Li",
"given": "Zihan"
},
{
"family": "Li",
"given": "Ziyu"
},
{
"family": "Bilgic",
"given": "Berkin"
},
{
"family": "Ying",
"given": "Kui"
},
{
"family": "Salat",
"given": "David H."
},
{
"family": "Polimeni",
"given": "Jonathan R."
},
{
"family": "Liao",
"given": "Hongen"
},
{
"family": "Huang",
"given": "Susie Y."
},
{
"family": "Tian",
"given": "Qiyuan"
}
],
"container-title-short":
"volume": "4",
"page": "IMAG.a.1163",
"DOI": "10.1162/
"PMID": "41890245",
"PMCID": "PMC13015446",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
24
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1002/advs.202307965 [code]
- DIMOND: DIffusion Model OptimizatioN with Deep Learning.Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)In common: NiBabel, SciPy, NumPy, structural MRI / diffusion, 10 references, 2 authors
- [2] doi:10.1162/imag.a.1089 [code]
- Effects of diffusion MRI spatial resolution on human brain short-range association fiber reconstruction and structural connectivity estimation.Journal: Imaging neuroscience (Cambridge, Mass.)In common: NiBabel, SciPy, NumPy, structural MRI / diffusion, 8 references, 2 authors
- [3] doi:
- Submillimeter diffusion MRI using an in-plane segmented 3D multi-slab acquisition and denoiser-regularized reconstructionJournal: Medical image analysisIn common: structural MRI / diffusion, 13 references
- [4] doi:10.1038/s41551-025-01457-x [code]
- Ultra-high gradient connectomics and microstructure MRI scanner for imaging of human brain circuits across scales.Journal: Nature biomedical engineeringIn common: structural MRI / diffusion, 10 references, author Susie Y. Huang
- [5] doi:10.1038/s41597-025-05640-2 [code]
- Diff5T: Benchmarking human brain diffusion MRI with an extensive 5.0 Tesla k-space and spatial dataset.Journal: Scientific dataIn common: NiBabel, SciPy, NumPy, methods / tools, structural MRI / diffusion, 13 references
- [6] doi:10.1038/s41586-025-09708-2 [code]
- A probabilistic histological atlas of the human brain for MRI segmentation.Journal: NatureIn common: Keras, TensorFlow, NiBabel, 2 other tools, humanconnectome.org/study/hcp-lifespan-aging, methods / tools, structural MRI / diffusion, 5 references
- [7] doi:10.1162/imag_a_00060 [code]
- Denoising diffusion MRI: Considerations and implications for analysis.Journal: Imaging neuroscience (Cambridge, Mass.)In common: NiBabel, SciPy, NumPy, methods / tools, structural MRI / diffusion, 10 references
- [8] doi:10.1177/0271678x251325399 [code]
- Sex-specific effects of intensity and dose of physical activity on BOLD-fMRI cerebrovascular reactivity and cerebral pulsatility.Journal: Journal of cerebral blood flow and metabolism : official journal of the International Society of Cerebral Blood Flow and MetabolismIn common: Keras, TensorFlow, NiBabel, 2 other tools, humanconnectome.org/study/hcp-lifespan-aging, 6 references
- [9] doi:10.1038/s41597-025-05350-9 [code]
- In vivo submillimeter diffusion MRI dataset of 9 macaque brains curated for tractography.Journal: Scientific dataIn common: Keras, TensorFlow, NiBabel, 2 other tools, methods / tools, structural MRI / diffusion, 6 references
- [10] doi:10.1016/j.neuroimage.2023.120168 [code]
- High-fidelity mesoscale in-vivo diffusion MRI through gSlider-BUDA and circular EPI with S-LORAKS reconstruction.Journal: NeuroImageIn common: structural MRI / diffusion, 10 references
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 4 scripts, and 2 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:4bce1b7c795c152e…
Add the badge to the code's README
If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.
Markdown
<!-- oscr-badge:start -->
[](https://openscicode.org/badge/doi/10.1162/imag.a.1163)
<!-- oscr-badge:end -->reStructuredText
.. oscr-badge:start
.. image:: https://openscicode.org/badge/doi/10.1162/imag.a.1163.svg
:target: https://openscicode.org/badge/doi/10.1162/imag.a.1163
:alt: OSCR
.. oscr-badge:endHTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1162/imag.a.1163"><img src="https://openscicode.org/badge/doi/10.1162/imag.a.1163.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".
If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.
Report a mismatch between the code and the paper · Report a code error (signed in)
Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.
Report a reproduction failure (signed in)
Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
