更新pyproject.toml以调整类型检查配置,修改README.md以改善数据预处理说明,重构settings.json以优化数据集参数,更新qa_generator.py以支持新的消息处理逻辑,删除旧的测试文件test_old_csv_to_json copy.py并更新相关测试用例。

This commit is contained in:
xming521
2025-04-13 19:14:27 +08:00
parent 6eec1ba8c4
commit f46b873f92
7 changed files with 45 additions and 341 deletions
+11
View File
@@ -0,0 +1,11 @@
---
description:
globs:
alwaysApply: true
---
# Your rule content
- You can @ files here
- The project uses uv as the package manager and pyproject.toml as the project configuration file.
+5 -11
View File
@@ -12,8 +12,8 @@
> [!IMPORTANT]
> WeClone仍在快速迭代期,当前效果不代表最终效果
> 微调LLM效果很大程度取决于聊天数据的数量和质量
> WeClone仍在快速迭代期,当前效果不代表最终效果
> 微调LLM效果很大程度取决于聊天数据的数量和质量
### 硬件要求
@@ -50,15 +50,9 @@ uv pip install --group main -e .
### 数据预处理
项目默认去除了数据中的手机号、身份证号、邮箱、网址。还提供了一个禁用词词库[blocked_words](make_dataset/blocked_words.json),可以自行添加需要过滤的词句(会默认去掉包括禁用词的整句)。
执行 `./make_dataset/csv_to_json.py` 脚本对数据进行处理
在同一人连续回答多句的情况下,有三种处理方式:
| 文件 | 处理方式 |
| --- | --- |
| csv_to_json.py | 用逗号连接 |
| csv_to_json-单句回答.py(已废弃) | 只选择最长的回答作为最终数据 |
| csv_to_json-单句多轮.py | 放在了提示词的'history'中 |
项目默认去除了数据中的手机号、身份证号、邮箱、网址。还提供了一个禁用词词库[blocked_words](make_dataset/blocked_words.json),可以自行添加需要过滤的词句(会默认去掉包括禁用词的整句)。
执行 `python ./make_dataset/qa_generator.py` 对数据进行处理,可以根据自己的聊天风格修改settings.json的`make_dataset_args`
目前仅支持时间窗口策略,根据`single_combine_time_window`将单人连续消息通过逗号连接合并为一句,根据`qa_match_time_window`匹配问答对。后续将增加大模型清洗数据的功能。
### 模型下载
+13 -4
View File
@@ -126,6 +126,15 @@ class DataProcessor:
qa_res.append(
{"instruction": current_instruction, "output": msg.msg}
)
else:
if self.c["prompt_with_history"]:
qa_res.append(
CutMessage(
is_sender=msg.is_sender,
cut_type=msg.type_name,
CreateTime=msg.CreateTime,
)
)
# 无论是否匹配,都重置状态
current_state = WAITING_INSTRUCTION
current_instruction = None
@@ -133,7 +142,7 @@ class DataProcessor:
return qa_res
def add_history_to_qa(self, qa_res: List[Dict]):
def add_history_to_qa(self, qa_res: List[Dict]) -> List[Dict]:
qa_res_with_history = []
last_res = {"instruction": "", "output": "", "history": []}
@@ -187,10 +196,10 @@ class DataProcessor:
ChatMessage: 合并后的消息
"""
base_msg = messages[0]
combined_content = messages[0].msg.strip()
combined_content = messages[0].msg
for i in messages[1:]:
content = i.msg.strip()
content = i.msg
if not content:
continue
@@ -343,7 +352,7 @@ class DataProcessor:
def save_result(self, qa_res: List[Dict]):
# 保存结果
with open(
f"./data/res_csv/sft/sft-{self.c['single_combine_strategy']}-{self.c['qa_match_strategy']}-my.json",
f"./data/res_csv/sft/sft-my.json",
"w",
encoding="utf-8",
) as f:
+1 -1
View File
@@ -48,7 +48,7 @@ default = true
[tool.pyright]
typeCheckingMode = "basic"
include = ["src"]
include = ["make_dataset"]
exclude = ["**/archive","**/tests"]
ignore = ["**/archive"]
+6 -6
View File
@@ -44,13 +44,13 @@
},
"make_dataset_args": {
// "enable_vision_model": false,//后续实现
"include_type": [
"文本"
],
// "include_type": [
// "文本"
// ],
"single_combine_strategy": "time_window", // 单人组成单句策略
"qa_match_strategy": "time_window", // 多人组成qa策略
"single_combine_time_window": 10, // 单人组成单句时间窗口(分钟),
"qa_match_time_window": 60, // 多人组成qa时间窗口(分钟),
"qa_match_strategy": "time_window", // 组成qa策略
"single_combine_time_window": 2, // 单人组成单句时间窗口(分钟),
"qa_match_time_window": 5, // 组成qa时间窗口(分钟),
"prompt_with_history": false // 是否在prompt中包含历史对话
},
"common_args": {
-311
View File
@@ -1,311 +0,0 @@
import csv
import json
import os
import sys
import pandas as pd
from collections import deque
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(current_dir)
sys.path.append(root_dir)
from make_dataset.qa_generator import DataProcessor
csv_folder = "./data/csv"
# csv_folder = './data/test'
os.chdir(root_dir)
print(f"当前处理目录{csv_folder}")
def handle_pt_csv(csvfile):
chat_df = pd.read_csv(csvfile)
# 选择type_name为文本的行、is_sender为1的行
chat_df = chat_df[chat_df["type_name"] == "文本"]
chat_df = chat_df[chat_df["is_sender"] == 1]
# 对每一行的content进行处理 转为dict 再取'msg'字段
chat_df["content"] = chat_df["content"].apply(lambda x: json.loads(x)["msg"])
# 如果content 包含 手机号、身份证号、邮箱、网址则删除这行
chat_df = chat_df[~chat_df["content"].str.contains("1\d{10}")]
chat_df = chat_df[~chat_df["content"].str.contains("\d{18}")]
chat_df = chat_df[~chat_df["content"].str.contains("\w+@\w+")]
chat_df = chat_df[~chat_df["content"].str.contains("http")]
chat_df = chat_df[~chat_df["content"].str.contains(r"\\xa0")]
chat_df = chat_df[~chat_df["content"].str.contains(r"\\u")]
# 纯content
chat_df = chat_df["content"]
chat_df = chat_df.dropna()
return chat_df
def make_pt_dataset():
csv_res = []
# csv文件夹里全是不同聊天对象文件夹 每个文件夹里是csv文件 先遍历不同聊天对象文件夹 再遍历聊天对象的csv文件
for chat_obj_folder in os.listdir(csv_folder):
chat_obj_folder_path = os.path.join(csv_folder, chat_obj_folder)
for csvfile in os.listdir(chat_obj_folder_path):
if not csvfile.endswith(".csv"):
continue
csvfile_path = os.path.join(chat_obj_folder_path, csvfile)
chat_df = handle_pt_csv(csvfile_path)
csv_res.append(chat_df)
csv_res = pd.concat(csv_res)
csv_res = csv_res.apply(lambda x: {"c": x}) # 设置数据集prompt键为c
csv_res.to_json("./data/res_csv/pt-my.json", orient="records", force_ascii=False)
def handle_sft_csv(csvfile):
chat_df = pd.read_csv(csvfile)
blocked_words = json.load(
open("./make_dataset/blocked_words.json", encoding="utf-8")
)["blocked_words"]
# 选择type_name为文本的行、is_sender为1的行
# 需要保留的type_name字段名
type_list = [
"文本",
"图片",
"视频",
"合并转发的聊天记录",
"语音",
"(分享)音乐",
"(分享)卡片式链接",
"(分享)笔记",
"(分享)小程序",
"(分享)收藏夹",
"(分享)小说(猜)",
"(分享)视频号名片",
"(分享)视频号视频",
"粘贴的文本", # 无法解析的分享链接
]
chat_df = chat_df[chat_df["type_name"].isin(values=type_list)]
# chat_df['content'] = chat_df['content'].apply(func=lambda x: json.loads(x)['msg'])
chat_df["content"] = chat_df["msg"]
# 如果type_name为文本 并且content 包含 手机号、身份证号、邮箱、网址则删除这行
for i in chat_df.index:
if chat_df.loc[i, "type_name"] == "文本":
if (
"1\d{10}" in chat_df.loc[i, "content"]
or "\d{18}" in chat_df.loc[i, "content"]
or "\w+@\w+" in chat_df.loc[i, "content"]
or "http" in chat_df.loc[i, "content"]
or r"\\xa0" in chat_df.loc[i, "content"]
or r"\\u" in chat_df.loc[i, "content"]
):
chat_df = chat_df.drop(index=i)
continue
for blocked_word in blocked_words:
if blocked_word in chat_df.loc[i, "content"]:
chat_df = chat_df.drop(index=i)
break
else:
chat_df.loc[i, "content"] = ""
chat_df = chat_df[["is_sender", "type_name", "content", "CreateTime"]]
chat_df = chat_df.dropna()
# 时间格式 2021-07-07 10:27:23
# 遍历行 相同is_sender的行合并content()遇到不同is_sender就重新开始
# CreateTime字段保留最后的CreateTime
chat_df["CreateTime"] = pd.to_datetime(chat_df["CreateTime"])
# 改到这了
type_list.remove("文本")
skip_list = type_list
res_df = []
last_is_sender = chat_df.iloc[0]["is_sender"]
last_content: str = chat_df.iloc[0]["content"]
last_CreateTime = chat_df.iloc[0]["CreateTime"]
# 超时处理 半天没说话就重新开始
# 注意这里只是处理了组装成一个句子 最后封装对话、配对在make_sft_dataset
# 遇到图片 连接 直接封装成一个句子
for i, row in chat_df.iterrows():
if row["type_name"] in skip_list:
if last_content != "":
if last_content[-1] == "":
last_content = last_content[:-1] + ""
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_CreateTime = row["CreateTime"]
last_content = ""
# cut表示被skip字段截断
res_df.append(
{
"is_sender": row["is_sender"],
"content": "cut",
"CreateTime": row["CreateTime"],
}
)
continue
if last_content == "": # 重新开始
last_content = row["content"]
last_is_sender = row["is_sender"]
last_CreateTime = row["CreateTime"]
continue
if row["is_sender"] == last_is_sender:
if row["CreateTime"] - last_CreateTime > pd.Timedelta(value="10m"):
# 如果超时 前面的添加到res_df 并重新开始
if last_content[-1] == "":
last_content = last_content[:-1] + ""
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_content = row["content"]
last_CreateTime = row["CreateTime"]
continue
# 如果content的结尾没有标点符号则添加逗号,最后结尾是句号
if last_content[-1] not in ["", "", "", "", ""]:
last_content += ""
last_content = last_content + row["content"]
last_CreateTime = row["CreateTime"]
else:
if last_content[-1] == "":
last_content = last_content[:-1] + ""
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_is_sender = row["is_sender"]
last_content = row["content"]
last_CreateTime = row["CreateTime"]
res_df = pd.DataFrame(res_df)
return res_df
def make_sft_dataset():
processor = DataProcessor()
csv_files = processor.get_csv_files()
csv_concat = []
csv_res = []
for csvfile_path in csv_files:
chat_df = handle_sft_csv(csvfile_path)
csv_concat.append(chat_df)
# 后续代码保持不变
csv_concat = pd.concat(csv_concat)
# 更全面地处理cut标记
# 1. 将连续的cut标记合并为一个
# 2. 标记数据区块的开始和结束
processed_rows = []
skip_row = False
last_row_was_cut = False
for i in range(len(csv_concat)):
if skip_row:
skip_row = False
continue
current_row = csv_concat.iloc[i].copy()
# 处理当前行是cut的情况
if current_row["content"] == "cut":
# 如果上一行已经是cut,则跳过当前行
if last_row_was_cut:
continue
# 查找连续的cut
j = i + 1
while j < len(csv_concat) and csv_concat.iloc[j]["content"] == "cut":
j += 1
# 如果有连续的cut,只保留最后一个
if j > i + 1:
current_row = csv_concat.iloc[j - 1].copy()
skip_row = True
last_row_was_cut = True
else:
last_row_was_cut = False
processed_rows.append(current_row)
# 创建新的DataFrame
csv_concat = pd.DataFrame(processed_rows)
# csv_res里is_sender必须是01 01 01 的顺序 csv_concat里不一定是01 01
# 相差超过1小时的时间戳分为不同的对话
# temp_res为一个长度为2的队列
# 将合并后的数据保存到CSV文件中
output_dir = "./test_output"
# 生成带时间戳的文件名
import datetime
now = datetime.datetime.now()
output_file = os.path.join(output_dir, f"csv_old_.csv")
# 保存合并后的数据
# csv_concat.to_csv(output_file, index=False, encoding="utf-8-sig")
# print(f"已将合并后的数据保存到: {output_file}")
# print(f"合并后数据总量: {len(csv_concat)} 条记录")
temp_res = deque(maxlen=2)
# 6种情况
# temp_res 为空 遇到 0入队 遇到1不处理 遇到cut不处理
# temp_res 有0 遇到0清空队列再入队 遇到1相差超过1小时清空队列 没有相差一小时入队再全部出队 遇到cut清空队列
for i, row in csv_concat.iterrows():
if len(temp_res) == 0:
if row["content"] == "cut":
continue
if row["is_sender"] == 0:
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
continue
elif len(temp_res) == 1:
if row["content"] == "cut":
temp_res.clear()
last_CreateTime = row["CreateTime"]
elif row["is_sender"] == 0:
# 遇到0 清空队列再入队
temp_res.clear()
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
if row["CreateTime"] - last_CreateTime > pd.Timedelta("10m"):
# 相差超过1小时清空队列
temp_res.clear()
last_CreateTime = row["CreateTime"]
else:
# 没有相差一小时入队再全部出队
temp_res.append(row["content"])
csv_res.append({"instruction": temp_res[0], "output": temp_res[1]})
temp_res.clear()
last_CreateTime = row["CreateTime"]
csv_res_df = pd.DataFrame(csv_res)
print(f"处理后数据量:{csv_res_df.shape[0]}")
csv_res_df.to_json('./data/res_csv/sft/sft-old-my.json', orient='records', force_ascii=False)
if __name__ == "__main__":
# make_pt_dataset()
make_sft_dataset()
+9 -8
View File
@@ -1,6 +1,7 @@
import csv
import json
import os
import re
import sys
import pandas as pd
@@ -91,12 +92,12 @@ def handle_sft_csv(csvfile):
for i in chat_df.index:
if chat_df.loc[i, "type_name"] == "文本":
if (
re.search(r"1\d{10}", msg_str)
or re.search(r"\d{18}", msg_str)
or re.search(r"\w+@\w+", msg_str)
or "http" in msg_str
or r"\\xa0" in msg_str
or r"\\u" in msg_str
re.search(r"1\d{10}", chat_df.loc[i, "content"])
or re.search(r"\d{18}", chat_df.loc[i, "content"])
or re.search(r"\w+@\w+", chat_df.loc[i, "content"])
or "http" in chat_df.loc[i, "content"]
or r"\\xa0" in chat_df.loc[i, "content"]
or r"\\u" in chat_df.loc[i, "content"]
):
chat_df = chat_df.drop(index=i)
continue
@@ -156,7 +157,7 @@ def handle_sft_csv(csvfile):
last_CreateTime = row["CreateTime"]
continue
if row["is_sender"] == last_is_sender:
if row["CreateTime"] - last_CreateTime > pd.Timedelta(value="10m"):
if row["CreateTime"] - last_CreateTime > pd.Timedelta(value="2m"):
# 如果超时 前面的添加到res_df 并重新开始
if last_content[-1] == "":
last_content = last_content[:-1]
@@ -290,7 +291,7 @@ def make_sft_dataset():
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
if row["CreateTime"] - last_CreateTime > pd.Timedelta("1h"):
if row["CreateTime"] - last_CreateTime > pd.Timedelta("5m"):
# 相差超过1小时清空队列
temp_res.clear()
last_CreateTime = row["CreateTime"]