Press "Enter" to skip to content

住手!!!这不是我认识的人工智障

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

前言

 

说到人工智障,相信大家应该都记得之前的那个仅有几个 replace 的智能回复吧。

 

 

嗯,非常的智能。

 

 

我们都知道中文博大精深,一句话可以有非常多种的解释。

 

举个例子:

 

他赞成我不赞成?

 

他赞成,我不赞成。

 

他赞成我不?赞成。

 

他赞成我?不赞成。

 

是吧,所以如果我们要实现一个非常聪明的智能是很难的。

 

不过若干智能助手也是有名的蠢了。

 

所以,我实现一个蠢但没完全蠢的智能回复应该问题不大。

 

我们来看一下demo演示:

 

 

演示中有个小bug,说今天天气的时候回复中有多个天气,下面代码中已经修复。

 

github项目地址: github.com/lionet1224/…

 

思路

 

一开始我是想通过词义来解析一句话。

 

区分词的类型,如:名词、动词、形容词…等等,然后通过权重将这些词关联起来,最后总结出一个最匹配的回答。

 

不过实现起来感觉很复杂就放弃了。

 

 

后面就想着,我可以简化这个过程啊,不去区分词的类型,直接就是在所有定义好的句子中取到最匹配的那条。

 

句子是定义好的回答模板

 

例如:

 

['我', '喜欢', '点赞']
保存所有句子的数组

 

那幺基于这个思路我们就可以开发了。

 

准备工作

 

我们需要用到 nodejieba 这个node库,所以就需要启动一个node服务。

 

NodeJieba是”结巴”中文分词的 Node.js 版本实现, 由CppJieba提供底层分词算法实现, 是兼具高性能和易用性两者的 Node.js 中文分词组件。 – github.com/yanyiwu/nod…

 

先安装一下 koa 及其相关的库吧。

 

npm install koa2 koa-router koa-static nodejieba nodemon --save

 

然后在根目录中创建一个文件 server.js 来编写对应的服务代码。

 

实现的功能不复杂,就是将编写一个api可以将前端输入的句子分解成数组然后返回给前端,并且创建一个静态文件服务器来展示页面。

 

const nodejieba = require('nodejieba')
const Koa = require('koa2')
const Router = require('koa-router')
const static = require('koa-static')
const app = new Koa();
const router = new Router();
// 一个get请求
router.get('/word', ctx => {
  // cut就是nodejieba来分解句子的方法
  // ctx.query.word 获取链接中"?word=x"的x
  ctx.body = nodejieba.cut(ctx.query.word);
})
// 创建静态文件服务器
app.use(static('.'))
// 应用路由
app.use(router.routes())
// 监听端口启动服务
app.listen(3005, () => {
  console.log('start server *:3005')
})

 

那幺我们在 package.json 中写入对应的启动命令。

 

{
  // ...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon server.js"
  },
  // ..
}

 

启动成功后,我们可以在终端中看到启动成功的显示。

 

 

编写前端代码

 

首先我们需要一个界面来展示及操作我们的对话。

 

这里不管你是仿QQ也好还是仿微信、钉钉什幺的,都行,实现了就好。

 

我就不贴代码了,有兴趣的可以去github中看看。

 

 

实现对应的发送信息的逻辑。

 

// 人工智障一代
function AImsg1(str){
  return str.replace(/[??]/g, '!')
            .replace(/[吗嘛]/g, '')
            .replace(/[你我]/g, (val) => {
              if(val === '我') return '你'
              else return '我'
            });
}
// 判断一下使用哪一代智能
// 默认使用二代
function getAImsg(str, type = 2){
  return new Promise(resolve => {
    if(type === 1){
      resolve(AImsg1(str))
    } else {
      AImsg2(str).then(res => {
        resolve(res)
      })
    }
  })
}
// 输入框
let inputMsg = document.querySelector('#input-msg')
// 发送按钮
let emitBtn = document.querySelector('#emit')
// 显示信息的容器
let wrapper = document.querySelector('.content');
// 用户发送信息,并且让AI也发送
function emitMsg(){
  let str = inputMsg.value;
  insertMsg(str, true);
  // 发送后清空输入框
  inputMsg.value = ''
  // 延迟一秒回复
  setTimeout(() => {
    getAImsg(str).then(res => {
      insertMsg(res);
    });
  }, 1000);
}
// 插入到页面中,flag来判断是用户发送的还是电脑发送的
function insertMsg(str, flag){
  let msg = document.createElement('div');
  msg.className = 'msg ' + (flag ? 'right' : '')
  msg.innerHTML = `<div>${str}</div>`;
  wrapper.appendChild(msg);
  wrapper.scrollTop = 100000;
}
emitBtn.onclick = () => {
  emitMsg();
};
// 回车键也可以发送
inputMsg.addEventListener('keyup', ev => {
  if(ev.keyCode === 13) emitMsg();
})

 

通过上面的代码,我们就实现了发送信息的功能。

 

定义句子

 

可以将句子理解为一个模板,如果用户发送的话匹配了一条句子,那幺人工智能就使用这条句子回复。

 

接下来,我们来实现 句子 的定义。

 

// 定义句子的类
// 句子
class Sentence{
  constructor(keys, answer){
    // 关键词
    this.keys = keys || [];
    // 回答的方法
    this.answer = answer;
    // 存储可变数据
    this.typeVariable = {};
  }
}

 

可以看出,我在其中定义了 keys / answer / typeVeriable 三个变量。

keys 就是用来匹配的关键词
answer 这是一个方法,当匹配了这条句子之后就调用这个方法返回对应的话
typeVariable 这个是为了让回答不那幺死板,可以将一些可变的词提取出来然后在 answer 进行判断,最后返回合适的回答。

简单的实现

 

我们先不考虑可变通的回答,先实现一个最简单的问答。

 

我说: 天气是蓝色的

 

智能回复: 嗯嗯,天空是蓝色滴

 

// 我们可以先实现一下AImsg2这个方法,以方便调用
function  AImsg2(str){
  return new Promise(resolve => {
    // 获取前面开发的那个api的数据,将用户输入的文字当做参数。
    axios.get(`http://localhost:3005/word?word=${str}`).then(res => {
      console.log(res.data)
      // 去匹配适合的句子
      let sentences = matchSentence(res.data);
      // 如果没有匹配的回复
      if(sentences.length <= 0){
        resolve('emm,你在说什幺呀,没看懂呢')
      } else {
      // 如果有匹配的就去获取回答
        resolve(sentences[0].sentence.get())
      }
    })
  })
}

 

来实现一下 matchSentence 这个方法。

 

// 匹配最适合的句子
// 低于30%的匹配当做不匹配
function matchSentence(arr){
  let result = [];
  sentences.map(item => {
    // 用句子类自身的match方法判断是否匹配,返回匹配成功的关键词数量
    let matchNum = item.match(arr);
    // 如果匹配数量低于总关键词数量的1/3就当做没看到
    if(matchNum >= item.keys.length / 3) result.push({
      sentence: item,
      // 这里是匹配的关键词与总关键词数量的比例,为了方便排序最合适的那条
      searchNum: matchNum / item.keys.length
    })
  })
  result = result.sort((a, b) => b.searchNum - a.searchNum);
  return result;
}

 

Sentence 中实现 matchget 方法。

 

class Sentence{
  // ...
  // 获取用户发送的语句与定义的句子的匹配程度
  match(arr){
    // 每次匹配都重置数据
    this.typeVariable = {};
    // 将其解构放入一个新数组(浅拷贝功能)
    // 为了数据不会影响去匹配下一个句子
    let userArr = [...arr];
    let matchNum = this.keys.reduce((val, item) => {
      // 关键词是否匹配
      let flag = userArr.find((v, i) => {
        return v === val;
      })
      return val += flag ? 1 : 0;
    }, 0)
    return matchNum;
  }
  // 调用回答方法并且将变量传入
  get(){
      return this.answer(this.typeVariable)
  }
}

 

最后添加句子的实例存入数组。

 

let sentences = [
  new Sentence(['天', '天空', '是', '蓝色', '颜色'], type => {
    let str = '嗯嗯,天空是蓝色滴';
    return str;
  }),
]

 

不出意外的话,我们输入 天空是蓝色的 就将会得到回复: 嗯嗯,天空是蓝色滴

 

 

为什幺关键词是 ['天', '天空', '是', '蓝色', '颜色'] 这样的?

 

因为我们可能的问法有 天是什幺颜色 / 天空的颜色是蓝色的 ,所以我们可以将更多的关键词加入,以方便匹配。

 

变通的回答

 

如果仅仅是上面的写法,我们就只能非常死板的回答,所以,我们可以给一些关键词定义为可变的数据,最后取到这些数据来灵活的回答。

 

例如:爸爸的爸爸叫什幺?

 

我们先来定义一个类,来保存一个种类的关键词。

 

// 种类,为一个系列的文字,如颜色的赤橙黄绿青蓝紫、时间的今天明天后天
class Type{
  // key是关键词
  // arr是这个种类下的词
  // exclude 排除关键词
  constructor(key, arr, exclude){
    this.key = key;
    this.arr = arr;
    this.exclude = exclude || [];
  }
  // 判断是否匹配
  match(str){
    return this.arr.find(item => {
      return str.indexOf(item) > -1;
    }) && this.exclude.indexOf(str) <= -1
  }
}

 

然后创建对应语句的实例:

 

let sentences = [
  // 使用%x%的语法来表示可变数据
  new Sentence(['%family%', '的', '%family%', '叫', '是', '什幺'], type => {
    let data = {
      '爸爸': {
        '爸爸': '爷爷',
        '妈妈': '奶奶'
      },
      '妈妈': {
        '爸爸': '姥爷',
        '妈妈': '姥姥'
      },
    }
    // 判断是否拥有这个叫法
    let result = data[type.family[0]] && data[type.family[0]][type.family[1]]
    // 最后返回
    if(result){
      return `${type.family[0]}的${type.family[1]}叫${result}喔`
    } else {
      return '咳咳,我不知道诶'
    }
  }),
]
let types = {
  // 创建family这个种类
  family: new Type('family', ['爸爸', '妈妈', '哥哥', '姐姐', '妹妹', '弟弟', '外公', '外婆', '婆婆', '爷爷'])
}

 

当然定义好了之后还需要在 Sentence 中编写获取可变数据的方法。

 

class Sentence{
  // ...
  // 获取用户发送的语句与定义的句子的匹配程度
  match(arr){
    this.typeVariable = {};
    let userArr = [...arr];
    let matchNum = this.keys.reduce((val, item) => {
      let flag = userArr.find((v, i) => {
        // 使用正则匹配%x%的写法,并且获取x的数据
        let isType = /^%(.*)%$/.exec(item);
        
        if(isType){
          // 判断关键词是否在这个种类中
          let matchType = types[isType[1]].match(v)
          if(matchType){
            // 存入typeVariable中
            if(!this.typeVariable[isType[1]]) this.typeVariable[isType[1]] = [];
            this.typeVariable[isType[1]].push(v);
            // 匹配过后,这个存入的数据应该删除,不然后面匹配的时候会将第一个数据重复输入
            userArr.splice(i, 1)
          }
          return matchType;
        } else {
          return item === v;
        }
      })
      return val += flag ? 1 : 0;
    }, 0)

    return matchNum;
  }
  // ...
}

 

到这里变通回答的功能就实现了,我们来看看效果。

 

 

更多的功能

 

可以增加的功能还是挺多的,如:

 

Type
answer

 

还有更多的想法就不一一列举了。

 

最后

 

感谢大家的阅读,此文仅为抛砖引玉,代码质量不佳请勿见怪。

 

(诚挚的眼神)

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注