Arondight / Adachi-BOT

一个在 QQ 中运行的原神助手
MIT License
344 stars 75 forks source link

命令防误触功能及改进 #417

Closed mark9804 closed 2 years ago

mark9804 commented 2 years ago

1. 个人改进建议

https://github.com/Arondight/Adachi-BOT/commit/04fc8e621bab46fb4ed991dcad5564a9613f3226#commitcomment-60639808

把原评论搬运了过来 > 现在我们用的是单纯的hamming距离来判断是否需要接受该指令,我们预先设置的别名越短,用户的输入越短,就越容易碰到这个阈值 > > 例: > > > 我的**鱼** → hamming距离 = 0,响应此次指令 > > 我的**驴** → hamming距离 = 1,响应此次指令 > > 如果把hamming距离除以字符串长度,得到一个浮点数,把这个浮点数作为字符串的【不相似率】进行处理会不会更好一些?就类似知网查重那样 > > 例: > > > 我的**鱼** → 不相似率 0.00,响应此次指令 > > 我的**驴** → 不相似率 1.00,不响应此次指令 > > 或者我们可以在这个基础上更进一步,假如用户输入的字符串与已知的`characterNames`均不匹配,但是和某个`characterNames`的不相似率低于某个阈值(例如0.40,三个字当中只有一个字不一样),我们可以猜测用户的本意就是想查询这个名称 > > 例: > > > 查询**达达利亚** → 与【达达利亚】的不相似率 = 0.00 ≤ 0.40,响应此次指令,返回达达利亚 > > 查询**达达利**鹅 → 与【达达利亚】的不相似率 = 0.33 ≤ 0.40,响应此次指令,返回达达利亚 > > 查询塔塔利娅 → 与【达达利亚】的不相似率 = 0.66 > 0.40,不响应此次指令,或不认为这是一个有效指令

2. 误响应问题合集

2.1 【我的/别人的某个角色】

image image

2.2 不太清楚这是怎么匹配上的

image 感觉这个如果真能匹配上也能返回天空书的图片也不算什么坏事,补充说明,就当是feature了

Arondight commented 2 years ago
  1. 2.1 没这个问题
  2. 2.2 把角色的逻辑放到武器上就好了
Arondight commented 2 years ago

这个建议很好呀,我试一下

Arondight commented 2 years ago

另外别名需要删一删,现在别名太多导致匹配也有问题

Arondight commented 2 years ago

行秋的秋秋和秋秋人明显是重复的,心海的鱼和热带鱼也是,现在的别名似乎成了一个梗字典,我觉得需要删掉大部分

Arondight commented 2 years ago
  1. 琴:琴团长,有人愿意输入琴必然不想再输入一个团长,所以应该只保留团长
  2. 莫娜:占星术师,永远不会用到的别名,同样的还有早柚的幼颜龙蜥,托马的家政官

之前我写了一条原则但是删掉了,我觉得应该补回来

https://github.com/Arondight/Adachi-BOT/blob/f0ee821f8ca82cd4746675dc42b7b4dc9a129fc4/config_defaults/alias.yml#L3

Arondight commented 2 years ago

之前删掉了这个原则是因为我们不太依赖这个配置文件的规范性,大家玩得开心就好,其实从给出查找建议开始就应该规范起来

Arondight commented 2 years ago

另外别名应该具有时效性,例如凯亚明显不再是矿工头子了,事实上半年前入坑的人就不知道凯亚为什么叫这个,硬要说的话现在的矿工头子应该是钟离

Arondight commented 2 years ago

再说一下你这个建议,我细看了一下你算距离和实际上的不太一样,理论上是很好的,不过距离其实不是这么算的。首先算 simhash ,文本分词,每个词算 hash ,每个 hash 按位加权后按位相加,然后进行归一变为一串二进制,至此 simhash 完成,认为这串二进制代表了文本的特征。然后汉明距离是两个二进制异或取置位个数

Arondight commented 2 years ago

英文名字也是需要删掉的,永远不会被使用,除了一些国内玩家常用的例如 xiao dio

Arondight commented 2 years ago

还有明显不需要处理的错别字,例如肖宮:小攻,相应反而会显得不正常

mark9804 commented 2 years ago

就是说汉明距离已经包含了我想法里的步骤了吗,我的理解是从一个字符串到另一个字符串需要替换的字符个数,所以借鉴论文查重提出了这个想法。 实际上跟我理解不一样的是,汉明距离是一个相对抽象的概念,已经可以(一定程度上)代表两个字符串之间的不相似率了吗?

Arondight commented 2 years ago

@Mark9804 不是这样的,汉明距离是判断信息编码相似性的一种做法,我们传递给他的信息编码是 simhash ——文本的特征值

citydirector commented 2 years ago

补充一些奇奇怪怪的触发 image image image

Arondight commented 2 years ago

我这里还有 武器池 的问题,但是后两个没有,我看看怎么做一些进一步的相似性检测

Arondight commented 2 years ago

补充一些奇奇怪怪的触发

  1. 现在降低了很多误触,例如 信息池
  2. 删除了武器和角色,本来这俩也没出现在帮助信息上
Arondight commented 2 years ago

另外加了一个旅行者的 json ,里面都是瞎写的,晚上有空再填

citydirector commented 2 years ago

等大佬们更新完这个阶段我再更新试试 (自定义了部分配置文件,每次都要重新写0.0) (sublime真好用)

mark9804 commented 2 years ago

之前我提出的相似性检测方法可能因为有误解导致说的不太清楚,但应该大方向是没问题的,我在我自己分支尝试一下

mark9804 commented 2 years ago

simhash感觉有点奇怪,我预期中达达鹅达达吖的响应应该是一样的,而不是一个响应一个不响应 image

Arondight commented 2 years ago

你可以单独写个脚本尝试

import { hamming, simhash } from "./src/utils/tools.js"
var s1 = "钟离";
var s2 = "钟来";
console.log(hamming(simhash(s1), simhash(s2)))

现在分词器是我自己写的,如果用 Segment 库分词就会很像了

Arondight commented 2 years ago

但是分词太好就会过于相似或者不相似了,所以我自己写了个分词。例如 迪卢克 会被 Segment 分词成一个词语 迪卢克 ,但是会被我分成 ,对于每个分词都计算 hash ,然后加权、纵向相加、归一。所以 迪卢克 这一个词相当于不会有加权、相加和归一的过程,就是绝对准确的。这就导致了一个问题,迪卢克迪卢壳 就是完全两个东西

mark9804 commented 2 years ago

这个结果感觉太不稳定了,有没有可能simhash在非英文环境下的表现比我们想象中的要差一些

import { hamming, simhash } from "./src/utils/tools.js"
const s1 = "达达利吖";
const s2 = "达达利鸭";
const s3 = "达达利亚";

console.log(hamming(simhash(s1), simhash(s3)))
console.log(hamming(simhash(s2), simhash(s3)))
5
8
Arondight commented 2 years ago

我感觉跟英文没关系,他这算法不关心语言编码,反正我现在是两层校验了, hamming 好不好我也不是很在意了

mark9804 commented 2 years ago

不经过simhash,我感觉这种结果是好的

import { hamming} from "./src/utils/tools.js"

const s1 = "达达利吖";
const s2 = "达达利鸭";
const s3 = "达达利亚";

console.log(hamming(s1, s3))
console.log(hamming(s2, s3))
1
1
Arondight commented 2 years ago

不经过 simhash 不行的,你把顺序改一下一下试试,例如 达达里亚公子达达利亚

Arondight commented 2 years ago

而且对于汉明距离来说这个串实在是太短了,算不出什么有代表性的东西

mark9804 commented 2 years ago

确实得过一遍 simhash ,我再想想

mark9804 commented 2 years ago

老大哥能提供一下现在的分词器的位置么

Arondight commented 2 years ago
import { segment } from "./src/utils/tools.js"
console.log(segment('今天星期 3,原神的名字为什么会叫Genshin Impact呢?'))
Arondight commented 2 years ago

实际上我试了下,少于六个字符,这样分词更加合理一下

Arondight commented 2 years ago

我更新了一下分词器,现在不会分出来空白字符了

mark9804 commented 2 years ago

https://github.com/xmcp/pakku.js/blob/1ef9d3998a27e6b69daf792bcbbbe79e13c1b525/pakkujs/core/edit_distance.js

上面是一个判断B站不同弹幕相似度并自动合并的项目,判断的是2-gram频率向量的夹角(不是那个编辑距离/莱文斯坦距离)

Arondight commented 2 years ago

我晚上回去看一下,这些算法我感觉在短字符串上表现都不一定多好,特征很容易变动,长字符串就不会

mark9804 commented 2 years ago

拉了一下代码重启试了试,确实会误触发image

Arondight commented 2 years ago

定轨我忘了加了,这个加一下限制就好,不需要算相似性。以前的代码因为有正则过滤掉很多不合适的聊天内容,所以插件内部都没有做这个工作。

Arondight commented 2 years ago

哦,定轨是需要算的,我晚上统一处理一下吧,中午加的旅行者 json 我也得改一下

mark9804 commented 2 years ago

开了个倒车,试了 Levenshtein 距离(编辑距离)

const levenshtein = require('fast-levenshtein');

let s0 = "玩具销售员达达利亚"
let s1 = "达达里亚";
let s2 = "公子达达利亚";
let s3 = "亚利达达"

let sn = "达达利亚";

console.log(levenshtein.get(s0, sn) + ",不相似率" + levenshtein.get(s0, sn)/s0.length);
console.log(levenshtein.get(s1, sn) + ",不相似率" + levenshtein.get(s1, sn)/s1.length);
console.log(levenshtein.get(s2, sn) + ",不相似率" + levenshtein.get(s2, sn)/s2.length);
console.log(levenshtein.get(s3, sn) + ",不相似率" + levenshtein.get(s3, sn)/s3.length);
5,不相似率0.5555555555555556
1,不相似率0.25
2,不相似率0.3333333333333333
4,不相似率1
mark9804 commented 2 years ago

我最初的想法应该是基于编辑距离的,触发点是我们预先设置好的某一个不相似率的阈值

比如假如我们设置不相似率阈值为0.4,那么就会把"达达里亚"和"公子达达利亚"重定向到“达达利亚”,感觉这个demo基本符合我的想法了

Arondight commented 2 years ago

这个看上去不错,虽然计算量会变很多

Arondight commented 2 years ago

@Mark9804 麻烦多试试,你这里如果得出结论可以的话我就直接换了,我还在改这堆防误触的东西

mark9804 commented 2 years ago

我这个测试用的脚本暂时整合不进tools.js里,会报require is not defined in ES module scope

刚刚我是直接把根目录的package.json里面的"type":"module"干掉来单独测试的,待会我再看看

Arondight commented 2 years ago

我来吧,我改完防误触了

Arondight commented 2 years ago

好在可以在算编辑距离之前首先算一下可能的别名,如果有的话再算编辑距离,我看了一下,这个算法跟汉明距离一比的确是小号太大了,而且 simhash 还是加载配置文件之后就把所有别名算好的

mark9804 commented 2 years ago

https://www.npmjs.com/package/fastest-levenshtein

这个包不知道表现会不会好一些

Arondight commented 2 years ago

这个包不知道表现会不会好一些

反正我也一时半会看不懂这算法,哪个名字牛逼就用哪个

Arondight commented 2 years ago
import levenshtein from "fastest-levenshtein";
import { readConfig } from "./src/utils/config.js";

readConfig();

const names = Object.keys(alias.allNames);
const n1 = process.argv[2];

for (const n2 of names) {
  const d = levenshtein.distance(n1, n2) / n1.length;

  if (d < 1) {
    console.log(`${n1} - ${n2} - ${d}`);
  }
}

我反复试了一下,这个值最好设置成 0.5

node t.js 钟来
ndde t.js 达达
mark9804 commented 2 years ago

两个字猜错一个,感觉误触率会不会太高……

:我的里 :有好康的

屋里 → 神里

Arondight commented 2 years ago

我的、信息、定轨,这三个字开头的,我觉得还好。我很想使用这个算法作为候选列表的算法,我写的简单的分词查找候选词过多了,但是这个算法消耗的资源又让我不敢这么做

mark9804 commented 2 years ago

刚刚看了一下网上对于距离算法的优化基本上是提前返回结果终止循环,但我们字符串不在长而在多,意义不大

有一种矩阵版本的距离算法(中文英文),但很可惜我数学不好,看天书了属于是

citydirector commented 2 years ago

(日常忘记打码) IMG_20211123_140916