Files

172 lines
6.9 KiB
TeX
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
\chapter{项目代码结构}
本研究核心代码已开源至 Gitea 仓库:\par
\url{https://lhy-git.liuhangyv.top/Serendipity/elderly-heat-warning}\par
项目采用模块化结构,总规模约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}