ML.Net - 开源的跨平台机器学习框架

  • 支持CPU/GPU训练
  • 轻松简洁的预测代码
  • 可扩展其他的机器学习平台
  • 跨平台

1.使用Visual Studio的Model Builder训练和使用模型

Visual Studio默认安装了Model Builder插件,可以很快地进行一些通用模型类型的训练和部署,提高接入机器学习的开发效率

1.1 新建模型

通过非常简单地 右键项目-添加-机器学习模型

1.2 选择模型

ModelBuilder中提供了集中常用的模型类型以供开发者使用,开发者可以通过这些类别的模型快速接入,并且训练自己的数据,本节内容将会使用计算机视觉中的”图像分类“进行演示

1.3 选择训练环境

接下来要选择训练的环境,提供了CPU/GPU/Azure云三种方式训练,这里为了简单演示,我使用了CPU训练,如果数据量大且复杂的请选择GPU,并且提前安装CUDA、cuDNN

1.4 添加训练数据

我从搜索引擎中,搜集到了一系列”奥特曼“的图片(我相信不是所有人都可以认出各个时代的各个奥特曼 哈哈哈)

然后将这些图片进行了文件夹分类,导入到ModelBuilder中,如下:

1.5 开始训练

本次演示训练157张图片,耗时50秒

1.6 评估

此环节,为了检验训练成果和准确率,ModelBuilder中提供了图形化的方式进行预测检测,我在另外的搜索引擎中,找到了一张没有经过训练的图片,它准确地判断出了”迪迦奥特曼“的概率为63%

1.7 代码编写

这一环节中,ModelBuilder给出了示例代码,直接复制粘贴就可以用到自己的实际项目中

同时还提供了,一键生成控制台或者WebAPI项目的入口。给力!

我新建了一个WPF项目,添加了一个Button,进行简单测试:

  1. <Grid>
  2. <Button Click="Button_Click" Content="预测一个奥特曼" />
  3. </Grid>
  1. private void Button_Click(object sender, RoutedEventArgs e) {
  2. OpenFileDialog dialog= new OpenFileDialog();
  3. if (dialog.ShowDialog().Value) {
  4. //Load sample data
  5. var imageBytes = File.ReadAllBytes(dialog.FileName);
  6. UltraMan.ModelInput sampleData = new UltraMan.ModelInput() {
  7. ImageSource = imageBytes,
  8. };
  9. //Load model and predict output
  10. var result = UltraMan.Predict(sampleData);
  11. if (result.Score.Any(s=>s>=0.6)) {
  12. MessageBox.Show(result.PredictedLabel);
  13. } else {
  14. MessageBox.Show("没有识别到奥特曼");
  15. }
  16. }
  17. }

选择一张图片,导入之后,即可弹出预测结果

ModelBuilder的操作非常简单,基本不需要了解机器学习的原理或者python,对一些有这些内置模型需求的.Net开发者很有帮助!~


2.使用ONNX模型进行分类预测

如果团队中有其他专业的AI人员进行模型训练和机器学习代码编写,如何将pytorch、tensenflow等框架训练的模型用在.Net中呢?

ML.Net在支持使用内置的ModelBuilder模型外,还支持使用onnx模型进行预测

这里需要提前介绍一下ML模型仪表盘



右侧的Inputs和Outputs在后续步骤中比较关键

2.1 下载所需模型

进入github根据需求下载模型文件,本篇文章使用了【Emotion FERPlus】模型进行情绪预测

下载地址:github

根据github中的接入说明,理解输入、输出、预处理、预测、后处理等流程后,开始接入

2.2 流程说明

输入:N*1*64*64 的float数组

表示可以预测多张(N)图片,且输入图片要是单色通道图(1),尺寸为64*64(需要缩放)

预处理:导入图片路径进行预测

python代码中,将图片导入,并进行了缩放处理,然后使用np.array把图片数据转为了float数组形式,最后把数组进行[1,1,64,64]的形状缩放,将rgb提取了单色数据

输出:1*8 的float数组

输出了一个8长度的一维数组,分别代表了8种表情的分数值,可能性最高的值为最终结果

2.3 ML.Net接入

使用ML.Net接入onnx前,需要安装几个nuget包:

  • Microsoft.ML
  • Microsoft.ML.ImageAnalytics
  • Microsoft.ML.OnnxTransformer

2.3.1 定义输入和输出的类

输入:

  1. public class EmotionInput {
  2. [ImageType(64,64)]
  3. public MLImage Image { get; set; }
  4. }

定义了一个输入类EmotionInput,标记图像为64*64,且类型为MLImage

输出:

  1. public class EmotionOutput {
  2. [ColumnName("Plus692_Output_0")]
  3. public float[] Result { get; set; }
  4. }

根据ML Dashboard可以看到输出列名为Plus692_Output_0,类型为一维浮点数组

开始预测:

  1. public class EmotionPrediction {
  2. private readonly string modelFile = "emotion-ferplus-8.onnx";
  3. private string[] emotions = new string[] { "一般", "快乐", "惊讶", "伤心", "生气", "疑惑", "害怕", "蔑视" };
  4. private PredictionEngine<EmotionInput, EmotionOutput> predictionEngine;
  5. public EmotionPrediction()
  6. {
  7. MLContext context = new MLContext();
  8. var emptyData = new List<EmotionInput>();
  9. var data = context.Data.LoadFromEnumerable(emptyData);
  10. var pipeline = context.Transforms.ResizeImages("resize", 64, 64, inputColumnName: nameof(EmotionInput.Image), Microsoft.ML.Transforms.Image.ImageResizingEstimator.ResizingKind.Fill).
  11. Append(context.Transforms.ExtractPixels("Input3", "resize", Microsoft.ML.Transforms.Image.ImagePixelExtractingEstimator.ColorBits.Blue)).
  12. Append(context.Transforms.ApplyOnnxModel(modelFile));
  13. var model = pipeline.Fit(data);
  14. predictionEngine = context.Model.CreatePredictionEngine<EmotionInput, EmotionOutput>(model);
  15. }
  16. public string Predict(string path) {
  17. using (var stream = new FileStream(path, FileMode.Open)) {
  18. using(var bitmap = MLImage.CreateFromStream(stream)) {
  19. var result = predictionEngine.Predict(new EmotionInput() { Image = bitmap });
  20. var max = result.Result.Max();
  21. var index = result.Result.ToList().IndexOf(max);
  22. return emotions[index];
  23. }
  24. }
  25. }
  26. }

其中预测部分,比较关键的地方是预测管道部分,从Input中拿到图片数据-->Resize-->提取图片的蓝色数据-->作为Input3输入列传入模型

这里使用蓝色作为提取色,是因为蓝色在色彩表示中较为明亮,计算机更容易识别这些像素和区域

使用WPF接入试试看:

  1. <Grid>
  2. <Button Click="Button_Click" Content="预测表情" />
  3. </Grid>
  1. private EmotionPrediction prediction = new EmotionPrediction();
  2. private void Button_Click(object sender, RoutedEventArgs e) {
  3. OpenFileDialog dialog = new OpenFileDialog();
  4. if (dialog.ShowDialog().Value) {
  5. var result = prediction.Predict(dialog.FileName);
  6. MessageBox.Show(result);
  7. }
  8. }

导入一张普通图片试试:



导入一张甜心美少女试试:



onnx模型的接入,使得ML.Net的可扩展性更高,不仅仅是内置模型,还可以更多~


3. 使用ONNX模型进行识别分割

接下来是一个稍微复杂一点的模型的接入方法

卷积神经网络中,人脸识别、车牌识别、物体识别很火热(比如有名的开源模型YOLO)

当然ModelBuilder已经内置物体识别模型,可以识别的物体在图中位置和矩形区域

进入github根据需求下载模型文件,本篇文章使用了【UltraFace】模型进行人脸识别

下载地址:github

通过描述,可以看出此模型会复杂一些。需要将数据进行BGR2RGB、偏移、归一化等预处理,也需要对预测结果进行非极大值抑制、矩形框变化处理

3.1 流程说明

输入:1*3*320*240

一张图片,进行缩放处理到320*240,且不要透明度

预处理:BRG-RGB + Resize + 偏移 + 归一化 + 数据转chw

这里是opencv中的一系列图片处理的方法,为了匹配模型的输入且加快训练预测的速度

在深度学习中,神经网络模型的输入数据一般都需要经过一些预处理才能被正确地输入到模型中进行训练或者预测。下面是对各个预处理步骤的解释:

- BGR-RGB转换:在OpenCV中读取图像时,图像的通道顺序是BGR,而在深度学习中通常使用的是RGB格式。因此,需要对输入图像进行BGR-RGB通道转换。

- Resize:由于神经网络模型对输入图像的大小有一定的要求,因此在输入图像大小不符合要求时,需要进行图像的缩放操作。缩放操作有助于保留输入图像中的重要特征,并且可以减少训练和预测的时间和计算资源消耗。

- 偏移:在进行归一化操作前,先将图像每个像素点的值减去一个常数,这个常数一般是对训练数据集像素值取平均值。通过这个操作,可以将输入图像的像素值整体向左偏移一定的偏移量,使得整个像素值的范围更加平衡,便于模型的训练和优化。

- 归一化:在神经网络模型中,通过对输入数据进行归一化的操作,可以使得数据更加平滑,减少噪声和异常情况的影响。一般地,归一化会将数据的数值范围缩放到0到1之间或者-1到1之间(或其他固定范围内),这样有助于加快训练和提高模型的稳定性。

- 数据转置和重排:在深度学习框架中,输入数据的格式通常是(batch_size, channel, height, width),所以需要将预处理后的图像从(height, width, channel)的格式转化为(channel, height, width)的格式,并加上一维batch_size,以便于输入到网络中进行训练或者预测。

综上所述,这些预处理步骤是为了将图像处理成与模型输入相对应的格式,并且预处理后的图像可以减少噪声、保留重要特征、加快训练和提高模型的稳定性。

输出:1*4420*2 和 1*4420*4的两个数组

分别代表了分数和矩形框的数据

后处理:矩形框的转换+非极大值抑制

需要对输出的两个矩形进行处理,根据分数排列、根据非极大值抑制筛选出最确定的矩形框结果

在目标检测中,经常会出现多个检测框(bounding box)重叠覆盖同一目标的情况,而我们通常只需要保留一个最佳的检测结果。非极大值抑制(Non-Maximum Suppression,NMS)就是一种常见的目标检测算法,用于在冗余的检测框中筛选出最佳的一个。

NMS 原理是在对检测结果进行处理前,按照检测得分进行排序(一般检测得分越高,表明检测框越可能包含目标),然后选择得分最高的检测框加入结果中。接下来,遍历排序后的其余检测框,如果检测框之间的IoU(Intersection over Union,交并比)大于一定阈值,那么就将该检测框删除,因为被保留的那一个框已经足够表明目标的存在。

该过程不断迭代,直到所有框都被遍历完毕为止,从未删除的框中即为最终结果。由于 NMS 算法可以过滤掉重叠检测框中的冗余结果,因此在很多基于深度学习的目标检测算法(如 YOLO、SSD 等)中都被广泛使用。

== 以上作为了解,具体看下面代码 ==

3.2 ML.Net接入

3.2.1 输入

  1. public class RTFInput {
  2. [ImageType(640, 480)]
  3. public MLImage Image { get; set; }
  4. }

3.2.2 输出

  1. public class RTFOutput {
  2. [ColumnName("scores")]
  3. [VectorType(1, 17640, 2)]
  4. public float[] Scores { get; set; }
  5. [ColumnName("boxes")]
  6. [VectorType(1, 17640, 4)]
  7. public float[] Boxes { get; set; }
  8. }

3.2.3 预测

  1. public class RTFPrediction {
  2. private readonly string modelFile = "version-RFB-640.onnx";
  3. private PredictionEngine<RTFInput, RTFOutput> predictionEngine = null;
  4. public RTFPrediction() {
  5. MLContext context = new MLContext();
  6. var emptyData = new List<RTFInput>();
  7. var data = context.Data.LoadFromEnumerable(emptyData);
  8. var pipeline =
  9. context.Transforms.ResizeImages(
  10. resizing: Microsoft.ML.Transforms.Image.ImageResizingEstimator.ResizingKind.Fill,//填充Resize
  11. outputColumnName: "resize",//Resize的结果放置到 data列
  12. imageWidth: 640,
  13. imageHeight: 480,
  14. inputColumnName: nameof(RTFInput.Image)//从Image属性来源,
  15. )
  16. .Append(
  17. context.Transforms.ExtractPixels(
  18. offsetImage: 127f,
  19. scaleImage: 1 / 128f,
  20. inputColumnName: "resize",
  21. outputColumnName: "input")
  22. ).Append(
  23. context.Transforms.ApplyOnnxModel(
  24. modelFile: modelFile,
  25. inputColumnNames: new string[] { "input" },
  26. outputColumnNames: new string[] { "scores", "boxes" }));
  27. var model = pipeline.Fit(data);
  28. predictionEngine = context.Model.CreatePredictionEngine<RTFInput, RTFOutput>(model);//生成预测引擎
  29. }
  30. public ImageSource Predict(string path) {
  31. using (var stream = new FileStream(path, FileMode.Open)) {
  32. using (var bitmap = MLImage.CreateFromStream(stream)) {
  33. var prediction = predictionEngine.Predict(new RTFInput() { Image = bitmap });
  34. var boxes = ParseBox(prediction);
  35. boxes = boxes.Where(b => b.Score > 0.9).OrderByDescending(s => s.Score).ToList();
  36. boxes = HardNMS(boxes, 0.4);
  37. var bitmapimage = new BitmapImage(new Uri(path));
  38. var rtb = new RenderTargetBitmap(bitmap.Width, bitmap.Height, 96, 96, PixelFormats.Pbgra32);
  39. int t = 0;
  40. var dv = new DrawingVisual();
  41. using (DrawingContext dc = dv.RenderOpen()) {
  42. dc.DrawImage(bitmapimage, new Rect(0, 0, bitmap.Width, bitmap.Height));
  43. foreach (var item in boxes) {
  44. dc.DrawRectangle(null, new Pen(Brushes.Red, 2), new Rect(item.Rect.X * bitmap.Width, item.Rect.Y * bitmap.Height, item.Rect.Width * bitmap.Width, item.Rect.Height * bitmap.Height));
  45. }
  46. }
  47. rtb.Render(dv);
  48. return rtb;
  49. }
  50. }
  51. }
  52. private List<Box> ParseBox(RTFOutput prediction) {
  53. var length = prediction.Boxes.Length / 4;
  54. var boxes = Enumerable.Range(0, length).Select(i => new Box() {
  55. X1 = prediction.Boxes[i * 4],
  56. Y1 = prediction.Boxes[i * 4 + 1],
  57. X2 = prediction.Boxes[i * 4 + 2],
  58. Y2 = prediction.Boxes[i * 4 + 3],
  59. Score = prediction.Scores[i * 2 + 1]
  60. }
  61. );
  62. boxes = boxes.OrderByDescending(b => b.Score);
  63. return boxes.ToList();
  64. }
  65. public List<Box> HardNMS(List<Box> boxes, double overlapThreshold) {
  66. var selectedBoxes = new List<Box>();
  67. while (boxes.Count > 0) {
  68. // 取出置信度最高的bbox
  69. var currentBox = boxes[0];
  70. selectedBoxes.Add(currentBox);
  71. // 计算当前bbox和其余bbox之间的IOU
  72. boxes.RemoveAt(0);
  73. for (int i = boxes.Count - 1; i >= 0; i--) {
  74. var iou = CalculateIOU(currentBox, boxes[i]);
  75. if (iou >= overlapThreshold) {
  76. boxes.RemoveAt(i);
  77. }
  78. }
  79. }
  80. return selectedBoxes;
  81. }
  82. public double CalculateIOU(Box boxA, Box boxB) {
  83. // 计算相交部分的坐标信息
  84. float xOverlap = Math.Max(0, Math.Min(boxA.X2, boxB.X2) - Math.Max(boxA.X1, boxB.X1) + 1);
  85. float yOverlap = Math.Max(0, Math.Min(boxA.Y2, boxB.Y2) - Math.Max(boxA.Y1, boxB.Y1) + 1);
  86. // 计算相交部分的面积和并集部分的面积
  87. float intersectionArea = xOverlap * yOverlap;
  88. float unionArea = boxA.Area + boxB.Area - intersectionArea;
  89. // 计算IoU
  90. double iou = (double)intersectionArea / unionArea;
  91. return iou;
  92. }
  93. }

Box的定义:

  1. public class Box {
  2. public float X1 { get; set; }
  3. public float Y1 { get; set; }
  4. public float X2 { get; set; }
  5. public float Y2 { get; set; }
  6. public float Score { get; set; }
  7. // 计算面积
  8. public float Area => (X2 - X1 + 1) * (Y2 - Y1 + 1);
  9. private Rect GetRect() {
  10. var hei = Y2 - Y1;
  11. var wid = X2 - X1;
  12. if (wid < 0) {
  13. wid = 0;
  14. }
  15. if (hei < 0) {
  16. hei = 0;
  17. }
  18. return new Rect(X1, Y1, wid, hei);
  19. }
  20. private Rect rect = Rect.Empty;
  21. public Rect Rect {
  22. get {
  23. if (rect.IsEmpty) {
  24. rect = GetRect();
  25. }
  26. return rect;
  27. }
  28. }
  29. }

3.2.4 使用WPF试试看~

  1. <Grid>
  2. <Grid.RowDefinitions>
  3. <RowDefinition Height="50" />
  4. <RowDefinition />
  5. </Grid.RowDefinitions>
  6. <Button Click="Button_Click" Content="识别人脸框" />
  7. <Image x:Name="image" Grid.Row="1" />
  8. </Grid>
  1. private RTFPrediction prediction = new RTFPrediction();
  2. private void Button_Click(object sender, RoutedEventArgs e) {
  3. OpenFileDialog dialog = new OpenFileDialog();
  4. if (dialog.ShowDialog().Value) {
  5. var result = prediction.Predict(dialog.FileName);
  6. if (result!=null) {
  7. image.Source = result;
  8. }
  9. }
  10. }

还是导入一张快乐美少女~

识别到了!!并且我在WPF中用DrawingContext为它绘制了红色矩形框。哎呀!应该用粉色!

再导入一张多人脸图试试?



完美~~

4. 其他

本文只描述了三种训练或者接入机器学习的方式,应该可以实现一部分机器学习的需求,本文只是作为一个.NET平台机器学习预测的例子,并没有将ML.Net和传统Pytorch等成熟框架进行比较,只是给出另外一种选择。

作为绝大多数.Net开发者,如果有现成的、不需要跨语言、上手成本低的机器学习框架可以用在现有业务中,当然没有必要再学习专业的人工智能技能了。

需要注意的是,ML.Net是符合.Net Standard2.0标准的,如果在.Net Framework中使用,需要注意版本>=4.6.1

本文只代表作者本人理解,如有出入,欢迎在评论区指出,不拉不踩,交流为主

本文中出现的代码已经上传至github : https://github.com/BigHeadDev/ML.Net.Demo

.Net使用第三方onnx或ModelBuilder轻松接入AI模型的更多相关文章

  1. EasyNVR智能云终端接入AI视频智能分析功能,用户可自定义接入自己的分析算法

    视频分析的需求 人工智能的发展和在行业中的作用就不用多说了,已经到了势在必行的一个程度了,尤其是对于流媒体音视频行业来说,这基本上是人工智能重中之重的领域,视频人工智能,也就是视频视觉分析的应用方式大 ...

  2. 一步步教你轻松学KNN模型算法

    一步步教你轻松学KNN模型算法( 白宁超 2018年7月24日08:52:16 ) 导读:机器学习算法中KNN属于比较简单的典型算法,既可以做聚类又可以做分类使用.本文通过一个模拟的实际案例进行讲解. ...

  3. 微信轻松接入QQ客服

    一直以来,大家都苦恼怎么实现微信公众帐号可以接入客服,也因此很多第三方接口平台也开发客服系统CRM系统,不过不是操作复杂就是成本太高.今天分享一个低成本又简便的方法,让你的公众帐号接入QQ客服.下面介 ...

  4. ZAO 换脸不安全?用 python 轻松实现 AI

    最近两天一款名为 「ZAO」 的 App 刷爆了朋友圈,它的主打功能是 AI 换脸,宣称「只需一张照片,就能出演天下好戏」 : 现实中不能实现当明星的梦,在这个 App 里你可以,想演谁演谁.新鲜.好 ...

  5. AoE 搭档 TensorFlow Lite ,让终端侧 AI 开发变得更加简单。

    AoE( AI on Edge , https://github.com/didi/AoE ) 是滴滴近期开源的终端侧 AI 集成运行时环境 ( IRE ). 随着人工智能技术快速发展,近几年涌现出了 ...

  6. 只需3步,快来用AI预测你爱的球队下一场能赢吗?

    摘要:作为球迷,我们有时候希望自己拥有预测未来的能力. 本文分享自华为云社区<用 AI 预测球赛结果只需三步,看看你爱的球队下一场能赢吗?>,作者:HWCloudAI. 还记得今年夏天的欧 ...

  7. SaaS应用“正益工作”发布,为大中型企业轻松构建移动门户

    6月24日,以“平台之上,应用无限”为主题的2016 AppCan移动开发者大会,在北京国际会议中心隆重举行,逾1500名移动开发者一起见证了此次大会盛况. 会上,在专家领导.技术大咖.移动开发者的共 ...

  8. Optimum + ONNX Runtime: 更容易、更快地训练你的 Hugging Face 模型

    介绍 基于语言.视觉和语音的 Transformer 模型越来越大,以支持终端用户复杂的多模态用例.增加模型大小直接影响训练这些模型所需的资源,并随着模型大小的增加而扩展它们.Hugging Face ...

  9. iOS微信第三方登录实现

    iOS微信第三方登录实现   一.接入微信第三方登录准备工作.移动应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统.在进行微信OAuth2.0授权登录接入之前,在微信开 ...

  10. atitit.恒朋无纸化彩票系统数据接入通信协议

    atitit.恒朋无纸化彩票系统数据接入通信协议 深圳市恒朋科技开发有限公司 Shenzhen Helper Science & Technology Co., Ltd. 恒朋无纸化彩票系统数 ...

随机推荐

  1. 陈大好:持续创造小而美的产品丨独立开发者 x 开放麦

    本文内容来自RTE NG-Lab 计划中「独立开发者 x 开放麦」活动分享,分享嘉宾独立开发者 @陈大好. 本次活动中,来自 W2solo 独立开发者社区的管理员 @Eric Woo 也以<独立 ...

  2. UI/UE设计学习路线图(超详细)

    很多小伙伴认为ui设计很简单,就是用相关的软件设计制作图片.界面等.其实不然,UI设计融合了很多学科内容.要从一个完全没有基础的人成长为一个ui设计者,该如何学习呢?主要分为基础阶段和专业课程阶段,其 ...

  3. Centos Linux 设置 jar 包 开机自启动

    1.设置jar包可执行权限 点击查看代码 mkdir /usr/java cd /usr/java chmod 777 xxx.jar 2.编写脚本文件 touch xxx.sh 将文件放置到 /us ...

  4. 【ACM算法竞赛日常训练】DAY5题解与分析【储物点的距离】【糖糖别胡说,我真的不是签到题目】| 前缀和 | 思维

    DAY5共2题: 储物点的距离(前缀和) 糖糖别胡说,我真的不是签到题目(multiset,思维) 作者:Eriktse 简介:19岁,211计算机在读,现役ACM银牌选手力争以通俗易懂的方式讲解算法 ...

  5. MarkdownStudy02DOS窗口

    打开dos窗口 开始里面win系统 win+r,输入cmd 在任意文件下,按住shift+鼠标右键点击,在此处打开PowerShell窗口 资源管理器的地址栏前面加上cmd路径 管理员身份运行 常用的 ...

  6. crictl和ctr与docker的命令的对比

    containerd 相比于docker , 多了namespace概念, 每个image和container 都会在各自的namespace下可见, 目前k8s会使用k8s.io 作为命名空间 cr ...

  7. C++的一些随笔(第一篇)

    C++中 ->的作用 ->用于指针 ->用于指向结构体的指针 ->用于指向结构体的指针,表示结构体内的元素  #include<stdio.h> struct ro ...

  8. 图与网络分析—R实现(一)

    图与网络 一个网络G,也可以称为图(graph)或网络图,是一种包含了节点V(即网络参与者,也称顶点)与边E(即节点之间的连接关系)的数学结构,记作G={V,E}.可以使用一个矩阵来存放节点之间的连接 ...

  9. C++/Qt网络通讯模块设计与实现(总结)

    至此,C++/Qt网络通讯模块设计与实现已分析完毕,代码已应用于实际产品中. C++/Qt网络通讯模块设计与实现(一) 该章节从模块的功能需求以及非功能需求进行分析,即网络通讯模块负责网络数据包的发送 ...

  10. ModelAndView方法的返回值类型

    一.ModelAndView @RequestMapping("/selectById") public ModelAndView queryById(Integer id){ M ...