【.Net 9下使用Tensorflow.net---通过TextCNN实现中文文本分类】

发布于:2025-03-31 ⋅ 阅读:(20) ⋅ 点赞:(0)


本文通过.net9下引用 Tensorflow.net,使用TextCNN模型,通过 Function API 创建模型的方式 实现了中文的文本分类分析(前面几个例子中用到了Sequential API的创建模型的方式)。
本文使用的 数据集是 今日头条的新闻数据集 toutiao_cat_data.txt
其中大致涵盖了以下的分类:

 /*
 100 民生 故事 news_story
 101 文化 文化 news_culture 0
 102 娱乐 娱乐 news_entertainment 1
 103 体育 体育 news_sports 2
 104 财经 财经 news_finance 3
 106 房产 房产 news_house 4
 107 汽车 汽车 news_car 5
 108 教育 教育 news_edu 6
 109 科技 科技 news_tech 7
 110 军事 军事 news_military 8
 112 旅游 旅游 news_travel 9
 113 国际 国际 news_world 10
 114 证券 股票 stock 11
 115 农业 三农 news_agriculture  12
 116 电竞 游戏 news_game 13
 */

本文中实现最终效果的步骤还是差不多:

1、导入新闻数据集;
2、进行拆分,构建词典;
3、对数据集进行向量化处理;
4、划分出训练集、结果集和测试集、测试结果集
5、构建模型进行训练;
6、进行预测。

一、创建项目,并导入各依赖项

1、创建.net9的控制台应用程序

2、通过nuget,导入依赖项:

TensorFlow.NET

TensorFlow.Keras

SciSharp.TensorFlow.Redist–如果使用GPU训练,请使用不同的依赖包

Jieba.NET

以上步骤不再赘述

添加引用

using Tensorflow.NumPy;
using Tensorflow;
using Tensorflow.Keras.Engine;
using Tensorflow.Keras.Layers;
using Tensorflow.Keras;
using Tensorflow.Keras.Optimizers;
using static Tensorflow.Binding;
using static Tensorflow.KerasApi;
using JiebaNet.Segmenter;

定义变量

        //结果集长度
        int NUM_CLASS = 14;
        //批次大小
        int BATCH_SIZE = 64;
        //训练5轮
        int NUM_EPOCHS = 5;
        float loss_value = 0;
        double max_accuracy = 0;

        
        List<int> labels=new List<int>();
        List<string> sentences = new List<string>();
        List<string[]> data;

        //停用词
        string[] stopWords;
        //定义词典
        Dictionary<string, int> word_dict;
        //词典的长度
        int vocabulary_size = -1;
        //定义一个中间值,用于截断 每条新闻的最大的词组数
        int maxLenth = 30;
        NDArray xTrain, yTrain, xTest, yTest;
        //定义一个模型
        IModel _model;
        private Dictionary<int, string> resultWords = new Dictionary<int, string>();

二、处理评论集数据

1、加载初始化数据 获取字典

本文使用的 数据集是 今日头条的新闻数据集 toutiao_cat_data.txt,示例如下:

6551700932705387022_!_101_!_news_culture_!_京城最值得你来场文化之旅的博物馆_!_保利集团,马未都,中国科学技术馆,博物馆,新中国
6552368441838272771_!_101_!_news_culture_!_发酵床的垫料种类有哪些?哪种更好?_!_
6552407965343678723_!_101_!_news_culture_!_上联:黄山黄河黄皮肤黄土高原。怎么对下联?_!_
6552332417753940238_!_101_!_news_culture_!_林徽因什么理由拒绝了徐志摩而选择梁思成为终身伴侣?_!_
6552475601595269390_!_101_!_news_culture_!_黄杨木是什么树?_!_
6552387648126714125_!_101_!_news_culture_!_上联:草根登上星光道,怎么对下联?_!_
6552271725814350087_!_101_!_news_culture_!_什么是超写实绘画?_!_
6552452982015787268_!_101_!_news_culture_!_松涛听雨莺婉转,下联?_!_
6552400379030536455_!_101_!_news_culture_!_上联:老子骑牛读书,下联怎么对?_!_
6552339283632455939_!_101_!_news_culture_!_上联:山水醉人何须酒。如何对下联?_!_
6552387367334838792_!_101_!_news_culture_!_国画山水,如何读懂山水画_!_林风眠,黄海归来步步云,秋山图,计白当黑,山水画,江山万里图,张大千,巫峡清秋图,活眼,山雨欲来图

该文本集 采用_!_作为分割字符,其中 101、102为分类类型,加载后发现,文本集合中不存在 100、105、111的分类。所以操作步骤如下:
1、加载文本后,先按照分隔符进行分割,使用结巴分词进行分词,分词后 进行每行的遍历,去掉停用词后。得到后续使用的词典,词典中包含拆分后的词和词的索引。
2、分割后的分类和正文分两个集合存储,以备后续分为训练集、测试集使用

方法如下:

 //path 指的是 toutiao_cat_data.txt的路径
 private Dictionary<string, int> init_word_dict(string path)
 {
     var words = new List<string>();
     var allLabel = new Dictionary<string, int>() { };

     /*
     100 民生 故事 news_story
     101 文化 文化 news_culture 0
     102 娱乐 娱乐 news_entertainment 1
     103 体育 体育 news_sports 2
     104 财经 财经 news_finance 3
     106 房产 房产 news_house 4
     107 汽车 汽车 news_car 5
     108 教育 教育 news_edu 6
     109 科技 科技 news_tech 7
     110 军事 军事 news_military 8
     112 旅游 旅游 news_travel 9
     113 国际 国际 news_world 10
     114 证券 股票 stock 11
     115 农业 三农 news_agriculture  12
     116 电竞 游戏 news_game 13
     */
     resultWords.Add(0, "文化");
     resultWords.Add(1, "娱乐");
     resultWords.Add(2, "体育");
     resultWords.Add(3, "财经");
     resultWords.Add(4, "房产");
     resultWords.Add(5, "汽车");
     resultWords.Add(6, "教育");
 
     resultWords.Add(7, "科技");
     resultWords.Add(8, "军事");
     resultWords.Add(9, "旅游");
     resultWords.Add(10, "国际");
     resultWords.Add(11, "证券");
     resultWords.Add(12, "农业");
 
     resultWords.Add(13, "电竞");
     for (int i = 100; i <= 116; i++) {
         int j = i - 101;
         if (i >= 105)
             j = j-1;
         if (i >= 111)
             j = j - 1;
         allLabel.Add(i.ToString(), j);
     }
     var segmenter = new JiebaSegmenter();
     int allong = 0;
     //因为字符集过大,所以仅仅读取前30000行数据
     data = File.ReadAllLines(path)
                   .Take(30000)                 
                   .Select(line => line.Split("_!_"))
                   .ToList();
     //data中包含了所有的 评论文本 和 评论结果,此处进行乱序处理
     data = data.OrderBy(x => Guid.NewGuid()).ToList();
     //加载停用词,如果初始化文本中存在停用词,就去掉
     stopWords = File.ReadAllLines("D:\\workspace\\src\\stopwords.txt");
     for (int i = 0; i < data.Count;)
     {
         if (data[i].Length < 4)
         {
             data.RemoveAt(i);
             continue;
         }
         string key = data[i][1].Trim();
         //去掉不存在的 分类(不存在的分类有 100、105、111
         if (!allLabel.ContainsKey(key)||key=="100")
         {
             data.RemoveAt(i);
             continue;
         }
         string sentence0 = data[i][3];
         string sentence1 = string.Empty;
         string tmpSentence = string.Empty;
         if (data[i].Length > 4)
             sentence1 = data[i][4];
         //使用结巴分词进行分词
         var cuts = segmenter.Cut(sentence0.Trim());
         cuts = cuts.Where(d => !stopWords.Contains(d) && d.Length > 1);
         if (cuts.Count() > 0)
         {
             allong += cuts.Count();
             //sentences.Add(string.Join(" ", cuts));
             tmpSentence = string.Join(" ", cuts);
             words.AddRange(cuts);
             i++;
         }
         else
         {
             data.RemoveAt(i);
             continue;
         }
         cuts = segmenter.Cut(sentence1.Trim());
         cuts = cuts.Where(d => !stopWords.Contains(d) && d.Length > 1);
         if (cuts.Count() > 0)
         {
             allong += cuts.Count();
             tmpSentence += string.Join(" ", cuts);           
             words.AddRange(cuts);
             i++;
         }
         sentences.add(tmpSentence);
         labels.add(allLabel.get(key));
     }
      
     var word_counter = words.GroupBy(x => x)
         .Select(x => new { Word = x.Key, Count = x.Count() })
         .OrderByDescending(x => x.Count)
         .ToArray();
     //word_dict 是最终的返回的字典,包括的是 词 及词的索引
     var word_dict = new Dictionary<string, int>();
     word_dict["<pad>"] = 0;
     word_dict["<unk>"] = 1;
     word_dict["<eos>"] = 2;
     foreach (var word in word_counter)
         word_dict[word.Word] = word_dict.Count;
     return word_dict;
 }

2、根据字典,对评论数据进行向量化处理

方法如下,比较简单,就是按照词典中的索引,转化新闻数据中的词,并且按照maxLenth,截断每行的最大长度,其中返回的 y 其实就是上个方法中已经初始化的labels,labels其实是一个从0-13的整数:
private (int[][], int[]) init_word_Array(Dictionary<string, int> word_dict, int document_max_len, int[] oneHotLabels)
{

 //var contents = File.ReadAllLines(path);
 var x = sentences.Select(c => (c + " <eos>")
     .Split(' ').Take(document_max_len)
     .Select(w => word_dict.ContainsKey(w) ? word_dict[w] : word_dict["<unk>"]).ToArray())
     .ToArray();

 for (int i = 0; i < x.Length; i++)
 {
     if (x[i].Length == document_max_len)
         x[i][document_max_len - 1] = word_dict["<eos>"];
     else
         Array.Resize(ref x[i], document_max_len);
 }
 var y = oneHotLabels;

 return (x, y);

}

3、准备训练集

以上准备工作基本就完成了,以下两个方法 是将 上述 返回x,y --向量化后的评论数据集和评论结果分别拆分为训练集、训练结果集,测试集和测试结果集,另一个方法 只是为了将动态数组转化为固定大小数组,以便于转化为张量

 private (NDArray, NDArray, NDArray, NDArray) train_test_split(NDArray x, NDArray y, float test_size = 0.3f)
 {
     Console.WriteLine("Splitting in Training and Testing data...");
     long len = x.shape[0];
     int train_size = (int)Math.Round(len * (1 - test_size));

     xTrain = x[new Slice(stop: train_size), new Slice()];
     xTest = x[new Slice(start: train_size), new Slice()];
     yTrain = y[new Slice(stop: train_size)];
     yTest = y[new Slice(start: train_size)];
     Console.WriteLine("\tDONE");

     return (xTrain, xTest, yTrain, yTest);
 }

 public int[,] TransformArray(int[][] x, int xLength, int yLength)
 {
     int[,] array = new int[xLength, yLength];
     for (int i = 0; i < xLength; i++)
     {
         for (int j = 0; j < yLength; j++)
         {
             array[i, j] = 0;
         }
     }

     for (int i = 0; i < x.Length; i++)
     {
         for (int j = 0; j < x[i].Length; j++)
         {
             array[i, j] = x[i][j];
         }
     }
     return array;
 }

三、创建模型,训练模型

使用TextCNN模型,TextCNN模型实际上是由多个层来进行嵌套后实现的,从示例中可以看出,该模型的结构主要包括嵌入层,多个不同尺寸内核的卷积层(学习不同的属性和向量)、池化层,最后有全连接层实现分类。主要可用来处理
自然语言处理‌:用于文本分类、情感分析等
该模型其实后续优化后有LSTM模型,更大型的 LLM模型等,本例中使用该模型,是对该模型的一个使用介绍,另外主要扩展下Tensorflow.net中创建模型的 Function API 方法,另外证明下,.NET是可以很快捷的用于AI模型设计和训练的。

        public IModel BuildModel(int maxLength, int embeddingDim, int vocabSize, int num_class)
        {
            var filter_sizes = new int[3, 4, 5];
            var num_filters = 128;
            LayersApi layers = new LayersApi();    
           
            var input = keras.Input(new Shape(maxLength), dtype: TF_DataType.TF_INT32);
            var embedding_layer = layers.Embedding(vocabSize, embeddingDim, input_length: maxLength).Apply(input);
          
            var pooled_outputs = new List<Tensor>();
            for (int len = 0; len < filter_sizes.Rank; len++)
            {
                int filter_size = filter_sizes.GetLength(len);
                Tensor conv = layers.Conv1D(
                        filters: num_filters,
                        kernel_size: filter_size,
                        padding: "valid",
                        activation: "relu",
                        strides: 1).Apply(embedding_layer);

                conv = layers.GlobalMaxPooling1D().Apply(conv);
                pooled_outputs.add(conv);

              
            }

            //合并所有卷积结果
            var concatenated = layers.Concatenate(axis: -1).Apply(pooled_outputs);          
            var dropout=layers.Dropout(0.5f).Apply(concatenated);           
            var output=layers.Dense(NUM_CLASS, activation: keras.activations.Softmax).Apply(dropout);
             var model = keras.Model(input, output, name: "mnist_model");        


            return model;
        }

四、 调用以上方法,得到训练后的模型

本例中未调用save 和load方法 对模型进行保存或者加载

  public void PrepareData(string path)
  {
      //var vocabSize = 0; // 根据实际词汇表大小调整

      word_dict = init_word_dict(path);         

      var x = new int[0][];
      int[] y;
      vocabulary_size = len(word_dict);
    
      (x, y) = init_word_Array(word_dict, maxLenth, labels.ToArray());

       划分训练集和测试集
      //var (xTrain, xTest, yTrain, yTest) = SplitData(paddedSequences, oneHotLabels, testSize: 0.2);
      var tmpX = TransformArray(x, x.Length, maxLenth);
      Tensor tensorX = tf.constant(tmpX, TF_DataType.TF_INT32);
      NDArray arrayX = tensorX.numpy();
      (xTrain, xTest, yTrain, yTest) = train_test_split(arrayX, y, test_size: 0.15f);
      var embeddingDim = 128;
//构建模型
      _model = BuildModel(maxLenth, embeddingDim,  vocabulary_size, NUM_CLASS);
//训练模型
      TrainModel(_model, xTrain, yTrain, xTest, yTest);
    
  }

四、预测

以下是 预测的方法 和调用预测的方法,
需要预测的评论文本 一样也要进行 向量化处理。

 public string Predict(string text)
 {
    
     var segmenter = new JiebaSegmenter();
     var words = segmenter.Cut(text);

     words = words.Where(d => !stopWords.Contains(d) && d.Length > 1);
     string tmpText = string.Empty;
     if (words.Count() > 0)
     {
         tmpText = string.Join(" ", words);
     }
     else
     {
         return "失败";
     }
     int[,] wordsArray = new int[1, maxLenth];
     for (int i = 0; i < maxLenth; i++)
         wordsArray[0, i] = 0;
     for (int i = 0; i < words.Count(); i++)
     {
         if (i >= maxLenth)
             break;
         if (word_dict.ContainsKey(words.ElementAt(i)))
             wordsArray[0, i] = word_dict[words.ElementAt(i)];
         else
             wordsArray[0, i] = word_dict["<unk>"];
     }

   
     Tensor tensorX = tf.constant(wordsArray, TF_DataType.TF_INT32);
     NDArray arrayX = tensorX.numpy().reshape(new Shape(1,30));

     var prediction = _model.predict(arrayX);
     var predict_label = np.argmax(prediction[0].numpy(), axis: 1);
     return resultWords.get((int)predict_label);
   
 }        

最终程序的输入结果如下:

训练模型 的准确率如下:
在这里插入图片描述
最终能达到85%以上 还存在调优的空间,但是证明方法可行
在这里插入图片描述
准确率还是很高的。