【.Net 9下使用Tensorflow.net---通过LSTM实现中文情感分析】

发布于:2025-03-16 ⋅ 阅读:(17) ⋅ 点赞:(0)


在这个例子里,使用了Tensorflow.net中的LSTM模型,加载电商评论集,经过文本拆分、向量化、构建LSTM模型训练,可以实现中文情感分析,正确率可以达到 90%以上。
本例的基本步骤为
1、导入评论数据集;
2、进行拆分,构建词典;
3、对评论数据集进行向量化处理;
4、构建模型进行训练;
5、按批次进行预测。

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

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

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

TensorFlow.NET

TensorFlow.Keras

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

Jieba.NET

二、处理评论集数据

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

本文选用的是电商评论数据,该数据集的下载网址为:https://github.com/renjunxiang/Text-Classification/blob/master/TextClassification/data/data_single.csv ,示例如下:

evaluation,label
用了一段时间,感觉还不错,可以,正面
电视非常好,已经是家里的第二台了。第一天下单,第二天就到本地了,可是物流的人说车坏了,一直催,客服也帮着催,到第三天下午5点才送过来。父母年纪大了,买个大电视画面清晰,趁着耳朵还好使,享受几年。,正面
电视比想象中的大好多,画面也很清晰,系统很智能,更多功能还在摸索中,正面
不错,正面
用了这么多天了,感觉还不错。夏普的牌子还是比较可靠。希望以后比较耐用,现在是考量质量的时候。,正面
物流速度很快,非常棒,今天就看了电视,非常清晰,非常流畅,一次非常完美的购物体验,正面
非常好,客服还特意打电话做回访,正面
物流小哥不错,辛苦了,东西还没用,正面
送货速度快,质量有保障,活动价格挺好的。希望用的久,不出问题。,正面
非常漂亮,也非常清晰,反应速度也快。,正面
很不错家里都喜欢。。。一次买了三台,正面
送货非常快!质量非常好,这次购物非常愉快!!,正面
58好大……都不错。看质量咯,正面
物流很快,物有所值,值得信赖。依旧会关顾!谢谢商家!,正面
这价钱超值,收到货马上装上看了一下。很清晰式样也蛮好!赞赞……,正面
目前感受还不错,正面
速度快、服务态度很好,正面
挺好的,物流也快,两天就到了,正面
发货速度快,正面

加载文本后,使用结巴分词进行分词,分词后 进行每行的遍历,去掉停用词后。得到后续使用的词典,词典中包含拆分后的词和词的索引。
此处注意:
该方法中初始化以下 计算结果 :词典——word_dict,评论数据——data(包含评论文本和评论结果,评论文本是拆分后 去除停用词后,以" "相连接的字符串,评论结果 为 0、1数组,1为正面评论,0为负面评论),训练集每行长度——maxLenth(计算每行词的平均数)
为了保证后续训练及预测文本的每个词都有对应的序号,所以词典中增加了几个特殊的词,词典中找不到的,以及填充数组的词。以保证后续向量化的数组都有对应的值
方法如下:

private Dictionary<string, int> init_word_dict(string path)
{          
    var words = new List<string>();           

    var segmenter = new JiebaSegmenter();
    int allong = 0;
    data = File.ReadAllLines(path)
                  .Skip(1) // 跳过标题行
                  .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;)
    {
        string sentence = data[i][0];
        //使用结巴分词进行分词
        var cuts = segmenter.Cut(sentence.Trim());
        cuts = cuts.Where(d => !stopWords.Contains(d) && d.Length > 1);
        if (cuts.Count() > 0)
        {
            allong += cuts.Count();
            sentences.Add(string.Join(" ", cuts));
            words.AddRange(cuts);                     
            i++;
        }
        else
        {
            data.RemoveAt(i);
        }
    }
    //使用平均数来定义训练集的每行最大词组数
    maxLenth=(int)allong/ (int)sentences.Count();
    //初始化词典 中包括的是 所有的拆分后的词和 词出现的频率
    //此处优化其实 可以去掉低频出现的词。本文中不再处理
    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,其实就是 已经做过 one_hot处理的评论结果:

 private (int[][], NDArray) init_word_Array( Dictionary<string, int> word_dict, int document_max_len,NDArray 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;
 }

二、创建模型,训练模型

使用LSTM模型,LSTM模型(Long Short-Term Memory)是一种特殊的循环神经网络(RNN),可用来处理
‌时间序列预测‌:如气象数据、股票价格预测等
自然语言处理‌:用于语言模型、机器翻译、情感分析等
一般大型的文本推理模型,使用的是LLM,而LSTM比较简单,适用于用来做少量的数据训练模型。

public IModel BuildModel(int maxLength, int embeddingDim, int vocabSize)
{

    var layers = keras.layers;
    var model = keras.Sequential(new List<ILayer> {

        //嵌入层
     layers.Embedding(vocabSize,embeddingDim,input_length:maxLength),
     layers.LSTM(128),
     //layers.Dropout(0.5f) ,  // 每次迭代丢弃50神经元 防止过拟合
     layers.Dense(64, activation: "relu"),
     layers.Dropout(0.5f) ,  // 每次迭代丢弃50神经元 防止过拟合
     layers.Dense(2, activation: "sigmoid"),
     //layers.out
    });

 return model;
}

public void TrainModel(IModel model, NDArray xTrain, NDArray yTrain, NDArray xTest, NDArray yTest)
{
    model.compile(optimizer: keras.optimizers.Adam(), loss: keras.losses.CategoricalCrossentropy(), metrics: new[] { "accuracy" }); // 编译模型,使用Adam优化器和二元交叉熵损失函数

    //model.compile(optimizer: new Adam(), loss: "categorical_crossentropy", metrics: new[] { "accuracy" });
    model.fit(xTrain, yTrain, batch_size: 64, epochs: 5, validation_data: (xTest, yTest));           
}

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

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

      word_dict=init_word_dict(path);

      labels = data.Select(d => { if (d.Length < 2) return 0;return d[1].Trim() == "正面" ? 1 : 0;}).ToList();

      // 构建词汇表

      var x = new int[0][];
      NDArray y;
      vocabulary_size = len(word_dict);           
     

      // 将标签转换为 one-hot 编码
       var oneHotLabels =  np.eye(2)[np.array(labels.ToArray())];
      (x, y) = init_word_Array(word_dict, maxLenth,oneHotLabels);

       划分训练集和测试集
      //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 = 150;

      _model = BuildModel(maxLenth, embeddingDim, vocabulary_size);

      TrainModel(_model, xTrain, yTrain, xTest, yTest);

      return _model;
  }

四、预测

以下是 预测的方法 和调用预测的方法,
需要预测的评论文本 一样也要进行 向量化处理,并且 注意,预测试,需要输入批次,批次大小可以和 训练时的批次大小保持一致。

  public string PredictSentiment(string text, IModel model)
  {
      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>"];
      }
          
      //var x = tmpText.Select(c => (c + " <eos>")
      //   .Split(' ').Take(maxLenth)
      //   .Select(w => word_dict.ContainsKey(w) ? word_dict[w] : word_dict["<unk>"]).ToArray())
      //   .ToArray();
      //var tmpX = TransformArray(x, x.Length, maxLenth);
      Tensor tensorX = tf.constant(wordsArray, TF_DataType.TF_INT32);
      NDArray arrayX = tensorX.numpy();
      //var sequence = tokenizer.TextsToSequences(new List<string> { string.Join(" ", words) })[0];
      //var paddedSequence = PadSequences(new List<List<int>> { sequence }, maxlen: maxLenth);
      var prediction = model.predict(arrayX,batch_size:64);
      var predict_label = np.argmax(prediction[0].numpy(), axis: 1);
      return predict_label[0].ToString() ;
  }

控制台主函数内调用预测方法

while (true) {
 Console.WriteLine("请输入您要分析的句子:");
 string input = Console.ReadLine();
 if (input == "break")
     break;
 var result = myConverter.PredictSentiment(input, _model);

 Console.WriteLine(result);
}

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

训练模型 的准确率如下:
在这里插入图片描述
总体还是上升的
预测文本的结果如下:
在这里插入图片描述
准确率还是很高的。