准备微调数据

今天计划完成如下目标:

  • 读取文件
  • 统计数据
  • 清洗数据
  • 构造微调JSON
  • 尝试微调LLaMA大模型

代码和运行结果放在Github中

解析角色发言

由于Java是我每天吃饭用的工具,虽然Python很香,但我选Java。毕竟准备微调数据JSON是一次性工作,不参与模型训练中,所以直接用比较顺手的工具解析并构造即可。

首先定义两个核心数据类,第一个是角色发言,每个对象存储角色名称以及对应的发言或思考。

1
2
3
4
5
@Data
public class CharacterQuote {
String name;
String quote;
}

第二个是用于微调大模型的JSON基础对象,需要符合Stanford Alpaca格式。

1
2
3
4
5
6
@Data
class FinetuneJson {
String instruction;
String input;
String output;
}

解析第一步肯定是读取文件、按行解析、去掉前后空格、尝试输出。感谢Galgame剧本格式,基本都在一行并使用“【XXX】”来标记是谁说的。

角色列表,都是老朋友真怀念啊
角色列表,都是老朋友真怀念啊

由于文本中,还有一些角色并没有通过“【】”圈起来,但处于单个句子的前方,因此先抽取角色名字列表,然后再做匹配,从而提升准确度。

因为文本中,被”【】“圈起来的角色文本在文件的上方,因此直接使用Set即可,不用再重新读一遍了。

解析未被中括号括起来的角色结果
解析未被中括号括起来的角色结果

接下来使用inQuote来识别当前行是否属于某个角色的发言,外加判断行末是否是结束符号,从而是否有多行发言。

能够识别多行发言
能够识别多行发言

至此Parser.java完成了,读取剧本并解析角色发言,放入对象列表中。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class Parser {

private final BufferedReader reader;

Parser(String file) throws FileNotFoundException {
reader = new BufferedReader(new FileReader(file));
}

public List<CharacterQuote> parseLines() throws IOException {
List<CharacterQuote> parseResult = new ArrayList<>();
Set<String> characters = new HashSet<>();

String currName = "";
StringBuilder currQuote = new StringBuilder();
boolean inQuote = false;

String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (StringUtils.isBlank(line)) {
continue;
}

// check character quote
if (line.startsWith("【")) {
// parse character name
String character = line.substring(1, line.indexOf("】"));
characters.add(character);
currName = character;
inQuote = true;
} else if (line.contains("「")) {
int idx = line.indexOf("「");
if (idx == 0) {
continue;
}
if (characters.contains(line.substring(0, idx))) {
currName = line.substring(0, idx);
// replace line name
line = "【" + currName + "】" + line.substring(idx);
System.out.println(line);
inQuote = true;
}
}

// multirow quote
if (!inQuote) {
continue;
}
currQuote.append(line);
if (line.endsWith("」") || line.endsWith(")")) {
parseResult.add(new CharacterQuote(currName, currQuote.toString()));
// reset
inQuote = false;
currName = "";
currQuote = new StringBuilder();
}
}
return parseResult;
}
}

构造微调JSON

成功完成了对话的解析,接下来另开一个类用于构造微调JSON。

将上一步的角色发言处理为Finetune中的对话。

构造12、23、34式对话,保留对话上文
构造12、23、34式对话,保留对话上文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class BuildFinetuneFile {

List<CharacterQuote> characterQuotes;

BuildFinetuneFile(List<CharacterQuote> quotes) {
characterQuotes = quotes;
}

public List<Finetune> build() {
if (CollectionUtils.isEmpty(characterQuotes)) {
return null;
}

List<Finetune> finetunes = new ArrayList<>();

String lastContinuedQuote = "";
String continuedQuote = "";
for (int i = 0; i < characterQuotes.size(); i++) {
if (i > 0 && characterQuotes.get(i).getName().equals(characterQuotes.get(i - 1).getName())) {
continuedQuote += "\n\n" + characterQuotes.get(i).getQuote();
continue;
}
// name changed submit last quotes
if (!StringUtils.isEmpty(continuedQuote)) {
finetunes.add(new Finetune(lastContinuedQuote, "", continuedQuote));
lastContinuedQuote = continuedQuote;
}
continuedQuote = characterQuotes.get(i).getQuote();
}
// submit last quotes
if (!StringUtils.isEmpty(continuedQuote)) {
finetunes.add(new Finetune(lastContinuedQuote, "", continuedQuote));
}
return finetunes;
}
}

输出文件

直接构造个Main文件,执行一波~接下来就是微调模型了,本节结束。

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) throws IOException {
Parser parser = new Parser("CLANNAD.txt");
BufferedWriter out = new BufferedWriter(new FileWriter("finetune_json/CLANNAD_LLaMA_finetune.json"));
out.write(new Gson().toJson(new BuildFinetuneFile(parser.parseLines()).build()));
out.close();
}
}