图1:作者提供的图片,显示了Python情感分析工具的输出结果。
简介本文通过代码生成实验来说明人与AI合作的重要性,强调当人类的专业知识与AI结合时所达到的协同效应。通过提供有针对性的知识和上下文,可以使模型输出更好地导向实际解决方案。这种协作动态展示了人类直觉和AI支持如何相互补充,共同发挥作用。
文章的核心内容是来自Python和C++代码生成实验的关键见解,展示了人类专业知识如何显著提升AI任务的性能。
实验最近我读了安德鲁·雷根等人2016年发表的论文《一种用于提取书面故事中读者感知情感内容的情绪分析工具》。巧合的是,我刚刚写了一篇短篇小说,并好奇地用它来测试这些算法。这也是评估ChatGPT代码生成能力(该能力已显著提高)的绝佳机会。我定期评估大型语言模型,以跟踪它们的进步,这些实验提供了宝贵的洞见。
这也是分享一些经验教训的好机会。在最近的一篇发表在 Substack 上的文章中,我讨论了如何采用了一种协作的、多轮提示的方法来编写有效的 Python 代码,用于一个简单的文本情感分析工具。与其让 ChatGPT 在一次提示中处理所有内容,不如通过迭代——逐步审查其输出,提供反馈,并逐步改进响应——来提高效率,反复这样做。您可以在下面的链接中找到更多关于该实验的细节。
ChatGPT o1-preview代码生成功能的看法 (substack.com)
我使用C++进行了一项类似的实验,最近让ChatGPT o1-preview根据我之前的Python实验生成一个情感分析工具。本文提供了一个基于这两个实验的轶事比较以及从中获得的见解。
虽然两个实验都成功了,但细节有所不同。Python代码生成过程中不需要调试,只需稍微整理一下代码。尽管我还需要指导其算法和API的选择。相比之下,C++代码生成过程不仅需要更多指导,还要进行调试和清理代码。
这种差异并不令人惊讶——大量的Python代码可供大型语言模型训练。与机器学习相关的Python代码还受益于大量开源模块的存在。例如,ChatGPT常常倾向于使用NLTK进行Python文本分词,并且在使用LangChain集成OpenAI API方面也很熟练。
ChatGPT生成的C++版本没有使用外部的分词器;相反,它自己实现了分词功能。然而,ChatGPT使用了外部的C++组件来与OpenAI API进行交互。最终,我让它调整应用程序的架构,以尽量减少所需的外部C++包的数量。
在这两种情况下,通过与模型进行逐步对话能够帮助细化需求,澄清模糊点,并通过逐步改进代码,使过程更加优化。
讨论部分将对比来自Python和C++代码生成实验的发现
Python 练习图1展示了我11000字的科幻短篇小说中情感变化。根据这个工具,整体的情感更倾向于“负面”而不是“正面”。
情绪的负面程度、正面程度以及戏剧性强度是通过与预定义的“情感锚点”嵌入的语义相似性来衡量的。我们使用的这些“情感锚点”如下:
- ‘戏剧性’:“我感到非常戏剧化,兴奋,充满了肾上腺素。”
- ‘积极’:“我今天感觉超级开心和快乐!”
- ‘中性’:“天气一般般,没什么特别的事情发生。”
- ‘消极’:“我今天感觉非常悲伤和沮丧。”
情绪是使用由ChatGPT o1-preview生成的Python脚本计算的(见附录A)。我没有调试代码。在整个代码生成过程中,ChatGPT和我反复讨论和完善了算法和设计(见附录B)。
这两个实验中的文本相似性是基于文本窗口内容和情感基准点之间的Cosine相似性计算。
在Python实验中,计算这些情感或情绪的Python脚本是由ChatGPT(o1-preview)生成的,如附录A所示。虽然我不需要调试代码,但ChatGPT和我通过迭代不断优化算法和代码设计,具体过程见附录B中的提示。
C++ 练习图2展示了C++程序的输出。我没让它画和图1一样的细节,但结果是一样的。不过架构上有明显的不同;它不是一个单一的程序或可执行文件。前端的C++情感分析部分通过REST接口与单独的Python进程进行通信。
图2. C++情感分析工具的输出结果,作者提供的图片。
我最初让ChatGPT使用一个提示来生成C++代码,这个提示整合了我们在之前交流中创建Python工具时得出的诸多结论。其中许多元素已在附录B中列出。
以下是最初我给ChatGPT的提示,用于生成一个C++情感工具。
提出一个基于OpenAI嵌入和滑动窗口的情感分析C++程序。
例如,它可能希望以以下方式获取文档的嵌入:
embeddings_model = OpenAIEmbeddings(openai_api_key=openai_api_key)
texts = [doc.page_content for doc in documents]
embeddings = embeddings_model.embed_documents(texts)
应使用输入文本的嵌入向量与表示关键情感(例如积极、中性和消极)的预定义嵌入向量之间的距离。这种方法利用语义相似性,根据输入文本的嵌入向量与预定义“情感锚点”嵌入向量之间的距离来分类情感。
构建块算法步骤如下,每份文档:
对文档中的文本进行分词。
创建滑动窗口。
利用大型语言模型为每个窗口生成嵌入。
对每个窗口进行情感分析。
可选的情感平滑处理。
将情感结果可视化为表示整个文档的图表。
设计指南:
程序应使用C++语言编写。
程序应调用一个REST Python服务,该服务使用Python模块实现分词器并获取OpenAI嵌入模型。
约束条件:
不得使用libcurl。
不得使用Boost和Boost.Asio
请注意,附录B中的提示不包括最后两个部分,“设计指南”和“约束”。这些是新增内容。我特别吩咐ChatGPT使用Python服务来整合OpenAI API,通过HTTP/REST与C++客户端连接,而不是尝试从C++客户端直接通过libcurl或Boost来整合。
在之前的实验中,除非另有说明,ChatGPT通常会生成使用libcurl或Boost.Asio进行HTTP(S)请求的C++代码。配置libcurl或Boost比使用Python自带的功能和模块要复杂得多。例如,使用libcurl时,需要为Windows专门构建其库文件。为了简化这次实验,我决定让C++客户端通过Python服务来调用OpenAI API。
图3描述了ChatGPT生成的用于实现C++情感分析工具的架构。情感分析程序(客户端)通过HTTP/REST与Python服务进行通信,该服务用于大型语言模型的嵌入操作。Python RESTful服务使用Python网络资源与OpenAI进行模型嵌入的通信操作。C++客户端将结果输出到CSV文件。ChatGPT生成了一个Python脚本,将这些结果以图形形式展示出来。ChatGPT还提供生成C++ 使用Qt可视化程序的选项(如果我进一步要求,可能还有其他选项)。但为了使这个实验保持简单,我选择了Python脚本。
图3. 作者提供的图像。C++情感分析工具应用架构。该架构依赖于一个REST风格的Python服务来获取嵌入。
使用 Microsoft Visual Studio Community 2022 版本 17.11.5 构建并运行了 C++ 客户端程序。在 PyCharm 2024.1(社区版)的虚拟环境中运行了 Python 服务端。
图4. 作者提供的图片。要运行C++情感分析工具时,需要同时运行后端的Python服务程序和C++客户端程序。使用了Visual Studio和PyCharm开发工具。
正如所述,我需要对生成的C++代码进行“代码调整”才能编译、链接和执行它。以下是主要遇到的问题。
- 代码与不同版本的 C++ 兼容性问题。本实验的代码是根据 ISO C++20 标准编译的。
- 由于不同版本的 C++ 在模板实现上的差异、我实验中使用的特定外部模块的不同版本、我处理的生成代码的不同版本以及所使用的 C++ 编译器版本的不同导致了不兼容性。
换个轻松点的话题,ChatGPT o1-preview 还挺有同理心的。下面就是一个例子,你可以看看它是怎么回应的。
讨论一下听到这个消息真遗憾你还在遇到与
stdext::checked_array_iterator<char*>
相关的警告
…
实验表明,在与AI协作生成代码时采用迭代、多种提示方法的重要性。这种方法能够优化算法、识别歧义以及提升生成代码的整体质量。
一个关键的洞察是,采用互动和迭代的方法比依赖单一详尽指令更能生成可用的代码。在Python的例子中,我通过多次迭代引导ChatGPT生成了一个符合我需求的算法。这种逐步细化的过程让代码更贴近我的目标。
同样,在C++的例子中,迭代对于解决代码错误和集成问题来说是必不可少的。这些发现说明,与模型进行互动,能够实现实用且功能完整的代码,而不是期望一开始就能获得完整的解决方案。
另一个重要的发现是,你事先了解的或在过程中掌握的知识——无论是通过经验还是其他来源——都可以让你更快地完善你的提示,从而帮助大型语言模型更快找到一个好的解决方案。在Python情感分析工具的例子中,我对相关算法和API的熟悉使我能够帮助ChatGPT迅速找到一个干净且有效的实现方法。
在C++版本中,我带领ChatGPT避开那些与之合作很费时间的外部模块,并建议了一个替代方案。ChatGPT遵循了我的建议,最终交付了一个基于Python的REST服务,填补了C++复杂性和Python API集成便捷性之间的鸿沟。这展示了人类与AI的协作的一个实际例子。通过引导AI进入更高效的架构,避免了不必要的麻烦,从而实现了简单而令人满意的解决方案。
参考资料如下:Reagan, A.J., Mitchell, L., Kiley, D. 等, 故事的情感变化曲线主要由六种基本模式构成。 EPJ数据科学. 5 , 31 (2016). https://doi.org/10.1140/epjds/s13688-016-0093-1
附录 A由ChatGPT生成的用于简单情感分析的Python代码。
导入 os
导入 numpy as np
导入 matplotlib.pyplot as plt
从 sklearn.metrics.pairwise 导入 cosine_similarity
导入 nltk
# 从 langchain 导入必要的类
从 langchain.embeddings 导入 OpenAIEmbeddings
从 langchain.document_loaders 导入 TextLoader
从 langchain.text_splitter 导入 NLTKTextSplitter
# 下载 NLTK 数据文件(仅需运行一次)
nltk.download('punkt')
# 设置 OpenAI API 密钥
openai_api_key = ""
# 指定文本文件的路径
file_paths = [
"./data/cyclops_storm.txt",
"./data/Wee-kh-sah_38.txt"
]
# 定义读取和分割文本文件的函数
def do_read_text_files(file_paths):
documents = []
for file_path in file_paths:
loader = TextLoader(file_path, encoding='utf-8')
raw_documents = loader.load()
for document in raw_documents:
content = document.page_content
metadata = document.metadata
print(f'文件: {file_path}, page_content 长度={len(content)}')
text_splitter = NLTKTextSplitter(chunk_size=1000, chunk_overlap=100)
text_content = " ".join([doc.page_content for doc in raw_documents])
text_sections = text_splitter.split_text(text_content)
print(f'文件: {file_path}, 分割的文本部分数量={len(text_sections)}')
documents.append({
'file_path': file_path,
'text_sections': text_sections
})
return documents
# 定义生成嵌入的函数
def generate_embeddings(texts, openai_api_key):
embeddings_model = OpenAIEmbeddings(openai_api_key=openai_api_key)
embeddings = embeddings_model.embed_documents(texts)
return embeddings
# 读取并分割文本文件为部分
documents = do_read_text_files(file_paths)
# 定义扩展的情绪锚文本并获取它们的嵌入
sentiment_texts = {
'dramatic': "我感觉戏剧化,兴奋,处于肾上腺素激增状态。",
'positive': "今天我感觉非常开心和愉快!",
'negative': "今天我感觉非常悲伤和消沉。"
}
# 为情绪锚生成嵌入
sentiment_embeddings = {}
for sentiment, text in sentiment_texts.items():
embedding = generate_embeddings([text], openai_api_key)[0]
sentiment_embeddings[sentiment] = embedding
# 逐个处理每个文档
for doc in documents:
file_path = doc['file_path']
text_sections = doc['text_sections']
# 为每个文本部分生成嵌入
window_embeddings = generate_embeddings(text_sections, openai_api_key)
# 计算每个情绪对每个文本部分的相似度
similarity_scores = {sentiment: [] for sentiment in sentiment_embeddings.keys()}
for embedding in window_embeddings:
for sentiment, sentiment_embedding in sentiment_embeddings.items():
similarity = cosine_similarity([embedding], [sentiment_embedding])[0][0]
similarity_scores[sentiment].append(similarity)
# 将相似度可视化为表示整个文档的图
plt.figure(figsize=(12, 6))
for sentiment, scores in similarity_scores.items():
plt.plot(scores, marker='o', linestyle='-', label=sentiment)
plt.title(f'情绪相似度在文档中: {os.path.basename(file_path)}')
plt.xlabel('文本部分索引')
plt.ylabel('余弦相似度分数')
plt.xticks(range(len(text_sections)))
plt.legend()
plt.grid(True)
plt.show()
附录二
本附录列出了我用来指导ChatGPT o1-preview生成情感分析工具的过程。它强调了结合了高层次的概念性提示和具体的技术反馈,包括代码、API和算法等方面的内容。
提出一种基于LLM嵌入和滑动文本窗口的情感分析算法。
===
提出一个简单的情感分类层实现。
===
你能否在不使用标注的情感数据和前馈分类层的情况下,计算输入文本的嵌入与表示关键情感(如积极、中性和消极)的嵌入之间的距离?
===
基于使用OpenAI嵌入和应用于文本文档的滑动文本窗口,提出一种情感分析Python程序。
例如,可能需要以如下方式获取文档的嵌入:
embeddings_model = OpenAIEmbeddings(openai_api_key=openai_api_key)
texts = [doc.page_content for doc in documents]
embeddings = embeddings_model.embed_documents(texts)
应该使用输入文本的嵌入与预定义的情感锚点嵌入接近程度来分类情感。
每个文档的构建算法步骤包括:
文档分词。
创建滑动窗口。
使用LLM生成每个窗口的嵌入。
对每个窗口进行情感分类。
可选的情感平滑处理。
将整个文档的情感可视化为图。
===
是否可以更新代码以使用OpenAI 1.0.0 API,示例如下。def generate_embeddings(documents, openai_api_key):
embeddings_model = OpenAIEmbeddings(openai_api_key=openai_api_key)
texts = [doc.page_content for doc in documents]
embeddings = embeddings_model.embed_documents(texts)
for i, document in enumerate(documents):
document.metadata['embedding'] = embeddings[i]
return documents
===
是否可以重构代码,使其使用以下文本读取器?def do_read_text_file(file_path):
loader = TextLoader(file_path, encoding='utf-8')
raw_documents = loader.load()
for document in raw_documents:
content = document.page_content
metadata = document.metadata
print(f'内容长度:{len(content)}')
text_splitter = NLTKTextSplitter(chunk_size=1000, chunk_overlap=100)
text_content = " ".join([doc.page_content for doc in raw_documents])
text_sections = text_splitter.split_text(text_content)
print(f'拆分为文本块的数量:{len(text_sections)}')
return text_sections
===
是否可以更新代码以读取并绘制多个文档(多个文件路径)的输出?
===
如果我希望扩展情感,包括当前三个情感之外的一些重叠情感,你将如何展示它们?参考以下示例列表。sentiment_texts = {
'dramatic' : "我感到非常戏剧化,兴奋,充满肾上腺素。",
'non-dramatic' : "我感到无聊,缺乏活力,没有能量。",
'cormac' : "像科马克·麦卡锡的风格",
'margaret' : "像玛格丽特·阿特伍德的风格",
'king' : "像斯蒂芬·金的风格",
'positive': "我今天感到非常快乐和愉快!",
'neutral': "天气一般,没有什么特别的事情发生。",
'negative': "我今天感到非常悲伤和沮丧。"
}
附录C:
ChatGPT生成的用于简单情绪分析的C++客户端代码。与附录A中的Python脚本不同的是,C++客户端则依赖于一个单独的Python服务来生成嵌入(见附录D)。请参见图3了解应用程序架构。
// main.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
#include <thread>
#include <mutex>
#include <httplib.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// 读取整个文本文件到字符串
std::string read_text_file(const std::string& filename) {
std::ifstream infile(filename);
if (!infile.is_open()) {
throw std::runtime_error("无法打开文件:" + filename);
}
std::stringstream buffer;
buffer << infile.rdbuf();
return buffer.str();
}
// 将文本分割成单词(简单的分词器)
std::vector<std::string> tokenize_text(const std::string& text) {
std::istringstream stream(text);
std::vector<std::string> tokens;
std::string word;
while (stream >> word) {
tokens.push_back(word);
}
return tokens;
}
// 从标记中创建滑动窗口
std::vector<std::string> create_sliding_windows(const std::vector<std::string>& tokens, size_t window_size, size_t overlap) {
std::vector<std::string> windows;
size_t step = window_size - overlap;
for (size_t i = 0; i < tokens.size(); i += step) {
size_t end = std::min(i + window_size, tokens.size());
std::string window_text = "";
for (size_t j = i; j < end; ++j) {
window_text += tokens[j] + " ";
}
windows.push_back(window_text);
if (end == tokens.size()) break;
}
return windows;
}
// 从REST服务获取嵌入
std::vector<float> get_embedding(const std::string& text) {
httplib::Client cli("localhost", 8000); // 根据实际情况调整主机和端口
json request_json = { {"text", text} };
auto res = cli.Post("/embed", request_json.dump(), "application/json");
if (res && res->status == 200) {
json response_json = json::parse(res->body);
return response_json["embedding"].get<std::vector<float>>();
}
else {
// 处理异常
std::cerr << "HTTP状态:" << res->status << "\n";
std::cerr << "响应体:" << res->body << "\n";
throw std::runtime_error("无法获取REST服务的嵌入");
}
}
// 计算余弦相似度
float cosine_similarity(const std::vector<float>& A, const std::vector<float>& B) {
float dot_product = 0.0f;
float norm_a = 0.0f;
float norm_b = 0.0f;
for (size_t i = 0; i < A.size(); ++i) {
dot_product += A[i] * B[i];
norm_a += A[i] * A[i];
norm_b += B[i] * B[i];
}
return dot_product / (std::sqrt(norm_a) * std::sqrt(norm_b) + 1e-8); // 添加小值以防止除零
}
// 根据相似度分配情感
std::string assign_sentiment(const std::vector<float>& embedding,
const std::vector<float>& positive_anchor,
const std::vector<float>& neutral_anchor,
const std::vector<float>& negative_anchor) {
float sim_positive = cosine_similarity(embedding, positive_anchor);
float sim_neutral = cosine_similarity(embedding, neutral_anchor);
float sim_negative = cosine_similarity(embedding, negative_anchor);
if (sim_positive >= sim_neutral && sim_positive >= sim_negative) {
return "Positive";
}
else if (sim_negative >= sim_positive && sim_negative >= sim_neutral) {
return "Negative";
}
else {
return "Neutral";
}
}
int sentiment_main() {
try {
// 步骤1:读取文件
std::string filename = "C:/Users/natha/source/repos/data/Wee-kh-sah_38.txt"; // 用文件名替换
std::string text = read_text_file(filename);
// 步骤2:分词文本
auto tokens = tokenize_text(text);
// 步骤3:创建滑动窗口
size_t window_size = 200; // 调整窗口大小
size_t overlap = 50; // 调整重叠大小
auto windows = create_sliding_windows(tokens, window_size, overlap);
// 步骤4:获取情感锚点的嵌入
std::vector<float> positive_anchor = get_embedding("I am happy");
std::vector<float> neutral_anchor = get_embedding("It is a day");
std::vector<float> negative_anchor = get_embedding("I am sad");
// 线程安全访问共享数据的互斥量
std::mutex mtx;
// 存储结果的向量
std::vector<std::string> sentiments(windows.size());
std::vector<float> positions(windows.size());
// 步骤5:处理每个窗口(使用多线程)
std::vector<std::thread> threads;
for (size_t idx = 0; idx < windows.size(); ++idx) {
threads.emplace_back([&, idx]() {
// 获取窗口的嵌入
auto embedding = get_embedding(windows[idx]);
// 分配情感
auto sentiment = assign_sentiment(embedding, positive_anchor, neutral_anchor, negative_anchor);
// 存储结果
std::lock_guard<std::mutex> lock(mtx);
sentiments[idx] = sentiment;
positions[idx] = static_cast<float>(idx);
});
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
// 可选平滑处理
/*
// 取消注释以启用平滑处理
for (size_t i = 1; i < sentiments.size() - 1; ++i) {
if (sentiments[i - 1] == sentiments[i + 1]) {
sentiments[i] = sentiments[i - 1];
}
}
*/
// 步骤6:输出结果到CSV以便可视化
std::ofstream outfile("sentiment_results.csv");
outfile << "Position,Sentiment\n";
for (size_t i = 0; i < sentiments.size(); ++i) {
outfile << positions[i] << "," << sentiments[i] << "\n";
}
outfile.close();
std::cout << "情感分析完成,结果保存在'sentiment_results.csv'文件中.\n";
}
catch (const std::exception& ex) {
std::cerr << "错误:" << ex.what() << "\n";
}
return 0;
}
附录D
以下是英文到中文的翻译结果:
由ChatGPT生成的用于情感分析的C++客户端代码(见附录C),依赖于一个Python服务来提供必要的embeddings(嵌入)。如下是Python服务。请参阅图3所示的应用架构。
值得一提的是,这个实现是基于FastAPI,而不是Flask。这篇文章提供了更多信息,并对比了这两个框架之间的差异。
# app.py
# uvicorn sentiment_server:app --host 0.0.0.0 --port 8000
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from langchain.embeddings import OpenAIEmbeddings
import os
app = FastAPI()
# 设置你的OpenAI API密钥
openai_api_key = os.getenv("OPENAI_API_KEY")
# 或者你可以直接使用这个API密钥
openai_api_key = "api-key"
# 使用指定的模型初始化嵌入模型
try:
embeddings_model = OpenAIEmbeddings(model="text-embedding-ada-002", openai_api_key=openai_api_key)
except Exception as e:
print(f"初始化OpenAIEmbeddings时发生错误: {e}")
raise
class TextData(BaseModel):
text: str
@app.post("/embed")
async def embed_text(data: TextData):
try:
# 用于单个文本输入的embed_query
embedding = embeddings_model.embed_query(data.text)
return {"embedding": embedding}
except Exception as e:
print(f"嵌入过程中出现异常: {e}")
raise HTTPException()
附录E
ChatGPT生成的C++客户端(见附录C),用于简单的情感分析,输出一个包含情感分析结果的CSV文件。下面给出了用于可视化CSV文件的Python工具。见图2为该工具的输出。见图3为系统架构。
# 可视化.py
import pandas as pd
import matplotlib.pyplot as plt
# 读取CSV文件
df = pd.read_csv('sentiment_results.csv')
# 将情感映射到数值以便绘图
sentiment_map = {'Positive': 1, 'Neutral': 0, 'Negative': -1}
df['SentimentScore'] = df['Sentiment'].map(sentiment_map)
# 绘制情感得分
plt.figure(figsize=(12, 6))
plt.plot(df['Position'], df['SentimentScore'], marker='o')
plt.title('文档中的情感分析图')
plt.xlabel('位置')
plt.ylabel('情感分数')
plt.yticks([-1, 0, 1], ['负面', '中立', '正面'])
plt.grid(True)
plt.show()