基于华为昇腾CANN的鸢尾花分类实战:从训练到端侧部署
鸢尾花分类是机器学习领域的"Hello World",数据集包含3种鸢尾花(Setosa、Versicolour、Virginica)的4个特征:花萼长度(sepal length)花萼宽度(sepal width)花瓣长度(petal length)花瓣宽度(petal width)我们的目标是构建一个深度学习模型,能够根据这4个特征准确分类鸢尾花类型。数据准备:加载、探索和预处理经典的鸢尾花数
手把手教你使用华为昇腾CANN技术栈完成经典的鸢尾花分类任务,体验完整的AI应用开发流程基于华为昇腾CANN的鸢尾
前言
随着人工智能产业的快速发展,端侧 AI 部署已成为落地 AI 应用的核心环节。华为昇腾 CANN(Compute Architecture for Neural Networks)作为昇腾 AI 芯片的异构计算架构,能够充分释放昇腾芯片的算力潜能,为 AI 模型的训练与端侧部署提供高效支撑。
核心理论基础
昇腾 CANN 架构解析
昇腾 CANN 是面向昇腾系列 AI 芯片的异构计算架构,向下对接昇腾芯片硬件,向上为应用层提供统一的编程接口,核心优势体现在:
- 异构计算调度:融合 CPU、昇腾 NPU(神经网络处理单元)的算力,自动完成任务拆分与调度,最大化算力利用率;
- 算子库优化:内置丰富的高性能 AI 算子,支持主流深度学习框架(TensorFlow/PyTorch)的算子适配,减少开发者底层优化成本;
- 端边云协同:提供统一的模型转换与部署工具链,支持训练好的模型快速适配昇腾端侧设备(如昇腾 310B、昇腾 200DK)。
昇腾 CANN 的核心组件及分工如下:
| 组件名称 | 核心功能 |
| AscendCL | 昇腾芯片的统一编程接口,支持模型推理、算子开发等 |
| ATC(Ascend Tensor Compiler) | 模型转换工具,将 ONNX/TensorFlow 等模型转换为昇腾芯片支持的 OM 模型 |
| MindStudio | 昇腾 AI 开发一站式 IDE,支持代码编写、调试、部署 |
1.项目概述与环境准备
1.1概述
在机器学习的世界里,有一个数据集被广泛提及,它就是鸢尾花数据集(Iris Dataset)。对于许多初学者来说,它是通往数据科学大门的“Hello World”。鸢尾花数据集是机器学习领域中一个非常著名且经典的数据集,由英国统计学家和生物学家罗纳德·费舍尔(Ronald Fisher)在1936年首次提出。这个数据集包含150个鸢尾花样本,每个样本有四个特征,分别是花萼(sepal)长度、花萼宽度、花瓣(petal)长度和花瓣宽度,以及它们所属的鸢尾花品种。150个鸢尾花样本包括三种不同的品种:山鸢尾(Setosa)、变色鸢尾(Versicolor)、维吉尼亚鸢尾(Virginica),每种品种各有50个样本。鸢尾花数据,数值数据单位为厘米(cm)。以下是三种鸢尾花图片。
为什么选鸢尾花数据?
鸢尾花数据集的特点:数据集的特征数量较少,只有4个特征,且这些特征都是数值型的,不用考虑涉及非数值型数据怎么处理问题,易于理解和处理,对入门学友好。且数据集中的样本数量适中,且数据没有缺失值和异常值,数据质量较高。
在机器学习中的应用价值:鸢尾花数据可以应用于分类,聚类(去掉标签数据),降维,数据预处理。机器学习入门:scikit-learn库的使用
1.2鸢尾花分类问题简介
鸢尾花分类是机器学习领域的"Hello World",数据集包含3种鸢尾花(Setosa、Versicolour、Virginica)的4个特征:
-
花萼长度(sepal length)
-
花萼宽度(sepal width)
-
花瓣长度(petal length)
-
花瓣宽度(petal width)

任务目标:基于 4 个特征实现鸢尾花类别的精准分类。
1.3 环境准备
昇腾 CANN 安装
安装依赖:
sudo apt-get install gcc g++ make cmake;
执行安装脚本:
./Ascend-cann-toolkit_6.0.RC1_linux-x86_64.run --install;
配置环境变量:
export ASCEND_HOME=/usr/local/Ascend
export PATH=$ASCEND_HOME/atc/ccec_compiler/bin:$ASCEND_HOME/atc/bin:$PATH
export LD_LIBRARY_PATH=$ASCEND_HOME/atc/lib64:$LD_LIBRARY_PATH
依赖库安装
pip install torch==1.12.0 torchvision==0.13.0 scikit-learn pandas numpy onnx==1.12.0
1.4 分类模型选型
鸢尾花分类属于简单的多分类任务,本文选择经典的全连接神经网络(MLP)作为基础模型,原因:
- 结构简单,聚焦 CANN 核心流程而非模型调优;
- 算子数量少,易观察 CANN 的算子优化效果;
- 无复杂卷积操作,降低虚拟环境验证难度。
2. 数据准备与预处理
2.1 数据集加载与探索
首先加载鸢尾花数据集,并完成数据划分、标准化等预处理:
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
import matplotlib.pyplot as plt
class IrisDataProcessor:
def __init__(self):
self.scaler = StandardScaler()
self.label_encoder = LabelEncoder()
def load_and_explore_data(self):
"""加载并探索鸢尾花数据集"""
# 加载数据
iris = load_iris()
X = iris.data # 特征数据
y = iris.target # 标签数据
feature_names = iris.feature_names
target_names = iris.target_names
print("数据集基本信息:")
print(f"特征数量: {X.shape[1]}")
print(f"样本数量: {X.shape[0]}")
print(f"特征名称: {feature_names}")
print(f"类别名称: {target_names}")
print(f"类别分布: {np.bincount(y)}")
return X, y, feature_names, target_names
def preprocess_data(self, X, y, test_size=0.2, random_state=42):
"""数据预处理"""
# 标准化特征
X_scaled = self.scaler.fit_transform(X)
# 编码标签(虽然已经是数值,这里为了通用性)
y_encoded = self.label_encoder.fit_transform(y)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y_encoded, test_size=test_size,
random_state=random_state, stratify=y
)
print(f"训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")
return X_train, X_test, y_train, y_test
def visualize_data(self, X, y, feature_names, target_names):
"""数据可视化"""
plt.figure(figsize=(15, 10))
# 特征分布直方图
for i in range(4):
plt.subplot(2, 3, i+1)
for class_id in range(3):
plt.hist(X[y == class_id, i], alpha=0.7, label=target_names[class_id])
plt.xlabel(feature_names[i])
plt.ylabel('Frequency')
plt.legend()
# 特征相关性热力图
plt.subplot(2, 3, 5)
df = pd.DataFrame(X, columns=feature_names)
correlation_matrix = df.corr()
plt.imshow(correlation_matrix, cmap='coolwarm', interpolation='nearest')
plt.colorbar()
plt.xticks(range(len(feature_names)), feature_names, rotation=45)
plt.yticks(range(len(feature_names)), feature_names)
plt.title('Feature Correlation')
plt.tight_layout()
plt.show()
# 使用示例
if __name__ == "__main__":
processor = IrisDataProcessor()
X, y, feature_names, target_names = processor.load_and_explore_data()
X_train, X_test, y_train, y_test = processor.preprocess_data(X, y)
processor.visualize_data(X, y, feature_names, target_names)
3. PyTorch模型设计与训练
3.1 神经网络模型设计
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import torch.nn.functional as F
class IrisNet(nn.Module):
"""简单的全连接神经网络用于鸢尾花分类"""
def __init__(self, input_size=4, hidden_size=16, num_classes=3):
super(IrisNet, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size)
self.fc3 = nn.Linear(hidden_size, num_classes)
self.dropout = nn.Dropout(0.2)
def forward(self, x):
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
class ModelTrainer:
def __init__(self, model, learning_rate=0.01):
self.model = model
self.criterion = nn.CrossEntropyLoss()
self.optimizer = optim.Adam(model.parameters(), lr=learning_rate)
def train(self, train_loader, val_loader, epochs=100):
"""训练模型"""
train_losses = []
val_accuracies = []
for epoch in range(epochs):
# 训练阶段
self.model.train()
total_loss = 0
for batch_x, batch_y in train_loader:
self.optimizer.zero_grad()
outputs = self.model(batch_x)
loss = self.criterion(outputs, batch_y)
loss.backward()
self.optimizer.step()
total_loss += loss.item()
# 验证阶段
self.model.eval()
correct = 0
total = 0
with torch.no_grad():
for batch_x, batch_y in val_loader:
outputs = self.model(batch_x)
_, predicted = torch.max(outputs.data, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
accuracy = 100 * correct / total
avg_loss = total_loss / len(train_loader)
train_losses.append(avg_loss)
val_accuracies.append(accuracy)
if (epoch + 1) % 20 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}, Accuracy: {accuracy:.2f}%')
return train_losses, val_accuracies
def evaluate(self, test_loader):
"""评估模型性能"""
self.model.eval()
correct = 0
total = 0
all_predictions = []
all_labels = []
with torch.no_grad():
for batch_x, batch_y in test_loader:
outputs = self.model(batch_x)
_, predicted = torch.max(outputs.data, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
all_predictions.extend(predicted.cpu().numpy())
all_labels.extend(batch_y.cpu().numpy())
accuracy = 100 * correct / total
print(f'测试集准确率: {accuracy:.2f}%')
return accuracy, all_predictions, all_labels
# 训练模型
def train_iris_model(X_train, X_test, y_train, y_test):
# 转换为PyTorch张量
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.LongTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.LongTensor(y_test)
# 创建数据加载器
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=8, shuffle=False)
# 创建模型和训练器
model = IrisNet(input_size=4, hidden_size=16, num_classes=3)
trainer = ModelTrainer(model, learning_rate=0.01)
# 训练模型
print("开始训练模型...")
train_losses, val_accuracies = trainer.train(train_loader, test_loader, epochs=100)
# 评估模型
accuracy, predictions, labels = trainer.evaluate(test_loader)
return model, trainer, train_losses, val_accuracies
# 使用示例
if __name__ == "__main__":
processor = IrisDataProcessor()
X, y, feature_names, target_names = processor.load_and_explore_data()
X_train, X_test, y_train, y_test = processor.preprocess_data(X, y)
model, trainer, losses, accuracies = train_iris_model(X_train, X_test, y_train, y_test)import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import torch.nn.functional as F
class IrisNet(nn.Module):
"""简单的全连接神经网络用于鸢尾花分类"""
def __init__(self, input_size=4, hidden_size=16, num_classes=3):
super(IrisNet, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size)
self.fc3 = nn.Linear(hidden_size, num_classes)
self.dropout = nn.Dropout(0.2)
def forward(self, x):
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
class ModelTrainer:
def __init__(self, model, learning_rate=0.01):
self.model = model
self.criterion = nn.CrossEntropyLoss()
self.optimizer = optim.Adam(model.parameters(), lr=learning_rate)
def train(self, train_loader, val_loader, epochs=100):
"""训练模型"""
train_losses = []
val_accuracies = []
for epoch in range(epochs):
# 训练阶段
self.model.train()
total_loss = 0
for batch_x, batch_y in train_loader:
self.optimizer.zero_grad()
outputs = self.model(batch_x)
loss = self.criterion(outputs, batch_y)
loss.backward()
self.optimizer.step()
total_loss += loss.item()
# 验证阶段
self.model.eval()
correct = 0
total = 0
with torch.no_grad():
for batch_x, batch_y in val_loader:
outputs = self.model(batch_x)
_, predicted = torch.max(outputs.data, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
accuracy = 100 * correct / total
avg_loss = total_loss / len(train_loader)
train_losses.append(avg_loss)
val_accuracies.append(accuracy)
if (epoch + 1) % 20 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}, Accuracy: {accuracy:.2f}%')
return train_losses, val_accuracies
def evaluate(self, test_loader):
"""评估模型性能"""
self.model.eval()
correct = 0
total = 0
all_predictions = []
all_labels = []
with torch.no_grad():
for batch_x, batch_y in test_loader:
outputs = self.model(batch_x)
_, predicted = torch.max(outputs.data, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
all_predictions.extend(predicted.cpu().numpy())
all_labels.extend(batch_y.cpu().numpy())
accuracy = 100 * correct / total
print(f'测试集准确率: {accuracy:.2f}%')
return accuracy, all_predictions, all_labels
# 训练模型
def train_iris_model(X_train, X_test, y_train, y_test):
# 转换为PyTorch张量
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.LongTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.LongTensor(y_test)
# 创建数据加载器
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=8, shuffle=False)
# 创建模型和训练器
model = IrisNet(input_size=4, hidden_size=16, num_classes=3)
trainer = ModelTrainer(model, learning_rate=0.01)
# 训练模型
print("开始训练模型...")
train_losses, val_accuracies = trainer.train(train_loader, test_loader, epochs=100)
# 评估模型
accuracy, predictions, labels = trainer.evaluate(test_loader)
return model, trainer, train_losses, val_accuracies
# 使用示例
if __name__ == "__main__":
processor = IrisDataProcessor()
X, y, feature_names, target_names = processor.load_and_explore_data()
X_train, X_test, y_train, y_test = processor.preprocess_data(X, y)
model, trainer, losses, accuracies = train_iris_model(X_train, X_test, y_train, y_test)
4. 模型转换:PyTorch → ONNX → OM(ATC工具)
4.1 导出为ONNX格式
昇腾 ATC 工具不直接支持 PyTorch 的.pth 模型,需先转换为 ONNX 格式:
def export_to_onnx(model, input_size=4, onnx_path="iris_model.onnx"):
"""将PyTorch模型导出为ONNX格式"""
# 设置为评估模式
model.eval()
# 创建示例输入
dummy_input = torch.randn(1, input_size)
# 导出模型
torch.onnx.export(
model,
dummy_input,
onnx_path,
export_params=True,
opset_version=11,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
print(f"模型已导出为: {onnx_path}")
return onnx_path
# 导出训练好的模型
onnx_path = export_to_onnx(model)
4.2 使用ATC工具转换为OM模型
#!/bin/bash
# convert_model.sh - 使用ATC工具转换模型
# 设置环境变量(根据实际安装路径调整)
# source /usr/local/Ascend/ascend-toolkit/set_env.sh
# 使用ATC转换ONNX模型为昇腾OM模型
atc \
--model=iris_model.onnx \
--framework=5 \
--output=iris_model_ascend \
--soc_version=Ascend310 \
--input_shape="input:1,4" \
--input_format=ND \
--output_type=FP32 \
--log=info \
--op_select_implmode=high_precision
echo "模型转换完成: iris_model_ascend.om"
5. AscendCL推理实现
5.1 基于CANN的推理引擎(模拟器模式)
import numpy as np
import acl
import ctypes
import sys
import os
class AscendIrisClassifier:
"""基于AscendCL模拟器的鸢尾花分类器(无硬件验证版)"""
def __init__(self, model_path):
self.model_path = model_path
self.device_id = 0 # 虚拟设备ID
self.context = None
self.model_id = None
self.model_desc = None
def init_resource(self):
"""初始化AscendCL资源(模拟器模式)"""
try:
# 1. 初始化ACL(仅加载框架,无硬件交互)
ret = acl.init()
if ret != 0:
raise Exception(f"ACL初始化失败: {ret}")
print("ACL框架初始化成功(模拟器模式)")
# 2. 绑定虚拟设备(无物理芯片仅验证API调用)
ret = acl.rt.set_device(self.device_id)
if ret != 0:
raise Exception(f"虚拟设备绑定失败: {ret}")
print(f"虚拟设备 {self.device_id} 绑定成功")
# 3. 创建上下文(核心API验证)
self.context, ret = acl.rt.create_context(self.device_id)
if ret != 0:
raise Exception(f"上下文创建失败: {ret}")
print("计算上下文创建成功")
# 4. 加载OM模型(仅验证模型格式合法性)
self.model_id, ret = acl.mdl.load_from_file(self.model_path)
if ret != 0:
raise Exception(f"OM模型加载失败: {ret}")
print(f"OM模型加载成功: {self.model_path}")
# 5. 解析模型描述(核心学习点:获取模型输入输出信息)
self.model_desc = acl.mdl.create_desc()
ret = acl.mdl.get_desc(self.model_desc, self.model_id)
if ret != 0:
raise Exception(f"模型描述解析失败: {ret}")
# 6. 解析模型输入输出维度(无硬件核心学习内容)
self._parse_model_io()
print("AscendCL资源初始化完成(模拟器模式)")
except Exception as e:
print(f"初始化失败: {e}")
self.release_resource()
sys.exit(1)
def _parse_model_io(self):
"""解析模型输入输出信息(无硬件核心学习点)"""
# 获取输入信息
input_num = acl.mdl.get_num_inputs(self.model_desc)
input_dim = acl.mdl.get_input_dims(self.model_desc, 0)
input_size = acl.mdl.get_input_size_by_index(self.model_desc, 0)
print(f"【模型解析】输入数量: {input_num} | 输入维度: {input_dim} | 输入大小: {input_size} bytes")
# 获取输出信息
output_num = acl.mdl.get_num_outputs(self.model_desc)
for i in range(output_num):
output_dim = acl.mdl.get_output_dims(self.model_desc, i)
output_size = acl.mdl.get_output_size_by_index(self.model_desc, i)
print(f"【模型解析】输出 {i} | 维度: {output_dim} | 大小: {output_size} bytes")
def simulate_predict(self, features):
"""模拟推理(仅验证输入预处理+数据格式适配,无实际计算)"""
try:
# 预处理输入(与训练端保持一致)
input_data = self._preprocess_input(features)
print(f"【模拟推理】输入形状: {input_data.shape} | 数据类型: {input_data.dtype}")
# 验证输入与模型维度匹配(核心校验逻辑)
input_dim = acl.mdl.get_input_dims(self.model_desc, 0)
model_input_shape = [dim for dim in input_dim[0]['dims']]
if list(input_data.shape) != model_input_shape:
raise Exception(f"输入维度不匹配!模型要求: {model_input_shape}, 实际: {input_data.shape}")
print("【模拟推理】输入格式验证通过(无硬件跳过实际计算)")
# 模拟输出(返回占位符,仅展示格式)
pred_class = np.argmax(np.random.rand(1, 3), axis=1)
confidence = np.max(np.random.rand(1, 3), axis=1)
return pred_class, confidence
except Exception as e:
print(f"模拟推理失败: {e}")
return None, None
def _preprocess_input(self, features):
"""预处理输入特征(与训练端标准化逻辑一致)"""
if not isinstance(features, np.ndarray):
features = np.array(features, dtype=np.float32)
if len(features.shape) == 1:
features = features.reshape(1, -1)
# 加载训练端保存的标准化参数
mean = np.load("scaler_mean.npy")
std = np.load("scaler_std.npy")
features = (features - mean) / std
return features.astype(np.float32)
def release_resource(self):
"""释放资源(模拟器模式)"""
try:
if self.model_desc:
acl.mdl.destroy_desc(self.model_desc)
if self.model_id:
acl.mdl.unload(self.model_id)
if self.context:
acl.rt.destroy_context(self.context)
acl.rt.reset_device(self.device_id)
acl.finalize()
print("资源释放完成(模拟器模式)")
except Exception as e:
print(f"资源释放异常: {e}")
5.2 完整的使用示例(无硬件验证版)
def main():
"""主函数:昇腾CANN全流程学习示例(无硬件版)"""
# 1. 数据准备
print("=== 步骤1: 数据准备 ===")
# 复用之前的IrisDataProcessor逻辑(仅保留数据加载+预处理)
processor = IrisDataProcessor()
X, y, feature_names, target_names = processor.load_and_explore_data()
X_train, X_test, y_train, y_test = processor.preprocess_data(X, y)
# 2. 模型训练
print("\n=== 步骤2: 模型训练 ===")
model, trainer, losses, accuracies = train_iris_model(X_train, X_test, y_train, y_test)
# 3. 导出为ONNX
print("\n=== 步骤3: ONNX模型导出 ===")
onnx_path = export_to_onnx(model)
# 4. ATC转换OM模型(模拟器模式)
print("\n=== 步骤4: ATC模型转换(模拟器模式) ===")
print("执行命令: atc --model=iris_mlp.onnx --framework=5 --output=iris_model_ascend --soc_version=Ascend310B1 --simulate=1")
print("OM模型转换完成(仅验证格式合法性)")
# 5. AscendCL模拟器验证
print("\n=== 步骤5: AscendCL API验证(模拟器模式) ===")
classifier = AscendIrisClassifier("iris_model_ascend.om")
classifier.init_resource()
# 模拟推理(仅验证输入格式+维度匹配)
test_samples = [
[5.1, 3.5, 1.4, 0.2], # Setosa
[6.7, 3.0, 5.2, 2.3], # Virginica
[5.9, 3.0, 4.2, 1.5], # Versicolour
]
print("\n模拟推理验证:")
for i, sample in enumerate(test_samples):
predicted_class, confidence = classifier.simulate_predict(sample)
if predicted_class is not None:
print(f"样本 {i+1}: {sample}")
print(f" 输入格式验证: 通过 | 模拟预测类别索引: {predicted_class[0]}")
# 6. 清理资源
classifier.release_resource()
if __name__ == "__main__":
main()
总结
通过本实战项目,我们完整地展示了昇腾 CANN 生态的核心学习流程(无硬件也能掌握):
- 数据准备:加载、探索和预处理经典的鸢尾花数据集,为模型训练奠定基础;
- 模型训练:使用 PyTorch 构建轻量化 MLP 分类器,保证模型与 CANN 算子兼容;
- 模型转换:通过 ONNX 中间格式 + ATC 工具,掌握昇腾专属 OM 模型的转换逻辑(模拟器验证);
- API 验证:基于 AscendCL 接口,掌握昇腾推理的核心流程(资源初始化、模型解析、输入校验)。
本项目聚焦昇腾 CANN 的 “软件层核心逻辑”,避开物理硬件依赖,所掌握的模型适配、算子兼容、ATC 转换等能力,可无缝迁移到有昇腾芯片的场景。在此基础上,你可进一步学习 CANN 自定义算子开发、模型量化等进阶内容,逐步掌握昇腾生态的核心开发能力。
欢迎加入CANN社区:https://atomgit.com/cann
更多推荐


所有评论(0)