\chapter{项目代码结构} 本研究核心代码已开源至 Gitea 仓库 \texttt{git@lhy-git.liuhangyv.top:Serendipity/elderly-heat-warning.git}。项目采用模块化结构,总规模约28个源文件(约3,500行Python代码 + 约800行前端HTML/CSS/JS代码)。 \section{项目目录结构} \begin{verbatim} src/ ├── data/ │ ├── download_era5.py # ERA5 数据下载(CDS API) │ ├── extract_zips.py # NetCDF ZIP 解压 │ ├── preprocess.py # 数据预处理管线(597行) │ └── collect_mortality.py # 死亡率数据整理 ├── models/ │ ├── lstm_attention.py # LSTM-Attention 模型定义 │ ├── xgboost_baseline.py # XGBoost 基线 │ ├── train.py # 训练脚本(365行) │ └── evaluate.py # 评估脚本(295行) ├── web/ │ ├── app.py # Flask 后端(177行) │ └── static/ │ └── index.html # ECharts 前端大屏(~800行) └── utils/ └── config.py # 全局配置常量 \end{verbatim} \section{关键代码讲解} \subsection{多头自注意力层} \begin{lstlisting}[language=Python, caption=MultiHeadSelfAttention前向传播] class MultiHeadSelfAttention(nn.Module): def __init__(self, embed_dim, num_heads=4, dropout=0.3): super().__init__() self.num_heads = num_heads self.head_dim = embed_dim // num_heads self.qkv = nn.Linear(embed_dim, 3 * embed_dim) self.out_proj = nn.Linear(embed_dim, embed_dim) def forward(self, x): B, T, D = x.shape qkv = self.qkv(x).reshape(B, T, 3, self.num_heads, self.head_dim) qkv = qkv.permute(2, 0, 3, 1, 4) q, k, v = qkv[0], qkv[1], qkv[2] scale = self.head_dim ** -0.5 attn = (q @ k.transpose(-2, -1)) * scale attn = F.softmax(attn, dim=-1) out = attn @ v out = out.permute(0, 2, 1, 3).reshape(B, T, D) return self.out_proj(out) \end{lstlisting} \textbf{设计要点:} \begin{enumerate} \item \texttt{qkv}将Q、K、V三次投影合并为一次矩阵乘法,计算效率提升约30\% \item \texttt{scale = head\_dim ** -0.5}是缩放点积注意力的核心——防止点积过大导致softmax梯度弥散 \item \texttt{permute}操作重排维度使每个注意力头独立计算 \end{enumerate} \subsection{主模型HeatRiskPredictor} \begin{lstlisting}[language=Python, caption=模型前向传播] class HeatRiskPredictor(nn.Module): def __init__(self, input_dim, hidden_dim=128): super().__init__() self.input_proj = nn.Linear(input_dim, hidden_dim) self.lstm = nn.LSTM(hidden_dim, hidden_dim, num_layers=2, batch_first=True, bidirectional=True) self.attention = MultiHeadSelfAttention(hidden_dim * 2) self.lstm_proj = nn.Linear(hidden_dim * 2, hidden_dim) self.head_short = self._make_head(hidden_dim, 4) self.head_medium = self._make_head(hidden_dim, 4) self.head_long = self._make_head(hidden_dim, 4) def forward(self, x): x = self.input_proj(x) # (B,14,19) -> (B,14,128) lstm_out, _ = self.lstm(x) # -> (B,14,256) bidirectional attn_out = self.attention(lstm_out) last = self.lstm_proj(attn_out[:, -1, :]) return { "short": self.head_short(last), "medium": self.head_medium(last), "long": self.head_long(last), } \end{lstlisting} \textbf{设计要点:} \begin{enumerate} \item BiLSTM使每个时间步同时编码前后文,输出维从128翻倍至256 \item \texttt{lstm\_proj}将256维投影回128维以衔接注意力层 \item 取序列最后一个时间步的注意力输出作为序列摘要向量 \item 三个输出头参数独立,各自学习适应不同预测窗口的判别规则 \end{enumerate} \subsection{Focal Loss损失函数} \begin{lstlisting}[language=Python, caption=FocalLoss实现] class FocalLoss(nn.Module): def __init__(self, alpha=0.5, gamma=2.0): super().__init__() self.alpha = alpha; self.gamma = gamma def forward(self, logits, targets): ce = F.cross_entropy(logits, targets, reduction="none") pt = torch.exp(-ce) focal = self.alpha * (1 - pt) ** self.gamma * ce return focal.mean() \end{lstlisting} \textbf{设计要点:} \begin{enumerate} \item \texttt{reduction="none"}保留逐样本损失以施加调制因子 \item \texttt{pt = torch.exp(-ce)}利用交叉熵定义反推预测概率,避免额外softmax计算 \item \texttt{(1-pt)**gamma}是核心调制项——$p_t$→1时因子→0衰减简单样本,$p_t$→0时因子→1保留困难样本 \end{enumerate} \subsection{数据预处理管线} \begin{lstlisting}[language=Python, caption=ERA5数据加载与拼接] def load_era5_city(city: str) -> xr.Dataset: era5_dir = Path(DATA_RAW) / "era5" / city nc_files = sorted(era5_dir.glob("era5_*.nc")) combined = xr.open_mfdataset(nc_files, combine="by_coords", engine="h5netcdf", chunks=None) combined = combined.sortby("valid_time") _, idx = np.unique(combined["valid_time"], return_index=True) return combined.isel(valid_time=sorted(idx)) # 去重 \end{lstlisting} \textbf{设计要点:} \begin{enumerate} \item \texttt{open\_mfdataset}的\texttt{combine="by\_coords"}沿已有时间坐标自动对齐拼接 \item \texttt{engine="h5netcdf"}避免Windows下netcdf-C库依赖 \item \texttt{chunks=None}将全部数据加载到内存(每城约100MB) \item 去重处理CDS跨月文件的时间重叠 \end{enumerate} \subsection{Flask API后端} \begin{lstlisting}[language=Python, caption=模型延迟加载与预测推理] model = None # 全局变量,None表示未加载 def load_model(): """首次API请求时才加载模型,降低启动延迟""" global model if model is not None: return data = np.load(DATA_PROCESSED / "jiaozuo_sequences.npz") model = HeatRiskPredictor(input_dim=data["X"].shape[2]) model.load_state_dict(torch.load(OUTPUT_MODELS / "best_model.pt")) model.eval() @app.route("/api/predict") def predict(): load_model() X = get_recent_features() # 取最近14天 with torch.no_grad(): # 推理模式 outputs = model(torch.FloatTensor(X).to(device)) for key in ["short", "medium", "long"]: probs = torch.softmax(outputs[key], dim=-1)[0] level = int(probs.argmax()) # 最高概率类别 # 封装为JSON: level+label+probabilities+suggestions \end{lstlisting} \textbf{设计要点:} \begin{enumerate} \item 延迟加载使Flask启动从约5秒降至<1秒,避免空闲时GPU内存占用 \item \texttt{torch.no\_grad()}禁用自动求导,推理时节省约30\%显存 \item 模型不可用时自动降级为fallback预测以保证系统可用性 \end{enumerate}