1 背景

为了提升用户对文本的准确理解,我们设计并实现了一个多音字与易读错字标注系统。通过上传Word文档,程序将自动检测文中的多音字和易错字,并为其添加拼音注释。输出的结果会以Word文件形式返回,注释信息会在字词后以圆括号形式标注,方便用户阅读和展示。

2 目的

通过训练双向LSTM模型、逻辑回归、g2pM、调用大模型API等方法实现对文本的多音字和易读错字标注。

通过Django框架实现在线系统,方便用户通过网页上传和下载文档,实现实时检测与注音。

3 系统实现框架

3.1 前端

用户通过网页上传Word文档。

然后向后端接口发起POST请求,返回处理结果。

// 处理文档
function DealDoc(e){
    // 处理选项
    var radios = document.querySelector('input[name="gender"]:checked').value;
    reader = document.getElementById("input").value
    document.getElementById("output").value = "Loading...";
    var url = 'https://nlp.bithao.com.cn/deal';
    // 发起POST请求
    fetch(url, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type':'application/x-www-form-urlencoded'
        },
        body: `input=${reader}&radios=${radios}`
    })
    .then(response => response.json())
    .then(data => {
        console.log('Success:', data);
        // 显示输出结果
        document.getElementById("output").value = data.result;
    })
    .catch(error => {
        console.error('Error:', error);
    });
}

用户点击下载,将处理好的数据保存为docx文件,直接在客户端完成,无需占用服务端资源。

3.2 后端

view层,数据处理接口,根据不同选项以Json格式返回不同方法的处理结果。

def deal(request):
    if request.method == 'POST':
        data = request.POST["input"]
        radios = int(request.POST["radios"])
        if radios == 1: # LSTM
            result = LSTMDealDoc(data)
        elif radios == 2:   # 逻辑回归
            result = LGDealDoc(data)
        elif radios == 3:   # g2pm
            result = g2pmDealDoc(data)
        elif radios == 4:   # 大模型
            result = AIDealDoc(data)
        content = {
            "result": result
        }
        return JsonResponse(content)

4 双向LSTM模型

功能-BiLSTM

双向长短时记忆网络(BiLSTM)在多音字消歧中的应用,主要是因为它具备从两个方向(前向和后向)处理序列数据的能力。具体来说,BiLSTM 相比于单向的 LSTM 可以更好地处理语言中的依赖关系,因为它不仅能够考虑到多音字前面的词语(前向信息),还能够结合后续的词语(后向信息)来做出判断。这种双向处理使得模型在消岐时能够利用更多的上下文信息,从而更准确地确定多音字的正确读音。

4.1 数据来源

一、汉字拼音库,使用《现代汉语词典》中的拼音数据,汉字数:11017,多音字个数:1217;

(来自pinyin-data

二、人民日报1988年一月的新闻语料,一共有19374条新闻,包含5593个多音字,其中有85种不同的多音字;来自(pkuopendata

4.2 数据预处理

数据统计:

  1. 统计了多音字的读音个数,了解多音字的分布情况。
  2. 对新闻语料中的多音字进行统计,包括出现次数和含多音字的句子数量等。
  3. 对短语中的多音字进行统计和分析。

语料划分:

将语料划分为训练集、验证集和测试集。

train.csv(训练集,29928 条短语,30041 个多音字)

valid.csv(验证集,8723 条短语,12696 个多音字)

test.csv(测试集,8638 条短语,19198 个多音字)

数据预处理:

  1. 使用 torchtext 库对文本进行处理。
  2. 对句子和读音进行 tokenize(分词),建立词典。
  3. 设定 batch size 和生成迭代器,以便将数据送入模型进行训练。

4.3 模型搭建

仿照词性标注问题的分类方法:

1. 仿照词性标注的处理思路,将训练数据编码为特定格式,其中 “NA” 代表非多音字,多音字则使用读音来编码。

2. 已将短语数据送入模型并进行训练,然后使用测试集计算准确率,将标注正确的语句和标注错误的语句输出到相应文件中(correct.csvwrong.csv)。

# 用于消岐的神经网络
class DisambiguationLSTM(nn.Module):
 def __init__(self, n_word, word_dim, word_hidden, n_pronounce):
 super(DisambiguationLSTM, self).__init__()
 self.word_embedding = nn.Embedding(n_word, word_dim)
 self.lstm = nn.LSTM(
 input_size=word_dim,
 hidden_size=word_hidden,
 num_layers=configure.num_layer,
 batch_first=True,
 bidirectional=True
 )
 self.linear1 = nn.Linear(word_hidden*2, n_pronounce)

 def forward(self, x):
 x = self.word_embedding(x)
 x, _ = self.lstm(x) 
 x = x.squeeze(0) 
 x = self.linear1(x) 
 return x

3. 使用optuna库寻找最优参数;

#寻找最优参数 import optuna def objective(trial): trn_rate = trial.suggest_float('trn_rate', 0.4, 0.8) val_rate = trial.suggest_float('val_rate', 0.1, 0.4) batch_size = trial.suggest_categorical('batch_size', [256, 512, 1024]) num_layer = trial.suggest_int('num_layer', 1, 3) # 更新配置中的参数 configure.trn_rate = trn_rate configure.val_rate = val_rate configure.batch_size = batch_size old_acc = 0 ......

4.4 word文档注音

假设已经有了一个处理 Word 文档并进行多音字注音的函数 annotate_word_document,其思路如下:

4.4.1 易读错字

除多音字外,还要求程序能够识别易读错字。通过网络信息检索获得400个易错字词,包括容易平翘舌不分、前后鼻音不分、h/f不分、鼻边音不分等难点发音字词,轻声儿化等需变调词语,以及一部分较生僻的字,例如摈(bìn)弃、疮(chuāng)疤、舂(chōng)等等。程序运行时,直接从目标文章中识别并注音即可。

1. 读取 Word 文档内容,将其转换为文本并导入易错字,多音字JSON词典;

def word_doc_to_text(file_path): doc = docx.Document(file_path) text = "" for paragraph in doc.paragraphs: text += paragraph.text return text #读取约400个易错字,注意csv编码格式为UTF-8 def read_mispronounced_words(file_path): df = pd.read_csv(file_path) return dict(zip(df['text'], df['label'])) # 读取多音字 JSON 文件 with open('../data/polyphones.json', 'r', encoding='utf-8') as f: polyphone_data = json.load(f) polyphone_list = list(polyphone_data.keys())

2. 使用jieba对文本进行分词,得到单词列表。

3. 对于每个单词,判断是否包含多音字:

def is_word_contain_polyphone(word): for char in word: if char in polyphone_list: return True return False

4. 分词词语转化为适配模型输入的张量,并根据模型输出确定读音;

#分词词语转化为模型输入的张量,进行预处理,如填充和截断

def word_to_tensor(word, vocab): word_index = vocab.stoi.get(word, vocab.stoi['<unk>']) return torch.tensor(word_index).unsqueeze(0) def words_to_tensor_list(word_list, vocab, max_length): tensor_list = [word_to_tensor(word, vocab) for word in word_list] padded_tensor_list = torch.nn.utils.rnn.pad_sequence(tensor_list, batch_first=True) if len(padded_tensor_list) > max_length: return padded_tensor_list[:max_length] else: return padded_tensor_list def determine_pronunciation(output, label_vocab): # 根据模型输出确定读音 predicted_label_index = torch.argmax(output).item() pronunciation_label = label_vocab.itos[predicted_label_index] # 根据标注体系,提取读音信息并返回 if pronunciation_label == "NA": #NA表示非多音字 word = label_vocab.itos[predicted_label_index] if word in mispronounced_words: #如果是易错字词,返回读音 return mispronounced_words[word] else: return "" else: return pronunciation_label

5. 如果单词包含多音字,使用训练好的LSTM模型进行预测,如果模型能够确定读音,则在单词中的多音字后面标注读音;如果模型未识别,则使用多音字JSON文件中的读音信息进行标注,如果单词不包含多音字,直接添加到结果中;

整体思路是通过对不同来源的数据进行统计和预处理,搭建多音字消歧模型,并将其应用于 Word 文档的多音字注音任务中。在模型搭建过程中,尝试了不同的方法,并选择了较为有效的仿照词性标注问题的分类方法进行训练和测试。

验证集上准确率达到85-90%,测试集准确率为75%-88%

4.4.2 模型改进-其他词性

由于训练语料库主要由短语或词语构成,将多音字消歧模型应用于长文本时,短语级别的训练限制了模型对更复杂语境中多义词的判断,无法充分利用句子或段落中的全局上下文信息。例如“的”作为助词时在长文本中及其常见,但在训练短语中仅出现67次,且不少作为固定短语,如“狗娘养的”、“一矢中的”。

为了改进模型,考虑通过识别常见的副词、助词、方位词、连词和介词中的多音字,并根据长文本上下文判断词性,再在原有模型基础上对这些字进行二次注音。不同词性下,某些多音字的发音是固定的,因此可以利用这一点进行二次注音,降低模糊性。此外,词性判断依赖于上下文,可以有效地利用句子和段落级别的信息,而不是仅依赖于短语。

以副词为例,常见的副词包括“非常、绝对、极度、十分、最、顶级、太、更、极其、格外、分外、一直、才、总 、也、都、全部、总体、总共、共、统统、又、仅仅、只、光、一概 、已经、曾经、早已、刚刚、正、正在、就、就要、将、将要、曾、刚、才、在 、不、非、没、没有、不用(甭)、必、必须、必定、准、的确、未、别、忽然、猛然、公然、特意、亲自、大肆、肆意、难道、决、岂、反正、也许、大约、大概、果然、居然、竟然、究竟 、家、这里、那里、每一处、楼”,其中包含多音字的有以下几个,且作为副词时读音固定,分别是“才(cái)、都(dōu)、将(jiāng)、没(méi)”。同理,手工找出常见助词、方位词、连词和介词中的多音字。最后汇总的这些词性的多音字一共有17个,分别为

副词:才(cái)、都(dōu)、将(jiāng)、没(méi)

助词:了(le)、着(zhe)、过(guo)、地(de)、的(de)、得(de)

方位词:中(zhōng)

介词:从(cóng)、当(dāng)、为(wèi)、给(gěi)

连词:和(hé)、便(biàn)

使用jieba库的pseg模块,对长文本进行分词及词性标注,并对上述特定词性的多音字二次标注。代码示例如下:

import jieba.posseg as pseg
# 定义要加注音的字及其拼音
pinyin_dict = {
'才': 'cái',
'都': 'dōu',
'将': 'jiāng',
'没': 'méi',
'了': 'le',
'着': 'zhe',
'过': 'guo',
'地': 'de',
'的': 'de',
'得': 'de',
'中': 'zhōng',
'从': 'cóng',
'当': 'dāng',
'为': 'wèi',
'给': 'gěi',
'和': 'hé',
'便': 'biàn'
}
# 定义目标词性集合(副词、助词、方位词、介词、连词)
target_pos = {'d', 'p', 'f', 'c', 'uv', 'ul', 'uj'}
# 处理句子的函数
def add_pinyin(sentence):
    words = pseg.cut(sentence) # 使用 jieba 分词并获取词性
    result = []
    for word, flag in words:
        if word in pinyin_dict and flag in target_pos:
            result.append(f"{word}({pinyin_dict[word]})") # 如果满足条件,添加拼音注释
        else:
            result.append(word) # 否则直接保留原字
    return ''.join(result)
# 示例
text = "才知道,你在雨中飞快地跑,我走了。"
# 输出加注音的句子
annotated_text = add_pinyin(text)
print(annotated_text)
# words = pseg.lcut(text)
# 打印每个词及其词性
# for word, flag in words:
# print(f"{word}: {flag}")

4.5 模型评估

输入输出案例:

5 逻辑回归

功能-逻辑回归

主要思想:

把多音字注音分解为每个多音字的读音识别,即分解为六百多个简单分类任务。利用多音字在不同句子中的词性,及其前后词语的词性作为特征训练模型,得到六百多个模型。使用的时候从前到后遍历word文档,如果遍历到多音字字典里的字,则调用对应的模型进行预测。

数据集:

CPP (Chinese Polyphones with Pinyin)

https://paperswithcode.com/dataset/cpp

拥有99000条中文多音字数据,数据格式如下:

用▁标记多音字的句子正确读音
斯考尔将她救出,并搭乘遗弃的星际飞船回到▁了▁星球。Le5

项目流程:

  1. 处理数据:

79100条作为训练集,9800条作为验证集,10250条作为测试集。

利用THULAC进行分词,经处理得到如下训练输入和输出:

输入输出   
多音字所在词前面词的词性所在词的词性后面词的词性正确读音
vunLe5
  1. 训练模型:

将输入输出数据进行编码后进行训练。

# 训练模型并评估验证集
for epoch in range(10):  # 假设我们进行10个epoch的训练
    model.fit(X_train_encoded, y_train_encoded)
    y_val_pred_encoded = model.predict(X_val_encoded)
    accuracy_val = accuracy_score(y_val_encoded, y_val_pred_encoded)

    # 如果验证集表现更好,保存当前的最佳模型
    if accuracy_val > best_accuracy:
        best_accuracy = accuracy_val
        best_model = model

# 保存最佳模型和编码器
if best_model:
    model_save_path = os.path.join(folder_path, 'best_model.pkl')
    encoder_save_path = os.path.join(folder_path, 'encoders.pkl')

    joblib.dump(best_model, model_save_path)
    joblib.dump({'onehot_encoder': onehot_encoder, 'label_encoder': label_encoder}, encoder_save_path)
  1. 测试结果:

该数据集部分多音字只标注了一个读音。综合正确率等于整体预测正确数目除以预测条数。

如果默认只有一个注音的多音字都预测正确:在测试集上综合正确率97.14%

如果不考虑只有一个注音的多音字,综合正确率为94.81%

写文档针对性进行测试效果如下图:

分析总结:

逻辑回归训练的模型在测试集上表现较好,但是实际应用效果比较一般。分析可能有如下几点原因:

  1. 数据集同一个字的不同读音数据量失衡。
  2. 部分多音字不同发音所组成的词用法区别不大,无法有效通过词性进行判断。

该方法实现起来比较简单,且对训练设备要求不高。但是这个方法只是浅显的通过词性训练,且每个字各有一个模型,没有建立理解语义的大模型。

6 g2pm

功能-g2pm

G2pM使用方法:

from g2pM import G2pM
model = G2pM()
sentence = "输入要注音的文本"
Pinyin=model(sentence, tone=True, char_split=False)
  1. 创建多音字字典文档
  2. 输入word文档,裁剪文本利用G2pM模型识别
  3. 处理返回的拼音数据,只截取多音字的注音并按要求输出word文档

效果展示:

7 大模型API调用

功能-大模型

这里选择Kimi大模型,通过添加提示词,让AI返回结果。

from openai import OpenAI
client = OpenAI(
    api_key = "sk-########################################",
    base_url = "https://api.moonshot.cn/v1",
)
history = [
    {"role": "system", "content": "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"}
]
def chat(query, history):
    history.append({
    "role": "user", 
    "content": query
    })
    completion = client.chat.completions.create(
        model="moonshot-v1-32k",
        messages=history,
        temperature=0.3,
    )
    result = completion.choices[0].message.content
    history.append({
    "role": "assistant",
    "content": result
    })
    return result
def MakeBiaozhu(input_data):
    output_data = f"你是一位骨灰级国语大师,对下面段落文字中多音字、易读错字,加上注音,放在圆括号内,不需要做出任何解释:'{input_data}'"
    return output_data
def AIDealDoc(input_doc):
    return chat(MakeBiaozhu(input_doc), history)

输入输出案例:

本方法调用大模型API做垂直领域的应用是可行的,但效果依赖于提示词的使用,同时成本较高。