.Net 9下使用Tensorflow.net---通过LSTM实现中文情感分析
在这个例子里,使用了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);
}
最终程序的输入结果如下:
训练模型 的准确率如下:
总体还是上升的
预测文本的结果如下:
准确率还是很高的。