原理可以参考:使用textCNN进行文本分类的原理
Keras的另一个实现可以参考:Keras实现textCNN文本分类
环境:
windows 10、tensorflow版本为2.3.0
模型构建与训练
定义网络结构
定义了一个TextCNN类
from tensorflow.keras import Input, Model
from tensorflow.keras.layers import Embedding, Dense, Conv1D, GlobalMaxPooling1D, Concatenate
class TextCNN(object):
def __init__(self, maxlen, max_features, embedding_dims, class_num=5, last_activation='softmax'):
self.maxlen = maxlen
self.max_features = max_features
self.embedding_dims = embedding_dims
self.class_num = class_num
self.last_activation = last_activation
def get_model(self):
input = Input((self.maxlen,))
embedding = Embedding(self.max_features, self.embedding_dims, input_length=self.maxlen)(input)
convs = []
for kernel_size in [3, 4, 5]:
c = Conv1D(128, kernel_size, activation='relu')(embedding)
c = GlobalMaxPooling1D()(c)
convs.append(c)
x = Concatenate()(convs)
output = Dense(self.class_num, activation=self.last_activation)(x)
model = Model(inputs=input, outputs=output)
return model
通用工具函数
此处在utils.py中定义了一些工具函数
utils.py的代码如下所示:
# coding: utf-8
import sys
from collections import Counter
import numpy as np
import tensorflow.keras as kr
import os
import platform
if sys.version_info[0] > 2:
is_py3 = True
else:
reload(sys)
sys.setdefaultencoding("utf-8")
is_py3 = False
def open_file(filename, mode='r'):
"""
常用文件操作,可在python2和python3间切换.
mode: 'r' or 'w' for read or write
"""
if is_py3:
return open(filename, mode, encoding='utf-8', errors='ignore')
else:
return open(filename, mode)
def read_file(filename):
"""读取单个文件,文件中包含多个类别"""
contents = []
labels = []
with open_file(filename) as f:
for line in f:
try:
raw = line.strip().split("\t")
content = raw[1].split(' ')
if content:
contents.append(content)
labels.append(raw[0])
except:
pass
return contents, labels
def read_single_file(filename):
"""读取单个文件,文件为单一类别"""
contents = []
if platform.system() == 'Linux':
label = filename.split('/')[-1].split('.')[0]
else:
label = filename.split('\\')[-1].split('.')[0]
with open_file(filename) as f:
for line in f:
try:
content = line.strip().split(' ')
if content:
contents.append(content)
except:
pass
return contents, label
def read_files(dirname):
"""读取文件夹"""
contents = []
labels = []
files = [f for f in os.listdir(dirname) if f.endswith(".txt")]
for filename in files:
content, label = read_single_file(os.path.join(dirname, filename))
contents.extend(content)
labels.extend([label]*len(content))
return contents, labels
def build_vocab(train_dir, vocab_file, vocab_size=5000):
"""根据训练集构建词汇表,存储"""
data_train, _ = read_files(train_dir)
all_data = []
for content in data_train:
all_data.extend(content)
counter = Counter(all_data)
count_pairs = counter.most_common(vocab_size - 1)
words, _ = list(zip(*count_pairs))
# 添加一个 <PAD> 来将所有文本pad为同一长度
words = ['<PAD>'] + list(words)
open_file(vocab_file, mode='w').write('\n'.join(words) + '\n')
def read_vocab(vocab_file):
"""读取词汇表"""
# words = open_file(vocab_dir).read().strip().split('\n')
with open_file(vocab_file) as fp:
# 如果是py2 则每个值都转化为unicode
words = [_.strip() for _ in fp.readlines()]
word_to_id = dict(zip(words, range(len(words))))
return words, word_to_id
def read_category():
"""读取分类,编码"""
categories = ['car', 'entertainment', 'military', 'sports', 'technology']
cat_to_id = dict(zip(categories, range(len(categories))))
return categories, cat_to_id
def encode_cate(content, words):
"""将id表示的内容转换为文字"""
return [(words[x] if x in words else 40000) for x in content]
def encode_sentences(contents, words):
"""将id表示的内容转换为文字"""
return [encode_cate(x,words) for x in contents]
def process_file(filename, word_to_id, cat_to_id, max_length=600):
"""将文件转换为id表示"""
contents, labels = read_file(filename)
data_id, label_id = [], []
for i in range(len(contents)):
data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id])
label_id.append(cat_to_id[labels[i]])
# 使用keras提供的pad_sequences来将文本pad为固定长度
x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length)
y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id)) # 将标签转换为one-hot表示
return x_pad, y_pad
def batch_iter(x, y, batch_size=64):
"""生成批次数据"""
data_len = len(x)
num_batch = int((data_len - 1) / batch_size) + 1
indices = np.random.permutation(np.arange(data_len))
x_shuffle = x[indices]
y_shuffle = y[indices]
for i in range(num_batch):
start_id = i * batch_size
end_id = min((i + 1) * batch_size, data_len)
yield x_shuffle[start_id:end_id], y_shuffle[start_id:end_id]
数据处理与模型训练
from tensorflow.keras.preprocessing import sequence
import random
from sklearn.model_selection import train_test_split
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.utils import to_categorical
from utils import *
from model import TextCNN
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
#路径等配置
data_dir = 'E:/personal/nlp/practice/processed_data'
vocab_file = 'E:/personal/nlp/practice/vocab/vocab.txt'
vocab_size = 40000
#神经网络配置
max_features = 40001
maxlen = 100
batch_size = 64
embedding_dims = 50
epochs = 8
print('数据预处理与加载数据')
#如果不存在词汇表,重建
if not os.path.exists(vocab_file):
build_vocab(data_dir, vocab_file, vocab_size)
#获得词汇/类别 与id映射字典
categories, cat_to_id = read_category()
words, word_to_id = read_vocab(vocab_file)
#全部数据
x, y = read_files(data_dir)
data = list(zip(x, y))
del x, y
#乱序
random.shuffle(data)
#切分训练集和测试集
train_data, test_data = train_test_split(data)
#对文本的词id和类别id进行编码
x_train = encode_sentences([content[0] for content in train_data], word_to_id)
y_train = to_categorical(encode_cate([content[1] for content in train_data], cat_to_id))
x_test = encode_sentences([content[0] for content in test_data], word_to_id)
y_test = to_categorical(encode_cate([content[1] for content in test_data], cat_to_id))
print('对序列做padding,保证是samples*timestep的维度')
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
print('构建模型。。。。')
model = TextCNN(maxlen, max_features, embedding_dims).get_model()
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print('训练。。。。')
#设定callbacks回调函数
my_callbacks = [
ModelCheckpoint('./cnn_model.h5', verbose=1),
EarlyStopping(monitor='val_accuracy', patience=2, mode='max')
]
# one_hot_labels = to_categorical(y_train, num_classes=5)
#fit拟合数据
history = model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
callbacks=my_callbacks,
validation_data=(x_test, y_test))
训练中间信息图片显示
import matplotlib.pyplot as plt
from precess_data import history
fig1 = plt.figure()
plt.plot(history.history['loss'], 'r', linewidth=3.0)#loss可改accuracy
plt.plot(history.history['val_loss'], 'b', linewidth=3.0)#val_loss可改val_accuracy
plt.legend(['Training loss', 'Validation loss'], fontsize=18)
plt.xlabel('Epochs', fontsize=16)
plt.ylabel('Loss', fontsize=16)
plt.title('Loss Curves:CNN', fontsize=16)
fig1.savefig('loss_cnn.png')
plt.show()
模型结构打印
from tensorflow.keras.utils import plot_model
from precess_data import model
plot_model(model, show_shapes=True, show_layer_names=True, to_file='model.png')#将模型结构保存到model.png中
遇到的问题:
1,运行数据处理与模型训练程序,运行报错:ValueError: Shapes (None, 40001) and (None, 5) are incompatible
出现这个问题很可能是多分类的label设置不当导致的,通过检查代码发现,经过to_categorical之后的y有40001维,且前40000维均为0,最后一维为1。定位到utils.py中的encode_cate函数,发现是label中带有文件夹名,无法映射到cat_to_id,再次定位到read_single_file,发现是Windows和linux分隔符不同导致的。解决方案如下:
将
label = filename.split('/')[-1].split('.')[0]
改为
import platform
if platform.system()=='Linux': # Windows will be : Windows, Linux will be : Linux
label = filename.split('/')[-1].split('.')[0]
else:
label = filename.split('\\')[-1].split('.')[0]
2,运行模型结构打印程序,报错:ImportError: Failed to import pydot. You must install pydot and graphviz for `pydotprint` to work.
解决方案如下:
pip install pydot
conda install graphviz
电脑安装graphviz并添加环境变量。下载地址:graphviz Windows10 64位下载,安装过程中,选择将Graphviz\bin加入系统path环境变量。
重新运行模型结构打印程序,模型结构会保存在同目录为model.png
本文介绍了一种基于TextCNN的文本分类方法,并提供了详细的模型构建与训练过程。文中还涉及了常见错误及其解决办法。

1万+

被折叠的 条评论
为什么被折叠?



