.Net 9下使用Tensorflow.net---通过TextCNN实现中文文本分类
本文通过.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%以上 还存在调优的空间,但是证明方法可行
准确率还是很高的。