vueX报错 unknown mutation type: SET_TOTAl

在actions.js中调用的setTotal方法与mutations中的SET_TOTAL方法参数处理不一致,具体表现为当尝试修改总价(totalPrice)和总数(totalMount)时,根据传入的‘PLUS’或‘MINUS’进行加减操作,由于两者的实现不统一,导致了程序逻辑错误。修复此问题需要确保actions和mutations中的方法匹配,以正确更新状态。

在这里插入图片描述
原因:不细心

actions.js 中调用的方法

setTotal ({ commit }, payload) {
	commit('SET_TOTAl', payload);
}

mutations中的方法

SET_TOTAL (state, payload) {
   const { price, type } = payload;
   switch (type) {
     case 'PLUS':
       state.totalPrice += price;
       state.totalMount += 1;
       break;
     case 'MINUS':
       state.totalPrice -= price;
       state.totalMount -= 1;
       break;
     default:
       break;
   }
 }

这两者不一致导致

import numpy as np import pandas as pd import matplotlib.pyplot as plt import os import logging from scipy.optimize import minimize from scipy.stats import norm import seaborn as sns from datetime import datetime from concurrent.futures import ProcessPoolExecutor import warnings from scipy.optimize import differential_evolution import traceback from scipy import stats from scipy.interpolate import CubicSpline warnings.filterwarnings('ignore', category=RuntimeWarning) # ====================== 全局配置 ====================== ATOMIC_MASS = {'C': 12.0107, 'H': 1.00794, 'Cl': 35.453} CONFIG = { 'max_rf_ratio': 6.0, # RF极差限制 'rsd_threshold': 0.1, # RSD阈值 'rsd_weight': 300000, # RSD惩罚权重 'outlier_sigma': 1, # 异常值检测阈值 'min_samples_for_rsd': 2 # RSD计算最小样本数 } # ====================== 主优化类 ====================== class CPOptimizer: def __init__(self, std_file='Standard_Sample.csv', unknown_file=None, result_path='result'): self.std_file = std_file self.unknown_file = unknown_file self.result_path = result_path self.standard_samples = None self.unknown_samples = None self.chain_optimizers = {} self.rf_database = {} os.makedirs(self.result_path, exist_ok=True) # 配置日志系统 log_file = os.path.join(result_path, 'optimization.log') logging.basicConfig( filename=log_file, level=logging.INFO, format='[%(asctime)s] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) self.logger = logging.getLogger('CPOptimizer') console = logging.StreamHandler() console.setLevel(logging.INFO) console.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', '%Y-%m-%d %H:%M:%S')) self.logger.addHandler(console) self.logger.info("CPOptimizer initialized") self.logger.setLevel(logging.DEBUG) # 确保 DEBUG 日志可见 def load_data(self): """加载标准样品和未知样品数据""" try: # 加载标准样品 self.standard_samples = pd.read_csv(self.std_file) self.logger.info(f"Loaded standard samples: {len(self.standard_samples)} records") # 加载未知样品(如果存在) if self.unknown_file and os.path.exists(self.unknown_file): self.unknown_samples = pd.read_csv(self.unknown_file) self.logger.info(f"Loaded unknown samples: {len(self.unknown_samples)} records") else: self.logger.info("No unknown samples file found") return True except Exception as e: self.logger.error(f"Error loading data: {str(e)}") return False def preprocess_standard_samples(self): """预处理标准样品数据""" self.logger.info("Starting standard samples preprocessing...") if self.standard_samples is None: self.logger.error("Standard samples not loaded") return False try: df = self.standard_samples.copy() # 1. 计算分子量 df['Base_MW'] = df['Carbon Number'].apply(self.calculate_base_mw) # 2. 计算氯原子数 df['Cls'] = df.apply(self.calculate_cls, axis=1) # 3. 计算平均分子量 df['Avg_MW'] = df.apply(lambda row: self.calculate_molar_mass(row['Carbon Number'], row['Cls']), axis=1) # 4. 计算总摩尔量 (nmol) df['Total_Molar_nmol'] = (df['Original Mass_ng'] * 1e-9) / df['Avg_MW'] * 1e9 # 5. 标准化峰面积 cl_columns = [f'Cl{i}' for i in range(1, 13)] for col in cl_columns: if col in df.columns: df[f'Norm_{col}'] = df[col] / df['Internal Standard'] self.standard_samples = df # 6. 按碳数分组创建优化器 for carbon_num, group in df.groupby('Carbon Number'): self.chain_optimizers[carbon_num] = ChainOptimizer( carbon_num, group, self.result_path, self.logger ) self.logger.info("Standard samples preprocessed") return True except Exception as e: self.logger.error(f"Error preprocessing standard samples: {str(e)}") self.logger.error(traceback.format_exc()) return False def run_optimization(self): """运行优化流程""" self.logger.info("Starting optimization process...") if not self.chain_optimizers: self.logger.error("No chain optimizers available") return False results = {} total_chains = len(self.chain_optimizers) self.logger.info(f"Optimizing {total_chains} carbon chains using parallel processing") # 使用多进程并行优化 with ProcessPoolExecutor() as executor: futures = {carbon_num: executor.submit(optimizer.optimize) for carbon_num, optimizer in self.chain_optimizers.items()} # 收集结果 successful_chains = 0 for carbon_num, future in futures.items(): try: result = future.result() results[carbon_num] = result if result: self.logger.info(f"Optimization completed for C{carbon_num}") successful_chains += 1 else: self.logger.error(f"Optimization failed for C{carbon_num}") except Exception as e: self.logger.error(f"Error optimizing C{carbon_num}: {str(e)}") results[carbon_num] = False self.logger.info(f"Successfully optimized {successful_chains}/{total_chains} carbon chains") # 构建RF数据库 self.build_rf_database() # 处理未知样品(商业CP) if self.unknown_samples is not None: self.logger.info("Processing unknown samples...") self.process_unknown_samples() # 生成最终报告 self.logger.info("Generating final report...") self.generate_final_report(results) self.logger.info("Optimization process completed") return successful_chains > 0 # 只要至少有一个碳链优化成功就返回True def build_rf_database(self): """构建响应因子(RF)数据库""" self.logger.info("Building RF database...") successful_chains = 0 for carbon_num, optimizer in self.chain_optimizers.items(): rf_values = optimizer.get_rf_values() if rf_values is not None and len(rf_values) > 0: self.rf_database[carbon_num] = rf_values self.logger.info(f"Added RF data for C{carbon_num} with {len(rf_values)} entries") successful_chains += 1 else: self.logger.warning(f"No valid RF data for C{carbon_num}") self.logger.info(f"RF database built with {successful_chains} entries") def process_unknown_samples(self): """处理未知样品""" self.logger.info("Processing unknown samples...") try: df = self.unknown_samples.copy() results = [] for _, row in df.iterrows(): carbon_num = row['Carbon Number'] # 检查是否有RF数据 if carbon_num not in self.rf_database: self.logger.warning(f"No RF data for C{carbon_num}, skipping sample") continue rf_values = self.rf_database[carbon_num] # 计算内标摩尔量 is_molar = (row['Internal Standard Mass_ng'] * 1e-9) / row['Internal Standard MW_g/mol'] * 1e9 molar_results = [] for m in range(1, 13): cl_col = f'Cl{m}' peak_area = row[cl_col] if cl_col in row else 0 # 获取RF几何平均值 rf_key = f'RF_geo_mean_{m}' rf_geo_mean = rf_values.get(rf_key, 0.0) # 计算摩尔量 denominator = row['Internal Standard'] * rf_geo_mean if denominator > 1e-10 and rf_geo_mean > 0: molar = (peak_area * is_molar) / denominator else: molar = 0.0 molar_results.append(molar) # 计算总量 total_molar = sum(molar_results) molar_masses = [self.calculate_molar_mass(carbon_num, m) for m in range(1, 13)] total_mass = sum([molar * mass for molar, mass in zip(molar_results, molar_masses)]) # 存储结果 result = {'StanID': row.get('StanID', 'Unknown'), 'Carbon_Number': carbon_num, 'Total_Molar_nmol': total_molar, 'Total_Mass_ng': total_mass} # 添加各氯代数据 for m in range(1, 13): result[f'Molar_Cl{m}'] = molar_results[m - 1] result[f'Mass_Cl{m}'] = molar_results[m - 1] * molar_masses[m - 1] results.append(result) # 保存结果 result_df = pd.DataFrame(results) unknown_file = os.path.join(self.result_path, 'Unknown_Samples_Results.csv') result_df.to_csv(unknown_file, index=False) self.logger.info(f"Unknown samples processed. Results saved to {unknown_file}") return True except Exception as e: self.logger.error(f"Error processing unknown samples: {str(e)}") self.logger.error(traceback.format_exc()) return False def generate_final_report(self, results): """生成最终报告""" self.logger.info("Generating final congener distribution report...") try: all_reports = [] for carbon_num, optimizer in self.chain_optimizers.items(): report = optimizer.get_final_report() if report is not None: all_reports.append(report) self.logger.info(f"Added report for C{carbon_num}") if not all_reports: self.logger.error("No valid reports to generate final report") return False # 合并所有报告 final_report = pd.concat(all_reports, ignore_index=True) report_file = os.path.join(self.result_path, 'Congener_Distribution_Report.csv') final_report.to_csv(report_file, index=False) self.logger.info(f"Final report generated: {report_file}") return True except Exception as e: self.logger.error(f"Error generating final report: {str(e)}") self.logger.error(traceback.format_exc()) return False # ====================== 实用方法 ====================== @staticmethod def calculate_base_mw(n): """计算基础分子量 (无氯)""" return ATOMIC_MASS['C'] * n + ATOMIC_MASS['H'] * (2 * n + 2) @staticmethod def calculate_molar_mass(n, m): """计算含氯分子量""" base = ATOMIC_MASS['C'] * n + ATOMIC_MASS['H'] * (2 * n + 2 - m) return base + ATOMIC_MASS['Cl'] * m def calculate_cls(self, row): """计算氯原子数 (浮点数)""" n = row['Carbon Number'] target_cl_content = row['Chlorine Content'] base_mw = self.calculate_base_mw(n) chlorine_increment = ATOMIC_MASS['Cl'] - ATOMIC_MASS['H'] # 初始化最佳值 best_m, min_error = 0, float('inf') target_error = 0.01 # 遍历1-12氯原子数 for m in range(1, 13): molar_mass = base_mw + chlorine_increment * m calculated_cl = (ATOMIC_MASS['Cl'] * m / molar_mass) * 100 error = abs(calculated_cl - target_cl_content) # 更新最佳值 if error < min_error: min_error, best_m = error, m if error <= target_error: return float(m) # 浮点优化-完整遍历1-12氯原子数后再确定最优解 m_float, step = float(best_m), 0.1 current_error = min_error # 初始化当前误差为最小误差 for _ in range(50): if current_error <= target_error: break improved = False for direction in [-1, 1]: test_m = m_float + direction * step if test_m < 1 or test_m > 12: continue molar_mass = base_mw + chlorine_increment * test_m calculated_cl = (ATOMIC_MASS['Cl'] * test_m / molar_mass) * 100 error = abs(calculated_cl - target_cl_content) if error < current_error: current_error, m_float, improved = error, test_m, True if not improved: step /= 2 return m_float # ====================== 碳链优化类 ====================== class ChainOptimizer: def __init__(self, carbon_num, samples, result_path, logger): self.carbon_num = carbon_num self.samples = samples self.result_path = result_path self.logger = logger # 结果存储 self.result_df = None self.rf_values = None self.raw_rf_matrix = {m: [] for m in range(1, 13)} self.rf_matrix = {m: [] for m in range(1, 13)} # RF数据存储 self.chain_path = os.path.join(result_path, f'C{carbon_num}') os.makedirs(self.chain_path, exist_ok=True) # 配置参数 self.cfg = CONFIG.copy() self.logger.info(f"Initialized optimizer for C{carbon_num} with {len(samples)} samples") def _safe_obj(self, func, params, *args): """安全的目标函数计算 (防止数值错误)""" if not np.all(np.isfinite(params)): return 1e9 try: return func(params, *args) except Exception: return 1e9 # ====================== 优化阶段1 ====================== def _phase1_obj(self, params, samples): """第一阶段目标函数 (总量匹配)""" cod = 0.0 # 目标函数值 # 遍历所有样品 for i, (_, sample) in enumerate(samples.iterrows()): mu, sigma = params[i * 2], max(0.5, params[i * 2 + 1]) # σ下界保护 # 计算同系物分布 ratios, molars, masses, _ = self.calculate_congener_distribution(mu, sigma, sample['Total_Molar_nmol'], sample) # RSR molar_sum = sum(molars) if sample['Total_Molar_nmol'] > 0: cod += ((sample['Total_Molar_nmol'] - molar_sum) ** 2) / sample['Total_Molar_nmol'] # RSM mass_sum = sum(masses) if sample['Original Mass_ng'] > 0: cod += ((sample['Original Mass_ng'] - mass_sum) ** 2) / sample['Original Mass_ng'] return cod # ====================== 优化阶段2 ====================== def _phase2_obj(self, params, samples): """专注最小化RF的RSD,移除总量误差干扰""" # 1. 重置数据结构 rf_matrix = {m: [] for m in range(1, 13)} # 按氯原子数分组存储RF值 all_sample_rfs = [] # 按样品存储RF值(每个样品一个12元素的列表) # 2. 计算当前参数下的RF值 for i, (_, sample) in enumerate(samples.iterrows()): mu = params[i * 2] sigma = max(0.5, params[i * 2 + 1]) # 计算RF值 _, _, _, rfs = self.calculate_congener_distribution( mu, sigma, sample['Total_Molar_nmol'], sample ) # 存储RF值 sample_rfs = [] for m, rf in enumerate(rfs, start=1): if rf > 1e-6: # 过滤极小值 rf_matrix[m].append(rf) sample_rfs.append(rf) else: sample_rfs.append(0.0) # 保持12个元素 all_sample_rfs.append(sample_rfs) # 添加当前样品的RF序列 # 3. 计算总RSD惩罚 rsd_penalty = 0.0 valid_congeners = 0 for m in range(1, 13): rf_vals = rf_matrix[m] n_vals = len(rf_vals) # 有效性检查 if n_vals < self.cfg['min_samples_for_rsd']: # 样本不足惩罚 (按缺失比例加权) rsd_penalty += self.cfg['rsd_weight'] * ( 1.0 - n_vals / self.cfg['min_samples_for_rsd'] ) continue # 计算几何RSD (更稳健) log_rf = np.log(rf_vals) log_mean = np.mean(log_rf) log_std = np.std(log_rf) geo_rsd = np.sqrt(np.exp(log_std ** 2) - 1) # 几何RSD公式 # 累加惩罚项 rsd_penalty += self.cfg['rsd_weight'] * geo_rsd valid_congeners += 1 # 4. 添加物理约束 (轻量级) phys_penalty = 0.0 for i, (_, sample) in enumerate(samples.iterrows()): mu = params[i * 2] # μ偏离自然值的惩罚 mu_dev = abs(mu - sample['Cls']) if mu_dev > 0.4: phys_penalty += 100 * (mu_dev - 0.3) ** 2 # 5. 添加RF约束惩罚 rf_penalty = 0.0 # a) 低氯代物递增约束 (Cl5-Cl8) for sample_rfs in all_sample_rfs: # 检查Cl5-Cl8是否满足递增:Cl5 < Cl6 < Cl7 < Cl8 for m in range(5, 8): # m=5,6,7 rf_current = sample_rfs[m - 1] # Cl(m) rf_next = sample_rfs[m] # Cl(m+1) # 跳过无效值(RF=0表示该同族体不存在) if rf_current <= 1e-6 or rf_next <= 1e-6: continue # 检查递增约束 if rf_current >= rf_next: rf_penalty += 10000 # 重大惩罚 # b) 高氯代物极差约束 (Cl9-Cl12) for m in range(9, 13): # m=9,10,11,12 rf_vals = rf_matrix[m] rf_vals = [rf for rf in rf_vals if rf > 1e-6] # 过滤无效值 if len(rf_vals) < 2: # 至少需要2个有效值 continue max_rf = max(rf_vals) min_rf = min(rf_vals) ratio = max_rf / min_rf # 检查极差约束(最大/最小 ≤ 10) if ratio > 10.0: rf_penalty += 3000 * (ratio - 10.0) # 按超出比例惩罚 return rsd_penalty + phys_penalty + rf_penalty def get_initial_params(self): """基于RF一致性生成初始参数""" rf_matrix = {m: [] for m in range(1, 13)} # 使用自然μ计算初始RF for _, sample in self.samples.iterrows(): _, _, _, rfs = self.calculate_congener_distribution( sample['Cls'], 1.0, # 初始σ=1.0 sample['Total_Molar_nmol'], sample ) for m, rf in enumerate(rfs, start=1): if rf > 1e-6: rf_matrix[m].append(rf) # 寻找RF最稳定的μ作为初始点 best_mu, best_rsd = None, float('inf') for test_mu in np.linspace(3, 9, 7): # 测试3-9之间的μ total_rsd = 0 valid = 0 for m in range(1, 13): if rf_matrix[m]: # 计算调整因子 adj_factors = [norm.pdf(m, test_mu, 1.0) / max(norm.pdf(m, sample['Cls'], 1.0), 1e-6) for sample in self.samples] adj_rf = [rf * factor for rf, factor in zip(rf_matrix[m], adj_factors)] # 计算几何RSD log_rf = np.log(adj_rf) log_std = np.std(log_rf) total_rsd += np.sqrt(np.exp(log_std ** 2) - 1) valid += 1 avg_rsd = total_rsd / valid if valid else 1e6 if avg_rsd < best_rsd: best_rsd, best_mu = avg_rsd, test_mu # 生成初始参数 initial_params = [] for _ in self.samples.iterrows(): initial_params.extend([best_mu, 1.0]) # 统一使用最优μ return initial_params # ====================== 主优化函数 ====================== def optimize(self): """执行优化过程""" self.logger.info(f"Starting optimization for C{self.carbon_num}") start_time = datetime.now() try: # ===== 第一阶段:差分进化 ===== self.logger.info(f"Phase 1: Differential Evolution for C{self.carbon_num}") # 定义第一阶段边界 phase1_bounds = [] for _, sample in self.samples.iterrows(): w = sample['Cls'] phase1_bounds.extend([ (max(1.0, w - 1.5), min(12.0, w + 1.5)), # μ边界 (0.5, 3.0) # σ边界 ]) # 动态设置优化参数 n_samples = len(self.samples) chain_factor = 1.0 + 0.04 * abs(self.carbon_num - 10) cl_discrete = np.std(self.samples['Chlorine Content']) maxiter = int(300 * chain_factor + 100 * n_samples * (1 + cl_discrete / 10)) popsize = int(15 * chain_factor + 8 * n_samples * (1 + cl_discrete / 20)) convergence_thresh = 1e-3 if n_samples <= 3 else 1e-4 * np.log10(n_samples) # 早停机制历史记录 self.best_history = [] # 执行差分进化 res1 = differential_evolution( self._phase1_obj, phase1_bounds, args=(self.samples,), maxiter=maxiter, popsize=popsize, tol=convergence_thresh, init='latinhypercube', callback=self._early_stopping, workers=1, updating='immediate', polish=True, # 确保最后进行局部优化 strategy='best1bin', # 更有效的策略 mutation=(0.5, 1.0), # 动态变异率 recombination=0.9 # 较高的重组率 ) # 添加重启机制 if res1.fun > 1.0 and res1.nit < maxiter // 2: # 结果不理想且提前停止 self.logger.warning(f"C{self.carbon_num} phase-1 restarted (previous best: {res1.fun:.2e})") # 使用更大种群重启 res1 = differential_evolution( self._phase1_obj, phase1_bounds, args=(self.samples,), maxiter=maxiter, popsize=int(popsize * 1.5), # 增加种群规模 tol=convergence_thresh, init='latinhypercube', polish=True, mutation=(0.7, 1.3), # 更大的变异范围 recombination=0.85 ) # 记录结果 stop_reason = "converged" if res1.success else f"maxiter reached ({res1.nit}/{maxiter})" self.logger.warning(f"C{self.carbon_num} phase-1 stopped: {stop_reason},Best COD: {res1.fun:.4e}") best_total = res1.fun x0 = res1.x # ===== 第二阶段:L-BFGS-B ===== self.logger.info(f"Phase 2: L-BFGS-B Optimization for C{self.carbon_num}") # 定义更严格的边界 phase2_bounds = [] for _, sample in self.samples.iterrows(): w = sample['Cls'] phase2_bounds.extend([ (max(1.0, w - 0.5), min(12.0, w + 0.5)), # μ严格边界 (0.5, 3.0) # σ边界 ]) # 确保初始值在严格边界内 x0_strict = [] for j, (low, high) in enumerate(phase2_bounds): x0_strict.append(np.clip(x0[j], low, high)) # 重置历史记录 self.best_history = [] # 执行L-BFGS-B优化 - 修正参数传递 res2 = minimize( self._phase2_obj, x0_strict, args=(self.samples,), # 只传递一个参数 method='L-BFGS-B', bounds=phase2_bounds, options={ 'maxiter': 1000, # 增加迭代次数 'ftol': 1e-10, # 更严格的收敛阈值 } ) # 记录结果 stop_reason = ("converged" if res2.success else f"maxiter reached ({res2.nit}/300)" if res2.nit >= 300 else "early stopped") self.logger.info(f"C{self.carbon_num} phase-2 stopped: {stop_reason},Final COD: {res2.fun:.4e}") # 检查优化结果是否有效 if not np.all(np.isfinite(res2.x)): self.logger.error(f"Optimization for C{self.carbon_num} produced invalid parameters") return False # ===== 处理结果 ===== opt_params = res2.x results = [] # 计算每个样品的结果 for i, (_, sample) in enumerate(self.samples.iterrows()): mu = opt_params[i * 2] sigma = max(0.5, opt_params[i * 2 + 1]) # σ下界保护 result = self.calculate_sample_result(sample, mu, sigma) if result is not None: results.append(result) # 检查是否有有效结果 if not results: self.logger.error(f"No valid results for C{self.carbon_num}") return False # 保存结果 self.result_df = pd.DataFrame(results) self.calculate_rf_stats() self.save_results() self.generate_plots() duration = (datetime.now() - start_time).total_seconds() self.logger.info(f"Optimization for C{self.carbon_num} completed in {duration:.2f} seconds") return True except Exception as e: self.logger.error(f"Error optimizing C{self.carbon_num}: {e}") self.logger.error(traceback.format_exc()) return False # ====================== 优化辅助方法 ====================== def _early_stopping(self, xk, convergence): """稳健的差分进化早停回调函数""" current_obj = self._phase1_obj(xk, self.samples) self.best_history.append(current_obj) n_iter = len(self.best_history) min_iter = max(50, len(self.samples) * 5) # 基于样本数的动态最小迭代次数 # 1. 强制最小迭代次数 if n_iter < min_iter: return False # 2. 目标函数值接近零时直接停止 if current_obj < 1e-6: # 足够小的目标值 self.logger.debug(f"C{self.carbon_num} early stop: target reached ({current_obj:.2e})") return True # 3. 改进量检测 window = min(20, n_iter // 3) # 更大的窗口 if n_iter > window: recent_values = self.best_history[-window:] improvements = np.abs(np.diff(recent_values)) avg_improve = np.mean(improvements) # 3.1 基于绝对值和相对值的复合条件 if avg_improve < max(1e-5, current_obj * 0.001): # 绝对阈值或相对0.1% self.logger.debug( f"C{self.carbon_num} early stop: avg_improve={avg_improve:.2e} (current={current_obj:.2e})") return True # 4. 平台期检测(更稳健的计算) plateau_window = min(25, n_iter // 4) if n_iter > plateau_window: plateau_values = self.best_history[-plateau_window:] start_val = plateau_values[0] end_val = plateau_values[-1] # 避免除以零错误 if abs(start_val) > 1e-10: relative_improve = abs(start_val - end_val) / abs(start_val) else: relative_improve = 0.0 absolute_improve = abs(start_val - end_val) # 复合平台检测条件 if (relative_improve < 0.001) and (absolute_improve < 0.1 * current_obj): self.logger.debug(f"C{self.carbon_num} early stop: plateau detected " f"(rel={relative_improve:.2%}, abs={absolute_improve:.2e}, current={current_obj:.2e})") return True return False def _lbfgsb_callback(self, xk): """L-BFGS-B早停回调函数""" current_obj = self._phase2_obj(xk, self.samples, self.best_history[0] if self.best_history else 1e9) self.best_history.append(current_obj) # 检查最近3次迭代改进 if len(self.best_history) > 3: recent_improve = abs(np.diff(self.best_history[-3:])).mean() if recent_improve < 5e-5: # 更严格的收敛阈值 # 通过抛出特殊异常停止优化 raise StopIteration("Convergence achieved") def calculate_sample_result(self, sample, mu_opt, sigma_opt): """计算单个样品结果""" try: total_molar = sample['Total_Molar_nmol'] # 计算同系物分布 ratios, molars, masses, rfs = self.calculate_congener_distribution( mu_opt, max(sigma_opt, 0.3), total_molar, sample ) # 检查计算结果是否有效 if not all(np.isfinite(ratios)) or not all(np.isfinite(molars)) or not all(np.isfinite(masses)): self.logger.warning(f"Invalid calculation results for sample {sample['StanID']}") return None # 存储RF值 for m, rf in enumerate(rfs, start=1): if rf > 0: self.raw_rf_matrix[m].append(rf) self.rf_matrix[m].append(rf) # 计算残差 original_mass = sample['Original Mass_ng'] calculated_mass = sum(masses) rsr = ((total_molar - sum(molars)) ** 2) / total_molar if total_molar > 0 else 0 rsm = ((original_mass - calculated_mass) ** 2) / original_mass if original_mass > 0 else 0 # 构建结果字典 result_data = { 'StanID': sample['StanID'], 'Carbon_Number': self.carbon_num, 'Chlorine_Content': sample['Chlorine Content'], 'Total_Molar_nmol': total_molar, 'Total_Mass_ng': original_mass, 'Cls': sample['Cls'], 'mu_natural': sample['Cls'], 'mu_optimized': mu_opt, 'mu_dev': abs(mu_opt - sample['Cls']), 'sigma': sigma_opt, 'RSR': rsr, 'RSM': rsm } # 添加各氯代数据 for m in range(1, 13): result_data[f'Ratio_Cl{m}'] = ratios[m - 1] if len(ratios) > m - 1 else 0.0 result_data[f'Molar_Cl{m}'] = molars[m - 1] if len(molars) > m - 1 else 0.0 result_data[f'Mass_Cl{m}'] = masses[m - 1] if len(masses) > m - 1 else 0.0 result_data[f'RF_Cl{m}'] = rfs[m - 1] if len(rfs) > m - 1 else 0.0 norm_col = f'Norm_Cl{m}' result_data[norm_col] = sample[norm_col] if norm_col in sample else 0.0 return result_data except Exception as e: self.logger.error(f"Error calculating sample result for {sample['StanID']}: {e}") return None def calculate_congener_distribution(self, mu, sigma, total_molar, sample): """计算同系物分布和RF值""" try: n = self.carbon_num m_values = np.arange(1, 13) # 计算正态分布PDF pdf_values = norm.pdf(m_values, mu, max(sigma, 0.3)) # σ下界保护调整为0.5 total_pdf = np.sum(pdf_values) if total_pdf > 0: ratios = pdf_values / total_pdf else: ratios = np.zeros(12) # 检查比率是否有效 if not all(np.isfinite(ratios)): self.logger.warning(f"Invalid PDF ratios for mu={mu}, sigma={sigma}") ratios = np.zeros(12) # 计算摩尔量、质量和RF molars, masses, rfs = [], [], [] is_molar = (sample['Internal Standard Mass_ng'] * 1e-9) / sample['Internal Standard MW_g/mol'] * 1e9 for m, ratio in zip(range(1, 13), ratios): # 摩尔量 molar = total_molar * ratio mw = CPOptimizer.calculate_molar_mass(n, m) mass = molar * mw molars.append(molar) masses.append(mass) # 响应因子(RF) cl_col = f'Cl{m}' peak_area = sample[cl_col] if cl_col in sample else 0 if molar > 1e-10 and peak_area > 0: denominator = max(sample['Internal Standard'] * molar, 1e-10) # 防止除以0 rf = (peak_area * is_molar) / denominator else: rf = 0.0 rfs.append(rf) return ratios, molars, masses, rfs except Exception as e: self.logger.error(f"Error calculating congener distribution: {e}") return np.zeros(12), np.zeros(12), np.zeros(12), np.zeros(12) # ====================== RF统计方法 ====================== def calculate_rf_stats(self): """使用优化后的参数重新计算RF,避免中间状态污染""" self.rf_values = {} rf_matrix = {m: [] for m in range(1, 13)} # 检查是否有有效的结果数据 if self.result_df is None or self.result_df.empty: self.logger.warning(f"No result data available for C{self.carbon_num}") return # 使用最终参数重新计算RF for _, row in self.result_df.iterrows(): # 查找对应的原始样品数据 sample_match = self.samples[self.samples['StanID'] == row['StanID']] if sample_match.empty: self.logger.warning(f"No matching sample found for StanID: {row['StanID']}") continue sample = sample_match.iloc[0] # 使用优化后的参数计算RF _, _, _, rfs = self.calculate_congener_distribution( row['mu_optimized'], row['sigma'], row['Total_Molar_nmol'], sample ) for m, rf in enumerate(rfs, start=1): if rf > 1e-6: rf_matrix[m].append(rf) # 计算几何平均和RSD for m in range(1, 13): rf_vals = rf_matrix[m] if len(rf_vals) > 0: log_vals = np.log(rf_vals) geo_mean = np.exp(np.mean(log_vals)) log_std = np.std(log_vals) geo_rsd = np.sqrt(np.exp(log_std ** 2) - 1) else: geo_mean, geo_rsd = 0.0, np.nan self.rf_values[f'RF_geo_mean_{m}'] = geo_mean self.rf_values[f'RF_geo_rsd_{m}'] = geo_rsd * 100 # 转换为百分比 def _calc_robust_rsd(self, rf_vals): """计算基于MAD的稳健RSD""" if len(rf_vals) < self.cfg['min_samples_for_rsd']: return np.nan # 样本不足返回NaN # 稳健RSD (MAD-based) median = np.median(rf_vals) mad = stats.median_abs_deviation(rf_vals, scale='normal') return mad / median if median > 0 else np.nan # ====================== 结果处理 ====================== def save_results(self): """保存结果到CSV""" if self.result_df is not None: result_file = os.path.join(self.chain_path, f'C{self.carbon_num}_Results.csv') self.result_df.to_csv(result_file, index=False) self.logger.info(f"Results for C{self.carbon_num} saved to {result_file}") def generate_plots(self): """生成所有可视化图表""" if self.result_df is None or self.result_df.empty: return # 生成各样品拟合曲线 for _, sample in self.samples.iterrows(): sample_id = sample['StanID'] sample_results = self.result_df[self.result_df['StanID'] == sample_id] if sample_results.empty: continue result = sample_results.iloc[0] self.plot_fit_curve(result, sample['Chlorine Content']) # 生成RF聚类图 self.plot_rf_cluster() # 生成偏差图 self.plot_deviation() # 生成RSD热力图 self.plot_rsd_heatmap() def plot_fit_curve(self, result, chlorine_content): """绘制拟合曲线图""" try: plt.figure(figsize=(10, 6)) # 获取优化参数 mu_opt = result['mu_optimized'] sigma = max(0.3, result['sigma']) mu_natural = result['mu_natural'] total_molar = result['Total_Molar_nmol'] n = self.carbon_num # 计算理论分布 m_values = np.arange(1, 13) dense_m = np.linspace(1, 12, 200) pdf_values = norm.pdf(m_values, mu_opt, sigma) total_pdf = pdf_values.sum() if total_pdf > 0: pdf_values = pdf_values / total_pdf dense_pdf = norm.pdf(dense_m, mu_opt, sigma) if total_pdf > 0: dense_pdf = dense_pdf / total_pdf # 实际分布 actual_molars = [result[f'Molar_Cl{m}'] for m in range(1, 13)] actual_ratios = [m / total_molar for m in actual_molars] if total_molar > 0 else [0] * 12 # 绘制曲线 plt.plot(dense_m, dense_pdf, 'r-', linewidth=2, label=f'Fit: μ={mu_opt:.2f}, σ={sigma:.2f}') plt.plot(m_values, pdf_values, 'rx', markersize=10, label='Predicted Ratios') plt.plot(m_values, actual_ratios, 'bo', markersize=8, label='Actual Ratios') # 添加双x轴(氯含量) ax1 = plt.gca() ax2 = ax1.twiny() ax2.set_xlim(ax1.get_xlim()) cl_contents = [(ATOMIC_MASS['Cl'] * m) / CPOptimizer.calculate_molar_mass(n, m) * 100 for m in m_values] ax2.set_xticks(m_values) ax2.set_xticklabels([f'{c:.1f}%' for c in cl_contents], fontsize=8, color='#888888') ax2.set_xlabel('Chlorine Content', fontsize=10, color='#888888') # 添加参考线 plt.axvline(mu_natural, color='#888888', linestyle='--', label=f'Natural μ: {mu_natural:.2f}') plt.axvline(mu_opt, color='red', linestyle='--', label=f'Optimized μ: {mu_opt:.2f}') # 图表装饰 plt.title(f'C{self.carbon_num} - {chlorine_content}% Chlorine', fontsize=14) plt.xlabel('Chlorine Substitution (m)', fontsize=12) plt.ylabel('Normalized Distribution', fontsize=12) plt.xticks(range(1, 13)) plt.ylim(bottom=-0.05) plt.grid(True, linestyle='--', alpha=0.7) plt.legend(loc='upper right') # 保存图表 plot_file = os.path.join(self.chain_path, f'Fit_Curve_C{self.carbon_num}_{chlorine_content}.png') plt.tight_layout() plt.savefig(plot_file, dpi=300) plt.close() self.logger.info(f"Fit curve plot saved: {plot_file}") except Exception as e: self.logger.error(f"Error generating fit curve plot: {str(e)}") def plot_rf_cluster(self): """绘制RF聚类图""" try: plt.figure(figsize=(12, 8)) rf_data = [] # 收集RF数据 for _, row in self.result_df.iterrows(): sample_id = row['StanID'] chlorine_content = row['Chlorine_Content'] rfs = [row[f'RF_Cl{m}'] for m in range(1, 13)] rf_data.append({'Sample': f'{sample_id} ({chlorine_content}%)', 'RF': rfs}) # 创建DataFrame df_rf = pd.DataFrame({ 'Chlorine Substitution': np.tile(range(1, 13), len(rf_data)), 'RF': np.concatenate([d['RF'] for d in rf_data]), 'Sample': np.repeat([d['Sample'] for d in rf_data], 12) }) # 绘制条形图 sns.barplot(x='Chlorine Substitution', y='RF', hue='Sample', data=df_rf, palette='viridis', errorbar=None) # 图表装饰 plt.title(f'Response Factor (RF) Cluster - C{self.carbon_num}', fontsize=16) plt.xlabel('Chlorine Substitution', fontsize=12) plt.ylabel('Response Factor (RF)', fontsize=12) plt.yscale('log') #plt.yscale('linear') #plt.ylim(0, 3) #plt.ylim(1e-3, 1e4) plt.legend(title='Sample', loc='upper right', fontsize=10) plt.grid(True, linestyle='--', alpha=0.5, which='both') # 保存图表 plot_file = os.path.join(self.chain_path, f'RF_Cluster_C{self.carbon_num}.png') plt.tight_layout() plt.savefig(plot_file, dpi=300) plt.close() self.logger.info(f"RF cluster plot saved: {plot_file}") except Exception as e: self.logger.error(f"Error generating RF cluster plot: {str(e)}") def plot_deviation(self): """绘制偏差图""" try: fig, ax = plt.subplots(figsize=(10, 6)) # 准备数据 rsr_values = self.result_df['RSR'].values rsm_values = self.result_df['RSM'].values sample_ids = [f"{row['StanID']} ({row['Chlorine_Content']}%)" for _, row in self.result_df.iterrows()] # 绘制柱状图 x = np.arange(len(sample_ids)) width = 0.35 rsr_bars = ax.bar(x - width / 2, rsr_values, width, label='RSR (Molar)', color='skyblue', alpha=0.8) rsm_bars = ax.bar(x + width / 2, rsm_values, width, label='RSM (Mass)', color='salmon', alpha=0.8) # 添加数值标签 for bar in rsr_bars: height = bar.get_height() ax.annotate(f'{height:.2e}', xy=(bar.get_x() + bar.get_width() / 2, height), xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=8) for bar in rsm_bars: height = bar.get_height() ax.annotate(f'{height:.2e}', xy=(bar.get_x() + bar.get_width() / 2, height), xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=8) # 图表装饰 ax.set_xlabel('Sample', fontsize=12) ax.set_ylabel('Residual Value', fontsize=12) ax.set_title(f'Calculation Deviation - C{self.carbon_num}', fontsize=14) ax.set_xticks(x) ax.set_xticklabels(sample_ids, rotation=45, ha='right', fontsize=10) ax.legend(loc='upper right') ax.grid(True, linestyle='--', alpha=0.3, axis='y') # 保存图表 plot_file = os.path.join(self.chain_path, f'Deviation_C{self.carbon_num}.png') plt.tight_layout() plt.savefig(plot_file, dpi=300) plt.close() self.logger.info(f"Deviation plot saved: {plot_file}") except Exception as e: self.logger.error(f"Error generating deviation plot: {str(e)}") def plot_rsd_heatmap(self): """单行热力图:显示优化后的几何RSD""" try: # 1. 准备数据(使用几何RSD) rsd_row = [] for m in range(1, 13): # 直接从RF值中获取几何RSD rsd = self.rf_values.get(f'RF_geo_rsd_{m}', np.nan) rsd_row.append(rsd) # 2. DataFrame 形式:一行、列名 Cl1…Cl12 heatmap_df = pd.DataFrame([rsd_row], columns=[f'Cl{i}' for i in range(1, 13)]) # 3. 绘图 plt.figure(figsize=(12, 1.8)) sns.heatmap( heatmap_df, annot=True, fmt='.1f', cmap='RdYlGn_r', linewidths=0.5, linecolor='#888888', vmin=0, vmax=20, cbar_kws={'label': 'RSD (%)'}, annot_kws={'size': 10} ) # 4. 装饰 plt.title(f'Optimized RF RSD - C{self.carbon_num}', fontsize=13) plt.xlabel('Congener', fontsize=11) plt.ylabel('') # 去掉纵轴标签 plt.yticks([]) # 去掉纵轴刻度 plt.xticks(rotation=0) # 5. 保存 plot_file = os.path.join(self.chain_path, f'Optimized_RSD_Row_Heatmap_C{self.carbon_num}.png') plt.tight_layout() plt.savefig(plot_file, dpi=300) plt.close() self.logger.info(f"Row-wise RSD heatmap saved: {plot_file}") except Exception as e: self.logger.error(f"Error generating row RSD heatmap: {str(e)}\n{traceback.format_exc()}") # ====================== 接口方法 ====================== def get_final_report(self): """获取最终报告""" return self.result_df.copy() if self.result_df is not None else None def get_rf_values(self): """获取RF值 - 确保始终返回有效字典""" if self.rf_values is None or len(self.rf_values) == 0: # 如果没有RF值,尝试重新计算 self.logger.warning(f"No RF values found for C{self.carbon_num}, attempting to recalculate...") self.calculate_rf_stats() # 如果仍然没有RF值,返回一个包含默认值的字典 if self.rf_values is None or len(self.rf_values) == 0: self.logger.error(f"Failed to calculate RF values for C{self.carbon_num}, returning default values") default_rf = {} for m in range(1, 13): default_rf[f'RF_geo_mean_{m}'] = 1.0 default_rf[f'RF_geo_rsd_{m}'] = 100.0 # 高RSD表示不可靠 return default_rf return self.rf_values # ====================== 主函数 ====================== def main(): """主执行函数""" # 初始化优化器 optimizer = CPOptimizer( std_file='Standard_Sample.csv', unknown_file='Unknown_Samples.csv', result_path='result' ) # 执行优化流程 if optimizer.load_data(): if optimizer.preprocess_standard_samples(): optimizer.run_optimization() optimizer.logger.info("Optimization completed successfully") else: optimizer.logger.error("Standard sample preprocessing failed") else: optimizer.logger.error("Data loading failed") if __name__ == "__main__": main()
08-23
代码和第一个文件发上来了import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import KFold import warnings warnings.filterwarnings("ignore") # 文档中iCOVID关键参数(用于特征处理对齐) CLINICAL_NUM_FEATURES = ["age", "albumin", "hemoglobin", "total_protein", "ldh"] # 数值型临床特征(参考🔶1 - 41、🔶1 - 94) CLINICAL_CAT_FEATURES = ["gender", "fever", "cough", "diabetes", "ards", "shock"] # 分类型临床特征(参考🔶1 - 41、🔶1 - 93) OUTCOME_DEF = {0: "censored", 1: "recovered", 2: "deceased"} # 结局类型定义(对应🔶1 - 51的σₙ) MAX_RECOVER_DAY = 31 # 恢复时间>30天设为31天,死亡设为32天(参考🔶1 - 60) # 更新文件路径 file_paths = { "metainfo": "/mnt/patients_metainfo (TARGET GDC,2025).csv", "mutation": "/mnt/patients_mutation (TARGET GDC,2025).csv", "outcomes": "/mnt/patients_outcomes (TARGET GDC,2025).csv", "mrna": "/mnt/mrna_zcores.xlsx", "cna": "/mnt/cna.xlsx" } # 数据加载与一致性校验(文档核心要求) # 加载已有的文件 df_metainfo = pd.read_csv(file_paths["metainfo"]) df_mutation = pd.read_csv(file_paths["mutation"]) df_outcomes = pd.read_csv(file_paths["outcomes"]) df_mrna = pd.ExcelFile(file_paths["mrna"]).parse() # 读取 Excel 文件 df_cna = pd.ExcelFile(file_paths["cna"]).parse() # 读取 Excel 文件 # 查看各文件基础信息 print("各文件患者数量与列名:") for name, df in [("metainfo", df_metainfo), ("mutation", df_mutation), ("outcomes", df_outcomes), ("mrna", df_mrna), ("cna", df_cna)]: print(f"- {name}文件: 患者数={len(df)}, 列数={len(df.columns)}, 列名={list(df.columns)[:5]}...") def unify_patient_id(df_list, base_df, id_col="patient_id"): """ 统一患者ID:以base_df(结局文件)的ID为基准,删除其他文件中不匹配的ID df_list: 待校验的DataFrame列表,base_df: 基准DataFrame(结局文件) """ # 基准ID集合 base_ids = set(base_df[id_col].unique()) print(f"基准结局文件患者数:{len(base_ids)}") # 校验并过滤每个文件 unified_dfs = [] for df in df_list: # 检查ID列是否存在 if id_col not in df.columns: raise ValueError(f"文件缺少{id_col}列,请检查格式!") # 筛选匹配ID df_filtered = df[df[id_col].isin(base_ids)].copy() # 统计删除数量 deleted = len(df) - len(df_filtered) print(f"筛选后{df.columns[0].split('_')[0]}文件:保留{len(df_filtered)}例,删除{deleted}例(ID不匹配)") unified_dfs.append(df_filtered) return unified_dfs + [base_df] # 返回所有筛选后的数据(含基准结局文件) # 执行ID统一(待校验文件:metainfo、mutation、mrna、cna) df_metainfo, df_mutation, df_mrna, df_cna, df_outcomes = unify_patient_id( df_list=[df_metainfo, df_mutation, df_mrna, df_cna], base_df=df_outcomes ) # 再次校验所有文件ID数量是否一致 all_ids = [set(df["patient_id"].unique()) for df in [df_metainfo, df_mutation, df_mrna, df_cna, df_outcomes]] assert len(set.intersection(*all_ids)) == len(all_ids[0]), "ID筛选后仍不匹配,请检查数据!" print(f"\n最终统一患者数:{len(set.intersection(*all_ids))}") def stat_missing_values(df_list, df_names): """统计每个文件的缺失值比例""" missing_stats = [] for df, name in zip(df_list, df_names): # 计算每列缺失率 missing_rate = (df.isnull().sum() / len(df) * 100).round(2) # 筛选缺失率>0的列 missing_cols = missing_rate[missing_rate > 0] if len(missing_cols) > 0: missing_stats.append({ "file": name, "missing_cols": dict(missing_cols), "max_missing_rate": missing_cols.max(), "min_missing_rate": missing_cols.min() }) else: missing_stats.append({ "file": name, "missing_cols": "无", "max_missing_rate": 0, "min_missing_rate": 0 }) # 转为DataFrame展示 df_missing = pd.DataFrame(missing_stats) return df_missing # 统计缺失值 df_missing = stat_missing_values( df_list=[df_metainfo, df_mutation, df_mrna, df_cna, df_outcomes], df_names=["metainfo", "mutation", "mrna", "cna", "outcomes"] ) print("各文件缺失值统计:") print(df_missing.to_string(index=False)) # 分文件特征预处理(严格对齐文档逻辑) def preprocess_metainfo(df_metainfo, id_col="patient_id"): """ 处理临床基础信息:分类特征二值化,数值特征归一化 分类特征(如gender、fever):0=无/女,1=有/男;数值特征(age):归一化到[0,1] """ df = df_metainfo.copy() # 1. 分类特征二值化(参考文档中症状/合并症的编码逻辑🔶1 - 41) cat_features = ["gender", "fever", "cough", "expectoration", "diabetes", "ards", "shock"] for feat in cat_features: if feat in df.columns: # 缺失值填0(无),非缺失值映射为0/1 df[feat] = df[feat].fillna(0) # 若原始值为字符串(如"男"/"女"),转换为0/1 if df[feat].dtype == "object": df[feat] = df[feat].map({"女": 0, "男": 1, "无": 0, "有": 1}).fillna(0) df[feat] = df[feat].astype(int) # 2. 数值特征归一化(年龄,参考文档🔶1 - 136的Min - Max归一化) num_features = ["age"] scaler = MinMaxScaler(feature_range=(0, 1)) for feat in num_features: if feat in df.columns: # 缺失值填均值 df[feat] = df[feat].fillna(df[feat].mean()) # 归一化 df[feat] = scaler.fit_transform(df[[feat]]).flatten() # 3. 保留ID和处理后的特征 keep_cols = [id_col] + cat_features + num_features keep_cols = [col for col in keep_cols if col in df.columns] df_processed = df[keep_cols] return df_processed, scaler # 执行处理 df_metainfo_processed, age_scaler = preprocess_metainfo(df_metainfo) print(f"\nmetainfo处理后:列数={len(df_metainfo_processed.columns)},示例:") print(df_metainfo_processed.head()) def preprocess_mutation(df_mutation, id_col="patient_id"): """ 处理基因突变特征:二值化(0=无突变,1=有突变),缺失值填0 """ df = df_mutation.copy() # 所有基因列(排除ID列) gene_cols = [col for col in df.columns if col != id_col] # 1. 缺失值填0(默认无突变) df[gene_cols] = df[gene_cols].fillna(0) # 2. 二值化(确保所有值为0/1,如原始为"突变"/"野生型"则映射) for col in gene_cols: if df[col].dtype == "object": df[col] = df[col].map({"野生型": 0, "突变": 1}).fillna(0) # 若为数值(如突变频率),>0视为1(有突变) else: df[col] = (df[col] > 0).astype(int) df_processed = df[[id_col] + gene_cols] return df_processed # 执行处理 df_mutation_processed = preprocess_mutation(df_mutation) print(f"\nmutation处理后:基因特征数={len(df_mutation_processed.columns)-1},示例:") print(df_mutation_processed.head()) def preprocess_mrna(df_mrna, id_col="patient_id"): """ 处理mRNA表达特征:z值已标准化,仅填充缺失值(填0,即与均值一致),转换为CSV格式 """ df = df_mrna.copy() # 1. 确保ID列存在且命名统一 if "sample_id" in df.columns and id_col not in df.columns: df.rename(columns={"sample_id": id_col}, inplace=True) # 2. 基因列(排除ID列) gene_cols = [col for col in df.columns if col != id_col] # 3. 缺失值填0(z值为0表示与均值一致,符合文档缺失值处理逻辑🔶1 - 136) df[gene_cols] = df[gene_cols].fillna(0) df_processed = df[[id_col] + gene_cols] return df_processed # 执行处理 df_mrna_processed = preprocess_mrna(df_mrna) print(f"\nmrna处理后:基因特征数={len(df_mrna_processed.columns)-1},示例:") print(df_mrna_processed.head()) def preprocess_cna(df_cna, id_col="patient_id"): """ 处理拷贝数变异特征:编码为-1(缺失)、0(正常)、1(扩增),缺失值填0 """ df = df_cna.copy() gene_cols = [col for col in df.columns if col != id_col] # 1. 缺失值填0(默认正常) df[gene_cols] = df[gene_cols].fillna(0) # 2. 编码(原始值如"缺失"/"正常"/"扩增"映射为-1/0/1) for col in gene_cols: if df[col].dtype == "object": df[col] = df[col].map({"缺失": -1, "正常": 0, "扩增": 1}).fillna(0) # 若为数值(如拷贝数),<0=-1,=0=0,>0=1 else: df[col] = np.where(df[col] < 0, -1, np.where(df[col] > 0, 1, 0)) df_processed = df[[id_col] + gene_cols] return df_processed # 执行处理 df_cna_processed = preprocess_cna(df_cna) print(f"\ncna处理后:基因特征数={len(df_cna_processed.columns)-1},示例:") print(df_cna_processed.head()) def preprocess_outcomes(df_outcomes, id_col="patient_id", max_recover_day=MAX_RECOVER_DAY): """ 处理结局变量:定义σₙ(0=截尾,1=恢复,2=死亡)和tₙ(结局时间) 恢复时间>30天设为31,死亡设为32(参考文档🔶1 - 60) """ df = df_outcomes.copy() # 1. 结局类型编码(σₙ) if "outcome_type" in df.columns: # 若原始为字符串,映射为0/1/2 if df["outcome_type"].dtype == "object": df["outcome_type"] = df["outcome_type"].map({ "censored": 0, "lost_to_follow_up": 0, # 截尾(失访) "recovered": 1, "discharged": 1, # 恢复(出院) "deceased": 2, "death": 2 # 死亡 }).fillna(0) # 缺失值视为截尾 else: # 若无outcome_type,按结局时间推断(参考文档🔶1 - 51) df["outcome_type"] = np.where( df["outcome_days"].isnull(), 0, # 无时间→截尾 np.where(df["outcome_days"] <= 30, 1, 2) # ≤30天→恢复,>30→死亡(暂设) ) # 2. 结局时间处理(tₙ) if "outcome_days" in df.columns: # 缺失值填7(截尾默认时间,参考文档🔶1 - 30) df["outcome_days"] = df["outcome_days"].fillna(7).astype(int) # 恢复患者:>30天设为31 df.loc[(df["outcome_type"] == 1) & (df["outcome_days"] > 30), "outcome_days"] = max_recover_day # 死亡患者:统一设为32 df.loc[df["outcome_type"] == 2, "outcome_days"] = 32 # 截尾患者:时间≤10天(参考文档🔶1 - 30) df.loc[df["outcome_type"] == 0, "outcome_days"] = np.clip(df.loc[df["outcome_type"] == 0, "outcome_days"], 3, 10) # 保留关键列 df_processed = df[[id_col, "outcome_type", "outcome_days"]] return df_processed # 执行处理 df_outcomes_processed = preprocess_outcomes(df_outcomes) print(f"\noutcomes处理后:结局分布={df_outcomes_processed['outcome_type'].value_counts().to_dict()},示例:") print(df_outcomes_processed.head()) # 多表整合为患者级多模态矩阵 def integrate_multimodal_data(df_list, id_col="patient_id"): """ 多表整合:以patient_id为键,左连接所有处理后的DataFrame df_list: 处理后的DataFrame列表(含metainfo、mutation、cna、mrna、blood_cell、outcomes) """ # 以第一个DataFrame为基础,逐步连接其他DataFrame integrated_df = df_list[0].copy() for df in df_list[1:]: integrated_df = integrated_df.merge(df, on=id_col, how="left") # 检查连接后是否有新增缺失值(理论上不应有,因ID已统一) new_missing = integrated_df.isnull().sum().sum() - integrated_df[id_col].isnull().sum() if new_missing > 0: print(f"连接{df.columns[1].split('_')[0]}后新增{new_missing}个缺失值,已填充为0") integrated_df = integrated_df.fillna(0) # 确保无重复行 integrated_df = integrated_df.drop_duplicates(subset=[id_col]) print(f"\n整合后多模态矩阵:患者数={len(integrated_df)},特征数={len(integrated_df.columns)-3}(不含ID、结局类型、结局时间)") return integrated_df # 执行整合(顺序:metainfo→mutation→mrna→cna→outcomes) df_integrated = integrate_multimodal_data([ df_metainfo_processed, df_mutation_processed, df_mrna_processed, df_cna_processed, df_outcomes_processed ]) # 查看整合结果 print("\n整合后数据示例(前5列+最后3列):") print(df_integrated.iloc[:, list(range(5)) + list(range(-3, 0))].head()) # 数据划分与输出(贴合文档验证策略) def split_and_save_data(df_integrated, id_col="patient_id", save_path="./processed_data/"): """ 数据划分:五折交叉验证划分训练/验证集,保存处理后数据(CSV格式) 参考文档🔶1 - 26的五折交叉验证逻辑 """ import os os.makedirs(save_path, exist_ok=True) # 1. 保存完整整合数据 df_integrated.to_csv(os.path.join(save_path, "integrated_patients_data.csv"), index=False) print(f"完整数据已保存至:{os.path.join(save_path, 'integrated_patients_data.csv')}") # 2. 五折交叉验证划分 kf = KFold(n_splits=5, shuffle=True, random_state=42) patients = df_integrated[id_col].unique() fold = 1 for train_idx, val_idx in kf.split(patients): # 划分训练/验证ID train_ids = patients[train_idx] val_ids = patients[val_idx] # 生成训练/验证集 df_train = df_integrated[df_integrated[id_col].isin(train_ids)] df_val = df_integrated[df_integrated[id_col].isin(val_ids)] # 保存 df_train.to_csv(os.path.join(save_path, f"train_fold{fold}.csv"), index=False) df_val.to_csv(os.path.join(save_path, f"val_fold{fold}.csv"), index=False) print(f"Fold{fold}:训练集{len(df_train)}例,验证集{len(df_val)}例,已保存") fold += 1 # 3. 保存特征说明(用于后续模型解释,参考文档FSR机制🔶1 - 61) feature_info = pd.DataFrame({ "feature_name": [col for col in df_integrated.columns if col not in [id_col, "outcome_type", "outcome_days"]], "feature_type": ["clinical" if col in df_metainfo_processed.columns else "mutation" if col in df_mutation_processed.columns else "mrna" if col in df_mrna_processed.columns else "cna" if col in df_cna_processed.columns else "outcome" if col in df_outcomes_processed.columns else ""], "processing_method": ["binary" if col in df_mutation_processed.columns or col in CLINICAL_CAT_FEATURES else "normalized" if col in df_metainfo_processed.columns else "z_score" if col in df_mrna_processed.columns else "ternary" if col in df_cna_processed.columns else "outcome_encoding" if col in df_outcomes_processed.columns else ""] }) feature_info.to_csv(os.path.join(save_path, "feature_metadata.csv"), index=False) print(f"特征说明已保存至:{os.path.join(save_path, 'feature_metadata.csv')}") return df_train, df_val # 执行划分与保存 df_train, df_val = split_and_save_data(df_integrated) # 关键结果可视化(验证处理效果) # 1. 结局分布可视化(参考文档🔶1 - 30) plt.figure(figsize=(12, 4)) # 结局类型分布 plt.subplot(1, 2, 1) outcome_counts = df_integrated["outcome_type"].value_counts().sort_index() outcome_labels = [OUTCOME_DEF[i] for i in outcome_counts.index] plt.bar(outcome_labels, outcome_counts.values, color=["lightblue", "lightgreen", "salmon"]) plt.title("患者结局类型分布(参考文档🔶1 - 30)") plt.ylabel("患者数") # 恢复时间分布(仅恢复患者) plt.subplot(1, 2, 2) recover_days = df_integrated[df_integrated["outcome_type"] == 1]["outcome_days"] plt.hist(recover_days, bins=10, color="lightgreen", edgecolor="black") plt.title("恢复患者时间分布(≤31天,参考文档🔶1 - 60)") plt.xlabel("恢复时间(天)") plt.ylabel("患者数") plt.tight_layout() plt.savefig("./processed_data/outcome_distribution.png", dpi=300, bbox_inches="tight") plt.close() print("\n结局分布图表已保存至:./processed_data/outcome_distribution.png") # 2. 关键生物标志物相关性(参考文档🔶1 - 94的Pearson分析) # 由于当前没有生物标志物数据,此部分代码先注释掉 # markers = ["albumin", "hemoglobin", "total_protein", "ldh"] # markers = [m for m in markers if m in df_integrated.columns] # recover_days = df_integrated[df_integrated["outcome_type"] == 1]["outcome_days"] # corr_results = [] # for marker in markers: # marker_vals = df_integrated[df_integrated["outcome_type"] == 1][marker] # corr, p_val = pearsonr(marker_vals, recover_days) # corr_results.append({"marker": marker, "pearson_corr": corr.round(3), "p_value": p_val.round(4)}) # df_corr = pd.DataFrame(corr_results) # print("\n关键生物标志物与恢复时间的Pearson相关性(参考文档🔶1 - 94):") # print(df_corr.to_string(index=False))
最新发布
10-27
# Ultralytics YOLOv5 🚀, AGPL-3.0 license """ Train a YOLOv5 model on a custom dataset. Models and datasets download automatically from the latest YOLOv5 release. Usage - Single-GPU training: $ python train.py --data coco128.yaml --weights yolov5s.pt --img 640 # from pretrained (recommended) $ python train.py --data coco128.yaml --weights '' --cfg yolov5s.yaml --img 640 # from scratch Usage - Multi-GPU DDP training: $ python -m torch.distributed.run --nproc_per_node 4 --master_port 1 train.py --data coco128.yaml --weights yolov5s.pt --img 640 --device 0,1,2,3 Models: https://github.com/ultralytics/yolov5/tree/master/models Datasets: https://github.com/ultralytics/yolov5/tree/master/data Tutorial: https://docs.ultralytics.com/yolov5/tutorials/train_custom_data """ import argparse import math import os os.environ["GIT_PYTHON_REFRESH"] = "quiet" os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" import random import subprocess import sys import time from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path try: import comet_ml # must be imported before torch (if installed) except ImportError: comet_ml = None import numpy as np import torch import torch.distributed as dist import torch.nn as nn import yaml from torch.optim import lr_scheduler from tqdm import tqdm FILE = Path(__file__).resolve() ROOT = FILE.parents[0] # YOLOv5 root directory if str(ROOT) not in sys.path: sys.path.append(str(ROOT)) # add ROOT to PATH ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative import val as validate # for end-of-epoch mAP from models.experimental import attempt_load from models.yolo import Model from utils.autoanchor import check_anchors from utils.autobatch import check_train_batch_size from utils.callbacks import Callbacks from utils.dataloaders import create_dataloader from utils.downloads import attempt_download, is_url from utils.general import ( LOGGER, TQDM_BAR_FORMAT, check_amp, check_dataset, check_file, check_git_info, check_git_status, check_img_size, check_requirements, check_suffix, check_yaml, colorstr, get_latest_run, increment_path, init_seeds, intersect_dicts, labels_to_class_weights, labels_to_image_weights, methods, one_cycle, print_args, print_mutation, strip_optimizer, yaml_save, ) from utils.loggers import LOGGERS, Loggers from utils.loggers.comet.comet_utils import check_comet_resume from utils.loss import ComputeLoss from utils.metrics import fitness from utils.plots import plot_evolve from utils.torch_utils import ( EarlyStopping, ModelEMA, de_parallel, select_device, smart_DDP, smart_optimizer, smart_resume, torch_distributed_zero_first, ) LOCAL_RANK = int(os.getenv("LOCAL_RANK", -1)) # https://pytorch.org/docs/stable/elastic/run.html RANK = int(os.getenv("RANK", -1)) WORLD_SIZE = int(os.getenv("WORLD_SIZE", 1)) GIT_INFO = check_git_info() def train(hyp, opt, device, callbacks): """ Train a YOLOv5 model on a custom dataset using specified hyperparameters, options, and device, managing datasets, model architecture, loss computation, and optimizer steps. Args: hyp (str | dict): Path to the hyperparameters YAML file or a dictionary of hyperparameters. opt (argparse.Namespace): Parsed command-line arguments containing training options. device (torch.device): Device on which training occurs, e.g., 'cuda' or 'cpu'. callbacks (Callbacks): Callback functions for various training events. Returns: None Models and datasets download automatically from the latest YOLOv5 release. Example: Single-GPU training: ```bash $ python train.py --data coco128.yaml --weights yolov5s.pt --img 640 # from pretrained (recommended) $ python train.py --data coco128.yaml --weights '' --cfg yolov5s.yaml --img 640 # from scratch ``` Multi-GPU DDP training: ```bash $ python -m torch.distributed.run --nproc_per_node 4 --master_port 1 train.py --data coco128.yaml --weights yolov5s.pt --img 640 --device 0,1,2,3 ``` For more usage details, refer to: - Models: https://github.com/ultralytics/yolov5/tree/master/models - Datasets: https://github.com/ultralytics/yolov5/tree/master/data - Tutorial: https://docs.ultralytics.com/yolov5/tutorials/train_custom_data """ save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze = ( Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze, ) callbacks.run("on_pretrain_routine_start") # Directories w = save_dir / "weights" # weights dir (w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir last, best = w / "last.pt", w / "best.pt" # Hyperparameters if isinstance(hyp, str): with open(hyp, errors="ignore") as f: hyp = yaml.safe_load(f) # load hyps dict LOGGER.info(colorstr("hyperparameters: ") + ", ".join(f"{k}={v}" for k, v in hyp.items())) opt.hyp = hyp.copy() # for saving hyps to checkpoints # Save run settings if not evolve: yaml_save(save_dir / "hyp.yaml", hyp) yaml_save(save_dir / "opt.yaml", vars(opt)) # Loggers data_dict = None if RANK in {-1, 0}: include_loggers = list(LOGGERS) if getattr(opt, "ndjson_console", False): include_loggers.append("ndjson_console") if getattr(opt, "ndjson_file", False): include_loggers.append("ndjson_file") loggers = Loggers( save_dir=save_dir, weights=weights, opt=opt, hyp=hyp, logger=LOGGER, include=tuple(include_loggers), ) # Register actions for k in methods(loggers): callbacks.register_action(k, callback=getattr(loggers, k)) # Process custom dataset artifact link data_dict = loggers.remote_dataset if resume: # If resuming runs from remote artifact weights, epochs, hyp, batch_size = opt.weights, opt.epochs, opt.hyp, opt.batch_size # Config plots = not evolve and not opt.noplots # create plots cuda = device.type != "cpu" init_seeds(opt.seed + 1 + RANK, deterministic=True) with torch_distributed_zero_first(LOCAL_RANK): data_dict = data_dict or check_dataset(data) # check if None train_path, val_path = data_dict["train"], data_dict["val"] nc = 1 if single_cls else int(data_dict["nc"]) # number of classes names = {0: "item"} if single_cls and len(data_dict["names"]) != 1 else data_dict["names"] # class names is_coco = isinstance(val_path, str) and val_path.endswith("coco/val2017.txt") # COCO dataset # Model check_suffix(weights, ".pt") # check weights pretrained = weights.endswith(".pt") if pretrained: with torch_distributed_zero_first(LOCAL_RANK): weights = attempt_download(weights) # download if not found locally ckpt = torch.load(weights, map_location="cpu") # load checkpoint to CPU to avoid CUDA memory leak model = Model(cfg or ckpt["model"].yaml, ch=3, nc=nc, anchors=hyp.get("anchors")).to(device) # create exclude = ["anchor"] if (cfg or hyp.get("anchors")) and not resume else [] # exclude keys csd = ckpt["model"].float().state_dict() # checkpoint state_dict as FP32 csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect model.load_state_dict(csd, strict=False) # load LOGGER.info(f"Transferred {len(csd)}/{len(model.state_dict())} items from {weights}") # report else: model = Model(cfg, ch=3, nc=nc, anchors=hyp.get("anchors")).to(device) # create amp = check_amp(model) # check AMP # Freeze freeze = [f"model.{x}." for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze for k, v in model.named_parameters(): v.requires_grad = True # train all layers # v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results) if any(x in k for x in freeze): LOGGER.info(f"freezing {k}") v.requires_grad = False # Image size gs = max(int(model.stride.max()), 32) # grid size (max stride) imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2) # verify imgsz is gs-multiple # Batch size if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size batch_size = check_train_batch_size(model, imgsz, amp) loggers.on_params_update({"batch_size": batch_size}) # Optimizer nbs = 64 # nominal batch size accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing hyp["weight_decay"] *= batch_size * accumulate / nbs # scale weight_decay optimizer = smart_optimizer(model, opt.optimizer, hyp["lr0"], hyp["momentum"], hyp["weight_decay"]) # Scheduler if opt.cos_lr: lf = one_cycle(1, hyp["lrf"], epochs) # cosine 1->hyp['lrf'] else: def lf(x): """Linear learning rate scheduler function with decay calculated by epoch proportion.""" return (1 - x / epochs) * (1.0 - hyp["lrf"]) + hyp["lrf"] # linear scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs) # EMA ema = ModelEMA(model) if RANK in {-1, 0} else None # Resume best_fitness, start_epoch = 0.0, 0 if pretrained: if resume: best_fitness, start_epoch, epochs = smart_resume(ckpt, optimizer, ema, weights, epochs, resume) del ckpt, csd # DP mode if cuda and RANK == -1 and torch.cuda.device_count() > 1: LOGGER.warning( "WARNING ⚠️ DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n" "See Multi-GPU Tutorial at https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training to get started." ) model = torch.nn.DataParallel(model) # SyncBatchNorm if opt.sync_bn and cuda and RANK != -1: model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device) LOGGER.info("Using SyncBatchNorm()") # Trainloader train_loader, dataset = create_dataloader( train_path, imgsz, batch_size // WORLD_SIZE, gs, single_cls, hyp=hyp, augment=True, cache=None if opt.cache == "val" else opt.cache, rect=opt.rect, rank=LOCAL_RANK, workers=workers, image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr("train: "), shuffle=True, seed=opt.seed, ) labels = np.concatenate(dataset.labels, 0) mlc = int(labels[:, 0].max()) # max label class assert mlc < nc, f"Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}" # Process 0 if RANK in {-1, 0}: val_loader = create_dataloader( val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls, hyp=hyp, cache=None if noval else opt.cache, rect=True, rank=-1, workers=workers * 2, pad=0.5, prefix=colorstr("val: "), )[0] if not resume: if not opt.noautoanchor: check_anchors(dataset, model=model, thr=hyp["anchor_t"], imgsz=imgsz) # run AutoAnchor model.half().float() # pre-reduce anchor precision callbacks.run("on_pretrain_routine_end", labels, names) # DDP mode if cuda and RANK != -1: model = smart_DDP(model) # Model attributes nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps) hyp["box"] *= 3 / nl # scale to layers hyp["cls"] *= nc / 80 * 3 / nl # scale to classes and layers hyp["obj"] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers hyp["label_smoothing"] = opt.label_smoothing model.nc = nc # attach number of classes to model model.hyp = hyp # attach hyperparameters to model model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights model.names = names # Start training t0 = time.time() nb = len(train_loader) # number of batches nw = max(round(hyp["warmup_epochs"] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations) # nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training last_opt_step = -1 maps = np.zeros(nc) # mAP per class results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) scheduler.last_epoch = start_epoch - 1 # do not move scaler = torch.cuda.amp.GradScaler(enabled=amp) stopper, stop = EarlyStopping(patience=opt.patience), False compute_loss = ComputeLoss(model) # init loss class callbacks.run("on_train_start") LOGGER.info( f'Image sizes {imgsz} train, {imgsz} val\n' f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n' f"Logging results to {colorstr('bold', save_dir)}\n" f'Starting training for {epochs} epochs...' ) for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------ callbacks.run("on_train_epoch_start") model.train() # Update image weights (optional, single-GPU only) if opt.image_weights: cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx # Update mosaic border (optional) # b = int(random.uniform(0.25 * imgsz, 0.75 * imgsz + gs) // gs * gs) # dataset.mosaic_border = [b - imgsz, -b] # height, width borders mloss = torch.zeros(3, device=device) # mean losses if RANK != -1: train_loader.sampler.set_epoch(epoch) pbar = enumerate(train_loader) LOGGER.info(("\n" + "%11s" * 7) % ("Epoch", "GPU_mem", "box_loss", "obj_loss", "cls_loss", "Instances", "Size")) if RANK in {-1, 0}: pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar optimizer.zero_grad() for i, (imgs, targets, paths, _) in pbar: # batch ------------------------------------------------------------- callbacks.run("on_train_batch_start") ni = i + nb * epoch # number integrated batches (since train start) imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0 # Warmup if ni <= nw: xi = [0, nw] # x interp # compute_loss.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou) accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round()) for j, x in enumerate(optimizer.param_groups): # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 x["lr"] = np.interp(ni, xi, [hyp["warmup_bias_lr"] if j == 0 else 0.0, x["initial_lr"] * lf(epoch)]) if "momentum" in x: x["momentum"] = np.interp(ni, xi, [hyp["warmup_momentum"], hyp["momentum"]]) # Multi-scale if opt.multi_scale: sz = random.randrange(int(imgsz * 0.5), int(imgsz * 1.5) + gs) // gs * gs # size sf = sz / max(imgs.shape[2:]) # scale factor if sf != 1: ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple) imgs = nn.functional.interpolate(imgs, size=ns, mode="bilinear", align_corners=False) # Forward with torch.cuda.amp.autocast(amp): pred = model(imgs) # forward loss, loss_items = compute_loss(pred, targets.to(device)) # loss scaled by batch_size if RANK != -1: loss *= WORLD_SIZE # gradient averaged between devices in DDP mode if opt.quad: loss *= 4.0 # Backward scaler.scale(loss).backward() # Optimize - https://pytorch.org/docs/master/notes/amp_examples.html if ni - last_opt_step >= accumulate: scaler.unscale_(optimizer) # unscale gradients torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) # clip gradients scaler.step(optimizer) # optimizer.step scaler.update() optimizer.zero_grad() if ema: ema.update(model) last_opt_step = ni # Log if RANK in {-1, 0}: mloss = (mloss * i + loss_items) / (i + 1) # update mean losses mem = f"{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G" # (GB) pbar.set_description( ("%11s" * 2 + "%11.4g" * 5) % (f"{epoch}/{epochs - 1}", mem, *mloss, targets.shape[0], imgs.shape[-1]) ) callbacks.run("on_train_batch_end", model, ni, imgs, targets, paths, list(mloss)) if callbacks.stop_training: return # end batch ------------------------------------------------------------------------------------------------ # Scheduler lr = [x["lr"] for x in optimizer.param_groups] # for loggers scheduler.step() if RANK in {-1, 0}: # mAP callbacks.run("on_train_epoch_end", epoch=epoch) ema.update_attr(model, include=["yaml", "nc", "hyp", "names", "stride", "class_weights"]) final_epoch = (epoch + 1 == epochs) or stopper.possible_stop if not noval or final_epoch: # Calculate mAP results, maps, _ = validate.run( data_dict, batch_size=batch_size // WORLD_SIZE * 2, imgsz=imgsz, half=amp, model=ema.ema, single_cls=single_cls, dataloader=val_loader, save_dir=save_dir, plots=False, callbacks=callbacks, compute_loss=compute_loss, ) # Update best mAP fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] stop = stopper(epoch=epoch, fitness=fi) # early stop check if fi > best_fitness: best_fitness = fi log_vals = list(mloss) + list(results) + lr callbacks.run("on_fit_epoch_end", log_vals, epoch, best_fitness, fi) # Save model if (not nosave) or (final_epoch and not evolve): # if save ckpt = { "epoch": epoch, "best_fitness": best_fitness, "model": deepcopy(de_parallel(model)).half(), "ema": deepcopy(ema.ema).half(), "updates": ema.updates, "optimizer": optimizer.state_dict(), "opt": vars(opt), "git": GIT_INFO, # {remote, branch, commit} if a git repo "date": datetime.now().isoformat(), } # Save last, best and delete torch.save(ckpt, last) if best_fitness == fi: torch.save(ckpt, best) if opt.save_period > 0 and epoch % opt.save_period == 0: torch.save(ckpt, w / f"epoch{epoch}.pt") del ckpt callbacks.run("on_model_save", last, epoch, final_epoch, best_fitness, fi) # EarlyStopping if RANK != -1: # if DDP training broadcast_list = [stop if RANK == 0 else None] dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranks if RANK != 0: stop = broadcast_list[0] if stop: break # must break all DDP ranks # end epoch ---------------------------------------------------------------------------------------------------- # end training ----------------------------------------------------------------------------------------------------- if RANK in {-1, 0}: LOGGER.info(f"\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.") for f in last, best: if f.exists(): strip_optimizer(f) # strip optimizers if f is best: LOGGER.info(f"\nValidating {f}...") results, _, _ = validate.run( data_dict, batch_size=batch_size // WORLD_SIZE * 2, imgsz=imgsz, model=attempt_load(f, device).half(), iou_thres=0.65 if is_coco else 0.60, # best pycocotools at iou 0.65 single_cls=single_cls, dataloader=val_loader, save_dir=save_dir, save_json=is_coco, verbose=True, plots=plots, callbacks=callbacks, compute_loss=compute_loss, ) # val best model with plots if is_coco: callbacks.run("on_fit_epoch_end", list(mloss) + list(results) + lr, epoch, best_fitness, fi) callbacks.run("on_train_end", last, best, epoch, results) torch.cuda.empty_cache() return results def parse_opt(known=False): """ Parse command-line arguments for YOLOv5 training, validation, and testing. Args: known (bool, optional): If True, parses known arguments, ignoring the unknown. Defaults to False. Returns: (argparse.Namespace): Parsed command-line arguments containing options for YOLOv5 execution. Example: ```python from ultralytics.yolo import parse_opt opt = parse_opt() print(opt) ``` Links: - Models: https://github.com/ultralytics/yolov5/tree/master/models - Datasets: https://github.com/ultralytics/yolov5/tree/master/data - Tutorial: https://docs.ultralytics.com/yolov5/tutorials/train_custom_data """ parser = argparse.ArgumentParser() parser.add_argument("--weights", type=str, default=ROOT / r"E:/yolov5-master/yolov5m.pt", help="initial weights path") parser.add_argument("--cfg", type=str, default=r"models/yolov5m.yaml", help="model.yaml path") parser.add_argument("--data", type=str, default=ROOT / r"E:/yolov5-master/data/data.yaml", help="dataset.yaml path") parser.add_argument("--hyp", type=str, default=ROOT / "data/hyps/hyp.scratch-low.yaml", help="hyperparameters path") parser.add_argument("--epochs", type=int, default=150, help="total training epochs") parser.add_argument("--batch-size", type=int, default=16, help="total batch size for all GPUs, -1 for autobatch") parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="train, val image size (pixels)") parser.add_argument("--rect", action="store_true", help="rectangular training") parser.add_argument("--resume", nargs="?", const=True, default=False, help="resume most recent training") parser.add_argument("--nosave", action="store_true", help="only save final checkpoint") parser.add_argument("--noval", action="store_true", help="only validate final epoch") parser.add_argument("--noautoanchor", action="store_true", help="disable AutoAnchor") parser.add_argument("--noplots", action="store_true", help="save no plot files") parser.add_argument("--evolve", type=int, nargs="?", const=300, help="evolve hyperparameters for x generations") parser.add_argument( "--evolve_population", type=str, default=ROOT / "data/hyps", help="location for loading population" ) parser.add_argument("--resume_evolve", type=str, default=None, help="resume evolve from last generation") parser.add_argument("--bucket", type=str, default="", help="gsutil bucket") parser.add_argument("--cache", type=str, nargs="?", const="ram", help="image --cache ram/disk") parser.add_argument("--image-weights", action="store_true", help="use weighted image selection for training") parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu") parser.add_argument("--multi-scale", action="store_true", help="vary img-size +/- 50%%") parser.add_argument("--single-cls", action="store_true", help="train multi-class data as single-class") parser.add_argument("--optimizer", type=str, choices=["SGD", "Adam", "AdamW"], default="SGD", help="optimizer") parser.add_argument("--sync-bn", action="store_true", help="use SyncBatchNorm, only available in DDP mode") parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)") parser.add_argument("--project", default=ROOT / "runs/train", help="save to project/name") parser.add_argument("--name", default="exp", help="save to project/name") parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment") parser.add_argument("--quad", action="store_true", help="quad dataloader") parser.add_argument("--cos-lr", action="store_true", help="cosine LR scheduler") parser.add_argument("--label-smoothing", type=float, default=0.0, help="Label smoothing epsilon") parser.add_argument("--patience", type=int, default=100, help="EarlyStopping patience (epochs without improvement)") parser.add_argument("--freeze", nargs="+", type=int, default=[0], help="Freeze layers: backbone=10, first3=0 1 2") parser.add_argument("--save-period", type=int, default=-1, help="Save checkpoint every x epochs (disabled if < 1)") parser.add_argument("--seed", type=int, default=0, help="Global training seed") parser.add_argument("--local_rank", type=int, default=-1, help="Automatic DDP Multi-GPU argument, do not modify") # Logger arguments parser.add_argument("--entity", default=None, help="Entity") parser.add_argument("--upload_dataset", nargs="?", const=True, default=False, help='Upload data, "val" option') parser.add_argument("--bbox_interval", type=int, default=-1, help="Set bounding-box image logging interval") parser.add_argument("--artifact_alias", type=str, default="latest", help="Version of dataset artifact to use") # NDJSON logging parser.add_argument("--ndjson-console", action="store_true", help="Log ndjson to console") parser.add_argument("--ndjson-file", action="store_true", help="Log ndjson to file") return parser.parse_known_args()[0] if known else parser.parse_args() def main(opt, callbacks=Callbacks()): """ Runs the main entry point for training or hyperparameter evolution with specified options and optional callbacks. Args: opt (argparse.Namespace): The command-line arguments parsed for YOLOv5 training and evolution. callbacks (ultralytics.utils.callbacks.Callbacks, optional): Callback functions for various training stages. Defaults to Callbacks(). Returns: None Note: For detailed usage, refer to: https://github.com/ultralytics/yolov5/tree/master/models """ if RANK in {-1, 0}: print_args(vars(opt)) check_git_status() check_requirements(ROOT / "requirements.txt") # Resume (from specified or most recent last.pt) if opt.resume and not check_comet_resume(opt) and not opt.evolve: last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run()) opt_yaml = last.parent.parent / "opt.yaml" # train options yaml opt_data = opt.data # original dataset if opt_yaml.is_file(): with open(opt_yaml, errors="ignore") as f: d = yaml.safe_load(f) else: d = torch.load(last, map_location="cpu")["opt"] opt = argparse.Namespace(**d) # replace opt.cfg, opt.weights, opt.resume = "", str(last), True # reinstate if is_url(opt_data): opt.data = check_file(opt_data) # avoid HUB resume auth timeout else: opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = ( check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project), ) # checks assert len(opt.cfg) or len(opt.weights), "either --cfg or --weights must be specified" if opt.evolve: if opt.project == str(ROOT / "runs/train"): # if default project name, rename to runs/evolve opt.project = str(ROOT / "runs/evolve") opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume if opt.name == "cfg": opt.name = Path(opt.cfg).stem # use model.yaml as name opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) # DDP mode device = select_device(opt.device, batch_size=opt.batch_size) if LOCAL_RANK != -1: msg = "is not compatible with YOLOv5 Multi-GPU DDP training" assert not opt.image_weights, f"--image-weights {msg}" assert not opt.evolve, f"--evolve {msg}" assert opt.batch_size != -1, f"AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size" assert opt.batch_size % WORLD_SIZE == 0, f"--batch-size {opt.batch_size} must be multiple of WORLD_SIZE" assert torch.cuda.device_count() > LOCAL_RANK, "insufficient CUDA devices for DDP command" torch.cuda.set_device(LOCAL_RANK) device = torch.device("cuda", LOCAL_RANK) dist.init_process_group( backend="nccl" if dist.is_nccl_available() else "gloo", timeout=timedelta(seconds=10800) ) # Train if not opt.evolve: train(opt.hyp, opt, device, callbacks) # Evolve hyperparameters (optional) else: # Hyperparameter evolution metadata (including this hyperparameter True-False, lower_limit, upper_limit) meta = { "lr0": (False, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3) "lrf": (False, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf) "momentum": (False, 0.6, 0.98), # SGD momentum/Adam beta1 "weight_decay": (False, 0.0, 0.001), # optimizer weight decay "warmup_epochs": (False, 0.0, 5.0), # warmup epochs (fractions ok) "warmup_momentum": (False, 0.0, 0.95), # warmup initial momentum "warmup_bias_lr": (False, 0.0, 0.2), # warmup initial bias lr "box": (False, 0.02, 0.2), # box loss gain "cls": (False, 0.2, 4.0), # cls loss gain "cls_pw": (False, 0.5, 2.0), # cls BCELoss positive_weight "obj": (False, 0.2, 4.0), # obj loss gain (scale with pixels) "obj_pw": (False, 0.5, 2.0), # obj BCELoss positive_weight "iou_t": (False, 0.1, 0.7), # IoU training threshold "anchor_t": (False, 2.0, 8.0), # anchor-multiple threshold "anchors": (False, 2.0, 10.0), # anchors per output grid (0 to ignore) "fl_gamma": (False, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5) "hsv_h": (True, 0.0, 0.1), # image HSV-Hue augmentation (fraction) "hsv_s": (True, 0.0, 0.9), # image HSV-Saturation augmentation (fraction) "hsv_v": (True, 0.0, 0.9), # image HSV-Value augmentation (fraction) "degrees": (True, 0.0, 45.0), # image rotation (+/- deg) "translate": (True, 0.0, 0.9), # image translation (+/- fraction) "scale": (True, 0.0, 0.9), # image scale (+/- gain) "shear": (True, 0.0, 10.0), # image shear (+/- deg) "perspective": (True, 0.0, 0.001), # image perspective (+/- fraction), range 0-0.001 "flipud": (True, 0.0, 1.0), # image flip up-down (probability) "fliplr": (True, 0.0, 1.0), # image flip left-right (probability) "mosaic": (True, 0.0, 1.0), # image mosaic (probability) "mixup": (True, 0.0, 1.0), # image mixup (probability) "copy_paste": (True, 0.0, 1.0), # segment copy-paste (probability) } # GA configs pop_size = 50 mutation_rate_min = 0.01 mutation_rate_max = 0.5 crossover_rate_min = 0.5 crossover_rate_max = 1 min_elite_size = 2 max_elite_size = 5 tournament_size_min = 2 tournament_size_max = 10 with open(opt.hyp, errors="ignore") as f: hyp = yaml.safe_load(f) # load hyps dict if "anchors" not in hyp: # anchors commented in hyp.yaml hyp["anchors"] = 3 if opt.noautoanchor: del hyp["anchors"], meta["anchors"] opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch # ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices evolve_yaml, evolve_csv = save_dir / "hyp_evolve.yaml", save_dir / "evolve.csv" if opt.bucket: # download evolve.csv if exists subprocess.run( [ "gsutil", "cp", f"gs://{opt.bucket}/evolve.csv", str(evolve_csv), ] ) # Delete the items in meta dictionary whose first value is False del_ = [item for item, value_ in meta.items() if value_[0] is False] hyp_GA = hyp.copy() # Make a copy of hyp dictionary for item in del_: del meta[item] # Remove the item from meta dictionary del hyp_GA[item] # Remove the item from hyp_GA dictionary # Set lower_limit and upper_limit arrays to hold the search space boundaries lower_limit = np.array([meta[k][1] for k in hyp_GA.keys()]) upper_limit = np.array([meta[k][2] for k in hyp_GA.keys()]) # Create gene_ranges list to hold the range of values for each gene in the population gene_ranges = [(lower_limit[i], upper_limit[i]) for i in range(len(upper_limit))] # Initialize the population with initial_values or random values initial_values = [] # If resuming evolution from a previous checkpoint if opt.resume_evolve is not None: assert os.path.isfile(ROOT / opt.resume_evolve), "evolve population path is wrong!" with open(ROOT / opt.resume_evolve, errors="ignore") as f: evolve_population = yaml.safe_load(f) for value in evolve_population.values(): value = np.array([value[k] for k in hyp_GA.keys()]) initial_values.append(list(value)) # If not resuming from a previous checkpoint, generate initial values from .yaml files in opt.evolve_population else: yaml_files = [f for f in os.listdir(opt.evolve_population) if f.endswith(".yaml")] for file_name in yaml_files: with open(os.path.join(opt.evolve_population, file_name)) as yaml_file: value = yaml.safe_load(yaml_file) value = np.array([value[k] for k in hyp_GA.keys()]) initial_values.append(list(value)) # Generate random values within the search space for the rest of the population if initial_values is None: population = [generate_individual(gene_ranges, len(hyp_GA)) for _ in range(pop_size)] elif pop_size > 1: population = [generate_individual(gene_ranges, len(hyp_GA)) for _ in range(pop_size - len(initial_values))] for initial_value in initial_values: population = [initial_value] + population # Run the genetic algorithm for a fixed number of generations list_keys = list(hyp_GA.keys()) for generation in range(opt.evolve): if generation >= 1: save_dict = {} for i in range(len(population)): little_dict = {list_keys[j]: float(population[i][j]) for j in range(len(population[i]))} save_dict[f"gen{str(generation)}number{str(i)}"] = little_dict with open(save_dir / "evolve_population.yaml", "w") as outfile: yaml.dump(save_dict, outfile, default_flow_style=False) # Adaptive elite size elite_size = min_elite_size + int((max_elite_size - min_elite_size) * (generation / opt.evolve)) # Evaluate the fitness of each individual in the population fitness_scores = [] for individual in population: for key, value in zip(hyp_GA.keys(), individual): hyp_GA[key] = value hyp.update(hyp_GA) results = train(hyp.copy(), opt, device, callbacks) callbacks = Callbacks() # Write mutation results keys = ( "metrics/precision", "metrics/recall", "metrics/mAP_0.5", "metrics/mAP_0.5:0.95", "val/box_loss", "val/obj_loss", "val/cls_loss", ) print_mutation(keys, results, hyp.copy(), save_dir, opt.bucket) fitness_scores.append(results[2]) # Select the fittest individuals for reproduction using adaptive tournament selection selected_indices = [] for _ in range(pop_size - elite_size): # Adaptive tournament size tournament_size = max( max(2, tournament_size_min), int(min(tournament_size_max, pop_size) - (generation / (opt.evolve / 10))), ) # Perform tournament selection to choose the best individual tournament_indices = random.sample(range(pop_size), tournament_size) tournament_fitness = [fitness_scores[j] for j in tournament_indices] winner_index = tournament_indices[tournament_fitness.index(max(tournament_fitness))] selected_indices.append(winner_index) # Add the elite individuals to the selected indices elite_indices = [i for i in range(pop_size) if fitness_scores[i] in sorted(fitness_scores)[-elite_size:]] selected_indices.extend(elite_indices) # Create the next generation through crossover and mutation next_generation = [] for _ in range(pop_size): parent1_index = selected_indices[random.randint(0, pop_size - 1)] parent2_index = selected_indices[random.randint(0, pop_size - 1)] # Adaptive crossover rate crossover_rate = max( crossover_rate_min, min(crossover_rate_max, crossover_rate_max - (generation / opt.evolve)) ) if random.uniform(0, 1) < crossover_rate: crossover_point = random.randint(1, len(hyp_GA) - 1) child = population[parent1_index][:crossover_point] + population[parent2_index][crossover_point:] else: child = population[parent1_index] # Adaptive mutation rate mutation_rate = max( mutation_rate_min, min(mutation_rate_max, mutation_rate_max - (generation / opt.evolve)) ) for j in range(len(hyp_GA)): if random.uniform(0, 1) < mutation_rate: child[j] += random.uniform(-0.1, 0.1) child[j] = min(max(child[j], gene_ranges[j][0]), gene_ranges[j][1]) next_generation.append(child) # Replace the old population with the new generation population = next_generation # Print the best solution found best_index = fitness_scores.index(max(fitness_scores)) best_individual = population[best_index] print("Best solution found:", best_individual) # Plot results plot_evolve(evolve_csv) LOGGER.info( f'Hyperparameter evolution finished {opt.evolve} generations\n' f"Results saved to {colorstr('bold', save_dir)}\n" f'Usage example: $ python train.py --hyp {evolve_yaml}' ) def generate_individual(input_ranges, individual_length): """ Generate an individual with random hyperparameters within specified ranges. Args: input_ranges (list[tuple[float, float]]): List of tuples where each tuple contains the lower and upper bounds for the corresponding gene (hyperparameter). individual_length (int): The number of genes (hyperparameters) in the individual. Returns: list[float]: A list representing a generated individual with random gene values within the specified ranges. Example: ```python input_ranges = [(0.01, 0.1), (0.1, 1.0), (0.9, 2.0)] individual_length = 3 individual = generate_individual(input_ranges, individual_length) print(individual) # Output: [0.035, 0.678, 1.456] (example output) ``` Note: The individual returned will have a length equal to `individual_length`, with each gene value being a floating-point number within its specified range in `input_ranges`. """ individual = [] for i in range(individual_length): lower_bound, upper_bound = input_ranges[i] individual.append(random.uniform(lower_bound, upper_bound)) return individual def run(**kwargs): """ Execute YOLOv5 training with specified options, allowing optional overrides through keyword arguments. Args: weights (str, optional): Path to initial weights. Defaults to ROOT / 'yolov5s.pt'. cfg (str, optional): Path to model YAML configuration. Defaults to an empty string. data (str, optional): Path to dataset YAML configuration. Defaults to ROOT / 'data/coco128.yaml'. hyp (str, optional): Path to hyperparameters YAML configuration. Defaults to ROOT / 'data/hyps/hyp.scratch-low.yaml'. epochs (int, optional): Total number of training epochs. Defaults to 100. batch_size (int, optional): Total batch size for all GPUs. Use -1 for automatic batch size determination. Defaults to 16. imgsz (int, optional): Image size (pixels) for training and validation. Defaults to 640. rect (bool, optional): Use rectangular training. Defaults to False. resume (bool | str, optional): Resume most recent training with an optional path. Defaults to False. nosave (bool, optional): Only save the final checkpoint. Defaults to False. noval (bool, optional): Only validate at the final epoch. Defaults to False. noautoanchor (bool, optional): Disable AutoAnchor. Defaults to False. noplots (bool, optional): Do not save plot files. Defaults to False. evolve (int, optional): Evolve hyperparameters for a specified number of generations. Use 300 if provided without a value. evolve_population (str, optional): Directory for loading population during evolution. Defaults to ROOT / 'data/ hyps'. resume_evolve (str, optional): Resume hyperparameter evolution from the last generation. Defaults to None. bucket (str, optional): gsutil bucket for saving checkpoints. Defaults to an empty string. cache (str, optional): Cache image data in 'ram' or 'disk'. Defaults to None. image_weights (bool, optional): Use weighted image selection for training. Defaults to False. device (str, optional): CUDA device identifier, e.g., '0', '0,1,2,3', or 'cpu'. Defaults to an empty string. multi_scale (bool, optional): Use multi-scale training, varying image size by ±50%. Defaults to False. single_cls (bool, optional): Train with multi-class data as single-class. Defaults to False. optimizer (str, optional): Optimizer type, choices are ['SGD', 'Adam', 'AdamW']. Defaults to 'SGD'. sync_bn (bool, optional): Use synchronized BatchNorm, only available in DDP mode. Defaults to False. workers (int, optional): Maximum dataloader workers per rank in DDP mode. Defaults to 8. project (str, optional): Directory for saving training runs. Defaults to ROOT / 'runs/train'. name (str, optional): Name for saving the training run. Defaults to 'exp'. exist_ok (bool, optional): Allow existing project/name without incrementing. Defaults to False. quad (bool, optional): Use quad dataloader. Defaults to False. cos_lr (bool, optional): Use cosine learning rate scheduler. Defaults to False. label_smoothing (float, optional): Label smoothing epsilon value. Defaults to 0.0. patience (int, optional): Patience for early stopping, measured in epochs without improvement. Defaults to 100. freeze (list, optional): Layers to freeze, e.g., backbone=10, first 3 layers = [0, 1, 2]. Defaults to [0]. save_period (int, optional): Frequency in epochs to save checkpoints. Disabled if < 1. Defaults to -1. seed (int, optional): Global training random seed. Defaults to 0. local_rank (int, optional): Automatic DDP Multi-GPU argument. Do not modify. Defaults to -1. Returns: None: The function initiates YOLOv5 training or hyperparameter evolution based on the provided options. Examples: ```python import train train.run(data='coco128.yaml', imgsz=320, weights='yolov5m.pt') ``` Notes: - Models: https://github.com/ultralytics/yolov5/tree/master/models - Datasets: https://github.com/ultralytics/yolov5/tree/master/data - Tutorial: https://docs.ultralytics.com/yolov5/tutorials/train_custom_data """ opt = parse_opt(True) for k, v in kwargs.items(): setattr(opt, k, v) main(opt) return opt if __name__ == "__main__": opt = parse_opt() main(opt) 这是源代码
06-21
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳晓黑胡椒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值