谈谈我的第一个情感分析NLP模型项目
先模仿,后超越
记得之前看过一篇文章,作者讲的就是人工智能一步步深入学习的路线,方法,态度以及目标。文章我现在也找不到了,只有脑子里还剩一点其中讲的学习方法和学习态度,但在其评论区里面一位某科技顶尖公司的leader给出的一段话让我感触很深。他大概讲的是他自己学习技术的方法,他讲的不多,但讲的都很精炼,他讲的不华丽高深,但却又颇具道理和深意。其中,最核心的,我将它总结出来就是:先模仿,后超越!
前言:对人工智能的一些微薄看法
在大多数人眼中,人工智能仿佛都很高深莫测,难度很高。这其实不假,包括很多人工智能的初学者或者想要入门的人都几乎会被其中极其复杂的数学统计方法原理以及极其繁多的技术代码所拷打并劝退(之前在B站看过一个评论我印象很深:学了五年了,人工智能还没入门),但目前我的经验和实践经历告诉我,其实人工智能和普通人之前真的就隔着一层纱,或者说隔着一段时期,一段需要沉下心来好好推算理解数学原理和代码复现的时期,一段需要对整个行业技术有一定了解的时期,一段需要对学习到的算法,技术和数学底层逻辑进行系统性归纳和整理的时期。当大家跨过那段确实比较难熬的时期之后,大家可以很清晰地知道,所谓人工智能,也就不过是:
底层数学−−>代码实现−−>解决实际问题底层数学-->代码实现-->解决实际问题底层数学−−>代码实现−−>解决实际问题
这是人工智能算法的根本法则,五花八门各种各样的算法其实也就是不同数学方法和原理的变相实现并用于解决各种各样的问题罢了,数学体系的不断完整完善其实也就是人工智能算法体系不断优化的过程。因此,看似是人工智能的高门槛俱人三分,其实防的都是一些"无心人"。
一、项目启动:
目前阶段,我的技术方向是人工智能中的NLP情感分析,在前期学习完基础编程语言,一些机器学习深度学习算法和项目分享后,在学长的建议以及自身好奇心的驱动下,我开始了第一个NLP情感分析项目。根据先模仿后超越的这个技术方法,在学长的推荐下,我选择了该项目模型源码做第一个NLP情感分析六分类实践项目:
二、模型结构:
通过查看其Github文件结构,我们可以知道该项目模型的结构总共分为:
- 数据清洗
- BERT模型微调
- 模型推理
- 测试
- Web网络端部署
整体代码我就不全部展现在这里了,我简单总结一下这里的每一部分代码:
(1)数据清洗(clean_data):
这是一个用于清洗文本数据的Python脚本,它滴主要目的是:去除文本中的URL和一些不需要的信息,以便进行情绪分析。
-
导入依赖:脚本开始时导入了必要的库,包括
json
用于处理JSON文件,tqdm
用于显示进度条,harvesttext
用于文本清洗,pyhanlp
用于中文文本处理,re
用于正则表达式,os
用于操作系统功能。 -
read_json 函数:读取JSON文件并返回数据。使用
with open
确保文件正确关闭,并使用json.load
解析JSON数据。 -
save_json 函数:将数据以JSON格式保存到文件。使用
json.dumps
将数据转换为JSON格式的字符串,并写入文件。 -
remove_url 函数:使用正则表达式去除文本中的URL。正则表达式匹配http或https开头的URL并将其替换为空字符串。
-
clean_text 函数:
- 接受数据文件路径和保存目录作为参数。
- 初始化
HarvestText
和CharTable
对象,用于文本清洗和中文字符转换。 - 读取原始JSON数据。
- 遍历数据,使用
CharTable.convert
转换中文字符,使用remove_url
去除URL。 - 计数清洗后为空的数据条目。
- 根据文件名是否包含
train
、eval
或test
,决定是否保留数据条目或更新数据条目。 - 将清洗后的数据保存为新的JSON文件。
-
数据清洗流程:
- 对训练集、测试集和评估集分别调用
clean_text
函数进行清洗。 - 清洗后的数据保存在指定的目录下,文件名与原始文件相同。
- 对训练集、测试集和评估集分别调用
-
进度条:使用
trange
显示进度条,提供用户友好的反馈。 -
数据过滤:在清洗训练集时,如果原始数据或清洗后的数据为空,则跳过这些数据条目。
-
输出信息:在读取、保存数据和完成清洗后,打印相应的信息,包括文件名和清洗过程中空数据的数量。
所以总结来说,这段代码提供了一个自动化的文本数据清洗流程,且特别适用于情绪分析任务中的预处理阶段。通过去除URL和过滤掉无效数据,我们可以提高后续模型训练和评估的质量和效率。
(2)模型微调(finetune):
这段代码呢,是一个用于微调(fine-tuning)预训练的BERT模型的Python脚本,目的是对文本数据进行情绪分类。这段代码也是整个模型的核心部分。
-
导入依赖:导入了必要的Python库,包括用于文件操作、日志记录、深度学习、数据处理和模型训练的工具。
-
AverageMeter 类:定义了一个用于计算和存储平均值和当前值的工具类。
-
WBDataset 类:自定义的PyTorch
Dataset
类,用于加载和处理训练数据。它从JSON文件中读取数据,使用指定的分词器进行分词,并将数据转换为模型的输入格式。 -
Trainer 类:
- 初始化方法:设置训练所需的参数,包括数据加载器、模型、学习率、训练轮数、输出目录等。
train_epoch
方法:执行一个训练周期,包括前向传播、反向传播、优化器更新和学习率调整。on_epoch_finish
方法:在每个训练周期结束时评估模型,并根据验证集上的性能保存最佳模型。_save_checkpoint
方法:保存模型的检查点,包括模型参数、优化器和学习率调度器的状态。train
方法:启动训练过程,并记录训练和验证过程中的性能。_val
方法:在验证集上评估模型的性能,计算损失、准确率和F1分数。
-
parse_args 函数:使用
argparse
解析命令行参数,设置模型训练的配置选项。 -
主函数:
- 根据解析的参数创建模型实例。
- 加载训练和验证数据集,并使用
DataLoader
对象进行封装,以便进行批量处理和多线程加载。 - 创建
Trainer
实例并调用其train
方法开始训练过程。
-
日志记录:使用
loguru
库进行日志记录,记录训练过程中的关键信息。 -
TensorBoard 集成:使用
SummaryWriter
将训练和验证过程中的损失、准确率和F1分数记录到TensorBoard。 -
模型保存:在训练过程中,根据验证集上的性能,周期性地保存模型的检查点,并在性能提升时保存最佳模型。
-
评估指标:使用
sklearn.metrics
中的f1_score
和accuracy_score
计算模型的性能。
总结来说,这段代码为我们提供了一个完整的流程,从数据加载、模型训练、性能评估到模型保存,用于该六分类情绪分类任务的BERT模型微调。
(3)模型推理(inference):
这段代码使用了预训练的序列分类模型进行推理(inference)。
-
首先,脚本导入了必要的库:
torch
:用于深度学习模型的PyTorch框架。argparse
:用于解析命令行参数。numpy
:用于数值计算的库。transformers
:Hugging Face提供的模型库,用于加载预训练的Transformer模型。
-
定义了一个
parse_args
函数,该函数使用argparse
库解析命令行参数。这些参数包括:--input
:输入文本。--device
:指定模型运行的设备,可以是CPU或GPU。--model_name
:指定使用的预训练模型的名称。--model_path
:模型的检查点文件路径。--num_labels
:分类任务的标签数量。
-
main
函数是脚本的主要执行函数:- 使用
parse_args
获取命令行参数。 - 根据
--device
参数设置设备(CPU或GPU)。 - 加载预训练的模型,使用
AutoModelForSequenceClassification.from_pretrained
方法,同时指定num_labels
参数。 - 加载模型的检查点文件,使用
torch.load
方法,并将模型的状态字典加载到模型中。 - 打印出加载模型时缺失的键(
missing_keys
)和意外的键(unexpected_keys
)。 - 定义了一个标签字典
label
,用于将模型输出的索引映射到具体的类别。 - 使用
AutoTokenizer.from_pretrained
方法加载与模型对应的分词器。 - 对输入文本进行分词,设置最大长度为140,并将其转换为模型的输入格式。
- 将模型设置为评估模式,并将模型和输入数据移动到指定的设备上。
- 在不计算梯度的情况下,使用模型进行推理,并获取输出。
- 将输出的logits转换为NumPy数组,并使用
np.argmax
函数找到概率最高的类别索引。 - 打印出预测结果,使用标签字典将索引转换为具体的类别名称。
- 使用
-
脚本的最后一行检查是否直接运行该脚本,如果是,则调用
main
函数。
总的来说,这段代码加载一个预训练的序列分类模型(也就是上文我们处理微调过的Transformer模型),对输入文本进行分类并打印出预测的类别。代码中使用了PyTorch和Hugging Face的Transformers库(这个库的官网无法访问,呜呜,但是我通过另一种方法解决了,写在下文)来实现模型的加载、推理和结果输出。
(4)模型测试(Test)
这段代码就是测试用的。
-
导入依赖:导入了PyTorch、argparse、NumPy、tqdm、loguru、自定义的
WBDataset
类、DataLoader
、sklearn的评估指标以及Hugging Face的Transformers库。 -
解析命令行参数:使用
argparse
定义并解析命令行参数,包括模型名称、模型路径、标签数量、测试数据路径、批处理大小和数据加载器的工作线程数。 -
加载模型和检查点:使用
AutoModelForSequenceClassification.from_pretrained
加载预训练的BERT模型,然后加载保存的检查点到模型中,并记录缺失和多余的键。 -
数据加载:使用自定义的
WBDataset
类加载测试数据,并使用DataLoader
封装数据,以便进行批处理和多线程加载。 -
模型评估:将模型设置为评估模式,并将模型和数据移动到GPU上(如果可用)。使用
DataLoader
迭代测试数据,并在没有梯度计算的情况下进行预测。 -
性能评估:计算并记录测试集上的损失、准确率和F1分数。使用
accuracy_score
和f1_score
从sklearn库评估模型性能。 -
日志记录:使用
loguru
库记录关键信息,包括加载检查点的状态和测试集的性能指标。 -
主函数:定义了
main
函数,执行上述步骤,并在脚本作为主程序运行时调用。 -
进度条:使用
tqdm
显示测试过程中的进度条。 -
输出结果:在日志中输出测试集上的F1分数、准确率和平均损失。
总之,这段代码提供了一个完整的流程,用于在给定的测试数据集上评估预训练BERT模型的性能。代码结构还是比较清晰的,易于我们理解和使用。
以上就是我对该模型源码的一些简单解读解释,因为源码实在有点多,我就不放在这上面了,大家可以进这个项目后直接查看即可,特别方便。
三、项目过程
这里的话我就大概和大家讲一讲我在项目实践中遇到的一些问题以及相关解决方法了。
(1):环境配置
因为这是我的第一个项目模型,之前根本没接触过,更别谈什么给它配运行环境了。
我记得很清楚,第一个遇到的问题就是虚拟环境的创建!刚开始的时候我根本不知道还有虚拟环境这一说法,只知道把需要的库或者软件包直接下载安装到本地,我滴妈,然后就是各种报错和下载安装失败。后来学长建议我配置到一个虚拟环境我才知道还有Anaconda这个好东西。
你以为知道用Anaconda配置虚拟环境就行了?NONONO。其中出现了新的一系列问题:
- 虚拟环境配置C盘导致"内存爆炸"问题!
- Pycharm无法识别Conda可执行文件问题(其实就是新版本文件后缀改了,给我搞错了,可恶)
- 各种各样的软件包,框架和Python语言之间的适配和版本兼容问题(想到这里我就想哭,第一次的时候,python选的太新了,因为我没看项目上给出的环境推荐,结果差不多搞完之后好像是一个
pyhanlp
的兼容出了问题导致我全部重搞,哭) - Anaconda文件位置以及安装软件包和库的默认存储位置
关于这里的环境配置我也写了相关文章,大家可以进入我主页查看我以往的文章。
(2):Transformer模型无法线上引入问题
[HuggingfaceTransformer−Model][Huggingface_Transformer-Model][HuggingfaceTransformer−Model]
(https://mirrors.aliyun.com/huggingface/models/bert−base−chinese/)(https://mirrors.aliyun.com/huggingface/models/bert-base-chinese/)(https://mirrors.aliyun.com/huggingface/models/bert−base−chinese/)
(3):\或者/
斜杠问题
我记得关于\
的问题是因为在文件的位置存储的时候,\
加上后面的字母被Pycharm自动识别成转义符号了,所以以后哦要注意此类问题。
(4):关于模型部分代码的生疏:
简单基础的python代码我能够大概了解了,比如下面这个数据清洗的过程:
import json
from tqdm import trange
from harvesttext import HarvestText
import pyhanlp
import re
import os
def read_json(file):
with open(file, 'r', encoding='utf-8') as f:
data = json.load(f)
print('%s -> data over' % file)
return data
def save_json(data, file, indent=1):
with open(file, 'w', encoding='utf-8') as f:
f.write(json.dumps(data, indent=1, ensure_ascii=False))
print('data -> %s over' % file)
def remove_url(src):
vTEXT = re.sub(r'(https|http)?://(\w|.|/|?|=|&|%)*\b', '', src, flags=re.MULTILINE)
return vTEXT
# file: 数据文件
def clean_text(file, save_dir):
ht = HarvestText()
CharTable = pyhanlp.JClass('com.hankcs.hanlp.dictionary.other.CharTable')
data = read_json(file)
num_null = 0
cleaned_data = []
for i in trange(len(data)):
content = CharTable.convert(data[i]['content'])
cleaned_content = remove_url(ht.clean_text(content, emoji=False)) # 过滤@后最多6个字符
num_null += 1 if cleaned_content == '' else 0
if 'train' in file and (not content or not cleaned_content): # 删除train中的自带的空数据或清洗后出现的空数据
continue
if 'eval' in file or 'test' in file:
cleaned_data.append({'id':data[i]['id'], 'content':cleaned_content, 'label':data[i]['label']})
else:
cleaned_data.append({'id':data[i]['id'], 'content':cleaned_content, 'label':data[i]['label']})
filename = file.split('/')[-1]
save_json(cleaned_data, os.path.join(save_dir, filename))
print('num data: ', num_null)
clean_text('path', 'path-no-object')
clean_text('path', 'path-no-object')
clean_text('path', 'path-no-object')
但是,关于涉及到模型操作之类的代码我还需要进一步了解,比如transformer的池化层之类的,还得加油。
(5):模型过拟合与F1_Score值 较低问题
因为我的电脑跑不了这个模型并且我对服务器上跑模型不熟悉,所以我将全部的代码修改好之后让学长帮我挂在服务器上跑了一下,得出的F1只有0.7多,我对此保持过拟合的怀疑
四、模型核心代码
在这里我把模型的核心代码抽取出来和大家讲解讲解,其实也就是微调部分的核心代码
代码如下:
def parse_args():
parser = argparse.ArgumentParser(description='Bert fine-tune on SMP2020-EWECT Dataset')
parser.add_argument('--model_name', default='D:/Conda-pkgs/emtional-analyse-pkgs/bert-base-chinese', type=str,
help='huggingface transformer model name')
parser.add_argument('--num_labels', default=6, type=int, help='fine-tune num labels')
parser.add_argument('--train_data_path', default='D:/pythonpro/Emotion-analyse/Date-Total/Clean-data/usual_train.txt', type=str, help='train data path')
parser.add_argument('--val_data_path', default='D:/pythonpro/Emotion-analyse/Date-Total/Clean-data/usual_eval_labeled.txt', type=str,
help='train data path')
parser.add_argument('--batch_size', default=64, type=int, help='train and validation batch size')
parser.add_argument('--dataloader_num_workors', default=8, type=int, help='pytorch dataloader num workers')
parser.add_argument('--lr', default=1e-5, type=float, help='train learning rate')
parser.add_argument('--epochs', default=30, type=int, help='train epochs')
parser.add_argument('--output_dir', default='workspace/wb', type=str, help='save dir')
parser.add_argument('--save_model_iter', default=5, type=int, help='save model num epochs on training')
args = parser.parse_args()
return args
这段代码使用了argparse
库来解析命令行参数,通常用于配置机器学习模型训练过程中的各种参数。
-
def parse_args():
- 定义了一个名为parse_args
的函数,这个函数没有参数。 -
parser = argparse.ArgumentParser(description='Bert fine-tune on SMP2020-EWECT Dataset')
- 创建了一个ArgumentParser
对象,用于解析命令行参数。description
参数提供了一个描述,说明了这个脚本是用来对SMP2020-EWECT数据集进行BERT模型的微调。(后面改成本地外界模型的话,这里也需要修改) -
parser.add_argument(...)
- 这是一个循环调用,向ArgumentParser
对象添加了多个参数:--
开头的字符串是参数的名称。default
是参数的默认值。type
指定了参数应该被解析成的数据类型。help
提供了关于参数的简短描述。
-
parser.add_argument('--model_name', default='D:/Conda-pkgs/emtional-analyse-pkgs/bert-base-chinese', type=str, help='huggingface transformer model name')
- 添加了一个名为--model_name
的参数,其默认值为一个文件路径,表示使用的是bert-base-chinese
模型。 -
parser.add_argument('--num_labels', default=6, type=int, help='fine-tune num labels')
- 添加了一个名为--num_labels
的参数,其默认值为6,表示微调时使用的标签数量。 -
parser.add_argument('--train_data_path', default='D:/pythonpro/Emotion-analyse/Date-Total/Clean-data/usual_train.txt', type=str, help='train data path')
- 添加了一个名为--train_data_path
的参数,其默认值为一个文件路径,表示训练数据的文件位置。 -
parser.add_argument('--val_data_path', default='D:/pythonpro/Emotion-analyse/Date-Total/Clean-data/usual_eval_labeled.txt', type=str, help='train data path')
- 这个参数名似乎应该是--val_data_path
,表示验证数据的路径。但是,它的帮助描述是train data path
,这可能是一个错误,应该改为validation data path
。 -
parser.add_argument('--batch_size', default=64, type=int, help='train and validation batch size')
- 添加了一个名为--batch_size
的参数,其默认值为64,表示训练和验证时每个批次的样本数量。 -
parser.add_argument('--dataloader_num_workors', default=8, type=int, help='pytorch dataloader num workers')
- 添加了一个参数,其默认值为8,表示PyTorch的DataLoader
使用的子进程数量。 -
parser.add_argument('--lr', default=1e-5, type=float, help='train learning rate')
- 添加了一个名为--lr
的参数,其默认值为1e-5
,表示训练时的学习率。 -
parser.add_argument('--epochs', default=30, type=int, help='train epochs')
- 添加了一个名为--epochs
的参数,其默认值为30,表示训练的轮数。 -
parser.add_argument('--output_dir', default='workspace/wb', type=str, help='save dir')
- 添加了一个名为--output_dir
的参数,其默认值为workspace/wb
,表示模型保存的目录。 -
parser.add_argument('--save_model_iter', default=5, type=int, help='save model num epochs on training')
- 添加了一个参数,其默认值为5,表示在训练过程中每过5个epoch保存一次模型。 -
args = parser.parse_args()
- 使用parse_args
方法解析命令行输入的参数,并将解析结果赋值给变量args
。 -
return args
- 函数返回args
对象,它包含了所有的命令行参数。
这个函数通常在Python脚本的开始部分被调用,并且其返回值被用来获取用户通过命令行指定的各种配置参数。
五、总结:
总的来说,项目实践过程并不顺利,一方面是因为是第一次全新的,自己完成这个情感分析模型,光个环境配置就花费了我一个星期琢磨,就是各种报错,从源文件受损到兼容问题,各种不行,后面还是在我瑞哥的倾力帮助下得以解决。这里我很感谢瑞哥,因为大家想想看,笔者一个人琢磨了一个星期,都没能很好地解决,本身还有学业比赛等其他压力,所以整个人都很低沉,没想到瑞哥会手把手一点点很耐心地帮助我排雷,梳理,重整,真的,当时和一束光似的。在模型修改整理过程中,遇到的问题基本上都是我一个人解决的,所以经过这次我也收获了很多。整个项目耗时接近两个月,虽然结果并不美好,但也让我收获很多,无论是技术上还是生活上。
Reference:
2.百度,维基,必应百科
3.数据集来源于项目自身提供的Date
以上就是我的第一个情感分析NLP模型项目,可能存在瑕疵,欢迎大家点赞,收藏和交流指正,O(∩_∩)O谢谢!
转载自:https://juejin.cn/post/7377635432966111241