LSTM原理解析
解决长期依赖:通过细胞状态和门控机制,能够记住长期信息梯度稳定:加法操作取代乘法操作,缓解梯度消失问题选择性记忆:智能地决定记住什么、忘记什么、输出什么广泛应用:在时间序列预测、自然语言处理、语音识别等领域表现优异LSTM的门控机制是深度学习中的一大突破,它让神经网络真正具备了"长期记忆"的能力。虽然现在Transformer等新架构在某些任务上表现更好,但理解LSTM的原理仍然非常重要,它是理解
LSTM原理解析
LSTM原理详解:从RNN困境到门控机制的革命
有了神经网络、反向传播和损失函数的基础,
写过线性回归和简单神经网络,
这是理解LSTM的起点。
接下来详细讲解LSTM的原理。
1. 为什么需要LSTM?—— RNN的困境
1.1 RNN的基本思想
循环神经网络(RNN)是为了处理序列数据而设计的。它的核心思想是引入"记忆"——通过循环连接,将之前步骤的信息传递到后续步骤。

1.2 RNN的致命缺陷:梯度消失/爆炸问题
在反向传播过程中,梯度需要沿着时间步反向传播。对于长序列,梯度会指数级地缩小(消失)或增大(爆炸)。
数学解释:
RNN的梯度包含连乘项: ∂ L ∂ W = ∑ t = 1 T ∂ L ∂ h T ∂ h T ∂ h t ∂ h t ∂ W \frac{\partial L}{\partial W} = \sum_{t=1}^T \frac{\partial L}{\partial h_T} \frac{\partial h_T}{\partial h_t} \frac{\partial h_t}{\partial W} ∂W∂L=∑t=1T∂hT∂L∂ht∂hT∂W∂ht
其中 ∂ h T ∂ h t = ∏ k = t T − 1 ∂ h k + 1 ∂ h k \frac{\partial h_T}{\partial h_t} = \prod_{k=t}^{T-1} \frac{\partial h_{k+1}}{\partial h_k} ∂ht∂hT=∏k=tT−1∂hk∂hk+1,当T很大时,这个连乘积会导致梯度消失或爆炸。
实际后果:RNN难以学习长期依赖关系,只能记住短期模式。
2. LSTM的革命性解决方案
LSTM(Long Short-Term Memory)由Sepp Hochreiter和Jürgen Schmidhuber于1997年提出,通过巧妙的"门控机制"解决了长期依赖问题。
2.1 LSTM的核心创新:细胞状态和门控机制
LSTM的关键创新是引入了:
- 细胞状态(Cell State):像传送带一样贯穿整个网络,信息可以几乎不变地流动
- 门控机制(Gates):选择性控制信息的流动

2.2 LSTM的三个门
LSTM通过三个门来控制信息流:
2.2.1 遗忘门(Forget Gate)
作用:决定从细胞状态中丢弃什么信息
数学表达:
f t = σ ( W f ⋅ [ h t − 1 , x t ] + b f ) f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) ft=σ(Wf⋅[ht−1,xt]+bf)
- σ \sigma σ:sigmoid函数,输出0到1之间的值
- W f W_f Wf:权重矩阵
- h t − 1 h_{t-1} ht−1:上一个时间步的隐藏状态
- x t x_t xt:当前输入
- b f b_f bf:偏置项
理解:输出接近0表示"完全忘记",接近1表示"完全保留"
2.2.2 输入门(Input Gate)
作用:决定将什么新信息存储到细胞状态中
数学表达:
i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) it=σ(Wi⋅[ht−1,xt]+bi) # 决定更新哪些值
C ~ t = tanh ( W C ⋅ [ h t − 1 , x t ] + b C ) \tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) C~t=tanh(WC⋅[ht−1,xt]+bC) # 候选值向量
理解:输入门控制哪些新信息是重要的,需要加入到细胞状态中
2.2.3 输出门(Output Gate)
作用:基于细胞状态,决定输出什么值
数学表达:
o t = σ ( W o ⋅ [ h t − 1 , x t ] + b o ) o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) ot=σ(Wo⋅[ht−1,xt]+bo)
h t = o t ∗ tanh ( C t ) h_t = o_t * \tanh(C_t) ht=ot∗tanh(Ct)
理解:输出门决定从细胞状态中提取什么信息作为当前时间步的输出
2.3 细胞状态的更新
细胞状态的更新是LSTM的核心:
C t = f t ∗ C t − 1 + i t ∗ C ~ t C_t = f_t * C_{t-1} + i_t * \tilde{C}_t Ct=ft∗Ct−1+it∗C~t
解读:
- f t ∗ C t − 1 f_t * C_{t-1} ft∗Ct−1:遗忘门控制旧信息的保留
- i t ∗ C ~ t i_t * \tilde{C}_t it∗C~t:输入门控制新信息的加入
这个简单的加法操作是解决梯度消失问题的关键!相比RNN中的连乘,加法操作使得梯度可以更稳定地流动。
3. LSTM的工作流程:一步步拆解
让我们通过一个具体例子理解LSTM如何处理一个时间步的数据:
步骤1:遗忘门决定忘记什么
假设我们正在处理一句话:“我今天去了公园,那里有很多…”,下一个词应该是"树"或"花"。
遗忘门可能会决定忘记"我今天"这样的早期信息,因为它们对预测下一个词不太重要。
步骤2:输入门决定记住什么
输入门会识别"公园"和"很多"是重要信息,需要加入到细胞状态中。
步骤3:更新细胞状态
细胞状态现在包含了"公园"和"很多"这些重要信息,同时忘记了不太相关的旧信息。
步骤4:输出门决定输出什么
基于当前的细胞状态,输出门决定输出与"公园"相关的词汇,最终预测下一个词可能是"树"。
4. 为什么LSTM能解决梯度消失问题?
4.1 梯度流动的改进
在标准RNN中: ∂ h t ∂ h t − 1 = W ⋅ diag ( σ ′ ( . . . ) ) \frac{\partial h_t}{\partial h_{t-1}} = W \cdot \text{diag}(\sigma'(...)) ∂ht−1∂ht=W⋅diag(σ′(...))
在LSTM中: ∂ C t ∂ C t − 1 = f t + 其他项 \frac{\partial C_t}{\partial C_{t-1}} = f_t + \text{其他项} ∂Ct−1∂Ct=ft+其他项
关键区别:LSTM中的梯度流动主要通过加法而不是乘法,避免了指数级缩小。
4.2 门控机制的保护
门控机制允许LSTM选择性地记住和忘记信息,保护梯度不被不适合的权重更新所破坏。
5. LSTM的变体和发展
5.1 Peephole连接
让门不仅看到隐藏状态和输入,还能看到细胞状态:
f t = σ ( W f ⋅ [ C t − 1 , h t − 1 , x t ] + b f ) f_t = \sigma(W_f \cdot [C_{t-1}, h_{t-1}, x_t] + b_f) ft=σ(Wf⋅[Ct−1,ht−1,xt]+bf)
5.2 GRU(Gated Recurrent Unit)
GRU是LSTM的简化版本,将遗忘门和输入门合并为更新门,并合并了细胞状态和隐藏状态。
5.3 双向LSTM
同时从两个方向处理序列,既能看过去也能看未来。
6. LSTM的PyTorch实现对应关系
在你提供的代码中:
class TrafficLSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(TrafficLSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态和细胞状态
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
# LSTM前向传播
out, _ = self.lstm(x, (h0, c0))
# 取最后一个时间步的输出
out = out[:, -1, :]
# 全连接层
out = self.fc(out)
return out
对应关系:
nn.LSTM封装了所有的门控机制和细胞状态更新h0,c0分别是初始隐藏状态和细胞状态out包含所有时间步的隐藏状态- 我们只取最后一个时间步的输出用于预测
7. 直观理解:LSTM就像一个有选择的记忆系统
想象LSTM是一个聪明的秘书:
- 遗忘门:秘书决定哪些旧文件可以归档(忘记)
- 输入门:秘书决定哪些新文件重要,需要放在桌上(记住)
- 细胞状态:秘书的桌面,上面放着当前重要的文件
- 输出门:秘书根据桌上的文件,决定给你什么信息(输出)
这种有选择的记忆机制使得LSTM能够既记住长期的重要信息,又忘记无关的细节。
8. 总结:LSTM的核心优势
- 解决长期依赖:通过细胞状态和门控机制,能够记住长期信息
- 梯度稳定:加法操作取代乘法操作,缓解梯度消失问题
- 选择性记忆:智能地决定记住什么、忘记什么、输出什么
- 广泛应用:在时间序列预测、自然语言处理、语音识别等领域表现优异
LSTM的门控机制是深度学习中的一大突破,它让神经网络真正具备了"长期记忆"的能力。虽然现在Transformer等新架构在某些任务上表现更好,但理解LSTM的原理仍然非常重要,它是理解现代序列模型的基础。
LSTM时间序列预测教程:从零开始理解车流量预测
逐步理解这个LSTM车流量预测代码。用简单易懂的方式解释每个部分,帮助掌握LSTM的核心概念。
1. 代码整体结构
这个代码实现了使用LSTM神经网络预测小时级别车流量的功能。主要步骤包括:
- 数据准备和预处理
- 构建LSTM模型
- 训练模型
- 评估模型性能
- 可视化结果
- 进行未来预测
2. 数据准备和预处理详解
2.1 导入库
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error
torch: PyTorch深度学习框架nn: 神经网络模块optim: 优化器模块numpy: 科学计算库matplotlib: 绘图库MinMaxScaler: 数据归一化工具- 评估指标:均方误差(MSE)、平均绝对误差(MAE)
2.2 设置中文显示和随机种子
# 设置兼容的字体,解决中文显示问题
try:
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
print("使用系统字体设置")
except:
try:
print("使用默认字体,中文可能显示为方块")
except:
print("使用默认字体,中文可能显示为方块")
# 设置随机种子以确保结果可重现
torch.manual_seed(42)
np.random.seed(42)
- 设置字体是为了让图表中的中文正常显示
- 设置随机种子确保每次运行结果一致,便于调试和比较
2.3 选择设备
# 检查是否有可用的GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
- 如果有GPU,使用GPU加速计算;否则使用CPU
- 深度学习模型在GPU上训练速度更快
2.4 数据预处理
# 1. 准备数据
# 您提供的车流数据
data = [424, 405, 441, 520, 311, 256, 300, 186, 131, 129, 74, 39, 16, 12, 11, 57, 182, 315, 354, 363, 504, 341, 411, 506,
...] # 这里是你提供的车流数据
# 转换为numpy数组
data = np.array(data, dtype=np.float32)
# 数据归一化
scaler = MinMaxScaler(feature_range=(0, 1))
data = scaler.fit_transform(data.reshape(-1, 1)).flatten()
- 将列表数据转换为numpy数组,便于处理
- 使用MinMaxScaler将数据归一化到0-1之间
- 归一化可以加速模型收敛,提高训练效果
- 注意:scaler对象需要保存,以便后续反归一化
2.5 创建时间序列数据集
# 创建时间序列数据集
def create_dataset(data, time_step=24):
X, y = [], []
for i in range(len(data) - time_step):
X.append(data[i:(i + time_step)])
y.append(data[i + time_step])
return np.array(X), np.array(y)
# 设置时间步长(使用24小时作为输入来预测下一小时)
time_step = 24
X, y = create_dataset(data, time_step)
- 这个函数将时间序列数据转换为监督学习格式
- 使用前24个小时的数据预测第25个小时的数据
- 例如:用第1-24小时预测第25小时,用第2-25小时预测第26小时,以此类推
2.6 划分训练集和测试集
# 划分训练集和测试集 (80% 训练, 20% 测试)
train_size = int(len(X) * 0.8)
test_size = len(X) - train_size
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# 转换为PyTorch张量
X_train = torch.FloatTensor(X_train).unsqueeze(-1) # 添加特征维度 (batch, seq_len, input_size)
y_train = torch.FloatTensor(y_train)
X_test = torch.FloatTensor(X_test).unsqueeze(-1)
y_test = torch.FloatTensor(y_test)
print(f"训练数据形状: {X_train.shape}")
print(f"测试数据形状: {X_test.shape}")
- 将数据分为训练集(80%)和测试集(20%)
- 转换为PyTorch张量,这是PyTorch处理数据的基本格式
unsqueeze(-1)添加一个维度,因为LSTM期望输入形状为(batch_size, seq_len, input_size)- 打印形状帮助理解数据结构
3. LSTM模型详解
3.1 LSTM模型定义
# 2. 定义LSTM模型
class TrafficLSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(TrafficLSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态和细胞状态
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
# 前向传播LSTM
out, _ = self.lstm(x, (h0, c0)) # out: (batch_size, seq_length, hidden_size)
# 取最后一个时间步的输出
out = out[:, -1, :]
# 全连接层
out = self.fc(out)
return out
nn.Module: PyTorch中所有神经网络的基类__init__方法定义网络层:nn.LSTM: LSTM层,参数包括输入大小、隐藏层大小、层数batch_first=True: 输入数据的第一个维度是batch大小nn.Linear: 全连接层,将LSTM输出映射到预测值
forward方法定义前向传播过程:- 初始化隐藏状态(h0)和细胞状态(c0)
- LSTM处理输入序列,返回所有时间步的输出
- 只取最后一个时间步的输出(因为我们预测下一个时间点)
- 通过全连接层得到最终预测
3.2 LSTM超参数和初始化
# 超参数
input_size = 1 # 每个时间步的特征数
hidden_size = 50 # LSTM隐藏单元数
num_layers = 2 # LSTM层数
output_size = 1 # 输出大小(预测值)
learning_rate = 0.001
num_epochs = 100
batch_size = 16
# 初始化模型
model = TrafficLSTM(input_size, hidden_size, num_layers, output_size).to(device)
# 损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
-
超参数:
input_size: 每个时间步的特征数量(这里只有车流量一个特征)hidden_size: LSTM隐藏单元数量(可以理解为记忆容量)num_layers: LSTM层数(更深的网络可以学习更复杂的模式)output_size: 输出大小(预测一个值)learning_rate: 学习率(控制参数更新步长)num_epochs: 训练轮数batch_size: 批大小(每次训练使用的样本数)
-
损失函数:均方误差(MSE),适合回归问题
-
优化器:Adam,一种常用的自适应学习率优化算法
3.3 数据加载器
# 创建数据加载器
train_dataset = torch.utils.data.TensorDataset(X_train, y_train)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
TensorDataset: 将数据和标签组合成数据集DataLoader: 数据加载器,提供批量加载和洗牌功能shuffle=True: 每个epoch打乱数据顺序,提高训练效果
4. 训练过程详解
# 3. 训练模型
train_losses = []
test_losses = []
for epoch in range(num_epochs):
model.train() # 设置为训练模式
epoch_loss = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
# 前向传播
outputs = model(batch_x)
loss = criterion(outputs.squeeze(), batch_y)
# 反向传播和优化
optimizer.zero_grad() # 清零梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
epoch_loss += loss.item() # 累加损失
# 计算平均训练损失
avg_loss = epoch_loss / len(train_loader)
train_losses.append(avg_loss)
# 在每个epoch后评估模型
model.eval() # 设置为评估模式
with torch.no_grad(): # 不计算梯度,节省内存
test_outputs = model(X_test.to(device))
test_loss = criterion(test_outputs.squeeze(), y_test.to(device))
test_losses.append(test_loss.item())
if (epoch+1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], 训练损失: {avg_loss:.6f}, 测试损失: {test_loss.item():.6f}')
-
训练循环:
model.train(): 设置模型为训练模式(启用Dropout和BatchNorm)- 遍历每个批次的数据
- 将数据移动到设备(GPU/CPU)
- 前向传播计算预测值
- 计算损失
optimizer.zero_grad(): 清零梯度(避免梯度累积)loss.backward(): 反向传播计算梯度optimizer.step(): 更新模型参数- 记录损失值
-
评估模式:
model.eval(): 设置模型为评估模式(禁用Dropout和BatchNorm)with torch.no_grad(): 不计算梯度,节省内存和计算资源- 计算测试集上的损失
5. 模型评估
# 4. 评估模型
model.eval()
with torch.no_grad():
# 对测试集进行预测
test_outputs = model(X_test.to(device))
# 反归一化预测值和真实值
test_predictions = scaler.inverse_transform(test_outputs.cpu().numpy())
test_actual = scaler.inverse_transform(y_test.numpy().reshape(-1, 1))
# 计算评估指标
mse = mean_squared_error(test_actual, test_predictions)
mae = mean_absolute_error(test_actual, test_predictions)
rmse = np.sqrt(mse)
print(f'\n模型评估指标:')
print(f'均方误差 (MSE): {mse:.2f}')
print(f'平均绝对误差 (MAE): {mae:.2f}')
print(f'均方根误差 (RMSE): {rmse:.2f}')
- 反归一化:将预测值从0-1范围转换回原始数据范围
- 评估指标:
- MSE: 均方误差,衡量预测值与真实值之间的平均平方差
- MAE: 平均绝对误差,衡量预测值与真实值之间的平均绝对差
- RMSE: 均方根误差,MSE的平方根,与原始数据单位一致
6. 可视化结果
# 5. 可视化结果
plt.figure(figsize=(15, 10))
# 绘制训练和测试损失
plt.subplot(2, 2, 1)
plt.plot(train_losses, label='训练损失')
plt.plot(test_losses, label='测试损失')
plt.title('训练和测试损失')
plt.xlabel('Epoch')
plt.ylabel('损失')
plt.legend()
# 绘制预测值与真实值对比
plt.subplot(2, 2, 2)
plt.plot(test_actual, label='真实值', color='blue', alpha=0.7)
plt.plot(test_predictions, label='预测值', color='red', alpha=0.7)
plt.title('测试集预测结果')
plt.xlabel('时间')
plt.ylabel('车流量')
plt.legend()
# 绘制预测值与真实值的散点图
plt.subplot(2, 2, 3)
plt.scatter(test_actual, test_predictions, alpha=0.5)
plt.plot([test_actual.min(), test_actual.max()], [test_actual.min(), test_actual.max()], 'r--')
plt.xlabel('真实值')
plt.ylabel('预测值')
plt.title('预测值与真实值散点图')
# 绘制误差分布
plt.subplot(2, 2, 4)
errors = test_predictions.flatten() - test_actual.flatten()
plt.hist(errors, bins=30)
plt.xlabel('预测误差')
plt.ylabel('频次')
plt.title('预测误差分布')
plt.tight_layout()
plt.show()
- 四个子图从不同角度展示模型性能:
- 训练和测试损失曲线:检查是否过拟合或欠拟合
- 预测值与真实值对比:直观查看预测效果
- 散点图:检查预测值与真实值的线性关系
- 误差分布:检查误差是否符合正态分布
7. 未来预测
# 6. 进行未来预测
# 使用最后24小时的数据预测未来
last_24_hours = data[-time_step:]
future_predictions = []
model.eval()
with torch.no_grad():
current_batch = last_24_hours.reshape(1, time_step, 1)
current_batch = torch.FloatTensor(current_batch).to(device)
# 预测未来24小时
for i in range(24):
pred = model(current_batch)
future_predictions.append(pred.item())
# 更新输入批次,添加新预测并移除最早的值
current_batch = torch.cat((current_batch[:, 1:, :], pred.unsqueeze(0).unsqueeze(-1)), dim=1)
# 反归一化未来预测
future_predictions = scaler.inverse_transform(np.array(future_predictions).reshape(-1, 1))
# 绘制未来预测
plt.figure(figsize=(12, 6))
# 绘制最后48小时的实际数据
last_48_hours_actual = scaler.inverse_transform(data[-48:].reshape(-1, 1))
plt.plot(range(-47, 1), last_48_hours_actual, label='历史数据', color='blue')
# 绘制未来24小时的预测
plt.plot(range(1, 25), future_predictions, label='未来预测', color='red')
plt.axvline(x=0, color='green', linestyle='--', label='当前时间')
plt.title('未来24小时车流量预测')
plt.xlabel('时间 (小时)')
plt.ylabel('车流量')
plt.legend()
plt.show()
- 使用最后24小时的数据预测未来24小时
- 循环预测:每次预测下一个时间点,然后将预测值作为输入的一部分继续预测
- 可视化历史数据和未来预测
8. LSTM核心概念
8.1 LSTM是什么?
LSTM(Long Short-Term Memory,长短期记忆网络)是一种特殊的循环神经网络(RNN),专门设计用来解决长期依赖问题。相比普通RNN,LSTM能够更好地记住长期信息。
8.2 LSTM的核心组件
LSTM通过三个"门"结构来控制信息流:
- 遗忘门:决定从细胞状态中丢弃什么信息
- 输入门:决定将什么新信息存入细胞状态
- 输出门:决定基于细胞状态输出什么值
8.3 为什么使用LSTM处理时间序列?
- 记忆能力:LSTM可以记住长期模式,适合处理具有周期性的时间序列数据
- 处理变长序列:可以处理不同长度的时间序列
- 捕捉时序依赖:能够捕捉数据中的时间依赖关系
9. 初学者学习建议
- 理解基础概念:先理解神经网络、反向传播、损失函数等基础概念
- 从简单模型开始:先尝试线性回归、简单神经网络,再学习LSTM
- 动手实践:修改代码参数,观察不同超参数对结果的影响
- 可视化分析:通过可视化理解模型的行为和性能
- 逐步深入:理解LSTM的门机制和记忆原理
10. 常见问题解答
Q: 为什么要归一化数据?
A: 归一化可以加速模型收敛,提高训练稳定性。不同特征尺度差异大时,归一化尤其重要。
Q: 时间步长应该设为多少?
A: 时间步长取决于数据的周期性。对于日周期数据,24是常见选择;对于周周期数据,168(24×7)可能更合适。
Q: 如何选择隐藏层大小和层数?
A: 通常通过实验选择。更复杂的模式需要更大的隐藏层和更多层数,但也更容易过拟合。
Q: 为什么测试损失比训练损失高?
A: 这是正常现象,表明模型在训练集上表现更好。如果差距过大,可能出现了过拟合。
希望这个详细的解释能帮助你理解LSTM和时间序列预测。如果有任何问题,欢迎继续提问!
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)